Small program canvas zoom/drag/restore/encapsulation and instance--out of the box

1. Preview

I have written canvas zoom/drag/restore/encapsulation and instances on the web before . Recently, small programs also need to be used, but the small program canvas is still more difficult to use, and it is much more difficult to use, so the relevant functions of the small program are rewritten on the basis of the web (refer to Fabric.js). The implementation functions are:

  • Support two-finger, button zoom
  • Support one-finger/two-finger touch drag
  • Support HD display
  • Support for throttling drawing
  • Support reset, clear canvas
  • Support element selection/deletion
  • built-in drawing methods

The effect is as follows:
insert image description here

Two, use

The case involves 3 files:

  • Graph instance component canvas.vue
  • The core CanvasDraw class canvasDraw.js
  • Tool method utils.js

2.1 Creation and configuration

After the applet obtains the #canvas object, it can create a CanvasDraw instance. When creating an instance, you can set various configurations according to your needs. Among them, drawCallBack is a drawing callback. The program will call back drawCallBack() after this.canvasDraw.draw() to realize user For drawing, users can use this.canvasDraw.ctx to use native canvas drawing.

    /** 初始化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() // 可以按实际需要调用绘图方法
        })
    },

method

canvasDraw.clear() // 清除画布
canvasDraw.clearSelect() // 清除选中
canvasDraw.destory() // 销毁
canvasDraw.draw() // 绘图
canvasDraw.drawLines(opt) // 内置简化绘制多线段方法
canvasDraw.drawPoint(opt) // 内置简化绘制点/图形点
canvasDraw.drawShape(opt) // 内置简化绘制多边形方法
canvasDraw.drawText(opt) // 内置简化绘制文字方法
canvasDraw.getChild(id) // 获取子元素
canvasDraw.getPoint(e) // 获取触摸对象的canvas坐标
canvasDraw.getSelect() // 获取选中元素
canvasDraw.on(type,callBack) // 绑定事件
canvasDraw.off(type,callBack) // 解绑事件
canvasDraw.removeChild(id) // 移除子元素
canvasDraw.reset() // 重置画布(恢复到第一次绘制的状态)
canvasDraw.zoomIn() // 中心放大
canvasDraw.zoomOut() // 中心缩小
canvasDraw.zoomTo(scale, zoomCenter) // 缩放到指定倍数(可指定缩放中心点)

3. Source code

3.1 Instance components

canvas.view

<template>
  <view class="container">
    <view class="canvas-wrap">
      <canvas
        type="2d"
        id="canvas"
        class="canvas"
        disable-scroll="true"
        @touchstart="touchstart"
        @touchmove="touchmove"
        @touchend="touchend"
      ></canvas>
    </view>
    <view class="buttons">
      <button @click="zoomIn">放大</button>
      <button @click="zoomOut">缩小</button>
      <button @click="reset">重置</button>
      <button @click="clear">清空</button>
    </view>
    <view class="buttons">
      <button @click="addShape" :disabled="isDrawing">多边形</button>
      <button @click="addLines" :disabled="isDrawing">多线段</button>
      <button @click="addPoint" :disabled="isDrawing"></button>
      <button @click="addImagePoint" :disabled="isDrawing">图片点</button>
      <button @click="addText" :disabled="isDrawing">文字</button>
    </view>
    <view class="buttons">
      <button @click="handDraw">{
   
   { isDrawing ? '关闭' : '开启' }}手绘矩形</button>
    </view>
  </view>
</template>
<script>
import {
      
       CanvasDraw } from '../../../components/custom-floor-map/canvasDraw'

export default {
      
      
  data() {
      
      
    this.canvasDraw = null // 绘图对象
    this.canvasEle = null // canvas元素对象
    this.imgs = {
      
      
      star: '../../../static/images/starActive.png',
      delete: '../../../static/images/cancel.png',
    }
    this.startPoint = null // 手绘起点
    this.createId = null // 手绘id
    this.fingers = 1 // 手指数量

    return {
      
      
      isDrawing: false, // 是否正在绘图
    }
  },

  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, // 像素比(高像素比可以解决高清屏幕模糊问题)
            controls: {
      
      
              delete: {
      
      
                radius: 20,
                fill: '#f00',
                customDraw: this.drawDeleteControl,
              },
            },
          }
          this.canvasDraw = new CanvasDraw(option) // 创建CanvasDraw实例后就可以使用实例的所有方法了
          this.addEvents() // 添加事件监听
          this.canvasDraw.draw() // 可以按实际需要调用绘图方法
          console.log('this.canvasDraw', this.canvasDraw)
        })
    },

    /** 绑定组件内置事件 */
    addEvents() {
      
      
      this.canvasDraw.on('selection:updated', this.onSelectionUpdated)
      this.canvasDraw.on('selection:cleared', this.onSelectionCleared)
      this.canvasDraw.on('touchstart', this.onTouchstart)
      this.canvasDraw.on('touchmove', this.onTouchmove)
      this.canvasDraw.on('touchend', this.onTouchend)
      this.canvasDraw.on('tap', this.onTap)
      this.canvasDraw.on('deleteControl:tap', this.onDeleteControl)
    },

    /** 用户自定义绘图内容 */
    draw() {
      
      
      // 1.默认绘图方式-圆形
      // const { ctx } = this.canvasDraw
      // ctx.beginPath()
      // ctx.strokeStyle = '#000'
      // ctx.arc(50, 50, 50, 0, 2 * Math.PI)
      // ctx.stroke()
      // ctx.closePath()
    },

    /** 中心放大 */
    zoomIn() {
      
      
      this.canvasDraw.zoomIn()
    },

    /** 中心缩小 */
    zoomOut() {
      
      
      this.canvasDraw.zoomOut()
    },

    /** 重置画布(回复初始效果) */
    reset() {
      
      
      this.canvasDraw.reset()
    },

    /** 清空画布 */
    clear() {
      
      
      this.canvasDraw.clear()
    },

    /** 组件方法-绘制多边形 */
    addShape() {
      
      
      const opt = {
      
      
        points: [
          {
      
       x: 148, y: 194 },
          {
      
       x: 196, y: 191 },
          {
      
       x: 215, y: 244 },
          {
      
       x: 125, y: 249 },
        ],
        style: {
      
       strokeWidth: 2, stroke: '#000', lineDash: [2, 2], fill: 'red' },
      }
      this.canvasDraw.drawShape(opt)
    },

    /** 组件方法-绘制多线段 */
    addLines() {
      
      
      const opt = {
      
      
        points: [
          {
      
       x: 53, y: 314 },
          {
      
       x: 116, y: 283 },
          {
      
       x: 166, y: 314 },
          {
      
       x: 224, y: 283 },
          {
      
       x: 262, y: 314 },
        ],
        style: {
      
       strokeWidth: 2, stroke: '#000', lineDash: [2, 2] },
        angle: 45,
      }
      this.canvasDraw.drawLines(opt)
    },

    /** 组件方法-绘制文字 */
    addText() {
      
      
      const opt = {
      
      
        text: '组件方法-绘制文字',
        points: [{
      
       x: 175, y: 150 }],
        style: {
      
      
          fill: '#000',
          textAlign: 'center',
          textBaseline: 'middle',
        },
      }
      this.canvasDraw.drawText(opt)
    },

    /** 组件方法-绘制点 */
    addPoint() {
      
      
      const opt = {
      
      
        points: [{
      
       x: 150, y: 50 }],
        style: {
      
       radius: 20, strokeWidth: 2, stroke: '#00f', lineDash: [2, 2], fill: '#0f0' },
      }
      this.canvasDraw.drawPoint(opt)
    },

    /** 组件方法-绘制图片点 */
    addImagePoint() {
      
      
      const opt = {
      
      
        points: [{
      
       x: 300, y: 50 }],
        style: {
      
       radius: 40, img: this.imgs.star },
        angle: 45,
      }
      this.canvasDraw.drawPoint(opt)
    },

    /** 用户手绘矩形 */
    handDraw() {
      
      
      // 如果是手绘则禁止拖拽画布,否则启动拖拽画布
      this.isDrawing = !this.isDrawing
      this.canvasDraw.canDragCanvas = !this.isDrawing
    },

    /** 组件内置事件 */
    onSelectionUpdated(item) {
      
      
      if (this.isDrawing) return
      console.log('选中元素:', item)
      item.style.fill = 'green'
      item.controlsVis = {
      
       delete: true }
      item.zIndex = 1
      this.canvasDraw.draw()
    },
    onSelectionCleared(item) {
      
      
      if (this.isDrawing) return
      console.log('取消选中:', item)
      if (!item) return
      item.style.fill = 'red'
      item.controlsVis = {
      
       delete: false }
      item.zIndex = 0
      this.canvasDraw.draw()
    },
    onTouchstart(e) {
      
      
      console.log('触摸开始:', e)
      this.startPoint = e.point
      this.createId = `user_${ 
        new Date().getTime()}`
      this.fingers = e.event.touches.length
    },
    onTouchmove(e) {
      
      
      // console.log('触摸移动:', e)
      // 如果是绘制状态,触摸移动则进行矩形绘制
      if (this.fingers !== 1 || !this.isDrawing) return
      const tsPoint = this.startPoint
      const tmPoint = e.point
      // 两点距离小于5,不进行绘制
      if (Math.abs(tmPoint.x - tsPoint.x) <= 5 || Math.abs(tmPoint.y - tsPoint.y) <= 5) return
      // 先移除,再绘制
      this.canvasDraw.removeChild(this.createId)
      this.canvasDraw.draw()
      const opt = {
      
      
        id: this.createId,
        points: [tsPoint, {
      
       x: tmPoint.x, y: tsPoint.y }, tmPoint, {
      
       x: tsPoint.x, y: tmPoint.y }],
        style: {
      
       strokeWidth: 2, stroke: 'rgba(0,0,0,.4)', fill: 'rgba(255,0,0,.4)' },
      }
      this.canvasDraw.drawShape(opt)
    },
    onTouchend(e) {
      
      
      console.log('触摸结束:', e)
      // 如果是绘制状态,设置最后一个绘制的为选中状态,及显示删除控制点
      if (!this.isDrawing) return
      this.canvasDraw.children.forEach((item) => {
      
      
        if (item.id === this.createId) {
      
      
          item.style.stroke = 'blue'
          item.isSelect = true
          item.controlsVis = {
      
       delete: true }
        } else {
      
      
          item.style.stroke = 'black'
          item.isSelect = false
          item.controlsVis = {
      
       delete: false }
        }
      })
      this.canvasDraw.draw()
    },
    onTap(e) {
      
      
      console.log('点击坐标:', e.point)
      console.log('所有canvas子对象:', this.canvasDraw.children)
    },
    onDeleteControl(e) {
      
      
      console.log('点击删除控制点', e)
      this.canvasDraw.removeChild(e.id)
      this.canvasDraw.draw()
    },
    // 自定义绘制删除控制点
    drawDeleteControl(opt) {
      
      
      this.canvasDraw.drawPoint(
        {
      
      
          id: 'delete',
          points: opt.points,
          style: {
      
      
            img: this.imgs.delete,
            radius: 20,
          },
        },
        false
      )
    },

    /** canvas事件绑定 */
    touchstart(e) {
      
      
      this.canvasDraw.touchstart(e)
    },
    touchmove(e) {
      
      
      this.canvasDraw.touchmove(e)
    },
    touchend(e) {
      
      
      this.canvasDraw.touchend(e)
    },
  },
}
</script>
<style>
page {
      
      
  background: #f2f2f2;
  height: 100vh;
  overflow: hidden;
  display: flex;
}
.container {
      
      
  display: flex;
  flex: 1;
  flex-direction: column;
  height: 100%;
}
.canvas-wrap {
      
      
  display: flex;
  margin: 10px;
  height: 50%;
}
.canvas {
      
      
  flex: 1;
  width: 100%;
  height: 100%;
  background: #fff;
}
.buttons {
      
      
  display: flex;
  justify-content: space-around;
  margin: 10px;
}
</style>

