mcut
@mcut/timeline

Time, speed, snapping, and trimming

Frame and time conversion, speed maps, snap targets, and edge trims.

Types

TimeMap

type TimeMap = z.infer<typeof timeMapSchema>

A time map as inferred from timeMapSchema, mapping element-local output time to a source offset relative to trimStartMs.

TimeMappedElement

interface TimeMappedElement {
  startMs: number
  durationMs: number
  trimStartMs: number
  timeMap?: TimeMap | undefined
  reversed?: boolean | undefined
}

The timing fields of a media clip that the speed helpers read.

MediaClip

type MediaClip = VideoElement | AudioElement | MulticamElement

An element that plays a window over source media. Video, audio, and multicam share trimStartMs, timeMap, reversed, volume, muted, and fades, so trims, splits, slips, and speed changes treat all three the same way.

EdgeTrimRange

interface EdgeTrimRange {
  minDeltaMs: number
  maxDeltaMs: number
}

The most negative and most positive deltaMs an edge trim accepts.

TrimEdge

type TrimEdge = 'start' | 'end'

The clip boundary an edge trim moves, 'start' or 'end'.

CollectSnapTargetsOptions

interface CollectSnapTargetsOptions {
  playheadMs?: number
  excludeElementIds?: ReadonlySet<string>
}

Options for collectSnapTargets, a playhead time to include and element ids to leave out.

SnapClipResult

interface SnapClipResult extends SnapResult {
  edge: 'start' | 'end' | null
}

A SnapResult plus which clip edge snapped, or null.

SnapOptions

interface SnapOptions {
  enabled?: boolean
  fps?: number
}

Options for snapping, whether magnets are enabled and an fps for frame quantization when nothing snaps.

SnapResult

interface SnapResult {
  ms: number
  guideMs: number | null
  target: SnapTarget | null
}

The snapped time, the guide line time to draw or null, and the target that won or null.

SnapTarget

interface SnapTarget {
  timeMs: number
  kind: SnapTargetKind
}

A time that attracts editing gestures, with its kind.

SnapTargetKind

type SnapTargetKind = 'origin' | 'clip-start' | 'clip-end' | 'marker' | 'playhead'

What a snap target represents, timeline zero, a clip start or end, a marker, or the playhead.

Schemas

timeMapSchema

const timeMapSchema = z
  .array(keyframeSchema)
  .min(2)
  .refine(
    (frames) => frames.every((k, i) => i === 0 || k.timeMs > frames[i - 1]!.timeMs),
    'timeMap keyframes must be strictly increasing in time',
  )
  .refine(
    (frames) => frames.every((k) => k.value >= 0),
    'timeMap values are source offsets and must be >= 0',
  )
  .refine(
    (frames) => frames.every((k, i) => i === 0 || k.value >= frames[i - 1]!.value),
    'timeMap values must be non-decreasing (reverse playback is not supported)',
  )

Zod schema for a time map, at least two keyframes with strictly increasing times and non-negative, non-decreasing source offsets.

Functions

getAverageSpeed

function getAverageSpeed(element: { durationMs: number; timeMap?: TimeMap | undefined }): number

Returns the source span divided by the output duration, or 1 without a time map.

getLocalTimeMs

function getLocalTimeMs(element: TimeMappedElement, sourceMs: number): number

Returns the element-local output time at which a source time plays, the inverse of getSourceTimeMs. It follows the time map and reversed, clamps to the clip, and resolves a freeze to its first local moment.

getMediaSourceDurationMs

function getMediaSourceDurationMs(project: Project, clip: MediaClip): number | undefined

Returns how much source media a clip can play from source time 0. For video and audio it is the asset duration. For a multicam it is the shortest coverage over the sources whose asset duration is known, each asset durationMs minus the source offsetMs. It returns undefined when no duration is known.

getSourceSpanMs

function getSourceSpanMs(element: { durationMs: number; timeMap?: TimeMap | undefined }): number

Returns the source duration the element consumes, which is durationMs without a time map and the last keyframe value with one.

getSourceTimeMs

function getSourceTimeMs(element: TimeMappedElement, localMs: number): number

Returns the source media time that plays at an element-local output time, following the time map when present and running the span backward when reversed is set.

getSpeedAt

function getSpeedAt(element: TimeMappedElement, localMs: number, windowMs = 8): number

Returns the instantaneous speed at an element-local time by finite difference over windowMs, which is 0 inside a freeze and 1 without a time map.

isMediaClip

function isMediaClip(element: TimelineElement): element is MediaClip

Returns true for video, audio, and multicam elements.

makeConstantSpeedMap

function makeConstantSpeedMap(durationMs: number, speed: number): TimeMap

Returns a two-keyframe time map that plays a clip of durationMs at a constant speed.

splitTimeMap

function splitTimeMap(timeMap: TimeMap, offsetMs: number): { left: TimeMap; right: TimeMap }

Splits a time map at an element-local offset into maps for both halves, pinning the boundary value on each side and rebasing the right half to start at 0.

frameToMs

function frameToMs(frame: number, fps: number): number

Returns the integer millisecond timestamp of a frame, which is the time export samples for that frame.

msPerFrame

function msPerFrame(fps: number): number

Returns the length of one frame in fractional milliseconds.

msToFrame

function msToFrame(timeMs: number, fps: number): number

Returns the frame index nearest to timeMs.

quantizeMsToFrame

function quantizeMsToFrame(timeMs: number, fps: number, mode: 'round' | 'floor' | 'ceil' = 'round'): number

Rounds, floors, or ceils a timestamp to a frame boundary and returns it in integer milliseconds.

applyEdgeTrim

function applyEdgeTrim(
  element: TimelineElement,
  edge: TrimEdge,
  deltaMs: number,
): TimelineElement

Moves one edge of an element by deltaMs while its content stays in place, shrinking through the split machinery and growing by revealing more source, and throws CommandError when the result is too short, starts before 0, or is unsupported.

getEdgeTrimRange

function getEdgeTrimRange(
  project: Project,
  element: TimelineElement,
  edge: TrimEdge,
): EdgeTrimRange

Returns the deltaMs range applyEdgeTrim accepts for an element and edge, bounded by the minimum duration, timeline zero, and the available media, with infinite bounds where media is unbounded.

collectSnapTargets

function collectSnapTargets(
  project: Project,
  options: CollectSnapTargetsOptions = {},
): SnapTarget[]

Returns every snap target in the project sorted by time, timeline zero, markers, clip edges, and the playhead when given.

nearestSnapTarget

function nearestSnapTarget(
  timeMs: number,
  targets: readonly SnapTarget[],
  thresholdMs: number,
): SnapTarget | null

Returns the target nearest to timeMs within thresholdMs, preferring the earlier one on ties, or null.

snapClip

function snapClip(
  startMs: number,
  durationMs: number,
  targets: readonly SnapTarget[],
  thresholdMs: number,
  options: SnapOptions = {},
): SnapClipResult

Snaps whichever clip edge is closer to a target and shifts the clip so that edge lands on it, and otherwise returns the start quantized to a frame when an fps is given.

snapTime

function snapTime(
  candidateMs: number,
  targets: readonly SnapTarget[],
  thresholdMs: number,
  options: SnapOptions = {},
): SnapResult

Snaps a time to the nearest target within thresholdMs, and otherwise returns the time quantized to a frame when an fps is given.

On this page