Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/betterspacx/app/llms.txt

Use this file to discover all available pages before exploring further.

Betterflow’s animation system transforms static screenshots into polished motion graphics. A built-in keyframe timeline lets you scrub through time, apply preset animations from a gallery, and preview playback in real time — all before exporting to MP4, WebM, or GIF. The engine follows Emil Kowalski’s animation principles: snappy entrances, ease-out defaults, and minimum scale values that prevent jarring scale-from-zero effects.

Animation Architecture

The animation system is built on three layers:

Interpolation Engine

lib/animation/interpolation.ts handles keyframe lookup, easing application, and linear interpolation (lerp) between numeric property values.

Preset Library

lib/animation/presets.ts defines 30+ named presets as arrays of AnimationTrack objects, each containing timed Keyframe entries.

Timeline UI

components/timeline/ provides the visual editor: ruler, playhead, track rows, keyframe markers, playback controls, and the preset gallery.

TypeScript Interfaces

The full type system for the animation engine:
// From types/animation.ts

export type EasingFunction =
  | 'linear'
  | 'ease-in'
  | 'ease-out'
  | 'ease-in-out'
  | 'ease-in-cubic'
  | 'ease-out-cubic'
  | 'ease-in-expo'
  | 'ease-out-expo';

export interface AnimatableProperties {
  perspective: number;  // CSS perspective distance in px
  rotateX: number;      // Degrees
  rotateY: number;      // Degrees
  rotateZ: number;      // Degrees
  translateX: number;   // Percentage offset
  translateY: number;   // Percentage offset
  scale: number;        // Multiplier (1 = original)
  imageOpacity: number; // 0–1
}

export interface Keyframe {
  id: string;
  time: number;                          // Milliseconds from start
  properties: Partial<AnimatableProperties>;
  easing: EasingFunction;
}

export interface AnimationTrack {
  id: string;
  name: string;
  type: 'transform' | 'opacity';
  keyframes: Keyframe[];
  isLocked: boolean;
  isVisible: boolean;
  clipId?: string;           // Links this track to an AnimationClip
  originalDuration?: number; // Original preset duration for time-scaling
}

export interface AnimationClip {
  id: string;
  presetId: string;
  name: string;
  startTime: number; // ms
  duration: number;  // ms
  color: string;     // Clip color for timeline display
}

export interface TimelineState {
  duration: number;        // Total timeline duration in ms
  playhead: number;        // Current position in ms
  isPlaying: boolean;
  isLooping: boolean;
  tracks: AnimationTrack[];
  zoom: number;            // Timeline zoom level (1 = 100%)
  snapToKeyframes: boolean;
}

Easing Functions

Eight easing functions control how property values accelerate and decelerate between keyframes:
FunctionFormulaBest For
lineartMechanical motion, turntables
ease-inExits, elements leaving the screen
ease-out1 − (1−t)²Default for entrances — feels natural
ease-in-outQuadratic S-curveOn-screen movement, ambient loops
ease-in-cubicFaster acceleration than quadratic
ease-out-cubic1 − (1−t)³Snappier entrances — used by most presets
ease-in-expo2^(10t−10)Explosive exits
ease-out-expo1 − 2^(−10t)Very snappy, dramatic entrances
// From lib/animation/interpolation.ts
export const easingFunctions: Record<EasingFunction, (t: number) => number> = {
  linear: (t) => t,
  'ease-in': (t) => t * t,
  'ease-out': (t) => 1 - (1 - t) * (1 - t),
  'ease-in-out': (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
  'ease-in-cubic': (t) => t * t * t,
  'ease-out-cubic': (t) => 1 - Math.pow(1 - t, 3),
  'ease-in-expo': (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
  'ease-out-expo': (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
};
33 presets are organized into 8 categories. Apply any preset from the AnimationPresetGallery component — it clones the preset’s tracks with fresh IDs and places them on the timeline.
3D entrance animations that land the screenshot into its final resting position.
PresetDurationKey Motion
Hero Entrance1200msrotateX 25°→0°, scale 0.95→1, fade in at 600ms
Slide In 3D1000msrotateY 30°→0°, translateX 35→0, fade in at 500ms
Rise & Settle1000mstranslateY 25→0, rotateX –15°→0°, fade in at 500ms
Drop In1000mstranslateY –20→0, rotateX 12°→0°, fade in at 500ms

Timeline Editor

The TimelineEditor component provides a complete visual editing environment:
1

Playback Controls

The TimelineControls component offers Play / Pause, Skip to Start, Skip to End, and a Loop toggle. When looping is enabled, the playhead wraps back to the start automatically using modulo math.
2

Timeline Ruler

TimelineRuler renders tick marks at regular intervals across the timeline. The zoom level (TimelineState.zoom) scales the ruler so short animations can be edited with precision.
3

Keyframe Markers

KeyframeMarker components appear at each keyframe position on their parent track. Click a marker to select it; drag it to shift its time. Selected markers show the keyframe’s easing and property values.
4

Draggable Playhead

TimelinePlayhead is a vertical line you can drag to any point in time. When paused, scrubbing the playhead immediately applies the interpolated properties to the canvas for a live preview.
5

Preset Gallery

AnimationPresetGallery shows all presets as thumbnail cards grouped by category. Clicking a preset calls clonePresetTracks() to apply it to the timeline with fresh unique IDs.

Playback Engine

The useTimelinePlayback hook drives real-time preview via requestAnimationFrame:
  1. Each frame, it calculates elapsed time since playback started
  2. Calls getClipInterpolatedProperties(clips, tracks, currentTime) to get the current property values
  3. Writes the result directly into the Zustand store’s perspective3D and imageOpacity fields
  4. The canvas re-renders synchronously, producing smooth animation at the display’s native refresh rate
When multiple clips overlap on the timeline, later-starting clips take precedence over earlier ones for any shared properties — allowing smooth clip-to-clip transitions.

Multi-Slide Animations & Looping

Betterflow supports multi-slide presentations where each slide has its own image and animation. The playback engine handles automatic slide switching based on the playhead position — when the playhead crosses a slide boundary, the canvas swaps to the next slide’s image.

Loop Mode

Enable Loop in the timeline controls to make the animation cycle indefinitely. The playhead wraps using playhead % duration, ensuring seamless looping without gaps.

Clip Time-Scaling

Clips can be stretched or compressed on the timeline. originalDuration is stored on each track so the interpolation engine can time-scale keyframe positions correctly regardless of the clip’s displayed duration.
The snapToKeyframes option in TimelineState causes the playhead to snap to the nearest keyframe when scrubbing within a 50ms threshold. This makes it easy to inspect the exact values at each keyframe without overshooting.

Build docs developers (and LLMs) love