微信小程序图片裁剪

1,204 阅读3分钟

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

微信小程序开发中上传头像时需要裁剪的,不然头像太大,记录自己使用的图片裁剪组件封装。组件基于we-cropper。

图片旋转和大小裁剪

主要是主页背景图的上传操作

组件结构

wxml

<view class="container">
  <!--  剪裁框与初始图片,剪裁框监听用户手势,获取移动缩放旋转值,images通过css样式显示变化  -->
  <view class="img" style="width:{{ width }}px; height:{{height}}px;top:{{topHeight+120}}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 class='footer'>
      <view bindtap='uploadTap'>重新选择</view> 
      <view bindtap='rotate'>旋转</view>
      <view bindtap='cropperImg'>修改好了</view>
  </view>
  <!--  canvas长宽设为初始图片设置的长款的两倍,使剪裁得到的图片更清晰,也不至于过大  -->
  <canvas class='imgcrop' style="width:{{ width * 2 }}px;height:{{ height * 2}}px;" canvas-id='imgcrop'></canvas>
</view>

wxss

.container {
  width: 100%;
  height: 100%;
  background: #000;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99;
}
.img {
   position: absolute;
   top: 5%;
   left: 50%;
   transform: translateX(-50%);
   overflow: hidden;
   background: #eee;
}
.img image {
  height:400px;
}
.imgcrop {
   position: absolute;
  left: -50000rpx;
  top: -500000rpx; 
}
.footer {
  position: absolute;
  width: 100%;
  height: 110rpx;
  color: #fff;
  background: #000;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-around;
}
.footer view {
  width: 30%;
  text-align: center;
}
.background {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  z-index: -1;
}

js

