小程序实现编辑相册功能
发布于 3 年前 作者 qiuxiuying 1479 次浏览 来自 分享

代码片段

https://developers.weixin.qq.com/s/fK5oLmmW7UsJ

  • 打开 ES6ES5
  • 打开增强编译
  • 打开不校验合法域名

需求分析

  1. 图片的宽高初始化不能大于镜头的宽高,如果大于需要缩放到镜头的宽高
  2. 图片的宽高不能小于镜头宽高,如果小于则需要按比例缩放
  3. 图片单指移动,移动停止的范围不能超过镜头的范围
  4. 图片双指缩放,缩放后的大小不能小于镜头宽高,如果小于则需要等比缩放到镜头宽高
  5. 图片可以旋转,点击下面的旋转按钮每次旋转 90deg

初始化展示样式

初始化数据结构

// 图片信息
const imageInfo = {
  startX: 0, // 起始按下X坐标
  startY: 0, // 起始按下Y坐标
  moveX: 0, // 当前移动X坐标
  moveY: 0, // 当前移动Y坐标
  width: 0, // 图片宽度
  height: 0, // 图片高度
  rotate: 0, // 旋转度数
  distance: 0, // 双指按下的距离
  touchCount: 0, // 当前按下的手指数量
  transformStyle: 'translate(0, 0) rotate(0deg)', // 图片的 transform 信息 
  rotateDirection: 'horizontal', // 旋转的轴向
  isDoubleFingerMove: false, // 是否是双指移动
}

Page({
  data: {
    imageInfo: { ...imageInfo },
    shotInfo: {
      width: 0, // 镜头宽度
      height: 0, // 镜头高度
      left: 0, // 镜头 x 坐标
      top : 0, // 镜头 y 坐标
    }
  },
})

获取镜头信息

Page({
  onReady() {
    this.initEditShot()
  },

  /**
   * 初始化镜头信息
   */
  initEditShot() {
    const query = wx.createSelectorQuery()
    query.select('.edit-shot').boundingClientRect()
    query.exec((res) => {
      const { width, height, left, top } = res[0]
      const shotInfo = {
        width,
        height,
        left,
        top,
      }

      this.data.shotInfo = shotInfo
      this.initImageInfo() // 初始化图片信息
    })
  },
})

初始化图片信息

Page({
  /**
   * 初始化图片信息
   */
  async initImageInfo() {
    // 处理图片信息
    const imgUrl = 'https://image-beta.djcars.cn/3/ef665764-8e83-4a31-ac85-a3292f379d4b.jpg'
    const { width, height, path } = await wx.getImageInfo({ src: imgUrl })
    const { width: shotWidth } = this.data.shotInfo
    let imageWidth = Math.ceil((shotWidth / width) * width) // 按比例缩放宽度
    let imageHeight = Math.ceil((shotWidth / width) * height) // 按比例缩放高度

    this.setData({
      imageUrl: path,
      ['imageInfo.width']: imageWidth,
      ['imageInfo.height']: imageHeight,
    })
  },
})

定义按比例缩放函数

上面的图片的高度已经小于镜头的高度了,所以需要按比例缩放

