Vue Canvas实现区域拉框选择

canvas.vue组件

<template><div class="all" ref="divideBox"><!-- 显示图片,如果 imgUrl 存在则显示 --><img id="img" v-if="imgUrl" :src="imgUrl" oncontextmenu="return false" draggable="false"><!-- 画布元素,绑定鼠标事件 --><canvas ref="canvas" id="mycanvas" @mousedown="startDraw" @mousemove="onMouseMove" @mouseup="endDraw"@click="onClick" :width="canvasWidth" :height="canvasHeight" oncontextmenu="return false"draggable="false"></canvas><el-dialog title="编辑区域数据" :visible.sync="dialogVisible" width="500"><div class="dialogDiv"><el-form :model="form" ref="form" label-width="110px" :rules="rules"><el-form-item label="车辆类型" prop="type"><el-select style="width: 100%;" v-model="form.type" placeholder="请选择车辆类型" size="small"clearable><el-option v-for="item in carTypeList" :key="item.value" :label="item.label":value="item.value" /></el-select></el-form-item><el-form-item label="JSON数据" prop="jsonData"><el-input size="small" type="textarea" v-model="form.jsonData" rows="10"></el-input></el-form-item></el-form></div><span slot="footer" class="dialog-footer"><el-button type="danger" @click="del">删 除</el-button><el-button type="primary" @click="clickOk">确 定</el-button></span></el-dialog></div>
</template><script>
export default {name: 'CanvasBox',// 引入组件才能使用props: {// 画布宽度canvasWidth: {type: Number,default: 0},// 画布高度canvasHeight: {type: Number,default: 0},// 时间戳timeStamp: {type: Number,default: 0},// 图片 URLimgUrl: {type: String,default: ""},// 类型颜色type: {type: String,default: ""},},components: {},data() {return {rules: {type: [{ required: true, message: '车辆类型不能为空', trigger: ['change', 'blur'] }],jsonData: [{ required: true, message: 'JSON数据不能为空', trigger: ['change', 'blur'] }],},carTypeList: [{value: "1",label: "人员"},{value: "2",label: "车辆"}],// 表单值form: {id: null,type: '',jsonData: ''},dialogVisible: false,originalCanvasWidth: this.canvasWidth,originalCanvasHeight: this.canvasHeight,url: null,// 是否是绘制当前的草图框isDrawing: false,start: { x: 0, y: 0 },end: { x: 0, y: 0 },// 储存所有的框数据boxes: [],// 框文字selectedCategory: {modelName: ""},categories: [],image: null, // 用于存储图片imageWidth: null, // 图片初始宽度imageHeight: null, // 图片初始高度piceList: [],startTime: null, // 用于记录鼠标按下的时间categoryColors: {'车辆': 'red','人员': 'yellow'},};},watch: {// 清空画布timeStamp() {this.test();},// 监听画布宽度canvasWidth(newVal) {this.$nextTick(() => {this.adjustBoxesOnResize();this.draw();})},// 监听类型type(newVal) {this.selectedCategory.modelName = newVal === '1' ? '人员' : newVal === '2' ? '车辆' : ''}},mounted() {this.draw();// 添加鼠标进入和离开画布的事件监听this.$refs.canvas.addEventListener('mouseenter', this.onMouseEnter);this.$refs.canvas.addEventListener('mouseleave', this.onMouseLeave);},beforeDestroy() {// 移除事件监听器this.$refs.canvas.removeEventListener('mouseenter', this.onMouseEnter);this.$refs.canvas.removeEventListener('mouseleave', this.onMouseLeave);},methods: {// 清空画布test() {this.boxes = []this.$nextTick(() => {this.draw();})},// 删除区域del() {if (this.form.id !== null) {this.boxes = this.boxes.filter(box => box.id !== this.form.id); // 根据ID删除多边形// this.form.id = null; // 清空ID // 清空formthis.form = {id: null,type: '',jsonData: ''};this.dialogVisible = false;this.$nextTick(() => {this.adjustBoxesOnResize();this.draw();})}},// 确认clickOk() {this.$refs.form.validate((valid) => {if (valid) {if (this.form.id !== null) {const boxIndex = this.boxes.findIndex(box => box.id === this.form.id);if (boxIndex !== -1) {const newCategory = this.form.type === '1' ? '人员' : '2' ? '车辆' : '';this.boxes[boxIndex] = {...this.boxes[boxIndex],category: newCategory,jsonData: this.form.jsonData};}}this.dialogVisible = false;this.draw();}});},// 点击框框onClick(event) {const rect = this.$refs.canvas.getBoundingClientRect();const mouseX = event.clientX - rect.left;const mouseY = event.clientY - rect.top;for (let box of this.boxes) {if (mouseX >= box.start.x && mouseX <= box.end.x &&mouseY >= box.start.y && mouseY <= box.end.y) {// console.log("点击的多边形参数", box);let jsons = box.category === '人员' ? `{\n"id": 0,\n"lifeJacket": true,\n"raincoat": false,\n"reflectiveVest": false,\n"safetyHat": { "color": "red" },\n"type": "rectangle",\n"workingClothes": false\n}` : `{\n"carType": "forklift",\n"hasGoods": true,\n"id": 0,\n"speed": 100,\n"type": "rectangle"\n}`this.form = {id: box.id, // 保存当前选中的多边形IDtype: box.category === '人员' ? '1' : '2',jsonData: box.jsonData || jsons,};this.dialogVisible = true;break;}}},// 新增的方法onMouseEnter() {// 当鼠标进入画布时,初始化光标样式为默认this.$refs.canvas.style.cursor = 'default';},// 当鼠标离开画布时,确保光标样式为默认onMouseLeave() {this.$refs.canvas.style.cursor = 'default';},adjustBoxesOnResize() {if (this.originalCanvasWidth === 0 || this.originalCanvasHeight === 0) return;const scaleX = this.canvasWidth / this.originalCanvasWidth;const scaleY = this.canvasHeight / this.originalCanvasHeight;this.boxes = this.boxes.map(box => ({id: box.id,category: box.category,start: {x: box.start.x * scaleX,y: box.start.y * scaleY},end: {x: box.end.x * scaleX,y: box.end.y * scaleY},jsonData: box.jsonData,}));this.originalCanvasWidth = this.canvasWidth;this.originalCanvasHeight = this.canvasHeight;},// 开始绘制startDraw(event) {if (event.which !== 1) return;if (!this.type) {this.$message({message: '请先选择车辆类型',type: 'warning'});return;}this.isDrawing = true;const rect = this.$refs.canvas.getBoundingClientRect();const scaleX = this.canvasWidth / this.originalCanvasWidth;const scaleY = this.canvasHeight / this.originalCanvasHeight;this.start = {x: (event.clientX - rect.left) / scaleX,y: (event.clientY - rect.top) / scaleY};// 记录鼠标按下的时间this.startTime = Date.now();},// 鼠标移动时更新绘制终点并重绘onMouseMove(event) {if (!this.isDrawing) {const rect = this.$refs.canvas.getBoundingClientRect();const mouseX = event.clientX - rect.left;const mouseY = event.clientY - rect.top;let cursorStyle = 'default';// 检查鼠标是否在任何框内for (let box of this.boxes) {if (mouseX >= box.start.x && mouseX <= box.end.x &&mouseY >= box.start.y && mouseY <= box.end.y) {cursorStyle = 'pointer';break; // 找到一个匹配的框后停止搜索}}// 更新光标样式this.$refs.canvas.style.cursor = cursorStyle;}// 继续原有逻辑if (!this.isDrawing) return;const rect = this.$refs.canvas.getBoundingClientRect();const scaleX = this.canvasWidth / this.originalCanvasWidth;const scaleY = this.canvasHeight / this.originalCanvasHeight;this.end = {x: (event.clientX - rect.left) / scaleX,y: (event.clientY - rect.top) / scaleY};this.draw();},// 结束绘制endDraw(event) {if (!this.type) return;this.isDrawing = false;const endTime = Date.now(); // 获取鼠标释放的时间const timeDifference = endTime - this.startTime; // 计算时间差// 如果时间差小于 100 毫秒,则认为用户只是点击了一下if (timeDifference < 200) {return;}const distanceThreshold = 5; // 定义一个最小距离阈值const distance = Math.sqrt(Math.pow((this.end.x - this.start.x), 2) +Math.pow((this.end.y - this.start.y), 2));// 只有当距离大于阈值时才绘制框if (distance > distanceThreshold) {const boxId = Date.now(); // 生成唯一的时间戳IDthis.boxes.push({id: boxId, // 添加唯一IDstart: this.start,end: this.end,category: this.selectedCategory.modelName,jsonData: '' // 初始JSON数据为空});this.draw();}},// 删除选中的框deleteSelectedBoxes() {this.boxes = this.boxes.filter(box => box.category !== this.selectedCategory.modelName);this.draw();},// 绘制方法draw() {const canvas = this.$refs.canvas;const context = canvas.getContext('2d');context.clearRect(0, 0, canvas.width, canvas.height);if (this.boxes.length > 0) {// 绘制所有的框this.boxes.forEach(box => {context.strokeStyle = this.categoryColors[box.category] || 'red'; // 默认为红色context.strokeRect(box.start.x, box.start.y, box.end.x - box.start.x, box.end.y - box.start.y);context.fillStyle = '#fff'; // 设置文字颜色为黑色context.fillText(box.category, box.start.x, box.start.y - 5);});}// 绘制当前的草图框if (this.isDrawing) {const scaleX = this.canvasWidth / this.originalCanvasWidth;const scaleY = this.canvasHeight / this.originalCanvasHeight;context.strokeStyle = this.type === '2' ? 'red' : this.type === '1' ? 'yellow' : '#000000';context.strokeRect(this.start.x * scaleX,this.start.y * scaleY,(this.end.x - this.start.x) * scaleX,(this.end.y - this.start.y) * scaleY);}// console.log("所有框", this.boxes);},},
}
</script><style lang="scss" scoped>
.all {position: relative;width: 100%;height: 100%;.dialogDiv {width: 100%;}
}#mycanvas {position: absolute;top: 0;bottom: 0;left: 0;right: 0;width: 100%;height: 100%;
}#img {width: 100%;height: 100%;user-select: none;
}
</style>

