Skip to main content

Canvas 实战补充

补充几个高频但前面专题未覆盖的实战点。

渲染循环与 deltaTime

动画应基于 requestAnimationFrame,并用时间差(deltaTime)驱动运动,保证不同刷新率/掉帧时速度一致。

let last = performance.now();
function loop(now) {
const dt = (now - last) / 1000; // 秒
last = now;

update(dt); // 位移 = 速度 × dt,与帧率解耦
render();

requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

面试题:为什么用 rAF 而不是 setInterval 做动画?

  • rAF 与屏幕刷新同步(通常 60Hz),不会出现丢帧/撕裂;
  • 页面切到后台时 rAF 自动暂停,省电省 CPU,而 setInterval 仍在跑;
  • 浏览器会合并 rAF 回调到一帧,避免无意义的中间帧。

save / restore 状态栈

save() 把当前绘图状态(变换矩阵、fillStylelineWidthclip 等)压入栈,restore() 弹出恢复。做局部变换时务必成对使用,避免状态污染后续绘制。

ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.fillRect(0, 0, 50, 50); // 在变换后的坐标系绘制
ctx.restore(); // 还原,后续绘制不受影响

变换后的坐标拾取(逆矩阵)

当画布经过 translate/scale/rotate(如画布缩放平移),鼠标事件拿到的是屏幕坐标,需要用逆变换换算回绘图坐标才能正确命中图形。

// 用 getTransform 拿到当前变换矩阵,求逆后映射鼠标点
const m = ctx.getTransform().invertSelf();
const x = m.a * mouseX + m.c * mouseY + m.e;
const y = m.b * mouseX + m.d * mouseY + m.f;
// (x, y) 即图形坐标系下的命中点,再做 isPointInPath / 几何判定

拾取算法(isPointInPath / 几何计算 / 颜色拾取)见 进阶交互

OffscreenCanvas + Web Worker

把渲染从主线程剥离,避免大量绘制阻塞 UI。

// 主线程:把 canvas 控制权转交给 Worker
const offscreen = document.querySelector("canvas").transferControlToOffscreen();
const worker = new Worker("render.worker.js");
worker.postMessage({ canvas: offscreen }, [offscreen]); // 注意 transfer

// render.worker.js
self.onmessage = (e) => {
const ctx = e.data.canvas.getContext("2d");
function loop() {
// 在 worker 里绘制,完全不占主线程
ctx.clearRect(0, 0, e.data.canvas.width, e.data.canvas.height);
// ...draw
requestAnimationFrame(loop);
}
loop();
};

图片解码:ImageBitmap

createImageBitmap() 可在 Worker 中异步解码图片,得到可直接 drawImage 的位图,避免主线程解码大图卡顿。

const bitmap = await createImageBitmap(blob);
ctx.drawImage(bitmap, 0, 0);

导出:toDataURL vs toBlob

API返回适用
canvas.toDataURL(type, quality)base64 字符串(同步)小图、直接赋给 img.src
canvas.toBlob(cb, type, quality)Blob(异步,更省内存)大图、上传、生成下载链接
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "canvas.png";
a.click();
URL.revokeObjectURL(url);
}, "image/png");

跨域图片污染画布后调用 toDataURL/toBlob/getImageData 会抛 SecurityError,解决方案见 canvas 概览 的「canvas 污染」。

imageSmoothing

绘制像素图/做像素风时关闭平滑,避免缩放发虚:ctx.imageSmoothingEnabled = false;

圆角矩形

注意:每段 arcTo 之间不能用 moveTo,否则会断开路径、无法形成闭合图形。正确做法是从某个点出发,用连续的 arcTo(内部已隐含 lineTo 直边)一路连下来:

function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r); // 上边 + 右上角
ctx.arcTo(x + w, y + h, x, y + h, r); // 右边 + 右下角
ctx.arcTo(x, y + h, x, y, r); // 下边 + 左下角
ctx.arcTo(x, y, x + w, y, r); // 左边 + 左上角
ctx.closePath();
// ctx.fill() 或 ctx.stroke()
}

现代浏览器已原生支持 ctx.roundRect(x, y, w, h, r),可直接使用。