// 上传自己的头像以及个人页面的背景图裁剪组件
const app = getApp();
let topHeight = app.globalData.sysInfo.statusBarHeight + app.globalData.sysInfo.navHeight;
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,
          height: device.windowWidth * 0.47
        });
      }
    },
    url: {
      type: String,
      observer(newVal, oldVal) {
        this.initImg(newVal);
      }
    }
  },
  data: {
    topHeight: topHeight,
    width: device.windowWidth, //剪裁框的宽度
    height: device.windowWidth * 0.47, //剪裁框的长度
    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
        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)
      });
    },
    // canvas剪裁图片
    cropperImg() {
      wx.showLoading({
        title: "loading",
        mask: true
      });
      let _this = this;
      let ctx = wx.createCanvasContext("imgcrop", this);
      let cropData = _this.data.stv;
      ctx.save();
      // 缩放偏移值
      let x =
        (_this.data.originImg.width -
          _this.data.originImg.width * cropData.scale) /
        2;
      let y =
        (_this.data.originImg.height -
          _this.data.originImg.height * cropData.scale) /
        2;

      //画布中点坐标转移到图片中心
      let movex =
        (cropData.offsetX + x) * 2 +
        _this.data.originImg.width * cropData.scale;
      let movey =
        (cropData.offsetY + y) * 2 +
        _this.data.originImg.height * cropData.scale;
      ctx.translate(movex, movey);
      ctx.rotate((cropData.rotate * Math.PI) / 180);
      ctx.translate(-movex, -movey);

      ctx.drawImage(
        _this.data.originImg.url,
        (cropData.offsetX + x) * 2,
        (cropData.offsetY + y) * 2,
        _this.data.originImg.width * 2 * cropData.scale,
        _this.data.originImg.height * 2 * cropData.scale
      );
      ctx.restore();
      ctx.draw(false, () => {
        wx.canvasToTempFilePath(
          {
            canvasId: "imgcrop",
            success(response) {
              _this.triggerEvent("getCropperImg", {
                url: response.tempFilePath
              });
              wx.hideLoading();
            },
            fail(e) {
              wx.hideLoading();
              wx.showToast({
                title: "生成图片失败",
                icon: "none"
              });
            }
          },
          this
        );
      });
    },

    initImg(url) {
      let _this = this;
      wx.getImageInfo({
        src: url,
        success(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 bind:getCropperImg="_getCropperImg" url="{{ originUrl }}" ratio="{{ratio}}"></cropper>

上传裁好图片

_getCropperImg(e) {
    var coverPath = e.detail.url;
    // 上传函数
}

用户头像裁剪

wxml

<view class="cropper-wrapper" catchtap='clickReturn'>
    <canvas
            class="cropper"
            disable-scroll="true"
            bindtouchstart="touchStart"
            bindtouchmove="touchMove"
            bindtouchend="touchEnd"
            style="width:{{cropperOpt.width}}px;height:{{cropperOpt.height}}px;background-color: rgba(0, 0, 0, 0.8)"
            canvas-id="{{cropperOpt.id}}">
    </canvas>
	<view class="cropper-buttons">
		<view class="left" catchtap="upload">重新选择</view>
		<view class="right" catchtap="getCropperImage">确定</view>
	</view>
</view>

wxss

.cropper-buttons view {
	line-height: 50px;
	display: inline-block;
	width: 50%;
	background-color: #000;
	color: #fff;
	font-size: 18px;
}

.left {
	text-align: left;
	padding-left: 40rpx;
}

.right {
	text-align: right;
	padding-right: 40rpx;
}
.cropper-buttons{
  display: flex;
}

js

import WeCropper from '../../utils/we-cropper.js';
const device = wx.getSystemInfoSync();
const width = device.windowWidth;
const height = device.windowHeight - 50;
var app = getApp();
var cropper = null;
Page({
  data: {
    cropperOpt: {
      id: 'cropper',
      width,
      height,
      scale: 1,
      zoom: 5,
      cut: {
        x: 15,
        y: (height - 300) / 2,
        width: (width - 30),
        height: (width - 30)
      },
      option: null
    },
    w: 0,
    h: 0,
    showCanvas: false
  },

  touchStart(e) {
    this.wecropper.touchStart(e)
  },
  touchMove(e) {
    this.wecropper.touchMove(e)
  },
  touchEnd(e) {
    this.wecropper.touchEnd(e)
  },
  cancel: function() {
    wx.navigateBack({
      delta: 1
    })
  },
  getCropperImage() {
    var option = this.data.option;
    this.wecropper.getCropperImage((src) => {
      var pages = getCurrentPages();
      var prePage = pages[pages.length - 2];
      if (option.avatar) {
        prePage.setAvatar(src);
      } else if (option.cover) {
        prePage.setCover(src);
      }
      setTimeout(() => {
        wx.navigateBack({
          delta: 1
        })
      }, 200)
    })
  },

  onLoad: function(options) {
    var self = this;
    self.setData({
      showCanvas: true
    })
    self.setData({
      option: options
    });
    const {
      cropperOpt
    } = this.data;
    if (options.cover) {
      cropperOpt.cut.height = cropperOpt.cut.width
    } else if (options.avatar || options.isSetRole) {
      cropperOpt.cut.height = cropperOpt.cut.width;
    } else if (options.bgImg) {
      cropperOpt.cut.y = height * 1 / 10;
      cropperOpt.cut.x = 15;
      cropperOpt.cut.width = 345;
      cropperOpt.cut.height = 614;
    } else {
      cropperOpt.cut.y = 50;
    }
    var src = options.src;

    cropper = new WeCropper(cropperOpt)
      .on('ready', (ctx) => {})
      .on('beforeImageLoad', (ctx) => {
        wx.showToast({
          title: '上传中',
          icon: 'loading',
          duration: 20000
        })
      })
      .on('imageLoad', (ctx) => {
        wx.hideToast()
      })
      .on('beforeDraw', (ctx, instance) => {})
      .updateCanvas();

    self.wecropper.pushOrign(src);
  },

  upload: function() {
    var self = this;
    wx.chooseImage({
      count: 1,
      sizeType: ['original'],
      sourceType: ['album', 'camera'],
      success: function(res) {
        var tempFilePath = self.checkPhoto(res.tempFilePaths[0]);
        var option = self.data.option;
        option.src = tempFilePath;
        self.onLoad(option);
      }
    })
  },
  checkPhoto(tempFilePath) {
    var type = "";
    if (tempFilePath.length) {
      type = tempFilePath.match(/^(.*)(\.)(.{1,8})$/)[3];
      type = type.toUpperCase();
    }
    if (type != "JPEG" && type != "PNG" && type != "JPG" && type != "GIF") {
      wx.showToast({
        title: '已过滤特殊图片格式',
        icon: "none",
        duration: 2000
      })
      return false;
    } else {
      return tempFilePath;
    }
  },

})