Skip to content
Merged

audit #305

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3a156c2
feat(core): anchor vw/vh resolution to the real output viewport
LeadcodeDev Sep 21, 2026
7d676d9
style(lint): replace constant-size chunks_exact with as_chunks
LeadcodeDev Sep 22, 2026
b629e65
Merge remote-tracking branch 'origin/fix/clippy-chunks-exact' into ch…
LeadcodeDev Sep 22, 2026
8444e4e
fix(paint): include box-shadow and descendant ink in the layer bounds…
LeadcodeDev Sep 22, 2026
f46548b
fix(geometry): resolve vw/vh against the scenario viewport (#227)
LeadcodeDev Sep 22, 2026
e34b402
fix(encode): publish the output only once the render succeeds (#234)
LeadcodeDev Sep 22, 2026
58c145e
perf(gif): decode a gif once across rayon workers (#239)
LeadcodeDev Sep 22, 2026
9d4bcb5
fix(background): wrap heropattern scroll on its per-axis period (#248)
LeadcodeDev Sep 22, 2026
1952295
fix(html): reject unknown scene attributes instead of dropping them (…
LeadcodeDev Sep 22, 2026
8358879
fix(studio): rebase the debounced write on the current document (#256)
LeadcodeDev Sep 22, 2026
b01fb1b
fix(assets): set a timeout on every outbound request (#259)
LeadcodeDev Sep 22, 2026
4099977
fix(components): resolve default sizes against the node font-size (#263)
LeadcodeDev Sep 22, 2026
39d9584
fix(css): resolve em against the element font-size (#221)
LeadcodeDev Sep 22, 2026
c8252d1
fix(layout): hand IntrinsicMeasure both sizes in the content box (#284)
LeadcodeDev Sep 22, 2026
1bfa4bc
fix(animator): start the underdamped spring from rest (#285)
LeadcodeDev Sep 22, 2026
4886960
fix(geometry): resolve transform lengths per axis and per font-size (…
LeadcodeDev Sep 22, 2026
91c1868
fix(geometry): check height overflow when white-space is nowrap (#231)
LeadcodeDev Sep 22, 2026
c16ec55
fix(geometry): compare terminal height in a single box model (#232)
LeadcodeDev Sep 22, 2026
03942a1
refactor(geometry): reuse the canonical component_kind helper (#233)
LeadcodeDev Sep 22, 2026
d30f49a
fix(encode): reject a non-positive playback rate (#287)
LeadcodeDev Sep 22, 2026
928f9ef
perf(preload): bound the extracted video frame cache (#288)
LeadcodeDev Sep 22, 2026
c029b9f
fix(render): clear asset caches between watch renders (#242)
LeadcodeDev Sep 22, 2026
9890365
fix(video): loop the picture with the audio (#245)
LeadcodeDev Sep 22, 2026
7298fd8
fix(video): treat decoded frames as unpremultiplied (#247)
LeadcodeDev Sep 22, 2026
05e0074
fix(html): emit lengths the core parser can read (#289)
LeadcodeDev Sep 22, 2026
23c2bed
fix(studio): surface a render panic instead of caching an empty frame…
LeadcodeDev Sep 22, 2026
e20a7d3
fix(assets): restrict the protocols a scenario src may use (#291)
LeadcodeDev Sep 22, 2026
b22b910
fix(components): apply the css cascade before painters read style (#292)
LeadcodeDev Sep 22, 2026
e37518f
fix(paint): orient linear gradients to the css angle convention (#225)
LeadcodeDev Sep 22, 2026
4274f9f
perf(animator): cache the spring settle time (#226)
LeadcodeDev Sep 22, 2026
0e47992
fix(validate): keep asset paths relative when rewriting a scenario (#…
LeadcodeDev Sep 22, 2026
e36dfec
fix(geometry): measure text overflow against the content box (#230)
LeadcodeDev Sep 22, 2026
e1381bd
fix(gif): cap decoded gif dimensions and frame count (#241)
LeadcodeDev Sep 22, 2026
e068341
fix(batch): index the name template by character (#243)
LeadcodeDev Sep 22, 2026
4ac841d
fix(video): honour the fit property (#244)
LeadcodeDev Sep 22, 2026
41689b8
fix(video): stop the picture at trim_end (#246)
LeadcodeDev Sep 22, 2026
0a0d175
refactor(scene): paint decorative fullscreen through the box tree (#283)
LeadcodeDev Sep 22, 2026
477618f
fix(background): canonicalize the heropattern colour before templatin…
LeadcodeDev Sep 22, 2026
3a49519
fix(html): reject unsupported children of text tags (#254)
LeadcodeDev Sep 22, 2026
fbf64bb
fix(html): preserve the document around the rustmotion element (#255)
LeadcodeDev Sep 22, 2026
0f3dc12
fix(expand): bound the total nodes produced by for-each (#261)
LeadcodeDev Sep 22, 2026
a37916f
fix(include): require opt-in for remote includes (#262)
LeadcodeDev Sep 22, 2026
3e5708b
fix(components): paint table columns at their measured widths (#265)
LeadcodeDev Sep 22, 2026
29dbd16
perf(studio): reuse the audio mix across optimistic edits (#258)
LeadcodeDev Sep 22, 2026
fbaf997
fix(components): reach every typographic component with the cascade (…
LeadcodeDev Sep 22, 2026
9ef377f
fix(encode): trust the decoded signal spec for rate and channels (#293)
LeadcodeDev Sep 22, 2026
07b8971
fix(encode): create temp files in a private directory (#294)
LeadcodeDev Sep 22, 2026
4dfed09
docs(readme): rewrite the architecture section to match the tree (#295)
LeadcodeDev Sep 22, 2026
9aacd53
docs(skills): correct the list of components that render (#296)
LeadcodeDev Sep 22, 2026
86a23b8
docs(components): correct the documented component count (#297)
LeadcodeDev Sep 22, 2026
a3e3ca6
ci(workflow): gate the build on cargo audit (#298)
LeadcodeDev Sep 22, 2026
ac8b9b7
ci(publish): pin actions to commit shas (#299)
LeadcodeDev Sep 22, 2026
5066a06
fix(schema): honour the scenario codec field (#300)
LeadcodeDev Sep 22, 2026
d3581da
fix(schema): enforce the declared variable type (#301)
LeadcodeDev Sep 22, 2026
dcf332d
test(schema): fail when a schema field is inert (#302)
LeadcodeDev Sep 22, 2026
c065a50
docs(non-goals): state what rustmotion will not do (#303)
LeadcodeDev Sep 22, 2026
ec6595a
fix(chantier): align the branch with the tree the suite was verified …
LeadcodeDev Sep 22, 2026
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
4 changes: 3 additions & 1 deletion crates/rustmotion-components/src/badge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ mod tests {
// Solid variant text is always white — probe for white ink
// specifically, since the pill background paints regardless.
let text_ink = buf
.chunks_exact(4)
.as_chunks::<4>()
.0
.iter()
.filter(|p| p[3] > 0 && p[0] > 200 && p[1] > 200 && p[2] > 200)
.count();
assert!(
Expand Down
Loading
Loading