区域画框:前端实现的带有网格背景、支持不规则框绘制、节点可拖动、右键删除节点以及获取坐标等功能的画框,适用于——摄像头区域侦测设置、预置位设置等

一、概览

基于网格的图形编辑组件:突出其基于网格背景,同时具备图形绘制、编辑(包括节点操作等)的功能。用户可以在网格背景下绘制不规则的图形元素,并对其进行各种编辑操作,方便创建复杂的图形结构。

二、预览

 

三、获取坐标的格式

[{"x":240,"y":135},{"x":240,"y":225},{"x":400,"y":225},{"x":400,"y":135},...]

 

四、功能介绍

结合前面画框功能及常见应用场景,为你详细列举该功能可能具备的特性:

  1. 基础绘制功能
    • 自由绘制不规则形状:用户可通过鼠标或触摸操作,自由绘制任意形状的框,满足对复杂图形的绘制需求,如在图像标注场景中绘制物体的不规则轮廓。
    • 支持多种绘制模式:除了直接绘制,还提供点选模式,用户点击多个点后自动连接形成多边形框;以及手绘模式,模拟真实笔触,让绘制更加自然流畅。
  1. 编辑功能
    • 节点操作:绘制的框上显示可操作节点,用户能拖动节点改变框的形状,实现对已绘制图形的精细调整,适用于对标注精度要求较高的场景。
    • 添加和删除节点:在绘制过程中或绘制完成后,用户可以随时添加新的节点来细化图形,也能删除不需要的节点,优化绘制的框。比如在绘制复杂的地图边界时,通过添加和删除节点来精确描绘边界形状。
    • 整体移动和缩放:支持对整个绘制的框进行移动和缩放操作,方便调整框在画布中的位置和大小,以适应不同的需求。在可视化设计中,可通过整体移动和缩放画框来调整元素布局。
  1. 样式与属性设置
    • 边框样式设置:用户可以自定义框的边框样式,包括线条颜色、粗细、虚线或实线等,增强视觉区分度。在图像编辑中,不同类型的标注可以使用不同样式的边框。
    • 填充设置:为绘制的框添加填充颜色或透明度设置,使框内区域与背景区分开来,或者根据需要设置半透明效果,以便同时查看框内和框外的内容。在数据可视化中,填充不同颜色的框可以代表不同的数据类别。
  1. 交互功能
    • 鼠标交互:通过鼠标的点击、拖动、右键操作实现绘制、选择、删除等功能。例如,点击开始绘制,拖动绘制线条,右键点击删除节点或整个框。
    • 触摸交互(适用于移动设备):支持触摸操作,如触摸屏幕绘制、缩放和移动框,满足在移动设备上的使用需求。在移动办公或现场数据采集场景中,用户可以直接在平板或手机上进行操作。
    • 快捷键操作:提供快捷键绑定,如使用特定的按键组合来快速完成常见操作,如撤销、重做、清除等,提高操作效率。对于频繁使用该功能的用户,快捷键操作能大大提升工作速度。
  1. 数据处理功能
    • 获取坐标数据:能够获取绘制框的顶点坐标数据,这些数据可以用于后续的分析、处理或存储。在机器学习数据标注中,坐标数据是训练模型的重要依据。
    • 数据导出与导入:支持将绘制的框及其相关属性数据导出为特定格式(如 JSON、XML),方便在不同系统或应用之间共享数据;同时也能导入已有的数据,实现数据的复用和编辑。在团队协作的标注项目中,数据的导出和导入功能可以方便不同标注员之间的数据交互。
  1. 辅助功能
    • 网格背景:提供可切换的网格背景,帮助用户更准确地绘制和对齐图形。在绘制具有规则形状或需要精确位置的图形时,网格背景能起到很好的辅助作用。
    • 撤销和重做:支持撤销上一步操作和重做已撤销的操作,让用户在绘制过程中可以随时纠正错误,避免从头开始绘制的麻烦。
    • 清除与重置:可以清除当前绘制的所有框,或者重置整个绘制区域,回到初始状态,方便用户重新开始绘制。

