代码片段
https://developers.weixin.qq.com/s/HbsH5pmu74tk
- 打开
ES6
转ES5
- 打开增强编译
- 打开不校验合法域名
需求分析
- 图片的宽高初始化不能大于镜头的宽高,如果大于需要缩放到镜头的宽高
- 图片的宽高不能小于镜头宽高,如果小于则需要按比例缩放
- 图片单指移动,移动停止的范围不能超过镜头的范围
- 图片双指缩放,缩放后的大小不能小于镜头宽高,如果小于则需要等比缩放到镜头宽高
- 图片可以旋转,点击下面的旋转按钮每次旋转
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 事件,所以需要所有手指离开屏幕时候才触发效果
isDoubleFingerMove
和currentToucnCount
一起判断是为了处理结束后的细节,比如两只手指按下后触发了缩放逻辑,但是松开我只搜开一只手指,这时候我不想让它再触发平移效果,否则会很混乱,只有两只手指离开(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()
},
})