微信小程序—自定义图片的拖拽、缩放、旋转

直接上效果

在这里插入图片描述

体验效果扫码

体验路径:自定义系列>拖拽、缩放和旋转
在这里插入图片描述

接着上代码

步骤

第一步拷贝以下组件cropper
cropper.js

// component/cropper/cropper.js
const device = wx.getSystemInfoSync();
var twoPoint = {
  x1: 0,
  y1: 0,
  x2: 0,
  y2: 0
}

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    ratio: {
      type: Number,
      observer: function (newVal, oldVal) {
        this.setData({
          width: device.windowWidth * 0.7,
          height: device.windowWidth * 0.7 / newVal
        })
      }
    },
    url: {
      type: String,
      observer ( newVal, oldVal ) {
        this.initImg( newVal )
      }
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    width: device.windowWidth * 0.8,                //剪裁框的宽度
    height: device.windowWidth * 0.8 / (102 / 152), //剪裁框的长度
    originImg: null,                                //存放原图信息
    stv: {
      offsetX: 0,                                   //剪裁图片左上角坐标x
      offsetY: 0,                                   //剪裁图片左上角坐标y
      zoom: false,                                  //是否缩放状态
      distance: 0,                                  //两指距离
      scale: 1,                                     //缩放倍数
      rotate: 0                                     //旋转角度
    },
  },

  /**
   * 组件的方法列表
   */
  methods: {
    uploadTap() {
      let _this = this
      wx.chooseImage({
        count: 1, // 默认9
        sizeType: ['original'], // 可以指定是原图还是压缩图,默认二者都有
        sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
        success(res) {
          _this.initImg( res.tempFilePaths[0]);
        }
      })
    },
    rotate() {
      let _this = this;
      _this.setData({
        'stv.rotate': _this.data.stv.rotate % 90 == 0 ? _this.data.stv.rotate = _this.data.stv.rotate + 90 : _this.data.stv.rotate = 0
      })
    },

    initImg(url) {
      let _this = this;
      wx.getImageInfo({
        src: url,
        success(resopne) {
          console.log(resopne);
          let innerAspectRadio = resopne.width / resopne.height;

          if (innerAspectRadio < _this.data.width / _this.data.height) {
            _this.setData({
              originImg: {
                url: url,
                width: _this.data.width,
                height: _this.data.width / innerAspectRadio
              },
              stv: {
                offsetX: 0,
                offsetY: 0 - Math.abs((_this.data.height - _this.data.width / innerAspectRadio) / 2),
                zoom: false, //是否缩放状态
                distance: 0,  //两指距离
                scale: 1,  //缩放倍数
                rotate: 0
              },
            })
          } else {
            _this.setData({
              originImg: {
                url: url,
                height: _this.data.height,
                width: _this.data.height * innerAspectRadio
              },
              stv: {
                offsetX: 0 - Math.abs((_this.data.width - _this.data.height * innerAspectRadio) / 2),
                offsetY: 0,
                zoom: false, //是否缩放状态
                distance: 0,  //两指距离
                scale: 1,  //缩放倍数
                rotate: 0
              }
            })
          }
        }
      })
    },
    //事件处理函数
    touchstartCallback: function (e) {
      if (e.touches.length === 1) {
        let { clientX, clientY } = e.touches[0];
        this.startX = clientX;
        this.startY = clientY;
        this.touchStartEvent = e.touches;
      } else {
        let xMove = e.touches[1].clientX - e.touches[0].clientX;
        let yMove = e.touches[1].clientY - e.touches[0].clientY;
        let distance = Math.sqrt(xMove * xMove + yMove * yMove);
        twoPoint.x1 = e.touches[0].pageX * 2
        twoPoint.y1 = e.touches[0].pageY * 2
        twoPoint.x2 = e.touches[1].pageX * 2
        twoPoint.y2 = e.touches[1].pageY * 2
        this.setData({
          'stv.distance': distance,
          'stv.zoom': true, //缩放状态
        })
      }
    },
    //图片手势动态缩放
    touchmoveCallback: function (e) {
      let _this = this
      fn(_this, e)
    },
    touchendCallback: function (e) {
      //触摸结束
      if (e.touches.length === 0) {
        this.setData({
          'stv.zoom': false, //重置缩放状态
        })
      }
    }
  }
})

