微信小程序canvas拖拽、截图组件功能
先看下微信小程序canvas拖拽功能
组件地址
近期补上
实现效果
如何实现
- 使用canvas
- 使用movable-view标签
由于movable-view无法实现旋转,所以选择使用canvas
需要解决的问题
- 如何将多个元素渲染到canvas上
- 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层
- 如何实现拖拽元素
- 如何缩放、旋转、删除元素
看起来挺简单的嘛,就把上面这几个问题解决了,就可以实现功能了;接下来我们一一解决。
如何将多个元素渲染到canvas上
定义一个draggraph类,传入元素的各种属性(坐标、尺寸…)实例化后推入一个 渲染数组 里,然后再循环这个数组调用实例中的渲染方法,这样就可以把多个元素渲染到canvas上了。
如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层
在draggraph类中定义了判断点击位置的方法,我们在canvas上绑定touchstart事件,将手指的坐标传入上面的方法,我们就可以知道手指是点击到元素本身,还是删除图标或者变换大小的图标上了,这个方法具体怎么判断后面会讲解。
通过循环 渲染数组 判断是非点击到哪个元素到,如果点击中了多个元素,也就是多个元素重叠,那第一个元素就是最上层的元素啦。
###如何实现拖拽元素
通过上面我们可以判断手指是否在元素上,当touchstart事件触发时我们记录当前的手指坐标,当touchmove事件触发时,我们也知道这时的坐标,两个坐标取差值,就可以得出元素位移的距离啦,修改这个元素实例的x和y,再重新循环渲染 渲染数组 就可以实现拖拽的功能。
如何缩放、旋转、删除元素
这一步相对比较难一点,我会通过示意图跟大家讲解。
我们先讲缩放和旋转
通过touchstart和touchmove我们可以获得旋转前的旋转后的坐标,图中的线a为元素的中点和旋转前点的连线;线b为元素中点和旋转后点的连线;我们只需要求a和b两条线的夹角就可以知道元素旋转的角度。缩放尺寸为a和b两条线长度之差。
计算旋转角度的代码如下:
const centerx = (this.x + this.w) / 2; // 中点坐标 const centery = (this.y + this.h) / 2; // 中点坐标 const diffxbefore = px - centerx; // 旋转前坐标 const diffybefore = py - centery; // 旋转前坐标 const diffxafter = x - centerx; // 旋转后坐标 const diffyafter = y - centery; // 旋转后坐标 const anglebefore = math.atan2(diffybefore, diffxbefore) / math.pi * 180; const angleafter = math.atan2(diffyafter, diffxafter) / math.pi * 180; // 旋转的角度 this.rotate = currentgraph.rotate + angleafter - anglebefore;
计算缩放尺寸的代码如下:
// 放大 或 缩小 this.x = currentgraph.x - (x - px); this.y = currentgraph.y - (x - px);
下面介绍下小程序canvas截图组件
最近做一个小程序的过程中,需要用到截图功能,网上搜了一下,发现没有符合要求的,就自己搞了个组件,方便复用。
目前功能很简单,传入宽高和图片路径即可,宽高是为了计算截图的比例,只支持缩放和移动。
实现思路是:
1.模拟一个截取框;
2.移动图片位置,缩放图片;
3.获取图片在其中的位置(left,top,width,height);
4.使用canvas绘制图片,然后截取就ok了。
其中第二步的缩放图片比较麻烦,缩放中心点以及平滑缩放
以下是我的实现方式
wxml:
<!--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布局的,看样式名也能猜到)
/* 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:
// component/picpro/picpro.js const state = { // 可用区域body window: { width: 0, height: 0 }, // 原始图片信息 originimg: { width: 0, height: 0 }, // 第一次图片缩放信息 firstscaleimg: { width: 0, height: 0 }, // 截取区域信息 interarea: { width: 0, height: 0 }, // 单手触摸位置 touchlast: { x: 0, y: 0 }, // 滑动距离 touchmove: { x: 0, y: 0 }, // 滑动离开时图片状态 moveimgstate: { width: 0, height: 0, top: 0, left: 0, }, // 双手触摸位置 touchlist: [{ x: 0, y: 0 }, { x: 0, y: 0 }], // 图片缩放比例 scale: 1, } component({ /** * 组件的属性列表 */ properties: { //宽(非实际值) width: { type: number, value: 600 }, //高 height: { type: number, value: 300 }, //图片路径 src: { type: string, value: "" }, //显示隐藏 hidden: { type: boolean, value: false }, //截取框的信息 margin: { type: object, value: { left: 15, right: 15, top: 200, bottom: 200, } } }, ready() { this.initialize(); // const canvas = wx.createcanvascontext('imgcanvas', this); // canvas.draw(false, () => { console.log('ccc') }, this); }, /** * 组件的初始数据 */ data: { touchrange: 8, img: { width: 0, height: 0, top: 0, left: 0, }, canvas: {}, ratio: 0, originimg: { width: 0, height: 0 } }, /** * 组件的方法列表 */ methods: { touchstart(e) { // console.log("touchstart", e); }, touchmove(e) { if (e.touches.length === 1) { this.singleslip(e.touches[0]) } else { this.doubleslip(e.touches) } }, touchend(e) { // console.log("touchend", e); const x = 0, y = 0; state.touchlast = { x, y }; state.touchmove = { x, y }; state.touchlist = [{ x, y }, { x, y }]; state.moveimgstate = this.data.img; // console.log(this.data.img); }, // 单手滑动操作 singleslip(e) { const { clientx: x, clienty: y } = e; const that = this; if (state.touchlast.x && state.touchlast.y) { state.touchmove = { x: x - state.touchlast.x, y: y - state.touchlast.y }; state.touchlast = { x, y }; const move = (_x = false, _y = false) => { const bottom = that.data.img.height + that.data.img.top; const right = that.data.img.width + that.data.img.left; const h = state.interarea.height; const w = state.interarea.width; const param = {}; if (_x) { if (right > w && that.data.img.left < 0) { param.left = that.data.img.left + state.touchmove.x * 0.1 } else if (right <= w && state.touchmove.x > 0) { param.left = that.data.img.left + state.touchmove.x * 0.1 } else if (that.data.img.left >= 0 && state.touchmove.x < 0) { param.left = that.data.img.left + state.touchmove.x * 0.1 } }; if (_y) { if (bottom > h && that.data.img.top < 0) { param.top = that.data.img.top + state.touchmove.y * 0.1 } else if (bottom <= h && state.touchmove.y > 0) { param.top = that.data.img.top + state.touchmove.y * 0.1 } else if (that.data.img.top >= 0 && state.touchmove.y < 0) { param.top = that.data.img.top + state.touchmove.y * 0.1 } }; // console.log(param); that.setimgpos(param) }; if (state.scale == 1) { if (that.data.img.width == state.interarea.width) { move(false, true) } else { move(true, false) } } else { move(true, true) } } else { state.touchlast = { x, y } } }, // 双手缩放操作 doubleslip(e) { const that = this; const { clientx: x0, clienty: y0 } = e[0]; const { clientx: x1, clienty: y1 } = e[1]; if (state.touchlist[0].x && state.touchlist[0].y) { 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; changescale = changescale >= 1.5 ? 1.5 : (changescale <= -1 ? -1 : changescale); state.scale = that.data.img.width / state.firstscaleimg.width < 1 ? 1 : (state.scale > 2.5 ? 2.5 : 1 + changescale); let width = state.firstscaleimg.width * (state.scale - 1) + state.moveimgstate.width; width = width < state.firstscaleimg.width ? state.firstscaleimg.width : width; let height = state.firstscaleimg.height * (state.scale - 1) + state.moveimgstate.height; height = height < state.firstscaleimg.height ? state.firstscaleimg.height : height; let left = width * (1 - state.scale) / 4 + state.moveimgstate.left; left = left * (-1) > width - state.interarea.width ? state.interarea.width - width: left > 0 ? 0 : left; let top = height * (1 - state.scale) / 4 + state.moveimgstate.top; top = top * (-1) > height - state.interarea.height ?state.interarea.height - height : top > 0 ? 0 : top; const setimgobj = { width, height, left, top }; that.setimgpos(setimgobj) } else { state.touchlist = [{ x: x0, y: y0 }, { x: x1, y: y1 }] } }, // 获取可用区域宽高 getscreeninfo() { const that = this; return new promise((resolve, reject) => { wx.getsysteminfo({ success: function (res) { const { windowheight, windowwidth } = res; state.window = { windowheight, windowwidth }; that.setdata({ windowheight, windowwidth }) // console.log(state.window); resolve(res); }, }) }) }, setshowarea() { const that = this; const w = state.window.windowwidth - that.data.margin.left - that.data.margin.right; const h = (that.data.height / that.data.width) * w; }, outputimg() { this.setdata({ hidden: true, }) }, getimginfo(path) { return new promise((resolve, reject) => { wx.getimageinfo({ src: path, success(res) { console.log(res); resolve(res); }, fail(err) { reject(err) } }) }) }, // 设置图片 setimgpos({ width, height, top, left }) { width = width || this.data.img.width; height = height || this.data.img.height; top = top || this.data.img.top; left = left || this.data.img.left this.setdata({ img: { width, height, top, left } }) }, // 初始化图片位置大小 initialize() { const that = this; const ratio = that.data.width / that.data.height; this.getscreeninfo().then(res => { console.log(res); 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 }; console.log("interarea", state.interarea) that.getimginfo(that.data.src).then(imginfo => { const { width, height } = imginfo; const imgratio = width / height; state.originimg = { width, height }; that.setdata({ ratio: ratio }); if (imgratio > ratio) { that.setimgpos({ height: state.interarea.height, width: state.interarea.height * imgratio }) } else { that.setimgpos({ height: state.interarea.width / imgratio, width: state.interarea.width, }) }; state.firstscaleimg = { width: that.data.img.width, height: that.data.img.height } }); }); }, // 截图 getimg(){ const that = this; // console.log('dudu', that.data.img); const canvas = wx.createcanvascontext('imgcanvas', this); const {width,height,left,top} = that.data.img; const saveimg = ()=>{ console.log('开始截取图片'); wx.canvastotempfilepath({ canvasid:"imgcanvas", success(res){ // console.log(res); that.setdata({ hidden:true, // src:"" }); that.triggerevent("putimg", { imgurl: res.tempfilepath},{}); }, fail(err){ console.log(err) } },that) }; canvas.drawimage(that.data.src, left, top, width, height); canvas.draw(false, () => { saveimg() }, that) } } })
引用的时候除了宽高路径以外,需要wx:if;如果不卸载组件,会出现只能截一次的bug
因为小程序里面没有类似vue中catch的观测数据变化的东西,也不想为了个组件专门去搞一个,就用这种方式代替了,嘻嘻,好敷衍。。
总结
以上所述是小编给大家介绍的微信小程序canvas拖拽、截图组件功能,希望对大家有所帮助