五、适用于

摄像头区域侦测设置、摄像头预置位设置、区域画框等

六、代码实现,提供vue2、vue3、原生js

1、 vue3组件实现,父组件注册该组件即可使用

<template>
  <!-- 使用传入的宽高动态设置 canvas 容器的样式 -->
  <div
    :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }"
    class="canvas-container"
  >
    <!-- 用于绘制网格和不规则图形的 canvas 元素 -->
    <canvas ref="gridCanvas"></canvas>
    <div class="button-group">
      <!-- 撤销上一步操作的按钮 -->
      <button @click="undo">撤销</button>
      <!-- 获取当前不规则图形各点坐标的按钮 -->
      <button @click="getCoordinates">获取坐标</button>
      <!-- 重置 canvas 内容的按钮 -->
      <button @click="reset">重置</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from "vue";

// 定义组件接收的 props
const props = defineProps({
  // canvas 容器的宽度,默认为 640
  canvasWidth: {
    type: Number,
    default: 640,
  },
  // canvas 容器的高度,默认为 360
  canvasHeight: {
    type: Number,
    default: 360,
  },
  // 初始的点数组,用于数据反显,默认为空数组
  initialPoints: {
    type: Array,
    // default: () => [],
    default: () => [
      { x: 240, y: 135 },
      { x: 240, y: 225 },
      { x: 400, y: 225 },
      { x: 400, y: 135 },
    ],
  },
});

// 引用 canvas 元素
const gridCanvas = ref(null);
// 存储当前不规则图形的点数组,初始值为传入的初始点数据
const points = ref(props.initialPoints.slice());
// 标记是否正在绘制图形
const isDrawing = ref(false);
// 标记是否正在拖动某个点
const isDragging = ref(false);
// 当前正在拖动的点的索引
const draggedPointIndex = ref(-1);
// 网格的大小
const gridSize = ref(10);
// 画布内边距,这里设置为 0 以铺满整个 canvas
const framePadding = ref(0);
// canvas 的 2D 上下文对象
let ctx = null;

// 初始化 canvas 并绘制网格
const initCanvas = () => {
  const canvas = gridCanvas.value;
  // 设置 canvas 的宽度和高度
  canvas.width = props.canvasWidth;
  canvas.height = props.canvasHeight;
  // 获取 canvas 的 2D 上下文对象
  ctx = canvas.getContext("2d");
  // 绘制网格
  drawGrid();
};

// 绘制网格
const drawGrid = () => {
  // 清空 canvas 内容
  ctx.clearRect(0, 0, props.canvasWidth, props.canvasHeight);
  // 设置网格线的颜色和宽度
  ctx.strokeStyle = "rgba(0, 255, 0, 0.5)";
  ctx.lineWidth = 1;
  // 绘制垂直网格线
  for (let x = 0; x < props.canvasWidth; x += gridSize.value) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, props.canvasHeight);
    ctx.stroke();
  }
  // 绘制水平网格线
  for (let y = 0; y < props.canvasHeight; y += gridSize.value) {
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(props.canvasWidth, y);
    ctx.stroke();
  }
};

// 处理鼠标按下事件
const handleMouseDown = (e) => {
  // 限制鼠标点击的坐标在 canvas 范围内
  const x = Math.min(Math.max(e.offsetX, 0), props.canvasWidth);
  const y = Math.min(Math.max(e.offsetY, 0), props.canvasHeight);
  const clickRadius = 5;
  // 检查是否点击在某个点上
  for (let i = 0; i < points.value.length; i++) {
    const point = points.value[i];
    if (Math.hypot(x - point.x, y - point.y) <= clickRadius) {
      isDragging.value = true;
      draggedPointIndex.value = i;
      return;
    }
  }
  // 若未点击在点上,则开始新的绘制
  isDrawing.value = true;
  points.value.push({ x, y });
  // 绘制新的图形
  drawShape();
};

