The InteractionTimeline class records cursor position, scale, and HUD state for each frame of a recording. It enables separate composition of overlays onto clean video.
Constructor
new InteractionTimeline(
width?: number,
height?: number,
options?: {
zoom?: number;
fps?: number;
initialCursor?: { x: number; y: number };
cursorSvg?: string;
cursorSize?: number;
cursorHotspot?: 'top-left' | 'center';
hud?: Partial<HudConfig>;
loadedFrames?: FrameData[];
loadedEvents?: SoundEvent[];
}
)
Viewport height in pixels
Display zoom factor (scales cursor and HUD)
Cursor size in pixels (before zoom)
options.cursorHotspot
'top-left' | 'center'
default:"'top-left'"
Cursor hotspot alignment
HUD theme customizationinterface HudConfig {
background: string; // CSS color
color: string; // CSS color
fontSize: number; // In pixels
fontFamily: string; // CSS font-family
borderRadius: number; // In pixels
position: 'top' | 'bottom';
}
Pre-existing frame data for loading saved timelines
Pre-existing sound events for loading saved timelines
Methods
setCursorPath()
Set a series of cursor positions to animate across multiple frames.
timeline.setCursorPath(positions)
Array of { x: number, y: number } positions
Each call to tick() advances through the path until complete.
setCursorScale()
Set the cursor scale factor (for click down/up animation).
timeline.setCursorScale(scale)
Scale multiplier (e.g., 0.75 for pressed state, 1.0 for normal)
showHud()
Display keyboard shortcut labels.
Array of label strings (e.g., ['⌘', 'K'] for Cmd+K)
hideHud()
Hide the keyboard shortcut HUD.
addEvent()
Record an interaction event with timestamp.
Event type for sound effects
Timestamp is calculated from current frame count and FPS.
tick()
Advance the timeline by one frame.
This method:
- Advances cursor along the current path if set
- Records current cursor and HUD state
- Resolves any pending
waitForNextTick() promises
- Increments frame count
tickDuplicate()
Duplicate the current frame state without advancing the cursor path.
Useful when dropped frames need to be compensated for.
waitForNextTick()
Wait for the next frame tick.
await timeline.waitForNextTick()
Used by typeText() to synchronize typing with frame capture.
getEvents()
Get all recorded sound events.
const events = timeline.getEvents()
Array of { type: 'click' | 'key', timeMs: number }
getFrameCount()
Get the total number of frames recorded.
const count = timeline.getFrameCount()
toJSON()
Serialize timeline to JSON.
const data = timeline.toJSON()
Complete timeline data structureinterface TimelineData {
fps: number;
width: number;
height: number;
zoom: number;
theme: {
cursorSvg: string;
cursorSize: number;
cursorHotspot: 'top-left' | 'center';
hud: {
background: string;
color: string;
fontSize: number;
fontFamily: string;
borderRadius: number;
position: 'top' | 'bottom';
};
};
frames: FrameData[];
events: SoundEvent[];
}
save()
Save timeline to a JSON file.
File path to write JSON data
timeline.save('./timeline.json');
load() (static)
Load a timeline from JSON data.
const timeline = InteractionTimeline.load(json)
Reconstructed timeline instance
import { readFileSync } from 'fs';
import { InteractionTimeline } from '@webreel/core';
const data = JSON.parse(readFileSync('./timeline.json', 'utf-8'));
const timeline = InteractionTimeline.load(data);
Usage Example
Recording with Timeline
import {
Recorder,
RecordingContext,
InteractionTimeline,
clickAt
} from '@webreel/core';
// Create timeline
const timeline = new InteractionTimeline(1920, 1080, {
fps: 60,
cursorSvg: customCursorSvg,
hud: {
background: 'rgba(0,0,0,0.8)',
fontSize: 56
}
});
// Configure context
const ctx = new RecordingContext();
ctx.setMode('record');
ctx.setTimeline(timeline);
// Record clean video
const recorder = new Recorder(1920, 1080);
recorder.setTimeline(timeline);
await recorder.start(client, './clean.mp4', ctx);
// Perform interactions
await clickAt(ctx, client, 500, 300);
// Stop recording
await recorder.stop();
// Save timeline for compositing
timeline.save('./timeline.json');
Inspecting Timeline Data
const data = timeline.toJSON();
console.log(`Total frames: ${data.frames.length}`);
console.log(`Duration: ${data.frames.length / data.fps}s`);
console.log(`Events: ${data.events.length}`);
// Inspect specific frame
const frame = data.frames[0];
console.log('Cursor position:', frame.cursor.x, frame.cursor.y);
console.log('Cursor scale:', frame.cursor.scale);
if (frame.hud) {
console.log('HUD labels:', frame.hud.labels);
}
Frame Data Structure
Each frame contains:
interface FrameData {
cursor: {
x: number; // Horizontal position
y: number; // Vertical position
scale: number; // Scale factor (0.75 when pressed, 1.0 otherwise)
};
hud: {
labels: string[]; // Keyboard shortcut labels
} | null;
}
Custom Cursor Design
Customize the cursor appearance:
const customCursor = `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="12" fill="#ff0000" stroke="#000" stroke-width="2"/>
</svg>
`;
const timeline = new InteractionTimeline(1920, 1080, {
cursorSvg: customCursor,
cursorSize: 32,
cursorHotspot: 'center'
});
HUD Customization
Customize the keyboard shortcut overlay:
const timeline = new InteractionTimeline(1920, 1080, {
hud: {
background: 'rgba(30, 30, 30, 0.95)',
color: 'rgba(255, 255, 255, 0.9)',
fontSize: 64,
fontFamily: '"SF Pro Display", system-ui, sans-serif',
borderRadius: 24,
position: 'top'
}
});