Skip to content

Scene-graph JSON

The scene graph is plain JSON. This is the exact representation the Rust engine (onda-scene) deserializes, and the format @onda-engine/react emits. You can hand-write it, generate it from an AI, or produce it from React — it all renders the same way.

Field names are snake_case; enums are internally tagged (a discriminant field like "type", "shape", or "gradient").

interface Scene {
composition: Composition
root: SceneNode
}
{
"composition": { "width": 1280, "height": 720, "fps": 60, "duration_in_frames": 120 },
"root": { "kind": { "type": "group" }, "children": [ /* ... */ ] }
}
interface Composition {
width: number
height: number
fps: number
duration_in_frames: number
}
interface SceneNode {
id?: number // omit if not animation-targeted
transform?: Transform // omit for identity
opacity?: number // default 1.0
clip?: ShapeGeometry // omit for none (GPU backend)
kind: NodeKind
children?: SceneNode[] // omit if empty
}

Omitted fields fall back to defaults on the Rust side (opacity → 1.0, etc.).

interface Transform {
translate?: Vec2 // default { x: 0, y: 0 }
scale?: Vec2 // default { x: 1, y: 1 }
}
interface Vec2 { x: number; y: number }

A tagged union on type:

type NodeKind =
| { type: 'group' }
| { type: 'text'; content: string; font_size?: number; color?: Color }
| { type: 'image'; src: string; width?: number; height?: number; fit?: ImageFit }
| { type: 'video'; src: string; time?: number; width?: number; height?: number; fit?: ImageFit }
| { type: 'shape'; geometry: ShapeGeometry; fill?: Color; gradient?: Gradient; stroke?: Stroke }
| { type: 'svg'; src?: string; markup?: string }

ImageFit is 'fill' | 'cover' | 'contain' (default 'cover'). A video node’s time is the source position in seconds to display; the frame is decoded by the player (browser) or onda export (native ffmpeg) and drawn like an image.

Defaults: text.font_size → 48, text.color → white. shape.gradient takes precedence over shape.fill.

A tagged union on shape (also used for clip):

type ShapeGeometry =
| { shape: 'rect'; size: Size; corner_radius?: number } // corner_radius default 0
| { shape: 'ellipse'; size: Size }
| { shape: 'path'; data: string } // SVG path data, local space
interface Size { width: number; height: number }

Straight-alpha sRGB, components in 0..1. a defaults to 1.

interface Color { r: number; g: number; b: number; a?: number }

A tagged union on gradient. Points are in the shape’s local space; stop offsets are 0..1.

type Gradient =
| { gradient: 'linear'; start: Vec2; end: Vec2; stops: GradientStop[] }
| { gradient: 'radial'; center: Vec2; radius: number; stops: GradientStop[] }
interface GradientStop { offset: number; color: Color }
interface Stroke { color: Color; width: number }
{
"composition": { "width": 640, "height": 280, "fps": 30, "duration_in_frames": 1 },
"root": {
"kind": { "type": "group" },
"children": [
{
"kind": {
"type": "shape",
"geometry": { "shape": "rect", "size": { "width": 640, "height": 280 } },
"fill": { "r": 0.04, "g": 0.05, "b": 0.09 }
}
},
{
"transform": { "translate": { "x": 40, "y": 210 } },
"kind": {
"type": "shape",
"geometry": { "shape": "rect", "size": { "width": 360, "height": 12 }, "corner_radius": 6 },
"gradient": {
"gradient": "linear",
"start": { "x": 0, "y": 0 },
"end": { "x": 360, "y": 0 },
"stops": [
{ "offset": 0, "color": { "r": 0.16, "g": 0.45, "b": 0.95 } },
{ "offset": 1, "color": { "r": 0.95, "g": 0.35, "b": 0.55 } }
]
}
}
},
{
"transform": { "translate": { "x": 40, "y": 60 } },
"kind": { "type": "text", "content": "Scene graph", "font_size": 56, "color": { "r": 1, "g": 1, "b": 1 } }
}
]
}
}

Render it:

Terminal window
cargo run -p onda-cli -- render scene.json out.png