Page({
  /**
   * 初始化图片信息
   */
  async initImageInfo() {
    // 处理图片信息
    const imgUrl = 'https://image-beta.djcars.cn/3/ef665764-8e83-4a31-ac85-a3292f379d4b.jpg'
    const { width, height, path } = await wx.getImageInfo({ src: imgUrl })
    const { width: shotWidth } = this.data.shotInfo
    let imageWidth = Math.ceil((shotWidth / width) * width)
    let imageHeight = Math.ceil((shotWidth / width) * height)
    const imageSizeInfo = this._handleImageMinSizeScale(
      imageWidth,
      imageHeight
    )

    imageWidth = imageSizeInfo.imageWidth
    imageHeight = imageSizeInfo.imageHeight
    
    // 记录图片缩放后大小,还原需要用到
    imageInfo.width = imageWidth
    imageInfo.height = imageHeight

    this.setData({
      imageUrl: path,
      ['imageInfo.width']: imageWidth,
      ['imageInfo.height']: imageHeight,
    })
  },

  /**
   * 处理图片低于最小大小缩放
   */
  _handleImageMinSizeScale(imageWidth, imageHeight) {
    const { width, height } = this.data.shotInfo
    let scale = 1

    // 如果图片宽度小于镜头宽度
    if (imageWidth < width) {
      const diffWidth = width - imageWidth // 获取相差的宽度
      scale = diffWidth / imageWidth + 1
      scale = Number(scale.toFixed(3))
      imageWidth = Math.ceil(imageWidth * scale)
      imageHeight = Math.ceil(imageHeight * scale)
    }

    // 如果图片高度小于镜头高度
    if (imageHeight < height) {
      const diffHeight = height - imageHeight // 获取相差的高度
      scale = diffHeight / imageHeight + 1
      scale = Number(scale.toFixed(3))
      imageWidth = Math.ceil(imageWidth * scale)
      imageHeight = Math.ceil(imageHeight * scale)
    }

    return { imageWidth, imageHeight }
  },
})

处理图片平移和旋转

图片展示的逻辑已经处理完毕,接下来是处理移动、缩放逻辑

touch 的事件全部使用 catch 去绑定,不要去使用 bind 去绑定,否则你会发觉在手机上面卡的一批,使用 catch 后由于我的图片层级过低移动不了,在所有层级高的元素加上 point-events: none 的样式效果即可解决

处理图片平移

Page({
  /**
   * 监听图片按下
   */
  changeImageStat(e) {
    const touches = e.touches
    const { imageInfo } = this.data
    const { clientX, clientY } = touches[0]
    
    // 防止第二次移动时候回到原点,所以要减去上一次移动的过的位置
    this.data.imageInfo.startX = clientX - imageInfo.moveX
    this.data.imageInfo.startY = clientY - imageInfo.moveY
  },
  
  /**
   * 监听图片移动
   */
  changeImageMove(e) {
    const touches = e.touches
    const { startX, startY, rotate } = this.data.imageInfo
    const { clientX, clientY } = touches[0]
    const x = clientX - startX
    const y = clientY - startY
    const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)`
    
    // 记录当前移动的 x,y
    this.data.imageInfo.moveX = x
    this.data.imageInfo.moveY = y
    this.setData({
      ['imageInfo.transformStyle']: transformStyle,
    })
  },
})

处理图片的位置超过镜头的范围

Page({
  /**
   * 监听图片是否移除最大范围
   */
  _handleImageMove() {
    const { shotInfo, imageInfo } = this.data
    const { width: shotWidth, height: shotHeight } = shotInfo
    const { width, height, moveX, moveY, rotate } = imageInfo
    let maxX = (width - shotWidth) / 2 // 最大 x
    let maxY = (height - shotHeight) / 2 // 最大 y
    let x = moveX
    let y = moveY
    
    // 判断是否超出左边或超出右边
    if (maxX - x < 0) {
      x = maxX
    } else if (maxX + x < 0) {
      x = -maxX
    }
    
    // 判断是否超出上边或超出下边
    if (maxY - y < 0) {
      y = maxY
    } else if (maxY + y < 0) {
      y = -maxY
    }

    const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)`
    this.data.imageInfo.moveX = x
    this.data.imageInfo.moveY = y
    this.setData({
      ['imageInfo.transformStyle']: transformStyle,
    })
  },
  
  /**
   * 监听移动结束
   */
  changeImageEnd() {
    this._handleImageMove()
  },
})

处理图片旋转

点击旋转按钮每次旋转 90deg

Page({
  /**
   * 监听图片旋转
   */
  changeImageRotate() {
    const rotateStep = 90
    const imageInfo = this.data.imageInfo
    const { rotate, moveX, moveY, width, height } = imageInfo
    
    const currentRotate = rotate + rotateStep
    const handledRotate = currentRotate === 360 ? 0 : currentRotate
    const transformStyle = `translate(${moveX}px, ${moveY}px) rotate(${handledRotate}deg)`
    let imageWidth = width
    let imageHeight = height

    this.data.imageInfo.rotate = handledRotate
    this.setData({
      ['imageInfo.transformStyle']: transformStyle,
      ['imageInfo.width']: imageWidth,
      ['imageInfo.height']: imageHeight,
    })
    
    // 旋转时候可能也会超出了镜头范围,所以需要处理一下最大移动范围
    wx.nextTick(() => {
      this._handleImageMove()
    })
  },
})

