微信小程序实现图片拖动、放大、缩小、旋转、滤镜和切图功能

目录

 

前言

实现过程


前言

本文介绍使用微信小程序API+canvas来实现图片的可拖动、放大、缩小和旋转,并可对选中的图片进行不同效果的滤镜和不同形状的切图,且可对最终效果进行保存到本地。

最终效果:

实现过程

1. 文件index.wxml和index.wxss代码如下,这一块比较简单,可自行查看,不做过多分析:

<view class='contentWarp'>
  <block wx:for="{
   
   {itemList}}" wx:key="id">
    <view class='touchWrap' style='transform: scale({
   
   {item.scale}});top:{
   
   {item.top}}px;left:{
   
   {item.left}}px; z-index:{
   
   {item.active?100:1}}'>
      <view class='imgWrap {
   
   {item.active? "touchActive":""}}' style="transform: rotate({
   
   {item.angle}}deg); border: {
   
   {item.active?4*item.oScale:0}}rpx #fff dashed;">
        <image src='{
   
   {item.image}}' data-id='{
   
   {item.id}}' style='width:{
   
   {item.width}}px;height:{
   
   {item.height}}px;' bindtouchstart='WraptouchStart' bindtouchmove='WraptouchMove' bindtouchend='WraptouchEnd'  mode="widthFix"></image>
        <image class='x' src='../../images/del.png' style='transform: scale({
   
   {item.oScale}});transform-origin:center;' data-id='{
   
   {item.id}}' bindtap='deleteItem'></image>
        <image class='o' src='../../images/scale.png' style='transform: scale({
   
   {item.oScale}});transform-origin:center;' data-id='{
   
   {item.id}}' bindtouchstart='oTouchStart' bindtouchmove='oTouchMove' bindtouchend='WraptouchEnd'></image>
      </view>
    </view>
  </block>
</view>
<!-- 右下角操作按钮 -->
<view class="operation-buttons">
  <image src="../../images/upload.png" bindtap="uploadImg"></image>
  <image src="../../images/fliters.png" bindtap="toggleFliters"></image>
  <image src="../../images/shapes.png" bindtap="toggleShapes"></image>
  <image src="../../images/synthesis.png" bindtap="openMask"></image>
</view>
<!-- 各种过滤效果 -->
<view class="fliters" hidden="{
   
   {!showFliters}}">
  <block wx:for="{
   
   {fliters}}" wx:key="id">
    <image data-fliter="{
   
   {item.fliter}}" src="{
   
   {item.src}}" bindtap="imgEffects"></image>
  </block>
</view>
<!-- 各种形状 -->
<view class="shapes" hidden="{
   
   {!showShapes}}">
  <block wx:for="{
   
   {shapes}}" wx:key="id">
    <image data-shape="{
   
   {item.shape}}" src="{
   
   {item.src}}" bindtap="imgEffects"></image>
  </block>
</view>
<!-- 保存显示效果图 -->
<view class='canvasWrap' hidden="{
   
   {!showCanvas}}">
  <image class="resImg" bindlongtap="saveImg" src="{
   
   {canvasTemImg}}" mode="widthFix"></image>
  <view class='btn_view'>
    <button bindtap='saveImg'>保存到手机</button>
    <button bindtap='disappearCanvas'>关闭</button>
  </view>
</view>
<!-- 画布 -->
<canvas class='maskCanvas' canvas-id="maskCanvas" style='width:{
   
   {canvasWidth}}px; height:{
   
   {canvasHeight}}px;'></canvas>
/**index.wxss**/
.contentWarp {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: auto;
}

.touchWrap {
  transform-origin: center;
  position: absolute;
  z-index: 100;
}

.imgWrap {
  box-sizing: border-box;
  width: 100%;
  transform-origin: center;
  float: left;
  border: 5rpx transparent dashed;
}

.imgWrap image {
  float: left;
}

.touchActive .x {
  display: block;
}

.touchActive .o {
  display: block;
}

