Timelinx

Time & Frames

How Timelinx represents time - TimelineFrame, FrameRate, Timecode, and conversion helpers

All positions, durations, and boundaries in TimelineState are stored as integer frames. There are no floats in the state model. This eliminates the class of bugs where sub-frame rounding errors accumulate across edits - a clip split at frame 450 will always land exactly at frame 450.

TimelineFrame

type TimelineFrame = number & { readonly __brand: 'TimelineFrame' };

A branded integer. The brand prevents accidentally passing a raw number (like a pixel coordinate or a seconds value) where a frame count is expected.

Create with toFrame():

import { toFrame } from '@timelinx/core';

const start = toFrame(0);
const end   = toFrame(300);  // 300 frames

toFrame() does no conversion - it only applies the brand. If you have a duration in seconds, convert first with secondsToFrames().

When to use toFrame()

Use toFrame() only when you know the value is already a frame count. For conversions from seconds or timecode, use the helpers below.


FrameRate

type FrameRate = {
  readonly num: number;  // numerator
  readonly den: number;  // denominator
};

Frame rates are stored as rational fractions to represent drop-frame rates without floating-point loss:

Frame rateRationalframeRate() call
24 fps24/1frameRate(24)
25 fps25/1frameRate(25)
30 fps30/1frameRate(30)
23.976 fps (drop)24000/1001frameRate(24000, 1001)
29.97 fps (drop)30000/1001frameRate(30000, 1001)
59.94 fps (drop)60000/1001frameRate(60000, 1001)
60 fps60/1frameRate(60)

frameRate(num, den?) shorthand:

import { frameRate } from '@timelinx/core';

const fps30     = frameRate(30);           // { num: 30, den: 1 }
const fps2997   = frameRate(30000, 1001);  // { num: 30000, den: 1001 }
const fps23976  = frameRate(24000, 1001);  // { num: 24000, den: 1001 }

Preset constants (FrameRates):

import { FrameRates } from '@timelinx/core';

FrameRates.fps24     // { num: 24, den: 1 }
FrameRates.fps25     // { num: 25, den: 1 }
FrameRates.fps30     // { num: 30, den: 1 }
FrameRates.fps2997   // { num: 30000, den: 1001 }
FrameRates.fps23976  // { num: 24000, den: 1001 }
FrameRates.fps60     // { num: 60, den: 1 }
FrameRates.fps5994   // { num: 60000, den: 1001 }

isDropFrame(fps):

import { isDropFrame } from '@timelinx/core';

isDropFrame(frameRate(30))           // false
isDropFrame(frameRate(30000, 1001))  // true

Drop-frame rates have den === 1001. Non-integer frame rates use drop-frame timecode numbering to stay aligned with real-world clock time.


Conversion Helpers

secondsToFrames(seconds, fps)

Convert a duration or position in seconds to frames:

import { secondsToFrames, frameRate } from '@timelinx/core';

const fps = frameRate(30);
const frames = secondsToFrames(10, fps);  // 300 frames (10 × 30)
const atHalf = secondsToFrames(0.5, fps); // 15 frames

This is the most common conversion - use it when accepting user input in seconds or reading audio durations from the browser's AudioBuffer.

framesToSeconds(frames, fps)

import { framesToSeconds, toFrame, frameRate } from '@timelinx/core';

framesToSeconds(toFrame(300), frameRate(30));  // 10.0
framesToSeconds(toFrame(1),   frameRate(24));  // 0.0416...

framesToTimecode(frames, fps)

Returns a display string in HH:MM:SS:FF format:

import { framesToTimecode, toFrame, frameRate } from '@timelinx/core';

framesToTimecode(toFrame(0),    frameRate(30));  // '00:00:00:00'
framesToTimecode(toFrame(30),   frameRate(30));  // '00:00:01:00'
framesToTimecode(toFrame(1799), frameRate(30));  // '00:00:59:29'
framesToTimecode(toFrame(1800), frameRate(30));  // '00:01:00:00'

Drop-frame rates use semicolon as the frame separator: '00:01:00;02' (frames 0 and 1 are skipped at minute boundaries).


Timecode

type Timecode = string;  // display format, e.g. '01:00:00:00'

Timecode is a display type - it is used as the startTimecode on a timeline (the origin offset shown in the ruler) and as human-readable output. All internal state positions use TimelineFrame.

toTimecode(frame, fps) converts a frame count to a Timecode string - it is an alias for framesToTimecode:

import { toTimecode, toFrame, frameRate } from '@timelinx/core';

const tc = toTimecode(toFrame(3600), frameRate(30));  // '00:02:00:00'

RationalTime

RationalTime is used only at import/export boundaries (OTIO, FCPXML). It is not stored in TimelineState and you will not encounter it in normal dispatch code:

type RationalTime = {
  readonly value: number;
  readonly rate: number;
};

The serialization module converts RationalTime to TimelineFrame at import time and back at export time. You do not need to handle this manually unless you are writing a custom serializer.


Common Patterns

Timeline position from seconds:

import { secondsToFrames, toFrame, frameRate, createTimeline } from '@timelinx/core';

const fps = frameRate(30);

const timeline = createTimeline({
  id: 'tl-1',
  name: 'My Edit',
  fps,
  duration: secondsToFrames(600, fps),  // 10 minutes
});

Display timecode in the UI:

import { framesToTimecode, toFrame, frameRate } from '@timelinx/core';

function formatFrame(frame: number, fps: FrameRate): string {
  return framesToTimecode(toFrame(frame), fps);
}

Convert a clip's playhead position to media frame:

// Inside a player: given the playhead is at timelineFrame
// and a clip started at timelineStart with mediaIn offset:
const mediaFrame = clip.mediaIn + (timelineFrame - clip.timelineStart);

The mediaFrameForClip() helper in @timelinx/core does this for you:

import { mediaFrameForClip } from '@timelinx/core';

const mediaFrame = mediaFrameForClip(clip, playheadFrame);

On this page