/**
* fn:延时调用函数
* delay:延迟多长时间
* mustRun:至少多长时间触发一次
*/
var throttle = function (fn, delay, mustRun) {
  var timer = null,
    previous = null;

  return function () {
    var now = +new Date(),
      context = this,
      args = arguments;
    if (!previous) previous = now;
    var remaining = now - previous;
    if (mustRun && remaining >= mustRun) {
      fn.apply(context, args);
      previous = now;
    } else {
      clearTimeout(timer);
      timer = setTimeout(function () {
        fn.apply(context, args);
      }, delay);

    }
  }
}

var touchMove = function (_this, e) {
  //触摸移动中
  if (e.touches.length === 1) {
    //单指移动
    if (_this.data.stv.zoom) {
      //缩放状态,不处理单指
      return;
    }
    let { clientX, clientY } = e.touches[0];
    let offsetX = clientX - _this.startX;
    let offsetY = clientY - _this.startY;
    _this.startX = clientX;
    _this.startY = clientY;
    let { stv } = _this.data;
    stv.offsetX += offsetX;
    stv.offsetY += offsetY;
    stv.offsetLeftX = -stv.offsetX;
    stv.offsetLeftY = -stv.offsetLeftY;
    _this.setData({
      stv: stv
    });

  } else if (e.touches.length === 2) {
    //计算旋转
    let preTwoPoint = JSON.parse(JSON.stringify(twoPoint))
    twoPoint.x1 = e.touches[0].pageX * 2
    twoPoint.y1 = e.touches[0].pageY * 2
    twoPoint.x2 = e.touches[1].pageX * 2

    function vector(x1, y1, x2, y2) {
      this.x = x2 - x1;
      this.y = y2 - y1;
    };

    //计算点乘
    function calculateVM(vector1, vector2) {
      return (vector1.x * vector2.x + vector1.y * vector2.y) / (Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) * Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y));

    }
    //计算叉乘
    function calculateVC(vector1, vector2) {
      return (vector1.x * vector2.y - vector2.x * vector1.y) > 0 ? 1 : -1;
    }

    let vector1 = new vector(preTwoPoint.x1, preTwoPoint.y1, preTwoPoint.x2, preTwoPoint.y2);
    let vector2 = new vector(twoPoint.x1, twoPoint.y1, twoPoint.x2, twoPoint.y2);
    let cos = calculateVM(vector1, vector2);
    let angle = Math.acos(cos) * 180 / Math.PI;

    let direction = calculateVC(vector1, vector2);
    let _allDeg = direction * angle;


    // 双指缩放
    let xMove = e.touches[1].clientX - e.touches[0].clientX;
    let yMove = e.touches[1].clientY - e.touches[0].clientY;
    let distance = Math.sqrt(xMove * xMove + yMove * yMove);

    let distanceDiff = distance - _this.data.stv.distance;
    let newScale = _this.data.stv.scale + 0.005 * distanceDiff;

    if (Math.abs(_allDeg) > 1) {
      _this.setData({
        'stv.rotate': _this.data.stv.rotate + _allDeg
      })
    } else {
      //双指缩放
      let xMove = e.touches[1].clientX - e.touches[0].clientX;
      let yMove = e.touches[1].clientY - e.touches[0].clientY;
      let distance = Math.sqrt(xMove * xMove + yMove * yMove);

      let distanceDiff = distance - _this.data.stv.distance;
      let newScale = _this.data.stv.scale + 0.005 * distanceDiff;
      if (newScale < 0.2 || newScale > 2.5) {
        return;
      }
      _this.setData({
        'stv.distance': distance,
        'stv.scale': newScale,
      })
    }
  } else {
    return;
  }
}

//为touchMove函数节流
const fn = throttle(touchMove, 10, 10);

cropper.json

{
  "component": true,
  "usingComponents": {}
}

cropper.wxml

<view class="container">
  <view class="img" style="width:{{ width }}px; height:{{height}}px;" catchtouchstart="touchstartCallback"  catchtouchmove="touchmoveCallback" catchtouchend="touchendCallback"  >
    <image style="transform: translate({{stv.offsetX}}px, {{stv.offsetY}}px) scale({{stv.scale}}) rotate({{ stv.rotate }}deg);width:{{originImg.width}}px; height: {{originImg.height}}px" src="{{ originImg.url }}"></image>
  </view>
