封装canvas选择区域的组件

news2025/3/13 10:55:47

大家好,我是南宫,最近我刚完成了一个canvas相关组件的封装。我个人其实很怕canvas和地图,就感觉这里有很复杂的操作,搞不懂,所以这次封装完了以后,决定写一篇博客来记录。

首先我先简单介绍一下这个组件的功能,然后分布介绍思路并列出代码,最后如果有需要的小伙伴,可以联系我获取完整代码。

一、背景和组件的功能

背景:这一次我在做一个新的项目,里面有一个新增编辑的弹窗,弹窗里面有一个字段需要选择区域,我当场就蒙了,然后去找了组长,主要是想问这个项目里面有没有已经写好的现成的组件能直接用的,结果得到了否定的答案,只能我来封装一个了。

然后又经过核对需求,我明白了,我的这个组件不需要支持用鼠标在canvas里面选点画框,而是通过传入的坐标把框渲染上去就好了,但是还需要实现点击选择其中一个区域的功能,也就是点击图片上的一个点,判断是否在框里面,是的话就认为是选中了这个框

我做的效果如下:(图片是什么不重要,框的位置在哪里不重要,这些都是使用组件的时候传进去的)

如图所示,里面现在有两个区域,我当前选择的是区域2。区域2的标记是高亮状态。

二、思路拆解

首先是初始化,初始化canvas,getContext得到上下文对象(废话)。然后drawImage画出图片到画布上,然后则是根据点的坐标画出多个框。(这一步我直接用了组长发给我的画框代码,我没写这个,我只是阅读了以后知道了要传什么参数,然后补充了填充颜色)

(这里我意识到一个问题,我的画面大小是根据父组件实际上的空间来决定的,但是要绘制的图片并不是只有这么大,说明看起来的坐标和实际坐标不一致,坐标发生了缩放。)

第二步是标记出每个区域的名字。这一步是我自己做的。就是找到区域里最高的一个点,在上面绘制一个矩形,矩形里面写上“区域X”。有了这个标记,才能选择区域。

