使用Canvas实现图片标注功能

一、功能概述

主要功能项为实现在图片上完成标注功能,简单理解,在图片坐标系上绘制矩形或者多边形,并且支持导出当前绘制矩形或者多边形的坐标。

Canvas有很多已经封装的工具库,能很快实现相应功能。但是便于学习因素,所以本次项目采用Canvas原生进行从零到一的开发。

需要实现功能为:

        图片加载;

        鼠标滚轮缩放;

        鼠标按下拖拽;

        矩形 / 多边形绘制;

        矩形 / 多边形编辑;

二、功能模块

功能模块编写前置工作,需要创建一个canvas,规定canvas的大小,并且获取到canvas的dom元素(监听尺寸变化,实时修改canvas大小)

// <div ref="canvasImage" class="image-annotation-main"></div>
const _domCanvas = document.createElement('canvas')
_dom.appendChild(_domCanvas)
this.canvas = _domCanvas;
this.ctx = _domCanvas.getContext('2d')!;

// 设置Canvas大小
 private resizeCanvas(_div: HTMLDivElement) {
    this.canvas.width = _div.clientWidth;
    this.canvas.height = _div.clientHeight;
    window.addEventListener('resize', () => this.resizeCanvas(_div));
 }

(1)图片加载

在Canvas上面加载图片,需要注意绘制的图片不能跨域,否则会绘制失败。

// 手动加载图片
  public loadImage(_imageSrc: string, width = 0, height = 0, top = 0, left = 0) {
    this.imgDom.src = _imageSrc;
    this.imgDom.onload = () => {
      width = width || this.imgDom.width
      height = height || this.imgDom.height
      this.ctx.drawImage(this.imgDom, top, left, width, height);
    };
  }

(2)鼠标滚轮缩放

监听鼠标滚轮事件对Canvas进行缩放操作,当用户使用鼠标滚轮时,会触发 wheel 事件,我们可以通过这个事件的 deltaY 属性来判断用户是向上滚动(放大)还是向下滚动(缩小)。

zoomIntensity 记录当前缩放精度,细致一点的话,可以稍微的动态调整缩放精度,在缩放到最下级时,可以减少精度,实现缓慢缩放的过程;

// 缩放处理
  private zoomCanvas() {
    this.canvas.addEventListener('wheel', e => {
      e.preventDefault();
      const zoomIntensity = 0.1;
      const delta = e.deltaY < 0 ? 1 : -1;
      const newScale = delta > 0 ? this.state.scale * (1 + zoomIntensity) : this.state.scale / (1 + zoomIntensity);

      // 以鼠标位置为中心缩放
      const mouseX = e.offsetX;
      const mouseY = e.offsetY;
      const imgPos = this.viewportToImage(mouseX, mouseY);

      this.state.scale = newScale;
      this.state.offsetX = mouseX - imgPos.x * this.state.scale;
      this.state.offsetY = mouseY - imgPos.y * this.state.scale;

      console.log(this.state)
      this.draw();
    }, { passive: false });
  }

 我们当前实现的是以当前鼠标为缩放原点,这样对用户体验最佳;你也可以选择以图片左上角、或者中心为缩放原点,但这样太过于固定,效果不佳;

通过缩放精度,计算得到当前的一个缩放比例 scale 变量,在后面重新绘制的过程中使用;

// 绘制操作
  private draw() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 修改画布点位(左上角)
    // 修改画布缩放
    this.ctx.save();
    this.ctx.translate(this.state.offsetX, this.state.offsetY);
    this.ctx.scale(this.state.scale, this.state.scale);
    // 重新绘制图片
    // this.ctx.drawImage(this.imgDom, 0, 0, this.canvas.width, this.canvas.height);
    this.ctx.drawImage(this.imgDom, 0, 0, this.imgDom.width, this.imgDom.height);
    this.ctx.restore();
  }

这里稍微解释一下 scale 函数,初次接触,可能会不太好理解。在 Canvas 中使用 scale 函数时,重要的是要理解它实际上是在缩放绘图坐标系统,而不是直接缩放绘制的图形。当你调用 ctx.scale(scaleX, scaleY) 时,你是在告诉 Canvas 之后的所有绘图操作都应该在一个被缩放的坐标系统中进行。

这意味着,如果你将缩放比例设置为 2,那么在这个缩放的坐标系统中,绘制一个宽度为 50 像素的矩形,实际上会在画布上产生一个宽度为 100 像素的矩形。因为在缩放的坐标系统中,每个单位长度都变成了原来的两倍。

因此,当我们谈论 scale 函数时,重点是要记住它是在缩放整个绘图坐标系统,而不是单独的图形。这就是为什么在使用 scale 函数后,所有的绘图操作(包括位置、大小等)都会受到影响。

(3) 鼠标按下拖拽

我们可以使用 Canvas 的 translate 方法来改变视口的位置。translate 方法接受两个参数,分别表示沿 x 轴和 y 轴移动的距离。在移动视口时,我们需要更新图片的位置,并重新绘制图像以反映新的视口位置。

// 鼠标拖动开始
  private dragStart() {
    this.canvas.addEventListener('mousedown', e => {
      // 开始拖拽画布
      this.state.isDragging = true;
      this.state.dragStart.x = e.offsetX - this.state.offsetX;
      this.state.dragStart.y = e.offsetY - this.state.offsetY;
      this.draw();
    });
  }

// 鼠标拖动 move
  private dragMove() {
    this.canvas.addEventListener('mousemove', e => {
      if (this.state.isDragging) {
        this.state.offsetX = e.offsetX - this.state.dragStart.x;
        this.state.offsetY = e.offsetY - this.state.dragStart.y;
      }
      this.draw();
    });
  }

  // 鼠标拖动结束
  private dragEnd() {
    this.canvas.addEventListener('mouseup', e => {
      this.state.isDragging = false;
    });
  }

通过记录当前拖拽的点,以此计算出拖拽之后的视口点,在重新绘制一下Canvas里面的元素即可。 

(4)矩形 / 多边形绘制

矩形 / 多边形的绘制功能也相对比较简单;

矩形,需要知道左上角坐标 和 矩形的宽高,就可以绘制一个矩形;

多边形,将多边形的点连接起来就是一个多边形;(至少三个点);

这样就得同步修改鼠标按下,鼠标移动,鼠标抬起操作;

1.绘制矩形

