CDarg诞生之路之Canvas实现拖拽(图片,文字)

前言

在业务中需要一种体现物体在平面图上的位置关系视图,比如IBMS中设备在楼层平面中的位置,同时可能有文字描述信息,实现这个最开始是dom加拖拽实现,但频繁操作dom太影响性能,所以使用Canvas重写这个功能

image.png

这篇文章会涉及到知识点

  1. canvas如何绘制图片与文字
  2. 文字与图片点击命中检测
  3. 拖拽的实现
  4. 解决频繁clearRect 图片出现的闪动问题

以Vue2为例

在此组件中接受要绘制要素的rect类型的列表,参数包括xy轴距离、宽高、旋转、缩放、层级等等,其他的看个人需求

<template>

  <canvas

    id="CDrag-canvas"

  />
</template>

<script>
//rect: {
  // left: 'left', // 距canvas原点X轴距离
  // top: 'top',  // 距canvas原点Y轴距离
  // rotate: 'rotate',  // 旋转角度
  // width: 'width',  // 宽度(图片才有)
  // height: 'height', // 高度(图片才有)
  // scale: 'scale', // 缩放(图片才有)
  // zIndex: 'zIndex',  // 层级(图片才有)
  // img: 'img', // 图片地址 (图片才有)
  // text: 'text', // 文字内容  (文字才有)
  // BColor: 'BColor',  // 背景颜色
  // color: 'color', // 文字颜色 (文字才有)
  // size: 'size', // 文字大小 (文字才有)
//}
let ctx = null
let canvas = null
let select = null
export default {
  props: {
    // 绘制数据列表
    rects: {
      type: Array,
      default: () => []
    },
  },
}
</script>

绘制图片

使用drawImage依次传入图片,距原点的距离、图片宽高
drawImage不能直接使用图片地址需要 new Image() 转为图片才行

记得在onload中绘制哦

drawImg (drawData) {
  if (ctx) {
        ctx.beginPath()

        ctx.fillStyle = drawData.BColor || 'transparent'
        const image = new Image() // 创建 img 元素
        image.onload = function () {
          // 执行 drawImage 语句
          ctx.drawImage(image, drawData.left, drawData.top, drawData.width, drawData.height)
        }
        image.src = drawData.img // 设置图片源地址
      }


    },

绘制文字

绘制文字使用fillText 传入距原点的距离,这里还可以限制文字的最大宽度,但我们不做限制

drawText (drawData) {
      if (ctx) {
        ctx.beginPath()

        ctx.textBaseline = 'top'
        ctx.font = `${drawData.size}px system-ui`
        // 设置文字宽高
        drawData.width = drawData.text?.length * drawData.size
        drawData.height = drawData.size
        ctx.fillStyle = drawData.color
        ctx.fillText(drawData.text, drawData.left, drawData.top)
      }


    },

切记要设置ctx.textBaseline = ‘top’ 否则文字会不在实际的的矩形范围内,因为默认是bottom看下图,红线以下其实才是内容区,所以要改为top对齐,不然后续的点击命中你就会发现根本检测不到文字

image.png

注意字体其实没有给他传宽和高,但为什么我这里有去设置了宽高了,同样是为了后续的点击命中检测。

这里高度使用字体size即可,而宽度得根据文本长度而定,一开始使用measureText() 获取文本宽度,但实际宽度远超过预测宽度,如下黑色背景为 measureText() 预测宽度,在后续命中检测中点击区域根本检测不到,所以我使用text.length * size计算宽度效果如图二

嗯~ 基本囊括整个文本,真是机智如我

8ffe14faef3ac36c4fe6a4bdc628030.jpg69dcb702391e4fd0b65f8ba8b8b127e.jpg

拖拽

拖拽逻辑如下

  1. 监听mousedown鼠标按下
  2. 根据点击坐标做图形命中检测
  3. 命中图形后在mousemove中不断更新rectlefttop
  4. 清空canvas后再重绘
<template>

  <canvas

    id="CDrag-canvas"

    @mousedown="handleMousedown"
  />
</template>