第三步,实现点击选择区域。思路是点击canvas以后,就去获取当前点击位置相对于canvas的坐标(注意缩放),然后挨个判断这个坐标是否在每一个区域里面。(判断点是否在区域里面的方法我是用了这个博客里面的代码,非常感谢原作者 https://www.cnblogs.com/tracyjfly/p/15891591.html )

第四步,获取到所在区域以后的操作。如果当前没有选择过区域,那么直接再绘制一个高亮的区域标记就可以了如果当前已经选择过区域了,而且现在选的不是之前的那个区域,那就不能直接再绘制高亮了,而是要去掉其他标识的高亮才可以

三、每一步的具体实现

① 初始化:

第一步的代码我看过了,大概意思是接收一个data对象,然后获取指定属性的值来作为图片地址,drawImage绘制到canvas中。然后获取data中准备好的坐标数组(每一个元素表示一个框),遍历每一个框的每一个点的坐标,用moveTo和lineTo绘制框

开始新区域的前后记得用beginPath和closePath关闭区域,最后stroke和fill这个区域。

(这一步简单说说,具体看代码就好,下面这个是关键代码,省略了初始化canvas的部分)

if (data.monitorAnalysisRuleList && data.monitorAnalysisRuleList.length > 0) {
  // 多框 布控框
  data.monitorAnalysisRuleList.forEach((e, index) => {
    ctx.strokeStyle = '#f00' //线条颜色
    ctx.fillStyle = 'rgba(255, 0, 0, 0.2)' // 填充颜色
    ctx.lineWidth = 5 //线条粗细
    ctx.beginPath() //绘图开始
    let point = ''
    point = JSON.parse(e.polygon)
    for (let i in point) {
      if (i == 0) {
        ctx.moveTo(point[i][0], point[i][1]) //设置路径起点坐标
      } else {
        ctx.lineTo(point[i][0], point[i][1])
      }
    }
    ctx.closePath()
    ctx.stroke()
    // 补充填充
    ctx.fill()
  })
}

效果是这样的(这里的坐标是我乱写的,画出来成这样了)

② 标记出区域的名字:

为了标记的位置比较正常,所以我让标记出现在区域最高的点的上方,所以是选择y值最小的点

找到这个点以后,稍微移动一下就是绘制标记的地方了。

区域的名字出现在矩形的上方,所以要先画矩形,而矩形的宽度则是比文字更高。这里就需要先设置文字的内容和字体样式,然后测量文字宽度了

然后给文字宽度稍微加一点,作为矩形的宽度,然后微调位置,绘制矩形。(stroke和fill)然后再绘制文字fillText。

/**
  * 绘制区域标签的方法
  * @param {*} ctx 画框的上下文对象
  * @param {*} point 一个框的点坐标集合
  * @param {*} index 当前画的是第几个框
  * @param {*} isActive 是否高亮
  */
drawAreaTag(ctx, point, index, isActive) {
  // 找到最高点,也就是y值最小的点,在这个点的上方写区域的名字
  let minY = point[0][1]
  let minYIndex = 0
  for (let i in point) {
    if (point[i][1] < minY) {
      minY = point[i][1]
      minYIndex = i
    }
  }
  const text = `区域${index + 1}`
  // 设置字体样式和大小
  ctx.font = '20px Arial'
  // 测量文字的宽度
  const textWidth = ctx.measureText(text).width
  const textHeight = 28 // 文字大小为16px,上下留出8px的间距
  const rectWidth = textWidth + 16 // 比文字所占区域稍微宽一些
  const rectHeight = textHeight + 12 // 比文字所占区域稍微宽一些
  // 绘制一个白色矩形,高亮状态为蓝色
  if (isActive) {
    ctx.fillStyle = '#0C84FF'
    ctx.strokeStyle = '#0C84FF'
  } else {
    ctx.fillStyle = 'white'
    ctx.strokeStyle = 'black'
  }
  ctx.lineWidth = 2
  ctx.fillRect(point[minYIndex][0] - 8, point[minYIndex][1] - 55, rectWidth, rectHeight) // 位置可以根据需要调整
  ctx.strokeRect(point[minYIndex][0] - 8, point[minYIndex][1] - 55, rectWidth, rectHeight) // 位置可以根据需要调整
  // 设置文本颜色,高亮状态为白色
  if (isActive) {
    ctx.fillStyle = 'white'
  } else {
    ctx.fillStyle = 'black'
  }
  ctx.fillText(text, point[minYIndex][0], point[minYIndex][1] - 28)
},

我画的矩形默认状态是白底黑边黑字,高亮状态是蓝底蓝边白字。可以根据你的需求修改颜色。

效果如下图:

③ 点击选择区域

这一步算是主流程了吧。

点击选择区域,然后获取到当前点击的位置在canvas中的坐标

具体方法比较绕(知道意思的话可跳过)——点击以后获取点击位置相对于窗口左上角的距离,然后得到canvas左上角相对于窗口左上角的距离,相减得到点击位置相对于canvas的像素位置;计算canvas外观的宽高除以图片具体的宽高,得到一个缩放的比例;最后用相对于canvas的像素位置除以缩放比例,得到相对于canvas当前坐标的坐标。

然后遍历框的数组,用这个坐标来判断是否在每一个框里面,匹配到第一个框就停止,并且得到当前框的下标,匹配不到则是返回-1。

// 点击canvas,获取坐标,转成相对于canvas实际大小的坐标,判断在哪个区域里面,并且进行处理
chooseArea(e) {
  console.log('你好,点击canvas', e.clientX, e.clientY)
  // 点击屏幕的坐标
  const point = { x: e.clientX, y: e.clientY }
  // 获取canvas左上角的坐标
  const canvas = this.$refs.canvas
  const rect = canvas.getBoundingClientRect()
  // console.log('你好,canvas左上角的坐标', rect.left, rect.top)
  const x = point.x - rect.left
  const y = point.y - rect.top
  // console.log('你好,点击的位置相对于canvas', x, y)
  // 计算缩放比例
  const scaleX = this.width / canvas.width
  const scaleY = this.height / canvas.height
  // 得到当前的点相对于canvas实际大小的坐标
  const canvasX = x / scaleX
  const canvasY = y / scaleY
  const pointObj = { x: canvasX, y: canvasY }
  // 比对每一个区域,看看是否在里面
  const area = this.findArea(pointObj)
  console.log('我点击的位置在哪个区域里呢', area)
},
// 把点的坐标转换成对象的格式
convertArray(arr) {
  let result = []
  for (let i = 0; i < arr.length; i++) {
    result.push({ x: arr[i][0], y: arr[i][1] })
  }
  return result
},
//判断点是否在多边形范围内(点坐标,多边形的点坐标集合)
queryPtInPolygon(point, polygon) {
  var p1, p2, p3, p4

  p1 = point
  p2 = { x: 1000000000000, y: point.y }

  var count = 0
  //对每条边都和射线作对比
  for (var i = 0; i < polygon.length - 1; i++) {
    p3 = polygon[i]

    p4 = polygon[i + 1]
    if (checkCross(p1, p2, p3, p4) == true) {
      count++
    }
  }
  p3 = polygon[polygon.length - 1]

  p4 = polygon[0]
  if (checkCross(p1, p2, p3, p4) == true) {
    count++
  }

  return count % 2 == 0 ? false : true

  //判断两条线段是否相交
  function checkCross(p1, p2, p3, p4) {
    var v1 = { x: p1.x - p3.x, y: p1.y - p3.y },
      v2 = { x: p2.x - p3.x, y: p2.y - p3.y },
      v3 = { x: p4.x - p3.x, y: p4.y - p3.y },
      v = crossMul(v1, v3) * crossMul(v2, v3)

    v1 = { x: p3.x - p1.x, y: p3.y - p1.y }
    v2 = { x: p4.x - p1.x, y: p4.y - p1.y }

    v3 = { x: p2.x - p1.x, y: p2.y - p1.y }
    return v <= 0 && crossMul(v1, v3) * crossMul(v2, v3) <= 0 ? true : false
  }

  //计算向量叉乘
  function crossMul(v1, v2) {
    return v1.x * v2.y - v1.y * v2.x
  }
},
// 判断当前的点是否在任意一个区域内(
findArea(pointObj) {
  let flag = -1
  for (let index = 0; index < this.data.monitorAnalysisRuleList.length; index++) {
    const polygon = this.convertArray(JSON.parse(this.data.monitorAnalysisRuleList[index].polygon))
    // console.log(`区域${index + 1}的坐标`, polygon)
    var pts = this.queryPtInPolygon(pointObj, polygon)
    // console.log(`请问当前的点击位置是否在区域${index + 1}里面`, pts)
    if (pts == true) {
      flag = index
      break
    }
  }
  return flag
},

④ 获取到区域以后的操作

这一步属于主流程的一部分,但是为了避免主流程太多内容,我把这一部分写到另一个方法里面了。

先判断是否已经选择过区域了,如果否就直接绘制一个高亮的区域标记,覆盖在原来的地方即可。

如果是,那就初始化canvas,重新画过(绘制图片,绘制框,绘制区域标记这些)

// 找到区域后的处理(area是比对到的点击位置所在的区域下标)
handleDrawArea(area) {
  // 大于-1就认为有在区域里面,记录一下当前选择了区域几
  if (area > -1) {
    // 如果是第一次选择,直接记录
    if (this.currentAreaIndex == -1) {
      this.currentAreaIndex = area
      const pointList = JSON.parse(this.data.monitorAnalysisRuleList[this.currentAreaIndex].polygon)
      this.drawAreaTag(this.ctx, pointList, this.currentAreaIndex, true)
    } else {
      // 判断是否跟当前选择的area相同,是就不做处理
      if (this.currentAreaIndex == area) {
        // console.log('你好,当前的选择的区域是第几个', this.currentAreaIndex)
        return
      } else {
        // 否就赋值,并且重新画过
        this.currentAreaIndex = area
        // console.log('你好,当前的选择的区域是第几个', this.currentAreaIndex)
        // 需要重新画过(控制高亮的逻辑写到初始化canvas方法里了)
        this.setDrawImage(this.data, 0, 'canvas', false, 'sceneUrl')
      }
    }
  }
},

这个时候就可以实现点击单选区域了。

四、优化

到这里还没完,为了让这个成为一个可以使用的组件,我明确了这个组件的输入和输出(输入包括data对象(坐标和图片地址等信息),输出则是当前选择的框的坐标集合(根据当前选择的是第几个,就返回第几个的坐标集合给父组件))

这个时候还有一个地方要优化的——初始化canvas的时候有一个clearRect的步骤,会清空整个画布,然后再绘制,所以点击选择区域的时候会有白光一闪的效果

我查了资料以后,决定用一个临时canvas优化掉这个问题。

经过一番纠结和思考以后,我的想法如下:

准备一个临时canvas,不显示出来,仅仅是用来存放内容。

临时canvas不需要每一步都跟着画,只需要初始化的时候绘制图片和不带高亮的区域标记即可。

当主要的canvas点击以后需要重新绘制的时候,就不需要清空画布了,直接覆盖一层临时canvas的内容,然后再绘制一个高亮标记就可以了。

(以下是处理部分的修改,另外要写一个初始化临时canvas的函数,初始化的时候也调用一下)

// 找到区域后的处理(area是比对到的点击位置所在的区域下标)
handleDrawArea(area) {
  // 大于-1就认为有在区域里面,记录一下当前选择了区域几
  if (area > -1) {
    // 如果是第一次选择,直接记录
    if (this.currentAreaIndex == -1) {
      this.currentAreaIndex = area
      const pointList = JSON.parse(this.data.monitorAnalysisRuleList[this.currentAreaIndex].polygon)
      this.drawAreaTag(this.ctx, pointList, this.currentAreaIndex, true)
    } else {
      // 判断是否跟当前选择的area相同,是就不做处理
      if (this.currentAreaIndex == area) {
        // console.log('你好,当前的选择的区域是第几个', this.currentAreaIndex)
        return
      } else {
        // 否就赋值,并且重新画过
        this.currentAreaIndex = area
        // console.log('你好,当前的选择的区域是第几个', this.currentAreaIndex)
        // 需要重新画过(控制高亮的逻辑写到初始化canvas方法里了)
        // 如果有临时canvas
        if (this.tempUrl) {
          // 用临时canvas的的结果初始化canvas
          this.ctx.drawImage(this.tempCanvas, 0, 0)
          // 计算是要在哪里绘制标记
          const pointList = JSON.parse(this.data.monitorAnalysisRuleList[this.currentAreaIndex].polygon)
          // 绘制高亮的标记
          this.drawAreaTag(this.ctx, pointList, this.currentAreaIndex, true)
        } else {
          this.setDrawImage(this.data, 0, 'canvas', false, 'sceneUrl')
        }
      }
    }
  }
},

这样操作下来,就没有白光的问题了,效果丝滑,本人亲测。

如果需要完整代码,联系本人QQ:807026100领取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1101708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【仙逆】王林极限跑酷,藤厉自食恶果,仙逆战斗获好评,张虎命运被改写

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 最新一集《仙逆》已经更新&#xff0c;相信很多小伙伴都已经先睹为快&#xff0c;在击杀了白展之后&#xff0c;张虎和王林担心其师傅即墨老人报复&#xff0c;因此躲到看似安全的藤家城&#xff0c;以为那里有…

MySQL学习(三)——多表连接查询

文章目录 1. 多表关系1.1 一对多1.2 多对多1.3 一对一 2. 概述2.1 数据准备2.2 简单查询2.3 分类 3. 内连接4. 外连接5. 自连接5.1 自连接查询5.2 联合查询 6. 子查询6.1 概念6.2 标量子查询6.3 列子查询6.4 行子查询6.5 表子查询 1. 多表关系 项目开发中&#xff0c;在进行数…

UE5 运行时生成距离场数据

1.背景 最近有在运行时加载模型的需求&#xff0c;使用DatasmithRuntimeActor可以实现&#xff0c;但是跟在编辑器里加载的模型对比起来&#xff0c;室内没有Lumen的光照效果。 图1 编辑器下加载模型的效果 图2 运行时下加载模型的效果 然后查看了距离场的数据&#xff0c;发现…

leetcode-48.旋转图像

1. 题目 leetcode题目链接 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 2. 编程 矩阵转置&#xff1a; 遍历矩阵&#x…

EKP接口开发Webservice服务和Restservice服务以及定时任务Demo

继承com.landray.kmss.sys.webservice2.interfaces.ISysWebservice&#xff0c;同时在接口上使用WebService注解将其标识为WebService接口 package com.landray.kmss.third.notify.webservice;import com.alibaba.fastjson.JSONObject; import com.landray.kmss.sys.webservic…

CAD图形导出为XAML实践

文章目录 一、前言二、方法与实践2.1 画出原图&#xff0c;借第三方工具导出至指定格式2.2 CAD导出并转换2.3 两种方法的优劣2.3.1 直接导出代码量大2.3.2 导入导出需要调参 三、总结 一、前言 上位机通常有一个设备/场景界面&#xff0c;该界面用于清晰直观地呈现设备状态。 …

线程通信java

有包子 不做了 唤醒别人等地自己 this.notifyAll(); this.wait() package TheadCpd;public class TheadCpd {//目标&#xff1a;了解线程通信public static void main(String[] args) {//需求&#xff1a;3个人生产或者线程 负责生产包子 每个线程生产1个包子放桌子上// 2…

Pytorch:cat、stack、squeeze、unsqueeze的用法

Pytorch&#xff1a;cat、stack、squeeze、unsqueeze的用法 torch.cat 在指定原有维度上链接传入的张量&#xff0c;所有传入的张量都必须是相同形状 torch.cat(tensors, dim0, *, outNone) → Tensor tensor:相同形状的tensor dim:链接张量的维度&#xff0c;不能超过传入张…

C++对象模型(10)-- 虚函数2

1、虚函数表、虚函数表指针的创建时机 我们知道虚函数表是属于类的&#xff0c;而虚函数表指针是属于对象的。在编译的时候&#xff0c;编译器会往类的构造函数中插入创建虚函数表指针的代码。同样&#xff0c;在编译期间编译器也为每个类确定好了对应的虚函数表的内容。 虚函…

巡检系统是什么?设备巡检系统有什么用?

在现今这个高度自动化的时代&#xff0c;许多企业的设备规模日益扩大&#xff0c;设备巡检工作也变得越来越重要。它不仅是保证企业设备正常运行的重要环节&#xff0c;也是维护生产安全和提升运营效率的关键。那么&#xff0c;如何有效地进行设备巡检呢&#xff1f;答案就是—…

CSS Vue/RN 背景使用opacity,文字在背景上显示

Vue <div class"training_project_tip"> <div class"tip">展示的文字</div> </div> .training_project_tip { font-size: 12px; font-weight: 400; text-align: left; color: #ffffff; margin-top: 8px; position: relative; dis…

6.自定义相机控制器

愿你出走半生,归来仍是少年&#xff01; Cesium For Unity自带的Dynamic Camera,拥有优秀的动态展示效果&#xff0c;但是其对于场景的交互方式用起来不是很舒服。 通过模仿Cesium JS 的交互方式&#xff0c;实现在Unity中的交互&#xff1a; 通过鼠标左键拖拽实现场景平移通过…

模式植物背景基因集制作

一边学习&#xff0c;一边总结&#xff0c;一边分享&#xff01; 写在前面 关于GO背景基因集文件的制作&#xff0c;我们在很早以前也发过。近两天&#xff0c;自己在分析时候&#xff0c;也是被搞了头疼。想重新制作一份GO背景基因集&#xff0c;进行富集分析。但是结果&…

Cpolar+Inis结合在Ubuntu上打造出色的博客网站,快速上线公网访问

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

编辑器功能:用一个快捷键来【锁定】或【解开】Inspector面板

一、需求 我有一个脚本&#xff0c;上面暴露了许多参数&#xff0c;我要在场景中拖物体给它进行配置。 如果不锁定Inspector面板的话&#xff0c;每次点击物体后&#xff0c;Inspector的内容就是刚点击的物体的内容&#xff0c;而不是挂载脚本的参数面板。 二、 解决 &…

适老化改造监管平台

文章目录 前言一、首页二、客户管理三、评估管理四、施工管理五、验收管理总结 前言 适老化改造监管平台是指一种为老年人住房进行适老化改造所建立的监管平台。该平台可以辅助政府监管相关企业或个人为老年人住房进行适老化改造的工作&#xff0c;确保改造符合相关标准和规定…

Android--Retrofit2执行多个请求任务并行,任务结束后执行统一输出结果

场景&#xff1a;后端上传文件接口只支持单个文件上传&#xff0c;而业务需求一次性上传多个图片&#xff0c;因此需要多个上传任务并发进行&#xff0c;拿到所有的返回结果后&#xff0c;才能进行下一个流程。 1、使用Java并发工具 private List<Response<JSONObject>…

JVM三色标记

三色标记 什么是三色标记法 三色标记法&#xff0c;也被称为Tri-color Marking Algorithm&#xff0c;是一种用于追踪对象存活状态的垃圾回收算法。它基于William D. Hana和Mark S. McCulleghan在1976年提出的两色标记法的基础上进行了改进。 与两色标记法只能将对象标记为“…

c++ --- 归并排序

2、归并排序 步骤&#xff1a; 选取中间点 mid (LR)/2递归排序 左边left 和 右边 right归并 ---- 将数组合二为一 模板代码 int temp[10]; void merge_sort(int q[], int l, int r) {//如果左右边界相等 直接退出if (l > r) return;//获取数组中心int mid (l r) / 2;/…

如何将模型原点设置到模型的中心

1、为什么要调整坐标原点位置&#xff1f; 从事3D建模相关工作的朋友们在工作中经常会需要调整模型的坐标原点&#xff0c;那么为什么一定要调整模型的坐标原点呢&#xff1f;主要原因如下&#xff1a; 方便后续操作&#xff1a;将原点设置为几何中心可以方便后续对模型进行旋…