一、预览
之前写过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: 卢景滔
* @Date: 2022-05-23 18:54:29
* @LastEditTime: 2022-05-24 12:46:55
* @LastEditors: 卢景滔
* @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
兄弟,如果帮到你,点个赞再走