You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas实现点击选中形状时在边角显示小矩形选中标记

实现方案

Canvas是即时模式的位图绘制接口,不存在“修改已绘制图形样式”的概念,所有选中效果都需要通过全量重绘实现,核心流程如下:

  • 不要硬编码坐标直接绘制图形,先把所有图形的类型、坐标、尺寸等参数存在结构化数组里,每个图形增加selected布尔字段标记选中状态
  • 每次画布状态变化(点击选中、拖拽、缩放)时,清空整个画布,重新绘制所有图形
  • 绘制完成后,遍历所有标记为选中的图形,计算其控制点/外接矩形,在对应位置绘制小矩形选择手柄
核心逻辑说明

手柄位置计算

不同图形的手柄位置计算逻辑统一即可对齐Office类软件的交互体验:

  • 直线、三角形等由离散点构成的图形:手柄直接落在图形顶点、外接矩形的边中点位置
  • 矩形、圆形等闭合规则图形:手柄落在其外接矩形的4个角、4条边中点,共8个位置
  • 手柄样式统一用6~8px见方的白色填充+1px黑色描边矩形,辨识度最高,绘制时注意以计算出的坐标为中心点,避免位置偏移

外接矩形计算规则

所有图形的外接矩形都可以通过自身参数快速计算:

  • 矩形:直接取自身的x、y坐标与宽高
  • 圆形:外接矩形起点为(圆心x-半径, 圆心y-半径),宽高均为2倍半径
  • 直线、多边形:遍历所有顶点坐标,取x、y的最小值作为外接矩形起点,x、y的最大值与最小值的差作为宽高

命中检测

简单场景可以直接用外接矩形做点击判断;如果需要精确命中(比如三角形内部空白区域、圆形外的矩形边角不触发选中),可以用Canvas原生的isPointInPath()API做路径级检测。

改造后的可运行代码

HTML

<div>
  <canvas id="draw_area" height="700" width="700"></canvas>
</div>

CSS

#draw_area{
  background-color: lightgrey;
}

JavaScript

const draw_area = document.getElementById('draw_area');
const ctx = draw_area.getContext('2d');
// 选择手柄尺寸配置
const HANDLE_SIZE = 6;
// 结构化存储所有图形参数
const shapes = [
  { type: 'rect', x: 50, y: 50, w: 100, h: 100, selected: false },
  { type: 'line', x1: 230, y1: 100, x2: 330, y2: 100, selected: false },
  { type: 'triangle', points: [[420,50], [470,150], [370,150]], selected: false },
  { type: 'circle', cx: 600, cy: 100, r: 50, selected: false }
];

// 计算图形外接矩形
function getBoundingBox(shape) {
  switch(shape.type) {
    case 'rect':
      return { x: shape.x, y: shape.y, w: shape.w, h: shape.h };
    case 'line':
      const minX = Math.min(shape.x1, shape.x2);
      const minY = Math.min(shape.y1, shape.y2);
      const maxX = Math.max(shape.x1, shape.x2);
      const maxY = Math.max(shape.y1, shape.y2);
      return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
    case 'triangle':
      const xs = shape.points.map(p => p[0]);
      const ys = shape.points.map(p => p[1]);
      return { 
        x: Math.min(...xs), 
        y: Math.min(...ys), 
        w: Math.max(...xs) - Math.min(...xs), 
        h: Math.max(...ys) - Math.min(...ys) 
      };
    case 'circle':
      return { x: shape.cx - shape.r, y: shape.cy - shape.r, w: shape.r * 2, h: shape.r * 2 };
  }
}

// 绘制单个图形
function drawShape(shape) {
  ctx.beginPath();
  switch(shape.type) {
    case 'rect':
      ctx.rect(shape.x, shape.y, shape.w, shape.h);
      break;
    case 'line':
      ctx.moveTo(shape.x1, shape.y1);
      ctx.lineTo(shape.x2, shape.y2);
      break;
    case 'triangle':
      ctx.moveTo(shape.points[0][0], shape.points[0][1]);
      shape.points.slice(1).forEach(p => ctx.lineTo(p[0], p[1]));
      ctx.closePath();
      break;
    case 'circle':
      ctx.arc(shape.cx, shape.cy, shape.r, 0, 2 * Math.PI);
      break;
  }
  ctx.stroke();
}

// 绘制选中状态手柄
function drawHandles(shape) {
  const { x, y, w, h } = getBoundingBox(shape);
  const handlePoints = [
    [x, y], [x + w/2, y], [x + w, y],
    [x, y + h/2], [x + w, y + h/2],
    [x, y + h], [x + w/2, y + h], [x + w, y + h]
  ];
  ctx.save();
  ctx.fillStyle = '#ffffff';
  ctx.strokeStyle = '#000000';
  ctx.lineWidth = 1;
  handlePoints.forEach(([px, py]) => {
    ctx.beginPath();
    ctx.rect(px - HANDLE_SIZE/2, py - HANDLE_SIZE/2, HANDLE_SIZE, HANDLE_SIZE);
    ctx.fill();
    ctx.stroke();
  });
  ctx.restore();
}

// 全局渲染函数,状态变更时调用
function render() {
  ctx.clearRect(0, 0, draw_area.width, draw_area.height);
  shapes.forEach(shape => drawShape(shape));
  shapes.filter(s => s.selected).forEach(shape => drawHandles(shape));
}

// 点击选中逻辑示例
draw_area.addEventListener('click', (e) => {
  const canvasRect = draw_area.getBoundingClientRect();
  const clickX = e.clientX - canvasRect.left;
  const clickY = e.clientY - canvasRect.top;
  shapes.forEach(s => s.selected = false);
  // 倒序遍历,优先选中上层图形
  for(let i = shapes.length - 1; i >= 0; i--) {
    const bbox = getBoundingBox(shapes[i]);
    if(clickX >= bbox.x && clickX <= bbox.x + bbox.w && clickY >= bbox.y && clickY <= bbox.y + bbox.h) {
      shapes[i].selected = true;
      break;
    }
  }
  render();
});

// 初始渲染
render();
扩展优化方向
  • 需要虚线选中边框时,在绘制选中图形前调用ctx.setLineDash([5, 3]),绘制完成后重置ctx.setLineDash([])即可
  • 实现拖拽缩放功能时,只需给每个手柄绑定鼠标事件,拖拽时修改对应图形的参数,调用render()重绘即可
  • 多图形选中、组选中逻辑可以基于当前结构扩展,给选中的组统一计算外接矩形绘制手柄即可

内容的提问来源于stack exchange,提问作者Cptain_Whitebeard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 23:12:33