小程序canvas 缩放/拖动/还原/封装和实例--开箱即用

news/2024/11/28 13:42:13/

小程序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

兄弟,如果帮到你,点个赞再走


http://www.ppmy.cn/news/3397.html

相关文章

现货贵金属白银用什么策略才能避免亏钱?

现货贵金属白银交易的策略多种多样&#xff0c;也千差万别。怎么才能让我们在交易的时候不亏钱&#xff0c;而且又能获取相当的利润呢&#xff1f;其实所谓的不会亏钱的情况&#xff0c;是不存在的。即便是跨过的大金融机构的智能交易&#xff0c;也不能避免地出现亏损&#xf…

python使用bs模块爬取小说数据

目录 一、BS模块介绍 二、分析页面架构 三、代码实现 四、结果展示 五、总结思路 一、BS模块介绍 Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱&#xff0c;通过解析文档为用户提供需要抓取的数据…

[附源码]计算机毕业设计的云网盘设计Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

技术分享 | 做为测试,那些必须掌握的测试技术体系

软件测试技术是软件开发过程中的一个重要组成部分&#xff0c;是贯穿整个软件开发生命周期、对软件产品&#xff08;包括阶段性产品&#xff09;进行验证和确认的活动过程。其目的是尽快尽早地发现在软件产品中所存在的各种问题&#xff0c;与用户需求、预先定义的不一致性。检…

2022,itbird的年终总结报告

最近公司要求个人在做年终总结了&#xff0c;趁着这个机会&#xff0c;也想对自己的2022年进行一下回顾总结&#xff0c;最重要的是&#xff0c;对2023的目标&#xff0c;可以有一个指引。 就从工作和生活两方面来讲吧。 1.工作 1.1 行业的状态 本人从事的是android开发工作…

树上操作【点分治】 - 原理 中心分解 【POJ No. 1741】 树上两点之间的路径数 Tree

树上操作【点分治】 - 原理 中心分解 分治法指将规模较大的问题分解为规模较小的子问题&#xff0c;解决各个子问题后合并得到原问题的答案。树上的分治算法分为点分治和边分治。 点分治经常用于带权树上的路径统计&#xff0c;本质上是一种带优化的暴力算法&#xff0c;并融…

【java】乐观锁和悲观锁、CAS和ABA问题

一、乐观锁VS悲观锁 1&#xff09;关于悲观锁 总是假设最坏的情况&#xff0c;每次去拿数据的时候都认为别人会修改&#xff0c;所以每次在拿数据的时候都会上锁&#xff0c;Java中synchronized和ReentrantLock以及Read-write locks等就是悲观锁思想的实现。 2&#xff09;关于…

1.专题 存储结构和逻辑结构

1. 存储结构 软件完成对象抽象&#xff0c;需要分配一定的内存资源。 根据对象对内存使用的特征&#xff0c;可以把数据存储的特征划分为&#xff1a; 顺序存储链接存储索引存储 和 散列存储 1.1 顺序存储 对象占用的资源表现为一段连续的内存存储 图示如下&#xff1a; M…