API Reference
Complete reference for TimelineEngine, PlaybackEngine, resolveTimeline, GpuRenderer, React hooks, and TypeScript types.
TimelineEngine
The single mutation funnel. All edits go through TimelineEngine. Backed by Immer for structural sharing; every commit produces a new Project snapshot.
engine.addClip(options: CreateClipOptions): ClipAdds a new clip to a track. The clip is placed at startFrame and returns the created Clip object.
trackIdstringTarget track IDtypeClipType'video' | 'audio' | 'text' | 'image'startFramenumberPosition on the timeline (integer frames)durationFramesnumberLength of the clip (integer frames)srcstring?URL or blob ref for video/audio/image clipstextTextClipData?Text content and styling for text clipsengine.moveClip(clipId: string, options: { startFrame: number; trackId?: string }): voidMoves a clip to a new position, optionally changing its track.
engine.removeClip(clipId: string, trackId: string): voidRemoves a clip from a track.
engine.updateClip(clipId: string, partial: Partial<Clip>): voidUpdates any fields on a clip. Common use: transform, text content, opacity.
engine.addTrack(kind: TrackKind, name?: string): TrackAdds a new track to the project.
engine.addTransition(options: TransitionOptions): voidAdds a transition between two adjacent clips on the same track.
fromClipIdstringThe outgoing cliptoClipIdstringThe incoming clipkindTransitionKind'fade' | 'slide' | 'wipe'durationFramesnumberHow many frames the transition spanseasingTransitionEasing?'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'engine.batch(fn: () => void, label?: string): voidGroups multiple mutations into a single undo entry. All mutations inside fn() are committed atomically.
engine.undo(): void | engine.redo(): voidStep backwards/forwards through the commit history.
engine.getProject(): ProjectReturns the current immutable Project snapshot.
engine.setStage(stage: { width: number; height: number }): voidSets the canvas output dimensions. All clips are letterboxed to this aspect ratio.
PlaybackEngine
Owns the RAF clock. Emits (frame, isPlaying) snapshots. React consumes it via usePlaybackStore.
// Direct usage (advanced — usually use usePlaybackStore instead)
import { PlaybackEngine } from '@elah/core'
const engine = new PlaybackEngine({ fps: 30 })
engine.play()
engine.pause()
engine.seekTo(frame)
// Subscribe to playback ticks
const unsub = engine.subscribe((snapshot) => {
console.log(snapshot.frame, snapshot.isPlaying)
})resolveTimeline()
The pure, deterministic resolver. Consumes a Project and frame index; produces a Scene. No side effects, no imports, safe to call in tests, workers, and export pipelines.
import { resolveTimeline, type Scene } from '@elah/core'
const scene: Scene = resolveTimeline(currentFrame, project)
// Scene shape:
interface Scene {
frame: number
videos: ActiveVideoClip[]
audios: ActiveAudioClip[]
texts: ActiveTextClip[]
images: ActiveImageClip[]
transitions: ActiveTransition[]
}
// Each ActiveVideoClip includes:
interface ActiveVideoClip extends ActiveClipBase {
src: string
opacity: number // 0..1, modified by transitions
drawRect: DrawRect // computed placement rectangle
transform: Transform
objectFit: 'contain' | 'cover' | 'fill'
}GpuRenderer
The shipped WebGL2 renderer. Accepts a Scene and draws sorted textured quads. Can be replaced with any Renderer-conforming implementation.
import { GpuRenderer, type Renderer } from '@elah/core'
// The Renderer interface
interface Renderer {
render(scene: Scene): void
destroy(): void
}
// Direct usage (usually consumed through <Preview>)
const canvas = document.createElement('canvas')
const renderer = new GpuRenderer(canvas, {
width: 1920,
height: 1080,
})
renderer.render(scene) // called each RAF tick
renderer.destroy() // cleanup on unmountHooks
useTimelineEngine()→ TimelineEngineAccess the TimelineEngine from any child of EditorProvider. Use this for mutations.
usePlaybackEngine()→ PlaybackEngineAccess the PlaybackEngine directly. Usually prefer usePlaybackStore for state.
useTracksStore(selector)→ TZustand store for tracks, clips, totalFrames. Reactive to all engine mutations.
usePlaybackStore(selector)→ TZustand store for currentFrame, isPlaying, togglePlayPause, setCurrentFrame.
useSelectionStore(selector)→ TZustand store for selected clip IDs.
useTransitionsStore(selector)→ TZustand store for all transitions.
useMediaLibrary()→ MediaLibraryStoreAccess the media library. Returns { assets, addAsset, removeAsset }.
Types
// Time
type FrameCount = number // always integer
// Core data types
interface Project {
tracks: Track[]
transitions: Transition[]
stage: { width: number; height: number }
fps: number
}
interface Track {
id: string
kind: 'video' | 'audio' | 'text'
name: string
muted: boolean
solo: boolean
zIndex: number
clips: Clip[]
}
interface Clip {
id: string
trackId: string
type: 'video' | 'audio' | 'text' | 'image'
name: string
src?: string
startFrame: FrameCount
durationFrames: FrameCount
trimInFrames: FrameCount
trimOutFrames: FrameCount
transform: Transform
text?: TextClipData
opacity: number
zIndex: number
}
interface Transform {
x: number // -1..1 offset (normalized)
y: number
scale: number // 1.0 = contain-fit
rotation: number // degrees
}
interface TextClipData {
content: string
fontSize: number
color: string
fontFamily?: string
fontWeight?: 'normal' | 'bold'
fontStyle?: 'normal' | 'italic'
textAlign?: 'left' | 'center' | 'right'
animation?: TextAnimation
}
interface TextAnimation {
kind: 'fade-in' | 'fade-out' | 'slide-in' | 'typewriter'
durationFrames: number
}
interface Transition {
id: string
fromClipId: string
toClipId: string
kind: 'fade' | 'slide' | 'wipe'
durationFrames: FrameCount
easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
direction?: 'left' | 'right' | 'up' | 'down'
}
// Utility types
interface ExportOptions {
fps: number
demuxerFactory: DemuxerFactory
videoCodec?: 'avc' | 'vp9'
audioCodec?: 'aac' | 'opus'
videoBitrate?: number
audioBitrate?: number
onProgress?: (p: ExportProgress) => void
}
interface ExportProgress {
frame: number
totalFrames: number
percent: number
phase: 'encoding' | 'muxing' | 'done'
}