vue-canvas-创建矩形框对指定区域的点数据进行坐标变换

news/2024/12/19 16:53:06/文章来源:https://www.cnblogs.com/Frey-Li/p/18617563

demo简介

  1. 读取两个csv文件(geo数据和drawing数据)
  2. 绘制散点图
  3. 使用矩形框选中范围内的数据(只选中drawing数据)
  4. 拖动矩形框 或 reshape矩形框,同时,矩形框内的数据点坐标也相应变换

核心代码介绍

1 template

  • 设置了工具栏和画布作为两个核心组件
    • 工具栏包含”绘制矩形框”,“删除矩形框”,“还原初始状态”和“导出数据”四个功能
    • canvas包含四个鼠标事件,鼠标按下,鼠标移动,鼠标松开和鼠标离开画布
<template><div class="match-container"><div class="toolbar"><button @click="startDrawingRect">绘制矩形框</button><button v-if="selectedRect" @click="deleteSelectedRect">删除选中的矩形框</button><button @click="resetToInitialState">还原初始状态</button><button @click="exportToCSV">导出数据</button></div><div class="canvas-container"><canvas ref="canvas" width="850" height="650" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp" @mouseleave="onMouseLeave"></canvas></div></div>
</template>

2 定义变量

  • 创建Rectangle类型时,我们要定义一个数据来记录包含在矩形内的所有数据点的索引
    • 记录索引能使我们在移动矩形框的位置后,其影响的数据点还是原始位置的那些数据点,不会影响到移动后的位置上的数据点
// ================ 类型定义 ================
interface Rectangle {x1: number;y1: number;x2: number;y2: number;selectedPointIndices: number[]; // 存储矩形内点的索引
}// ================ 状态变量 ================
// 数据相关
const geoData = ref([]);                    // 地理数据点
const drawingData = ref([]);                // 绘制数据点
const initialDrawingData = ref([]);         // 初始状态的数据
const rectangles = ref<Rectangle[]>([]);    // 矩形框数组// 坐标系相关
const geoOffsetX = ref(0);                  // 地理数据X偏移
const geoOffsetY = ref(0);                  // 地理数据Y偏移
const width = ref(0);                       // 画布宽度
const height = ref(0);                      // 画布高度
const padding = ref(20);                    // 画布内边距
const scaleX = ref(0);                      // X轴缩放比例
const scaleY = ref(0);                      // Y轴缩放比例
const minX = ref(0);                        // X轴最小值
const minY = ref(0);                        // Y轴最小值
const maxX = ref(0);                        // X轴最大值
const maxY = ref(0);                        // Y轴最大值// 交互状态
const CORNER_SIZE = 10;                     // 角落判定范围大小
const isDrawing = ref(false);               // 是否正在绘制矩形
const selectedRect = ref(null);             // 当前选中的矩形
const isDragging = ref(false);              // 是否正在拖拽矩形
const isResizing = ref(false);              // 是否正在调整大小
const resizeCorner = ref('');               // 正在调整的角落
const dragStartX = ref(0);                  // 拖拽起始X坐标
const dragStartY = ref(0);                  // 拖拽起始Y坐标// 数据源路径
const geoDataUrl = './datasets/match/g_data.csv';
const drawingDataUrl = './datasets/match/d_data.csv';

3 数据加载与处理

  • 加载数据时,增加一个状态标记,方便后续进行矩形内的数据点选择
  • 使用initialDrawingData来记录初始状态,方便还原
  • 对考虑到原始数据有一个偏移值,不便于绘制,就先进行偏移,使数据靠近原点(0,0)坐标
