import type { TitleCue } from './titles.js'; export interface FrameClockEntry { frame: number; wallMs: number; } export interface TimelineRecording { /** Frame files are named `_%06d.png`, indexed from 0. */ name: string; gameId?: string; genre?: string; width: number; height: number; fps: number; frameCount: number; /** Frame indices whose PNG failed to save; may be absent or empty. */ missingFrames?: number[]; /** Ignored in v1. */ frameClock?: FrameClockEntry[]; /** * The window the DOM HUD laid out against during capture (the recorder * resizes only the canvas). Absent on older recordings — HUD overlays then * raster at the recording resolution, as they always did. */ window?: RecordingWindow; } export interface RecordingWindow { innerWidth: number; innerHeight: number; devicePixelRatio: number; } export interface TimelineEvent { frame: number; type: string; intensity?: number; position?: number[]; actor?: string; data?: Record; } export type SynthWave = 'sine' | 'square' | 'sawtooth' | 'triangle' | 'noise'; export interface SynthLayer { wave: SynthWave; /** Constant Hz or [start, end] linear ramp. Ignored for 'noise'. */ freq?: number | [number, number]; /** Constant gain or [start, end] linear ramp. Default 1. */ gain?: number | [number, number]; } export interface SynthRecipe { /** Duration in seconds. */ dur: number; layers: SynthLayer[]; } export interface AssetSound { frame: number; kind: 'asset'; assetId: string; url: string; volume?: number; } export interface SynthSound { frame: number; kind: 'synth'; name?: string; recipe: SynthRecipe; volume?: number; } export type TimelineSound = AssetSound | SynthSound; export type HudElementKind = 'counter' | 'progress' | 'icon-text' | 'timer' | 'custom'; /** * Recorded HUD state changes (the engine's `TimelineHudOp`). Frames use the * recording's source clock. Elements that existed before the recording started * are emitted as frame-0 `create` ops, so frame 0 is always a complete * snapshot, and array order within a frame is stacking order in an anchor. * * `update` carries already-rendered text, so the replay never applies a game's * `format` callback. Timers are the exception — they log start/pause/reset * only and the displayed value is derived (see hud-state.ts). */ export type HudOp = { frame: number; op: 'create'; id: string; elType: HudElementKind; params: Record; } | { frame: number; op: 'update'; id: string; text?: string; icon?: string; percent?: number; } | { frame: number; op: 'custom-html'; id: string; html: string; css?: string; } | { frame: number; op: 'timer'; id: string; action: 'start' | 'pause' | 'reset'; seconds?: number; } | { frame: number; op: 'show' | 'hide' | 'remove'; id: string; } | { frame: number; op: 'health'; current: number; max: number; } | { frame: number; op: 'health-visible'; visible: boolean; width?: number; } | { frame: number; op: 'toast'; message: string; variant?: string; durationMs?: number; anchor?: string; } | { frame: number; op: 'hud-visible'; visible: boolean; } | { frame: number; op: 'theme'; theme: unknown; } | { frame: number; op: 'stylesheet'; id: string; css: string; }; export interface Timeline { version: number; recording: TimelineRecording; events: TimelineEvent[]; sounds: TimelineSound[]; /** HUD state changes; absent/empty when the recording captured no HUD. */ hud: HudOp[]; } export interface EdlClip { /** Source frame index, inclusive. */ in: number; /** Source frame index, exclusive. */ out: number; /** Playback speed 0.25–2.0; default 1.0 (0.5 = slow-mo). */ speed?: number; /** Transition between the PREVIOUS clip and this one; first clip's is ignored. */ transition?: 'cut' | 'crossfade'; /** Crossfade length in seconds; default 0.25. */ transitionDuration?: number; label?: string; /** Draw the recorded HUD over this clip. Overrides the global --ui flag. */ ui?: boolean; } export interface Edl { version: number; /** Informational only. */ targetDuration?: number; clips: EdlClip[]; /** Burned-in title cards, timed in OUTPUT seconds. */ titles?: TitleCue[]; } export interface AnalysisWindow { startFrame: number; endFrame: number; score: number; /** Compact digest lines, e.g. "f3121 explosion i0.9". */ events: string[]; /** Path relative to the recording dir, e.g. "sheets/window_00.png". */ sheet: string; } export interface Analysis { version: number; fps: number; frameCount: number; windows: AnalysisWindow[]; } export declare function frameFileName(recordingName: string, frame: number): string; export declare function frameFilePath(recordingDir: string, recordingName: string, frame: number): string; export declare function parseRecipe(raw: unknown, ctx: string): SynthRecipe; /** * Parse the hud[] track. Skipped entries are reported (grouped by op name) so * a forward-compatible skip is never silent. */ export declare function parseHudOps(raw: unknown, source: string): HudOp[]; export declare function parseTimeline(raw: unknown, source: string): Timeline; export declare function loadTimeline(recordingDir: string): Timeline; export declare function parseEdl(raw: unknown, source: string): Edl; export declare function loadEdl(filePath: string): Edl;