•
阅读时长 3 分钟

可视化之 DOM、SVG 和 Canvas 拖拽区别

目录

拖动一个矩形,DOM、SVG 和 Canvas 都能做。区别不只在“怎么画出来”,还在图形有没有节点、怎么接事件,以及谁来保存位置和处理命中。

下面三个区域共用一份坐标,拖动任意一个,另外两个一起变化。只比较一个矩形的交互,不拿它评价大量图形时的性能。

保存为一个 HTML 文件就能运行

创建 drawing.html,用浏览器打开:

<!doctype html>
<html lang="zh-CN">
  <meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>
    三种矩形</title
  ><style>
    .surface {
      width: 300px;
      height: 160px;
      border: 1px solid #999;
      touch-action: none;
      position: relative;
      margin: 12px;
    }
    #rect {
      position: absolute;
      width: 70px;
      height: 50px;
      background: #457b9d;
    }
    #rect:focus {
      outline: 3px solid orange;
    }
  </style>
  <h1>拖动一个矩形</h1>
  <p>DOM 矩形可用方向键移动;三个场景共享同一份坐标。</p>
  <div id="dom" class="surface">
    <button id="rect" aria-label="矩形" style="border: 0"></button>
  </div>
  <svg id="svg" class="surface" viewBox="0 0 300 160">
    <rect id="svg-rect" width="70" height="50" fill="#457b9d" /></svg
  ><canvas id="canvas" class="surface"></canvas>
  <pre id="coords"></pre>
  <script>
    const state = { x: 20, y: 20 };
    window.drawingState = state;
    const dom = document.querySelector("#dom"),
      svg = document.querySelector("#svg"),
      canvas = document.querySelector("#canvas");
    const dpr = devicePixelRatio || 1;
    canvas.width = 300 * dpr;
    canvas.height = 160 * dpr;
    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);
    function draw() {
      const rect = document.querySelector("#rect");
      rect.style.left = state.x + "px";
      rect.style.top = state.y + "px";
      document.querySelector("#svg-rect").setAttribute("x", state.x);
      document.querySelector("#svg-rect").setAttribute("y", state.y);
      ctx.clearRect(0, 0, 300, 160);
      ctx.fillStyle = "#457b9d";
      ctx.fillRect(state.x, state.y, 70, 50);
      document.querySelector("#coords").textContent = JSON.stringify(state);
    }
    function move(x, y) {
      state.x = Math.max(0, Math.min(230, x));
      state.y = Math.max(0, Math.min(110, y));
      draw();
    }
    for (const surface of [dom, svg, canvas]) {
      let offset = null;
      function point(event) {
        const bounds = surface.getBoundingClientRect();
        return {
          x: event.clientX - bounds.left - surface.clientLeft,
          y: event.clientY - bounds.top - surface.clientTop,
        };
      }
      surface.addEventListener("pointerdown", (event) => {
        const p = point(event);
        if (p.x < state.x || p.x > state.x + 70 || p.y < state.y || p.y > state.y + 50)
          return;
        offset = { x: p.x - state.x, y: p.y - state.y };
        surface.setPointerCapture(event.pointerId);
      });
      surface.addEventListener("pointermove", (event) => {
        if (offset) {
          const p = point(event);
          move(p.x - offset.x, p.y - offset.y);
        }
      });
      const stop = () => {
        offset = null;
      };
      surface.addEventListener("pointerup", stop);
      surface.addEventListener("pointercancel", stop);
    }
    document.querySelector("#rect").addEventListener("keydown", (event) => {
      const delta = {
        ArrowLeft: [-5, 0],
        ArrowRight: [5, 0],
        ArrowUp: [0, -5],
        ArrowDown: [0, 5],
      }[event.key];
      if (delta) {
        event.preventDefault();
        move(state.x + delta[0], state.y + delta[1]);
      }
    });
    draw();
  </script>
</html>

DOM 版本是绝对定位的 button,更新 left 和 top;SVG 版本是 rect,更新 x 和 y;Canvas 则清空并按坐标重新绘制。坐标数据保存在 state,不依赖从某个画面反查。

图形节点和像素画面,交互成本不同

DOM 和 SVG 保留节点,可以把事件直接绑定到图形。Canvas 只有画布元素,内部矩形不是能 querySelector 的节点,需要根据场景数据判断指针是否命中,再更新数据和重画。

示例为了让三种方式共用拖动流程,都在区域上判断命中。这不是 DOM 或 SVG 必须手写 hit test,而是这个对照例子的选择。

DOM 的 button 可以获得焦点,示例补了方向键移动。SVG 可以设计键盘和语义,Canvas 也可以配合 DOM 提供可访问层,但这些都不会因为图形被画出来就自动完成。

拖动时用的是逻辑坐标

指针的 clientX / clientY 减去区域的位置和边框,得到内部坐标。示例区域是固定的 300 × 160,没有 CSS 缩放。加了缩放、SVG 变换或可缩放视口之后,要补坐标映射,不能原样沿用减法。

Canvas 的 bitmap 按 devicePixelRatio 放大,再 scale 回逻辑尺寸,减少高清屏的模糊。它仍然是位图,不会因此变成矢量图。

pointer capture 让指针移出区域后仍然能收到后续事件;pointerup 和 pointercancel 都结束拖动。位置限制在区域内,不让矩形跑到看不见的地方。

实际操作检查三块区域都能拖动,另外两块同步;用 Tab 聚焦 DOM 矩形后按方向键,坐标也更新。这个例子没有给 SVG 和 Canvas 补完整键盘访问层,不能说三者在可访问性上已经等价。

表单、文字和普通组件优先利用 DOM 的现成能力;需要图形节点和矢量表达时,可以考虑 SVG;Canvas 适合自建绘制和场景系统,但命中、编辑和重绘也要自己处理。可以组合使用,不需要整站只选一种。基础 API 参考 Canvas 文档。