精灵图时间轴与录像
动画不只是「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