Skip to content

API.md: everything @openvidstudio/core exports

Written because the alternative is reading the library source. If you are an agent about to write a scene, this file has every prop you need and you should not have to open packages/core/src.

Import everything from the package root:

import { CinematicScene, Layer, TerminalReplay, E, color } from "@openvidstudio/core";

Scene wrapper

CinematicScene

Wraps every scene. Grain, vignette and colour grade are always on, and the camera rig lives inside it. Nothing renders outside one.

PropTypeDefaultNotes
cameraCamKeyframe[]requiredAt least two, or nothing moves
stageWidthnumber1920
stageHeightnumber1080
variant"dark" | "light""dark"
grainnumber0.05
vignettenumber0.32
overlayReactNodeScreen space, not camera transformed. Captions and title cards go here
childrenReactNoderequired

The overlay is not optional knowledge. Anything inside children moves with the camera, so a caption placed there drifts off screen as the shot pushes in. Captions, title cards and dips belong in overlay.


Camera

CamKeyframe

type CamKeyframe = {
  frame: number;
  x: number;        // focal point on the stage, lands at frame centre
  y: number;
  scale: number;    // 1 = stage px equals output px, 1.6 = 160% close up
  rotX?: number;
  rotY?: number;
  rotZ?: number;
  easing?: (t: number) => number;  // easing INTO this keyframe
};

The one thing that catches everyone. The visible area is the stage divided by scale. At scale: 1.4 only 1371x771 of a 1920x1080 stage is on screen, so content laid out at full stage width is cropped at the frame edge, and the render does not warn. The binding number is the scene's highest scale, not its first.

Rules of thumb:

  • Real captures: 1.2 to 2.5. STYLE.md requires the camera to sit inside the UI rather than showing a flat full-screen grab.
  • Constructed panels: 1.04 to 1.15. They are already built at stage size, so they need a gentle push, not a zoom.
  • Run validate_scenes and it will tell you which scenes are cropping.

Layer

PropTypeDefaultNotes
depthnumber0Parallax and blur. 0 is the focal plane
maxBlurnumber10Blur in px at depth 1
styleCSSProperties

One focal plane per shot. Put background elements on a non-zero depth so they blur.

CameraRig, useCamera

CinematicScene sets these up for you. Reach for them only when building a scene that needs the camera state directly.


Motion

E and SPRING

E.cinematic  // default move: slow out, soft landing
E.whip       // aggressive, for cuts and fast pans
E.drift      // gentle ambient motion inside a shot
E.snap       // snappy UI reaction, button press, blip reveal

SPRING.soft  SPRING.pop  SPRING.heavy

Linear easing fails QC. There is no linear preset on purpose.

tween, pop, jitter, staggerDelay

tween(frame, [fromFrame, toFrame], [fromValue, toValue], E.cinematic): number
pop(frame, fps, atFrame): number          // spring entrance scalar
jitter(seed, n): number                   // deterministic 0..1, same every render
staggerDelay(index, step = 3): number     // 3 frames at 30fps is 100ms

jitter is deterministic. Never use Math.random() in a scene: every frame renders in a separate process and random values differ per frame, which shows up as flicker.


Text

TitleSlam

PropTypeDefault
textstringrequired
atnumberrequired
fontSizenumber120
colorstringcolor.textPrimary
glowColorstring
align"center" | "left""center"
sfxbooleantrue

Per word entrance. Renders nothing before at.

Caption

PropTypeDefault
textstringrequired
atnumberrequired
outnumber
fontSizenumber34

Sits 84px from the bottom. Put it in overlay, not children.


Panels

BrowserFrame

PropTypeDefault
urlstringrequired
widthnumber1360
heightnumber850
darkbooleantrue
childrenReactNoderequired

Chrome bar is 56px, so height must be the capture height plus 56 or the page is letterboxed. scaffold_scene computes this from the real PNG for you.

Screenshots may only appear inside a BrowserFrame. Recordings and AI clips must not: wrapping generated footage in browser chrome makes it look like a captured page, which STYLE.md's provenance rule forbids.

CodePanel

PropTypeDefault
titlestringrequired
linesCodeLine[]required
widthnumber860
heightnumber640
fontSizenumber22
scrollnumber0
highlightnumber[][]
type Token = { t: string; c?: "kw" | "str" | "fn" | "cm" | "num" | "plain" };
type CodeLine = Token[];
lines={[
  [{ t: "{", c: "plain" }],
  [{ t: '  "name"', c: "str" }, { t: ": ", c: "plain" }, { t: '"value"', c: "str" }],
]}

