跳到主要内容

路径文字图片与合成

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 依赖当前 fontdirectionletterSpacing。换字体必须先设 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);
}

ImageBitmapclose() 释 GPU/CPU 资源。列表里滚动卸载缩略图时要关。
跨域图不带 CORS 会把 canvas taint,之后 toDataURL / getImageDataSecurityError。见 污染 CORS 与像素导出

4. 合成:globalCompositeOperation 不是图层系统

source-over(默认)、destination-inxorlighter 都是 下一次绘制 相对 已经在 backing store 里的像素 的代数。

它没有 z-index。要实现「下层擦除上层」,你需要:

  • 分层 canvas / ImageBitmap 缓存;或
  • 按从底到顶重绘;或
  • 离屏先合成再一次性 drawImage

shadowBlur 按绘制命令做高斯,大面积 + 高 blur 是性能杀手,不要当设计规范里的「全局投影」每帧对整场景开。

5. 失败形态

症状原因
evenodd 期望的洞没了用了 nonzero,或 path 没 close
文字命中偏上忽略了 baseline 和 actualBoundingBox
图片偶发空白没等 decode / createImageBitmap
导出 PNG 失败被跨域图污染

权威资料

核对日期:2026-08-26