赞
踩
提示:模拟器下由于微信自己的原因,裁剪按钮会被canvas覆盖,真机测试正常。
代码获取:https://download.csdn.net/download/anbuqi/88608193
图片裁剪框应用很普遍,也有很多成熟的组件,但是很多组件都是固定裁剪框的位置,通过移动图片来完成图片指定区域的裁剪。这种方法比较容易实现,但是同时存在灵活性不高,裁剪区域选择不精确,图片尺寸不能灵活改变的局限性。下面介绍一种通过使用微信小程序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) }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。