Docs/Reference

Compositions

A video is a folder: brief.md, storyboard.md, video.json, composition/ (index.html, main.js, styles.css) and audio/. npx demovie new <slug> --type … --style … scaffolds one. The composition is a web page: index.html loads /__demovie/clock.js (a virtual clock) and the runtime CSS, and main.js imports the runtime, builds a GSAP timeline and calls v.ready(). The renderer seeks it frame by frame.

Served paths: /__demovie/* (runtime, clock, CSS, GSAP, fonts, SFX), /brand/*, /captures/*, /assets/*, /audio/* (the video's audio folder), /video.json and /glossary.json; composition-relative files resolve normally and .ts files are transpiled on the fly. Nothing else is reachable: every other request is blocked.

Rules that keep videos true and renderable:

  • Product UI only through screen() with a capture id; targets are element ids from the capture's elements.json.
  • Everything is a pure function of time: tweens on v.timeline or the helpers, v.css() for CSS animations, v.onSeek() for canvas, v.random(seed) instead of Math.random, no timers after ready().
  • Declare every shot with v.shot(id, start, end, { kind }); mark the CTA with data-dm-cta.

npx demovie preview <slug> serves a player with scrubbing, frame stepping, safe-area and grid guides, a QA overlay and hot reload. See styles for the presets and the reference compositions in examples/compositions.

Runtime API#

Compositions import the runtime as an ES module served by demovie: import { createVideo, screen, … } from "/__demovie/runtime.js". index.html must load /__demovie/clock.js first (the virtual clock) and /__demovie/runtime.css. Everything visible must be a pure function of time: build tweens on v.timeline (or with these helpers) and call v.ready() last.

Functions and values#

cursor#

A truthful cursor: every target comes from an element map, and it follows the screen's camera (SPEC §10.3).

ts
export function cursor(v: Video, o: {
    style?: "mac" | "pointer" | "dot";
    size?: number;
} = {}): CursorHandle;

Example:

js
const pointer = cursor(v, { style: "mac" });
pointer.moveTo(app, "button:new-project", { at: 8.9, duration: 0.85 });
pointer.click({ at: 9.85 }); // QA DM-G01 checks the click lands inside the element

callout#

A labelled pointer next to a real element (stage overlay; follows the camera).

ts
export function callout(s: ScreenHandle, elementId: string, o: {
    label: string;
    at: number;
    duration: number;
    side?: "top" | "right" | "bottom" | "left";
}): void;

Example:

callout(app, "generic:project-checklist", { label: "Checklist", at: 15.5, duration: 2 });

captions#

Burned-in captions from the voice manifest (SPEC §11.5). Only burns in for formats in video.json captions.burnIn.

ts
export function captions(v: Video, o: {
    style?: "clean" | "bold" | "karaoke";
    position?: "bottom" | "top";
} = {}): void;

Example:

captions(v, { style: "karaoke" }); // burns in for formats in video.json captions.burnIn

The brand logo (wordmark or mark) with a soft entrance at at.

ts
export function logo(v: Video, o: {
    at: number;
    variant?: "mark" | "wordmark";
    parent?: HTMLElement;
}): HTMLElement;

Example:

logo(v, { at: 30.55, variant: "wordmark", parent: card.querySelector(".end-logo") });

text#

ts
const text: {
  /** Staggered reveal of a text element by char, word or line. */
  reveal(target: HTMLElement, o: { at: number; by?: "char" | "word" | "line"; stagger?: number; duration?: number; ease?: string; from?: RevealFrom; }): void;
  /** Count from → to. Values must come from the brief, seed data or captures (QA DM-G04 checks the final value). */
  counter(target: HTMLElement, o: { at: number; from: number; to: number; duration: number; format?: NumberFormatOptions; }): void;
};

Example:

js
text.reveal(h1, { at: 0.3, by: "word", stagger: 0.06, duration: 0.8, ease: "expo.out" });
text.counter(el, { at: 2, from: 0, to: 9, duration: 1.2 }); // numbers must exist in brief/seed/captures

transition#

Transitions between Shot containers (SPEC §10.3).

ts
const transition: {
  crossfade(a: Shot, b: Shot, o: { at: number; duration?: number; }): void;
  wipe(a: Shot, b: Shot, o: { at: number; duration?: number; direction?: "left" | "right" | "up" | "down"; }): void;
  push(a: Shot, b: Shot, o: { at: number; duration?: number; direction?: "left" | "right" | "up" | "down"; }): void;
  zoomThrough(a: Shot, b: Shot, o: { at: number; duration?: number; target?: Rect; }): void;
  colorFlash(a: Shot, b: Shot, o: { at: number; duration?: number; color?: string; }): void;
};

Example:

js
transition.crossfade(intro, product, { at: 3.1, duration: 0.5 });
transition.push(product, dashboard, { at: 20.1, duration: 0.6, direction: "left" });

typeText#

Overlay typed text in the input's captured font and box (SPEC §10.3).

ts
export function typeText(s: ScreenHandle, elementId: string, text: string, o: {
    at: number;
    cps?: number;
    caret?: boolean;
}): void;