// 鼠标拖动开始
  private dragStart() {
    this.canvas.addEventListener('mousedown', e => {
      const imgPos = this.viewportToImage(e.offsetX, e.offsetY);

      if (this.state.mode === 'rect') {
        this.state.currentShape = {
          id: operation.moreOperation.generateRandomString(16),
          type: 'rect',
          x: imgPos.x,
          y: imgPos.y,
          width: 0,
          height: 0,
          style: this.state.currentStyle
        };
      } else {
        // 开始拖拽画布
          this.setCursor('grabbing');
          this.state.isDragging = true;
          this.state.dragStart.x = e.offsetX - this.state.offsetX;
          this.state.dragStart.y = e.offsetY - this.state.offsetY;
      }
      this.draw();
    });
  }

  // 鼠标拖动 move
  private dragMove() {
    this.canvas.addEventListener('mousemove', e => {
      if (this.state.isDragging) {
        this.state.offsetX = e.offsetX - this.state.dragStart.x;
        this.state.offsetY = e.offsetY - this.state.dragStart.y;
      } else if (this.state.currentShape?.type === 'rect') {
        const start = this.state.currentShape;
        const current = this.viewportToImage(e.offsetX, e.offsetY);
        start.width = current.x - start.x!;
        start.height = current.y - start.y!;
      }
      this.draw();
    });
  }

  // 鼠标拖动结束
  private dragEnd() {
    this.canvas.addEventListener('mouseup', e => {
      this.state.isDragging = false;
      if (this.state.currentShape) {
        if (this.state.currentShape.type === 'rect') {
          if (this.state.currentShape.width !== 0 && this.state.currentShape.height !== 0) {
            this.state.shapes.push(this.state.currentShape);
          }
          this.state.currentShape = null;
        }
      }
    });
  }

2.绘制多边形 

// 鼠标拖动开始
  private dragStart() {
    this.canvas.addEventListener('mousedown', e => {
      const imgPos = this.viewportToImage(e.offsetX, e.offsetY);

      if (this.state.mode === 'polygon') {
        if (!this.state.currentShape) {
          this.state.currentShape = {
            id: operation.moreOperation.generateRandomString(16),
            type: 'polygon',
            points: [imgPos],
            style: this.state.currentStyle
          };
        } else {
          this.state.currentShape.points!.push(imgPos);
        }
      } else {
        // 开始拖拽画布
          this.setCursor('grabbing');
          this.state.isDragging = true;
          this.state.dragStart.x = e.offsetX - this.state.offsetX;
          this.state.dragStart.y = e.offsetY - this.state.offsetY;
      }
      this.draw();
    });
  }

  // 鼠标拖动 move
  private dragMove() {
    this.canvas.addEventListener('mousemove', e => {
      if (this.state.isDragging) {
        this.state.offsetX = e.offsetX - this.state.dragStart.x;
        this.state.offsetY = e.offsetY - this.state.dragStart.y;
      } else if (this.state.currentShape?.type === 'polygon') {
        const current = this.viewportToImage(e.offsetX, e.offsetY);
        let currentIndex = this.state.currentShape.points!.length - 1 == 0 ? 1 : this.state.currentShape.points!.length - 1;
        this.state.currentShape.points![currentIndex] = current;
      }
      this.draw();
    });
  }

  // 鼠标拖动结束
  private dragEnd() {
    this.canvas.addEventListener('mouseup', e => {
      this.state.isDragging = false;
    });
  }

  // 双击结束 - 本次 - 绘制polygon
  private doubleClick(_type: 'Listening' | 'remove') {
    const dbClickEndShape = () => {
      if (this.state.currentShape?.type === 'polygon') {
        // 移出多个 多出来的点 - 双击也同时触发了鼠标按下事件,所以需要移除这两个异常点
        this.state.currentShape.points.pop();
        this.state.currentShape.points.pop();
        this.state.shapes.push(this.state.currentShape);
        this.state.currentShape = null;
        this.draw();
      }
    }
    if (_type == 'Listening') {
      this.canvas.addEventListener('dblclick', dbClickEndShape);
    } else {
      this.canvas.removeEventListener('dblclick', dbClickEndShape);
    }
  }

3. 动态绘制以绘制的图形 

// 绘制操作
  private draw() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 修改画布点位(左上角)
    // 修改画布缩放
    this.ctx.save();
    this.ctx.translate(this.state.offsetX, this.state.offsetY);
    this.ctx.scale(this.state.scale, this.state.scale);
    // 重新绘制图片
    // this.ctx.drawImage(this.imgDom, 0, 0, this.canvas.width, this.canvas.height);
    this.ctx.drawImage(this.imgDom, 0, 0, this.imgDom.width, this.imgDom.height);
    this.ctx.restore();

    // 修改 - 绘制的所有形状
    this.redrawStateShapes();

    // 修改 - 绘制当前正在创建的形状
    if (this.state.currentShape) this.redrawCurrentShape(this.state.currentShape, true);
  }

  // 重新绘制当前所绘制的形状
  public redrawStateShapes() {
    this.state.shapes.forEach(shape => {
      const label = this.labels.find(item => item.styleId === shape.style.styleId)
      if (label) shape.style = label
      // 原生绘制操作
      this.redrawCurrentShape(shape)
    });
  }

  // 绘制当前正在创建的形状
  public redrawCurrentShape(_shape: reactType | polygonType, _edit = false) {
    this.ctx.save();
    this.ctx.translate(this.state.offsetX, this.state.offsetY);
    this.ctx.scale(this.state.scale, this.state.scale);

    // 是否存在可以编辑
    const _editingIsCurrent = this.state.selectedShape && _shape.id == this.state.selectedShape.id
    const _color = _edit || _shape.pointer ? this.drawingColor : _shape.style.strokeStyle
    const _fillColor = operation.moreOperation.colorToRgba(_color, 0.3);
    this.ctx.strokeStyle = _color;
    this.ctx.fillStyle = _fillColor;
    this.ctx.lineWidth = 2 / this.state.scale;
    const _text = `#${_shape.style.styleIndex} ${_shape.style.styleLabel}`

    if (_shape.type === 'rect') {
      const { x, y, width, height } = _shape;
      this.ctx.fillRect(x, y, width, height); // 填充矩形
      this.ctx.strokeRect(x, y, width, height);
      // 绘制标签
      if (!_edit && !_editingIsCurrent) this.redrawLabel(x + width / 2, y + height / 2, _text, _color);
    } else if (_shape.type === 'polygon') {
      this.ctx.beginPath();
      _shape.points.forEach((p: { x: number; y: number }, i: number) => {
        if (i === 0) this.ctx.moveTo(p.x, p.y);
        else this.ctx.lineTo(p.x, p.y);
      });
      if (!_edit) this.ctx.closePath();
      this.ctx.fill(); // 填充多边形
      this.ctx.stroke();
      if (!_editingIsCurrent) {
        if (!_edit) {
          // 绘制标签
          const centerX = _shape.points.reduce((sum, p) => sum + p.x, 0) / _shape.points.length;
          const centerY = _shape.points.reduce((sum, p) => sum + p.y, 0) / _shape.points.length;
          this.redrawLabel(centerX, centerY, _text, _color);
        } else {
          // 编辑模式下提示双击结束编辑
          this.redrawLabel(_shape.points[_shape.points.length - 1].x + 60, _shape.points[_shape.points.length - 1].y - 15, '双击结束编辑', 'rgba(255, 255, 255, 0.7)', 'black');
        }
      }
    }
    this.ctx.restore();
  }