// ================ 数据处理函数 ================
/*** 加载CSV数据* @param url - CSV文件路径* @returns 解析后的数据数组*/const loadCSV = async (url) => {const response = await fetch(url);const text = await response.text();const parsedData = text.split('\n').slice(1).map(row => {const columns = row.split(' ');return [...columns.map((column, index) => {if (index === 0 || index === 1) {return Number(column.trim());}return column.trim();}),false  // 添加选中状态标记,默认为 false];});// 深拷贝保存初始状态initialDrawingData.value = JSON.parse(JSON.stringify(parsedData));return parsedData;};/*** 对齐两组数据的坐标系*/const offsetTwoData = (geoData, drawingData) => {const minGeoX = Math.min(...geoData.value.map(row => row[0]));const minGeoY = Math.min(...geoData.value.map(row => row[1]));const minDrawingX = Math.min(...drawingData.value.map(row => row[0]));const minDrawingY = Math.min(...drawingData.value.map(row => row[1]));const minX = Math.min(minGeoX, minDrawingX);const minY = Math.min(minGeoY, minDrawingY);geoData.value.forEach(row => {row[0] = Number(row[0]) - minX;row[1] = Number(row[1]) - minY;});drawingData.value.forEach(row => {row[0] = Number(row[0]) - minX;row[1] = Number(row[1]) - minY;});geoOffsetX.value = minX;geoOffsetY.value = minY;};

4 矩形框相关操作

  • isPointInRect函数和getPointsInRect函数能够获取所绘制的矩形框内的所有数据点的索引
  • updateSelectedPoints函数和updatePointsOnResize函数分别对矩形拖动矩形缩放两种情况进行数据点坐标的更新
// ================ 矩形操作函数 ================
/*** 判断点是否在矩形内*/const isPointInRect = (point, rect) => {const [x, y] = point;const minX = Math.min(rect.x1, rect.x2);const maxX = Math.max(rect.x1, rect.x2);const minY = Math.min(rect.y1, rect.y2);const maxY = Math.max(rect.y1, rect.y2);return x > minX && x < maxX && y > minY && y < maxY;};/*** 获取矩形内的所有点索引*/const getPointsInRect = (rect: Rectangle) => {const indices: number[] = [];drawingData.value.forEach((point, index) => {// 将数据坐标转换为画布坐标const canvasX = (point[0] - minX.value) * scaleX.value + padding.value;const canvasY = height.value - (point[1] - minY.value) * scaleY.value + padding.value;if (isPointInRect([canvasX, canvasY], rect)) {indices.push(index);// 设置点的选中状态point[3] = true;}});return indices;};/*** 更新矩形内点的位置(拖动时)*/const updateSelectedPoints = (dx: number, dy: number, rect: Rectangle) => {// 将画布位移转换为数据位移const dataDX = dx / scaleX.value;const dataDY = -dy / scaleY.value; // 注意Y轴方向相反// 只更新矩形内的点rect.selectedPointIndices.forEach(index => {const point = drawingData.value[index];point[0] += dataDX;point[1] += dataDY;});};/*** 更新矩形内点的位置(缩放时)*/const updatePointsOnResize = (rect: Rectangle, oldRect: Rectangle) => {rect.selectedPointIndices.forEach(index => {const point = drawingData.value[index];// 将数据坐标转换为画布坐标const canvasX = (point[0] - minX.value) * scaleX.value + padding.value;const canvasY = height.value - (point[1] - minY.value) * scaleY.value + padding.value;// 计算点在原矩形中的相对位置(0-1之间)const oldWidth = oldRect.x2 - oldRect.x1;const oldHeight = oldRect.y2 - oldRect.y1;const relativeX = (canvasX - oldRect.x1) / oldWidth;const relativeY = (canvasY - oldRect.y1) / oldHeight;// 计算点在新矩形中的位置const newWidth = rect.x2 - rect.x1;const newHeight = rect.y2 - rect.y1;const newCanvasX = rect.x1 + (newWidth * relativeX);const newCanvasY = rect.y1 + (newHeight * relativeY);// 将新的画布坐标转换回数据坐标point[0] = (newCanvasX - padding.value) / scaleX.value + minX.value;point[1] = (height.value - newCanvasY + padding.value) / scaleY.value + minY.value;});};

5 事件处理相关操作

  • 当按下鼠标时,有三种情况
    • 开始绘制矩形
    • 选择了某个矩形,准备进行拖动或者缩放操作
    • 没选中矩形
  • 鼠标移动事件有两种触发情况
    • 拖拽矩形的角点,来改变矩形的尺寸
    • 拖动矩形,改变矩形的位置
  • 鼠标释放事件
    • 绘制状态:结束绘制,生成一个矩形
  • 鼠标离开画布事件
    • 取消绘制状态
// ================ 事件处理函数 ================
/*** 鼠标按下事件处理*/const onMouseDown = (event) => {const x = event.offsetX;const y = event.offsetY;if (isDrawing.value) {// 开始绘制新矩形selectedRect.value = { x1: x, y1: y, x2: x, y2: y, selectedPointIndices: [] };} else {// 查找点击的是否在某个已有矩形内const clickedRect = rectangles.value.find(rect => {// 先检查是否点击在角落if (rect === selectedRect.value) {const corner = getClickedCorner(x, y, rect);if (corner) {isResizing.value = true;resizeCorner.value = corner;return true;}}// 再检查是否点击在矩形内return isInsideRect(x, y, rect);});if (clickedRect) {selectedRect.value = clickedRect;if (!isResizing.value) {isDragging.value = true;}dragStartX.value = x;dragStartY.value = y;} else {selectedRect.value = null;}}};// 鼠标按下事件:判断是否点击在角落const getClickedCorner = (x, y, rect) => {const { x1, y1, x2, y2 } = rect;// 检查左上角if (Math.abs(x - x1) <= CORNER_SIZE && Math.abs(y - y1) <= CORNER_SIZE) {return 'topLeft';}// 检查右下角if (Math.abs(x - x2) <= CORNER_SIZE && Math.abs(y - y2) <= CORNER_SIZE) {return 'bottomRight';}return '';};// 鼠标按下事件:检查点是否在矩形内const isInsideRect = (x, y, rect) => {return x >= rect.x1 && x <= rect.x2 && y >= rect.y1 && y <= rect.y2;};/*** 鼠标移动事件处理*/const onMouseMove = (event) => {const x = event.offsetX;const y = event.offsetY;if (isDrawing.value && selectedRect.value) {selectedRect.value.x2 = x;selectedRect.value.y2 = y;} else if (isResizing.value && selectedRect.value) {// 保存调整前的矩形状态const oldRect = { ...selectedRect.value };if (resizeCorner.value === 'topLeft') {selectedRect.value.x1 = x;selectedRect.value.y1 = y;} else if (resizeCorner.value === 'bottomRight') {selectedRect.value.x2 = x;selectedRect.value.y2 = y;}// 更新点位置updatePointsOnResize(selectedRect.value, oldRect);} else if (isDragging.value && selectedRect.value) {// 计算移动距离const dx = x - dragStartX.value;const dy = y - dragStartY.value;// 移动矩形selectedRect.value.x1 += dx;selectedRect.value.y1 += dy;selectedRect.value.x2 += dx;selectedRect.value.y2 += dy;// 只更新选中的点updateSelectedPoints(dx, dy, selectedRect.value);// 更新拖动起始位置dragStartX.value = x;dragStartY.value = y;}drawCanvas();};/*** 鼠标释放事件处理*/const onMouseUp = () => {if (isDrawing.value && selectedRect.value) {// 获取矩形内的点的索引const selectedIndices = getPointsInRect(selectedRect.value);// 创建新矩形,包含选中点的索引const newRect: Rectangle = {...selectedRect.value,selectedPointIndices: selectedIndices};rectangles.value.push(newRect);selectedRect.value = null;isDrawing.value = false;}// 重置所有状态isDragging.value = false;isResizing.value = false;resizeCorner.value = '';drawCanvas();};/*** 鼠标离开画布事件处理*/const onMouseLeave = () => {if (isDrawing.value) {// 取消绘制矩形的状态selectedRect.value = null;isDrawing.value = false;drawCanvas();}};

6 工具栏相关操作

  • 开始绘制函数
  • 删除矩形函数:当删除矩形后,矩形内的drawingData的数据点的位置被固定(除非重新绘制一个矩形)
  • 还原到初始状态:防止矩形的reshape等操作出问题,提供还原功能
  • 导出到csv:将reshape的drawingData数据导出,实战时应该传回后端,重新匹配
// ================ 功能操作函数 ================
/*** 开始绘制矩形*/
const startDrawingRect = () => {isDrawing.value = true;
};/*** 删除选中的矩形*/const deleteSelectedRect = () => {if (selectedRect.value) {// 找到选中矩形的索引const index = rectangles.value.findIndex(rect => rect === selectedRect.value);if (index > -1) {// 恢复该矩形内点的未选中状态selectedRect.value.selectedPointIndices.forEach(pointIndex => {drawingData.value[pointIndex][3] = false;});// 从数组中移除该矩形rectangles.value.splice(index, 1);selectedRect.value = null;// 重绘画布drawCanvas();}}};/*** 还原到初始状态*/const resetToInitialState = () => {// 确认对话框if (rectangles.value.length > 0 && !confirm('确定要还原到初始状态吗?这将清除所有矩形框。')) {return;}// 还原数据到初始状态drawingData.value = JSON.parse(JSON.stringify(initialDrawingData.value));// 清除所有矩形rectangles.value = [];selectedRect.value = null;// 重置所有状态isDrawing.value = false;isDragging.value = false;isResizing.value = false;resizeCorner.value = '';// 重绘画布drawCanvas();};/*** 导出数据到CSV*/const exportToCSV = () => {try {// 准备数据const exportData = drawingData.value.map(point => {// 还原偏移,转换回原始坐标const originalX = point[0] + geoOffsetX.value;const originalY = point[1] + geoOffsetY.value;// 返回转换后的数据(只包含坐标和标签,不包含选中状态)return [originalX.toFixed(6), // 保留6位小数originalY.toFixed(6),point[2] // 标签].join(' '); // 使用空格分隔});// 添加CSV头部const header = 'x y label'; // CSV文件的头部const csvContent = [header, ...exportData].join('\n');// 创建Blob对象const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });// 创建下载链接const link = document.createElement('a');const url = URL.createObjectURL(blob);// 设置下载属性link.setAttribute('href', url);link.setAttribute('download', `drawing_data_${getFormattedDateTime()}.csv`);// 添加到文档并触发下载document.body.appendChild(link);link.click();// 清理document.body.removeChild(link);URL.revokeObjectURL(url);// 提示成功alert('数据导出成功!');} catch (error) {console.error('导出失败:', error);alert('导出失败,请查看控制台了解详情。');}};// 导出功能:获取格式化的日期时间字符串const getFormattedDateTime = () => {const now = new Date();return `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;};

7 画布渲染

 const drawCanvas = () => {const canvas = document.querySelector('canvas');const ctx = canvas?.getContext('2d');if (!ctx) return;// 获取数据的最大最小值const allData = [...geoData.value, ...drawingData.value];const xValues = allData.map(row => row[0]);const yValues = allData.map(row => row[1]);minX.value = Math.min(...xValues);maxX.value = Math.max(...xValues);minY.value = Math.min(...yValues);maxY.value = Math.max(...yValues);// 设置坐标轴范围padding.value = 20;width.value = canvas.width - 2 * padding.value;height.value = canvas.height - 2 * padding.value;// 计算比例尺scaleX.value = width.value / (maxX.value - minX.value);scaleY.value = height.value / (maxY.value - minY.value);// 清空画布ctx.clearRect(0, 0, canvas.width, canvas.height);// 绘制坐标轴ctx.beginPath();ctx.moveTo(padding.value, padding.value);ctx.lineTo(padding.value, height.value + padding.value);ctx.lineTo(width.value + padding.value, height.value + padding.value);ctx.stroke();// 绘制 x 轴标签ctx.fillText(minX.value.toFixed(2), padding.value, height.value + padding.value + 15);ctx.fillText(maxX.value.toFixed(2), width.value + padding.value, height.value + padding.value + 15);// 绘制 y 轴标签ctx.fillText(maxY.value.toFixed(2), padding.value - 15, padding.value);ctx.fillText(minY.value.toFixed(2), padding.value - 15, height.value + padding.value);// 绘制 geoData 数据点ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';geoData.value.forEach(row => {const x = (row[0] - minX.value) * scaleX.value + padding.value;const y = height.value - (row[1] - minY.value) * scaleY.value + padding.value;ctx.beginPath();ctx.arc(x, y, 2, 0, Math.PI * 2);ctx.fill();});// 绘制 drawingData 数据点drawingData.value.forEach(point => {const x = (point[0] - minX.value) * scaleX.value + padding.value;const y = height.value - (point[1] - minY.value) * scaleY.value + padding.value;// 根据选中状态设置颜色ctx.fillStyle = point[3] ? 'green' : 'red';ctx.beginPath();ctx.arc(x, y, 2, 0, Math.PI * 2);ctx.fill();});// 绘制所有矩形rectangles.value.forEach(rect => {// 设置矩形样式ctx.strokeStyle = rect === selectedRect.value ? 'red' : 'blue';ctx.lineWidth = 2;// 绘制矩形ctx.strokeRect(rect.x1, rect.y1, rect.x2 - rect.x1, rect.y2 - rect.y1);// 如果是选中的矩形,绘制调整大小的角落标记if (rect === selectedRect.value) {ctx.fillStyle = 'blue';// 左上角ctx.fillRect(rect.x1 - CORNER_SIZE/2, rect.y1 - CORNER_SIZE/2, CORNER_SIZE, CORNER_SIZE);// 右下角ctx.fillRect(rect.x2 - CORNER_SIZE/2, rect.y2 - CORNER_SIZE/2, CORNER_SIZE, CORNER_SIZE);}});// 如果正在绘制新矩形,也绘制它if (isDrawing.value && selectedRect.value) {ctx.strokeStyle = 'red';ctx.lineWidth = 2;ctx.strokeRect(selectedRect.value.x1,selectedRect.value.y1,selectedRect.value.x2 - selectedRect.value.x1,selectedRect.value.y2 - selectedRect.value.y1);}};

8 其余操作

// ================ 生命周期钩子 ================
onMounted(async () => {// 加载数据geoData.value = await loadCSV(geoDataUrl);drawingData.value = await loadCSV(drawingDataUrl);// 处理数据offsetTwoData(geoData, drawingData);initialDrawingData.value = JSON.parse(JSON.stringify(drawingData.value));// 初始化画布drawCanvas();
});
</script><style scoped>
.match-container {height: calc(92vh - 60px);border: 1px solid black;
}.canvas-container {width: 50%;height: 100%;border: 1px solid black;margin: auto;
}.toolbar {padding: 10px;display: flex;gap: 10px;
}.toolbar button {padding: 5px 10px;cursor: pointer;
}.toolbar button:disabled {cursor: not-allowed;opacity: 0.5;
}</style>

完整代码

<template><div class="match-container"><div class="toolbar"><button @click="startDrawingRect">绘制矩形框</button><button v-if="selectedRect" @click="deleteSelectedRect">删除选中的矩形框</button><button @click="resetToInitialState">还原初始状态</button><button @click="exportToCSV">导出数据</button></div><div class="canvas-container"><canvas ref="canvas" width="850" height="650" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp" @mouseleave="onMouseLeave"></canvas></div></div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue';// ================ 类型定义 ================
interface Rectangle {x1: number;y1: number;x2: number;y2: number;selectedPointIndices: number[]; // 存储矩形内点的索引
}// ================ 状态变量 ================
// 数据相关
const geoData = ref([]);                    // 地理数据点
const drawingData = ref([]);                // 绘制数据点
const initialDrawingData = ref([]);         // 初始状态的数据
const rectangles = ref<Rectangle[]>([]);    // 矩形框数组// 坐标系相关
const geoOffsetX = ref(0);                  // 地理数据X偏移
const geoOffsetY = ref(0);                  // 地理数据Y偏移
const width = ref(0);                       // 画布宽度
const height = ref(0);                      // 画布高度
const padding = ref(20);                    // 画布内边距
const scaleX = ref(0);                      // X轴缩放比例
const scaleY = ref(0);                      // Y轴缩放比例
const minX = ref(0);                        // X轴最小值
const minY = ref(0);                        // Y轴最小值
const maxX = ref(0);                        // X轴最大值
const maxY = ref(0);                        // Y轴最大值// 交互状态
const CORNER_SIZE = 10;                     // 角落判定范围大小
const isDrawing = ref(false);               // 是否正在绘制矩形
const selectedRect = ref(null);             // 当前选中的矩形
const isDragging = ref(false);              // 是否正在拖拽矩形
const isResizing = ref(false);              // 是否正在调整大小
const resizeCorner = ref('');               // 正在调整的角落
const dragStartX = ref(0);                  // 拖拽起始X坐标
const dragStartY = ref(0);                  // 拖拽起始Y坐标// 数据源路径
const geoDataUrl = './datasets/match/g_data.csv';
const drawingDataUrl = './datasets/match/d_data.csv';// ================ 数据处理函数 ================
/*** 加载CSV数据* @param url - CSV文件路径* @returns 解析后的数据数组*/const loadCSV = async (url) => {const response = await fetch(url);const text = await response.text();const parsedData = text.split('\n').slice(1).map(row => {const columns = row.split(' ');return [...columns.map((column, index) => {if (index === 0 || index === 1) {return Number(column.trim());}return column.trim();}),false  // 添加选中状态标记,默认为 false];});// 深拷贝保存初始状态initialDrawingData.value = JSON.parse(JSON.stringify(parsedData));return parsedData;};/*** 对齐两组数据的坐标系*/const offsetTwoData = (geoData, drawingData) => {const minGeoX = Math.min(...geoData.value.map(row => row[0]));const minGeoY = Math.min(...geoData.value.map(row => row[1]));const minDrawingX = Math.min(...drawingData.value.map(row => row[0]));const minDrawingY = Math.min(...drawingData.value.map(row => row[1]));const minX = Math.min(minGeoX, minDrawingX);const minY = Math.min(minGeoY, minDrawingY);geoData.value.forEach(row => {row[0] = Number(row[0]) - minX;row[1] = Number(row[1]) - minY;});drawingData.value.forEach(row => {row[0] = Number(row[0]) - minX;row[1] = Number(row[1]) - minY;});geoOffsetX.value = minX;geoOffsetY.value = minY;};// ================ 矩形操作函数 ================
/*** 判断点是否在矩形内*/const isPointInRect = (point, rect) => {const [x, y] = point;const minX = Math.min(rect.x1, rect.x2);const maxX = Math.max(rect.x1, rect.x2);const minY = Math.min(rect.y1, rect.y2);const maxY = Math.max(rect.y1, rect.y2);return x > minX && x < maxX && y > minY && y < maxY;};/*** 获取矩形内的所有点索引*/const getPointsInRect = (rect: Rectangle) => {const indices: number[] = [];drawingData.value.forEach((point, index) => {// 将数据坐标转换为画布坐标const canvasX = (point[0] - minX.value) * scaleX.value + padding.value;const canvasY = height.value - (point[1] - minY.value) * scaleY.value + padding.value;if (isPointInRect([canvasX, canvasY], rect)) {indices.push(index);// 设置点的选中状态point[3] = true;}});return indices;};/*** 更新矩形内点的位置(拖动时)*/const updateSelectedPoints = (dx: number, dy: number, rect: Rectangle) => {// 将画布位移转换为数据位移const dataDX = dx / scaleX.value;const dataDY = -dy / scaleY.value; // 注意Y轴方向相反// 只更新矩形内的点rect.selectedPointIndices.forEach(index => {const point = drawingData.value[index];point[0] += dataDX;point[1] += dataDY;});};/*** 更新矩形内点的位置(缩放时)*/const updatePointsOnResize = (rect: Rectangle, oldRect: Rectangle) => {rect.selectedPointIndices.forEach(index => {const point = drawingData.value[index];// 将数据坐标转换为画布坐标const canvasX = (point[0] - minX.value) * scaleX.value + padding.value;const canvasY = height.value - (point[1] - minY.value) * scaleY.value + padding.value;// 计算点在原矩形中的相对位置(0-1之间)const oldWidth = oldRect.x2 - oldRect.x1;const oldHeight = oldRect.y2 - oldRect.y1;const relativeX = (canvasX - oldRect.x1) / oldWidth;const relativeY = (canvasY - oldRect.y1) / oldHeight;// 计算点在新矩形中的位置const newWidth = rect.x2 - rect.x1;const newHeight = rect.y2 - rect.y1;const newCanvasX = rect.x1 + (newWidth * relativeX);const newCanvasY = rect.y1 + (newHeight * relativeY);// 将新的画布坐标转换回数据坐标point[0] = (newCanvasX - padding.value) / scaleX.value + minX.value;point[1] = (height.value - newCanvasY + padding.value) / scaleY.value + minY.value;});};// ================ 事件处理函数 ================
/*** 鼠标按下事件处理*/const onMouseDown = (event) => {const x = event.offsetX;const y = event.offsetY;if (isDrawing.value) {// 开始绘制新矩形selectedRect.value = { x1: x, y1: y, x2: x, y2: y, selectedPointIndices: [] };} else {// 查找点击的是否在某个已有矩形内const clickedRect = rectangles.value.find(rect => {// 先检查是否点击在角落if (rect === selectedRect.value) {const corner = getClickedCorner(x, y, rect);if (corner) {isResizing.value = true;resizeCorner.value = corner;return true;}}// 再检查是否点击在矩形内return isInsideRect(x, y, rect);});if (clickedRect) {selectedRect.value = clickedRect;if (!isResizing.value) {isDragging.value = true;}dragStartX.value = x;dragStartY.value = y;} else {selectedRect.value = null;}}};// 鼠标按下事件:判断是否点击在角落const getClickedCorner = (x, y, rect) => {const { x1, y1, x2, y2 } = rect;// 检查左上角if (Math.abs(x - x1) <= CORNER_SIZE && Math.abs(y - y1) <= CORNER_SIZE) {return 'topLeft';}// 检查右下角if (Math.abs(x - x2) <= CORNER_SIZE && Math.abs(y - y2) <= CORNER_SIZE) {return 'bottomRight';}return '';};// 鼠标按下事件:检查点是否在矩形内const isInsideRect = (x, y, rect) => {return x >= rect.x1 && x <= rect.x2 && y >= rect.y1 && y <= rect.y2;};/*** 鼠标移动事件处理*/const onMouseMove = (event) => {const x = event.offsetX;const y = event.offsetY;if (isDrawing.value && selectedRect.value) {selectedRect.value.x2 = x;selectedRect.value.y2 = y;} else if (isResizing.value && selectedRect.value) {// 保存调整前的矩形状态const oldRect = { ...selectedRect.value };if (resizeCorner.value === 'topLeft') {selectedRect.value.x1 = x;selectedRect.value.y1 = y;} else if (resizeCorner.value === 'bottomRight') {selectedRect.value.x2 = x;selectedRect.value.y2 = y;}// 更新点位置updatePointsOnResize(selectedRect.value, oldRect);} else if (isDragging.value && selectedRect.value) {// 计算移动距离const dx = x - dragStartX.value;const dy = y - dragStartY.value;// 移动矩形selectedRect.value.x1 += dx;selectedRect.value.y1 += dy;selectedRect.value.x2 += dx;selectedRect.value.y2 += dy;// 只更新选中的点updateSelectedPoints(dx, dy, selectedRect.value);// 更新拖动起始位置dragStartX.value = x;dragStartY.value = y;}drawCanvas();};/*** 鼠标释放事件处理*/const onMouseUp = () => {if (isDrawing.value && selectedRect.value) {// 获取矩形内的点的索引const selectedIndices = getPointsInRect(selectedRect.value);// 创建新矩形,包含选中点的索引const newRect: Rectangle = {...selectedRect.value,selectedPointIndices: selectedIndices};rectangles.value.push(newRect);selectedRect.value = null;isDrawing.value = false;}// 重置所有状态isDragging.value = false;isResizing.value = false;resizeCorner.value = '';drawCanvas();};/*** 鼠标离开画布事件处理*/const onMouseLeave = () => {if (isDrawing.value) {// 取消绘制矩形的状态selectedRect.value = null;isDrawing.value = false;drawCanvas();}};// ================ 功能操作函数 ================
/*** 开始绘制矩形*/
const startDrawingRect = () => {isDrawing.value = true;
};/*** 删除选中的矩形*/const deleteSelectedRect = () => {if (selectedRect.value) {// 找到选中矩形的索引const index = rectangles.value.findIndex(rect => rect === selectedRect.value);if (index > -1) {// 恢复该矩形内点的未选中状态selectedRect.value.selectedPointIndices.forEach(pointIndex => {drawingData.value[pointIndex][3] = false;});// 从数组中移除该矩形rectangles.value.splice(index, 1);selectedRect.value = null;// 重绘画布drawCanvas();}}};/*** 还原到初始状态*/const resetToInitialState = () => {// 确认对话框if (rectangles.value.length > 0 && !confirm('确定要还原到初始状态吗?这将清除所有矩形框。')) {return;}// 还原数据到初始状态drawingData.value = JSON.parse(JSON.stringify(initialDrawingData.value));// 清除所有矩形rectangles.value = [];selectedRect.value = null;// 重置所有状态isDrawing.value = false;isDragging.value = false;isResizing.value = false;resizeCorner.value = '';// 重绘画布drawCanvas();};/*** 导出数据到CSV*/const exportToCSV = () => {try {// 准备数据const exportData = drawingData.value.map(point => {// 还原偏移,转换回原始坐标const originalX = point[0] + geoOffsetX.value;const originalY = point[1] + geoOffsetY.value;// 返回转换后的数据(只包含坐标和标签,不包含选中状态)return [originalX.toFixed(6), // 保留6位小数originalY.toFixed(6),point[2] // 标签].join(' '); // 使用空格分隔});// 添加CSV头部const header = 'x y label'; // CSV文件的头部const csvContent = [header, ...exportData].join('\n');// 创建Blob对象const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });// 创建下载链接const link = document.createElement('a');const url = URL.createObjectURL(blob);// 设置下载属性link.setAttribute('href', url);link.setAttribute('download', `drawing_data_${getFormattedDateTime()}.csv`);// 添加到文档并触发下载document.body.appendChild(link);link.click();// 清理document.body.removeChild(link);URL.revokeObjectURL(url);// 提示成功alert('数据导出成功!');} catch (error) {console.error('导出失败:', error);alert('导出失败,请查看控制台了解详情。');}};// 导出功能:获取格式化的日期时间字符串const getFormattedDateTime = () => {const now = new Date();return `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;};// ================ 画布渲染函数 ================
/*** 绘制画布内容*/const drawCanvas = () => {const canvas = document.querySelector('canvas');const ctx = canvas?.getContext('2d');if (!ctx) return;// 获取数据的最大最小值const allData = [...geoData.value, ...drawingData.value];const xValues = allData.map(row => row[0]);const yValues = allData.map(row => row[1]);minX.value = Math.min(...xValues);maxX.value = Math.max(...xValues);minY.value = Math.min(...yValues);maxY.value = Math.max(...yValues);// 设置坐标轴范围padding.value = 20;width.value = canvas.width - 2 * padding.value;height.value = canvas.height - 2 * padding.value;// 计算比例尺scaleX.value = width.value / (maxX.value - minX.value);scaleY.value = height.value / (maxY.value - minY.value);// 清空画布ctx.clearRect(0, 0, canvas.width, canvas.height);// 绘制坐标轴ctx.beginPath();ctx.moveTo(padding.value, padding.value);ctx.lineTo(padding.value, height.value + padding.value);ctx.lineTo(width.value + padding.value, height.value + padding.value);ctx.stroke();// 绘制 x 轴标签ctx.fillText(minX.value.toFixed(2), padding.value, height.value + padding.value + 15);ctx.fillText(maxX.value.toFixed(2), width.value + padding.value, height.value + padding.value + 15);// 绘制 y 轴标签ctx.fillText(maxY.value.toFixed(2), padding.value - 15, padding.value);ctx.fillText(minY.value.toFixed(2), padding.value - 15, height.value + padding.value);// 绘制 geoData 数据点ctx.fillStyle = 'rgba(0, 0, 255, 0.5)';geoData.value.forEach(row => {const x = (row[0] - minX.value) * scaleX.value + padding.value;const y = height.value - (row[1] - minY.value) * scaleY.value + padding.value;ctx.beginPath();ctx.arc(x, y, 2, 0, Math.PI * 2);ctx.fill();});// 绘制 drawingData 数据点drawingData.value.forEach(point => {const x = (point[0] - minX.value) * scaleX.value + padding.value;const y = height.value - (point[1] - minY.value) * scaleY.value + padding.value;// 根据选中状态设置颜色ctx.fillStyle = point[3] ? 'green' : 'red';ctx.beginPath();ctx.arc(x, y, 2, 0, Math.PI * 2);ctx.fill();});// 绘制所有矩形rectangles.value.forEach(rect => {// 设置矩形样式ctx.strokeStyle = rect === selectedRect.value ? 'red' : 'blue';ctx.lineWidth = 2;// 绘制矩形ctx.strokeRect(rect.x1, rect.y1, rect.x2 - rect.x1, rect.y2 - rect.y1);// 如果是选中的矩形,绘制调整大小的角落标记if (rect === selectedRect.value) {ctx.fillStyle = 'blue';// 左上角ctx.fillRect(rect.x1 - CORNER_SIZE/2, rect.y1 - CORNER_SIZE/2, CORNER_SIZE, CORNER_SIZE);// 右下角ctx.fillRect(rect.x2 - CORNER_SIZE/2, rect.y2 - CORNER_SIZE/2, CORNER_SIZE, CORNER_SIZE);}});// 如果正在绘制新矩形,也绘制它if (isDrawing.value && selectedRect.value) {ctx.strokeStyle = 'red';ctx.lineWidth = 2;ctx.strokeRect(selectedRect.value.x1,selectedRect.value.y1,selectedRect.value.x2 - selectedRect.value.x1,selectedRect.value.y2 - selectedRect.value.y1);}};// ================ 生命周期钩子 ================
onMounted(async () => {// 加载数据geoData.value = await loadCSV(geoDataUrl);drawingData.value = await loadCSV(drawingDataUrl);// 处理数据offsetTwoData(geoData, drawingData);initialDrawingData.value = JSON.parse(JSON.stringify(drawingData.value));// 初始化画布drawCanvas();
});
</script><style scoped>
.match-container {height: calc(92vh - 60px);border: 1px solid black;
}.canvas-container {width: 50%;height: 100%;border: 1px solid black;margin: auto;
}.toolbar {padding: 10px;display: flex;gap: 10px;
}.toolbar button {padding: 5px 10px;cursor: pointer;
}.toolbar button:disabled {cursor: not-allowed;opacity: 0.5;
}</style>

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

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

相关文章

人车防碰撞识别智慧矿山一体机矿山监控系统中的平台一体机和解码器如何选型?

在构建高效、可靠的视频监控系统时,选择合适的平台一体机和解码器是至关重要的一步。这不仅关系到监控系统的稳定性和可靠性,还直接影响到监控画面的清晰度和系统的扩展性。以下是在选择过程中需要考虑的关键因素,以确保您的监控系统能够满足特定场景的需求,并在未来几年内…

2024年项目管理软件对比:14款高效工具帮你提升工作效率

在快节奏的现代社会,项目管理的重要性日益凸显。为了提高工作效率,各类项目管理软件应运而生。本文将为您介绍14款高效的项目管理工具,包括禅道、Trello、Jira、Asana、Teambition、Wrike、Monday.com、ClickUp、ProjectManager、Basecamp、Zoho Projects、Smartsheet、Liqu…

2024年项目管理软件对比:14款高效工具帮你提升工作效

在快节奏的现代社会,项目管理的重要性日益凸显。为了提高工作效率,各类项目管理软件应运而生。本文将为您介绍14款高效的项目管理工具,包括禅道、Trello、Jira、Asana、Teambition、Wrike、Monday.com、ClickUp、ProjectManager、Basecamp、Zoho Projects、Smartsheet、Liqu…

vb编译环境运行没问题,生成exe运行时报错,错误48加载dll错误,右键以管理员身份运行可以但麻烦,其解决办法如下。

解决办法(推荐): 打开vb后,弹出新建工程标准exe,要点打开。 然后再打开已建的工程,这样生成的exe可以直接双击运行,就不会报错了。2、如需重装vb,要记得“数据访问”点“更改选项”去掉ADO和RDS前面的勾选,不然会一直停在更新状态。 1.打开安装包点击SETUP.EXE(如果会…

Java项目实战之Java小游戏-俄罗斯方块设计与实现(附项目源代码地址)

该项目gitee地址:https://gitee.com/lsy_loren/loren-tetris.git一、游戏概述 本游戏是一款经典的俄罗斯方块游戏,使用Java语言开发,具有图形用户界面(GUI)。玩家通过操作方块的移动、旋转和下落,使其填满一行或多行来消除得分,并随着得分的增加提升等级。游戏还具备暂停…

charles中map local改写接口返回参数

先找到接口-》右键-》save response -》存入桌面然后文件的返回参数 右键-》map local 即可修改返回结果

renben-openstack-keystone操作

controller节点操作source /root/keystonerc_admin 1.查看openstack中keystone的endpoint openstack endpoint list +----------------------------------+-----------+--------------+--------------+| ID | Region | Service Name | Servic…

docker可视化管理工具lazydocker使用

工具介绍 Lazydocker是一个基于Go语言开发的命令行界面Docker管理工具。它通过一个简洁的终端界面,可以实时查看Docker容器、镜像、网络等信息。它提供了交互式的操作方式,可以直接用鼠标操作也可以键盘操作,几乎所有操作都可以通过方向键和快捷键完成,省去了很多需要用命令…

17盒子模型练习-设置背景-附加背景图片

一、元素的水平居中方案 这个是在开发中比较常见的功能,就是元素的水平居中,需要元素在父元素中水平居中显示,父元素一般都是块级元素,inline-block 如果想要居中目前我们学习了两种方案: 行内级元素(包括inline-block元素) 水平居中:在父元素中设置text-align:center …

2024年项目管理工具大对比:15款软件助力团队高效协作

在当今竞争激烈的商业环境中,项目管理的效率直接关系到团队的成败。无论是大型企业的复杂项目,还是小型团队的日常任务,合适的项目管理工具都能起到事半功倍的效果。随着技术的不断发展,市场上涌现出了众多功能各异的项目管理软件。在 2024 年,有 15 款软件脱颖而出,成为…

视频融合平台EasyCVR热知识:5G网络对智能监控系统远程控制的影响有哪些潜在风险?

随着5G技术的快速发展和广泛应用,智能监控系统正迎来一场革命性的变化。5G网络以其高速率、低延迟和大连接数的特性,极大地提升了智能监控系统的远程控制能力,使其在城市管理、交通监控、公共安全等多个领域发挥着越来越重要的作用。然而,正如任何技术进步都伴随着新的挑战…