// 处理鼠标移动事件
const handleMouseMove = (e) => {
  // 限制鼠标移动的坐标在 canvas 范围内
  const x = Math.min(Math.max(e.offsetX, 0), props.canvasWidth);
  const y = Math.min(Math.max(e.offsetY, 0), props.canvasHeight);
  if (isDrawing.value && points.value.length > 0) {
    // 重新绘制 canvas
    redraw();
    // 绘制临时的图形(包含当前鼠标位置)
    drawShape([...points.value, { x, y }]);
  } else if (isDragging.value) {
    // 更新被拖动点的坐标
    points.value[draggedPointIndex.value] = { x, y };
    // 重新绘制 canvas
    redraw();
  }
};

// 处理鼠标松开事件
const handleMouseUp = () => {
  // 停止绘制和拖动状态
  isDrawing.value = false;
  isDragging.value = false;
  draggedPointIndex.value = -1;
  // 绘制最终的图形
  drawShape();
};

// 处理鼠标右键点击事件
const handleContextMenu = (e) => {
  // 阻止默认的右键菜单行为
  e.preventDefault();
  // 限制鼠标右键点击的坐标在 canvas 范围内
  const x = Math.min(Math.max(e.offsetX, 0), props.canvasWidth);
  const y = Math.min(Math.max(e.offsetY, 0), props.canvasHeight);
  const clickRadius = 5;
  // 检查是否右键点击在某个点上
  for (let i = 0; i < points.value.length; i++) {
    const point = points.value[i];
    if (Math.hypot(x - point.x, y - point.y) <= clickRadius) {
      // 移除该点
      points.value.splice(i, 1);
      // 重新绘制 canvas
      redraw();
      break;
    }
  }
};

// 绘制不规则图形
const drawShape = (pointsToDraw = points.value) => {
  if (pointsToDraw.length < 2) return;
  // 开始绘制路径
  ctx.beginPath();
  // 移动到第一个点
  ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
  // 依次连接各个点
  for (let i = 1; i < pointsToDraw.length; i++) {
    ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
  }
  // 闭合路径
  ctx.closePath();
  // 设置边框颜色和宽度
  ctx.strokeStyle = "blue";
  ctx.lineWidth = 2;
  // 绘制边框
  ctx.stroke();
  if (pointsToDraw.length === points.value.length) {
    // 设置填充颜色
    ctx.fillStyle = "rgba(0, 0, 255, 0.1)";
    // 填充图形
    ctx.fill();
  }
  // 设置点的颜色
  ctx.fillStyle = "red";
  // 绘制各个点
  pointsToDraw.forEach(({ x, y }) => {
    ctx.beginPath();
    ctx.arc(x, y, 5, 0, 2 * Math.PI);
    ctx.fill();
  });
};

// 重新绘制 canvas(先初始化,再绘制图形)
const redraw = () => {
  initCanvas();
  drawShape();
};

// 撤销上一步操作
const undo = () => {
  if (points.value.length > 0) {
    // 移除最后一个点
    points.value.pop();
    // 重新绘制 canvas
    redraw();
  }
};

// 获取当前不规则图形各点的坐标
const getCoordinates = () => {
  console.log(
    "不规则框坐标:",
    points.value.map((p) => ({ x: p.x, y: p.y }))
  );
};

// 重置 canvas 内容
const reset = () => {
  // 清空点数组
  //   points.value = [];
  points.value = [
    { x: 240, y: 135 },
    { x: 240, y: 225 },
    { x: 400, y: 225 },
    { x: 400, y: 135 },
  ];
  // 重新绘制 canvas
  redraw();
};

onMounted(() => {
  // 初始化 canvas
  initCanvas();
  // 绘制初始的不规则图形
  drawShape();
  const canvas = gridCanvas.value;
  // 绑定鼠标事件
  canvas.addEventListener("mousedown", handleMouseDown);
  canvas.addEventListener("mousemove", handleMouseMove);
  canvas.addEventListener("mouseup", handleMouseUp);
  canvas.addEventListener("contextmenu", handleContextMenu);
});

