Canvas Timeline is built as a layered, modular framework for timeline-based video, audio, and animation editors. Before diving into the packages or code, it helps to understand how the core engine, the rendering pipeline, the React bindings, and your application’s metadata fit together.
System Architecture
The following map illustrates how state, mutations, rendering, and application metadata interact across the Canvas Timeline ecosystem:
Your Application
UI & Rendering Layers
Core Engine
Subsystem Guides
To understand specific areas of the model and integration layers, refer to these detailed guides:
- Rational time — Learn about fraction-based time representation and arithmetic helpers.
- Tracks and clips — Understand tracks and clips configuration, move commands, and dragging hooks.
- Keyframes — Animate clip properties with engine-level keyframes and headless interaction hooks.
- Events and lifecycle — Retrieve active playhead layers, subscribe to events, and use metadata.
- React editor hooks — Bind timeline state using domain hooks and accessibility wrappers.
Timeline state
Canvas Timeline centers on a serializable TimelineState object. This state acts as the single source of truth and the contract between the engine, React bindings, canvas renderer, and any persistence layer in your application.
At the top level, TimelineState contains:
| Field | Type | What it represents |
|---|---|---|
tracks |
Track[] |
Ordered lanes and their clips. |
playheadTime |
RationalTime |
The current edit or playback cursor. |
zoomScale |
number |
Horizontal scale in pixels per second. |
scrollLeft |
number |
Horizontal viewport offset in pixels. |
markers |
TimelineMarker[] |
Optional annotations pinned to timeline time. |
inPoint / outPoint |
RationalTime | undefined |
Optional range boundaries for loop regions and edit selection. |
duration |
RationalTime | undefined |
Optional fixed sequence duration. |
The engine owns the live structural state and emits events when edits, playback, or viewport changes occur.
import { TimelineEngine, fromSeconds } from '@techsquidtv/canvas-timeline';
const engine = new TimelineEngine({ duration: fromSeconds(30), tracks: [ { id: 'track-visual-1', kind: 'visual', name: 'Visual 1', selected: false, locked: false, muted: false, visible: true, clips: [], targeted: true, }, ],});Decoupling Application Metadata
This separation provides significant performance benefits:
- Lightweight History: Canvas Timeline manages a full undo/redo stack. Keeping the state model small allows snapshots to be cloned and serialized in milliseconds.
- Normalized Media Data: If the same source asset is used 10 times in a timeline, all 10 clips share a single
sourceId. Your app needs to load and cache the asset’s duration, dimensions, waveform, thumbnails, or transcript only once.
Use the table below to guide where metadata should reside:
| Key Type | What to Store | Examples |
|---|---|---|
sourceId |
Shared assets and media file attributes | Video URLs, durations, dimensions, waveform caches, transcripts. |
clip.id |
Clip-specific editorial overrides and presets | Volume levels, visual crops, color grading presets, text captions, comments. |
track.id |
App-specific lane metadata | Audio routing, record arm, owners, language, meters, automation mode. |
// Example: Resolving assets from a separate storeinterface AssetMeta { url: string; thumbnailUrl?: string;}
const assetMetadata = new Map<string, AssetMeta>([ ['asset-interview-1', { url: '/media/interview.mp4', thumbnailUrl: '/thumbs/interview.jpg' }],]);
// Inside your rendering loop or React component:const asset = assetMetadata.get(clip.sourceId);Track-level state belongs in Track only when Canvas Timeline itself needs to
understand it for rendering, media lookup, editing, history, or shared hooks. Use
an app-owned store for product-specific header controls:
interface AudioTrackMeta { recordArmed: boolean; routeId: string; meterSourceId: string;}
const audioTrackMetaById: Record<string, AudioTrackMeta> = { 'audio-1': { recordArmed: false, routeId: 'mix-bus', meterSourceId: 'meter-audio-1', },};Rendering layers
Canvas Timeline splits rendering responsibilities to optimize performance:
- Canvas Layer: Renders dense timeline graphics (ruler, time ticks, tracks, clips, In/Out range fill, snap points, etc.). Rendering hundreds of clips in DOM nodes triggers browser reflows and input lag; drawing to a 2D Canvas keeps interactions at 60fps.
- React Layer: Coordinates layout structure, pointer events, click detection, scrollbar elements, and delegated editing grabbers. React excels at context injection, focus, and state bindings.
CSS styles interaction layers and chrome; renderer theme styles canvas-painted timeline visuals. This keeps shadcn-style token control available without moving clips, tracks, rulers, or markers out of the canvas rendering path.
| Surface | Controlled by | Examples |
|---|---|---|
| Canvas visuals | Renderer theme | Clip fills, clip labels, track lanes, ruler ticks, markers, snap lines. |
| Interaction layers | React and CSS | Scrollbars, the single active clip affordance, playhead grabbers, and In/Out grabbers. |
| Product chrome | Your app CSS | Toolbars, inspectors, panels, menus, dialogs, and keyboard-focus styling. |
| Project color data | Timeline state | Per-clip color metadata that overrides renderer clip background colors. |
| Layer | Responsibility | Output/API |
|---|---|---|
| Core engine | State transitions, editing commands, playback loops, history, snapping math. | TimelineEngine |
| Renderer | Double-buffered Canvas drawing of ticks, backgrounds, waveforms, ruler. | CanvasRenderer |
| React package | Layout components, interaction surfaces, scroll/drag hooks, Provider context. | Timeline.Root, useTimeline |
| Product application | Media decoding, project persistence, hotkeys, asset panel, styling themes. | Your application |
Package boundaries
The easiest way to start is importing @techsquidtv/canvas-timeline. However, the code is separated into individual sub-packages so you can import only what your app needs:
| Package | Use it when |
|---|---|
@techsquidtv/canvas-timeline-core |
You need the data model, editing math, and playback engine in a non-React or node environment. |
@techsquidtv/canvas-timeline-react |
You are binding your custom UI or elements to the engine hooks and context. |
@techsquidtv/canvas-timeline-html-media-adapter |
You want one native HTMLMediaElement to follow timeline media. |
@techsquidtv/canvas-timeline-mediabunny-adapter |
You want Mediabunny to decode, render, or schedule media from timeline clips. |
@techsquidtv/canvas-timeline-renderer |
You want to configure, subclass, or theme the canvas drawing pipeline. |
@techsquidtv/canvas-timeline-utils |
You only need rational-time arithmetic or helper functions. |
Media packages follow the same boundary rule as the rest of the architecture:
timeline state stays serializable, while heavy media objects stay in the app or
adapter. Clips connect to media by stable clip.sourceId values. The HTML media
adapter maps those IDs to a record of URLs, blobs, or files for one native media
element; the Mediabunny adapter maps them to source descriptors for decoded
frame/audio preview. In React apps, useHTMLTimelineMedia and
useMediabunnyTimelineMedia are the recommended first hooks because they create
the adapter and connect it to timeline playback in one call.
Mental model summary
When debugging or designing features, remember:
- Serializable State: State is pure data (
TimelineState). - Single Mutator: Only
TimelineEngineupdates the state. - Canvas for Density: Visuals are drawn on a canvas, avoiding DOM overhead.
- React for Interaction: Handlers, hover states, scroll events, and dialogs belong in React.
- Decoupled Data: Heavy media assets and domain details live in your app state, mapped to the timeline by stable IDs.