rust-motion-animation 1.0.0

Animation primitives for Rust Motion — interpolate, spring, easing (Remotion-compatible)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
<p align="center">
  <img src="./assets/logo.svg" alt="Rust Motion" width="100%" />
</p>

<p align="center">
  <b>Rust Motion 1.0 is a stable, deterministic, browser-free programmatic video rendering platform for Rust, Python, and coding agents, with optional browser and GPU backends for explicitly documented capabilities.</b>
</p>

<p align="center">
  <a href="https://github.com/sjkim1127/rust-motion/actions/workflows/ci.yml"><img src="https://github.com/sjkim1127/rust-motion/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
  <a href="#license"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-4ec9b0?style=flat-square" alt="License" /></a>
  <a href="https://dioxuslabs.com/"><img src="https://img.shields.io/badge/Dioxus-0.6-e05c4b?style=flat-square&logo=rust&logoColor=white" alt="Dioxus 0.6" /></a>
  <img src="https://img.shields.io/badge/status-production--ready-22c55e?style=flat-square" alt="Status: Production Ready" />
</p>

### 📚 Documentation

- [Quickstart Guide](docs/quickstart.md) — Get up and running in under 5 minutes.
- [Hello World Tutorial](docs/hello-world.md) — Step-by-step composition tutorial.
- [CLI & Agent Reference](docs/cli.md) — Complete CLI reference, exit codes, and JSON automation.
- [Python Guide](docs/python.md) — Python bindings, NumPy buffer exports, and usage.
- [Backend Capabilities](docs/backends.md) — Determinism guarantees, native CPU reference, GPU, and browser tiers.
- [Project Format](docs/project-format.md) — `.rust_motion.json` schema v1 specification.
- [API Stability](docs/stability.md) — SemVer 1.x policy, MSRV, and OS support matrix.
- [Security & Advisories](docs/security.md) — Threat model, sandboxing, and reachability analysis.
- [1.0 Migration Guide](docs/migration-1.0.md) — Migration steps from pre-1.0 and Remotion.
- [Release Runbook](docs/release-runbook.md) — Official release engineering procedures.

---

## 🚀 Installation

### 🤖 For AI Coding Agents & Automated CI (Recommended)
Installs the pre-compiled standalone binary in 1–2 seconds with zero external toolchain dependencies:
```bash
curl -fsSL https://raw.githubusercontent.com/sjkim1127/rust-motion/main/scripts/install.sh | bash
```
> Installs to `~/.local/bin/rust-motion` by default. Set `RUST_MOTION_INSTALL_DIR` to customize destination.

### 🍺 Homebrew (macOS)
```bash
brew tap sjkim1127/tap
brew install rust-motion
```

### 🐍 Python Package (PyPI)
```bash
pip install rust-motion
# or using uv:
uv tool install rust-motion
```

### 🦀 Cargo (Rust Developers)
```bash
# Fast binary install via cargo-binstall:
cargo binstall rust-motion-cli

# Or compile from source:
cargo install rust-motion-cli --features rhai
```

---

Rust Motion is a production-quality programmatic video rendering platform. Its native export path renders a registered `Composition` into a deterministic scene graph, rasterizes frames with `tiny-skia` or `wgpu`, and streams a bounded window of ordered RGBA frames to FFmpeg while rendering subsequent frames. Local video frames are decoded through FFmpeg, and declared audio tracks are mixed into the encoded output. The same native scene can be previewed in the Dioxus Player through `NativeCompositionPreview` or edited in the interactive Studio shell.

The repository also contains Dioxus timeline, media, shape, transition, player, and Studio-preview components. The opt-in `rust-motion-vdom` adapter can translate ordinary Dioxus elements, a documented CSS subset, text, local media elements, and basic SVG shapes into the native scene graph. Existing explicit `Scene` and `SceneEmitter` APIs remain available for precise rendering control.

## What works today

- Native scene graph with rectangles, circles, paths, shaped text, local raster images, decoded video frames, audio tracks, gradients, and transformed groups.
- CPU rendering through `tiny-skia`.
- Experimental GPU rendering through `wgpu` for rectangles, circles, tessellated paths, strokes, transformed groups, gradients with up to 16 stops, cached image/video/Lottie textures, and text atlas draws; unsupported node combinations still use CPU fallback.
- Bounded-memory parallel frame rendering into an FFmpeg stdin pipe, including
  persistent Chromium workers for Browser compositions.