Example:

typeText(app, "textbox:project-name", "Q4 Launch", { at: 11.45, cps: 14 });

inspect#

Registries plus visible text/layout at the current t (SPEC §12).

export function inspect(): InspectResult;

Example:

js
// QA calls this; agents rarely need it directly
const state = window.__DEMOVIE__.inspect(); // texts, shots, screens, cursor, clicks, fonts…

screen#

Show a real capture inside a CSS-drawn device frame (SPEC §10.3).

export function screen(v: Video, o: ScreenOptions): ScreenHandle;

Example:

js
const app = screen(v, { capture: "routes/app-projects@desktop", parent: shot.el, device: "browser" });
app.focus("link:q3-launch", { at: 4.8, duration: 1.2, scale: 1.4 }); // ids come from elements.json
app.highlight("link:q3-launch", { at: 6.6, duration: 1.6, style: "ring" });
app.swap("flows/create-project@desktop/created", { at: 13.2, transition: "crossfade" });

createVideo#

Create the video: load video.json/brand/glossary/beats, size the stage, register fonts, preload captures.

ts
export function createVideo(opts: {
    stage?: HTMLElement;
} = {}): Promise<Video>;

Example:

js
const v = await createVideo(); // reads video.json, brand, glossary, beats; sizes the stage
const intro = v.shot("intro", 0, 3.4, { kind: "title" });
// … build the timeline …
v.ready(); // always last

FORMATS#

const FORMATS: Record<FormatId, { width: number; height: number; safe: Insets; }>;

Example:

const { width, height, safe } = FORMATS["9:16"]; // 1080×1920, safe-area insets in %

RUNTIME_VERSION#

const RUNTIME_VERSION: "0.1.0";

Example:

console.info(`demovie runtime ${RUNTIME_VERSION}`);

registerVideoElement#

Register a

export function registerVideoElement(video: HTMLVideoElement, start = 0): void;

Example:

js
// for <video> assets imported with `demovie add`: keep playback a pure function of t
registerVideoElement(videoEl, 4); // the clip's t = 0 is the video's 4 s

Types#

CursorHandle#

ts
interface CursorHandle {
  moveTo(
    s: ScreenHandle,
    elementId: string,
    o: { at: number; duration?: number; ease?: string; anchor?: "center" | [number, number] },
  ): void;
  click(o: { at: number; ripple?: boolean }): void;
  show(o: { at: number }): void;
  hide(o: { at: number }): void;
}

Device#

type Device = "browser" | "laptop" | "phone" | "tablet" | "none";

ScreenHandle#

ts
interface ScreenHandle {
  id: string;
  el: HTMLElement;
  capture: CaptureState;
  /**
   * Rect of an element in stage px at the screen's base transform (no camera, no user tweens). With several ids, the
   * box around all of them, e.g. a column heading and its last card.
   */
  rect(...elementIds: [string, ...string[]]): Rect;
  focus(
    target: string | Rect,
    o: { at: number; duration?: number; scale?: number; padding?: number; ease?: string },
  ): void;
  reset(o: { at: number; duration?: number; ease?: string }): void;
  /**
   * Ring, glow or spotlight an element — or a Rect in stage px at the base transform (the space of rect()), e.g. a row
   * framed from two element rects when the row itself has no element.
   */
  highlight(target: string | Rect, o: { at: number; duration?: number; style?: "ring" | "glow" | "dim-others" }): void;
  swap(
    capture: string,
    o: { at: number; duration?: number; transition?: "cut" | "crossfade" | "slide-left" | "slide-up" },
  ): void;
  scroll(to: number | string, o: { at: number; duration?: number; ease?: string }): void;
}

ScreenOptions#

ts
interface ScreenOptions {
  capture: string;
  parent?: HTMLElement;
  device?: Device;
  width?: number;
  image?: "screen" | "full";
  shadow?: boolean;
  tilt?: { x: number; y: number };
  /** Top-left position in stage px (default: centered in the safe area). */
  x?: number;
  y?: number;
}

Beats#

ts
interface Beats {
  bpm: number;
  offset: number;
  beats: number[];
  bars: number[];
  sections: { name: string; start: number; end: number }[];
}

Brand#

ts
interface Brand {
  name: string;
  tagline: string | null;
  url: string | null;
  colors: { light: BrandColors; dark: BrandColors | null };
  fonts: { heading: BrandFont | null; body: BrandFont | null; mono: BrandFont | null };
  radius: { sm: string | null; md: string | null; lg: string | null };
  shadows: string[];
  logo: { mark: string | null; wordmark: string | null; onDark: string | null };
}

CaptureElement#

One captured element (subset of elements.json).

ts
interface CaptureElement {
  id: string;
  role: string;
  name: string;
  tag: string;
  text?: string;
  bbox: Rect;
  visible: boolean;
  interactive: boolean;
  style: {
    fontFamily: string;
    fontSize: number;
    fontWeight: number;
    color: string;
    background: string | null;
    radius: number;
    lineHeight?: number;
    paddingLeft?: number;
    paddingTop?: number;
  };
  value?: string;
  placeholder?: string;
}