// 监听 canvas 宽高的变化,重新初始化 canvas 并绘制图形
watch([() => props.canvasWidth, () => props.canvasHeight], () => {
  initCanvas();
  drawShape();
});

// 监听初始点数据的变化,更新 points 数组,重新初始化 canvas 并绘制图形
watch(
  () => props.initialPoints,
  (newPoints) => {
    points.value = newPoints.slice();
    initCanvas();
    drawShape();
  }
);
// 导出函数以供外部调用,可以使用refs方法调用
defineExpose({
  getCoordinates,
  reset,
  undo,
});
</script>

<style scoped>
.canvas-container {
  position: relative;
  margin: 20px auto;
  border: 1px solid #ccc;
  overflow: hidden;
}

canvas {
  display: block;
  width: 100%;
  height: 100%;
  background-color: #f0f0f0;
}

.button-group {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 10;
}

button {
  padding: 6px 12px;
  font-size: 14px;
  cursor: pointer;
  background-color: #fff;
  border: 1px solid #ddd;
  margin-right: 8px;
}
</style>

2、vue2 组件实现,父组件注册该组件即可使用

<template>
  <!-- 使用传入的宽高动态设置 canvas 容器的样式 -->
  <div
    :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }"
    class="canvas-container"
  >
    <!-- 用于绘制网格和不规则图形的 canvas 元素 -->
    <canvas ref="gridCanvas"></canvas>
    <div class="button-group">
      <!-- 撤销上一步操作的按钮 -->
      <button @click="undo">撤销</button>
      <!-- 获取当前不规则图形各点坐标的按钮 -->
      <button @click="getCoordinates">获取坐标</button>
      <!-- 重置 canvas 内容的按钮 -->
      <button @click="reset">重置</button>
    </div>
  </div>
</template>

