路径文字图片与合成
1. 路径:用 Path2D,不要用隐式当前路径
Path2D 可复用、可组合、可交给 fill / stroke / clip / isPointInPath。当前默认路径留作遗留 API。
export function roundedRect(
x: number,
y: number,
w: number,
h: number,
r: number,
): Path2D {
const path = new Path2D();
const radius = Math.max(0, Math.min(r, w / 2, h / 2));
path.moveTo(x + radius, y);
path.arcTo(x + w, y, x + w, y + h, radius);
path.arcTo(x + w, y + h, x, y + h, radius);
path.arcTo(x, y + h, x, y, radius);
path.arcTo(x, y, x + w, y, radius);
path.closePath();
return path;
}
Path2D 也有 addPath(other, transform?: DOMMatrix2DInit)。做图标库时,把单位 path 存一份,实例化时 addPath(unit, node.local),避免每个节点复制贝塞尔。
fill(path, "evenodd") 做环、挖洞。自交多边形默认 nonzero 会「填实」,不是 bug。
2. 文字:度量走 TextMetrics,不要猜
export interface TextLayout {
x: number;
y: number;
width: number;
height: number;
baseline: number;
}
export function layoutText(
ctx: CanvasRenderingContext2D,
text: string,
originX: number,
originY: number,
): TextLayout {
const m = ctx.measureText(text);
const ascent = m.actualBoundingBoxAscent;
const descent = m.actualBoundingBoxDescent;
return {
x: originX - m.actualBoundingBoxLeft,
y: originY - ascent,
width: m.actualBoundingBoxLeft + m.actualBoundingBoxRight,
height: ascent + descent,
baseline: originY,
};
}
注意:
measureText依赖当前font、direction、letterSpacing。换字体必须先设ctx.font。textAlign/textBaseline会移动字形相对(x,y)的锚点,命中盒要用actualBoundingBox*,不要只用width。- Canvas 文字 不可选、读屏几乎不可用。需要无障碍就叠一层 DOM,或
drawFocusIfNeeded只解决焦点环,不解决语义树。 maxWidth是压缩字形,不是自动换行。多行要自己按码点/语言拆。
3. 图片:解码和绘制拆开
drawImage(HTMLImageElement) 在 decode() 完成前可能画空白。生产路径:
export async function loadBitmap(url: string): Promise<ImageBitmap> {
const response = await fetch(url, { mode: "cors" });
if (!response.ok) {
throw new Error(`bitmap fetch failed: ${response.status}`);
}
const blob = await response.blob();
return createImageBitmap(blob);
}
export function drawBitmap(
ctx: CanvasRenderingContext2D,
bitmap: ImageBitmap,
dx: number,
dy: number,
dw: number,
dh: number,
): void {
ctx.imageSmoothingEnabled = dw < bitmap.width || dh < bitmap.height;
ctx.imageSmoothingQuality = "high";
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, dx, dy, dw, dh);
}
ImageBitmap 可 close() 释 GPU/CPU 资源。列表里滚动卸载缩略图时要关。
跨域图不带 CORS 会把 canvas taint,之后 toDataURL / getImageData 抛 SecurityError。见 污染 CORS 与像素导出。
4. 合成:globalCompositeOperation 不是图层系统
source-over(默认)、destination-in、xor、lighter 都是 下一次绘制 相对 已经在 backing store 里的像素 的代数。
它没有 z-index。要实现「下层擦除上层」,你需要:
- 分层 canvas /
ImageBitmap缓存;或 - 按从底到顶重绘;或
- 离屏先合成再一次性
drawImage。
shadowBlur 按绘制命令做高斯,大面积 + 高 blur 是性能杀手,不要当设计规范里的「全局投影」每帧对整场景开。
5. 失败形态
| 症状 | 原因 |
|---|---|
| evenodd 期望的洞没了 | 用了 nonzero,或 path 没 close |
| 文字命中偏上 | 忽略了 baseline 和 actualBoundingBox |
| 图片偶发空白 | 没等 decode / createImageBitmap |
| 导出 PNG 失败 | 被跨域图污染 |
权威资料
核对日期:2026-08-26