3.2 Core classes

canvasDraw.js

import {
    
     isInPolygon, isInCircle, getBoundingBox, get2PointsDistance, getOCoords, isSameDirection, getPolygonCenterPoint } from './utils'

/**
 * 绘图类
 * @param {object} option
 */
export function CanvasDraw(option) {
    
    
  if (!option.ele) {
    
    
    console.error('canvas对象不存在')
    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.children = [] // 子对象
  this.canDragCanvas = true // 能拖动画布

  /** 内部使用变量 */
  let startDistance = 0 // 拖动开始时距离(二指缩放)
  let preScale = 1 // 上次缩放
  let touchMoveTimer = null // 触摸移动计时器,用于节流
  let touchEndTimer = null // 触摸结束计时器,用于节流
  let fingers = 1 // 手指触摸个数
  const events = {
    
     'selection:updated': [], 'selection:cleared': [], touchstart: [], touchmove: [], touchend: [], tap: [], 'deleteControl:tap': [] } // 事件集合
  let curControlKey = null // 当前选中控件key
  let preTouches = [] // 上次触摸点
  let imgCache = {
    
    } // 图片缓存,防止拖动的时候反复加载图片造成闪烁

  /** 初始化 */
  const 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 // 像素比(高像素比解决高清屏幕模糊问题)
    // 全局控制器设置,目前只有delete,属性为radius: 半径, fill: 默认绘制颜色,customDraw: 自定义绘制函数,若存在则覆盖默认绘制
    // 由于optionCopy不能复制customDraw函数,所以这里只能用option
    this.controls = option.controls ?? {
    
    
      delete: {
    
     radius: 10, fill: '#f00', customDraw: null },
    }
    // this.controls.delete.customDraw = option.controls.delete.customDraw
    // 全局控制器可见性,目前只做了delete;元素本身也可以单独设置
    this.controlsVis = optionCopy.controlsVis ?? {
    
    
      delete: false,
    }

    startDistance = 0 // 拖动开始时距离(二指缩放)
    preScale = this.scale // 上次缩放
    touchMoveTimer = null
    touchEndTimer = null
    fingers = 1 // 手指触摸个数
  }

  init()

  /** 绘图(会进行缩放和位移) */
  this.draw = () => {
    
    
    clear()
    drawChildren()
    option.drawCallBack()
  }

  /** 私有清除画布(重设canvas尺寸会清空地图并重置canvas内置的scale/translate等) */
  const clear = () => {
    
    
    this.canvasNode.width = ele.width * this.pixelRatio
    this.canvasNode.height = ele.height * this.pixelRatio
    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)
  }

  /** 清除画布,并清空子对象 */
  this.clear = () => {
    
    
    clear()
    this.children.length = 0
  }

  /**
   * 绘制多边形
   * @param {boolean} isAddChild 是否添加到canvas子对象
   * @param {object} opt 参数{ points:array, style:{strokeWidth:number, stroke:string, fill:string, lineDash:array} }
   */
  this.drawShape = (opt, isAddChild = true) => {
    
    
    if (opt.points.length < 3) return
    const tempObj = {
    
     type: 'Shape', angle: opt.angle, points: opt.points }
    this.rotateDraw(tempObj, () => {
    
    
      this.ctx.beginPath()
      this.ctx.lineWidth = opt.style.strokeWidth ?? 1
      this.ctx.fillStyle = opt.style.fill
      this.ctx.strokeStyle = opt.style.stroke ?? '#000'

      // 设置虚线
      if (opt.style.stroke && opt.style.lineDash && opt.style.lineDash.length > 0) {
    
    
        this.ctx.setLineDash(opt.style.lineDash)
      }

      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.style.stroke) {
    
    
        this.ctx.stroke()
        this.ctx.setLineDash([])
      }
      if (opt.style.fill) {
    
    
        this.ctx.fill()
      }
    })

    if (isAddChild) {
    
    
      return this.addChild('Shape', opt)
    }
  }

  /** 绘制多条线段 */
  this.drawLines = (opt, isAddChild = true) => {
    
    
    if (opt.points.length < 2) return
    const tempObj = {
    
     type: 'Lines', angle: opt.angle, points: opt.points }
    this.rotateDraw(tempObj, () => {
    
    
      this.ctx.beginPath()
      this.ctx.lineWidth = opt.style.strokeWidth ?? 1
      this.ctx.strokeStyle = opt.style.stroke ?? '#000'

      // 设置虚线
      if (opt.style.stroke && opt.style.lineDash && opt.style.lineDash.length > 0) {
    
    
        this.ctx.setLineDash(opt.style.lineDash)
      }

      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)
        }
      }
      if (opt.style.stroke) {
    
    
        this.ctx.stroke()
        this.ctx.setLineDash([])
      }
    })
    if (isAddChild) {
    
    
      return this.addChild('Lines', opt)
    }
  }

  /** 绘制文字 */
  this.drawText = (opt, isAddChild = true) => {
    
    
    const p = opt.points[0]
    if (!p) return
    const tempObj = {
    
     type: 'Text', angle: opt.angle, points: opt.points }
    this.rotateDraw(tempObj, () => {
    
    
      this.ctx.fillStyle = opt.style.fill
      this.ctx.textAlign = opt.style.textAlign ?? 'center'
      this.ctx.textBaseline = opt.style.textBaseline ?? 'middle'
      this.ctx.fillText(opt.text, p.x, p.y)
    })
    if (isAddChild) {
    
    
      return this.addChild('Text', opt)
    }
  }

  /** 绘制点图片 */
  const drawPointImg = (img, p, opt) => {
    
    
    this.ctx.drawImage(img, p.x - opt.style.radius, p.y - opt.style.radius, opt.style.radius * 2, opt.style.radius * 2)
  }

  /** 绘制点填充 */
  const drawPointFill = (p, opt) => {
    
    
    this.ctx.beginPath()
    this.ctx.lineWidth = opt.style.strokeWidth ?? 1
    this.ctx.fillStyle = opt.style.fill
    this.ctx.strokeStyle = opt.style.stroke ?? '#000'

    // 设置虚线
    if (opt.style.stroke && opt.style.lineDash && opt.style.lineDash.length > 0) {
    
    
      this.ctx.setLineDash(opt.style.lineDash)
    }

    this.ctx.arc(p.x, p.y, opt.style.radius, 0, 2 * Math.PI)

    this.ctx.closePath()
    if (opt.style.stroke) {
    
    
      this.ctx.stroke()
      this.ctx.setLineDash([])
    }
    if (opt.style.fill) {
    
    
      this.ctx.fill()
    }
  }

  /** 绘制点 */
  this.drawPoint = (opt, isAddChild = true) => {
    
    
    const p = opt.points[0]
    if (!p) return
    const tempObj = {
    
     type: 'Point', angle: opt.angle, points: opt.points }

    // 图片点
    if (opt.style.img) {
    
    
      let img = imgCache[opt.style.img]
      if (!img) {
    
    
        img = this.canvasNode.createImage()
        img.src = opt.style.img
        img.onload = () => {
    
    
          imgCache[opt.style.img] = img
          this.rotateDraw(tempObj, drawPointImg.bind(this, img, p, opt))
        }
      } else {
    
    
        this.rotateDraw(tempObj, drawPointImg.bind(this, img, p, opt))
      }
    }
    // 绘画点
    else {
    
    
      this.rotateDraw(tempObj, drawPointFill.bind(this, p, opt))
    }

    if (isAddChild) {
    
    
      return this.addChild('Point', opt)
    }
  }

  /** 旋转绘制对象  */
  this.rotateDraw = (object, callBack) => {
    
    
    const angle = object.angle ?? 0
    const centerPoint = this.getObjectCenterPoint(object)
    this.ctx.save()
    this.ctx.translate(centerPoint.x, centerPoint.y)
    this.ctx.rotate((angle * Math.PI) / -180)
    this.ctx.translate(-centerPoint.x, -centerPoint.y)
    callBack()
    this.ctx.restore()
  }

  /** 获取子对象中心点 */
  this.getObjectCenterPoint = (object) => {
    
    
    switch (object.type) {
    
    
      case 'Point':
        return object.points[0]
      default:
        return getPolygonCenterPoint(object.points)
    }
  }

  /** 获取点击事件的画布坐标 */
  this.getPoint = (e) => {
    
    
    const t = getTouchPont(e, 0)
    return {
    
    
      x: (t.x - this.translate.x) / this.scale,
      y: (t.y - this.translate.y) / this.scale,
    }
  }

  /** 获取点击事件的屏幕坐标 */
  this.getScreenPoint = (e) => {
    
    
    const t = getTouchPont(e, 0)
    return {
    
    
      x: t.x,
      y: t.y,
    }
  }

  /** 获取当前选中的元素 */
  this.getSelect = () => {
    
    
    return this.children.find((item) => item.isSelect)
  }

  /** 清除选中 */
  this.clearSelect = () => {
    
    
    this.children.forEach((item) => {
    
    
      item.isSelect = false
    })
  }

  /** 添加子对象 */
  this.addChild = (type, opt) => {
    
    
    const aCoords = getBoundingBox(opt.points)
    const cv = opt.controlsVis ?? this.controlsVis
    const obj = {
    
    
      id: opt.id ?? `c_${
      
      new Date().getTime()}`,
      zIndex: opt.zIndex ?? 0,
      angle: opt.angle ?? 0,
      isSelect: opt.isSelect ?? false,
      points: JSON.parse(JSON.stringify(opt.points)),
      style: opt.style ?? {
    
    },
      text: opt.text,
      type,
      controlsVis: cv,
      aCoords, // 多边形的包围盒
      oCoords: getOCoords(aCoords, cv, this.controls), // 控制器
    }
    // 如果已存在,则更新,否则添加
    const oldOjb = this.getChild(obj.id)
    if (oldOjb) {
    
    
      oldOjb.zIndex = obj.zIndex
      oldOjb.angle = obj.angle
      oldOjb.isSelect = obj.isSelect
      oldOjb.points = obj.points
      oldOjb.style = obj.style
      oldOjb.text = obj.text
      oldOjb.type = obj.type
      oldOjb.controlsVis = obj.controlsVis
      oldOjb.aCoords = obj.aCoords
      oldOjb.oCoords = obj.oCoords
    } else {
    
    
      this.children.push(obj)
    }
    addControls(obj)
    return obj
  }

  /** 移除子对象 */
  this.removeChild = (id) => {
    
    
    const index = this.children.findIndex((item) => item.id === id)
    if (index !== -1) {
    
    
      this.children.splice(index, 1)
    }
  }

  /** 获取子对象 */
  this.getChild = (id) => {
    
    
    return this.children.find((item) => item.id === id)
  }

  /** 重置画布(恢复到第一次绘制的状态) */
  this.reset = () => {
    
    
    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) => {
    
    
    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
  }

  /** tap事件 */
  this.tap = (e) => {
    
    
    if (fingers !== 1) return
    const ep = e.changedTouches[0]
    const sp = preTouches[0]
    if (!isSaveTouchPoint(sp, ep)) return
    if (curControlKey) {
    
    
      triggerControl(curControlKey)
      return
    }
    const p = this.getPoint(e)
    triggerEvent('tap', {
    
     point: p, event: e })
    for (let i = this.children.length - 1; i >= 0; i--) {
    
    
      const item = this.children[i]
      // 这里只做了点击时否在多边形或圆形的判断,后期可以扩展
      if (isInPolygon(p, item.points, item.angle) || isInCircle(p, item.points[0], item.style.radius)) {
    
    
        item.isSelect = true
        triggerEvent('selection:updated', item)
        return item
      }
    }
  }

  /** 触摸开始 */
  this.touchstart = (e) => {
    
    
    // console.log('touchstart', e)
    fingers = e.touches.length
    if (fingers > 2) return
    preTouches = JSON.parse(JSON.stringify(e.touches))

    // 单指
    if (fingers === 1) {
    
    
      // 如果是触摸了控制器
      curControlKey = getControlByPoint(this.getPoint(e))
      if (curControlKey) {
    
    
        return
      }
      triggerEvent('selection:cleared', this.getSelect())
      this.clearSelect()
      triggerEvent('touchstart', {
    
     point: this.getPoint(e), event: e })
    } else if (fingers === 2) {
    
    
      startDistance = get2PointsDistance(e)
    }
  }

  /** 触摸移动 */
  this.touchmove = (e) => {
    
    
    // console.log('touchmove', e)
    if (fingers > 2 || isSaveTouchPoint(preTouches[0], e.changedTouches[0])) return
    if (this.isThrottleDraw) {
    
    
      if (touchMoveTimer) return
      // this.touchMoveEvent = e
      touchMoveTimer = setTimeout(this.touchmoveSelf.bind(this, e), this.throttleInterval)
    } else {
    
    
      // this.touchMoveEvent = e
      this.touchmoveSelf(e)
    }
  }

  /** 触摸移动实际执行 */
  this.touchmoveSelf = (e) => {
    
    
    // const e = this.touchMoveEvent
    // 单指移动
    if (fingers === 1) {
    
    
      if (!curControlKey) {
    
    
        triggerEvent('touchmove', {
    
     point: this.getPoint(e), event: e })
        drag(e)
      }
    } else if (fingers === 2 && e.touches.length === 2 && preTouches.length === 2) {
    
    
      // 如果移动方向一致则拖动画布否则缩放
      if (isSameDirection(preTouches[0], getTouchPont(e, 0), preTouches[1], getTouchPont(e, 1))) {
    
    
        drag(e)
      } else {
    
    
        // 双指缩放
        const endDistance = get2PointsDistance(e)
        const distanceDiff = endDistance - startDistance
        startDistance = endDistance
        const zoomCenter = {
    
    
          x: (getTouchPont(e, 0).x + getTouchPont(e, 1).x) / 2,
          y: (getTouchPont(e, 0).y + getTouchPont(e, 1).y) / 2,
        }
        this.zoomTo(preScale + this.touchScaleStep * distanceDiff, zoomCenter)
      }
    }
    preTouches = e.touches
    // preTouches = JSON.parse(JSON.stringify(e.touches))
    touchMoveTimer = null
  }

  /** 触摸结束 */
  this.touchend = (e) => {
    
    
    // console.log('touchend', e)
    if (this.isThrottleDraw) {
    
    
      touchEndTimer = setTimeout(this.touchendSelf.bind(this, e), this.throttleInterval)
    } else {
    
    
      this.touchendSelf(e)
    }
  }

  /** 触摸结束实际执行 */
  this.touchendSelf = (e) => {
    
    
    // console.log('touchend', e)
    this.tap(e)
    curControlKey = null
    triggerEvent('touchend', {
    
     point: this.getPoint(e), event: e })
    touchEndTimer = null
  }

  /** 绑定事件 */
  this.on = (type, callBack) => {
    
    
    if (!events[type]) return
    events[type].push(callBack)
  }

  /** 解绑事件 */
  this.off = (type, callBack) => {
    
    
    if (!events[type]) return
    const index = events[type].indexOf(callBack)
    if (index !== -1) {
    
    
      events[type].splice(index, 1)
    }
  }

  /** 销毁 */
  this.destroy = () => {
    
    
    resetEvents()
    clearTimeout(touchMoveTimer)
    clearTimeout(touchEndTimer)
    touchMoveTimer = null
    touchEndTimer = null
    imgCache = null
    this.canvasNode = null
    this.children = null
    this.ctx = null
    // this.touchMoveEvent = null
    option.drawCallBack = null
  }

  /** 绘制所有子对象 */
  const drawChildren = () => {
    
    
    this.children.sort((a, b) => a.zIndex - b.zIndex)
    this.children.forEach((item) => {
    
    
      const opt = {
    
    
        id: item.id,
        zIndex: item.zIndex,
        angle: item.angle,
        isSelect: item.isSelect,
        points: item.points,
        style: item.style,
        text: item.text,
        type: item.type,
        controlsVis: item.controlsVis,
      }
      this[`draw${
      
      item.type}`](opt)
    })
  }

  /**
   * 拖动画布
   * @param {event} e 鼠标事件
   */
  const drag = (e) => {
    
    
    if (!this.canDragCanvas) return
    this.translate.x += getTouchPont(e, 0).x - preTouches[0].x
    this.translate.y += getTouchPont(e, 0).y - preTouches[0].y
    this.draw()
  }

  /**
   * 获取点击的控制器
   * @param {Point} p 点坐标
   * @param {object} obj 画布元素
   * @return {string} 控制器名称
   */
  const getControlByPoint = (p) => {
    
    
    const obj = this.getSelect()
    if (!obj) return
    const controls = obj.oCoords
    const keys = Object.keys(controls)
    for (let i = 0; i < keys.length; i++) {
    
    
      const key = keys[i]
      if (controls[key].vis) {
    
    
        const control = controls[key]
        if (isInCircle(p, control.point, control.radius)) {
    
    
          return key
        }
      }
    }
  }

  /** 添加控制器 */
  const addControls = (obj) => {
    
    
    Object.keys(obj.oCoords).forEach((key) => {
    
    
      const item = obj.oCoords[key]
      if (!item.vis) return
      if (item.customDraw) {
    
    
        item.customDraw({
    
     points: [obj.oCoords[key].point] })
        return
      }
      this.drawPoint(
        {
    
    
          id: key,
          points: [obj.oCoords[key].point],
          style: {
    
    
            fill: this.controls[key].fill,
            radius: this.controls[key].radius,
          },
        },
        false
      )
    })
  }

  /** 触发控制器 */
  const triggerControl = (key) => {
    
    
    switch (key) {
    
    
      case 'delete':
        triggerEvent('deleteControl:tap', this.getSelect())
        break

      default:
        break
    }
  }

  /** 触发某类事件 */
  const triggerEvent = (type, param) => {
    
    
    events[type].forEach((callBack) => {
    
    
      callBack(param)
    })
  }

  /** 重置事件 */
  const resetEvents = () => {
    
    
    Object.keys(events).forEach((key) => {
    
    
      events[key] = []
    })
  }

  /** 是否相同点击坐标 */
  const isSaveTouchPoint = (sp, ep) => {
    
    
    return Math.round(ep.x) === Math.round(sp.x) && Math.round(ep.y) === Math.round(sp.y)
  }

  /** 获取触摸点 */
  const getTouchPont = (e, index) => {
    
    
    if (e.touches && e.touches[index]) return e.touches[index]
    return e.changedTouches && e.changedTouches[index]
  }
}

