canvas绘制拓扑图

90 阅读3分钟

屏幕截图 2025-07-03 203027.png

canvas坐标系统默认为左上角为原点,横轴x轴,向右为正,纵轴为y轴,向下为正的窗口坐标系统,可以对坐标系统坐标变换,平移、缩放、旋转、自定义变换方式等。
mousemove、mouseup和mousedown等事件获取到的是默认窗口坐标系统的事件位置。在获取绘画内容的位置时,需要转换后的坐标系统和默认窗口坐标系统转换计算。
canvas对于画笔样式使用save,restore栈的方式记录。

坐标变换
移动

translate(x, y),画布沿着X轴方向和Y轴方向进行平移。
下图,有一个点在原坐标系的坐标是(20,10)。 屏幕截图 2025-06-13 144449.png translate(6, 5),坐标系变换,原点(画布)向右平移6,向下平移5。此时得到圆点在原始坐标系位置为(20+6, 10+5)即(26, 15)。

屏幕截图 2025-07-04 143733.png

缩放

scale(x, y),画布沿着X轴方向和Y轴方向进行比例缩放。 scale(0.5, 0.5),坐标系在平移基础上再次进行变换,画布在x轴和y轴上缩小0.5倍。圆点到变换后的坐标系的实际坐标为(10, 5)。此时,圆点在最初始的坐标系的位置为(10+6, 5+5),即(16, 10)。

屏幕截图 2025-07-04 145037.png

事件event位置

canvas事件存储的是相对于最原始坐标系的位置。在需要判断事件是否在画布上的某个位置时,需要把转换坐标系中的坐标转换成原始坐标系的坐标。 上面的实例,先translate(6, 5),在scale(0.5, 0.5)。圆点(x, y)(即(20, 10))在最原始坐标系的坐标是:
x' = 6 + (x* 0.5),
y' = 5 + (y* 0.5)。

屏幕截图 2025-07-04 170730.png

clientX表示事件到窗口的x轴距离,react.left表示canvas容器到窗口的距离,posX(react.left - e.clientX)则表示事件在坐标系上x轴上的距离。


 canvas.addEventListener('mousemove',(e: any) => {
    // canvas容器相对于窗口的位置
    let react = canvas.getBoundingClientRect();

    //e.clientX相对于窗口的位置,canvasBox.left canvas容器相对于窗口的左侧距离,mouseX鼠标在原坐标系的位置
    let x = e.clientX -  react.left;
    let y = e.clientY - react.top;
    console.log('鼠标在原坐标系x轴得位置':x);
    console.log('鼠标在原坐标系y轴得位置':y);
})

如果click的位置(posX, posY)在(x', y')上,点击位置在圆点上。

拓补图支持适配

通过初始化一个_w和_h的值,_w = 375, _h = 318,_unit = 1。监听页面的大小canvas的大小跟随变动,计算_unit的值。

let _unit = 1;
function updateCanvas() {
  if (!ctx) return;
  const _w = 375;
  const _h = 318;
  if (canvas_w / _w > canvas_h / _h) {
    _unit = canvas_h / _h;
  } else {
    _unit = canvas_w / _w;
  }
  ctx.clearRect(0, 0, canvas_w, canvas_h);
  ctx.scale(_unit, _unit);
 
  ctx && draw(ctx);
}
鼠标在节点上展示手的形状

监听mousemove事件,如果鼠标在节点上显示手状。通过事件e.clientX和e.clientY的值,与节点在坐标轴上的转换值对比。

function bindEvent() {
  canvas.value!.addEventListener('mousemove',(e: any) => {
    let mouseX = e.clientX;
    let mousY = e.clientY;
    checkMousePos(mouseX, mousY);
  })
}
function checkMousePos(mouseX: number, mouseY: number) {
  for (let i = 0 ; i < imgList.length; i++) {
    let canvasBox = canvas.value!.getBoundingClientRect();
   
    let minx = (imgList[i].x - imgWidth / 2 ) * _unit + canvas_w / 2 + canvasBox.left;
    let maxx = (imgList[i].x  + imgWidth / 2) * _unit + canvas_w / 2 + canvasBox.left;
    let miny = (imgList[i].y - imgWidth / 2)  * _unit + canvas_h / 2 + canvasBox.top ;
    let maxy = (imgList[i].y  + imgWidth / 2) * _unit + canvas_h / 2  + canvasBox.top;
    
    if (mouseX > minx && mouseX < maxx && mouseY > miny && mouseY < maxy) {
      canvas.value &&(canvas.value.style.cursor = 'pointer');
      break
    } else {
      canvas.value &&(canvas.value.style.cursor = 'default');
    }
  }
}
鼠标在节点展示tooltip

原理和节点展示手状相同。

function checkMousePos(mouseX: number, mouseY: number) {
  for (let i = 0 ; i < imgList.length; i++) {
 
     //....
    
        if (mouseX > minx && mouseX < maxx && mouseY > miny && mouseY < maxy) 
    {
     let x = imgList[i].x + (mouseX - minx);
     let y = imgList[i].y + (mouseY - miny);
      drawToolTip('测试',x, y);
      break
    }
  }
}