- Cached FFprobe metadata and persistent, bounded FFmpeg rawvideo decoder sessions.
- Registry-based Rust compositions and optional sandboxed Rhai compositions, both with JSON props.
- Named frame events for retiming compositions against voice-over audio in Studio; see the [time event guide](docs/time-events.md).
- Shared native composition contract for CLI export and Dioxus Player/Studio preview.
- Dioxus 0.6 `VirtualDom` mutation renderer with block, Flexbox, and Grid layout through Taffy.
- Composable `SceneEmitter` adapters for media, procedural shapes, kinetic captions, fitted multiline text, fades, slides, sequences, freezes, and composited layers.
- Player media synchronization for seek, pause/play, buffering, volume, rate, looping, timeline offsets, and drift correction.
- H.264, H.265, VP9, AV1, ProRes, and GIF video output plus direct PNG, JPEG, and WebP still rendering.
- FFmpeg audio trim, timeline delay, volume, playback-rate, looping, and multi-track mixing with container-appropriate AAC, Opus, or PCM muxing.
- Inclusive frame-range rendering, per-frame progress events, timeout control, and cancellation through the library API or CLI `Ctrl-C`.
- Animation, shape, path, caption, noise, timeline, player, server, encoder, and CLI test coverage.
- Dioxus web example and desktop Studio preview shell.

## Architecture

```text
Native export
  RenderRequest
      -> CompositionRegistry, VdomComposition, or compiled Rhai AST
      -> Composition::prepare(props, context)
      -> PreparedComposition::render(frame)
      -> Scene
      -> TinySkiaBackend / WgpuBackend with CPU fallback
      -> bounded ordered RGBA window
      -> FFmpeg + collected audio tracks
      -> MP4 / WebM / MOV / GIF

Dioxus native-scene preview
  CompositionHandle
      -> Composition::prepare(props, context)
      -> PreparedComposition::render(frame)
      -> SceneView (SVG)
      -> Player / Studio

General Dioxus preview
  Composition / Sequence / Freeze / media / shapes
      -> Player
      -> Dioxus web or desktop UI
```

Native compositions share one `Scene` contract between preview and export. `VdomComposition` crosses the Dioxus/native boundary for its supported DOM and CSS subset, while direct `Scene` and `SceneEmitter` compositions bypass that conversion when exact scene control is preferable.

## Workspace

| Package | Purpose |
|---|---|
| `rust-motion-animation` | Interpolation, easing, springs, and color interpolation |
| `rust-motion-composition` | Shared native composition contract, registry, and built-in composition |
| `rust-motion-core` | Dioxus composition timeline, sequence, freeze, Lottie, and hooks |
| `rust-motion-media` | Native media helpers and scene emitters, with optional Dioxus image, video, and audio components |
| `rust-motion-player` | Interactive player, controls, and native Scene preview adapter |
| `rust-motion-shapes` | Procedural SVG paths and optional Dioxus components |
| `rust-motion-paths` | SVG path parsing, metrics, and transforms |
| `rust-motion-captions` | Subtitle parsing, native scene captions, and optional Dioxus components |
| `rust-motion-noise` | Deterministic simplex noise helpers |
| `rust-motion-transitions` | Dioxus fade and slide transitions |
| `rust-motion-rasterizer` | Scene IR, CPU renderer, experimental GPU renderer, FFmpeg pipe |
| `rust-motion-vdom` | Dioxus VDOM mutation renderer, CSS cascade, Taffy layout, and Scene conversion |
| `rust-motion-renderer` | Static server and PNG-sequence encoding utilities |
| `rust-motion-cli` | Render command and Rhai composition runtime |
| `apps/example` | Dioxus web composition preview |
| `apps/studio` | Dioxus desktop preview shell |

## Prerequisites

- A current stable Rust toolchain.
- FFmpeg available on `PATH` for video and GIF output. Direct still rendering does not require FFmpeg.
- A supported native GPU only when using `--backend gpu`.

### Backend selection