.x {
  position: absolute;
  top: -25rpx;
  left: -25rpx;
  z-index: 500;
  display: none;
  width: 50rpx;
  height: 50rpx;
  overflow: hidden;
  font-weight: bold;
  color: #d1e3f1;
}

.o {
  position: absolute;
  bottom: -25rpx;
  right: -25rpx;
  width: 50rpx;
  height: 50rpx;
  text-align: center;
  display: none;
  overflow: hidden;
  font-weight: bold;
  color: #d1e3f1;
}

.active {
  background-color: rgb(78, 114, 151);
}

.active view {
  border: none;
}

.touchActive {
  z-index: 400;
}

.operation-buttons {
  position: absolute;
  bottom: 100rpx;
  right: 20rpx;
  display: flex;
  flex-direction: column;
  z-index: 101;
}

.operation-buttons image {
  width: 100rpx;
  height: 100rpx;
  margin-top: 40rpx;
}

.canvasWrap {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.6);
  z-index: 999;
  text-align: center;
}

.maskCanvas {
  position: absolute;
  left: -200%;
  top: 0;
}

.btnView view {
  padding-bottom: 20rpx;
}

.hand {
  position: absolute;
  left: 100rpx;
  right: 0;
  margin: auto;
  z-index: 100;
}

.getUserInfoBtn {
  position: initial;
  border: none;
  background-color: none;
}

.getUserInfoBtn::after {
  border: none;
}

.btn_view {
  display: flex;
  padding: 20rpx;
}

.btn_view button {
  width: 210rpx;
  font-size: 28rpx;
  background-color: #eb4985;
  color: #fff;
  line-height: 80rpx;
}

.resImg {
  width: 75%;
  margin-top: 10px;
}

/* 特效样式 */
.fliters {
  display: flex;
  flex-direction: column;
  position: absolute;
  bottom: 382rpx;
  right: 120rpx;
  z-index: 201;
}

.shapes {
  display: flex;
  flex-direction: column;
  position: absolute;
  bottom: 242rpx;
  right: 120rpx;
  z-index: 201;
}

.fliters image, .shapes image {
  width: 60rpx;
  height: 60rpx;
  border: 2rpx solid #eb4985;
}

2. 文件index.js存放所有功能的逻辑代码,相对比较复杂,下面分开来分析几个重点方法:

1)方法uploadImg+setDropItem:获取上传图片的信息,跟设置的最大宽高进行判断(maxWidth, maxHeight),然后根据判断的结果进行缩放,避免大图溢出,且设置图片的地址、坐标、定位和是否选中等信息;用于后面功能使用,支持多图使用;

2)方法WraptouchStart+WraptouchMove:获取图片移动坐标和触发时坐标的差值,加上图片本来的坐标来实现移动效果,注意要把移动坐标赋值给触发时坐标(items[index].lx = e.touches[0].clientX),不然会导致移动有问题;

3)方法oTouchStart+oTouchMove:获取拖动后图片的半径跟触发时图片的半径的比值,再使用scale来实现缩放功能(items[index].disPtoO / items[index].r);获取触发时的图片角度+拖动后图片的角度,再使用rotate来实现旋转功能(items[index].angle = items[index].rotate);

4)方法imgEffects:调用滤镜方法util.imgFliters(详细可到https://jingyan.baidu.com/article/ed15cb1b9fd9bb1be3698183.html查看),根据设置的滤镜值,进行不同的滤镜处理;而调用形状方法util.imgShapes,根据设置的形状值,进行不同的切图效果;

5)方法synthesis:用来把所有图片的最终效果合成一个画布,用于保存图片到本地使用;

6)方法saveImg:把画布保存到本地相册。

let index = 0, // 当前点击图片的index
  items = [], // 图片数组信息
  itemId = 1, // 图片id,用于识别点击图片
  fliter = 'init', // 默认过滤类型(原图)
  shape = 'init'; // 默认形状(原图)
