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

小程序canvas 缩放/拖动/还原/封装和实例

一、预览

之前写过web端的canvas 缩放/拖动/还原/封装和实例。最近小程序也需要用到,但凡是涉及小程序canvas还是比较多坑的,而且难用多了,于是在web的基础上重新写了小程序的相关功能(参考了Fabric.js)。实现功能有:

  • 支持双指、按钮缩放
  • 支持单指/双指触摸拖动
  • 支持高清显示
  • 支持节流绘图
  • 支持重置、清除画布
  • 支持元素选中/删除
  • *内置绘图方法

效果如下:

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

; 二、使用

案例涉及到3个文件:

  • 图实例组件canvas.vue
  • 核心CanvasDraw类canvasDraw.js
  • 工具方法utils.js

2.1 创建和配置

小程序获取#canvas对象后就可以创建CanvasDraw实例了,创建实例时可以根据需要设置各种配置,其中drawCallBack是绘图回调,程序会在this.canvasDraw.draw()后再回调drawCallBack()来实现用户的绘图,用户可以使用this.canvasDraw.ctx来使用原生的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,
            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,
            pixelRatio: wx.getSystemInfoSync().pixelRatio,
          }
          this.canvasDraw = new CanvasDraw(option)
          this.canvasDraw.draw()
        })
    },

方法

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)
canvasDraw.getSelect()
canvasDraw.on(type,callBack)
canvasDraw.off(type,callBack)
canvasDraw.removeChild(id)
canvasDraw.reset()
canvasDraw.zoomIn()
canvasDraw.zoomOut()
canvasDraw.zoomTo(scale, zoomCenter)

三、源码

3.1 实例组件

canvas.vue

<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
    this.imgs = {
      star: '../../../static/images/starActive.png',
      delete: '../../../static/images/cancel.png',
    }
    this.startPoint = null
    this.createId = null
    this.fingers = 1

    return {
      isDrawing: false,
    }
  },

  created() {},
  beforeDestroy() {

    if (this.canvasDraw) {
      this.canvasDraw.destroy()
      this.canvasDraw = null
    }
  },
  mounted() {

    this.initCanvas()
  },
  methods: {

    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,
            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,
            pixelRatio: wx.getSystemInfoSync().pixelRatio,
            controls: {
              delete: {
                radius: 20,
                fill: '#f00',
                customDraw: this.drawDeleteControl,
              },
            },
          }
          this.canvasDraw = new CanvasDraw(option)
          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() {

    },

    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) {

      if (this.fingers !== 1 || !this.isDrawing) return
      const tsPoint = this.startPoint
      const tmPoint = e.point

      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
      )
    },

    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 核心类

canvasDraw.js

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

export function CanvasDraw(option) {
  if (!option.ele) {
    console.error('canvas对象不存在')
    return
  }
  const { ele } = option

  this.canvasNode = ele.node
  this.canvasNode.width = ele.width
  this.canvasNode.height = ele.height
  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
  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
    this.pixelRatio = optionCopy.pixelRatio ?? 1

    this.controls = option.controls ?? {
      delete: { radius: 10, fill: '#f00', customDraw: null },
    }

    this.controlsVis = optionCopy.controlsVis ?? {
      delete: false,
    }

    startDistance = 0
    preScale = this.scale
    touchMoveTimer = null
    touchEndTimer = null
    fingers = 1
  }

  init()

  this.draw = () => {
    clear()
    drawChildren()
    option.drawCallBack()
  }

  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)

  }

  this.clear = () => {
    clear()
    this.children.length = 0
  }

  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)
  }

  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
  }

  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) => {

    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) => {

    if (fingers > 2 || isSaveTouchPoint(preTouches[0], e.changedTouches[0])) return
    if (this.isThrottleDraw) {
      if (touchMoveTimer) return

      touchMoveTimer = setTimeout(this.touchmoveSelf.bind(this, e), this.throttleInterval)
    } else {

      this.touchmoveSelf(e)
    }
  }

  this.touchmoveSelf = (e) => {

    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

    touchMoveTimer = null
  }

  this.touchend = (e) => {

    if (this.isThrottleDraw) {
      touchEndTimer = setTimeout(this.touchendSelf.bind(this, e), this.throttleInterval)
    } else {
      this.touchendSelf(e)
    }
  }

  this.touchendSelf = (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

    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)
    })
  }

  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()
  }

  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 工具类

utils.js


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
}

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 }
}

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
}

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
}

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) {

  const vector1 = {
    x: p2.x - p1.x,
    y: p2.y - p1.y,
  }

  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
}

&#x5144;&#x5F1F;&#xFF0C;&#x5982;&#x679C;&#x5E2E;&#x5230;&#x4F60;&#xFF0C;&#x70B9;&#x4E2A;&#x8D5E;&#x518D;&#x8D70;

Original: https://blog.csdn.net/iamlujingtao/article/details/128289849
Author: iamlujingtao
Title: 小程序canvas 缩放/拖动/还原/封装和实例–开箱即用

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/725839/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球