</view>

cropper.wxss

.container{
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center
}
.img {
   overflow: hidden;
   background: #fff;
}

第二步页面使用
js

Page({
 data: {
    originUrl:"cloud://normal-env-ta6pc.6e6f-normal-env-ta6pc-1300924598/meinv/00006.jpg",
    ratio: 102 / 152,
  },
  chooseImg:function(){
    var that = this
    wx.chooseImage({
      count: 1,
      sizeType: ['original'],
      sourceType: ['album', 'camera'],
      success(res) {
        that.setData({
          originUrl: res.tempFilePaths[0],
        })
      }
    })
  }
})

json

{
  "usingComponents": {
    "cropper":"component/cropper/cropper"
  }
}

wxml

<view class="container">
<view class="view-row" style="color:#aaa;font-size:36rpx;margin:30rpx;border-bottom:1rpx solid #aaa;padding:10rpx">custom—拖拽、缩放和旋转</view>
  <view style="font-size:32rpx;color:#aaa;margin-bottom:30rpx">请拖拽、缩放或旋转下图</view>
  <cropper  url="{{ originUrl }}" ratio="{{ ratio }}"></cropper>
  <button bindtap='chooseImg' style="margin-top:30rpx">变更图片</button>
</view>

程序员不易,鼓励一下!

在这里插入图片描述