<script>
export default {
  // 接收父组件传递的参数
  props: {
    // canvas 容器的宽度,默认为 640
    canvasWidth: {
      type: Number,
      default: 640,
    },
    // canvas 容器的高度,默认为 360
    canvasHeight: {
      type: Number,
      default: 360,
    },
    // 初始的点数组,用于数据反显,默认为空数组
    initialPoints: {
      type: Array,
      // default: () => [],
      default: () => [
        { x: 240, y: 135 },
        { x: 240, y: 225 },
        { x: 400, y: 225 },
        { x: 400, y: 135 },
      ],
    },
  },
  data() {
    return {
      // 存储当前不规则图形的点数组
      points: [],
      // 标记是否正在绘制图形
      isDrawing: false,
      // 标记是否正在拖动某个点
      isDragging: false,
      // 当前正在拖动的点的索引
      draggedPointIndex: -1,
      // 网格的大小
      gridSize: 10,
      // 画布内边距,这里设置为 0 以铺满整个 canvas
      framePadding: 0,
      // canvas 的 2D 上下文对象
      ctx: null,
    };
  },
  mounted() {
    // 将初始点数据赋值给 points 数组,实现数据反显
    this.points = this.initialPoints.slice();
    // 初始化 canvas
    this.initCanvas();
    // 绑定鼠标事件
    this.bindEvents();
    // 绘制初始的不规则图形
    this.drawShape();
  },
  methods: {
    // 初始化 canvas 并绘制网格
    initCanvas() {
      const canvas = this.$refs.gridCanvas;
      // 设置 canvas 的宽度和高度
      canvas.width = this.canvasWidth;
      canvas.height = this.canvasHeight;
      // 获取 canvas 的 2D 上下文对象
      this.ctx = canvas.getContext("2d");
      // 绘制网格
      this.drawGrid();
    },
    // 绘制网格
    drawGrid() {
      const ctx = this.ctx;
      // 清空 canvas 内容
      ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
      // 设置网格线的颜色和宽度
      ctx.strokeStyle = "rgba(0, 255, 0, 0.5)";
      ctx.lineWidth = 1;
      // 绘制垂直网格线
      for (let x = 0; x < this.canvasWidth; x += this.gridSize) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, this.canvasHeight);
        ctx.stroke();
      }
      // 绘制水平网格线
      for (let y = 0; y < this.canvasHeight; y += this.gridSize) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(this.canvasWidth, y);
        ctx.stroke();
      }
    },
    // 处理鼠标按下事件
    handleMouseDown(e) {
      // 限制鼠标点击的坐标在 canvas 范围内
      const x = this.limitCoordinate(e.offsetX, this.canvasWidth);
      const y = this.limitCoordinate(e.offsetY, this.canvasHeight);
      const clickRadius = 5;
      // 检查是否点击在某个点上
      for (let i = 0; i < this.points.length; i++) {
        const point = this.points[i];
        if (this.calculateDistance(x, y, point.x, point.y) <= clickRadius) {
          this.isDragging = true;
          this.draggedPointIndex = i;
          return;
        }
      }
      // 若未点击在点上,则开始新的绘制
      this.isDrawing = true;
      this.points.push({ x, y });
      // 绘制新的图形
      this.drawShape();
    },
    // 处理鼠标移动事件
    handleMouseMove(e) {
      // 限制鼠标移动的坐标在 canvas 范围内
      const x = this.limitCoordinate(e.offsetX, this.canvasWidth);
      const y = this.limitCoordinate(e.offsetY, this.canvasHeight);
      if (this.isDrawing && this.points.length > 0) {
        // 重新绘制 canvas
        this.redraw();
        // 绘制临时的图形(包含当前鼠标位置)
        this.drawShape([...this.points, { x, y }]);
      } else if (this.isDragging) {
        // 更新被拖动点的坐标
        this.points[this.draggedPointIndex] = { x, y };
        // 重新绘制 canvas
        this.redraw();
      }
    },
    // 处理鼠标松开事件
    handleMouseUp() {
      // 停止绘制和拖动状态
      this.isDrawing = false;
      this.isDragging = false;
      this.draggedPointIndex = -1;
      // 绘制最终的图形
      this.drawShape();
    },
    // 处理鼠标右键点击事件
    handleContextMenu(e) {
      // 阻止默认的右键菜单行为
      e.preventDefault();
      // 限制鼠标右键点击的坐标在 canvas 范围内
      const x = this.limitCoordinate(e.offsetX, this.canvasWidth);
      const y = this.limitCoordinate(e.offsetY, this.canvasHeight);
      const clickRadius = 5;
      // 检查是否右键点击在某个点上
      for (let i = 0; i < this.points.length; i++) {
        const point = this.points[i];
        if (this.calculateDistance(x, y, point.x, point.y) <= clickRadius) {
          // 移除该点
          this.points.splice(i, 1);
          // 重新绘制 canvas
          this.redraw();
          break;
        }
      }
    },
    // 绘制不规则图形
    drawShape(pointsToDraw = this.points) {
      if (pointsToDraw.length < 2) return;
      const ctx = this.ctx;
      // 开始绘制路径
      ctx.beginPath();
      // 移动到第一个点
      ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
      // 依次连接各个点
      for (let i = 1; i < pointsToDraw.length; i++) {
        ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
      }
      // 闭合路径
      ctx.closePath();
      // 设置边框颜色和宽度
      ctx.strokeStyle = "blue";
      ctx.lineWidth = 2;
      // 绘制边框
      ctx.stroke();
      if (pointsToDraw.length === this.points.length) {
        // 设置填充颜色
        ctx.fillStyle = "rgba(0, 0, 255, 0.1)";
        // 填充图形
        ctx.fill();
      }
      // 设置点的颜色
      ctx.fillStyle = "red";
      // 绘制各个点
      pointsToDraw.forEach(({ x, y }) => {
        ctx.beginPath();
        ctx.arc(x, y, 5, 0, 2 * Math.PI);
        ctx.fill();
      });
    },
    // 重新绘制 canvas(先初始化,再绘制图形)
    redraw() {
      this.initCanvas();
      this.drawShape();
    },
    // 撤销上一步操作
    undo() {
      if (this.points.length > 0) {
        // 移除最后一个点
        this.points.pop();
        // 重新绘制 canvas
        this.redraw();
      }
    },
    // 获取当前不规则图形各点的坐标
    getCoordinates() {
      console.log(
        "不规则框坐标:",
        this.points.map((p) => ({ x: p.x, y: p.y }))
      );
    },
    // 重置 canvas 内容
    reset() {
      // 清空点数组
      //   points.value = [];
      points.value = [
        { x: 240, y: 135 },
        { x: 240, y: 225 },
        { x: 400, y: 225 },
        { x: 400, y: 135 },
      ];
      // 重新绘制 canvas
      this.redraw();
    },
    // 限制坐标在指定范围内
    limitCoordinate(value, max) {
      return Math.min(Math.max(value, 0), max);
    },
    // 计算两点之间的距离
    calculateDistance(x1, y1, x2, y2) {
      const dx = x1 - x2;
      const dy = y1 - y2;
      return Math.sqrt(dx * dx + dy * dy);
    },
    // 绑定鼠标事件
    bindEvents() {
      const canvas = this.$refs.gridCanvas;
      canvas.addEventListener("mousedown", this.handleMouseDown);
      canvas.addEventListener("mousemove", this.handleMouseMove);
      canvas.addEventListener("mouseup", this.handleMouseUp);
      canvas.addEventListener("contextmenu", this.handleContextMenu);
    },
  },
};
</script>