export default CanvasDraw


3.2 Tools

utils.js


/**
 * 判断点是否在多边形内部
 * @param {object} point 点坐标
 * @param {object} points 多边形坐标组
 * @param {number} angle 多边形中心点旋转角度
 * @returns
 */
export function isInPolygon(point, points, angle = 0) {
    
    
  const center = getPolygonCenterPoint(points)
  const newPoints = points.map((p) => rotatePoint(p, center, angle))

  const n = newPoints.length
  let nCross = 0
  for (let i = 0; i < n; i++) {
    
    
    const p1 = newPoints[i]
    const p2 = newPoints[(i + 1) % n]
    if (p1.y === p2.y) continue
    if (point.y < Math.min(p1.y, p2.y)) continue
    if (point.y >= Math.max(p1.y, p2.y)) continue
    const x = ((point.y - p1.y) * (p2.x - p1.x)) / (p2.y - p1.y) + p1.x
    if (x > point.x) nCross++
  }
  return nCross % 2 === 1
}

/** 点p1围绕点p2逆时针旋转angle度数后的坐标 */
function rotatePoint(p1, p2, angle) {
    
    
  const radians = (angle * Math.PI) / 180
  const dx = p1.x - p2.x
  const dy = p1.y - p2.y
  const cosRadians = Math.cos(radians)
  const sinRadians = Math.sin(radians)
  const x3 = cosRadians * dx - sinRadians * dy + p2.x
  const y3 = sinRadians * dx + cosRadians * dy + p2.y
  return {
    
     x: x3, y: y3 }
}

