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'
startFramenumberPosition on the timeline (integer frames)
durationFramesnumberLength of the clip (integer frames)
srcstring?URL or blob ref for video/audio/image clips
textTextClipData?Text content and styling for text clips
moveClip
engine.moveClip(clipId: string, options: { startFrame: number; trackId?: string }): void

Moves a clip to a new position, optionally changing its track.

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

Removes a clip from a track.

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

Updates any fields on a clip. Common use: transform, text content, opacity.

addTrack
engine.addTrack(kind: TrackKind, name?: string): Track

Adds a new track to the project.

addTransition
engine.addTransition(options: TransitionOptions): void

Adds a transition between two adjacent clips on the same track.

fromClipIdstringThe outgoing clip
toClipIdstringThe incoming clip
kindTransitionKind'fade' | 'slide' | 'wipe'
durationFramesnumberHow many frames the transition spans
easingTransitionEasing?'linear' | 'ease-in' | 'ease-out' | 'ease-in-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(): void | engine.redo(): void

Step backwards/forwards through the commit history.

getProject
engine.getProject(): Project

Returns the current immutable Project snapshot.

setStage
engine.setStage(stage: { width: number; height: number }): void

Sets 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.

typescript
// 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.

typescript
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.

typescript
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 unmount

Hooks

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.

useMediaLibrary()MediaLibraryStore

Access the media library. Returns { assets, addAsset, removeAsset }.

Types

types.ts
// 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'
}