const hCw = 1.62; // 图片宽高比
const canvasPre = 1; // 展示的canvas占mask的百分比
const maskCanvas = wx.createCanvasContext('maskCanvas', this); // 创建 canvas 的绘图上下文 CanvasContext 对象

const util = require('../../utils/util.js');
Page({
  /**
   * 页面的初始数据
   */
  data: {
    itemList: [],
    showFliters: false, // 默认不显示过滤效果框
    showShapes: false, // 默认不显示形状效果框
    fliters: [{
      fliter: 'init',
      src: '../../images/init.jpg'
    }, {
      fliter: 'bw',
      src: '../../images/bw.jpg'
    }, {
      fliter: 'groundGlass',
      src: '../../images/groundGlass.jpg'
    }, {
      fliter: 'pictureStory',
      src: '../../images/pictureStory.jpg'
    }, {
      fliter: 'reminiscence',
      src: '../../images/reminiscence.jpg'
    }, {
      fliter: 'sketch',
      src: '../../images/sketch.jpg'
    }],
    shapes: [{
      shape: 'circle',
      src: '../../images/init.jpg'
    }, {
      shape: 'star',
      src: '../../images/init.jpg'
    }, {
      shape: 'irregularityHeart',
      src: '../../images/init.jpg'
    }, {
      shape: 'SudokuHeart',
      src: '../../images/init.jpg'
    }]
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function(options) {
    items = this.data.itemList;
    wx.getSystemInfo({ // 获取系统信息
      success: sysData => {
        this.sysData = sysData
        // 设置画布宽高,this.sysData.windowWidth为屏幕的宽度
        this.setData({
          canvasWidth: this.sysData.windowWidth * canvasPre, // 如果觉得不清晰的话,可以把所有组件、宽高放大一倍
          canvasHeight: this.sysData.windowWidth * canvasPre * hCw,
        })
      }
    })
  },
  // 上传图片
  uploadImg() {
    let that = this;
    wx.chooseImage({
      count: 1,
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success (res) {
        // tempFilePath可以作为img标签的src属性显示图片
        that.setDropItem({
          url: res.tempFilePaths[0]
        });
      }
    })
  },
  // 设置图片的信息
  setDropItem(imgData) {
    let data = {}; // 存储图片信息
    // 获取图片信息,网络图片需先配置download域名才能生效
    wx.getImageInfo({
      src: imgData.url,
      success: res => {
        // 初始化数据
        let maxWidth = 150, maxHeight = 150; // 设置最大宽高
        if (res.width > maxWidth || res.height > maxHeight) { // 原图宽或高大于最大值就执行
            if (res.width / res.height > maxWidth / maxHeight) { // 判断比例使用最大值的宽或高作为基数计算
                data.width = maxWidth;
                data.height = Math.round(maxWidth * (res.height / res.width));
            } else {
                data.height = maxHeight;
                data.width = Math.round(maxHeight * (res.width / res.height));
            }   
        }
        data.image = imgData.url; // 显示地址
        data.initImage = imgData.url; // 原始地址
        data.id = ++itemId; // id
        data.top = 0; // top定位
        data.left = 0; // left定位
        // 圆心坐标
        data.x = data.left + data.width / 2;
        data.y = data.top + data.height / 2;
        data.scale = 1; // scale缩放
        data.rotate = 1; // 旋转角度
        data.active = false; // 选中状态
        items[items.length] = data; // 每增加一张图片数据增加一条信息
        this.setData({
          itemList: items
        })
      }
    })
  },
  // 点击图片
  WraptouchStart: function(e) {
    // 循环图片数组获取点击的图片信息
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }
    this.setData({
      itemList: items
    })

    // 获取点击的坐标值
    items[index].lx = e.touches[0].clientX;
    items[index].ly = e.touches[0].clientY;
  },
  // 拖动图片
  WraptouchMove(e) {
    items[index]._lx = e.touches[0].clientX;
    items[index]._ly = e.touches[0].clientY;

    items[index].left += items[index]._lx - items[index].lx;
    items[index].top += items[index]._ly - items[index].ly;
    items[index].x += items[index]._lx - items[index].lx;
    items[index].y += items[index]._ly - items[index].ly;

    items[index].lx = e.touches[0].clientX;
    items[index].ly = e.touches[0].clientY;

    this.setData({
      itemList: items
    })
  },
  // 放开图片
  WraptouchEnd() {
    this.synthesis(); // 调用合成图方法
  },
  // 点击伸缩图标
  oTouchStart(e) {
    //找到点击的那个图片对象,并记录
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }
    //获取作为移动前角度的坐标
    items[index].tx = e.touches[0].clientX;
    items[index].ty = e.touches[0].clientY;
    //移动前的角度
    items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty);
    //获取图片半径
    items[index].r = this.getDistancs(items[index].x, items[index].y, items[index].left, items[index].top);
  },
  oTouchMove: function(e) {
    //记录移动后的位置
    items[index]._tx = e.touches[0].clientX;
    items[index]._ty = e.touches[0].clientY;
    //移动的点到圆心的距离
    items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx, items[index]._ty - 10)

    items[index].scale = items[index].disPtoO / items[index].r;

    //移动后位置的角度
    items[index].angleNext = this.countDeg(items[index].x, items[index].y, items[index]._tx, items[index]._ty)
    //角度差
    items[index].new_rotate = items[index].angleNext - items[index].anglePre;

    //叠加的角度差
    items[index].rotate += items[index].new_rotate;
    items[index].angle = items[index].rotate; //赋值

    //用过移动后的坐标赋值为移动前坐标
    items[index].tx = e.touches[0].clientX;
    items[index].ty = e.touches[0].clientY;
    items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty)

    //赋值setData渲染
    this.setData({
      itemList: items
    })
  },
  // 计算坐标点到圆心的距离
  getDistancs(cx, cy, pointer_x, pointer_y) {
    var ox = pointer_x - cx;
    var oy = pointer_y - cy;
    return Math.sqrt(
      ox * ox + oy * oy
    );
  },
  /*
   *参数cx和cy为图片圆心坐标
   *参数pointer_x和pointer_y为手点击的坐标
   *返回值为手点击的坐标到圆心的角度
   */
  countDeg: function(cx, cy, pointer_x, pointer_y) {
    var ox = pointer_x - cx;
    var oy = pointer_y - cy;
    var to = Math.abs(ox / oy);
    var angle = Math.atan(to) / (2 * Math.PI) * 360;
    if (ox < 0 && oy < 0) //相对在左上角,第四象限,js中坐标系是从左上角开始的,这里的象限是正常坐标系  
    {
      angle = -angle;
    } else if (ox <= 0 && oy >= 0) //左下角,3象限  
    {
      angle = -(180 - angle)
    } else if (ox > 0 && oy < 0) //右上角,1象限  
    {
      angle = angle;
    } else if (ox > 0 && oy > 0) //右下角,2象限  
    {
      angle = 180 - angle;
    }
    return angle;
  },
  deleteItem: function(e) {
    let newList = [];
    for (let i = 0; i < items.length; i++) {
      if (e.currentTarget.dataset.id != items[i].id) {
        newList.push(items[i])
      }
    }
    if (newList.length > 0) {
      newList[newList.length - 1].active = true; // 剩下图片组最后一个选中
    }
    items = newList;
    this.setData({
      itemList: items
    })
  },
  // 打开遮罩层
  openMask() {
    this.synthesis();
    this.setData({
      showCanvas: true
    })
  },
  synthesis() { // 合成图片
    maskCanvas.save();
    maskCanvas.beginPath();

    // 画背景色(白色)
    maskCanvas.setFillStyle('#fff');
    maskCanvas.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);

    items.forEach((currentValue, index) => {
      maskCanvas.save();
      maskCanvas.translate(0, 0);
      maskCanvas.beginPath();
      maskCanvas.translate(currentValue.x, currentValue.y); // 圆心坐标
      maskCanvas.rotate(currentValue.angle * Math.PI / 180);
      maskCanvas.translate(-(currentValue.width * currentValue.scale / 2), -(currentValue.height * currentValue.scale / 2))
      maskCanvas.drawImage(currentValue.image, 0, 0, currentValue.width * currentValue.scale, currentValue.height * currentValue.scale);
      maskCanvas.restore();
    })
    // reserve 参数为 false,则在本次调用绘制之前 native 层会先清空画布再继续绘制
    maskCanvas.draw(false, (e) => {
      wx.canvasToTempFilePath({
        canvasId: 'maskCanvas',
        success: res => {
          this.setData({
            canvasTemImg: res.tempFilePath
          })
        }
      }, this);
    })
  },
  // 点击切换显示过滤框
  toggleFliters() {
    this.setData({
      showFliters: !this.data.showFliters,
      showShapes: false
    });
  },
  // 点击切换显示形状框
  toggleShapes() {
    this.setData({
      showShapes: !this.data.showShapes,
      showFliters: false
    });
  },
  // 图片特效
  imgEffects(e) {
    fliter = e.currentTarget.dataset.fliter || 'init';
    shape = e.currentTarget.dataset.shape || 'init';
    let that = this;
    items.forEach((currentValue, index) => {
      if (currentValue.active) {
        maskCanvas.save();
        maskCanvas.beginPath();
        util.imgShapes(maskCanvas, 0, 0, currentValue.width, currentValue.width, shape, 0, currentValue); // 图片剪切不同形状
        maskCanvas.clearRect(0, 0, currentValue.width, currentValue.height);
        maskCanvas.drawImage(currentValue.initImage, 0, 0, currentValue.width, currentValue.height);

        maskCanvas.draw(false, function() {
          wx.canvasGetImageData({ // 获取canvas区域的像素数据
            canvasId: 'maskCanvas',
            x: 0,
            y: 0,
            width: currentValue.width,
            height: currentValue.height,
            success(res) {
              let imageData = res.data;
              util.imgFliters(maskCanvas, fliter, res); // 调用图片滤镜函数

              maskCanvas.clearRect(0, 0, currentValue.width, currentValue.height); // 清除旧的,不然会导致卡顿

              maskCanvas.restore();
              wx.canvasPutImageData({ // 将像素数据绘制到canvas
                canvasId: 'maskCanvas',
                x: 0,
                y: 0,
                width: currentValue.width,
                height: currentValue.height,
                data: imageData,
                success(res) {
                  wx.canvasToTempFilePath({
                    canvasId: 'maskCanvas',
                    width: currentValue.width,
                    height: currentValue.height,
                    destWidth: currentValue.width,
                    destHeight: currentValue.height,
                    success: res => {
                      items[index].image = res.tempFilePath
                      that.setData({
                        itemList: items
                      })
                    }
                  }, this)
                }
              })
            }
          });
        })
      };
    })
  },
  // 关闭遮罩层
  disappearCanvas() {
    this.setData({
      showCanvas: false
    })
  },
  // 保存图片到系统相册
  saveImg: function() {
    wx.saveImageToPhotosAlbum({
      filePath: this.data.canvasTemImg,
      success: res => {
        wx.showToast({
          title: '保存成功',
          icon: "success"
        })
      },
      fail: res => {
        wx.openSetting({
          success: settingdata => {
            if (settingdata.authSetting['scope.writePhotosAlbum']) {
              console.log('获取权限成功,给出再次点击图片保存到相册的提示。')
            } else {
              console.log('获取权限失败,给出不给权限就无法正常使用的提示')
            }
          },
          fail: error => {
            console.log(error)
          }
        })
        wx.showModal({
          title: '提示',
          content: '保存失败,请确保相册权限已打开',
        })
      }
    })
  }
})

查看公众号信息:

 

猜你喜欢

转载自blog.csdn.net/king0964/article/details/105193597