Skip to content
Docs / Introduction
Reference

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.

addClip
engine.addClip(options: CreateClipOptions): Clip

Adds a new clip to a track. The clip is placed at startFrame and returns the created Clip object.

trackIdstringTarget track ID
typeClipType'video' | 'audio' | 'text' | 'image' | 'shape' | 'freehand'
startFramenumberPosition on the timeline (integer frames)
durationFramesnumberLength of the clip (integer frames)
srcstring?URL or blob ref for video/audio/image clips
textTextClipMetadata?Content + style; required when type is 'text'
moveClip
engine.moveClip(clipId: string, fromTrackId: string, toTrackId: string, startFrame: number): void

Moves a clip to a new start frame, optionally onto a different track. Pass the same id for fromTrackId and toTrackId to move within a track. No-ops if the move would overlap a neighbour or either track is locked.

removeClip
engine.removeClip(clipId: string, trackId: string): void

Removes a clip from a track.

updateClip
engine.updateClip(clipId: string, trackId: string, updates: Partial<Clip>): void

Updates any fields on a clip. Common use: transform, text content, opacity. During drag/trim gestures prefer previewClip() + commitInteraction() so the interaction is a single undo entry.

addTrack
engine.addTrack(kind: TrackKind, options?: Partial<CreateTrackOptions>): Track

Adds a new track. Any number of tracks of any kind is allowed. Video tracks composite in track order (the topmost lane draws on top) and a new video track is placed directly below the last existing one; other kinds append, above any bottom-pinned lane.

namestring?Display name; defaults to a kind-based name
heightnumber?Lane height in px (default 64)
ordernumber?Explicit render order; normally left to the engine
protectedboolean?When true, the user cannot remove the track
pinned'bottom'?Keeps the lane below every non-pinned track, whatever is added later
loadProject
engine.loadProject(project: Project, options?: { transport?: 'rewind' | 'keep'; history?: 'reset' | 'keep' }): void

Replaces the whole composition. Pass the result of readProjectDocument(json), which throws ProjectDocumentError for an unreadable or too-new document. By default undo history is cleared and the playhead rewinds; { transport: 'keep', history: 'keep' } is for the relinkProjectMedia repair pass over a composition that is already on screen. Emits 'change' then 'project:loaded'.

transport'rewind' | 'keep''rewind' (default) stops and returns to frame 0; 'keep' leaves the playhead alone
history'reset' | 'keep''reset' (default) clears undo/redo; 'keep' preserves the stacks and any open batch or drag
setClipSpeed
engine.setClipSpeed(clipId: string, trackId: string, speed: number): void

Sets a video clip's playback multiplier, clamped to 0.25–4. The clip's on-timeline length follows the new speed; growth is clamped to the gap before the next clip.

addTransition
engine.addTransition(options): Transition | null

Adds a transition between two adjacent clips on the same track. Returns the created Transition, or null if the clips aren't found on that track.

fromClipIdstringThe outgoing clip
toClipIdstringThe incoming clip
trackIdstringTrack both clips live on (required)
kindTransitionKind'fade' | 'slide' | 'wipe'
durationFramesnumberHow many frames the transition spans
easingTransitionEasing?'linear' | 'ease-in' | 'ease-out'
batch
engine.batch(fn: () => void, label?: string): void

Groups multiple mutations into a single undo entry. All mutations inside fn() are committed atomically.

undo / redo
engine.undo(): boolean | engine.redo(): boolean

Step backwards/forwards through the commit history. Returns true if a step was applied (use canUndo() / canRedo() to gate UI).

getProject
engine.getProject(): Project

Returns the current immutable Project snapshot.

setStage
engine.setStage(width: number, height: number): void

Sets the canvas output dimensions. Clips re-fit to the new stage on the next resolve — placement is normalized, so no per-clip migration is needed.

PlaybackEngine

Owns the RAF clock. Emits (frame, isPlaying) snapshots. React consumes it via usePlaybackStore.

typescript
// Direct usage (advanced — usually use usePlaybackStore instead)
import { PlaybackEngine } from '@elah/core'

const engine = new PlaybackEngine({ fps: 30 })