<style scoped>
.canvas-container {
  position: relative;
  margin: 20px auto;
  border: 1px solid #ccc;
  overflow: hidden;
}

canvas {
  display: block;
  width: 100%;
  height: 100%;
  background-color: #f0f0f0;
}

.button-group {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 10;
}

button {
  padding: 6px 12px;
  font-size: 14px;
  cursor: pointer;
  background-color: #fff;
  border: 1px solid #ddd;
  margin-right: 8px;
}
</style>

3、原生js实现,在HTML文件里面运行即可使用

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Camera Grid Frame with Draggable, Deletable, Coordinate Retrieval and Reset</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f0f0;
        }

        canvas {
            border: 1px solid #ccc;
        }

        button {
            position: absolute;
            padding: 8px 16px;
            font-size: 14px;
            cursor: pointer;
        }

        #undoButton {
            top: 20px;
            left: 20px;
        }

        #getCoordinatesButton {
            top: 20px;
            left: 120px;
        }

        #resetButton {
            top: 20px;
            left: 240px;
        }
    </style>
</head>

<body>
    <canvas id="gridCanvas" width="400" height="300"></canvas>
    <button id="undoButton">撤销</button>
    <button id="getCoordinatesButton">获取坐标</button>
    <button id="resetButton">重置</button>
    <script>
        const canvas = document.getElementById('gridCanvas');
        canvas.width = 640;
        canvas.height = 360;
        const ctx = canvas.getContext('2d');
        const undoButton = document.getElementById('undoButton');
        const getCoordinatesButton = document.getElementById('getCoordinatesButton');
        const resetButton = document.getElementById('resetButton');

        const gridSize = 10;
        const framePadding = 0; // 边框内边距
        const frameWidth = canvas.width - 2 * framePadding;
        const frameHeight = canvas.height - 2 * framePadding;

        // 预设的 points 数据,可根据需要修改
        const initialPoints = [
            { x: 240, y: 135 },
            { x: 240, y: 225 },
            { x: 400, y: 225 },
            { x: 400, y: 135 },
        ];

        const points = initialPoints.slice(); // 复制预设数据到 points

        // 绘制方格
        function drawGrid() {
            for (let x = framePadding; x < framePadding + frameWidth; x += gridSize) {
                for (let y = framePadding; y < framePadding + frameHeight; y += gridSize) {
                    ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';
                    ctx.lineWidth = 1;
                    ctx.strokeRect(x, y, gridSize, gridSize);
                }
            }
        }

        drawGrid();

        let isDrawing = false;
        let isDragging = false;
        let draggedPointIndex = -1;

        // 页面加载时绘制初始图形
        if (points.length > 1) {
            drawIrregularShape();
        }

        canvas.addEventListener('mousedown', (e) => {
            const x = e.offsetX;
            const y = e.offsetY;
            const clickRadius = 5;

            // 检查是否点击在节点上
            for (let i = 0; i < points.length; i++) {
                const point = points[i];
                const dx = x - point.x;
                const dy = y - point.y;
                if (Math.sqrt(dx * dx + dy * dy) <= clickRadius) {
                    isDragging = true;
                    draggedPointIndex = i;
                    return;
                }
            }

            // 若未点击在节点上,则开始新的绘制
            isDrawing = true;
            points.push({ x, y });
            drawIrregularShape();
        });

        canvas.addEventListener('mousemove', (e) => {
            const x = e.offsetX;
            const y = e.offsetY;

            if (isDrawing && points.length > 0) {
                redrawCanvas();
                drawIrregularShape([...points, { x, y }]);
            } else if (isDragging) {
                points[draggedPointIndex] = { x, y };
                redrawCanvas();
                drawIrregularShape();
            }
        });

        canvas.addEventListener('mouseup', () => {
            isDrawing = false;
            isDragging = false;
            draggedPointIndex = -1;
        });

        canvas.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            const x = e.offsetX;
            const y = e.offsetY;
            const clickRadius = 5;

            for (let i = 0; i < points.length; i++) {
                const point = points[i];
                const dx = x - point.x;
                const dy = y - point.y;
                if (Math.sqrt(dx * dx + dy * dy) <= clickRadius) {
                    points.splice(i, 1);
                    redrawCanvas();
                    drawIrregularShape();
                    break;
                }
            }
        });

        undoButton.addEventListener('click', () => {
            if (points.length > 0) {
                points.pop();
                redrawCanvas();
                drawIrregularShape();
            }
        });

        getCoordinatesButton.addEventListener('click', () => {
            const coordinates = points.map(point => ({ x: point.x, y: point.y }));
            console.log('不规则框各点位的坐标:', coordinates);
            alert('不规则框各点位的坐标:' + JSON.stringify(coordinates));
            // 你可以在这里将坐标信息用于其他用途,比如发送到服务器等
        });

        resetButton.addEventListener('click', () => {
            points.length = 0;
            redrawCanvas();
        });

        function redrawCanvas() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawGrid();
        }

        function drawIrregularShape(pointsToDraw = points) {
            if (pointsToDraw.length > 1) {
                ctx.beginPath();
                ctx.moveTo(pointsToDraw[0].x, pointsToDraw[0].y);
                for (let i = 1; i < pointsToDraw.length; i++) {
                    ctx.lineTo(pointsToDraw[i].x, pointsToDraw[i].y);
                }
                ctx.closePath();
                ctx.strokeStyle = 'blue';
                ctx.lineWidth = 2;
                ctx.stroke();
                if (pointsToDraw.length === points.length) {
                    ctx.fillStyle = 'rgba(0, 0, 255, 0.1)';
                    ctx.fill();
                }

                // 绘制节点
                const nodeRadius = 5;
                ctx.fillStyle = 'red';
                for (let i = 0; i < pointsToDraw.length; i++) {
                    const point = pointsToDraw[i];
                    ctx.beginPath();
                    ctx.arc(point.x, point.y, nodeRadius, 0, 2 * Math.PI);
                    ctx.fill();
                }
            }
        }

    </script>
</body>

</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值