The renderer uses one shared timeline, props, frame scheduling, audio, and
encoding contract across hosts. Choose the backend according to the scene:

| Backend | Best for | Current trade-off |
| --- | --- | --- |
| `native` | Fast, low-memory Rust rendering and AI-generated short scenes | Complex media, text, and browser APIs use the CPU renderer |
| `gpu` | Native shapes, gradients, SVG paths, cached image/video/Lottie textures, and text atlas draws | Unsupported shaders and complex compositing combinations still fall back to CPU |
| `browser` | Tauri/Chromium, Three.js, WebGL, Canvas, and Remotion-compatible web APIs | Requires a browser worker and has process/transport overhead |

Media, shapes, and captions expose Dioxus components through an optional
Cargo feature. Their component API remains enabled by default for compatibility,
while the native CLI and Python binding disable those features so headless native
renders do not depend on Dioxus. Dioxus app dependencies enable the component
feature explicitly. Native GPU texture and text paths are being expanded only
when their output can be checked against CPU and browser references.

Install FFmpeg on common platforms:

```bash
brew install ffmpeg                 # macOS
sudo apt-get install -y ffmpeg      # Debian / Ubuntu
choco install ffmpeg -y             # Windows
```

## Quickstart

The standalone CLI ships with the `HelloWorld` native composition:

```bash
printf '%s\n' '{
  "title": "Hello Rust Motion",
  "subtitle": "Bounded native rendering",
  "background_start": "#0f172a",
  "background_end": "#1e1b4b",
  "accent_color": "#6c63ff"
}' > props.json

cargo run -p rust-motion-cli -- render \
  --composition HelloWorld \
  --props props.json \
  --output output.mp4 \
  --width 1280 \
  --height 720 \
  --fps 30 \
  --duration 150
```

An unknown composition ID or malformed props file fails before FFmpeg starts.

## Rhai compositions

The optional `rhai` feature adds scriptable composition logic while preserving
JSON as the external data contract. Scripts are compiled once per render job,
receive `ctx` and `props`, and return a restricted native scene builder:

```bash
cargo run -p rust-motion-cli --features rhai -- render \
  --script examples/hello.rhai \
  --props examples/hello-props.json \
  --output rhai-output.mp4 \
  --width 1280 \
  --height 720 \
  --duration 150
```

Each script defines `fn render(ctx, props)`. The context contains `frame`,
exposes `scene()`, `rect`, `round_rect`, `circle`, `text`, `text_bold`, `text_font`, `text_box`, `image`,
`video`, `audio`, `group`, and `interpolate`. Image and video nodes accept local
paths or `file://` URIs and the fit values `cover`, `contain`, `fill`, `none`, and
`scale-down`:

```rhai
output.image(x, y, width, height, "card.png", "contain", 1.0);
output.text_font(x, y, "Pinned font", 48.0, "#ffffff", "assets/Inter-Regular.ttf");
output.text_box(x, y, width, height, "Wrapped text", 48.0, 20.0, 3, "#ffffff", "assets/Inter-Regular.ttf", "center");
output.video(x, y, width, height, "clip.mp4", source_time, "cover", 1.0);
output.video(x, y, width, height, "clip.mp4", source_time, "cover", 1.0, true); // loop
output.audio("clip.mp4", source_offset, timeline_offset, duration, volume, playback_rate, looped);
```

An audio `duration` of `0.0` means the remainder of the composition. For a
native render, explicit render-level audio should be returned through
`PreparedComposition::audio_tracks()`; otherwise the runtime uses the first
rendered scene as a legacy fallback. See [`examples/hello.rhai`](examples/hello.rhai)
for the basic scene API.

The runtime disables module imports and limits operations, call depth,
expression depth, variables, functions, strings, arrays, and maps. It does not
expose direct filesystem, network, clock, or random APIs. Media nodes can request
local files from the renderer, so hosts accepting untrusted scripts should also
validate or restrict media paths. A new Rhai scope is created for every frame so
parallel rendering does not share mutable script state.

## Registering a native composition

Applications can implement the shared composition contract and pass the same composition to CLI export or `NativeCompositionPreview`:

```rust,ignore
use rust_motion_cli::{execute_render_command_with_registry, RenderRequest};
use rust_motion_composition::{
    CompositionError, CompositionRegistry, NativeComposition, NativeCompositionContext,
};
use rust_motion_rasterizer::{Color, Scene, SceneNode};
use serde_json::Value;

struct TitleCard;

impl NativeComposition for TitleCard {
    fn id(&self) -> &str {
        "TitleCard"
    }

    fn render(
        &self,
        frame: u32,
        _props: &Value,
        context: NativeCompositionContext,
    ) -> Result<Scene, CompositionError> {
        let mut scene = Scene::new();
        scene.push(SceneNode::Rect {
            x: 0.0,
            y: 0.0,
            w: context.width as f32,
            h: context.height as f32,
            fill: Color::rgb(frame as u8, 24, 48),
            stroke: None,
            stroke_width: 0.0,
            corner_radius: 0.0,
        });
        Ok(scene)
    }
}

// Register TitleCard, construct a RenderRequest, then call:
// execute_render_command_with_registry(&request, &registry).await?;
```

## Rendering a Dioxus VDOM natively

`VdomComposition` creates a fresh `VirtualDom` for each frame and implements the
same `NativeComposition` contract used by the CLI, Player, and Studio. This
keeps parallel frame rendering isolated:

```rust,ignore
use dioxus::prelude::*;
use dioxus_core::VirtualDom;
use rust_motion_composition::{CompositionRegistry, NativeCompositionContext};
use rust_motion_vdom::VdomComposition;

#[derive(Clone, PartialEq, Props)]
struct CardProps {
    frame: u32,
}

fn Card(props: CardProps) -> Element {
    rsx! {
        main { class: "card",
            h1 { "Frame {props.frame}" }
            img { src: "assets/poster.png", class: "poster" }
        }
    }
}

let composition = VdomComposition::new(
    "DioxusCard",
    |frame, _props, _context: NativeCompositionContext| {
        VirtualDom::new_with_props(Card, CardProps { frame })
    },
)
.with_css(r#"
    .card {
        display: flex;
        width: 1280px;
        height: 720px;
        padding: 64px;
        gap: 32px;
        background: #0f172a;
        color: white;
    }
    .poster { width: 480px; height: 270px; object-fit: cover; }
"#)?;

let mut registry = CompositionRegistry::new();
registry.register(composition)?;
```

Supported selectors are `tag`, `.class`, `#id`, `*`, comma-separated groups,
and compounds such as `main.card#hero`. The CSS subset covers block, Flexbox,
and Grid sizing and placement; position and inset; margin, padding, and gap;
alignment; solid backgrounds and borders; font size, weight, line height, color,
opacity, overflow clipping, aspect ratio, and media object fit. Inline `style`
and Dioxus style-namespace attributes override stylesheet rules. Complex
combinators, pseudo-selectors, browser APIs, event behavior, and the full CSS
painting model are intentionally outside this adapter.

## Spring playback controls

The vendored Remotion 4.0.495 spring timing behavior is available as native Rust
through `rust_motion_animation` and the `rust_motion_core` re-exports:

```rust
use rust_motion_animation::{measure_spring, spring_with_options, SpringConfig, SpringOptions};

let config = SpringConfig::default();
assert_eq!(measure_spring(30.0, &config, 0.005).unwrap(), 28.0);
let y = spring_with_options(18.5, 30.0, config, SpringOptions {
    from: 100.0,
    to: 0.0,
    delay: 8.0,
    duration_in_frames: Some(30.0),
    reverse: false,
    ..Default::default()
}).unwrap();
```

`spring_with_options` supports fractional and negative frames, custom ranges,
delay, duration scaling, reversal, and a settling threshold. The existing
`spring(frame, fps, config)` signature remains available. `SpringTiming` now
scales its physics curve to its requested transition duration; the example app
uses duration-controlled title scaling and delayed subtitle motion.

Frame evaluation takes constant time; settling durations use a bounded cache
per render thread (64 entries, with no global lock). Both new functions return
`SpringError` for invalid inputs. Settling simulations and supported adjusted
frame indices are limited to 100,000. A measurement threshold of zero returns
infinity; duration scaling and reversal require a threshold strictly between
zero and one. Overshoot clamping applies to normalized progress before range
mapping, so ascending and descending ranges stay within their endpoints. This
intentionally differs from Remotion 4.0.495's clamping for non-unit ranges.
There is no runtime dependency on Node or `vendor`.

