Pure functions of the frame: hooks, easing, camera curves, transitions, copy checks. Signatures are read from the source.
useInOut
Entrance and exit progress for an element, driven by the current frame only. enter goes 0 -> 1 with a spring. exit goes 0 -> 1 over the last exitFrames. visible = enter * (1 - exit), handy for opacity.
useInOut({ delay = 0, durationInFrames, spring: springName = "smooth", exitFrames = timing.exit, }: InOutOptions = {})
useStagger
Spring progress for one item in a staggered group.
useStagger()
useSafeArea
Safe-area insets and format for the current composition.
useSafeArea()
useFrameType
Frame-relative type style (vw ramp, text floor enforced) for the current composition width.
useFrameType(step: FrameStep)
directionVector
Unit vector for a direction: the element travels FROM the opposite side toward rest.
directionVector(direction: Direction): [number, number]
buoyant
"Buoyant" ease: a cubic ease-out plus a half-sine bob over the WHOLE lifetime, so a rising element keeps drifting through its peak instead of freezing. Returns 0 at t=0 and exactly 1 at t=1; in between it overshoots by about bob and settles back.
buoyant(bob = 0.03): EasingFn
entryDecay
Distance still to travel (px, always >= 0) for the INCOMING shot frame frames into an entry of entryFrames. Starts at speed and decays by decay per 60-fps frame; the offset is exactly 0 on the last frame. Apply it as a translate in the travel direction.
entryDecay( frame: number, entryFrames: number, { fps, speed = VELOCITY_CUT_DEFAULTS.speed, decay = VELOCITY_CUT_DEFAULTS.decay }: VelocityCutOptions, )
exitRamp
Distance travelled (px, always >= 0) by the OUTGOING shot frame frames into an exit of exitFrames. Speed grows by growth per 60-fps frame and is speed on the last frame, i.e. the fastest frame is the cut. Closed form of the geometric speed series.
exitRamp( frame: number, exitFrames: number, { fps, speed = VELOCITY_CUT_DEFAULTS.speed, growth = VELOCITY_CUT_DEFAULTS.growth }: VelocityCutOptions, )
exitOpacity
Exit opacity 1 -> 0 as 1 - (n/N)^1.5: reaches exactly 0 on the last frame (so a hard cut after it does not pop) and stays high early.
exitOpacity(frame: number, start: number, length: number)
softFade
Fade in that never shows an empty first frame: starts at floor (25 to 57 % is the range the source used) and reaches 1 after length frames.
softFade(frame: number, start: number, length: number, floor = 0.3)
logZoom
Zoom interpolated in log space, so equal steps feel like equal zoom changes and a 1x to 4x zoom does not rush at the end. t is progress 0..1 (apply any easing first). Never mix with a linear zoom on the same shot: it turns into a zoom-in/zoom-out wobble.
logZoom(t: number, from: number, to: number)
speedCurve
Camera driven by speed, not by position keys. The table gives speed as a share of cruise at times (0 at 0 s, 0.5 by 0.6 s, 1.0 by 1.6 s, held, easing to 0.12 at 8.3 s), the shares are smoothstepped between keys, integrated into position, and scaled so the camera ends exactly at distance. Because position is an integral of a continuous speed, it cannot surge or stop unless the table says so. The shape of the table IS the camera language of a glide shot.
speedCurve( table: readonly SpeedKey[], { fps, distance }: { fps: number; distance: number }, )
sampleCurve
Sample fn(frame) for frames startFrame .. startFrame + count - 1.
sampleCurve( fn: (frame: number) => number, { name, fps, unit = "value", startFrame = 0, count }: { name: string; fps: number; unit?: string; startFrame?: number; count: number; }, ): CurveFile
velocityOf
Per-frame velocity (difference of consecutive values; first entry repeats the second). Use it to find a zero-velocity run in the middle of a move.
velocityOf(values: readonly number[])
longestStall
Longest run of near-zero velocity strictly inside the move (ignores the first and last 10 % of frames). Returns its length in frames; > 2 usually reads as a stall.
longestStall(values: readonly number[], epsilon = 1e-4)
lintCopy
Returns warnings, never throws. An empty warnings array means the copy fits the budget.
lintCopy({ fragments, durationInFrames, fps, visibleFrames, coveredFrames, muted = false, }: LintInput): CopyLint
countWords
countWords(text: string)
Copy budget lint (F-A2). Pure, no imports, so scripts/copy-lint.mjs can load it.
{ wordsPer30s, mutedWordsPer30s, maxFragmentWords, maxWordsOnOneFrame, maxCoverage }
maskWipe
Hard-edged directional wipe. The incoming scene is revealed from from; optional 3 px accent edge.
maskWipe( props: MaskWipeProps, ): TransitionPresentation<MaskWipeProps>
circleReveal
Incoming scene grows out of a circle. Use as a match cut: put the origin where the previous focal point was.
circleReveal( props: CircleRevealProps = { origin: [50, 50] }, ): TransitionPresentation<CircleRevealProps>
transitionTiming
Default timing for every scene transition: spring, no overshoot, timing.transitionFrames frames.
transitionTiming( durationInFrames: number = timing.transitionFrames, )
presets
The approved set. Pick from here instead of reaching for other presentations.
{ fade, wipe, circle, velocity, occluder }
velocityCut
Velocity-matched cut: the outgoing scene accelerates and is cut at its fastest frame, the incoming scene starts at the same speed and brakes to rest, so momentum crosses the edit. Pair with velocityCutTiming(frames) using the SAME frames. Delay the incoming scene's own entrances by cutAt * frames: it is hidden until then. Text in a moving scene should be under 20 px per frame by the time it is read.
velocityCut({ frames = 30, travel = "left", speed = 36, cutAt = 0.4, blur = true, }: Partial<VelocityCutProps> = {}): TransitionPresentation<VelocityCutProps>
velocityCutTiming
Linear timing for the velocity cut: its curves are closed forms of the frame, so no spring.
velocityCutTiming(frames = 30)
occluderCut
Hidden cut: a soft, neutral band sweeps from from across the frame and the hard cut happens at the midpoint, while the band covers the screen. Pair with occluderTiming(frames). Delay the incoming scene's entrances by half the length. Line-carry, impact slam and the light leak from the same source are not built (see the ledger).
occluderCut( props: Partial<OccluderCutProps> = {}, ): TransitionPresentation<OccluderCutProps>
occluderTiming
occluderTiming(frames = 24)