CaptureState#

A loaded capture state (SPEC §6.5).

ts
interface CaptureState {
  id: string;
  url: string;
  path: string;
  title: string | null;
  viewport: { width: number; height: number; deviceScaleFactor: number };
  dpr: number;
  image: { width: number; height: number };
  fullPage: { width: number; height: number } | null;
  /** The color scheme the page was captured in (phone frames match their status bar to it). */
  colorScheme: "light" | "dark";
  elements: CaptureElement[];
  screenUrl: string;
  fullUrl: string | null;
}

DemovieHandle#

ts
interface DemovieHandle {
  version: string;
  ready: boolean;
  error?: string;
  meta: { fps: number; duration: number; width: number; height: number; format: FormatId; scale: number };
  seek(t: number): Promise<void>;
  inspect(): InspectResult;
}

FormatId#

type FormatId = "16:9" | "9:16" | "1:1" | "4:5";

Glossary#

ts
interface Glossary {
  productName: string;
  tagline: string | null;
  features: { term: string; source: string | null }[];
  uiLabels: string[];
  entities: string[];
  people: string[];
  ctaUrl: string | null;
  avoid: string[];
}

Insets#

ts
interface Insets {
  top: number;
  right: number;
  bottom: number;
  left: number;
}

InspectResult#

ts
interface InspectResult {
  t: number;
  format: FormatId;
  width: number;
  height: number;
  safe: Insets;
  texts: TextBox[];
  shots: {
    id: string;
    start: number;
    end: number;
    kind: ShotKind;
    active: boolean;
    screens: number;
    window: [number, number];
  }[];
  screens: {
    id: string;
    capture: string;
    captures: string[];
    bbox: Rect;
    visible: boolean;
    cameraScale: number;
    /** Image pixels per output pixel; > 1 means upscaled (DM-G05). */
    upscale: number;
  }[];
  cursor: { visible: boolean; x: number; y: number }[];
  clicks: { at: number; screen: string; elementId: string; point: { x: number; y: number }; rect: Rect | null }[];
  highlights: { screen: string; elementId: string; rect: Rect }[];
  logos: { bbox: Rect; visible: boolean }[];
  animations: { registered: number; unregistered: { name: string; target: string }[] };
  strayTweens: { targets: string; duration: number }[];
  timersAfterReady: number;
  console: { level: string; text: string }[];
  fonts: { declared: string[]; loaded: string[]; failed: string[] };
  images: { broken: string[] };
  transitions: { at: number; end: number; kind: string }[];
}

Rect#

ts
interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

Shot#

ts
interface Shot {
  id: string;
  start: number;
  end: number;
  kind: ShotKind;
  el: HTMLElement;
}

ShotKind#

type ShotKind = "product" | "title" | "text" | "logo" | "other";

TextBox#

ts
interface TextBox {
  id: string;
  text: string;
  words: number;
  bbox: Rect;
  fontSize: number;
  fontFamily: string;
  fontWeight: number;
  color: string;
  opacity: number;
  clipped: boolean;
  overflowing: boolean;
  ui: boolean;
  caption: boolean;
  counter: boolean;
  role: "text" | "logo" | "cta";
}

Video#

ts
interface Video {
  meta: VideoJson;
  format: FormatId;
  width: number;
  height: number;
  fps: number;
  duration: number;
  scale: number;
  stage: HTMLElement;
  safe: Insets;
  unit: number;
  brand: Brand;
  glossary: Glossary | null;
  beats?: Beats;
  voice?: VoiceManifest;
  gsap: Gsap;
  timeline: Timeline;
  shot(id: string, start: number, end: number, o?: { kind?: ShotKind; el?: HTMLElement }): Shot;
  css(
    el: Element,
    keyframes: Keyframe[],
    o: { start: number; duration: number; easing?: string; fill?: FillMode },
  ): void;
  onSeek(fn: (t: number) => void): void;
  random(seed: string | number): () => number;
  beat(n: number): number;
  bar(n: number): number;
  ready(): void;
}

VideoJson#

video.json as the runtime sees it (SPEC §6.6).

ts
interface VideoJson {
  slug: string;
  title: string;
  type: string;
  fps: number;
  duration: number;
  formats: FormatId[];
  style: string;
  poster?: number | null;
  status?: string;
  audio?: {
    music?: { src: string; gain?: number; beats?: string | null } | null;
    voice?: { manifest: string } | null;
    sfx?: { name: string; at: number; gain?: number }[];
  };
  captions?: { enabled?: boolean; burnIn?: FormatId[] };
  captures?: string[];
  qa?: { ignore?: string[] };
}

VoiceManifest#

ts
interface VoiceManifest {
  provider: string;
  voice: string;
  lines: {
    id: string;
    text: string;
    start: number;
    duration: number;
    file: string;
    words: WordTiming[];
    estimated?: boolean;
  }[];
}
↑ ↓ to move↵ to openesc to close