跳到主要内容

精灵图时间轴与录像

动画不只是「rAF 里挪矩形」。产品侧常见三类:精灵表播放、按时间轴插值、把 canvas 编成视频。三者的时间基准不能混。

1. 精灵表

export interface SpriteSheet {
bitmap: ImageBitmap;
frameWidth: number;
frameHeight: number;
columns: number;
frameCount: number;
}

export function blitFrame(
ctx: CanvasRenderingContext2D,
sheet: SpriteSheet,
frame: number,
dx: number,
dy: number,
): void {
const i = ((frame % sheet.frameCount) + sheet.frameCount) % sheet.frameCount;
const sx = (i % sheet.columns) * sheet.frameWidth;
const sy = Math.floor(i / sheet.columns) * sheet.frameHeight;
ctx.drawImage(
sheet.bitmap,
sx,
sy,
sheet.frameWidth,
sheet.frameHeight,
dx,
dy,
sheet.frameWidth,
sheet.frameHeight,
);
}

export function frameAt(nowMs: number, originMs: number, fps: number, count: number): number {
const elapsed = Math.max(0, nowMs - originMs);
return Math.floor((elapsed / 1000) * fps) % count;
}

originMs 用 rAF timestamp,暂停时记下 pausedAt,恢复时把 origin 前移,避免追帧把动画瞬移。不要用 ++frame 每 rAF 加一:掉帧会变慢,高刷会变快。

imageSmoothingEnabled = false 才是像素风。高 DPI 上仍要 backing 足够,否则最近邻也会糊。

2. 时间轴插值

场景属性用 t 采样,绘制保持纯函数:

export function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}

export function easeInOutCubic(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

多轨道(位移动画 + 透明度)共用同一 now,不要各轨道自己 performance.now()。seek 时清粒子之类的不可逆状态,或把它们也变成 t 的函数。

3. 录像:captureStream vs 离线逐帧

路径适用
canvas.captureStream(fps) + MediaRecorder实时演示、会议共享画板fps 只是目标;标签页 hidden 会降帧;时间戳跟 vsync 走
离线:固定 t += 1/fps 画完 convertToBlob 再交给 Worker 编码导出 mp4/webm 要稳定帧间隔自己管时间,不要 rAF
captureStream(0) 手动 track.requestFrame()只在 scene 变时出帧支持度要探测
export function startLiveRecord(
canvas: HTMLCanvasElement,
fps: number,
): { recorder: MediaRecorder; chunks: Blob[] } {
const stream = canvas.captureStream(fps);
const mime = MediaRecorder.isTypeSupported("video/webm;codecs=vp9")
? "video/webm;codecs=vp9"
: "video/webm";
const chunks: Blob[] = [];
const recorder = new MediaRecorder(stream, { mimeType: mime });
recorder.ondataavailable = (event) => {
if (event.data.size > 0) chunks.push(event.data);
};
recorder.start(200);
return { recorder, chunks };
}

tainted canvas 不能 capture 进需要读像素的管道。跨源视频当源时先 CORS。音频要另加 AudioContext 混轨,canvas 流只有画面。

导出高质量:不要用 live record。用离线循环,每帧 draw(scene, n / fps)OffscreenCanvas.convertToBlob 或 WebCodecs VideoEncoder。rAF 录像的帧间隔不均匀,后期会对不上音。

4. 失败形态

症状原因
高刷动画过快按帧计数而不是按时间
像素风发糊smoothing 开着或 backing 不足
录像掉帧/加速hidden 时仍在 record live stream
导出 WebM 失败mime 未探测,Safari 可能不支持 vp9
黑屏录像表面 tainted,或 capture 在 getContext 之前

权威资料

核对日期:2026-08-26