// 绘制标签
  public redrawLabel(x: number, y: number, text: string, backgroundColor: string = 'rgba(255, 255, 255, 0.7)', textColor: string = 'black', borderRadius: number = 5) {
    const padding = 5; // 标签内边距
    const fontSize = 16; // 字体大小

    // 设置字体样式
    this.ctx.font = `${fontSize}px Arial`;
    this.ctx.textAlign = 'center';
    this.ctx.textBaseline = 'middle';

    // 计算文本宽度和高度
    const textWidth = this.ctx.measureText(text).width;
    const textHeight = fontSize; // 简单估计文本高度

    // 绘制背景矩形(圆角)
    this.ctx.fillStyle = backgroundColor;
    this.ctx.beginPath();
    this.ctx.moveTo(x - textWidth / 2 - padding + borderRadius, y - textHeight / 2 - padding);
    this.ctx.lineTo(x + textWidth / 2 + padding - borderRadius, y - textHeight / 2 - padding);
    this.ctx.quadraticCurveTo(x + textWidth / 2 + padding, y - textHeight / 2 - padding, x + textWidth / 2 + padding, y - textHeight / 2 - padding + borderRadius);
    this.ctx.lineTo(x + textWidth / 2 + padding, y + textHeight / 2 + padding - borderRadius);
    this.ctx.quadraticCurveTo(x + textWidth / 2 + padding, y + textHeight / 2 + padding, x + textWidth / 2 + padding - borderRadius, y + textHeight / 2 + padding);
    this.ctx.lineTo(x - textWidth / 2 - padding + borderRadius, y + textHeight / 2 + padding);
    this.ctx.quadraticCurveTo(x - textWidth / 2 - padding, y + textHeight / 2 + padding, x - textWidth / 2 - padding, y + textHeight / 2 + padding - borderRadius);
    this.ctx.lineTo(x - textWidth / 2 - padding, y - textHeight / 2 - padding + borderRadius);
    this.ctx.quadraticCurveTo(x - textWidth / 2 - padding, y - textHeight / 2 - padding, x - textWidth / 2 - padding + borderRadius, y - textHeight / 2 - padding);
    this.ctx.closePath();
    this.ctx.fill();

    // 绘制文本
    this.ctx.fillStyle = textColor;
    this.ctx.fillText(text, x, y);
  }

(5)矩形 / 多边形编辑

矩形 / 多边形编辑相对比较复杂;首先需要明确编辑点,拖拽修改编辑点修改原有图形内的属性;

判断是否选中图形

// 判断鼠标当前的位置是否在已经绘制的形状中
  private isPointInShape(imgPos: { x: number, y: number }) {
    // 检测是否选中形状
    for (let i = 0; i < this.state.shapes.length; i++) {
      const shape = this.state.shapes[i];
      const _selectShape = this.state.selectedShape && shape.id == this.state.selectedShape.id
      const _currentCursor = _selectShape ? 'all-scroll' : 'pointer'
      // 设置指针
      shape.pointer = false;
      // 简单矩形碰撞检测
      if (shape.type === 'rect' &&
        imgPos.x >= shape.x && imgPos.x <= shape.x + shape.width &&
        imgPos.y >= shape.y && imgPos.y <= shape.y + shape.height) {
        this.setCursor(_currentCursor)
        shape.pointer = true;
        return shape;
      } else if (shape.type === 'polygon') {
        const points = shape.points;
        let inside = false;

        for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
          const xi = points[i].x, yi = points[i].y;
          const xj = points[j].x, yj = points[j].y;

          const intersect = ((yi > imgPos.y) !== (yj > imgPos.y)) &&
            (imgPos.x < (xj - xi) * (imgPos.y - yi) / (yj - yi) + xi);
          if (intersect) inside = !inside;
        }

        if (inside) {
          this.setCursor(_currentCursor);
          shape.pointer = true;
          return shape;
        }
      }
    }
    this.changeCursor()
    return undefined;
  }

先修改鼠标按下、移动、抬起逻辑(实现图形编辑状态、可整体拖拽)

