ZhouXiaolin/opencat
ZhouXiaolin/opencat has 72 GitHub stars and 12 forks. GitHot measured 0 stars in 24 hours and 0 in 7 days. Last checked 2026-11-15 09:46 UTC. Last push 2026-11-03.
Measured star growth
- in 24h
- 0
- in 7 days
- 0
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
About
Write JSONL/XML. Render video/image. Built for agents.
Creative & MediaREADME.md
OriginalJSX defines scenes, animations, and layouts. Skia GPU renders, FFmpeg encodes to MP4 — deterministic, cross-platform, cross-machine consistent. No Chromium snapshots, no Puppeteer, no bloated Web rendering pipeline.
A video is just JSX (below is scene A of examples/k3-promo — entry index.jsx, scenes in scenes/ — the spot above):
import { gsap } from 'gsap';
import { useGSAP } from 'opencat/runtime';
export const meta = { width: 1920, height: 1080, fps: 30, duration: 16.4667 };
function StageCanvas({ id }) {
return <canvas id={id} class="absolute inset-0 w-[1920px] h-[1080px]" />;
}
function SceneA() {
useGSAP((ctx, t) => {
var T = t; // seconds over the whole 16.4667s
if (T >= 0.75 && T < 4.62) {
gsap.timeline()
.set('azoom', { scale: 1.07 }, 0.933)
.to('azoom', { scale: 0.9417, duration: 3.667, ease: 'none' }, 0.933)
.fromTo('atitlegrp', { opacity: 0 }, { opacity: 1, duration: 0.2, ease: 'power1.inOut' }, 3.03);
}
});
return (
<div id="sceneA" class="absolute inset-0 z-[2] opacity-0">
<div id="azoom" class="absolute inset-0 [transform-origin:50%_50%]">
<div id="afield" class="absolute inset-0"><StageCanvas id="afield-canvas" /></div>
<div id="atitlegrp" class="absolute left-[755px] top-[444px] w-[467px] h-[185px] opacity-0">
<div id="a-sel" class="absolute inset-0 border-[1px] border-white/25"></div>
<text id="aline1" class="absolute text-[#d8d8da] text-[63.6426px] tracking-[-0.8px] leading-none">Every token</text>
</div>
</div>
</div>
);
}
export default function K3Promo() {
return <div id="root" class="relative w-[1920px] h-[1080px] bg-[#010101] overflow-hidden"><SceneA /></div>;
}
cd examples/k3-promo
npm install # bun install works too
npm run build # vite: bundles JSX, embeds the compiled CSS, renders → dist/video.mp4
MP4 ready. No browser, no screenshots, no GUI needed (the vite plugin renders through the opencat binary, resolved from PATH or OPENCAT_BIN).
Why OpenCat
| OpenCat | Remotion / HyperFrames | |
|---|---|---|
| Render | Rust native GPU (Skia) | Chrome snapshot |
| Speed | 10x | Baseline |
| Deployment | Any environment / pure CLI | Requires Chromium |
| Animation | Real GSAP — the official npm package runs in-engine | Direct GSAP / anime.js |
| Browser render | WASM + CanvasKit | Native |
| Deterministic | ✅ Cross-machine consistent | ❌ |
| AI-friendly | Declarative JSX | JSX/HTML, more complex |
Remotion reuses the Web ecosystem, but Chrome snapshot has inherent limitations — constrained GPU access, high memory overhead, capped frame rates, and Chromium required in deployment. OpenCat calls GPU and FFmpeg natively, with an order of magnitude higher performance ceiling.
Capabilities
Real GSAP — the official package, in-engine
The official gsap 3.x source runs inside the engine (V8) on a deterministic seek clock: tweens are g(t), evaluated per frame — the ticker never touches wall-clock time. Non-interactive official plugins are bundled (eases, SplitText, MorphSVG, DrawSVG, MotionPath, ScrambleText, Physics2D/PhysicsProps, Flip, CustomEase, …); scroll/pointer-driven plugins are excluded by design — input events cannot be a pure function of t.
import { gsap } from 'gsap';
import { SplitText } from 'gsap/SplitText';
// Declarative timeline — scene A of examples/k3-promo (scenes/scene-a.jsx).
// Tween targets are node ids; every frame evaluates the timeline at t.
gsap.timeline()
.set('azoom', { scale: 1.07 }, 0.933)
.to('azoom', { scale: 0.9417, duration: 3.667, ease: 'none' }, 0.933)
.to(['afield', 'afieldp'], { opacity: 0.35, duration: 0.4333, ease: 'power1.inOut' }, 1.9667)
.fromTo('apwrap', { opacity: 0 }, { opacity: 1, duration: 0.2, ease: 'power1.inOut' }, 3.03);
// Official SplitText — the same package gsap ships. Wrappers are real nodes.
var chars = new SplitText(document.getElementById('otxt'), { type: 'chars' }).chars;
var tl = gsap.timeline();
chars.forEach(function (p, i) {
tl.fromTo(p, { opacity: 0, x: (i - 6.5) * 21 },
{ opacity: 1, x: 0, duration: 0.43, ease: 'power2.out' }, 7.5663);
});
Multi-scene timelines + transitions
<tl id="main-tl">
<Scene1 /> {/* duration="4" on its root */}
<transition from="scene1" to="scene2" effect="fade" duration="0.6" />
<Scene2 /> {/* duration="4" on its root */}
</tl>
Each scene is a component whose root carries the duration. A scene's hooks bind to that scene's clock, so ctx.current.time starts at 0 when the scene becomes visible — no manual start-time bookkeeping, and transitions do not shift it.
Built-in: fade / slide / wipe / clock_wipe / iris / light_leak, with custom GLSL shader support.
Components — reusable building blocks
Any JSX function is a component. k3-promo hoists shared building blocks out of the node tree (scenes/shared.jsx) — props replace the old template parameters:
function StageCanvas({ id }) {
return <canvas id={id} class="absolute inset-0 w-[1920px] h-[1080px]" />;
}
function Cdot({ id, s, c }) {
return <div id={id} class={'absolute w-[' + s + '] h-[' + s + '] bg-[' + c + ']'} />;
}
// call sites pass only the differences
<StageCanvas id="afield-canvas" />
<Cdot id="a-tl" s="13px" c="#d8d8da" />
Components compose by nesting, and props.children composes markup from the call site:
function Card({ bg, title, children }) {
return <div class={'w-[400px] rounded-xl p-6 ' + bg}>
<text class="text-[20px] font-bold">{title}</text>
{children}
</div>;
}
<Card bg="bg-white" title="Hello">
<text class="text-gray-500">Card content.</text>
</Card>
WASM rendering in the browser
import { initWasm, preloadAssets, getRendererOrThrow, exportMp4 } from 'opencat-web';
await initWasm();
const catalog = await preloadAssets(jsxContent);
const renderer = getRendererOrThrow();
renderer.build_frame(jsxContent, frameNumber, canvas, catalog);
await exportMp4({ /* ... */ });
Pure WASM + CanvasKit, no server required.
Canvas drawing + SKSL effects
Every <canvas> node is a real canvas: the official CanvasKit API surface (CK.MakeCanvasSurface, Paint, RuntimeEffect, …) draws straight into the composition, and the node's CSS box, transforms and effects still apply on top. The engine accepts the official CanvasKit signatures and lowers them to a draw-operation stream — replayed natively by the Rust renderer, and in the browser by real canvaskit.wasm. Custom per-pixel effects are hand-written SKSL through CK.RuntimeEffect.Make — the only effect path, and the same one a browser would run:
// examples/inspector-launch — an SKSL halftone background
var fx = CK.RuntimeEffect.Make(SKSL_SOURCE); // compiled once, cached
var paint = new CK.Paint();
paint.setStyle(CK.PaintStyle.Fill);
paint.setShader(fx.makeShader([ax, ay, bx, by, blend, phase])); // uniforms
canvas.drawRect([0, 0, 1920, 1080], paint);
A canvas subtree — layout, images, text, video — composites into the scene through the surface pipeline (canvas target → surface → picture); pixel buffers never cross the JS bridge. Reference: examples/inspector-launch/inspector-launch.jsx, examples/hyperframes-launch; guide: skills/opencat-creator/references/canvaskit.md.
More
- 3D scenes:
import * as THREE from 'three'— the official package executes as-is; theWebGLRendereroutput is captured into the scene graph and rendered natively in Rust (and replayed with real three in the browser). GLTF/GLB viaGLTFLoader, textures, custom shaders - Standard CSS layout:
class="flex items-center justify-center gap-4"compiles to plain CSS via tailwindcss and runs through the built-in CSS engine (selector matching, cascade, var/calc/color-mix); thestyleattribute accepts full declarations - Audio mixing:multi-track, scene-attached, auto-mix to output
- Subtitle engine:SRT parsing, cross-scene persistent display
- SVG: file (
src) and inline markup (inline) channels, scaled to the node box - Deterministic rendering:
value = f(time), consistent across machines
Quick start
# Render MP4 (the spot above) — bundle + embed CSS + render in one command
cd examples/k3-promo && npm install && npm run build # → dist/video.mp4
# Desktop player for live preview (macOS / Windows / Linux)
cargo run --release --bin opencat -- view path/to/input.jsx
The CLI is opencat render <input> / opencat view <input>; the legacy bare form opencat input.jsx still works. Pass --base-dir when rendering a build artifact outside the project (relative asset paths resolve against the input file's directory by default).
Web (WASM):
cd crates/opencat-web/web && npm run build, requiresCross-Origin-Isolatedenvironment.
Vite project template
For standalone projects, scaffold a vite-based workspace where tailwind compilation and rendering are one command:
# Scaffold (from the OpenCat checkout; zero deps)
node packages/create-opencat/bin.mjs my-video
cd my-video
bun install # or npm install
npm run build # bundles the entry with the compiled CSS embedded → dist/video.mp4
Preview: opencat view dist/index.bundle.jsx --base-dir src (the bundle carries the embedded stylesheet).
The generator copies packages/create-opencat/template/: a vite config with an inline plugin that (1) bundles the entry with esbuild — real npm packages such as lucide-react resolve here, and react/react/jsx-runtime are aliased to a shim that serializes <svg> subtrees into the engine's <svg inline> channel — (2) compiles tailwind classes into standard CSS in memory and embeds it in the bundle as export const css, so the bundle is single-file self-contained, and (3) shells out to opencat render on the bundle. The opencat binary is resolved from OPENCAT_BIN or PATH. Build artifacts (bundle + video) land in dist/. Assets and fonts must live under src/ — the engine resolves relative paths against the entry file's directory, and the plugin passes --base-dir <entry dir> when rendering the bundle from dist/.
Engine / Web alignment (k3diff pixel metrics)
Hard pixel metrics (mae/maxd/p8, not SSIM) — the verdict comes from tools/k3diff.py: render both sides to MP4 and diff the videos. Native side: opencat render. Web side: crates/opencat-web-oracle drives the harness page headlessly via ChromeDriver (wasm facade + CanvasKit) and hands back a per-range H.264 MP4; diff it against the native render or the original reference video. Beyond the spot compositions, a shader-effect registry (docs/shaders-align, 200+ presets) pins engine↔web↔compute pixel parity per preset.
# 1) build web facade (wasm + JS + web-demuxer.wasm → dist/)
cd crates/opencat-web/web && npm run build && cd -
# 2) media used by the oracle compositions (loads http://127.0.0.1:8080/...)
# serve your mp4/png/mp3 tree on :8080
Architecture
JSX ──→ Taffy layout ──→ Skia render ──→ encode → MP4
↑
V8 animation scripts
Dual pipeline: Rust (GPU) + FFmpeg → MP4 | WASM + CanvasKit (WebGL) → Canvas / MP4
Incremental rendering: Resolve → Layout → Display,Merkle Tree skips unchanged subtrees + Scene Snapshot zero-cost reuse.
opencat
├── crates/
│ ├── opencat-core/ # JSX runtime (V8), layout (Taffy), text (cosmic-text), CSS engine
│ ├── opencat-engine/ # Skia render, three rasterizer, FFmpeg encode, V8 host
│ ├── opencat-web/ # WASM: browser render + MP4 export
│ ├── opencat-web-oracle/ # headless web renderer — the alignment oracle
│ ├── opencat/ # CLI: render / view
│ └── faac/ # AAC encoder binding
├── packages/create-opencat/ # vite project template
├── web/ # Web video editor
└── examples/ # Example projects
Build from source
Prerequisites
Rust toolchain (edition 2024). Install via rustup:
rustup install nightly # edition 2024 requires nightly as of early 2025FFmpeg dev libraries (for MP4 encoding). The crate
ffmpeg-nextdiscovers FFmpeg viapkg-config— no manual path configuration is needed on standard systems.Linux (Ubuntu / Debian)
sudo apt install \ libavcodec-dev libavformat-dev libavutil-dev \ libavfilter-dev libswscale-devMinimum version: FFmpeg 6.x. Verify:
ffmpeg -versionOn this system: FFmpeg 7.1.1 is installed and all dev packages are present.
macOS
brew install ffmpegHomebrew installs ffmpeg to
/opt/homebrew(Apple Silicon) or/usr/local(Intel). SetFFMPEG_DIRto point to the Homebrew prefix:# Apple Silicon (M1/M2/M3/M4) export FFMPEG_DIR=/opt/homebrew # Intel Mac export FFMPEG_DIR=/usr/localIf
pkg-configcannot automatically find the ffmpeg libs, setFFMPEG_DIRto specify the path. Add the export to your shell config (~/.zshrc/~/.bashrc) to persist it.Verify:
ls $FFMPEG_DIR/lib/libavcodec.*Windows
Download FFmpeg dev packages from gyan.dev or
vcpkg install ffmpeg. Then set:$env:FFMPEG_DIR = "C:\path\to\ffmpeg"OpenGL / EGL dev libraries (Linux, for Skia GPU rendering):
sudo apt install libegl-dev libgles-dev libgl1-mesa-dev libx11-devmacOS provides Metal via the system SDK (no manual install). Windows provides OpenGL via the system driver.
opencat view(the desktop preview player) creates its GL context via EGL on Linux, matching the prebuilt Skia backend. It currently requires an X11 window handle, so on a Wayland session it runs through XWayland (setWAYLAND_DISPLAY=only if auto-selection fails). Native Wayland (wl_surface) support is not wired up yet.Fontconfig dev library (Linux):
sudo apt install libfontconfig-dev
Windows: two required linker flags
The pre-built Skia and V8 binaries are built with incompatible C runtimes, so a plain
cargo build on Windows fails to link. Both fixes go in ~/.cargo/config.toml
(or .cargo/config.toml in the repo):
[target.x86_64-pc-windows-msvc]
rustflags = [
"-C", "link-arg=/STACK:16777216",
"-C", "link-arg=/FORCE:MULTIPLE",
]
/STACK:16777216 — ffmpeg-sys-next's build script runs bindgen over the full FFmpeg
header set, which recurses deeper than the 1 MiB Windows default main-thread stack
(SizeOfStackReserve in the PE header). Without this the build script dies with
STATUS_STACK_OVERFLOW (exit 0xc00000fd).
/FORCE:MULTIPLE — rusty_v8.lib carries a /DEFAULTLIB:libcmt.lib directive
(static CRT); skia.lib carries /DEFAULTLIB:msvcrt.lib + msvcprt.lib (dynamic CRT).
Both directives are baked into the prebuilt binaries and cannot be changed by Cargo
features or environment variables. Each library defines its own std::terminate, so the
link fails with LNK2005 / LNK1169. This flag tells the linker to pick one
implementation instead of erroring.
Known risk: the two STL implementations are mixed in one executable. Ordinary rendering and JS
try/catchare verified working, but the behaviour of C++ exceptions crossing between the Skia and V8 sides is undefined. If you hit inexplicable crashes, suspect this flag first. The clean fix is building V8 from source with a dynamic CRT (EXTRA_GN_ARGS="is_component_build=true"), which takes hours and is not wired up yet.
Skia
Skia is pulled in via skia-safe with the binary-cache feature enabled. This causes skia-bindings to download a pre-built Skia binary at build time — no local compilation or static package download is required.
- Linux:
glbackend (OpenGL) - macOS:
metalbackend (Metal) - Extra:
skottiefor Lottie animation support
The pre-built binaries are cached in ~/.cargo/skia-binaries/ after the first build.
Build commands
CLI (MP4 rendering):
cargo build --release --bin opencat
The binary is at target/release/opencat. Render a video:
opencat render examples/k3-promo/dist/index.bundle.jsx
Desktop preview player (macOS / Windows / Linux):
cargo run --release --bin opencat -- view path/to/input.jsx
Web (WASM):
cd crates/opencat-web && npm run build
Requires wasm-pack and a Cross-Origin-Isolated environment to run.
Verification
Check that the build picked up the correct FFmpeg and Skia versions:
cargo run --bin opencat -- --version
No ffmpegDir or SKIA_BINARIES_URL environment variables are needed in a standard setup — everything is resolved through pkg-config and the binary-cache feature. If you do use a non-standard FFmpeg path, set FFMPEG_DIR before building.
Who is it for
- AI video pipelines: model outputs JSX, engine renders video, lowest integration cost
- Web apps: in-browser video rendering/editing, no server needed
- Procedural animation: deterministic GPU-accelerated rendering, consistent across machines
- Batch production: component-based video, swap props = swap a new video
Reference
- JSX Format Reference
- Animation System
- Transitions
- Canvas API
- Three.js Scenes
- Templates
- Design Principles
Community
- Bugs / Feature requests → Open an Issue
- Linux Do Discussion → OpenCat Community
Star History
License
MIT License