Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22
with:
components: rustfmt
- name: Check formatting
Expand All @@ -22,27 +22,74 @@ jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Install system dependencies
# webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop)
# asound: required by cpal, which rodio pulls in for preview audio
run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev libasound2-dev
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Install system dependencies
# webkit/gtk/xdo: required to compile rustmotion-studio (dioxus desktop)
# asound: required by cpal, which rodio pulls in for preview audio
run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev libasound2-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Run tests
run: cargo test --workspace

audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install cargo-audit
run: cargo install cargo-audit --locked
# Blocking on any advisory not listed below — a newly introduced
# vulnerability fails this job. Every `--ignore` is a pre-existing
# transitive-dependency advisory tolerated today because the fix is a
# dependency version bump, and version bumps for the published
# `rustmotion` crate are being handled separately from this workstream
# (crates/rustmotion/Cargo.toml, orchestrator-owned). Unmaintained/
# unsound/yanked advisories (17 as of 2026-09-22) print but do not fail
# the job — that's `cargo audit`'s own default, left unchanged here.
#
# Review by 2026-12-22, or sooner once the dependency bumps land:
# RUSTSEC-2025-0008 — openh264-sys2 0.6.6, heap overflow in decoding.
# Direct dependency of the published `rustmotion` crate. Fix: openh264 >=0.8.0.
# RUSTSEC-2026-0204 — crossbeam-epoch 0.9.18, invalid pointer deref in `fmt::Pointer`.
# Via rayon-core <- exr <- image, reaches rustmotion-core/-components. Fix: >=0.9.20.
# RUSTSEC-2026-0195, RUSTSEC-2026-0194 — quick-xml 0.38.4 / 0.39.4, DoS + quadratic runtime.
# 0.38.4 via syntect reaches the published crates; 0.39.4 via dioxus-desktop/rfd is
# rustmotion-studio-only (Linux/Wayland file dialogs). Fix: >=0.41.0.
# RUSTSEC-2026-0285 — rustls 0.23.37, TLS 1.3 handshake level-boundary bug.
# Via ureq, used by rustmotion/rustmotion-core for Google Fonts + Iconify fetches. Fix: >=0.23.45.
# RUSTSEC-2026-0104, RUSTSEC-2026-0098, RUSTSEC-2026-0099, RUSTSEC-2026-0049 — rustls-webpki
# 0.103.9, four CRL/name-constraint parsing bugs. Same ureq path as rustls above.
# Fix: >=0.103.13,<0.104.0-alpha.1 (or the matching 0.104 alpha per advisory).
# RUSTSEC-2026-0257 — webbrowser 1.2.1, BROWSER env argument injection on Unix.
# Via dioxus-desktop, rustmotion-studio only (`publish = false`, never reaches a published
# crate). Fix: >=1.2.2.
- name: Audit dependencies
run: >
cargo audit
--ignore RUSTSEC-2025-0008
--ignore RUSTSEC-2026-0204
--ignore RUSTSEC-2026-0195
--ignore RUSTSEC-2026-0194
--ignore RUSTSEC-2026-0285
--ignore RUSTSEC-2026-0104
--ignore RUSTSEC-2026-0098
--ignore RUSTSEC-2026-0099
--ignore RUSTSEC-2026-0049
--ignore RUSTSEC-2026-0257
4 changes: 2 additions & 2 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Install system dependencies
# Doit rester identique à ci.yaml : l'étape « Run tests » ci-dessous lance
Expand All @@ -20,7 +20,7 @@ jobs:
# asound: required by cpal, which rodio pulls in for preview audio
run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev libwebkit2gtk-4.1-dev libgtk-3-dev libxdo-dev libasound2-dev

- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable channel, 2026-09-22