ChecklistPanel

PropTypeDefault
titlestring"Community Standards"
itemsChecklistItem[]required
widthnumber760
fontSizenumber27
sfxbooleantrue
type ChecklistItem = { label: string; flipAt?: number };  // omit flipAt to stay unchecked

flipAt is an absolute scene frame.

TerminalReplay

PropTypeDefault
stepsTermStep[]required
titlestring"Project"
widthnumber1280
fontSizenumber26
cpsnumber16
sfxbooleantrue
trailingPromptbooleantrue
type TermLine = { text: string; color?: string; glow?: boolean; bold?: boolean };
type TermStep =
  | { type: "cmd"; text: string }
  | { type: "out"; lines: TermLine[]; stagger?: number; preDelay?: number }
  | { type: "pause"; frames: number };

Terminals are always this component, never a screen recording of a terminal.

computeTerminalTimeline(steps, fps, cps) returns { items, totalFrames }, so camera keyframes can land frame-exact on content events rather than being guessed.

CursorActor

PropTypeDefault
pathCursorKeyframe[]required
clicksnumber[][]
scalenumber1.6
sfxbooleantrue
type CursorKeyframe = { frame: number; x: number; y: number };

Coordinates are stage space. Movement is eased automatically; never linear.

RepoCta

PropTypeDefault
ownerstringrequired
namestringrequired
descriptionstringrequired
chipsstring[]required
titleTextstringrequired
durationInFramesnumber270

A whole scene, not a panel. Do not wrap it in a CinematicScene; it brings its own. No star action by default.


Sound

All synthesized by scripts/gen-sfx.sh with ffmpeg, so a rendered video carries no third-party audio rights. Regenerate with bash scripts/gen-sfx.sh.

ComponentPropsUse
KeySoundat, seed?, volume? (0.16)One keystroke
TypingSfxcharFrames, volume?One key per typed character
EnterKeyat, volume? (0.2)End of a typed command
Clickat, volume?Cursor click
Whooshat, volume?Camera move or cut
Blipat, volume? (0.3)UI reveal
SuccessChimeat, volume? (0.35)Completion
Bellat, volume? (0.3)Heavier completion
MusicBeddurationInFrames, volume? (0.16)Ambient bed, loops

TerminalReplay emits its own typing sounds. Pass sfx={false} to silence it.

Narration is separate: generate_narration writes public/audio/vo/<beatId>.mp3 and stitch_composition picks it up.


Look pass

FilmGrain, Vignette, ColorGrade, LookPass. CinematicScene applies these, so reach for them only when compositing outside a scene.


Tokens

color.bg0 color.bg1 color.panel color.panelBorder
color.textPrimary color.textSecondary
color.accent color.accentAlt color.success color.warn color.danger
color.lightBg color.lightCard color.lightShadow

font.ui    // "Inter"
font.mono  // "JetBrains Mono"

radius.window  // 12
radius.card    // 10
radius.pill    // 999

glow(color, strength = 1)       // returns { textShadow }
panelShadow(dark = true)        // returns a box-shadow string

Use tokens rather than literal hex, so a per-video theme can override them.


A scene, end to end

import React from "react";
import { CinematicScene, Layer, TerminalReplay, Caption, color, E } from "@openvidstudio/core";

export const Install: React.FC = () => (
  <CinematicScene
    camera={[
      { frame: 0, x: 960, y: 540, scale: 1.05, rotZ: -0.4 },
      { frame: 300, x: 960, y: 548, scale: 1.15, rotZ: 0.35, easing: E.drift },
    ]}
    overlay={<Caption text="one command" at={40} out={240} />}
  >
    <Layer depth={0}>
      <div style={{ position: "absolute", inset: 0, display: "flex",
                    alignItems: "center", justifyContent: "center" }}>
        <TerminalReplay
          width={1400}
          steps={[
            { type: "cmd", text: "npx -y @openvidstudio/mcp-server" },
            { type: "out", lines: [{ text: "connected over stdio", color: color.success, glow: true }] },
          ]}
        />
      </div>
    </Layer>
  </CinematicScene>
);

Two keyframes so it moves, content centred inside the safe area, caption in the overlay, tokens instead of hex.