一键导入
frontend-scene-timeline-playback
Use when implementing or modifying timeline playback, animation loops, or audio synchronization in the Scene Editor
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when implementing or modifying timeline playback, animation loops, or audio synchronization in the Scene Editor
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | frontend-scene-timeline-playback |
| description | Use when implementing or modifying timeline playback, animation loops, or audio synchronization in the Scene Editor |
Guidelines for implementing smooth, performant playback in the Scene Editor timeline. Covers RAF architecture, audio sync, and common pitfalls.
NEVER create multiple independent requestAnimationFrame loops for the same feature.
The Scene Editor uses a single RAF loop in PageScene_Timeline.tsx that:
performance.now() deltatimelineTimeRef.current (the source of truth)// ❌ BAD: Multiple independent RAF loops
// Each AudioLane running its own loop
useEffect(() => {
const tick = () => {
syncAudio(timelineTimeRef.current);
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
}, []);
// Problems:
// 1. Loops fire at different times within the same frame
// 2. Unsynchronized reads from shared state (timelineTimeRef)
// 3. Accumulated CPU overhead (N tracks = N+1 loops)
// 4. Can cause jitter and timing conflicts
// ✅ GOOD: Single RAF loop with callbacks/events
// Main loop notifies subsystems
const tick = (timestamp: number) => {
const newTime = calculateNewTime(timestamp);
timelineTimeRef.current = newTime;
// Option A: Direct call to audio manager
audioManager.syncToTime(newTime);
// Option B: Event-based notification
eventBus.emit("timelineTime", newTime);
updatePlayheadDOM(newTime);
rafRef.current = requestAnimationFrame(tick);
};
The main playback loop is in:
PageScene_Timeline.tsx:22-138 - usePageScene_Timeline_Playback hookKey refs used:
timelineTimeRef - Current playback time (source of truth during playback)playheadRef - DOM element for direct manipulationpxPerSecRef, scrollOffsetPxRef - Cached zoom/scroll values<audio> element uses browser's audio clock (independent from performance.now())audio.currentTime is a blocking seek operation that causes frame drops// ❌ BAD: Checking drift every frame and seeking
const tick = () => {
const drift = Math.abs(audio.currentTime - expectedOffset);
if (drift > 0.05) {
// 50ms threshold
audio.currentTime = expectedOffset; // CAUSES JITTER!
}
rafRef.current = requestAnimationFrame(tick);
};
// ❌ BAD: Multiple RAF loops for audio sync
// Each track has its own loop
useEffect(() => {
const tick = () => {
clips.forEach((clip) => checkAndSync(clip));
rafRef.current = requestAnimationFrame(tick);
};
// ...
}, []);
Option A: Fire-and-Forget (Simple)
// Start audio at correct offset, let it play naturally
// Accept minor drift as acceptable tradeoff
if (!clip.isPlaying && isInClip) {
clip.audio.currentTime = audioOffset;
clip.audio.play();
clip.isPlaying = true;
}
// NO drift correction during playback
Option B: Infrequent Drift Correction
// Check drift less frequently (every 30 frames ≈ 0.5s)
// Use larger threshold (200-300ms)
frameCountRef.current++;
if (frameCountRef.current % 30 === 0) {
const drift = Math.abs(audio.currentTime - expectedOffset);
if (drift > 0.2) {
// 200ms threshold
audio.currentTime = expectedOffset;
}
}
Option C: Web Audio API (Professional-grade) - IMPLEMENTED
The Scene Editor uses Web Audio API for audio playback:
// WebAudioManager singleton (webAudioManager.ts)
// - AudioContext for precise timing
// - AudioBuffer cache per file
// - GainNode per clip for instant volume changes
// - AudioBufferSourceNode per playback (fire-and-forget)
// Main RAF loop calls syncPlayback with current clip data
webAudioManager.syncPlayback(audioClipsDataRef.current, newTime, true);
// Key files:
// - webAudioManager.ts - Singleton service
// - useAudioPlayback_PageScene_Timeline.ts - Centralized hook
// - PageScene_Timeline.tsx - Integration point (line 117)
When adding new features that need time sync:
timelineTimeRef.current - Don't create your own time trackingPlayhead jitter during audio playback - RESOLVED (2025-12-18)
Root cause was multiple RAF loops (per-track) + HTML5 audio drift correction. Fixed by migrating to Web Audio API with single RAF loop architecture.
See docs/spark/frontend/my-vite-app/audio-drag-drop-spec.md Section 15 for historical context.
Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code