/**
 * 判断点是否在圆形半径内
 * @param {Point} point 点
 * @param {Point} center 圆心
 * @param {number} radius 圆半径
 */
export function isInCircle(point, center, radius) {
    
    
  const dx = point.x - center.x
  const dy = point.y - center.y
  return dx * dx + dy * dy <= radius * radius
}

/**
 * 获取多边形中心点坐标
 * @param {array} points 多边形点坐标
 * @returns 2触摸点距离
 */
export function getPolygonCenterPoint(points) {
    
    
  const result = {
    
     x: 0, y: 0 }
  points.forEach((p) => {
    
    
    result.x += p.x
    result.y += p.y
  })
  result.x /= points.length
  result.y /= points.length
  return result
}

/**
 * 获取2触摸点距离
 * @param {object} e 触摸对象
 * @returns 2触摸点距离
 */
export function 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 function getBoundingBox(points) {
    
    
  const boundingBox = {
    
    }

  // 计算最左、最右、最上和最下的坐标
  let left = points[0].x
  let right = points[0].x
  let top = points[0].y
  let bottom = points[0].y
  for (let i = 1; i < points.length; i++) {
    
    
    if (points[i].x < left) {
    
    
      left = points[i].x
    } else if (points[i].x > right) {
    
    
      right = points[i].x
    }
    if (points[i].y < top) {
    
    
      top = points[i].y
    } else if (points[i].y > bottom) {
    
    
      bottom = points[i].y
    }
  }

  boundingBox.bl = {
    
     x: left, y: bottom }
  boundingBox.br = {
    
     x: right, y: bottom }
  boundingBox.tl = {
    
     x: left, y: top }
  boundingBox.tr = {
    
     x: right, y: top }

  return boundingBox
}

