小程序canvas截图组件
最近做一个小程序的过程中,需要用到截图功能,网上搜了一下,发现没有符合要求的,就自己搞了个组件,方便复用。
目前功能很简单,传入宽高和图片路径即可,宽高是为了计算截图的比例,只支持缩放和移动。
实现思路是:
1.模拟一个截取框;2.移动图片位置,缩放图片;3.获取图片在其中的位置(left,top,width,height);4.使用canvas绘制图片,然后截取就ok了。
其中第二步的缩放图片比较麻烦,缩放中心点以及平滑缩放
以下是我的实现方式
wxml:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!--component/picPro/picPro.wxml--> <scroll-view class='body' hidden="{{hidden}}"> <view class='flex-column flex-between full-height full-width' bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend"> <view class='bg_dark out_item'></view> <view class='flex-row main flex-between' style='height:{{(windowWidth - margin.left - margin.right)/ratio + "px"}}'> <view class='bg_dark main_item full-height' style='width:{{margin.left + "px"}}'></view> <view class='inner relative full-width' id='showArea'> <image class='absolute img' src='{{src}}' style="width:{{img.width}}px;height:{{img.height}}px;left:{{img.left}}px;top:{{img.top}}px;"></image> <canvas canvas-id='imgCanvas' class='absolute img_canvas full-height full-width' /> <view class='absolute inner_item left_top'></view> <view class='absolute inner_item right_top'></view> <view class='absolute inner_item right_bottom'></view> <view class='absolute inner_item left_bottom'></view> </view> <view class='bg_dark main_item full-height' style='width:{{margin.right + "px"}}'></view> </view> <view class='bg_dark out_item flex-column flex-end'> <view class='flex-around text_white text_bg'> <view catchtap='outputImg' data-type='1'><text>重新上传</text></view> <view catchtap='getImg'><text>选择图片</text></view> </view> </view> <!-- --> <view class='absolute full-width full-height bg_black'></view> </view> </scroll-view>
wxss:(其中引入了一个公共样式,关于flex布局的,看样式名也能猜到)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/* component/picPro/picPro.wxss */ @import '../../resource/style/flex.wxss'; .body{ position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .text_white{ color: white; } .main{ } .out_item{ width: 100%; height: 100%; flex: 1; } .bg_dark{ background-color: rgba(0, 0, 0, 0.85) } .main_item{ width: 15px; } .inner{ outline: 3rpx solid white; background-color: rgba(0, 0, 0, 0.12); box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset; } .inner_item{ width: 8px; height: 8px; } .inner_item.left_top{ border-left: 3px solid white; border-top: 3px solid white; left: -3px; top: -3px; } .inner_item.right_top{ border-right: 3px solid white; border-top: 3px solid white; right: -3px; top: -3px; } .inner_item.right_bottom{ border-right: 3px solid white; border-bottom: 3px solid white; right: -3px; bottom: -3px; } .inner_item.left_bottom{ border-left: 3px solid white; border-bottom: 3px solid white; left: -3px; bottom: -3px; } .img{ z-index: -1; } .bg_black{ background-color:black; z-index: -2; } .text_bg{ padding-bottom: 2em; font-size: 0.9em; } .img_canvas{ opacity: 0.5; } .newImg{ z-index: 2 }
js:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // component/picPro/picPro.js 2 const state = { 3 // 可用区域body 4 window: { width: 0, height: 0 }, 5 // 原始图片信息 6 originImg: { width: 0, height: 0 }, 7 // 第一次图片缩放信息 8 firstScaleImg: { width: 0, height: 0 }, 9 // 截取区域信息 10 interArea: { width: 0, height: 0 }, 11 // 单手触摸位置 12 touchLast: { x: 0, y: 0 }, 13 // 滑动距离 14 touchMove: { x: 0, y: 0 }, 15 // 滑动离开时图片状态 16 moveImgState: { 17 width: 0, 18 height: 0, 19 top: 0, 20 left: 0, 21 }, 22 // 双手触摸位置 23 touchList: [{ x: 0, y: 0 }, { x: 0, y: 0 }], 24 // 图片缩放比例 25 scale: 1, 26 } 27 Component({ 28 /** 29 * 组件的属性列表 30 */ 31 properties: { 32 //宽(非实际值) 33 width: { 34 type: Number, 35 value: 600 36 }, 37 //高 38 height: { 39 type: Number, 40 value: 300 41 }, 42 //图片路径 43 src: { 44 type: String, 45 value: "" 46 }, 47 //显示隐藏 48 hidden: { 49 type: Boolean, 50 value: false 51 }, 52 //截取框的信息 53 margin: { 54 type: Object, 55 value: { 56 left: 15, 57 right: 15, 58 top: 200, 59 bottom: 200, 60 } 61 } 62 }, 63 64 ready() { 65 this.initialize(); 66 // const canvas = wx.createCanvasContext('imgCanvas', this); 67 // canvas.draw(false, () => { console.log('ccc') }, this); 68 }, 69 70 /** 71 * 组件的初始数据 72 */ 73 data: { 74 touchRange: 8, 75 img: { 76 width: 0, 77 height: 0, 78 top: 0, 79 left: 0, 80 }, 81 canvas: {}, 82 ratio: 0, 83 originImg: { 84 width: 0, 85 height: 0 86 } 87 }, 88 89 /** 90 * 组件的方法列表 91 */ 92 methods: { 93 touchstart(e) { 94 // console.log("touchstart", e); 95 96 }, 97 touchmove(e) { 98 if (e.touches.length === 1) { this.singleSlip(e.touches[0]) } else { 99 this.doubleSlip(e.touches) 100 } 101 }, 102 touchend(e) { 103 // console.log("touchend", e); 104 const x = 0, y = 0; 105 state.touchLast = { x, y }; 106 state.touchMove = { x, y }; 107 state.touchList = [{ x, y }, { x, y }]; 108 state.moveImgState = this.data.img; 109 // console.log(this.data.img); 110 }, 111 // 单手滑动操作 112 singleSlip(e) { 113 const { clientX: x, clientY: y } = e; 114 const that = this; 115 if (state.touchLast.x && state.touchLast.y) { 116 state.touchMove = { x: x - state.touchLast.x, y: y - state.touchLast.y }; 117 state.touchLast = { x, y }; 118 const move = (_x = false, _y = false) => { 119 const bottom = that.data.img.height + that.data.img.top; 120 const right = that.data.img.width + that.data.img.left; 121 const h = state.interArea.height; 122 const w = state.interArea.width; 123 const param = {}; 124 if (_x) { 125 if (right > w && that.data.img.left < 0) { 126 param.left = that.data.img.left + state.touchMove.x * 0.1 127 } else if (right <= w && state.touchMove.x > 0) { 128 param.left = that.data.img.left + state.touchMove.x * 0.1 129 } else if (that.data.img.left >= 0 && state.touchMove.x < 0) { 130 param.left = that.data.img.left + state.touchMove.x * 0.1 131 } 132 }; 133 if (_y) { 134 if (bottom > h && that.data.img.top < 0) { 135 param.top = that.data.img.top + state.touchMove.y * 0.1 136 } else if (bottom <= h && state.touchMove.y > 0) { 137 param.top = that.data.img.top + state.touchMove.y * 0.1 138 } else if (that.data.img.top >= 0 && state.touchMove.y < 0) { 139 param.top = that.data.img.top + state.touchMove.y * 0.1 140 } 141 }; 142 // console.log(param); 143 that.setImgPos(param) 144 }; 145 if (state.scale == 1) { 146 if (that.data.img.width == state.interArea.width) { 147 move(false, true) 148 } else { 149 move(true, false) 150 } 151 } else { 152 move(true, true) 153 } 154 } else { 155 state.touchLast = { x, y } 156 } 157 }, 158 // 双手缩放操作 159 doubleSlip(e) { 160 const that = this; 161 const { clientX: x0, clientY: y0 } = e[0]; 162 const { clientX: x1, clientY: y1 } = e[1]; 163 if (state.touchList[0].x && state.touchList[0].y) { 164 let changeScale = (Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)) - Math.sqrt((state.touchList[1].x - state.touchList[0].x) * (state.touchList[1].x - state.touchList[0].x) + (state.touchList[1].y - state.touchList[0].y) * (state.touchList[1].y - state.touchList[0].y))) * 0.0005; 165 changeScale = changeScale >= 1.5 ? 1.5 : (changeScale <= -1 ? -1 : changeScale); 166 state.scale = that.data.img.width / state.firstScaleImg.width < 1 ? 1 : (state.scale > 2.5 ? 2.5 : 1 + changeScale); 167 let width = state.firstScaleImg.width * (state.scale - 1) + state.moveImgState.width; 168 width = width < state.firstScaleImg.width ? state.firstScaleImg.width : width; 169 let height = state.firstScaleImg.height * (state.scale - 1) + state.moveImgState.height; 170 height = height < state.firstScaleImg.height ? state.firstScaleImg.height : height; 171 let left = width * (1 - state.scale) / 4 + state.moveImgState.left; 172 left = left * (-1) > width - state.interArea.width ? state.interArea.width - width: left > 0 ? 0 : left; 173 let top = height * (1 - state.scale) / 4 + state.moveImgState.top; 174 top = top * (-1) > height - state.interArea.height ?state.interArea.height - height : top > 0 ? 0 : top; 175 const setImgObj = { width, height, left, top }; 176 that.setImgPos(setImgObj) 177 } else { 178 state.touchList = [{ x: x0, y: y0 }, { x: x1, y: y1 }] 179 } 180 }, 181 // 获取可用区域宽高 182 getScreenInfo() { 183 const that = this; 184 return new Promise((resolve, reject) => { 185 wx.getSystemInfo({ 186 success: function (res) { 187 const { windowHeight, windowWidth } = res; 188 state.window = { windowHeight, windowWidth }; 189 that.setData({ windowHeight, windowWidth }) 190 // console.log(state.window); 191 resolve(res); 192 }, 193 }) 194 }) 195 }, 196 setShowArea() { 197 const that = this; 198 const w = state.window.windowWidth - that.data.margin.left - that.data.margin.right; 199 const h = (that.data.height / that.data.width) * w; 200 }, 201 outputImg() { 202 this.setData({ 203 hidden: true, 204 }) 205 }, 206 getImgInfo(path) { 207 return new Promise((resolve, reject) => { 208 wx.getImageInfo({ 209 src: path, 210 success(res) { 211 console.log(res); 212 resolve(res); 213 }, 214 fail(err) { 215 reject(err) 216 } 217 }) 218 }) 219 }, 220 // 设置图片 221 setImgPos({ width, height, top, left }) { 222 width = width || this.data.img.width; 223 height = height || this.data.img.height; 224 top = top || this.data.img.top; 225 left = left || this.data.img.left 226 this.setData({ 227 img: { width, height, top, left } 228 }) 229 }, 230 // 初始化图片位置大小 231 initialize() { 232 const that = this; 233 const ratio = that.data.width / that.data.height; 234 this.getScreenInfo().then(res => { 235 console.log(res); 236 state.interArea = { width: res.windowWidth - that.data.margin.left - that.data.margin.right + 2, height: (res.windowWidth - that.data.margin.left - that.data.margin.right) / ratio }; 237 console.log("interArea", state.interArea) 238 that.getImgInfo(that.data.src).then(imgInfo => { 239 const { width, height } = imgInfo; 240 const imgRatio = width / height; 241 state.originImg = { width, height }; 242 that.setData({ 243 ratio: ratio 244 }); 245 if (imgRatio > ratio) { 246 that.setImgPos({ 247 height: state.interArea.height, 248 width: state.interArea.height * imgRatio 249 }) 250 } else { 251 that.setImgPos({ 252 height: state.interArea.width / imgRatio, 253 width: state.interArea.width, 254 }) 255 }; 256 state.firstScaleImg = { width: that.data.img.width, height: that.data.img.height } 257 }); 258 }); 259 }, 260 // 截图 261 getImg(){ 262 const that = this; 263 // console.log('dudu', that.data.img); 264 const canvas = wx.createCanvasContext('imgCanvas', this); 265 const {width,height,left,top} = that.data.img; 266 const saveImg = ()=>{ 267 console.log('开始截取图片'); 268 wx.canvasToTempFilePath({ 269 canvasId:"imgCanvas", 270 success(res){ 271 // console.log(res); 272 that.setData({ 273 hidden:true, 274 // src:"" 275 }); 276 that.triggerEvent("putimg", { imgUrl: res.tempFilePath},{}); 277 }, 278 fail(err){ 279 console.log(err) 280 } 281 },that) 282 }; 283 canvas.drawImage(that.data.src, left, top, width, height); 284 canvas.draw(false, () => { saveImg() }, that) 285 } 286 } 287 })
引用的时候除了宽高路径以外,需要wx:if;如果不卸载组件,会出现只能截一次的bug
因为小程序里面没有类似vue中catch的观测数据变化的东西,也不想为了个组件专门去搞一个,就用这种方式代替了,嘻嘻,好敷衍。。
<------------------------以前做的,现在发一下源码,不要吐槽--------------------------->
链接:https://pan.baidu.com/s/1MnTVhdGDmtFLTJdO5m024w
提取码:7yun