拖动一个矩形,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 文档。