engine.play()
engine.pause()
engine.seek(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.

typescript
import { resolveTimeline, type Scene } from '@elah/core'

const scene: Scene = resolveTimeline(currentFrame, project)

// Scene shape:
interface Scene {
  frame: number
  fps: number
  stage: { width: number; height: number }
  videos: ActiveVideoClip[]
  audios: ActiveAudioClip[]
  texts: ActiveTextClip[]
  images: ActiveImageClip[]
  shapes: ActiveShapeClip[]
  freehand: ActiveFreehandClip[]
  transitions: ActiveTransition[]
}

// Fields shared by every active clip:
interface ActiveClipBase {
  id: string
  trackId: string
  name: string
  sourceFrame: number      // source-asset frame at the current playhead
  opacity: number          // 0..1, modified by transitions
  zIndex: number           // higher = closer to viewer (front)
  transform?: Transform    // undefined → renderer default (contain-fit)
}

// Each ActiveVideoClip adds:
interface ActiveVideoClip extends ActiveClipBase {
  type: 'video'
  src: string
  volume: number           // 0..1, after track mute
}

GpuRenderer

The shipped WebGL2 renderer. Accepts a Scene and draws sorted textured quads. Can be replaced with any Renderer-conforming implementation.

typescript
import { GpuRenderer, type Renderer } from '@elah/core'

// The Renderer interface
interface Renderer {
  mount(container: HTMLElement): void
  resize(cssWidth: number, cssHeight: number, dpr?: number): void
  render(scene: Scene): void
  prewarm?(scene: Scene): void   // optional decode look-ahead
  dispose(): void
}

// Direct usage (usually consumed through <Preview>)
// The constructor takes RendererOptions — the stage size comes from the
// Scene each tick, not the constructor. Pass a demuxerFactory to enable
// the real WebCodecs decode pipeline.
const renderer = new GpuRenderer({
  demuxerFactory: () => createMediabunnyBackend(mediabunny),
})

renderer.mount(containerEl)      // attach to the DOM once
renderer.resize(1920, 1080, window.devicePixelRatio)
renderer.render(scene)           // called each RAF tick
renderer.dispose()               // cleanup on unmount

Hooks

All hooks below live in @elah/react and are re-exported by @elah/editor — install @elah/react directly if you're building custom UI on @elah/core without the full editor SDK. The @elah/react page covers the context, the store hooks, the audio hooks and using the same stores without React in detail.

useTimelineEngine()→ TimelineEngine

Access the TimelineEngine from any child of EditorProvider. Use this for mutations.

usePlaybackEngine()→ PlaybackEngine

Access the PlaybackEngine directly. Usually prefer usePlaybackStore for state.

useTracksStore(selector)→ T

Zustand store for tracks, clips, totalFrames. Reactive to all engine mutations.

usePlaybackStore(selector)→ T

Zustand store for currentFrame, isPlaying, togglePlayPause, setCurrentFrame.

useSelectionStore(selector)→ T

Zustand store for selected clip IDs.

useTransitionsStore(selector)→ T

Zustand store for all transitions.

useTextStylePresetsStore(selector)→ T

Zustand store for reusable text looks: the built-in presets plus any the user saves.

useClipLoadStore(selector)→ T

Zustand store of which clips the preview cannot draw yet: byClipId[id] is 'loading' or 'error'. Renderer state only; never saved or undoable.

useMediaLibrary()→ UseMediaLibraryApi

Access the media library. Returns { assets, getAsset, removeAsset, updateAsset, importFiles, importUrl, importBlob }.

useAudioMixer(controller)→ AudioMixerApi

Click-free master/track gain controls for an AudioPlaybackController. Pass null while the controller isn’t initialized yet — methods become no-ops.

useMasterVolume(controller, engine)→ MasterVolumeApi

Master volume with both immediate audio-graph control and persistence to the project model.

useTrackLevels(controller)→ Map<string, TrackLevel>

Polls per-track RMS levels on every animation frame, for building level meters.

Types

The clip and track fields added in 0.6.0 (Clip.speed, Clip.crop, Clip.cornerRadius, Track.protected, Track.pinned) are explained on Clips & Tracks. Layered text motion is on Text Templates & Motion.

types.ts
// Time
type FrameCount = number // always integer

type ClipType = 'video' | 'audio' | 'text' | 'image' | 'shape' | 'freehand'
type TrackKind = 'video' | 'audio' | 'elements'

// Core data types
interface Project {
  id: string
  fps: number
  stage: { width: number; height: number }
  tracks: Track[]
  // Clips are stored keyed by trackId, NOT nested on Track.
  clips: Record<string, Clip[]>
  transitions: Transition[]
  version: number
  masterVolume?: number   // 0..2, linear
}

interface Track {
  id: string
  name: string
  kind: TrackKind
  order: number           // lower = closer to top of timeline
  height: number          // px
  locked: boolean
  disabled: boolean
  muted: boolean
  solo: boolean
  volume?: number         // 0..2, linear
  protected?: boolean     // the user cannot remove this track
  pinned?: 'bottom'       // addTrack keeps this lane below every non-pinned track
}

interface Clip {
  id: string
  trackId: string
  type: ClipType
  name: string
  startFrame: FrameCount        // position on the timeline
  durationFrames: FrameCount    // length on the timeline
  sourceStartFrame: FrameCount  // trim in-point into the source
  sourceDurationFrames: FrameCount
  src?: string
  assetId?: string
  transform?: Transform
  opacity?: number              // 0..1
  volume?: number               // 0..1
  speed?: number                // video only; 0.25..4, default 1
  cornerRadius?: number         // video/image; 0..0.5 of the shorter side
  crop?: { x: number; y: number; width: number; height: number } // 0..1 of the source
  locked?: boolean
  disabled?: boolean
  // Text clips (flat fields, not a nested object):
  content?: string
  fontSize?: number
  color?: string
  fontFamily?: string
  fontWeight?: 'normal' | 'bold'
  textAlign?: 'left' | 'center' | 'right'
  textAnimation?: TextAnimation
  shapeAnimation?: TextAnimation
  // Shape / freehand clips have their own shape*/stroke*/pathData fields.
}

interface Transform {
  x: number        // 0..1, normalized to stage width
  y: number        // 0..1, normalized to stage height
  scale: number    // 1 = native size
  scaleX?: number  // extra horizontal stretch on top of scale; omitted = 1
  scaleY?: number  // extra vertical stretch on top of scale; omitted = 1
  rotation: number // radians, positive = clockwise
  anchor: { x: number; y: number } // 0..1 within the clip box
}

// Entry/exit ramp for text (and shape) clips.
type TextAnimationKind = 'fade' | 'spin' | 'slide-up' | 'slide-down' | 'slide-left' | 'slide-right'

interface TextAnimation {
  in?: TextAnimationKind
  out?: TextAnimationKind
  durationFrames: number
  // Layered motion. When present it replaces in / out; used by text templates.
  inMotion?: MotionSpec
  outMotion?: MotionSpec
}

// One end of a layered animation: the state at the FAR end of the ramp.
// The near end is always the clip's resting state. Omitted channels stay at rest.
interface MotionSpec {
  opacity?: number       // 0..1
  offsetX?: number       // fraction of stage width
  offsetY?: number       // fraction of stage height
  scale?: number         // multiplier on the authored scale
  rotation?: number      // radians
  ease?: TextAnimationEasing        // geometry channels
  opacityEase?: TextAnimationEasing // default 'linear'
}

interface Transition {
  id: string
  kind: 'fade' | 'slide' | 'wipe'
  fromClipId: string
  toClipId: string
  trackId: string
  startFrame: FrameCount   // = toClip.startFrame - durationFrames / 2
  durationFrames: FrameCount
  direction?: 'left' | 'right' | 'up' | 'down'
  easing?: 'linear' | 'ease-in' | 'ease-out'
}

// Export — fps is read from project.fps; the export worker uses
// mediabunny directly, so there is no demuxerFactory here.
type ExportVideoCodec = 'avc' | 'vp9' | 'vp8'
type ExportAudioCodec = 'aac' | 'opus'

interface ExportOptions {
  videoCodec?: ExportVideoCodec   // default 'avc'
  audioCodec?: ExportAudioCodec
  videoBitrate?: number           // bits/s, default 8 Mbps
  audioBitrate?: number           // bits/s, default 128 kbps
  outputHeight?: number           // target SHORT edge in px (1080 = 1080p in either
                                  // orientation); other edge is rounded to even.
                                  // Default = the stage's own short edge
  onProgress?: (p: ExportProgress) => void
  signal?: AbortSignal
}

interface ExportProgress {
  frame: number
  totalFrames: number
}
On this page