处理图片旋转后的宽高

上面的图片效果是因为对 image 标签进行了旋转,由于 transform 旋转的轴向改变了,所以在审查元素时候 宽变成高、高变成宽,所以需要在旋转的时候判断轴向,然后对 宽高进行取反 ,然后再去判断当前图片宽高是否小于镜头宽高,再做缩放处理

Page({
  /**
   * 监听图片旋转
   */
  changeImageRotate() {
    const rotateStep = 90
    const imageInfo = this.data.imageInfo
    const { rotate, moveX, moveY, width, height, rotateDirection } = imageInfo
    
    const currentRotate = rotate + rotateStep
    const handledRotate = currentRotate === 360 ? 0 : currentRotate
    const transformStyle = `translate(${moveX}px, ${moveY}px) rotate(${handledRotate}deg)`
    
    const direction =
      rotateDirection === 'horizontal' ? 'vertical' : 'horizontal'
    let imageWidth = width
    let imageHeight = height
    
    // 如果是垂直方向则宽变高、高变框,同时处理缩放
    if (direction === 'vertical') {
      const imageSizeInfo = this._handleImageMinSizeScale(height, width)
      imageWidth = imageSizeInfo.imageHeight
      imageHeight = imageSizeInfo.imageWidth
    }
	
    // 记录旋转的方向和当前旋转的度数
    this.data.imageInfo.rotate = handledRotate
    this.data.imageInfo.rotateDirection = direction
    
    this.setData({
      ['imageInfo.transformStyle']: transformStyle,
      ['imageInfo.width']: imageWidth,
      ['imageInfo.height']: imageHeight,
    })
    
    // 旋转时候可能也会超出了镜头范围,所以需要处理一下最大移动范围
    wx.nextTick(() => {
      this._handleImageMove()
    })
  },
})

处理图片旋转后的平移

由于旋转后的宽高取反了,所以根据当前旋转的轴向然后把 宽高取反即可

