Coming from Remotion
If you know Remotion, this page maps its ideas to alpha2’s, shows the same video written both ways, and says where the two differ.
The main difference
Section titled “The main difference”In Remotion, a video is a React component. Remotion renders it frame by frame in a browser, so anything the browser can draw (HTML, CSS, SVG, canvas, WebGL) can be in the picture, and your code runs on every frame.
In alpha2, a video is a JSON document: sources, tracks and clips, with effects, transitions, generators for text, shapes and flexbox or grid layouts, captions and an audio mixer. A GPU engine renders it. No code of yours runs per frame: motion is written as keyframes in the document. That makes the document something you can store, diff, validate and generate, and lets the same frames come out of a browser, Node.js, Python, Rust or a CLI.
Concept map
Section titled “Concept map”| Remotion | alpha2 | Notes |
|---|---|---|
<Composition> (width, height, fps, durationInFrames) |
The project and its settings (width, height, frame_rate) |
The duration is where the timeline ends. frame_rate is a rational, { "num": 30000, "den": 1001 } for 29.97. |
| A frame number | A time in ticks, 120,000 per second | Ticks are exact at every common frame rate, so a project’s times don’t depend on its frame rate. |
<AbsoluteFill> |
A clip on a track, or a layout generator’s root |
A clip’s picture is the full canvas. A layout’s root box is the canvas, placed with justify_content, align_items and padding. |
<Sequence from durationInFrames> |
A clip’s timeline_range |
source_range chooses which part of the source plays. |
<Series> |
Clips one after another on one track | |
| Layering by JSX order | Track order | Later tracks draw on top. One track shows one clip at a time. |
useCurrentFrame() + interpolate() |
Keyframes: { "mode": "keyframes", "keyframes": [...] } |
Each keyframe’s interpolation (linear, hold, cubic_bezier) shapes the segment after it. |
Easing.bezier(...) |
{ "type": "cubic_bezier", "x1", "y1", "x2", "y2" } |
Same curve as CSS cubic-bezier(). |
<Img>, <Video>, <OffthreadVideo> |
image and video sources |
Your app or the host decodes them. |
<Audio> |
Audio tracks and clips | Gain, pan, fades, effects, ducking and a master bus. |
CSS transform, opacity |
A clip’s visual (position, anchor, scale, rotation_degrees, opacity) |
Or translate, scale, rotation_degrees, opacity on a layout box. |
| CSS filters, custom shaders | Effects (62 built in), or runtime WGSL effects | |
@remotion/transitions |
Transitions between two clips on a track (122 built in) | |
<Player> |
The browser player, or @alpha2-sh/react’s canvas |
|
renderMedia() |
exportProject() in @alpha2-sh/node, or alpha2-host export |
|
renderStill() |
still() in @alpha2-sh/node, or alpha2.still in Python |
|
| Input props | Generate the JSON with your own code | A template is a function that returns a document. |
The same video, both ways
Section titled “The same video, both ways”A white background and a title that fades in and rises from 0.5 s to 1.5 s, in a 3-second 1080p video:
import { AbsoluteFill, Composition, Easing, interpolate, Sequence, useCurrentFrame } from "remotion";
const Title = () => { const frame = useCurrentFrame(); const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" }); const rise = interpolate(frame, [0, 30], [54, 0], { easing: Easing.bezier(0.2, 0, 0, 1), extrapolateRight: "clamp", }); return ( <AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}> <h1 style={{ opacity, transform: `translateY(${rise}px)` }}>Hello from alpha2</h1> </AbsoluteFill> );};
export const Root = () => ( <Composition id="Title" width={1920} height={1080} fps={30} durationInFrames={90} component={() => ( <AbsoluteFill style={{ backgroundColor: "white" }}> <Sequence from={15}> <Title /> </Sequence> </AbsoluteFill> )} />);{ "schema_version": 1, "id": "remotion-title", "name": "Title", "settings": { "width": 1920, "height": 1080, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "bg", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "solid", "color": { "red": 1, "green": 1, "blue": 1, "alpha": 1 } } } }, { "id": "title", "kind": "generated", "generator": { "type": "text", "text": "Hello from alpha2", "style": { "font_family": "Hanken Grotesk", "font_size": 0.12, "fill": { "type": "solid", "color": { "red": 0.05, "green": 0.05, "blue": 0.08, "alpha": 1 } }, "position": { "x": 0.5, "y": 0.5 }, "anchor": { "x": 0.5, "y": 0.5 } } } } ], "timeline": { "tracks": [ { "id": "background", "name": "Background", "kind": "video", "clips": [ { "id": "bg-clip", "source_id": "bg", "timeline_range": { "start": 0, "end": 360000 }, "source_range": { "start": 0, "end": 360000 } } ], "transitions": [] }, { "id": "title", "name": "Title", "kind": "video", "clips": [ { "id": "title-clip", "source_id": "title", "timeline_range": { "start": 60000, "end": 360000 }, "source_range": { "start": 0, "end": 300000 }, "visual": { "opacity": { "mode": "keyframes", "keyframes": [ { "time": 60000, "value": 0, "interpolation": { "type": "linear" } }, { "time": 180000, "value": 1, "interpolation": { "type": "hold" } } ] }, "position": { "mode": "keyframes", "keyframes": [ { "time": 60000, "value": { "x": 0.5, "y": 0.55 }, "interpolation": { "type": "cubic_bezier", "x1": 0.2, "y1": 0, "x2": 0, "y2": 1 } }, { "time": 180000, "value": { "x": 0.5, "y": 0.5 }, "interpolation": { "type": "hold" } } ] } } } ], "transitions": [] } ] }}In the alpha2 version the title is a text generator on its own track, over a solid fill. The clip starts at 0.5 s (60000 ticks), like <Sequence from={15}>. Its opacity and position keyframes do what the two interpolate calls do. A clip’s visual keyframes are in timeline time, so they start at the clip’s start, 0.5 s. Before the first keyframe and after the last, a value holds the nearest key’s value, like extrapolateRight: "clamp". y 0.55 to 0.5 is 5% of the canvas height, the 54 pixels of the Remotion version.
Rendering
Section titled “Rendering”import { exportProject } from "@alpha2-sh/node";
await exportProject({ project, // the document above assets: {}, // no files: every source is generated fonts: ["HankenGrotesk.ttf"], startTicks: 0, endTicks: 360_000, frameRate: { num: 30, den: 1 }, output: "title.mp4",});alpha2-host export --project title.json --font HankenGrotesk.ttf --out title.mp4Where Remotion drives a headless browser, alpha2-host is a native program that renders on the GPU (Vulkan, Metal or Direct3D 12), or on the CPU where there is none, and encodes with FFmpeg. See Render on a server.
What alpha2 does not do
Section titled “What alpha2 does not do”- It does not render React or the DOM. There is no HTML, no CSS cascade, no SVG DOM and no canvas API in the picture. Layout generators cover flexbox and grid boxes with text and shapes, and SVG and Lottie files can be sources, but arbitrary web content cannot.
- No code runs per frame. You can’t compute a value from the frame number at render time. Compute it ahead and write it as keyframes (a layout
texttakes keyframes of strings, so a counter is a list of held keys), or write a runtime WGSL effect for per-pixel work. - No data fetching during a render. Everything the render needs is in the document or given with it (asset files, fonts, effect resources).
- Fonts are files you give it. The engine never reads system fonts. A text that names a family none of the supplied fonts has draws in the fallback order of the fonts given, and a native export lists that family in
unmatchedFontFamilies.
What you get in exchange: a schema-checked document that loads the same everywhere, frames that are the same in the browser preview and the server export, and GPU rendering without a browser on the server.
See also
Section titled “See also”- Project model: sources, tracks, clips.
- Animation: keyframes and time bases.
- Layout: the flexbox and grid boxes that replace JSX layout.
- Coming from FFmpeg