// 鼠标拖动开始
  private dragStart() {
    this.canvas.addEventListener('mousedown', e => {
      // 检测是否选中形状
        this.state.selectedShape = null;
        const _selectShape = this.isPointInShape(imgPos);
if (_selectShape) {
          this.state.selectedShape = _selectShape;
          this.currentAllScroll = true
          // 计算坐标差
          if (_selectShape.type == 'rect') {
            this.draggingRect[0].x = imgPos.x - _selectShape.x;
            this.draggingRect[0].y = imgPos.y - _selectShape.y;
          } else {
            // 记录拖拽时,每一个点的原始位置
            _selectShape.points.forEach((point, index) => {
              const _point = this.draggingRect[index]
              const _x = imgPos.x - point.x;
              const _y = imgPos.y - point.y;
              if (_point) {
                _point.x = _x;
                _point.y = _y;
              } else {
                this.draggingRect.push({
                  x: _x,
                  y: _y
                })
              }
            })
          }
      this.draw();
    });
  }

  // 鼠标拖动 move
  private dragMove() {
    this.canvas.addEventListener('mousemove', e => {
      if (!this.state.currentShape) {
        // 不是拖拽情况下时,判断是否move到某个已经绘制好的标注上面
        const imgPos = this.viewportToImage(e.offsetX, e.offsetY);
          const _selectShape = this.isPointInShape(imgPos)
          // 判断是否选中 编辑形状
          if (this.state.selectedShape) {
            if (this.currentAllScroll) {
              if (this.state.selectedShape.type === 'rect') {
                this.state.selectedShape.x = imgPos.x - this.draggingRect[0].x;
                this.state.selectedShape.y = imgPos.y - this.draggingRect[0].y;
              } else {
                // 更新多边形的每个点的位置
                this.state.selectedShape.points.forEach((point, index) => {
                  point.x = imgPos.x - this.draggingRect[index].x;
                  point.y = imgPos.y - this.draggingRect[index].y;
                });
              }
            }
          }
        }
      this.draw();
    });
  }

  // 鼠标拖动结束
  private dragEnd() {
    this.canvas.addEventListener('mouseup', e => {
      this.editableDragging = false;
    });
  }

绘制编辑状态的图形

// 编辑图形的相应操作
  public editShape(shape: reactType | polygonType) {
    this.coordinates = {
      'ns-resize': [],
      'ew-resize': [],
      'nwse-resize': [],
      'nesw-resize': [],
      'crosshair': [],
    }
    let _points = [];
    if (shape.type === 'rect') {
      _points = this.getRectangleCorners(shape.x, shape.y, shape.width, shape.height, true);
      this.coordinates['ns-resize'].push(_points[4], _points[6]);
      this.coordinates['ew-resize'].push(_points[5], _points[7]);
      this.coordinates['nwse-resize'].push(_points[0], _points[2]);
      this.coordinates['nesw-resize'].push(_points[1], _points[3]);
    } else {
      _points = shape.points;
      this.coordinates['crosshair'] = _points;
    }

    // 绘制可编辑点
    _points.forEach(p => {
      // 绘制边框和填充
      this.ctx.fillStyle = this.editingColor.background; // 设置填充颜色
      this.ctx.strokeStyle = this.editingColor.border; // 设置边框颜色
      this.ctx.lineWidth = 2; // 设置边框宽度

      this.ctx.beginPath();
      this.ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
      this.ctx.fill(); // 填充颜色
      this.ctx.stroke(); // 绘制边框
    });
  }

1. 矩形编辑

实现逻辑:通过矩形的x,y,width,height计算获取矩形可编辑点(左上、左下、右上、右下、上中、下中、左中、右中)八个可编辑的点,明确每个点的修改逻辑

左上、左下、右上、右下:x,y,width,height 均被修改

上中、下中:y,height 被修改

左中、右中:x,width被修改

标记当前拖拽的是哪个点,根据不同点,不同逻辑实现拖拽操作;

(代码过于长,实现逻辑如上,具体代码,请查阅代码概览)

2. 多边形编辑

实现逻辑:拖拽并且同时修改某个顶点坐标、重新绘制即可;

三、代码概览

这是最原始的实现方式,当然你也可以借助Canvas的工具库进行更加简易的实现方式。(比如:fabric.js)

// 结合 npm install fabric --save (fabric.js) 实现对其编辑操作
// import * as fabric from 'fabric';
// 导入工具类函数
import operation from '@/utils/basicFunctionalFunction';

interface ImageOption {
  src: string
  width?: number
  height?: number
  top?: number
  left?: number
}

interface styleType {
  styleId: string
  strokeStyle: string
  styleIndex: number
  styleLabel: string
}

interface reactType {
  id: string
  type: 'rect'
  x: number
  y: number
  width: number
  height: number
  style: styleType
  pointer: boolean
}

interface polygonType {
  id: string
  type: 'polygon'
  points: { x: number, y: number }[]
  style: styleType
  pointer: boolean
}

interface drawState {
  scale: number // 缩放
  offsetX: number // 偏移X
  offsetY: number // 偏移Y
  isDragging: boolean // 是否拖拽
  dragStart: { x: number, y: number } // 拖拽开始位置
  shapes: (reactType | polygonType)[] // 所有形状
  currentShape: reactType | polygonType | null | any, // 当前绘制的形状
  mode: 'rect' | 'polygon' | null, // 'rect', 'polygon' // 当前绘制模式
  selectedShape: reactType | polygonType | null // 当前选中的形状
  currentStyle: styleType
}

class CanvasImage {
  private canvas: HTMLCanvasElement; // 当前画布
  private ctx: CanvasRenderingContext2D; // 当前画布内容
  private imgDom: HTMLImageElement; // 渲染的图片
  private state: drawState; // 画布状态
  // private fabricCanvas: fabric.Canvas; // fabric画布
  public labels: styleType[] = [] // 标注样式
  public drawingColor: string = '#fff'
  public editingColor: {
    border: string
    background: string
  } = {
      border: '#000',
      background: '#fff'
    }
  public currentAllScroll: boolean = false
  public draggingRect: { x: number, y: number }[] = [{ x: 0, y: 0 }]
  public coordinates: {
    'ns-resize': { x: number, y: number }[],
    'ew-resize': { x: number, y: number }[],
    'nwse-resize': { x: number, y: number }[],
    'nesw-resize': { x: number, y: number }[],
    'crosshair': { x: number, y: number }[],
  } = {
    'ns-resize': [],
    'ew-resize': [],
    'nwse-resize': [],
    'nesw-resize': [],
    'crosshair': [],
  }
  public editableDragging: boolean = false // 可编辑状态下的拖拽状态
  public lastEditablePoint: { type: 'ns-resize' | 'ew-resize' | 'nwse-resize' | 'nesw-resize' | 'crosshair', index: number } | null = null
  // 矩形拖拽时,需要记录当前拖拽的图形的左上角和宽高
  public rectInfo: {
    x: number
    y: number
    width: number
    height: number
  } = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  }

  constructor(_domCanvas: HTMLCanvasElement, _dom: HTMLDivElement, _imageOption?: ImageOption) {
    // this.fabricCanvas = new fabric.Canvas(_domCanvas);
    // console.log(this.fabricCanvas)
    // this.canvas = this.fabricCanvas.elements.lower.el
    this.canvas = _domCanvas;
    this.ctx = _domCanvas.getContext('2d')!;
    // 初始化图片
    this.imgDom = new Image();
    // 初始化状态
    this.state = {
      scale: 1,
      // offsetX: _dom.offsetLeft,
      // offsetY: _dom.offsetTop,
      offsetX: 0,
      offsetY: 0,
      isDragging: false,
      dragStart: { x: 0, y: 0 },
      shapes: [],
      currentShape: null,
      mode: null, // 'rect', 'polygon'
      selectedShape: null,
      currentStyle: { // '#95989e'
        styleId: 'string',
        strokeStyle: '#95989e',
        styleIndex: 1,
        styleLabel: ''
      }
    };
    // 初始化画布
    this.initCanvas(_dom);
    // 加载图片
    if (_imageOption) this.loadImage(_imageOption.src, _imageOption.width, _imageOption.height, _imageOption.top, _imageOption.left);
  }

  // 初始化画布
  public initCanvas(_div: HTMLDivElement) {
    // 监听大小变化
    this.resizeCanvas(_div)
    // 鼠标滚动缩放
    this.zoomCanvas()
    // 鼠标拖动
    this.dragCanvas()
    // 默认绘制逻辑为 null
    this.setMode(null);
  }

  // 修改当前绘制逻辑
  public setMode(mode: 'rect' | 'polygon' | null) {
    // polygon 绘制状态时,加入双击结束编辑操作
    mode == 'polygon' ? this.doubleClick('Listening') : this.doubleClick('remove');
    this.state.mode = mode;
    this.state.currentShape = null;
    this.changeCursor()
  }

  // 设置当前绘制样式
  public setStyle(style: styleType) {
    this.state.currentStyle = style;
    // this.state.currentStrokeStyle = style.currentStrokeStyle;
  }

  // 设置标注样式
  public setLabels(labels: styleType[]) {
    this.labels = labels;
  }

  // 设置当前canvas的鼠标样式
  public setCursor(cursor: 'grabbing' | 'grab' | 'pointer' | 'crosshair' | 'all-scroll' | 'e-resize' | 'n-resize' | 'w-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'nw-resize' | 'ne-resize' | 'ns-resize' | 'ew-resize' | 'nwse-resize' | 'nesw-resize' | 'col-resize' | 'row-resize') {
    this.canvas.style.cursor = cursor;
  }

  // 根据mode改变鼠标样式
  private changeCursor() {
    if (this.state.mode == null) {
      this.setCursor('grab')
    } else if (this.state.mode == 'rect') {
      this.setCursor('crosshair')
    } else if (this.state.mode == 'polygon') {
      this.setCursor('crosshair')
    }
  }

  // 绘制操作
  private draw() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 修改画布点位(左上角)
    // 修改画布缩放
    this.ctx.save();
    this.ctx.translate(this.state.offsetX, this.state.offsetY);
    this.ctx.scale(this.state.scale, this.state.scale);
    // 重新绘制图片
    // this.ctx.drawImage(this.imgDom, 0, 0, this.canvas.width, this.canvas.height);
    this.ctx.drawImage(this.imgDom, 0, 0, this.imgDom.width, this.imgDom.height);
    this.ctx.restore();

    // 修改 - 绘制的所有形状
    this.redrawStateShapes();

    // 修改 - 绘制当前正在创建的形状
    if (this.state.currentShape) this.redrawCurrentShape(this.state.currentShape, true);
  }

  // 重新绘制当前所绘制的形状
  public redrawStateShapes() {
    this.state.shapes.forEach(shape => {
      const label = this.labels.find(item => item.styleId === shape.style.styleId)
      if (label) shape.style = label
      // 原生绘制操作
      this.redrawCurrentShape(shape)
      // 替换原生操作 - 使用工具库 - 当前已经完成使用fabric重新绘制
      // if (shape.type === 'rect') {
      //   const { x, y, width, height } = shape;
      //   const rect = new fabric.Rect({
      //     left: x,
      //     top: y,
      //     fill: 'red',
      //     width,
      //     height,
      //   })
      //   this.fabricCanvas.add(rect);       
      // } else {

      // }
    });
  }

  // 绘制当前正在创建的形状
  public redrawCurrentShape(_shape: reactType | polygonType, _edit = false) {
    this.ctx.save();
    this.ctx.translate(this.state.offsetX, this.state.offsetY);
    this.ctx.scale(this.state.scale, this.state.scale);

    // 是否存在可以编辑
    const _editingIsCurrent = this.state.selectedShape && _shape.id == this.state.selectedShape.id
    const _color = _edit || _shape.pointer ? this.drawingColor : _shape.style.strokeStyle
    const _fillColor = operation.moreOperation.colorToRgba(_color, 0.3);
    this.ctx.strokeStyle = _color;
    this.ctx.fillStyle = _fillColor;
    this.ctx.lineWidth = 2 / this.state.scale;
    const _text = `#${_shape.style.styleIndex} ${_shape.style.styleLabel}`

    if (_shape.type === 'rect') {
      const { x, y, width, height } = _shape;
      this.ctx.fillRect(x, y, width, height); // 填充矩形
      this.ctx.strokeRect(x, y, width, height);
      // 绘制标签
      if (!_edit && !_editingIsCurrent) this.redrawLabel(x + width / 2, y + height / 2, _text, _color);
    } else if (_shape.type === 'polygon') {
      this.ctx.beginPath();
      _shape.points.forEach((p: { x: number; y: number }, i: number) => {
        if (i === 0) this.ctx.moveTo(p.x, p.y);
        else this.ctx.lineTo(p.x, p.y);
      });
      if (!_edit) this.ctx.closePath();
      this.ctx.fill(); // 填充多边形
      this.ctx.stroke();
      if (!_editingIsCurrent) {
        if (!_edit) {
          // 绘制标签
          const centerX = _shape.points.reduce((sum, p) => sum + p.x, 0) / _shape.points.length;
          const centerY = _shape.points.reduce((sum, p) => sum + p.y, 0) / _shape.points.length;
          this.redrawLabel(centerX, centerY, _text, _color);
        } else {
          // 编辑模式下提示双击结束编辑
          this.redrawLabel(_shape.points[_shape.points.length - 1].x + 60, _shape.points[_shape.points.length - 1].y - 15, '双击结束编辑', 'rgba(255, 255, 255, 0.7)', 'black');
        }
      }
    }

    if (_editingIsCurrent) this.editShape(_shape);
    this.ctx.restore();
  }

  // 绘制标签
  public redrawLabel(x: number, y: number, text: string, backgroundColor: string = 'rgba(255, 255, 255, 0.7)', textColor: string = 'black', borderRadius: number = 5) {
    const padding = 5; // 标签内边距
    const fontSize = 16; // 字体大小

    // 设置字体样式
    this.ctx.font = `${fontSize}px Arial`;
    this.ctx.textAlign = 'center';
    this.ctx.textBaseline = 'middle';

    // 计算文本宽度和高度
    const textWidth = this.ctx.measureText(text).width;
    const textHeight = fontSize; // 简单估计文本高度

    // 绘制背景矩形(圆角)
    this.ctx.fillStyle = backgroundColor;
    this.ctx.beginPath();
    this.ctx.moveTo(x - textWidth / 2 - padding + borderRadius, y - textHeight / 2 - padding);
    this.ctx.lineTo(x + textWidth / 2 + padding - borderRadius, y - textHeight / 2 - padding);
    this.ctx.quadraticCurveTo(x + textWidth / 2 + padding, y - textHeight / 2 - padding, x + textWidth / 2 + padding, y - textHeight / 2 - padding + borderRadius);
    this.ctx.lineTo(x + textWidth / 2 + padding, y + textHeight / 2 + padding - borderRadius);
    this.ctx.quadraticCurveTo(x + textWidth / 2 + padding, y + textHeight / 2 + padding, x + textWidth / 2 + padding - borderRadius, y + textHeight / 2 + padding);
    this.ctx.lineTo(x - textWidth / 2 - padding + borderRadius, y + textHeight / 2 + padding);
    this.ctx.quadraticCurveTo(x - textWidth / 2 - padding, y + textHeight / 2 + padding, x - textWidth / 2 - padding, y + textHeight / 2 + padding - borderRadius);
    this.ctx.lineTo(x - textWidth / 2 - padding, y - textHeight / 2 - padding + borderRadius);
    this.ctx.quadraticCurveTo(x - textWidth / 2 - padding, y - textHeight / 2 - padding, x - textWidth / 2 - padding + borderRadius, y - textHeight / 2 - padding);
    this.ctx.closePath();
    this.ctx.fill();

    // 绘制文本
    this.ctx.fillStyle = textColor;
    this.ctx.fillText(text, x, y);
  }

  // 编辑图形的相应操作
  public editShape(shape: reactType | polygonType) {
    this.coordinates = {
      'ns-resize': [],
      'ew-resize': [],
      'nwse-resize': [],
      'nesw-resize': [],
      'crosshair': [],
    }
    let _points = [];
    if (shape.type === 'rect') {
      _points = this.getRectangleCorners(shape.x, shape.y, shape.width, shape.height, true);
      this.coordinates['ns-resize'].push(_points[4], _points[6]);
      this.coordinates['ew-resize'].push(_points[5], _points[7]);
      this.coordinates['nwse-resize'].push(_points[0], _points[2]);
      this.coordinates['nesw-resize'].push(_points[1], _points[3]);
    } else {
      _points = shape.points;
      this.coordinates['crosshair'] = _points;
    }

    // 绘制可编辑点
    _points.forEach(p => {
      // 绘制边框和填充
      this.ctx.fillStyle = this.editingColor.background; // 设置填充颜色
      this.ctx.strokeStyle = this.editingColor.border; // 设置边框颜色
      this.ctx.lineWidth = 2; // 设置边框宽度

      this.ctx.beginPath();
      this.ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
      this.ctx.fill(); // 填充颜色
      this.ctx.stroke(); // 绘制边框
    });
  }

  // 清空图片
  public clearImageLoad() {
    this.imgDom = new Image();
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  // 手动加载图片
  public loadImage(_imageSrc: string, width = 0, height = 0, top = 0, left = 0) {
    this.imgDom.src = _imageSrc;
    this.imgDom.onload = () => {
      width = width || this.imgDom.width
      height = height || this.imgDom.height
      this.ctx.drawImage(this.imgDom, top, left, width, height);
    };
  }

  // 设置Canvas大小
  private resizeCanvas(_div: HTMLDivElement) {
    this.canvas.width = _div.clientWidth;
    this.canvas.height = _div.clientHeight;
    window.addEventListener('resize', () => this.resizeCanvas(_div));
  }

  // 缩放处理
  private zoomCanvas() {
    this.canvas.addEventListener('wheel', e => {
      e.preventDefault();
      const zoomIntensity = 0.1;
      const delta = e.deltaY < 0 ? 1 : -1;
      const newScale = delta > 0 ? this.state.scale * (1 + zoomIntensity) : this.state.scale / (1 + zoomIntensity);

      // 以鼠标位置为中心缩放
      const mouseX = e.offsetX;
      const mouseY = e.offsetY;
      const imgPos = this.viewportToImage(mouseX, mouseY);

      this.state.scale = newScale;
      this.state.offsetX = mouseX - imgPos.x * this.state.scale;
      this.state.offsetY = mouseY - imgPos.y * this.state.scale;

      console.log(this.state)
      this.draw();
    }, { passive: false });
  }

  private dragCanvas() {
    // 鼠标按下
    this.dragStart();
    // 鼠标移动
    this.dragMove();
    // 鼠标抬起
    this.dragEnd();
    // 鼠标右键
    this.rightClick();
  }

  // 鼠标拖动开始
  private dragStart() {
    this.canvas.addEventListener('mousedown', e => {
      if (e.button === 2) {
        // 右键点击
        console.log("右键点击");
        // 处理右键点击的逻辑
        return
      }
      const imgPos = this.viewportToImage(e.offsetX, e.offsetY);

      if (this.state.mode === 'rect') {
        this.state.currentShape = {
          id: operation.moreOperation.generateRandomString(16),
          type: 'rect',
          x: imgPos.x,
          y: imgPos.y,
          width: 0,
          height: 0,
          style: this.state.currentStyle
        };
      } else if (this.state.mode === 'polygon') {
        if (!this.state.currentShape) {
          this.state.currentShape = {
            id: operation.moreOperation.generateRandomString(16),
            type: 'polygon',
            points: [imgPos],
            style: this.state.currentStyle
          };
        } else {
          this.state.currentShape.points!.push(imgPos);
        }
      } else {
        // 检测是否选中形状
        let _activeSelectShapeId = this.state.selectedShape?.id
        this.state.selectedShape = null;
        const _editablePoint = this.isPointInEditablePoints(imgPos);
        const _selectShape = this.isPointInShape(imgPos);
        if (_editablePoint) {
          this.setActiveShape(_activeSelectShapeId!)
          this.setCursor(_editablePoint.type)
          this.editableDragging = true
          this.lastEditablePoint = _editablePoint
          if (this.state.selectedShape!.type == 'rect') {
            const _shape: any = JSON.parse(JSON.stringify(this.state.selectedShape))
            this.rectInfo = _shape
          }
        } else if (_selectShape) {
          this.state.selectedShape = _selectShape;
          this.currentAllScroll = true
          // 计算坐标差
          if (_selectShape.type == 'rect') {
            this.draggingRect[0].x = imgPos.x - _selectShape.x;
            this.draggingRect[0].y = imgPos.y - _selectShape.y;
          } else {
            // 记录拖拽时,每一个点的原始位置
            _selectShape.points.forEach((point, index) => {
              const _point = this.draggingRect[index]
              const _x = imgPos.x - point.x;
              const _y = imgPos.y - point.y;
              if (_point) {
                _point.x = _x;
                _point.y = _y;
              } else {
                this.draggingRect.push({
                  x: _x,
                  y: _y
                })
              }
            })
          }
        } else {
          // 开始拖拽画布
          this.setCursor('grabbing');
          this.state.isDragging = true;
          this.state.dragStart.x = e.offsetX - this.state.offsetX;
          this.state.dragStart.y = e.offsetY - this.state.offsetY;
        }
      }
      this.draw();
    });
  }

  // 鼠标拖动 move
  private dragMove() {
    this.canvas.addEventListener('mousemove', e => {
      if (this.state.isDragging) {
        this.state.offsetX = e.offsetX - this.state.dragStart.x;
        this.state.offsetY = e.offsetY - this.state.dragStart.y;
      } else if (!this.state.currentShape) {
        // 检测是否选中形状
        let _activeSelectShapeId = this.state.selectedShape?.id
        // 不是拖拽情况下时,判断是否move到某个已经绘制好的标注上面
        const imgPos = this.viewportToImage(e.offsetX, e.offsetY);
        if (this.editableDragging && this.lastEditablePoint) {
          if (this.state.selectedShape?.type == 'rect') {
            const { x, y, width, height } = this.adjustRectangle(
              this.rectInfo.x,
              this.rectInfo.y,
              this.rectInfo.width,
              this.rectInfo.height,
              this.setCurrentPointIng(),
              imgPos.x,
              imgPos.y
            )
            // 重新计算矩形的 x, y, width, height
            this.state.selectedShape.x = x;
            this.state.selectedShape.y = y;
            this.state.selectedShape.width = width;
            this.state.selectedShape.height = height;
          } else {
            this.coordinates[this.lastEditablePoint.type][this.lastEditablePoint.index].x = imgPos.x
            this.coordinates[this.lastEditablePoint.type][this.lastEditablePoint.index].y = imgPos.y
          }
        } else {
          const _editablePoint = this.isPointInEditablePoints(imgPos);
          const _selectShape = this.isPointInShape(imgPos)
          // 是否选中 编辑形状
          if (_editablePoint) {
            this.setActiveShape(_activeSelectShapeId!)
            this.setCursor(_editablePoint.type)
            // this.lastEditablePoint = _editablePoint
          }
          // 判断是否选中 编辑形状
          if (this.state.selectedShape) {
            if (this.currentAllScroll) {
              if (this.state.selectedShape.type === 'rect') {
                this.state.selectedShape.x = imgPos.x - this.draggingRect[0].x;
                this.state.selectedShape.y = imgPos.y - this.draggingRect[0].y;
              } else {
                // 更新多边形的每个点的位置
                this.state.selectedShape.points.forEach((point, index) => {
                  point.x = imgPos.x - this.draggingRect[index].x;
                  point.y = imgPos.y - this.draggingRect[index].y;
                });
              }
            }
          }
        }
      } else if (this.state.currentShape?.type === 'rect') {
        const start = this.state.currentShape;
        const current = this.viewportToImage(e.offsetX, e.offsetY);
        start.width = current.x - start.x!;
        start.height = current.y - start.y!;
      } else if (this.state.currentShape?.type === 'polygon') {
        const current = this.viewportToImage(e.offsetX, e.offsetY);
        let currentIndex = this.state.currentShape.points!.length - 1 == 0 ? 1 : this.state.currentShape.points!.length - 1;
        this.state.currentShape.points![currentIndex] = current;
      }
      this.draw();
    });
  }

  // 鼠标拖动结束
  private dragEnd() {
    this.canvas.addEventListener('mouseup', e => {
      this.state.isDragging = false;
      this.editableDragging = false;
      // 停止拖拽 图形
      this.currentAllScroll = false
      this.changeCursor()
      if (this.state.currentShape) {
        if (this.state.currentShape.type === 'rect') {
          if (this.state.currentShape.width !== 0 && this.state.currentShape.height !== 0) {
            this.state.shapes.push(this.state.currentShape);
          }
          this.state.currentShape = null;
        }
      }
    });
  }

  // 双击结束 - 本次 - 绘制polygon
  private doubleClick(_type: 'Listening' | 'remove') {
    const dbClickEndShape = () => {
      if (this.state.currentShape?.type === 'polygon') {
        // 移出多个 多出来的点
        this.state.currentShape.points.pop();
        this.state.currentShape.points.pop();
        this.state.shapes.push(this.state.currentShape);
        this.state.currentShape = null;
        this.draw();
      }
      // this.state.mode = null;
      // this.setMode('polygon')
    }
    if (_type == 'Listening') {
      this.canvas.addEventListener('dblclick', dbClickEndShape);
    } else {
      this.canvas.removeEventListener('dblclick', dbClickEndShape);
    }
  }

  // 鼠标右键
  private rightClick() {
    this.canvas.addEventListener('contextmenu', e => {
      e.preventDefault();
    })
  }

  // 拖拽修改矩形修改点之后,针对修改之后的操作,计算最新的矩形坐标
  private adjustRectangle(originalX: number, originalY: number, originalWidth: number, originalHeight: number, draggedPointIndex: number, newX: number, newY: number) {
    let x = originalX;
    let y = originalY;
    let width = originalWidth;
    let height = originalHeight;

    // 计算当前矩形的四个角点
    const currentRight = originalX + originalWidth;
    const currentBottom = originalY + originalHeight;

    const updateCorner = (oppositeX: number, oppositeY: number) => {
      // 动态计算新矩形坐标和尺寸
      const newLeft = Math.min(newX, oppositeX);
      const newTop = Math.min(newY, oppositeY);
      const newRight = Math.max(newX, oppositeX);
      const newBottom = Math.max(newY, oppositeY);

      x = newLeft;
      y = newTop;
      width = newRight - newLeft;
      height = newBottom - newTop;
    }

    const updateTopEdge = () => {
      const bottomY = originalY + originalHeight;
      const newHeight = bottomY - newY;
      if (newHeight < 0) {
        y = bottomY;
        height = -newHeight;
      } else {
        y = newY;
        height = newHeight;
      }
      width = originalWidth;
      x = originalX;
    }

    const updateBottomEdge = () => {
      const newHeight = newY - originalY;
      if (newHeight < 0) {
        y = newY;
        height = -newHeight;
      } else {
        height = newHeight;
      }
      width = originalWidth;
      x = originalX;
    }

    const updateRightEdge = () => {
      const newWidth = newX - originalX;
      if (newWidth < 0) {
        x = newX;
        width = -newWidth;
      } else {
        width = newWidth;
      }
      height = originalHeight;
      y = originalY;
    }

    const updateLeftEdge = () => {
      const rightX = originalX + originalWidth;
      const newWidth = rightX - newX;
      if (newWidth < 0) {
        x = rightX;
        width = -newWidth;
      } else {
        x = newX;
        width = newWidth;
      }
      height = originalHeight;
      y = originalY;
    }

    switch (draggedPointIndex) {
      // 角点处理
      case 0: // 左上角
        updateCorner(currentRight, currentBottom); // 使用当前右下角作为对顶点
        break;
      case 2: // 右上角
        updateCorner(originalX, currentBottom);
        break;
      case 4: // 右下角
        updateCorner(originalX, originalY);
        break;
      case 6: // 左下角
        updateCorner(currentRight, originalY);
        break;
      // 边中点处理
      case 1: // 上边中点
        updateTopEdge();
        break;
      case 5: // 下边中点
        updateBottomEdge();
        break;
      case 3: // 右边中点
        updateRightEdge();
        break;
      case 7: // 左边中点
        updateLeftEdge();
        break;
      default:
        throw new Error('Invalid point index');
    }

    return { x, y, width, height };
  }

  // 设置当前 的是哪个点
  private setCurrentPointIng() {
    // this.lastEditablePoint.type this.lastEditablePoint.index
    if (this.lastEditablePoint!.type == 'ew-resize') {
      if (this.lastEditablePoint!.index == 0) {
        return 7
      } else if (this.lastEditablePoint!.index == 1) return 3
    } else if (this.lastEditablePoint!.type == 'ns-resize') {
      if (this.lastEditablePoint!.index == 0) {
        return 1
      } else if (this.lastEditablePoint!.index == 1) return 5
    } else if (this.lastEditablePoint!.type == 'nwse-resize') {
      if (this.lastEditablePoint!.index == 0) {
        return 0
      } else if (this.lastEditablePoint!.index == 1) return 4
    } else if (this.lastEditablePoint!.type == 'nesw-resize') {
      if (this.lastEditablePoint!.index == 0) {
        return 2
      } else if (this.lastEditablePoint!.index == 1) return 6
    }
    return 0
  }

  // 通过矩形左上角x、y、width、height获得矩形的四个坐标
  private getRectangleCorners(x: number, y: number, width: number, height: number, _edit = false): { x: number, y: number }[] {
    const _points = [
      { x: x, y: y }, // 左上角
      { x: x + width, y: y }, // 右上角
      { x: x + width, y: y + height }, // 右下角
      { x: x, y: y + height }, // 左下角
    ]
    if (_edit) {
      _points.push(...[
        { x: x + width / 2, y: y }, // 上边中点
        { x: x, y: y + height / 2 }, // 左边中点
        { x: x + width / 2, y: y + height }, // 下边中点
        { x: x + width, y: y + height / 2 } // 右边中点
      ])
    }
    return _points;
  }

  // 坐标转换:视口坐标 -> 图片坐标
  private viewportToImage(x: number, y: number) {
    return {
      x: (x - this.state.offsetX) / this.state.scale,
      y: (y - this.state.offsetY) / this.state.scale
    };
  }

  // 坐标转换:图片坐标 -> 视口坐标
  private imageToViewport(x: number, y: number) {
    return {
      x: x * this.state.scale + this.state.offsetX,
      y: y * this.state.scale + this.state.offsetY
    };
  }

  // 判断鼠标当前的位置是否在已经绘制的形状中
  private isPointInShape(imgPos: { x: number, y: number }) {
    // 检测是否选中形状
    for (let i = 0; i < this.state.shapes.length; i++) {
      const shape = this.state.shapes[i];
      const _selectShape = this.state.selectedShape && shape.id == this.state.selectedShape.id
      const _currentCursor = _selectShape ? 'all-scroll' : 'pointer'
      // 设置指针
      shape.pointer = false;
      // 简单矩形碰撞检测
      if (shape.type === 'rect' &&
        imgPos.x >= shape.x && imgPos.x <= shape.x + shape.width &&
        imgPos.y >= shape.y && imgPos.y <= shape.y + shape.height) {
        this.setCursor(_currentCursor)
        shape.pointer = true;
        return shape;
      } else if (shape.type === 'polygon') {
        const points = shape.points;
        let inside = false;

        for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
          const xi = points[i].x, yi = points[i].y;
          const xj = points[j].x, yj = points[j].y;

          const intersect = ((yi > imgPos.y) !== (yj > imgPos.y)) &&
            (imgPos.x < (xj - xi) * (imgPos.y - yi) / (yj - yi) + xi);
          if (intersect) inside = !inside;
        }

        if (inside) {
          this.setCursor(_currentCursor);
          shape.pointer = true;
          return shape;
        }
      }
    }
    this.changeCursor()
    return undefined;
  }

  // 判断鼠标按下的地方是否在图片内
  private isPointInImage(imgPos: { x: number, y: number }) {
    const imgX = this.imgDom.offsetLeft; // 图片的左上角 x 坐标
    const imgY = this.imgDom.offsetTop;  // 图片的左上角 y 坐标
    const imgWidth = this.imgDom.width;  // 图片的宽度
    const imgHeight = this.imgDom.height; // 图片的高度

    return imgPos.x >= imgX && imgPos.x <= imgX + imgWidth &&
      imgPos.y >= imgY && imgPos.y <= imgY + imgHeight;
  }

  // 检查鼠标是否在可编辑点内
  private isPointInEditablePoints(mousePos: { x: number, y: number }): {
    type: 'ns-resize' | 'ew-resize' | 'nwse-resize' | 'nesw-resize' | 'crosshair'
    index: number
  } | undefined {
    const _isPoint = (dx: number, dy: number) => {
      const radius = 5; // 可编辑点的半径
      return Math.sqrt(dx * dx + dy * dy) <= radius
    }

    for (const key of Object.keys(this.coordinates) as ('ns-resize' | 'ew-resize' | 'nwse-resize' | 'nesw-resize' | 'crosshair')[]) {
      const _points = this.coordinates[key];
      for (let i = 0; i < _points.length; i++) {
        const point = _points[i];
        const dx = mousePos.x - point.x;
        const dy = mousePos.y - point.y;
        const _flag = _isPoint(dx, dy)
        if (_flag) {
          return { type: key, index: i }
        }
      }
    }

    return undefined
  }

  // 设置某个图形 激活
  private setActiveShape(shapeId: string) {
    for (let i = 0; i < this.state.shapes.length; i++) {  // 遍历所有图形
      const _shape = this.state.shapes[i];
      if (_shape.id == shapeId) { // 找到对应的图形
        this.state.selectedShape = _shape; // 设置为激活状态
        _shape.pointer = true
        break;
      }
    }
  }
}

export default CanvasImage

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值