微信小程序可移动缩放图片裁剪框

效果预览

提示:模拟器下由于微信自己的原因,裁剪按钮会被canvas覆盖,真机测试正常。

前言

图片裁剪框应用很普遍,也有很多成熟的组件,但是很多组件都是固定裁剪框的位置,通过移动图片来完成图片指定区域的裁剪。这种方法比较容易实现,但是同时存在灵活性不高,裁剪区域选择不精确,图片尺寸不能灵活改变的局限性。下面介绍一种通过使用微信小程序canvas来实现一个可移动缩放的图片裁剪框的方法。

技术要点分析

使用canvas组件实现图片裁剪框涉及以下技术内容:
(1)使用canvas绘制图片
(2)绘制图片裁剪框
(3)裁剪框四个缩放点位置计算
(4)裁剪框的移动和缩放
(5)防止裁剪框超出屏幕
(6)在裁剪框发生变化时重绘图片和裁剪框
(7)生成指定图片区域的裁剪图片
以上这些技术的实现需要进行一定的几何计算,在这里,裁剪框的原点坐标是左上角缩放点的坐标,另外三个缩放点可以根据裁剪框的宽度和高度进行坐标偏移得到。

具体实现

(0)页面数据结构

data:{
// 是否打开相机拍照
  open_camera: true,
  // 剪切图片的canvas上下文
  cut_image_canvas: {},
  // canvas实例
  mycanvas: {},
  // 图片对象
  image_obj: {},
  // 屏幕宽度和高度
  window_width: 0,
  window_heigt: 0,
  // 画布宽高
  cut_img_canvas_w: 0,
  cut_img_canvas_h: 0,
  // 裁剪区域
  cut_area: {
    // 裁剪区域左上角离屏幕原点坐标
    x: 0,
    y: 0,
    // 裁剪宽高
    cut_width: 0,
    cut_height: 60,
    // 裁剪区域边框颜色
    cut_area_color: 'red',
  },
  // 屏幕像素比
  pixelRatio: 1,
  // 裁剪区域移动和缩放时上一个触摸点坐标
  // 用来计算移动距离和移动方向
  last_touches_x: 0,
  last_touches_y: 0,
  // 裁剪框变换类型,可选值有:
  // move,left_up_scale,left_down_scale,
  // right_up_scale,right_down_scale
  cut_area_change_status: 'move',
  // 裁剪好的图片临时文件链接
  cut_image: ''
}

(1)wxml,wxss和页面数据初始化

图片裁剪主要涉及的系统参数有三个:屏幕窗口宽度(windowWidth),窗口高度(windowHeight,除去状态栏和小程序导航栏剩下的高度)和像素比例(pixelRatio,定义参考微信官方文档:相对像素和像素比计算)
注意事项: 推荐在pageonReady或者自定义组件的ready生命周期中进行初始化。
wxml:

<view wx:if="{{!cut_image}}">
  
  <camera flash="off" style="width: 100%; height: 100vh;" wx:if="{{open_camera}}">camera>
  
  <cover-view class="take_photo" hover-class="take_photo_hover" bindtap="cut_image" wx:if="{{!open_camera}}">
    <cover-view>裁剪cover-view>
  cover-view>
  
  <canvas id="cut_image_canvas" style="width:{{cut_img_canvas_w}}px;height:{{cut_img_canvas_h}}px;" type="2d" wx:if="{{!open_camera}}" disable-scroll="true" bindtouchmove="cut_area_move_and_scale" bindtouchstart="get_cut_area_change_status" canvas-id="cut_image_canvas">canvas>

  
  <cover-view class="take_photo" hover-class="take_photo_hover" wx:if="{{open_camera}}" bindtap="take_photo">
    <cover-view>拍照cover-view>
  cover-view>
view>

<view class="preview" wx:if="{{cut_image}}">
  <image mode="widthFix" src="{{cut_image}}">image>
view>

wxss

/* 拍照按钮 和图片裁剪按钮*/
.take_photo  {
  width: 240rpx;
  height: 96rpx;
  background: white;
  z-index: 1000;
  display: flex;
  flex-direction: row;
  align-content: center;
  align-items: center;
  justify-content: space-around;
  justify-items: center;
  border-radius: 10rpx;
  background: rgba(0, 0, 0, 0.4);
  color: white;
  position: fixed;
  bottom: 8%;
  left: 35%;
  text-align: center;
  font-size: 30rpx;
}
/* 拍照和图片裁剪按钮点击态 */
.take_photo_hover{
  transform: scale(0.9);
}