参加比赛的作品,开发周期一个月,使用了 Wafer2 框架,后台采用腾讯云提供的 Node.js SDK 接入对象存储 API ,前端核心代码实现了类似于图片编辑器的功能,支持图片和文字的移动、旋转缩放、生成预览图以及编辑状态的保存,动画部分采用 CSS 动画实现小程序中的模态输入框部分使用了自己封装的 InputBox 组件代码已移除 AppId 等敏感信息,可自行添加自己的 AppId 和 AppSecret 以配置后台环境,实现登录测试,详细添加方法见下文「使用方法」,若本地运行可通过修改 app.json 文件中 page 字段的顺序来查看不同页面微信小程序定制需求请联系作者微信:aweawds (注明来意)效果展示      使用方法首先点击右上角 Star ʕ •ᴥ•ʔ获取Demo代码执行 git clone https://github.com/goolhanrry/Weapp-Demo-LemonJournal.git或 点击此处 下载最新版本的代码解压后在微信开发者工具中打开 Weapp-Demo-LemonJournal 文件夹即可如需进行登录测试,还要执行以下步骤准备好自己的 AppId 和 AppSecret(可在微信公众平台注册后获取)在 project.config.json 的 appid 字段中填入 AppId在 /client/utils/util.js 中相应位置填入 AppId 和 AppSecret在微信开发者工具中重新导入整个项目,上传后台代码后编译运行即可核心代码组件的移动、旋转缩放主要思路是把  标签(对应图片)和  标签(对应文字)封装在同一个自定义组件  中,通过对外暴露的 text 变量是否为空来进行条件渲染,然后绑定 onTouchStart() 、onTouchEnd() 和 onTouchMove() 三个事件来对整个组件的位置、角度、大小、层级以及 “旋转” 和 “移除” 两个按钮的行为进行操作onTouchStart: function (e) {     // 若未选中则直接返回     if (!this.data.selected) {         return     }     switch (e.target.id) {         case 'sticker': {             this.touch_target = e.target.id             this.start_x = e.touches[0].clientX * 2             this.start_y = e.touches[0].clientY * 2             break         }         case 'handle': {             // 隐藏移除按钮             this.setData({                 hideRemove: true             })             this.touch_target = e.target.id             this.start_x = e.touches[0].clientX * 2             this.start_y = e.touches[0].clientY * 2             this.sticker_center_x = this.data.stickerCenterX;             this.sticker_center_y = this.data.stickerCenterY;             this.remove_center_x = this.data.removeCenterX;             this.remove_center_y = this.data.removeCenterY;             this.handle_center_x = this.data.handleCenterX;             this.handle_center_y = this.data.handleCenterY;             this.scale = this.data.scale;             this.rotate = this.data.rotate;             break         }     } }, onTouchEnd: function (e) {     this.active()     this.touch_target = ''     // 显示移除按钮     this.setData({         removeCenterX: 2 * this.data.stickerCenterX - this.data.handleCenterX,         removeCenterY: 2 * this.data.stickerCenterY - this.data.handleCenterY,         hideRemove: false     })     // 若点击移除按钮则触发移除事件,否则触发刷新数据事件     if (e.target.id === 'remove') {         this.triggerEvent('removeSticker', this.data.sticker_id)     } else {         this.triggerEvent('refreshData', this.data)     } }, onTouchMove: function (e) {     // 若无选中目标则返回     if (!this.touch_target) {         return     }     var current_x = e.touches[0].clientX * 2     var current_y = e.touches[0].clientY * 2     var diff_x = current_x - this.start_x     var diff_y = current_y - this.start_y     switch (e.target.id) {         case 'sticker': {             // 拖动组件则所有控件同时移动             this.setData({                 stickerCenterX: this.data.stickerCenterX   diff_x,                 stickerCenterY: this.data.stickerCenterY   diff_y,                 removeCenterX: this.data.removeCenterX   diff_x,                 removeCenterY: this.data.removeCenterY   diff_y,                 handleCenterX: this.data.handleCenterX   diff_x,                 handleCenterY: this.data.handleCenterY   diff_y             })             break         }         case 'handle': {             // 拖动操作按钮则原地旋转缩放             this.setData({                 handleCenterX: this.data.handleCenterX   diff_x,                 handleCenterY: this.data.handleCenterY   diff_y             })             var diff_x_before = this.handle_center_x - this.sticker_center_x;             var diff_y_before = this.handle_center_y - this.sticker_center_y;             var diff_x_after = this.data.handleCenterX - this.sticker_center_x;             var diff_y_after = this.data.handleCenterY - this.sticker_center_y;             var distance_before = Math.sqrt(diff_x_before * diff_x_before   diff_y_before * diff_y_before);             var distance_after = Math.sqrt(diff_x_after * diff_x_after   diff_y_after * diff_y_after);             var angle_before = Math.atan2(diff_y_before, diff_x_before) / Math.PI * 180;             var angle_after = Math.atan2(diff_y_after, diff_x_after) / Math.PI * 180;             this.setData({                 scale: distance_after / distance_before * this.scale,                 rotate: angle_after - angle_before   this.rotate             })             break         }     }     this.start_x = current_x;     this.start_y = current_y; }编辑状态的保存一篇手帐包含的组件类型包括 sticker(软件自带的贴纸)、image(用户上传的图片)和 text(自定义文字)三种,全部保存在一个如下格式的 json 对象中,每个独立组件都包含了一个不重复的 id 以及相关的信息,保存时由客户端生成该对象并编码成 json 字符串存储在数据库,恢复编辑状态时通过解析 json 字符串获得对象,再由编辑页面渲染{     "backgroundId": "5",                                        背景图id     "assemblies": [         {             "id": "jhjg",                                       组件id             "component_type": "image",                          组件类型(自定义图片)             "image_url": "https://example.com/jhjg.png",        图片地址             "stickerCenterX": 269,                              中心横坐标             "stickerCenterY": 664,                              中心纵坐标             "scale": 1.7123667831396403,                        缩放比例             "rotate": -3.0127875041833434,                      旋转角度             "wh_scale": 1,                                      图片宽高比             "z_index": 19                                       组件层级         },         {             "id": "gs47",             "component_type": "text",                           组件类型(文字)             "text": "test",                                     文字内容             "stickerCenterX": 479,             "stickerCenterY": 546,             "scale": 1.808535318980528,             "rotate": 29.11614626607893,             "z_index": 10         },         {             "id": "chjn",             "component_type": "sticker",                        组件类型(贴纸)             "sticker_type": "food",                             贴纸类型             "sticker_id": "1",                                  贴纸id             "image_url": "https://example.com/weapp/stickers/food/1.png",             "stickerCenterX": 277,             "stickerCenterY": 260,             "scale": 1.3984276885130673,             "rotate": -16.620756913892055,             "z_index": 5         }     ] }
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玩烂小程序

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值