// 鼠标按下
handleMousedown (event) {
 // 记录点击坐标
  const { clientX: downX, clientY: downY } = event
  // 这里做命中检测
  const result = this.getHitTestGraphic(event) || {}
  const toDragX = result ? downX - result.left : 0 // 点击点到rect左边距离
  const toDragY = result ? downY - result.top : 0 // 点击点到rect右边距离
  const handleMousemove = ({ clientX, clientY }) => {
    if (result.left) {
      result.left = clientX - toDragX // 实际移动距离
      result.top = clientY - toDragY // 实际移动距离
      this.draw(this.rects) // 重绘方法
    }
  }
  const handleMouseup = () => {
    document.removeEventListener('mousemove', handleMousemove)
    document.removeEventListener('mouseup', handleMouseup)
    select = null
  }
  document.addEventListener('mousemove', handleMousemove)
  document.addEventListener('mouseup', handleMouseup)
},

看看效果,怎么图片一闪一闪的?

因为图片是加载回来的,每次重绘时 可能图片都没加载完,导致渲染空白,在高频率的重绘时就形成闪动了,

WonderFox_Video_Recording_001.gif

解决图片闪动

网上有一些方法,比如使用双缓存创建副canvas,一个负责绘制,绘制完成后在给另一个canvas展示。

但我不用这个方法,既然第一次已经加载完了,第二次是不是就可以不用再重新加载呢?

第一次加载后把image存起来

// 核心代码
// 有image对象直接绘制,不走onload逻辑
if (drawData.image) {
      ctx.drawImage(drawData.image, drawData.left, drawData.top, drawData.width, drawData.height)
    } else {
      const image = new Image() // 创建 img 元素
      drawData.image = image // 第一次加载后存起来
      image.onload = function () {
        // 执行 drawImage 语句
        ctx.drawImage(image, drawData.left, drawData.top, drawData.width, drawData.height)
      }


      image.src = drawData.img // 设置图片源地址
    }

再来看看效果 完美!

WonderFox_Video_Recording_003.gif

命中检测方法

handleMousedown 时调用 getHitTestGraphic传入点击event

遍历整个rects列表,计算图片或者文字矩形的四个顶点,再使用hitTest判断当前图形是否被点击命中,命中则返回rect,接着在mousemove中不断更新lefttop实现拖拽

网上也有其他命中检测方法,比如mousedown时清空画布再重绘,每绘制一个图形就使用 isPointInPath() 判断当前绘制图形是否命中。

但本着能少重绘就少重绘原则,选择了计算矩形范围做命中的方法,这也是之前为什么给文字添加宽高的原因,因为得计算它的矩形坐标

/**
 * 获取命中图形
 * @param { PointEvent} event
 */
getHitTestGraphic ({ clientX, clientY }) {
  const result = []
  this.rects.forEach(rect => {
    const leftTop = [rect.left, rect.top] // 左上顶点
    const rightTop = [rect.left + rect.width, rect.top] // 右上顶点
    const rightBottom = [rect.left + rect.width, rect.top + rect.height] // 右下顶点
    const leftBottom = [rect.left, rect.top + rect.height] // 左下顶点
    
    if (hitTest([clientX - canvas.offsetLeft, clientY - canvas.offsetTop], [leftTop, rightTop, leftBottom, rightBottom])) {
      result.push(rect)
    }
  })
  select = result.pop()
  return select
},

/**
 * 判断点是否在要素范围内
 * @param {*} point 点击位置
 * @param {*} rect 矩形的四个顶点坐标
 * @returns
 */
export function hitTest ([downX, downY], [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]) {
  const v1 = [x1 - downX, y1 - downY]
  const v2 = [x2 - downX, y2 - downY]
  const v3 = [x3 - downX, y3 - downY]
  const v4 = [x4 - downX, y4 - downY]
  if (
    (v1[0] * v2[1] - v2[0] * v1[1]) > 0 &&
      (v2[0] * v4[1] - v4[0] * v2[1]) > 0 &&
      (v4[0] * v3[1] - v3[0] * v4[1]) > 0 &&
      (v3[0] * v1[1] - v1[0] * v3[1]) > 0
  ) {
    return true
  }
  return false
}

这个canvas系列第一篇,后续还会实现缩放、旋转等,敬请期待

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYh1fxP5' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片