From 4d0de37b482d8d0c9276f623996e2fc7e3062642 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Tue, 22 Sep 2026 11:09:29 +0200 Subject: [PATCH] docs(skills): correct the list of components that render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heading (line 40) and list (line 44) name `shape, image, icon, svg, video, gif, callout, chart, comparison, dot_map, gauge, heatmap, lottie, marquee, mockup, pill_nav, skeleton, sparkline, stat, stepper, tag_cloud, tooltip, treemap` as having no size source, "verified against ... `component_intrinsic` and `apply_intrinsic_overrides`". box_builder.rs:1605-1955 now gives every one of those a default size — the block's own comment cites this exact issue ("#126 / W3: the 23 components with no size source") — and box_builder.rs:2883 asserts each gets a positive box. This file is the LLM-facing generation guide shipped inside the published crate: it makes the model add redundant explicit `width`/`height` everywhere and, worse, line 48-50 tells it to suspect this list first when a component doesn't render, sending debugging down a dead path. The doc even claims `stat` in a flex row produces a blank frame, which the `Stat(_) => apply_default_size(css, 280.0, 180.0)` arm (box_builder.rs:1817-1825) was written specifically to fix. Refs #220 --- README.md | 158 ++++++++++++++---- .../skills/rules/card-flex-layout.md | 32 +++- 2 files changed, 155 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f9b34b6a..15f3dd20 100644 --- a/README.md +++ b/README.md @@ -87,19 +87,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 ` | Path to the JSON scenario file | (required) | +| `--report ` | 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 ` | Load variable overrides from a JSON object file | | +| `--var ` | 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 ` | Path to the JSON scenario file (or `--json ` for inline input) | (required) | | `-o, --output` | Output file path | `output.mp4` | | `--frame ` | Render a single frame to PNG (0-indexed) | | +| `--frames ` | 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 ` | Video codec: `h264`, `h265`, `vp9`, `prores` | `h264` | | `--crf <0-51>` | Constant Rate Factor (lower = better quality) | `23` | | `--format ` | 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 ` | Load variable overrides from a JSON object file | | +| `--var ` | 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 ` | 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 ` | Path to the JSON scenario file | (required) | +| `-o, --output` | Output file path | `still.png` | +| `--time ` | Time to capture | `0.0` | +| `--format ` | 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 ` | Path to the scenario template (JSON or HTML dialect) | (required) | +| `--data ` | JSONL file, one object of variable overrides per line | (required) | +| `--output-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 ` | 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 `. See [Claude Code Skills](#claude-code-skills). + +### `rustmotion completions` + +Generates or installs shell completions — `install`, `uninstall`, `generate `. See [Shell Completions](#shell-completions). --- @@ -2025,42 +2123,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 diff --git a/crates/rustmotion/skills/rules/card-flex-layout.md b/crates/rustmotion/skills/rules/card-flex-layout.md index e22eb345..c2908579 100644 --- a/crates/rustmotion/skills/rules/card-flex-layout.md +++ b/crates/rustmotion/skills/rules/card-flex-layout.md @@ -37,15 +37,37 @@ Children flow in the flexbox. Use `positioned` container for absolute positionin **Grid sizing:** `height: "auto"` on a grid container sizes correctly to content — you don't need an explicit `height` just to avoid stretching. See [rules/grid-card-height.md](rules/grid-card-height.md). -## 23 component types have no intrinsic size — they need explicit `width`/`height` +## 23 component types have no *intrinsic* size — they fall back to a documented default -Most components either measure their own content (`text`, `codeblock`, `counter`, `badge`, `table`, `terminal`, `caption`, `kbd`, `gradient_text`, `rich_text`) or get a computed fallback size from their own fields (`icon`-like shapes such as `avatar`, `divider`, `line`, `arrow`, `switch`, `slider`, `progress`, `list`, `timeline`, `notification`, `rating`, `qr_code`, `countdown`, `particle`, `cursor`, `connector`, `waveform`, `audio_spectrum`). The following **23 types have neither** (verified against `crates/rustmotion-components/src/box_builder.rs`'s `component_intrinsic` and `apply_intrinsic_overrides` — both are exhaustive `match`es and these fall through to their `_ => None` / `_ => {}` arms, and no component overrides `Painter::intrinsic_size` either): +Most components either measure their own content (`text`, `codeblock`, `counter`, `badge`, `table`, `terminal`, `caption`, `kbd`, `gradient_text`, `rich_text`) or get a computed fallback size from their own fields (`icon`-like shapes such as `avatar`, `divider`, `line`, `arrow`, `switch`, `slider`, `progress`, `list`, `timeline`, `notification`, `rating`, `qr_code`, `countdown`, `particle`, `cursor`, `connector`, `waveform`, `audio_spectrum`). The following **23 types have no intrinsic measurement** (they fall through `component_intrinsic`'s `_ => None` arm and don't override `Painter::intrinsic_size`), but every one of them gets a **default `width`/`height` applied by `apply_intrinsic_overrides`** in `crates/rustmotion-components/src/box_builder.rs` whenever the JSON doesn't already set `style.width`/`style.height` — an explicit size is an *override*, not a requirement: -`shape`, `image`, `icon`, `svg`, `video`, `gif`, `callout`, `chart`, `comparison`, `dot_map`, `gauge`, `heatmap`, `lottie`, `marquee`, `mockup`, `pill_nav`, `skeleton`, `sparkline`, `stat`, `stepper`, `tag_cloud`, `tooltip`, `treemap` +| Component | Default size | Where it comes from | +|---|---|---| +| `sparkline` | 120×40 | fixed convention | +| `stat` | 280×180 | fixed convention | +| `gauge` | square, `2·(88 + track_width/2 + 4)` | derived from its own `track_width` field | +| `dot_map` | 640×320 (2:1) | fixed (equirectangular aspect) | +| `comparison` | 520×280 | fixed convention | +| `treemap` | 416×368 | fixed convention | +| `chart` | 320×320 (pie/donut/radar/radial_bar) or 400×300 (other 8 types) | fixed, by chart shape | +| `mockup` | per `device` (e.g. 320×690 for iphone/android, 640×400 laptop, 640×360 browser) | fixed, by device aspect | +| `icon` | 64×64 | fixed convention | +| `svg` | 200×200 | fixed convention | +| `shape` | 80×80 | fixed convention | +| `image` | 400×300 (4:3) | fixed convention | +| `video`, `gif` | 400×225 (16:9) | fixed convention | +| `lottie` | 300×300 | fixed convention | +| `skeleton` | 400×200 (rectangle) / 64×64 (circle) / `240×(lines·line_height + gaps)` (text) | fixed, or derived from its own `lines`/`line_height`/`line_gap` for the `text` variant | +| `marquee` | 800×`2·font_size` | fixed width, height derived from its own `font_size` | +| `heatmap` | derived from `data` rows/cols and `cell_size`/`cell_gap` | fully content-derived | +| `callout`, `tooltip` | derived from the measured text width + padding + arrow | fully content-derived | +| `pill_nav` | derived from each item's measured label width + padding + gap | fully content-derived | +| `stepper` | derived from `node_size` and each step's label/description width | fully content-derived | +| `tag_cloud` | derived from each tag's weighted font size, wrapped at a conventional content width | fully content-derived | -As a flex/grid child with no explicit `style.width`/`style.height`, any of these lays out at **0×0 and renders nothing** — not a smaller-than-expected box, no pixels at all. Confirmed by rendering: three `stat`s in a flex-row card with no explicit size produce a blank frame. `rustmotion validate` does not flag this (a 0×0 box doesn't overflow anything). +A default only fills in the axis that's actually missing — `apply_default_size` respects an explicit `width` or `height` (and derives the other one from `style.aspect-ratio` when only one is set). Explicit `style.width`/`style.height` is still worth setting whenever the default doesn't match the layout you want (e.g. a `stat` narrower than 280px in a tight row), but omitting it no longer produces a blank frame — three `stat`s in a flex-row card with no explicit size now lay out at 280×180 each, confirmed by `box_builder.rs`'s own tests. -Always give these components explicit `style.width`/`style.height` (or a fixed size via their own dedicated `size` field where one exists, e.g. `qr_code`'s `size`) wherever they're a flow child of a `card`/`flex`/`grid` — not just when the parent has `height: "auto"`. If a component silently doesn't render, check this list before assuming a schema-field-placement bug (see [rules/component-field-placement.md](rules/component-field-placement.md) for that other, more common cause of invisible components). +If a component isn't showing up despite that, look at [rules/component-field-placement.md](rules/component-field-placement.md) first — schema-field misplacement is the more common cause of an invisible component. **GOOD** (icon + text row): ```json