小程序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, // 节流绘图间隔,单位mspixelRatio: 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"><canvastype="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, // 节流绘图间隔,单位mspixelRatio: wx.getSystemInfoSync().pixelRatio, // 像素比(高像素比可以解决高清屏幕模糊问题)}this.canvasDraw = new CanvasDraw(option) // 创建CanvasDraw实例后就可以使用实例的所有方法了this.canvasDraw.draw() // 可以按实际需要调用绘图方法})},/** 用户自定义绘图内容 */draw() {// 默认绘图方式-圆形const { ctx } = this.canvasDrawctx.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.pixelRatiothis.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 // 节流绘图间隔,单位msthis.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.xcurTranslate.y = this.translate.ycurScale = this.scalepreScale = 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.fillStylethis.ctx.strokeStyle = opt.HighlightStrokeStylefor (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.strokeStylefor (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.fillStyleif (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 = scalethis.scale = this.scale > this.maxScale ? this.maxScale : this.scalethis.scale = this.scale < this.minScale ? this.minScale : this.scaleconst zoomCenter = zoomCenter0 || this.zoomCenterthis.translate.x = zoomCenter.x - ((zoomCenter.x - this.translate.x) * this.scale) / preScalethis.translate.y = zoomCenter.y - ((zoomCenter.y - this.translate.y) * this.scale) / preScalethis.draw()preScale = this.scalecurTranslate.x = this.translate.xcurTranslate.y = this.translate.y}/*** 触摸开始*/this.touchstart = (e) => {fingers = e.touches.lengthif (fingers > 2) returnthis.setDefault()// 单指if (fingers === 1) {startPoint.x = e.touches[0].xstartPoint.y = e.touches[0].y} else if (fingers === 2) {startDistance = this.get2PointsDistance(e)}}/*** 触摸移动*/this.touchmove = (e) => {if (fingers > 2) returnif (this.isThrottleDraw) {if (drawTimer) returnthis.touchMoveEvent = edrawTimer = setTimeout(this.touchmoveSelf, this.throttleInterval)} else {this.touchMoveEvent = ethis.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 - startDistanceconst 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 = nulltouchEndTimer = nullthis.canvasNode = nullthis.ctx = nullthis.touchMoveEvent = nulloption.drawCallBack = null}/*** 获取2触摸点距离* @param {object} e 触摸对象* @returns 2触摸点距离*/this.get2PointsDistance = (e) => {if (e.touches.length < 2) return 0const xMove = e.touches[1].x - e.touches[0].xconst yMove = e.touches[1].y - e.touches[0].yreturn Math.sqrt(xMove * xMove + yMove * yMove)}
}export default CanvasDraw
兄弟,如果帮到你,点个赞再走