提示:模拟器下由于微信自己的原因,裁剪按钮会被canvas覆盖,真机测试正常。
图片裁剪框应用很普遍,也有很多成熟的组件,但是很多组件都是固定裁剪框的位置,通过移动图片来完成图片指定区域的裁剪。这种方法比较容易实现,但是同时存在灵活性不高,裁剪区域选择不精确,图片尺寸不能灵活改变的局限性。下面介绍一种通过使用微信小程序canvas来实现一个可移动缩放的图片裁剪框的方法。
使用canvas组件实现图片裁剪框涉及以下技术内容:
(1)使用canvas绘制图片
(2)绘制图片裁剪框
(3)裁剪框四个缩放点位置计算
(4)裁剪框的移动和缩放
(5)防止裁剪框超出屏幕
(6)在裁剪框发生变化时重绘图片和裁剪框
(7)生成指定图片区域的裁剪图片
以上这些技术的实现需要进行一定的几何计算,在这里,裁剪框的原点坐标是左上角缩放点的坐标,另外三个缩放点可以根据裁剪框的宽度和高度进行坐标偏移得到。
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: ''
}
图片裁剪主要涉及的系统参数有三个:屏幕窗口宽度(windowWidth),窗口高度(windowHeight,除去状态栏和小程序导航栏剩下的高度)和像素比例(pixelRatio,定义参考微信官方文档:相对像素和像素比计算)
注意事项: 推荐在page的onReady或者自定义组件的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
},
})
}
微信小程序上的图片来源基本就两种,一种是从相册选取或者相机拍摄,另一种是网络图片,一般情况下图片裁剪主要在用户拍摄并上传图片的时候出现,因此这里使用的待裁剪图片由相机拍摄生成,代码如下:
/**
* 拍照
*/
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)
}
})
},
微信官方文档:系统相机
绘制图片到canvas使用的API是drawImage,绘制图片分为两步:
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()
}
裁剪框缩放和移动时需要进行重绘,因此需要不断计算新的缩放点位置,计算实现如下:
/**
* 获取裁剪框四个缩放点位置
*/
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
}
},
创建完裁剪框之后,接下来需要给裁剪框添加缩放和移动响应,裁剪框缩放和移动的原理是首先根据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'
}
}
缩放感应区域和移动感应区域划分如下:
红圈中就是缩放感应区域,矩形中除去红圈剩下的区域就是移动感应区域。
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()
}
},
裁剪框进行移动和缩放时,可能会超出屏幕,因此需要根据四个缩放点的位置判断是否超出屏幕,在超出屏幕时阻止裁剪框缩放和移动,实现如下:
/**
* 防止裁剪框超出屏幕
* 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
}
做完(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
}
最后,还需要把裁剪的图片区域导出成图片,代码实现如下:
/**
* 图片裁剪
*/
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