小程序canvas 缩放/拖动/还原/封装和实例
一、预览二、使用2.1 创建和配置方法 三、源码3.1 实例组件3.2 核心类
一、预览
之前写过web端的canvas 缩放/拖动/还原/封装和实例。最近小程序也需要用到,但凡是涉及小程序canvas还是比较多坑的,而且难用多了,于是在web的基础上重新写了小程序的相关功能。实现功能有:
支持双指、按钮缩放支持触摸拖动支持高清显示支持节流绘图支持还原、清除画布内置简化绘图方法效果如下:
二、使用
案例涉及到2个文件,一个是绘图组件canvas.vue,另一个是canvasDraw.js,核心是canvasDraw.js里定义的CanvasDraw类
2.1 创建和配置
小程序获取#canvas对象后就可以创建CanvasDraw实例了,创建实例时可以根据需要设置各种配置,其中drawCallBack是必须的,是用户自定义的绘图方法,程序会在this.canvasDraw.draw()后再回调drawCallBack()来实现用户的绘图。
拖动、缩放画布都会调用this.canvasDraw.draw()。
/** 初始化canvas */ initCanvas() { const query = wx.createSelectorQuery().in(this) query .select('#canvas') .fields({ node: true, size: true, rect: true }) .exec((res) => { const ele = res[0] this.canvasEle = ele // 配置项 const option = { ele: this.canvasEle, // canvas元素 drawCallBack: this.draw, // 必须:用户自定义绘图方法 scale: 1, // 当前缩放倍数 scaleStep: 0.1, // 缩放步长(按钮) touchScaleStep: 0.005, // 缩放步长(手势) maxScale: 2, // 缩放最大倍数(缩放比率倍数) minScale: 0.5, // 缩放最小倍数(缩放比率倍数) translate: { x: 0, y: 0 }, // 默认画布偏移 isThrottleDraw: true, // 是否开启节流绘图(建议开启,否则安卓调用频繁导致卡顿) throttleInterval: 20, // 节流绘图间隔,单位ms pixelRatio: wx.getSystemInfoSync().pixelRatio, // 像素比(高像素比可以解决高清屏幕模糊问题) } this.canvasDraw = new CanvasDraw(option) // 创建CanvasDraw实例后就可以使用实例的所有方法了 this.canvasDraw.draw() // 可以按实际需要调用绘图方法 }) },
方法
canvasDraw.draw() // 绘图canvasDraw.clear() // 清除画布canvasDraw.reset() // 重置画布(恢复到第一次绘制的状态)canvasDraw.zoomIn() // 中心放大canvasDraw.zoomOut() // 中心缩小canvasDraw.zoomTo(scale, zoomCenter) // 缩放到指定倍数(可指定缩放中心点)canvasDraw.destory() // 销毁canvasDraw.drawShape(opt) // 内置简化绘制多边形方法canvasDraw.drawLines(opt) // 内置简化绘制多线段方法canvasDraw.drawText(opt) // 内置简化绘制文字方法
三、源码
3.1 实例组件
canvas.vue
<template> <view class="canvas-wrap"> <canvas type="2d" id="canvas" class="canvas" disable-scroll="true" @touchstart="touchstart" @touchmove="touchmove" @touchend="touchend" @tap="tap" ></canvas> </view></template><script>import { CanvasDraw } from './canvasDraw'export default { data() { this.canvasDraw = null // 绘图对象 this.canvasEle = null // canvas元素对象 return {} }, created() {}, beforeDestroy() { /** 销毁对象 */ if (this.canvasDraw) { this.canvasDraw.destroy() this.canvasDraw = null } }, mounted() { /** 初始化 */ this.initCanvas() }, methods: { /** 初始化canvas */ initCanvas() { const query = wx.createSelectorQuery().in(this) query .select('#canvas') .fields({ node: true, size: true, rect: true }) .exec((res) => { const ele = res[0] this.canvasEle = ele // 配置项 const option = { ele: this.canvasEle, // canvas元素 drawCallBack: this.draw, // 必须:用户自定义绘图方法 scale: 1, // 当前缩放倍数 scaleStep: 0.1, // 缩放步长(按钮) touchScaleStep: 0.005, // 缩放步长(手势) maxScale: 2, // 缩放最大倍数(缩放比率倍数) minScale: 0.5, // 缩放最小倍数(缩放比率倍数) translate: { x: 0, y: 0 }, // 默认画布偏移 isThrottleDraw: true, // 是否开启节流绘图(建议开启,否则安卓调用频繁导致卡顿) throttleInterval: 20, // 节流绘图间隔,单位ms pixelRatio: wx.getSystemInfoSync().pixelRatio, // 像素比(高像素比可以解决高清屏幕模糊问题) } this.canvasDraw = new CanvasDraw(option) // 创建CanvasDraw实例后就可以使用实例的所有方法了 this.canvasDraw.draw() // 可以按实际需要调用绘图方法 }) }, /** 用户自定义绘图内容 */ draw() { // 默认绘图方式-圆形 const { ctx } = this.canvasDraw ctx.beginPath() ctx.strokeStyle = '#f00' ctx.arc(150, 150, 120, 0, 2 * Math.PI) ctx.stroke() // 组件方法-绘制多边形 const shapeOption = { points: [ { x: 127, y: 347 }, { x: 151, y: 304 }, { x: 173, y: 344 }, { x: 214, y: 337 }, { x: 184, y: 396 }, { x: 143, y: 430 }, { x: 102, y: 400 }, ], fillStyle: '#00f', } this.canvasDraw.drawShape(shapeOption) // 组件方法-绘制多线段 const linesOption = { points: [ { x: 98, y: 178 }, { x: 98, y: 212 }, { x: 157, y: 236 }, { x: 208, y: 203 }, { x: 210, y: 165 }, ], strokeStyle: '#0f0', } this.canvasDraw.drawLines(linesOption) // 组件方法-绘制文字 const textOption = { text: '组件方法-绘制文字', isCenter: true, point: { x: 150, y: 150 }, fillStyle: '#000', } this.canvasDraw.drawText(textOption) }, /** 中心放大 */ zoomIn() { this.canvasDraw.zoomIn() }, /** 中心缩小 */ zoomOut() { this.canvasDraw.zoomOut() }, /** 重置画布(回复初始效果) */ reset() { this.canvasDraw.reset() }, /** 事件绑定 */ tap(e) { const p = { x: (e.detail.x - this.canvasEle.left) / this.canvasDraw.scale, y: (e.detail.y - this.canvasEle.top) / this.canvasDraw.scale, } console.log('点击坐标:', p) }, touchstart(e) { this.canvasDraw.touchstart(e) }, touchmove(e) { this.canvasDraw.touchmove(e) }, touchend(e) { this.canvasDraw.touchend(e) }, },}</script><style scoped>.canvas-wrap { position: relative; flex: 1; width: 100%; height: 100%;}.canvas { width: 100%; flex: 1;}</style>
3.2 核心类
canvasDraw.js
/** * @Author: 大话主席 * @Description: 自定义小程序绘图类 *//** * 绘图类 * @param {object} option */export function CanvasDraw(option) { if (!option.ele) { console.error('canvas对象不存在') return } if (!option.drawCallBack) { console.error('缺少必须配置项:drawCallBack') return } const { ele } = option /** 外部可访问属性 */ this.canvasNode = ele.node // wx的canvas节点 this.canvasNode.width = ele.width // 设置canvas节点宽度 this.canvasNode.height = ele.height // 设置canvas节点高度 this.ctx = this.canvasNode.getContext('2d') this.zoomCenter = { x: ele.width / 2, y: ele.height / 2 } // 缩放中心点 this.touchMoveEvent = null // 触摸移动事件 /** 内部使用变量 */ let startPoint = { x: 0, y: 0 } // 拖动开始坐标 let startDistance = 0 // 拖动开始时距离(二指缩放) let curTranslate = {} // 当前偏移 let curScale = 1 // 当前缩放 let preScale = 1 // 上次缩放 let drawTimer = null // 绘图计时器,用于节流 let touchEndTimer = null // 触摸结束计时器,用于节流 let fingers = 1 // 手指触摸个数 /** * 根据像素比重设canvas尺寸 */ this.resetCanvasSize = () => { this.canvasNode.width = ele.width * this.pixelRatio this.canvasNode.height = ele.height * this.pixelRatio } /** * 初始化 */ this.init = () => { const optionCopy = JSON.parse(JSON.stringify(option)) this.scale = optionCopy.scale ?? 1 // 当前缩放倍数 this.scaleStep = optionCopy.scaleStep ?? 0.1 // 缩放步长(按钮) this.touchScaleStep = optionCopy.touchScaleStep ?? 0.005 // 缩放步长(手势) this.maxScale = optionCopy.maxScale ?? 2 // 缩放最大倍数(缩放比率倍数) this.minScale = optionCopy.minScale ?? 0.5 // 缩放最小倍数(缩放比率倍数) this.translate = optionCopy.translate ?? { x: 0, y: 0 } // 默认画布偏移 this.isThrottleDraw = optionCopy.isThrottleDraw ?? true // 是否开启节流绘图(建议开启,否则安卓调用频繁导致卡顿) this.throttleInterval = optionCopy.throttleInterval ?? 20 // 节流绘图间隔,单位ms this.pixelRatio = optionCopy.pixelRatio ?? 1 // 像素比(高像素比解决高清屏幕模糊问题) startPoint = { x: 0, y: 0 } // 拖动开始坐标 startDistance = 0 // 拖动开始时距离(二指缩放) curTranslate = JSON.parse(JSON.stringify(this.translate)) // 当前偏移 curScale = this.scale // 当前缩放 preScale = this.scale // 上次缩放 drawTimer = null // 绘图计时器,用于节流 fingers = 1 // 手指触摸个数 this.resetCanvasSize() } this.init() /** * 绘图(会进行缩放和位移) */ this.draw = () => { this.clear() this.ctx.translate(this.translate.x * this.pixelRatio, this.translate.y * this.pixelRatio) this.ctx.scale(this.scale * this.pixelRatio, this.scale * this.pixelRatio) // console.log('当前位移', this.translate.x, this.translate.y, '当前缩放倍率', this.scale) option.drawCallBack() drawTimer = null } /** * 设置默认值( */ this.setDefault = () => { curTranslate.x = this.translate.x curTranslate.y = this.translate.y curScale = this.scale preScale = this.scale } /** * 清除画布(重设canvas尺寸会清空地图并重置canvas内置的scale/translate等) */ this.clear = () => { this.resetCanvasSize() } /** * 绘制多边形 */ this.drawShape = (opt) => { this.ctx.beginPath() this.ctx.lineWidth = '1' this.ctx.fillStyle = opt.isSelect ? opt.HighlightfillStyle : opt.fillStyle this.ctx.strokeStyle = opt.HighlightStrokeStyle for (let i = 0; i < opt.points.length; i++) { const p = opt.points[i] if (i === 0) { this.ctx.moveTo(p.x, p.y) } else { this.ctx.lineTo(p.x, p.y) } } this.ctx.closePath() if (opt.isSelect) { this.ctx.stroke() } this.ctx.fill() } /** * 绘制多条线段 */ this.drawLines = (opt) => { this.ctx.beginPath() this.ctx.strokeStyle = opt.strokeStyle for (let i = 0; i < opt.points.length; i++) { const p = opt.points[i] if (i === 0) { this.ctx.moveTo(p.x, p.y) } else { this.ctx.lineTo(p.x, p.y) } } this.ctx.stroke() } /** * 绘制文字 */ this.drawText = (opt) => { this.ctx.fillStyle = opt.isSelect ? opt.HighlightfillStyle : opt.fillStyle if (opt.isCenter) { this.ctx.textAlign = 'center' this.ctx.textBaseline = 'middle' } this.ctx.fillText(opt.text, opt.point.x, opt.point.y) } /** * 重置画布(恢复到第一次绘制的状态) */ this.reset = () => { this.init() this.draw() } /** * 中心放大 */ this.zoomIn = () => { this.zoomTo(this.scale + this.scaleStep) } /** * 中心缩小 */ this.zoomOut = () => { this.zoomTo(this.scale - this.scaleStep) } /** * 缩放到指定倍数 * @param {number} scale 缩放大小 * @param {object} zoomCenter 缩放中心点(可选 */ this.zoomTo = (scale, zoomCenter0) => { // console.log('缩放到:', scale, '缩放中心点:', zoomCenter0) this.scale = scale this.scale = this.scale > this.maxScale ? this.maxScale : this.scale this.scale = this.scale < this.minScale ? this.minScale : this.scale const zoomCenter = zoomCenter0 || this.zoomCenter this.translate.x = zoomCenter.x - ((zoomCenter.x - this.translate.x) * this.scale) / preScale this.translate.y = zoomCenter.y - ((zoomCenter.y - this.translate.y) * this.scale) / preScale this.draw() preScale = this.scale curTranslate.x = this.translate.x curTranslate.y = this.translate.y } /** * 触摸开始 */ this.touchstart = (e) => { fingers = e.touches.length if (fingers > 2) return this.setDefault() // 单指 if (fingers === 1) { startPoint.x = e.touches[0].x startPoint.y = e.touches[0].y } else if (fingers === 2) { startDistance = this.get2PointsDistance(e) } } /** * 触摸移动 */ this.touchmove = (e) => { if (fingers > 2) return if (this.isThrottleDraw) { if (drawTimer) return this.touchMoveEvent = e drawTimer = setTimeout(this.touchmoveSelf, this.throttleInterval) } else { this.touchMoveEvent = e this.touchmoveSelf() } } /** * 触摸移动实际执行 */ this.touchmoveSelf = () => { const e = this.touchMoveEvent // 单指移动 if (fingers === 1) { this.translate.x = curTranslate.x + (e.touches[0].x - startPoint.x) this.translate.y = curTranslate.y + (e.touches[0].y - startPoint.y) this.draw() } else if (fingers === 2 && e.touches.length === 2) { // 双指缩放 const newDistance = this.get2PointsDistance(e) const distanceDiff = newDistance - startDistance const zoomCenter = { x: (e.touches[0].x + e.touches[1].x) / 2, y: (e.touches[0].y + e.touches[1].y) / 2, } this.zoomTo(curScale + this.touchScaleStep * distanceDiff, zoomCenter) } else { drawTimer = null } } /** * 触摸结束 */ this.touchend = () => { if (this.isThrottleDraw) { touchEndTimer = setTimeout(this.setDefault, this.throttleInterval) } else { this.setDefault() } } /** * 销毁 */ this.destroy = () => { clearTimeout(drawTimer) clearTimeout(touchEndTimer) drawTimer = null touchEndTimer = null this.canvasNode = null this.ctx = null this.touchMoveEvent = null option.drawCallBack = null } /** * 获取2触摸点距离 * @param {object} e 触摸对象 * @returns 2触摸点距离 */ this.get2PointsDistance = (e) => { if (e.touches.length < 2) return 0 const xMove = e.touches[1].x - e.touches[0].x const yMove = e.touches[1].y - e.touches[0].y return Math.sqrt(xMove * xMove + yMove * yMove) }}export default CanvasDraw
兄弟,如果帮到你,点个赞再走