Parity fixtures cover 810 spring values and 48 settling durations generated by
executing the vendored TypeScript. Regenerate them from the repository root with
Node 24 using `node crates/animation/tests/fixtures/generate-spring-reference.mjs`;
run them with `cargo test --locked -p rust-motion-animation --test spring_parity`.

## CLI reference

```text
rust-motion render [OPTIONS] (--composition <ID> | --script <PATH>)

  -c, --composition <ID>       Registered composition ID
      --script <PATH>          Rhai composition file; requires feature `rhai`
  -p, --props <PATH>           JSON props file
  -o, --output <PATH>          Output path [default: out.mp4]
      --audio <PATH>           Local audio file to mix; may be repeated
      --width <PX>             Even output width [default: 1920]
      --height <PX>            Even output height [default: 1080]
      --fps <FPS>              Finite positive FPS [default: 30]
      --duration <FRAMES>      Positive frame count [default: 150]
      --backend <BACKEND>      native or gpu [default: native]
      --codec <CODEC>          h264, h265, vp9, av1, prores, gif, png, jpeg, or webp [default: h264]
      --frame-start <FRAME>    First composition frame [default: 0]
      --frame-end <FRAME>      Last composition frame, inclusive
      --timeout-seconds <SEC>  Cancel after a positive timeout
      --crf <VALUE>            H.264/H.265: 0-51; VP9/AV1: 0-63 [default: 18]
      --preset <PRESET>        H.264/H.265 encoder preset [default: fast]
      --hw-accel <MODE>        Hardware encoder: auto, disabled, videotoolbox, nvenc [default: auto]
```

The output extension must match the codec: `.mp4` for H.264/H.265, `.webm` for
VP9/AV1, `.mov` for ProRes, and the matching extension for GIF or still images.
Still codecs render `--frame-start` only; omit `--frame-end` or set it to the
same frame. GIF and still outputs do not accept audio tracks. AV1 selects
`libsvtav1` when available and otherwise uses `libaom-av1`.

## Python SDK (`rust-motion-py`)

Native high-performance video rendering for Python with zero Chromium/Puppeteer overhead:

The native extension uses PyO3 0.29 with the stable Python 3.10 ABI3 target.

```bash
pip install rust-motion
```

```python
import rust_motion

# 1. Render dynamic Rhai motion graphics template (4K / 60 FPS)
rust_motion.render_script(
    "templates/motion.rhai",
    "output.mp4",
    props={"title": "Hello World", "theme": "cyberpunk"},
    width=3840,
    height=2160,
    fps=60.0,
    duration=180,
    hw_accel="videotoolbox"  # Hardware acceleration on Apple Silicon / NVENC on Linux
)

# 2. Render registered Rust composition
rust_motion.render(
    "HelloWorld",
    "clip.mp4",
    width=1920,
    height=1080,
    fps=30.0,
    duration=90
)

# 3. Export still frame thumbnail
rust_motion.render_still("HelloWorld", frame=30, output="thumbnail.png")
```

Build GPU support explicitly:

```bash
cargo build -p rust-motion-cli --features gpu
```

Build Rhai and GPU support together with `--features rhai,gpu`.

## Performance comparisons

New Remotion ports should preserve output and beat the corresponding Remotion
workload before being described as faster. Reproducible spring and MP4-render
comparisons, raw timings, and output checks live in [`benchmarks`](benchmarks/README.md).
The spring benchmark includes Remotion's warm caches; render comparisons reuse
Chromium, share the same FFmpeg binary, and verify pixels and frame counts.
Results apply to the measured scenes and hardware, not all compositions.

## Testing

The project treats default and optional-feature builds as required quality gates:

```bash
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
cargo check --locked --workspace --all-targets --all-features
cargo test --locked --workspace --all-features
```

The render integration tests produce real H.264, H.265, VP9, AV1, ProRes, and
GIF containers when their FFmpeg encoders are installed, and decode direct PNG,
JPEG, and WebP still outputs.