Page({
  /**
   * 监听图片是否移除最大范围
   */
  _handleImageMove() {
    const { shotInfo, imageInfo } = this.data
    const { width: shotWidth, height: shotHeight } = shotInfo
    const { width, height, moveX, moveY, rotate, rotateDirection } = imageInfo
    let maxX = (width - shotWidth) / 2
    let maxY = (height - shotHeight) / 2
    let x = moveX
    let y = moveY
    
    // 处理旋转后的最大X和最大Y
    if (rotateDirection === 'vertical') {
      maxX = (height - shotWidth) / 2
      maxY = (width - shotHeight) / 2
    }

    if (maxX - x < 0) {
      x = maxX
    } else if (maxX + x < 0) {
      x = -maxX
    }

    if (maxY - y < 0) {
      y = maxY
    } else if (maxY + y < 0) {
      y = -maxY
    }

    const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)`
    this.data.imageInfo.moveX = x
    this.data.imageInfo.moveY = y
    this.setData({
      ['imageInfo.transformStyle']: transformStyle,
    })
  },
})

处理图片双指缩放

首先来分析一下需求:

  • 触发缩放的前提必须是两只手指以上(多于两只取第二条按下的手指作为参考)
  • 两条手指往近移则缩小、往远移则放大
  • 两条手指移动时不会触发单指平移的效果,只会缩放

处理双指按下时不触发平移效果

当多只手指按下屏幕时,会同时触发基于手指数量的 touchstart 事件,所以需要通过详细的数字去判断,而不能只根据长度去判断

Page({
  /**
   * 监听图片按下
   */
  changeImageStat(e) {
    const doubleFingerMoveCount = 2
    const touches = e.touches
    const { imageInfo } = this.data

    // 判断是否是单指
    if (touches.length < doubleFingerMoveCount) {
      const { clientX, clientY } = touches[0]
      this.data.imageInfo.startX = clientX - imageInfo.moveX
      this.data.imageInfo.startY = clientY - imageInfo.moveY
    } else if (touches.length === doubleFingerMoveCount) {
      this.data.imageInfo.isDoubleFingerMove = true // 记录是双指操作
      this.data.imageInfo.touchCount = doubleFingerMoveCount // 记录双指数量
      this.data.imageInfo.distance = this._calculateDistance(touches) // 计算两只手指距离
    }
  },
})

计算双指按下的距离

通过勾股定理求出两只手指的距离:

Page({
  /**
   * 计算双指距离
   */
  _calculateDistance(touches) {
    const x1 = touches[0].clientX
    const y1 = touches[0].clientY
    const x2 = touches[1].clientX
    const y2 = touches[1].clientY
    const clientX = x1 - x2
    const clientY = y1 - y2
    const distance = Math.sqrt(clientX ** 2 + clientY ** 2)

    return distance
  },
})

处理双指移动时缩放效果

当多只手指移动屏幕时,会同时触发基于手指数量的 touchmove 事件,所以需要通过详细的数字去判断,而不能只根据长度去判断

Page({
  /**
   * 监听图片移动
   */
  changeImageMove(e) {
    const touches = e.touches
    const doubleFingerMoveCount = 2
    const {
      startX,
      startY,
      width,
      height,
      rotate,
      distance,
      isDoubleFingerMove,
    } = this.data.imageInfo

    if (!isDoubleFingerMove && touches.length < doubleFingerMoveCount) {
      const { clientX, clientY } = touches[0]
      const x = clientX - startX
      const y = clientY - startY
      const transformStyle = `translate(${x}px, ${y}px) rotate(${rotate}deg)`

      this.data.imageInfo.moveX = x
      this.data.imageInfo.moveY = y
      this.setData({
        ['imageInfo.transformStyle']: transformStyle,
      })
    } else if (isDoubleFingerMove && touches.length === doubleFingerMoveCount) {
      const moveDistance = this._calculateDistance(touches) // 获取当前移动的双指距离
      const diffScale = Number((moveDistance / distance).toFixed(3)) // 获取当前缩放值
      const imageWidth = width * diffScale
      const imageHeight = height * diffScale

      this.data.imageInfo.distance = moveDistance // 实时记录双指距离才能实现每次移动慢慢缩放的效果
      this.setData({
        ['imageInfo.width']: imageWidth,
        ['imageInfo.height']: imageHeight,
      })
    }
  },
})

处理缩放时候手指松开效果

当多只手指离开屏幕时,会同时触发基于手指数量的 touchend 事件,所以需要所有手指离开屏幕时候才触发效果
isDoubleFingerMovecurrentToucnCount 一起判断是为了处理结束后的细节,比如两只手指按下后触发了缩放逻辑,但是松开我只搜开一只手指,这时候我不想让它再触发平移效果,否则会很混乱,只有两只手指离开 (currentToucnCount = 0) 才会把 isDoubleFingerMove 变为 false

Page({
  /**
   * 监听移动结束
   */
  changeImageEnd() {
    const { width, height, touchCount, rotateDirection, isDoubleFingerMove } = this.data.imageInfo
    const currentToucnCount = touchCount - 1 // 手指离开就减一
	
    // 记录当前手指数量
    this.data.imageInfo.touchCount = currentToucnCount
	
    // 如果是双指按下并且手指数量为0,则触发双指缩放离开处理逻辑
    if (isDoubleFingerMove && currentToucnCount <= 0) {
      let imageSizeInfo = {}
	  
      // 根据旋转轴向判断当前图片缩放是否小于镜头宽高
      if (rotateDirection === 'vertical') {
        imageSizeInfo = this._handleImageMinSizeScale(height, width)
        this.setData({
          ['imageInfo.width']: imageSizeInfo.imageHeight,
          ['imageInfo.height']: imageSizeInfo.imageWidth,
        })
      } else {
        imageSizeInfo = this._handleImageMinSizeScale(width, height)
        this.setData({
          ['imageInfo.width']: imageSizeInfo.imageWidth,
          ['imageInfo.height']: imageSizeInfo.imageHeight,
        })
      }
      
      // 重置双指缩放控制变量
      this.data.imageInfo.isDoubleFingerMove = false
      
      // 缩放后判断是否超出了镜头的范围
      wx.nextTick(() => {
        this._handleImageMove()
      })

      return
    }
    
    // 如果是单指则直接处理平移逻辑
    if (!isDoubleFingerMove) {
      this._handleImageMove()
    }
  }
})

将处理后的图片生成新的图片

由于画布是放在镜头元素的里面,而镜头在页面的效果是居中的,所以我的 canvas 也是居中在屏幕中间的,而宽高和镜头是相等的(边框差2像素后面补充细节)

<view class="edit-shot">
  <canvas class="image-canvas" type="2d"></canvas>
</view>

初始化画布信息

Page({
  /**
   * 初始化镜头画布
   */
  async initCanvas() {
    const query = wx.createSelectorQuery()
    query.select('.image-canvas').fields({ node: true, size: true })
    query.exec((res) => {
      const imageCanvas = res[0].node
      const imageCtx = imageCanvas.getContext('2d')
      const dpr = systemInfo.pixelRatio
      const width = res[0].width
      const height = res[0].height

      imageCanvas.width = width * dpr
      imageCanvas.height = height * dpr
      imageCtx.scale(dpr, dpr)

      this.imageCtx = imageCtx
      this.imageCanvas = imageCanvas
    })
  },
})

点击确认按钮裁切图片

Page({
  /**
   * 生成图片
   */
  async generatorImage() {
    await this._drawHandledImage()
    
    const { width, height } = this.data.shotInfo
    const { tempFilePath } = await wx.canvasToTempFilePath({
      width,
      height,
      canvas: this.imageCanvas,
    })

    wx.previewImage({
      urls: [tempFilePath],
    })
  },

    /**
   * 绘制图片
   */
  async _drawHandledImage() {
    const { imageInfo, imageUrl, shotInfo } = this.data
    const imageCanvas = this.imageCanvas
    const imageCtx = this.imageCtx
    const image = imageCanvas.createImage()
    const { width, height, moveX, moveY, rotate } = imageInfo

    let rotateMoveX = moveX
    let rotateMoveY = moveY

    /**
     * - 由于有旋转,则需要按照当前旋转的方向取反
     * - 由于镜头的宽高和canvas差2像素,所以根据旋转加或减一半,
     *   当然还有其他处理方法
     */
    if (rotate === 0) {
      rotateMoveX = moveX - 1
      rotateMoveY = moveY - 1
    }

    if (rotate === 90) {
      rotateMoveY = -moveX + 1
      rotateMoveX = moveY - 1
    }

    if (rotate === 180) {
      rotateMoveY = -moveY + 1
      rotateMoveX = -moveX + 1
    }

    if (rotate === 270) {
      rotateMoveY = moveX - 1
      rotateMoveX = -moveY + 1
    }
	
    // 获取当前移动的 x,y 在图片的坐标
    const diffX = (shotInfo.width - width) / 2 + rotateMoveX
    const diffY = (shotInfo.height - height) / 2 + rotateMoveY
   
   // 获取镜头中心点
    const rawX = shotInfo.width / 2
    const rawY = shotInfo.height / 2
    
    /**
     * - 由于我的 canvas 的 x,y 将会移动到中心点,因为旋转需要
     * - 图片移动的 x,y 药减去镜头的一半,因为坐标 0 是在中心点,
     *   而不是在镜头左上角
     */
    const x = diffX - rawX
    const y = diffY - rawY

    await new Promise((resolve) => {
      image.src = imageUrl
      image.onload = resolve
    })

    imageCtx.save()
    imageCtx.translate(rawX, rawY)
    imageCtx.rotate(rotate * (Math.PI / 180))
    imageCtx.drawImage(image, x, y, width, height)
    imageCtx.restore()
  },
})
回到顶部