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() 把当前绘图状态(变换矩阵、fillStyle、lineWidth、clip 等)压入栈,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),可直接使用。