## Releasing

Releases are driven by a version tag such as `v0.2.0`. The release workflow
validates the tag against the Cargo workspace version, tests the complete
workspace, publishes all public `rust-motion-*` packages to crates.io in dependency
order, builds Rhai-enabled CLI archives for Linux, macOS, and Windows, and then
creates one GitHub Release with SHA-256 checksums.

The Python extension wheels and source distribution are built and published to
PyPI by a separate workflow on the same version tag.

Repository maintainers must configure the crates.io API token as the GitHub
Actions secret `CRATES_IO_TOKEN` and the PyPI API token as `PYPI_API_TOKEN`.
Tokens must never be committed, placed in a tag, or written into workflow files.

## Current limitations

- `rust-motion-vdom` translates an explicit DOM/CSS subset, not a browser engine. Complex selectors, intrinsic browser layout, general DOM APIs, events, animations, and advanced paint effects still require direct Scene APIs or further adapter work. Basic CSS linear/radial gradients, translate/scale/rotate transforms, borders, non-inset `box-shadow`, common pixel filters (including `drop-shadow`), `mix-blend-mode`, and the cross-backend `CanvasImage` source fallback are supported; arbitrary canvas/WebGL drawing remains a Browser/Three.js concern.
- Native video and audio sources are local files, while image nodes additionally accept local SVG files and base64 `data:image/png`, `data:image/jpeg`, `data:image/webp`, and `data:image/svg+xml` sources up to 32 MiB. SVG assets are rasterized through the same bounded image cache. Project renders can opt into downloading HTTP(S) assets with `--asset-cache-dir`; the CLI caps each asset at 256 MiB and the project at 1 GiB by default (`--max-total-asset-bytes` changes the total). Direct remote URLs are still not opened by the rasterizer itself.
- Video frames use cached FFprobe stream metadata, up to four persistent FFmpeg decoder sources, fixed-output-FPS sampling for VFR input, and a 128 MiB frame LRU. Backward or large forward seeks restart only the affected decoder.
- Audio declarations may be supplied by `PreparedComposition::audio_tracks()` as
  an explicit render-level timeline, including per-track timing and volume
  keyframes. The CLI scans rendered scenes across the full timeline when that
  method returns `None`, so it includes tracks first emitted after frame zero.
  This legacy fallback renders frames once before encoding; implement
  `audio_tracks()` to avoid the extra pass.
- `SceneLayer` supports rectangular or SVG-path clips, alpha or luminance masks, twelve blend modes, ordered blur/brightness/grayscale/opacity filters, and drop shadows. These effects use CPU offscreen surfaces for export and SVG/CSS equivalents for Player preview.
- GPU acceleration covers rectangles, circles, tessellated path fills and strokes, nested group transforms and opacity, plain normal layers, gradients with up to 16 stops, cached image/video/Lottie textures, and text atlas draws. Unsupported shaders, gradients beyond the stop limit, and some composited mask/blend/filter combinations use CPU fallback; the supported texture/text paths have dedicated CPU/GPU parity tests.
- Text nodes accept ordered local TTF/OTF `font_sources`; native rendering caches those files, shapes glyph runs with Rustybuzz, and falls through per grapheme. `SceneTextBlock` and Rhai `text_box` add Unicode line breaking, fitting, alignment, line limits, and ellipsis. Text without explicit sources still uses platform font discovery and is not pixel-identical across platforms; full mixed-direction paragraph layout remains incomplete.
- Studio project loading and Tauri render-queue controls are available; timeline editing and media management are not yet a full editor.

## Roadmap

1. Expand VDOM/CSS conversion beyond the current simple-selector and native-paint subset.
2. Full bidirectional paragraph layout, variable-font axes, and advanced typography controls.
3. Complete GPU parity for text, media, composited layers, masks, blend modes, and filters.
4. Additional color, distortion, and convolution filter primitives.
5. Studio timeline editing and media management on top of the shared project/job contract.

6. A first-class dynamic audio provider for compositions whose audio topology
   changes over time, without requiring a frame-by-frame scene scan.

## License

Licensed under either [Apache License 2.0](LICENSE) or [MIT License](LICENSE-MIT), at your option.