# La version se lit via `cargo metadata`, pas en grepant un manifeste : elle
# est déclarée dans `[workspace.package]` et héritée, donc un `grep
Expand Down
163 changes: 131 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ A CLI tool that renders motion design videos from JSON scenarios. No browser, no
[![docs.rs](https://docs.rs/rustmotion/badge.svg)](https://docs.rs/rustmotion)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

MIT-licensed: no licence key, no telemetry, no per-render billing. See [Non-goals](docs/non-goals.md) for this and everything else rustmotion deliberately doesn't do (no embeddable Player/browser/React, no vendor-cloud deploy target, ...).

## Install

```bash
Expand Down Expand Up @@ -87,19 +89,117 @@ Once installed, Claude Code automatically loads the skills when you work in that

## CLI Reference

### `rustmotion validate`

Schema + geometry checks, with no render. This is the gate every generated scenario is expected to pass before use.

| Flag | Description | Default |
|---|---|---|
| `-f, --file <FILE>` | Path to the JSON scenario file | (required) |
| `--report <FILE>` | Write a machine-readable JSON report of all violations | |
| `--fix` | Auto-fix safe violations in place (`auto_scroll: true`, drop `white-space` back to wrap, `text-autofit: true`) — refuses templated scenarios (`include`/`for-each`/`use`) | `false` |
| `--strict-anim` | Sample animated frames and reapply renderer transforms to detect per-frame viewport overflow (slower) | `false` |
| `--lenient` | Treat geometry violations as warnings instead of errors | `false` |
| `--props <FILE>` | Load variable overrides from a JSON object file | |
| `--var <KEY=VALUE>` | Set a single variable override (repeatable); `--var` wins over `--props` | |

### `rustmotion render`

| Flag | Description | Default |
|---|---|---|
| `input` | Path to the JSON scenario file | (required) |
| `-f, --file <FILE>` | Path to the JSON scenario file (or `--json <STRING>` for inline input) | (required) |
| `-o, --output` | Output file path | `output.mp4` |
| `--frame <N>` | Render a single frame to PNG (0-indexed) | |
| `--frames <START-END>` | Render only frames `START..=END` as a standalone segment with its own windowed audio slice, for joining later with `rustmotion concat`. Mutually exclusive with `--frame`/`--watch`; only mp4/webm/mov are implemented for a range | |
| `--codec <CODEC>` | Video codec: `h264`, `h265`, `vp9`, `prores` | `h264` |
| `--crf <0-51>` | Constant Rate Factor (lower = better quality) | `23` |
| `--format <FMT>` | Output format: `mp4`, `webm`, `mov`, `gif`, `png-seq` | auto from extension |
| `--transparent` | Transparent background (PNG sequence, WebM, ProRes 4444) | `false` |
| `--hardware-acceleration` | Probe `ffmpeg -encoders` and use this machine's hardware encoder (VideoToolbox/NVENC/QSV/AMF) when available; explicit message and software fallback otherwise | `false` |
| `-w, --watch` | Watch the input file and re-render on change (not compatible with `--props`/`--var`) | `false` |
| `--no-validate` | Skip the implicit validate pass (schema + geometry + variables) before rendering | `false` |
| `--lenient` | Treat geometry violations as warnings during the implicit validate pass | `false` |
| `--strict-anim` | Sample animated frames for per-frame viewport overflow during the implicit validate pass | `false` |
| `--props <FILE>` | Load variable overrides from a JSON object file | |
| `--var <KEY=VALUE>` | Set a single variable override (repeatable); `--var` wins over `--props` | |
| `--output-format json` | Machine-readable JSON output for CI pipelines | |
| `-q, --quiet` | Suppress all output except errors | |
| `--threads <N>` | Number of parallel rendering threads (global flag) | all cores |

### `rustmotion concat`

Joins segment files — e.g. several `render --frames a-b` outputs from the same scenario — via ffmpeg's concat demuxer (`-c copy`, no re-encoding). Requires ffmpeg on PATH.

```bash
rustmotion concat seg1.mp4 seg2.mp4 -o out.mp4
```

### `rustmotion still`

Exports a single frame as a still image (PNG/JPEG/WebP).

| Flag | Description | Default |
|---|---|---|
| `-f, --file <FILE>` | Path to the JSON scenario file | (required) |
| `-o, --output` | Output file path | `still.png` |
| `--time <SECONDS>` | Time to capture | `0.0` |
| `--format <FMT>` | Image format: `png`, `jpeg`, `webp` | from extension |
| `--quality <1-100>` | JPEG quality | `90` |
| `--props` / `--var` | Variable overrides, same as `render` | |

### `rustmotion captions`

Generates word-level caption timings from audio (via a local `whisper.cpp` binary) or by importing subtitles.

```bash
rustmotion captions voice.mp3 -o words.json
rustmotion captions --from-srt subs.srt -o words.json
```

| Flag | Description | Default |
|---|---|---|
| `audio` | Audio file to transcribe (mutually exclusive with `--from-srt`/`--from-vtt`) | |
| `-o, --output` | Output JSON file (stdout if omitted) | |
| `--model` | Whisper model name (`tiny`, `base`, `small`, `medium`, `large-v3`) or a path to a `.bin` | `base` |
| `--lang` | Spoken language code (auto-detected if omitted) | |
| `--from-srt` / `--from-vtt` | Import cues from a subtitle file instead of transcribing | |

### `rustmotion batch`

Renders one video per line of a JSONL data file, substituting each line's fields as variable overrides.

| Flag | Description | Default |
|---|---|---|
| `-f, --file <FILE>` | Path to the scenario template (JSON or HTML dialect) | (required) |
| `--data <FILE>` | JSONL file, one object of variable overrides per line | (required) |
| `--output-dir <DIR>` | Directory to write output files into | (required) |
| `--name-template` | Output filename template (`{field}`, `{index}`) | `"{index}.mp4"` |
| `--codec` / `--crf` / `--format` / `--transparent` | Same as `render` | |
| `--jobs <N>` | Videos to render in parallel (the render itself already uses all cores via rayon) | `1` |

### `rustmotion schema`

Prints the JSON Schema for scenario files (editor autocompletion, LLM prompts).

```bash
rustmotion schema -o schema.json
```

### `rustmotion info`

Shows information about a scenario (duration, scene count, dimensions, ...).

```bash
rustmotion info scenario.json
```

### `rustmotion skills`

Manages the built-in Claude Code skills — `install [--global]`, `uninstall [--global]`, `list`, `show <name>`. See [Claude Code Skills](#claude-code-skills).

### `rustmotion completions`

Generates or installs shell completions — `install`, `uninstall`, `generate <shell>`. See [Shell Completions](#shell-completions).

---

Expand All @@ -123,7 +223,6 @@ Once installed, Claude Code automatically loads the skills when you work in that
"height": 1920,
"fps": 30,
"background": "#0f172a",
"codec": "h264",
"crf": 23
}
}
Expand All @@ -135,7 +234,7 @@ Once installed, Claude Code automatically loads the skills when you work in that
| `height` | `u32` | (required) | Video height in pixels (must be even) |
| `fps` | `u32` | `30` | Frames per second |
| `background` | `string` | `"#000000"` | Default background color (hex) |
| `codec` | `string` | `"h264"` | Video codec: `h264`, `h265`, `vp9`, `prores` |
| `codec` | `string` | | Accepted by the schema (`h264`, `h265`, `vp9`, `prores`) but not yet read by the encoder — set the codec with `render --codec`/`batch --codec` instead |
| `crf` | `u8` | `23` | Constant Rate Factor (0-51, lower = better quality) |

### Audio Tracks
Expand Down Expand Up @@ -2025,42 +2124,42 @@ Transparency is supported with `--transparent` for PNG sequences, WebM (VP9), an
- **JSON Schema:** schemars (auto-generated from Rust types)
- **Parallelism:** rayon (multi-threaded frame rendering)

## Architecture
rustmotion ships 60 components, each implementing the `Painter` trait, through a CSS-inspired **box_tree → layout_pass → paint_pass** pipeline:

rustmotion uses a Flutter-inspired **measure → layout → paint** pipeline built on Skia:
1. **box_tree** — builds a tree of `BoxNode { css: CssStyle, children, intrinsic }` from the resolved JSON components
2. **layout_pass** — runs [taffy](https://github.com/DioxusLabs/taffy) to compute each node's `BoxLayout { x, y, width, height }`; leaves that carry an `IntrinsicMeasure` (text, images, codeblocks, ...) are measured through a `measure_fn`
3. **paint_pass** — walks the tree top-down, applies transform/opacity, paints decorations (background, border, shadow), and delegates content painting to the component's `Painter` implementation

```
src/
├── components/ # 51 components (each implements Widget trait)
│ ├── chart/ # Chart sub-modules (bar, line, pie, radar, etc.)
│ └── *.rs # One file per component
├── engine/
│ ├── render/ # Render pipeline (component, scene, background, transforms)
│ ├── codeblock/ # Codeblock rendering (highlight, chrome, reveal, diff)
│ ├── animator.rs # Animation resolver, easing, spring solver
│ └── renderer.rs # Skia drawing primitives
├── schema/ # Data models
│ ├── scenario.rs # Scenario, View, Scene, VideoConfig
│ ├── style.rs # LayerStyle, FontWeight, layout types
│ ├── background.rs # Animated backgrounds
│ ├── animation.rs # EasingType, presets
│ └── video.rs # AnimationEffect, shapes, fills
├── layout/ # Flex/grid layout engines
├── traits/ # Widget, Styled, Animatable, Timed, Container
└── macros.rs # impl_traits! macro
```rust
pub trait Painter {
fn paint_content(&self, canvas: &Canvas, layout: &BoxLayout, props: &AnimatedProperties, ctx: &PaintCtx);
fn intrinsic_size(&self, available: AvailableSize, ctx: &MeasureCtx) -> Option<(f32, f32)> { None }
}
```

Every component implements the `Widget` trait:
`PaintCtx` carries `time`, `scene_duration`, `fps`, `frame_index`, `video_width`, `video_height`, `stagger_offset`.

```rust
trait Widget {
fn paint(&self, canvas: &Canvas, ctx: &PaintContext) -> Result<()>;
fn measure(&self, constraints: &Constraints) -> (f32, f32);
fn layout(&self, constraints: &Constraints) -> LayoutNode;
}
### Workspace layout

```
crates/
├── rustmotion-core/src/
│ ├── css/ # CssStyle, units, cascade, taffy bridge, animation resolution
│ ├── engine/ # box_tree, layout_pass, paint_pass, animator, transitions, Skia primitives
│ ├── schema/ # Scenario, Scene, VideoConfig, style, background, animation, codeblock models
│ └── traits/ # Painter, Animatable, Timed, Styled
├── rustmotion-components/src/
│ ├── lib.rs # `Component` enum (60 variants) + dispatch (as_painter, as_animatable, ...)
│ ├── box_builder.rs # JSON components → BuiltScene (box tree + stagger delays)
│ ├── chart/ # bar/line/pie/radar/scatter/radial/funnel/waterfall sub-modules
│ └── *.rs # one file per component (Painter implementation)
└── rustmotion/src/
├── cli/ # the `rustmotion` binary (clap subcommands)
├── encode/ # video/audio encoders and muxing
└── loader.rs # JSON/HTML → ResolvedScenario
```

`PaintContext` provides timing, layout dimensions, parent info, and resolved animated properties in a single struct.
The `rustmotion` crate is where the binary lives — a crate with only a `[lib]` target installs nothing executable via `cargo install`.

## License

Expand Down
2 changes: 1 addition & 1 deletion crates/rustmotion-components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rustmotion-components"
version.workspace = true
edition = "2021"
description = "Component library for rustmotion (51 components)"
description = "Component library for rustmotion (60 components)"
license = "MIT"
repository = "https://github.com/LeadcodeDev/rustmotion"
readme = "../../README.md"
Expand Down
Loading