/** 获取控制点坐标 */
export function getOCoords(aCoords, controlsVis, controls) {
    
    
  function getOCoord(type, p) {
    
    
    return {
    
    
      point: p,
      vis: controlsVis[type],
      radius: controls[type].radius,
      fill: controls[type].fill,
      customDraw: controls[type].customDraw,
    }
  }

  function getPoint(key) {
    
    
    switch (key) {
    
    
      case 'ml':
        return {
    
     x: aCoords.tl.x, y: aCoords.tl.y + (aCoords.bl.y - aCoords.tl.y) / 2 }
      case 'mt':
        return {
    
     x: aCoords.tl.x + (aCoords.tr.x - aCoords.tl.x) / 2, y: aCoords.tl.y }
      case 'mr':
        return {
    
     x: aCoords.tr.x, y: aCoords.tr.y + (aCoords.br.y - aCoords.tr.y) / 2 }
      case 'mb':
        return {
    
     x: aCoords.bl.x + (aCoords.br.x - aCoords.bl.x) / 2, y: aCoords.bl.y }
      case 'mtr':
        return {
    
     x: aCoords.tl.x + (aCoords.tr.x - aCoords.tl.x) / 2, y: aCoords.tl.y - 20 }
      case 'delete':
        return {
    
     x: aCoords.bl.x + (aCoords.br.x - aCoords.bl.x) / 2, y: aCoords.bl.y + 20 }
      default:
        return aCoords[key]
    }
  }

  const result = {
    
    }
  Object.keys(controls).forEach((key) => {
    
    
    result[key] = getOCoord(key, getPoint(key))
  })

  return result
}

/** 使用向量的方式来判断两个坐标是否处于相同方向 */
export function isSameDirection(p1, p2, p3, p4) {
    
    
  // 获取 p1 到 p2 的向量
  const vector1 = {
    
    
    x: p2.x - p1.x,
    y: p2.y - p1.y,
  }

  // 获取 p3 到 p4 的向量
  const vector2 = {
    
    
    x: p4.x - p3.x,
    y: p4.y - p3.y,
  }

  if (vector1.x === 0 && vector1.y === 0 && vector2.x === 0 && vector2.y === 0) return true
  if ((vector1.x === 0 && vector1.y === 0) || (vector2.x === 0 && vector2.y === 0)) return false

  const result = !(
    (vector1.x < 0 && vector2.x > 0) ||
    (vector1.y < 0 && vector2.y > 0) ||
    (vector1.x > 0 && vector2.x < 0) ||
    (vector1.y > 0 && vector2.y < 0)
  )

  return result
}

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

Guess you like

Origin blog.csdn.net/iamlujingtao/article/details/128289849