js数据初始化

//在page中用时放到onReady方法中
ready: function() {
      const that = this
      // 获取屏幕宽高和像素比
      wx.getSystemInfo({
        success: function(res) {
          that.data.window_heigt = res.windowHeight
          that.data.window_width = res.windowWidth
          that.data.pixelRatio = res.pixelRatio
        },
      })
    }

(2)待裁剪图片生成

微信小程序上的图片来源基本就两种,一种是从相册选取或者相机拍摄,另一种是网络图片,一般情况下图片裁剪主要在用户拍摄并上传图片的时候出现,因此这里使用的待裁剪图片由相机拍摄生成,代码如下:

/**
     * 拍照
     */
    take_photo: function() {
      const that = this
      //相机上下文对象
      const ctx = wx.createCameraContext()
      ctx.takePhoto({
        quality: 'high',
        success: (res) => {
        //loading,防止闪屏
          wx.showLoading({
            title: '',
          })
           // 隐藏相机
          that.setData({
            open_camera: false
          })
          //创建图片裁剪画布并绘制图片
          that.create_cut_image_canvas(res.tempImagePath)
        }
      })
    },

微信官方文档:系统相机

(3)创建canvas绘制图片和裁剪框

绘制图片到canvas使用的APIdrawImage,绘制图片分为两步:
1.根据相机生成的临时图片链接生成要绘制的图片对象。
2.绘制图片
具体实现如下:

/**
 * 创建图片裁剪画布和图片裁剪框
 * image_url:要绘制的图片,只接受微信小程序临时文件链接
 * 网络图片需要先下载到本地
 *
 */
create_cut_image_canvas:function(image_url) {
  const that=this
  // 在page中使用时应改为wx.createSelectorQuery()
  const query = that.createSelectorQuery()
  query.select('#cut_image_canvas')
    .fields({
      node: true,
      size: true
    })
    .exec((res) => {
      //获取画布实例
      const canvas = res[0].node
      that.data.mycanvas = canvas
      //获取画布上下文
      that.data.cut_image_canvas = canvas.getContext('2d')
      let w = 0
      let h = 0
      // 根据要绘制的图片调整画布宽高
      wx.getImageInfo({
        src: image_url,
        success: function(res) {
          // 画布宽度
          canvas.width = that.data.window_width;
          w = that.data.window_width
          // 画布高度
          canvas.height = that.data.window_width / res.width * res.height
          h = canvas.height

          // 预先设置裁剪区域宽度为屏幕宽度的80%
          that.data.cut_area.cut_width = 0.8 * w
          // 预先设定的裁剪区域左上角顶点位置
          that.data.cut_area.x = (w - that.data.cut_area.cut_width) / 2
          that.data.cut_area.y = (h - that.data.cut_area.cut_height) / 2
          // 设置画布宽高
          that.setData({
            cut_img_canvas_h: h,
            cut_img_canvas_w: w
          })
          // 绘制图片
          that.data.image_obj = canvas.createImage();
          that.data.image_obj.src = image_url
          that.data.image_obj.onload = () => {
            that.data.cut_image_canvas.drawImage(that.data.image_obj, 0, 0, w, 
            h)
            // 创建图片裁剪区域
            that.create_cut_area()
          }
        }
      })

    })
}

绘制裁剪框

/**
 * 绘制裁剪区域
 * that:自定义组件实例或page实例,即this
 */