父组件引入使用

 <CanvasBox ref="CanvasBox" v-if="canvasIsShow" :imgUrl="imgUrl" :type="form.type" :canvasWidth="canvasWidth" :canvasHeight="canvasHeight" :timeStamp="timeStamp" />

如果canvas是宽高不固定,可以改成响应式的
父组件中:

  mounted() {window.addEventListener('resize', this.onWindowResize);// 监听盒子尺寸变化// this.observeBoxWidth();},methods: {// 清空画布clearCanvas() {this.timeStamp = Date.now();},onWindowResize() {const offsetWidth = this.$refs.divideBox.offsetWidth;const offsetHeight = this.$refs.divideBox.offsetHeight;this.canvasWidth = offsetWidththis.canvasHeight = offsetHeight// console.log("canvas画布宽高", offsetWidth, offsetHeight);},// 保存async submitForm() {if (this.form.cameraId == null || this.form.cameraId == undefined) {this.$message({message: "请先选择摄像头",type: "warning",});return;}let newData = {"cameraId": this.form.cameraId,"photoCodeType": this.form.photoCodeType,"sendDataDtoList": [// {//   "type": 2,//   "pointList": [//     [//       544.45,//       432.42//     ],//     [//       595.19,//       455.17//     ]//   ],//   "jsonData": "{\"carType\":\"forklift\",\"hasGoods\":true,\"id\":0,\"speed\":100,\"type\":\"rectangle\"}"// }]}// 现在盒子的宽高const offsetWidth = this.$refs.divideBox.offsetWidthconst offsetHeight = this.$refs.divideBox.offsetWidth / this.pxData.x * this.pxData.yconst boxesData = JSON.parse(JSON.stringify(this.$refs.CanvasBox.boxes))if (boxesData && boxesData.length > 0) {boxesData.forEach(item => {newData.sendDataDtoList.push({type: this.findValueByLabel(item.category),pointList: [[item.start.x / offsetWidth * this.pxData.x,item.start.y / offsetHeight * this.pxData.y,],[item.end.x / offsetWidth * this.pxData.x,item.end.y / offsetHeight * this.pxData.y,]],jsonData: item.jsonData})})}console.log("发送车辆信息", newData);const { code } = await getRegionalTools(newData);if (code === 200) {this.$message({message: '发送成功',type: 'success'});}},findValueByLabel(label) {const item = this.carTypeList.find(item => item.label === label);return item ? item.value : null;},},

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

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

相关文章

开源音乐分离器Audio Decomposition:可实现盲源音频分离,无需外部乐器分离库,从头开始制作。将音乐转换为五线谱的程序

今天给大家分析一个音频分解器&#xff0c;通过傅里叶变换和信封匹配分离音乐中的各个音符和乐器&#xff0c;实现音乐到乐谱的转换。将音乐开源分离为组成乐器。该方式是盲源分离&#xff0c;从头开始制作&#xff0c;无需外部乐器分离库。 相关链接 代码&#xff1a;https:…

智慧安防丨以科技之力,筑起防范人贩的铜墙铁壁

近日&#xff0c;贵州省贵阳市中级人民法院对余华英拐卖儿童案做出了一审宣判&#xff0c;判处其死刑&#xff0c;剥夺政治权利终身&#xff0c;并处没收个人全部财产。这一判决不仅彰显了法律的威严&#xff0c;也再次唤起了社会对拐卖儿童犯罪的深切关注。 余华英自1993年至2…

【原创】java+ssm+mysql房屋租赁管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

Linux高阶——1116—环形队列生产者消费者

目录 1、环形队列 2、生产者消费者 环形队列数组实现代码 成功截图 1、环形队列 相比于线性队列&#xff0c;环形队列可以有效避免访问越界问题&#xff0c;使用下标访问队列元素时&#xff0c;到达末尾后下标归0&#xff0c;返回起始位置&#xff0c;使用下标运算即可 a…

构建SSH僵尸网络

import argparse import paramiko# 定义一个名为Client的类&#xff0c;用于表示SSH客户端相关操作 class Client:# 类的初始化方法&#xff0c;接收主机地址、用户名和密码作为参数def __init__(self, host, user, password):self.host hostself.user userself.password pa…

199. 二叉树的右视图【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 199. 二叉树的右视图 一、题目描述 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 二…

Mongo数据库集群搭建

目录 1、Mongo集群优势 1.1 高可用性 1.2 水平扩展性 1.3 高性能 1.4 灵活的架构设计 1.5 数据安全 1.6 管理与监控 2、下载指定操作系统版本包 3、部署和验证工作 3.1 准备配置文件及依赖 3.2 启动第一个节点 3.3 部署更多的节点 3.4 初始化副本集 3.5 设置管理…

DB Type

P位 p 1时段描述符有效&#xff0c;p 0时段描述符无效 Base Base被分成了三个部分&#xff0c;按照实际拼接即可 G位 如果G 0 说明描述符中Limit的单位是字节&#xff0c;如果是G 1 &#xff0c;那么limit的描述的单位是页也就是4kb S位 S 1 表示代码段或者数据段描…

Qt 5.6.3 手动配置 mingw 环境

- 安装 qt 5.6.3 mingw 版 - 打开 qt creator - 找到选项 工具 - 选项- 构建和运行 - 找到 “编译器” 选项卡 ,点击 "添加" “编译器路径” 设置为 qt 安装目录下&#xff0c; tool 文件夹内的 g.exe 设置完成后&#xff0c;点击 "apply" ,使选项生…

k8s上部署redis高可用集群

介绍&#xff1a; Redis Cluster通过分片&#xff08;sharding&#xff09;来实现数据的分布式存储&#xff0c;每个master节点都负责一部分数据槽&#xff08;slot&#xff09;。 当一个master节点出现故障时&#xff0c;Redis Cluster能够自动将故障节点的数据槽转移到其他健…

抖音热门素材去哪找?优质抖音视频素材网站推荐!

是不是和我一样&#xff0c;刷抖音刷到停不下来&#xff1f;越来越多的朋友希望在抖音上创作出爆款视频&#xff0c;但苦于没有好素材。今天就来推荐几个超级实用的抖音视频素材网站&#xff0c;让你的视频内容立刻变得高大上&#xff01;这篇满是干货&#xff0c;直接上重点&a…

Dify 通过导入 DSL 文件创建 Workflow 过程及实现

本文使用 Dify v0.9.2 版本&#xff0c;主要介绍 Dify 通过导入 DSL&#xff08;或 URL&#xff09;文件创建&#xff08;或导出&#xff09;Workflow 的操作过程及源码分析实现过程。Dify通过导入DSL文件创建Workflow过程及实现&#xff1a;https://z0yrmerhgi8.feishu.cn/wik…

代码随想录第46期 单调栈

这道题主要是单调栈的简单应用 class Solution { public:vector<int> dailyTemperatures(vector<int>& T) {vector<int> result(T.size(),0);stack<int> st;st.push(0);for(int i1;i<T.size();i){if(T[i]<T[st.top()]){st.push(i);}else{wh…

3步实现贪吃蛇

方法很简单&#xff0c;打开页面&#xff0c;复制&#xff0c;粘贴 一.整体思维架构 我们根据游戏的开始&#xff0c;运行&#xff0c;结束&#xff0c;将整个游戏划分成三个部分。在每个部分下面又划分出多个功能&#xff0c;接下来我们就根据模块一一实现功能。 二.Gamesta…

【linux012】文件操作命令篇 - more 命令

文章目录 more 命令1、基本用法2、常见选项3、交互式键盘命令4、举例5、注意事项 more 命令 more 是 Linux 中的一个分页查看命令&#xff0c;用于逐屏显示文件内容。它特别适合用于查看较长的文件&#xff0c;与 cat 不同&#xff0c;more 不会一次性输出所有内容&#xff0c…

机器学习笔记2 - 机器学习的一般流程

image.png 1、数据基本处理 数据集的划分 根据用途可将获取到的数据划分为训练集和测试集&#xff0c;有时还会有验证集。一般而言训练集用于训练模型&#xff0c;测试集用于测试模型的效果&#xff08;泛化误差&#xff09;。严格来讲&#xff0c;测试集的数据不能直接或间接&…

《C陷阱与缺陷》

文章目录 1、【词法陷阱】1.1 符号与组成符号间的关系1.1 与 1.3 y x/*p 与 y x/(*p)&#xff0c;a-1 与 a - 1 与 a -1, 老版本编译器的处理是不同的&#xff0c;严格的ANSI C则会报错1.4 十进制的 076&#xff0c;会被处理为八进制&#xff0c;ANSI C禁止这种用法&#x…

小白快速上手 labelme:新手图像标注详解教程

前言 本教程主要面向初次使用 labelme 的新手&#xff0c;详细介绍了如何在 Windows 上通过 Anaconda 创建和配置环境&#xff0c;并使用 labelme 进行图像标注。 1. 准备工作 在开始本教程之前&#xff0c;确保已经安装了 Anaconda。可以参考我之前的教程了解 Anaconda 的下…

脑机接口、嵌入式 AI 、工业级 MR、空间视频和下一代 XR 浏览器丨RTE2024 空间计算和新硬件专场回顾

这一轮硬件创新由 AI 引爆&#xff0c;或许最大受益者仍是 AI&#xff0c;因为只有硬件才能为 AI 直接获取最真实世界的数据。 在人工智能与硬件融合的新时代&#xff0c;实时互动技术正迎来前所未有的创新浪潮。从嵌入式系统到混合现实&#xff0c;从空间视频到脑机接口&…

【STM32】MPU6050简介

文章目录 MPU6050简介MPU6050关键块带有16位ADC和信号调理的三轴MEMS陀螺仪具有16位ADC和信号调理的三轴MEMS加速度计I2C串行通信接口 MPU6050对应的数据手册&#xff1a;MPU6050 陀螺仪加速度计 链接: https://pan.baidu.com/s/13nwEhGvsfxx0euR2hMHsyw?pwdv2i6 提取码: v2i6…