create_cut_area:function() {
  const that=this
  // 创建预先选中的图片裁剪区域
  that.data.cut_image_canvas.strokeStyle = that.data.cut_area.cut_area_color
  // 裁剪区域边界线
  that.data.cut_image_canvas.strokeRect(that.data.cut_area.x, that.data.cut_area.y, that.data.cut_area.cut_width, that.data.cut_area.cut_height)

  // 裁剪区域边界的缩放圆点

  // 左上角外圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x, that.data.cut_area.y, 5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = 'white'
  that.data.cut_image_canvas.fill()
  // 左上角内圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x, that.data.cut_area.y, 2.5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = '#1296db'
  that.data.cut_image_canvas.fill()

  // 左下角外圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x, that.data.cut_area.y + that.data.cut_area.cut_height, 5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = 'white'
  that.data.cut_image_canvas.fill()
  // 左下角内圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x, that.data.cut_area.y + that.data.cut_area.cut_height, 2.5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = '#1296db'
  that.data.cut_image_canvas.fill()

  // 右下角外圆点
  that.data.cut_image_canvas.beginPath()
  //绘制圆点,半径乘以pixelRatio的目的是在不同屏幕下保持圆点大小一致
  that.data.cut_image_canvas.arc(that.data.cut_area.x + that.data.cut_area.cut_width, that.data.cut_area.y + that.data.cut_area.cut_height, 5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = 'white'
  that.data.cut_image_canvas.fill()
  // 右下角内圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x + that.data.cut_area.cut_width, that.data.cut_area.y + that.data.cut_area.cut_height, 2.5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = '#1296db'
  that.data.cut_image_canvas.fill()

  // 右上角外圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x + that.data.cut_area.cut_width, that.data.cut_area.y, 5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = 'white'
  that.data.cut_image_canvas.fill()
  // 右上角内圆点
  that.data.cut_image_canvas.beginPath()
  that.data.cut_image_canvas.arc(that.data.cut_area.x + that.data.cut_area.cut_width, that.data.cut_area.y, 2.5 * that.data.pixelRatio, 0, 2 * Math.PI)
  that.data.cut_image_canvas.fillStyle = '#1296db'
  that.data.cut_image_canvas.fill()
}

(4)计算四个缩放点位置

裁剪框缩放和移动时需要进行重绘,因此需要不断计算新的缩放点位置,计算实现如下:

    /**
     * 获取裁剪框四个缩放点位置
     */
    get_scale_point_location: function() {
      const that = this
      return {
        // 裁剪框左上角缩放点位置
        left_up_point_x: that.data.cut_area.x,
        left_up_point_y: that.data.cut_area.y,
        // 左下角缩放点位置
        left_down_point_x: that.data.cut_area.x,
        left_down_point_y: that.data.cut_area.y + that.data.cut_area.cut_height,
        // 右下角缩放点位置
        right_down_point_x: that.data.cut_area.x + that.data.cut_area.cut_width,
        right_down_point_y: that.data.cut_area.y + that.data.cut_area.cut_height,
        // 右上角缩放点位置
        right_up_point_x: that.data.cut_area.x + that.data.cut_area.cut_width,
        right_up_point_y: that.data.cut_area.y

      }
    },

(5)辨别裁剪框移动和缩放手势

创建完裁剪框之后,接下来需要给裁剪框添加缩放和移动响应,裁剪框缩放和移动的原理是首先根据canvas 使用bindtouchstart绑定的get_cut_area_change_status方法,计算第一次触摸屏幕的位置,根据触点位置判断接下来裁剪框是要进行缩放(触点离缩放点很近)还是移动(触点离缩放点远但是落在裁剪框内),然后根据使用bindtouchmove绑定的cut_area_move_and_scale方法计算触摸移动距离,然后根据之前的判断结果进行响应的缩放和移动。get_cut_area_change_status方法如下:

/**
 * 根据触点位置辨别是移动还是缩放图片裁剪框
 * e:点击事件
 */
get_cut_area_change_status:function (e) {
  const that=this
  // 触摸点位置
  let x = e.touches[0].x
  let y = e.touches[0].y
  // 复位裁剪框变化状态
  that.data.cut_area_change_status = ''
  // 缩放感应区域半径,触点落入该区域响应缩放
  //目的是防止缩放点太小,缩放反应迟钝
  let scale_reponse_radius = 30

  // 记录当前坐标,用来计算触摸移动距离
  that.data.last_touches_x = x
  that.data.last_touches_y = y

  // 获取四个缩放点位置
  const scale_point_location = that.get_scale_point_location()
  // 裁剪框左上角缩放点位置
  let left_up_point_x = scale_point_location.left_up_point_x
  let left_up_point_y = scale_point_location.left_up_point_y
  // 左下角缩放点位置
  let left_down_point_x = scale_point_location.left_down_point_x
  let left_down_point_y = scale_point_location.left_down_point_y
  // 右下角缩放点位置
  let right_down_point_x = scale_point_location.right_down_point_x
  let right_down_point_y = scale_point_location.right_down_point_y
  // 右上角缩放点位置
  let right_up_point_x = scale_point_location.right_up_point_x
  let right_up_point_y = scale_point_location.right_up_point_y

  // 判断是否是在移动裁剪框
  if ((left_up_point_x + scale_reponse_radius) < x && x < (right_down_point_x - scale_reponse_radius) && left_up_point_y < y && y < right_down_point_y) {
    that.data.cut_area_change_status = 'move'
  }
  // 按住左上角缩放
  if (Math.sqrt(Math.pow(x - left_up_point_x, 2) + Math.pow(y - left_up_point_y, 2)) < scale_reponse_radius) {
    that.data.cut_area_change_status = 'left_up_scale'
  }
  // 按住左下角缩放
  if (Math.sqrt(Math.pow(x - left_down_point_x, 2) + Math.pow(y - left_down_point_y, 2)) < scale_reponse_radius) {
    that.data.cut_area_change_status = 'left_down_scale'
  }
  // 按住右上角缩放
  if (Math.sqrt(Math.pow(x - right_up_point_x, 2) + Math.pow(y - right_up_point_y, 2)) < scale_reponse_radius) {
    that.data.cut_area_change_status = 'right_up_scale'
  }
  // 按住右下角缩放
  if (Math.sqrt(Math.pow(x - right_down_point_x, 2) + Math.pow(y - right_down_point_y, 2)) < scale_reponse_radius) {
    that.data.cut_area_change_status = 'right_down_scale'
  }
}

缩放感应区域和移动感应区域划分如下:
微信小程序可移动缩放图片裁剪框_第1张图片
红圈中就是缩放感应区域,矩形中除去红圈剩下的区域就是移动感应区域。

(6)canvas重绘

canvas重绘的目的是在裁剪框发生变化时,更新canvas,防止裁剪框绘制发生重叠。实现如下:

    /**
     * 画布重绘
     */
    redraw: function(mode = 0) {
      const that = this
      // 清空画布
      that.data.cut_image_canvas.clearRect(0, 0, that.data.cut_img_canvas_w, that.data.cut_img_canvas_h)
      // 重绘图片
      that.data.cut_image_canvas.drawImage(that.data.image_obj, 0, 0, that.data.cut_img_canvas_w, that.data.cut_img_canvas_h)
      // 重绘裁剪框
      // mode=0时重绘裁剪框
      // mode=1时不绘制裁剪框,防止切图时裁剪框被切入
      if (mode == 0) {
        that.create_cut_area()
      }
    },

(7)防止裁剪框超出屏幕边界

裁剪框进行移动和缩放时,可能会超出屏幕,因此需要根据四个缩放点的位置判断是否超出屏幕,在超出屏幕时阻止裁剪框缩放和移动,实现如下:

/**
 * 防止裁剪框超出屏幕
 * that:自定义组件或者页面实例,即this
 */
watch_cut_area_overflow:function () {
  const that=this
  // 距离屏幕边界裕度
  let margin = 5 * that.data.pixelRatio
  // 获取四个缩放点位置
  const scale_point_location = that.get_scale_point_location()
  // 裁剪框左上角缩放点位置
  let left_up_point_x = scale_point_location.left_up_point_x
  let left_up_point_y = scale_point_location.left_up_point_y
  // 左下角缩放点位置
  let left_down_point_x = scale_point_location.left_down_point_x
  let left_down_point_y = scale_point_location.left_down_point_y
  // 右下角缩放点位置
  let right_down_point_x = scale_point_location.right_down_point_x
  let right_down_point_y = scale_point_location.right_down_point_y
  // 右上角缩放点位置
  let right_up_point_x = scale_point_location.right_up_point_x
  let right_up_point_y = scale_point_location.right_up_point_y
  // 裁剪框左边超出屏幕
  if (left_up_point_x < margin) {
    return 1
  }
  //裁剪框右边超出屏幕
  if (right_down_point_x > (that.data.window_width - margin)) {
    return 1
  }
  // 裁剪框上边超出屏幕
  if (left_up_point_y < margin) {
    return 1
  }
  //裁剪框下边超出屏幕
  if (right_down_point_y > (that.data.window_heigt - margin)) {
    return 1
  }
  return 0
}

(8)裁剪框移动和缩放

做完(4)——(7)所述的工作,接下来可以实现裁剪框的移动和缩放和响应了。具体实现如下:

/**
 * 裁剪框移动和缩放
 * e:微信小程序点击事件
 */
cut_area_move_and_scale:function(e) {
  const that=this
  // 当前触点x和y坐标
  let touch_x = e.touches[0].x
  let touch_y = e.touches[0].y
  // 坐标变化量
  let dx = touch_x - that.data.last_touches_x
  let dy = touch_y - that.data.last_touches_y
  // 按住左上角缩放点缩放
  if (that.data.cut_area_change_status == "left_up_scale" || that.data.cut_area_change_status == "left_down_scale") {
    // 更新裁剪框高度
    that.data.cut_area.cut_height += -dy
    that.data.cut_area.cut_width += -dx
    // 更新左上角坐标
    that.data.cut_area.x += dx
    that.data.cut_area.y += dy
    // 裁剪框超出屏幕
    if (that.watch_cut_area_overflow()) {
      // 超出屏幕,撤销变化
      that.data.cut_area.cut_height -= -dy
      that.data.cut_area.cut_width -= -dx
      that.data.cut_area.x -= dx
      that.data.cut_area.y -= dy
      return;
    }
    // 重绘画布
    that.redraw()
  }
  // 按住其他缩放点缩放
  if (that.data.cut_area_change_status == "right_up_scale" || that.data.cut_area_change_status == "right_down_scale") {
    // 更新裁剪框高度
    that.data.cut_area.cut_height += dy
    that.data.cut_area.cut_width += dx
    // 裁剪框超出屏幕
    if (that.watch_cut_area_overflow()) {
      // 超出屏幕,撤销坐标变化
      that.data.cut_area.cut_width -= dx
      that.data.cut_area.cut_height -= dy
      return;
    }
    // 重绘画布
    that.redraw()
  }
  // 整体移动裁剪框
  if (that.data.cut_area_change_status == "move") {
    // 更新裁剪框左上角坐标量
    that.data.cut_area.x += dx
    that.data.cut_area.y += dy
    // 裁剪框超出屏幕
    if (that.watch_cut_area_overflow()) {
      // 超出屏幕,撤销坐标增量
      that.data.cut_area.x -= dx
      that.data.cut_area.y -= dy
      return;
    }
    // 重绘画布
    that.redraw()

  }
  // 更新点坐标
  that.data.last_touches_x = touch_x
  that.data.last_touches_y = touch_y
}

(9)生成裁剪后的图片及图片预览

最后,还需要把裁剪的图片区域导出成图片,代码实现如下:

/**
     * 图片裁剪
     */
    cut_image: function() {
      const that = this
      //此处重绘的目的是隐藏裁剪框,
      //防止裁剪框被切入图片
      that.redraw(1)
      wx.canvasToTempFilePath({
        // 除以像素比例,因为切图时使用的是相对像素
        x: that.data.cut_area.x / that.data.pixelRatio,
        y: that.data.cut_area.y / that.data.pixelRatio,
        //必须取整,否则安卓下回切图失败
        width: Math.round(that.data.cut_area.cut_width / that.data.pixelRatio),
        height: Math.round(that.data.cut_area.cut_height / that.data.pixelRatio),
        canvas: that.data.mycanvas,
        filetype: 'png',
        success: function(res) {
          console.log("裁剪的图片", res.tempFilePath)
          // 预览剪切好的图片
          that.setData({
            cut_image: res.tempFilePath
          })
        },
        fail: function(err) {
          console.log("裁剪图片失败", err)
          wx.showModal({
            title: '错误',
            content: '剪切图片失败',
          })
        }
      }, this)
    }

集成到项目

上述代码已经封装成一个自定义组件,方便集成到项目,如果想要直接在page中使用,直接复制以上代码到相应的page中即可,自定义组件下载地址:
链接:https://pan.baidu.com/s/1p3o1AM5ZOE6y_hnkHNFShw
提取码:wbau

你可能感兴趣的:(微信小程序,小程序,canvas)