Skip to content
Merged
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
154 changes: 90 additions & 64 deletions .claude/skills/rustmotion/SKILL.md

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions .claude/skills/rustmotion/rules/color-palettes.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,28 +95,29 @@ Pick the palette in Phase 2. Every scene, every card, every text element uses co

### Rule P2: Use $ref for backgrounds

If the same gradient background appears in multiple scenes, define it once and reference it:
If the same gradient background appears in multiple scenes, define it once and reference it. Entries under the top-level `backgrounds` map are stored as raw JSON and only get type-checked when a scene actually resolves the `$ref` — at that point they must deserialize as an `AnimatedBackground`, which **requires a `preset`** (`gradient_shift`, `grid_dots`, `concentric_circles`, `halo`, or `heropattern`) plus that preset's config nested under its own name key. A template shaped like `{ "type": "gradient", "gradient": {...} }` is not an `AnimatedBackground` at all — `rustmotion validate` does not catch this (the `backgrounds` map and `background.$ref` overrides are permissive `serde_json::Value`s at the schema level), so the file validates clean but **renders a flat, ungra­dient background** at render time:

```json
{
"backgrounds": {
"dark_radial": {
"type": "gradient",
"gradient": {
"type": "radial",
"preset": "gradient_shift",
"gradient_shift": {
"colors": ["#1e1b4b", "#0f172a"],
"center_x": 0.5,
"center_y": 0.4
}
"gradient_type": "radial"
},
"speed": 10
}
},
"scenes": [
{ "background": { "$ref": "dark_radial" }, ... },
{ "background": { "$ref": "dark_radial" }, ... }
{ "background": { "$ref": "dark_radial" }, "duration": 3.0, "children": [] },
{ "background": { "$ref": "dark_radial" }, "duration": 3.0, "children": [] }
]
}
```

`speed` defaults to `0` (static) if omitted — set it low for a near-static gradient, higher for a visibly rotating one (degrees/sec for `gradient_shift`). Per-scene overrides merge on top of the template (e.g. `{ "$ref": "dark_radial", "gradient_shift": { "colors": [...] } }` swaps just the colors) — everything except `$ref` and `transition` deep-merges into the referenced template.

### Rule P3: Accent color as the single highlight

Use the accent color for: CTAs, icon fills, badge backgrounds, glow effects, border highlights.
Expand Down
10 changes: 6 additions & 4 deletions .claude/skills/rustmotion/rules/depth-layering.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,18 @@ Background elements feel farther away when slightly blurred. Use `backdrop-filte

## Scale + opacity gradient across siblings

When cards or icons are in a row, give back-row items a smaller scale and lower opacity to simulate perspective receding.
When cards or icons are in a row, give back-row items a smaller scale and lower opacity to simulate perspective receding. `transform` is an **array of tagged transform functions** (`{ "fn": "scale", "x": ..., "y": ... }`), never a CSS string like `"scale(0.82)"` — a string fails to deserialize and drops the component.

```json
[
{ "type": "card", "style": { "opacity": 0.4, "transform": "scale(0.82)" }, "..." : "back" },
{ "type": "card", "style": { "opacity": 0.7, "transform": "scale(0.91)" }, "..." : "mid" },
{ "type": "card", "style": { "opacity": 1.0, "transform": "scale(1.00)" }, "..." : "front" }
{ "type": "card", "style": { "opacity": 0.4, "transform": [{ "fn": "scale", "x": 0.82, "y": 0.82 }] } },
{ "type": "card", "style": { "opacity": 0.7, "transform": [{ "fn": "scale", "x": 0.91, "y": 0.91 }] } },
{ "type": "card", "style": { "opacity": 1.0, "transform": [{ "fn": "scale", "x": 1.00, "y": 1.00 }] } }
]
```

(Back/mid/front comment above each entry omitted here for brevity — each object above is a separate sibling `card`, e.g. inside a `div` with `flex-direction: "row"`.)

Use `scale` steps of ~0.08–0.12 between planes. More than 3 planes starts looking mechanical.

---
Expand Down
6 changes: 5 additions & 1 deletion .claude/skills/rustmotion/rules/easing-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ Entrance presets already use appropriate easing internally.

## Available Easing Functions

`linear`, `ease_in`, `ease_out`, `ease_in_out`, `ease_in_quad`, `ease_out_quad`, `ease_in_cubic`, `ease_out_cubic`, `ease_in_expo`, `ease_out_expo`, `spring`
17 named easings, plus `cubic_bezier` for a custom curve — 18 total:

`linear`, `ease_in`, `ease_out`, `ease_in_out`, `ease_in_quad`, `ease_out_quad`, `ease_in_cubic`, `ease_out_cubic`, `ease_in_expo`, `ease_out_expo`, `ease_in_out_quad`, `ease_in_out_expo`, `ease_in_back`, `ease_out_back`, `ease_out_elastic`, `bounce`, `spring`

`cubic_bezier` takes a parameter object instead of a bare string — `{ "cubic_bezier": { "x1": 0.4, "y1": 0.0, "x2": 0.2, "y2": 1.0 } }` — anywhere an `easing` value is accepted (top-level `animation.easing`, per-keyframe `easing`).

### Spring Physics

Expand Down
35 changes: 19 additions & 16 deletions .claude/skills/rustmotion/rules/geometry-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

No textual content may bleed out of the device viewport. The renderer enforces this through three opt-in mechanisms, all checked by `rustmotion validate`.

## 1. Text wrapping (`style.wrap`)
## 1. Text wrapping (`style.white-space`)

`text` is constraint-aware: it wraps to its parent's allocated width by default. Set `style.wrap: false` only when you intentionally want the text to render on one line and you guarantee it fits (e.g. a marquee that bleeds, a ticker, a title with a fixed `max-width`).
`text` is constraint-aware: it wraps to its parent's allocated width by default. There is no `style.wrap` field — that boolean belonged to the pre-CSS `LayerStyle` model and no longer exists in `CssStyle` (`deny_unknown_fields` rejects it and silently drops the component). Wrapping is controlled by the standard CSS `white-space` property instead:

- Default: `wrap: true` (wraps at parent or `max_width`, whichever is smaller).
- `wrap: false` → validator fails with `unwrappable_text_overflow` if the natural width exceeds the box.
- Default: unset / `"normal"` (and `"pre-line"`, `"pre-wrap"`, `"break-spaces"`) — wraps at parent width or `max-width`, whichever is smaller.
- `white-space: "nowrap"` or `"pre"` → the text renders on one line. The validator measures its natural (unbounded) width and fails with `unwrappable_text_overflow` if that width exceeds the box. Only set this when you intentionally want a single line and a finite `max-width` + reasonable `font-size` guarantee it fits (e.g. a title, a ticker-like label — not a marquee, which has its own component).

```json
{ "type": "text", "content": "Long sentence...", "style": { "wrap": true, "max-width": 800 } }
{ "type": "text", "content": "Long sentence...", "style": { "white-space": "nowrap", "max-width": 800 } }
```

## 2. Codeblock / Terminal `auto_scroll`
Expand Down Expand Up @@ -39,38 +39,41 @@ CSS-like semantics: `visible` (default) lets children bleed; `hidden` clips at t

## What the validator catches

`rustmotion validate scenario.json` reports three geometry violation kinds:
`rustmotion validate scenario.json` reports five geometry violation kinds:

- `viewport_overflow` — absolute bbox crosses the device edge
- `unwrappable_text_overflow` — `wrap: false` but natural width > available width
- `unwrappable_text_overflow` — `white-space: "nowrap"`/`"pre"` but natural width > available width
- `content_overflows_box` — wrapping text needs more room than the box it was actually assigned, typically a paragraph inside a card with a fixed `height` too small for it. Text painters never clip themselves, so this paints outside its box even when the box sits comfortably inside the frame — which is why the viewport check alone never caught it.
- `auto_scroll_disabled_overflow` — `auto_scroll: false` but content > box
- `animated_text_overflow` — an animated transform (scale/translate/wiggle/orbit) pushes the bbox out of the viewport at some sampled time. Only checked with `--strict-anim` (default runs check the resting, untransformed layout only).

`marquee` and `cursor` are exempt (their job is to bleed).
`marquee` and `cursor` are exempt (their job is to bleed). A node is also exempt when it clips itself, or when any ancestor clips it — `overflow` set to anything other than `visible`. Deliberate bleed under a clipping parent is a composition technique, not a defect: that is how you get giant type running off the frame.

## CLI usage

```bash
rustmotion validate scenario.json # human-readable
rustmotion validate scenario.json --report report.json # JSON report
rustmotion validate scenario.json --fix # safe auto-fixes
rustmotion validate scenario.json --strict-anim # per-frame check
rustmotion validate scenario.json --strict-anim # per-frame check, adds animated_text_overflow
rustmotion validate scenario.json --lenient # warnings only
```

`--fix` rewrites the file in place. It only applies *safe* mutations:
- sets `style.wrap: true` on text that overflowed because wrap was off
- sets `auto_scroll: true` on codeblock/terminal with overflow
`--fix` rewrites the file in place:
- `auto_scroll_disabled_overflow` → sets `auto_scroll: true`. Safe.
- `unwrappable_text_overflow` → removes `style.white-space`, so the text falls back to the `normal` default and wraps again. Non-destructive: it only ever deletes the property that caused the violation. If you want the line to stay unbroken, widen the box or lower `font-size` by hand instead of running `--fix`.

Position/size clamping is never auto-applied — fix those by hand.
Position/size clamping (`viewport_overflow`) is never auto-applied — fix those by hand too.

## When to use what

| Symptom | Fix |
|---|---|
| Long sentence cut at viewport edge | leave `wrap: true` (default) and ensure parent has finite width |
| Need a single-line title that must fit | set `max-width` and a small enough `font-size`, leave `wrap: true` |
| Marquee / ticker text that intentionally scrolls past edges | use `marquee` (exempt) — never `text` with `wrap: false` |
| Long sentence cut at viewport edge | leave `white-space` unset (default wraps) and ensure parent has finite width |
| Need a single-line title that must fit | set `max-width` and a small enough `font-size`, leave `white-space` unset |
| Marquee / ticker text that intentionally scrolls past edges | use `marquee` (exempt) — never `text` with `white-space: "nowrap"` |
| Code listing taller than its box | leave `auto_scroll: true` (default) |
| Terminal log streaming many lines | leave `auto_scroll: true` |
| Badge protruding from a card on purpose | container has `overflow: visible` (default) — no change needed |
| Hard-clip children to a card border | container `style.overflow: "hidden"` |
| Animated element (wiggle/orbit/keyframe scale) might drift off-screen | run `rustmotion validate --strict-anim` to sample frames, not just the resting layout |
7 changes: 4 additions & 3 deletions .claude/skills/rustmotion/rules/glassmorphism.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,11 @@ La bordure et l'ombre reprennent la teinte accent — le verre a une couleur.

```json
{
"video": { "background": "#0f172a" },
"video": { "width": 1080, "height": 1920, "fps": 30, "background": "#0f172a" },
"scenes": [{
"duration": 3.0,
"children": [
{ "type": "card", "style": { "backdrop-filter": [{ "fn": "blur", "radius": 24 }], "background": "#FFFFFF18" } }
{ "type": "card", "style": { "width": 800, "height": 400, "backdrop-filter": [{ "fn": "blur", "radius": 24 }], "background": "#FFFFFF18" } }
]
}]
}
Expand All @@ -252,6 +253,6 @@ Fond uni `#0f172a` + verre → on ne voit rien à travers le flou, la carte est
`backdrop-filter` floute ce qui est **derrière le composant dans le viewport**, pas ce qui est dans le parent. Si appliqué à un `text` ou `icon` enfant d'une carte opaque, il n'a aucun effet visible.

```json
{ "type": "text", "style": { "backdrop-filter": [{ "fn": "blur", "radius": 20 }], "color": "#fff" } }
{ "type": "text", "content": "Label", "style": { "backdrop-filter": [{ "fn": "blur", "radius": 20 }], "color": "#fff" } }
```
Aucun effet — `backdrop-filter` est uniquement pertinent sur les éléments directement superposés à une texture/image/blob de fond. ✗
21 changes: 15 additions & 6 deletions .claude/skills/rustmotion/rules/icon-sizing-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,28 @@ Each card is 230px = 77px CSS → icon and text completely unreadable.

## GOOD: 2×2 grid on mobile

`grid-template-columns` takes an array (`["1fr", "1fr"]`), never a raw CSS shorthand string like `"1fr 1fr"` — a bare string fails to deserialize (`Vec<GridTrack>` expected) and drops the card. Grid containers also need an explicit `height` (not `"auto"`) — see [rules/grid-card-height.md](grid-card-height.md).

```json
{
"type": "card",
"style": { "width": 984, "display": "grid", "grid-template-columns": "1fr 1fr", "gap": 24 },
"style": {
"width": 984,
"height": 640,
"display": "grid",
"grid-template-columns": ["1fr", "1fr"],
"grid-template-rows": ["1fr", "1fr"],
"gap": 24
},
"children": [
{ "type": "card", "style": { "width": 480, "padding": 40 }, "children": [...] },
{ "type": "card", "style": { "width": 480, "padding": 40 }, "children": [...] },
{ "type": "card", "style": { "width": 480, "padding": 40 }, "children": [...] },
{ "type": "card", "style": { "width": 480, "padding": 40 }, "children": [...] }
{ "type": "card", "style": { "padding": 40 }, "children": [] },
{ "type": "card", "style": { "padding": 40 }, "children": [] },
{ "type": "card", "style": { "padding": 40 }, "children": [] },
{ "type": "card", "style": { "padding": 40 }, "children": [] }
]
}
```
480px per card = 160px CSS → readable content. 24px gap. ✓
Each cell fills its grid track (~480px wide here) = 160px CSS → readable content. 24px gap. ✓

## BAD: Icon inconsistency across scenes

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/rustmotion/rules/module-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The workspace is split into three crates:
| Crate | Role |
|---|---|
| `rustmotion-core` | Engine, CSS model, schema types, Painter trait |
| `rustmotion-components` | 51 component structs + Painter impls + box builder |
| `rustmotion-components` | 57 component structs + Painter impls + box builder |
| `rustmotion-cli` | CLI commands (render, validate, schema, info) |

---
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/rustmotion/rules/prefer-presets.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rule: Prefer Presets Over Manual Keyframes

Presets are simpler, less error-prone, and produce consistent motion design. Only use `keyframes` animation effects for custom behavior not covered by the 31 built-in presets.
Presets are simpler, less error-prone, and produce consistent motion design. Only use `keyframes` animation effects for custom behavior not covered by the 40 built-in presets (+ 6 char-only presets on `text`).

**GOOD:**
```json
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/rustmotion/rules/responsive-device-sizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ Reference: Tailwind CSS default spacing (4px base unit).

Sizing rules above are guidelines — `rustmotion validate` is the source of truth. It refuses any scenario whose layout tree leaves the device viewport. See [geometry-safety.md](geometry-safety.md):

- `text` wraps automatically at the parent's max width — leave `style.wrap` at its default (`true`) unless you have a finite `max-width`.
- `text` wraps automatically at the parent's max width — leave `style.white-space` unset (default wraps) unless you have a finite `max-width`.
- `codeblock` / `terminal` auto-scroll when content exceeds their `size` — leave `auto_scroll: true` (default).
- Long single-line content that should bleed must use `marquee`, never `text` with `wrap: false`.
- Long single-line content that should bleed must use `marquee`, never `text` with `white-space: "nowrap"`.

## BAD: Using desktop sizes on mobile

Expand Down
14 changes: 8 additions & 6 deletions .claude/skills/rustmotion/rules/text-background.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

`text-background` adds a colored rectangle behind text content. The background is positioned to tightly wrap the text glyphs with configurable padding and corner radius.

`text-background` is a **root field** on `text` — a sibling of `style`, not nested inside it. `CssStyle` has no `text-background` key (`deny_unknown_fields`); putting it in `style` silently drops the whole `text` component.

**GOOD:**
```json
{
"type": "text",
"content": " Highlighted text ",
"text-background": {
"color": "#6366F1",
"padding": 12,
"corner_radius": 8
},
"style": {
"font-size": 36,
"color": "#FFFFFF",
"text-align": "center",
"text-background": {
"color": "#6366F1",
"padding": 12,
"corner_radius": 8
}
"text-align": "center"
}
}
```
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/rustmotion/rules/validate-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ Every generated JSON scenario MUST be validated with `rustmotion validate` befor

## Geometry violations

The validator detects three viewport-overflow conditions:
The validator detects five overflow conditions:
- `viewport_overflow` — absolute bbox crosses the device edge
- `unwrappable_text_overflow` — `style.wrap: false` but natural width > box
- `unwrappable_text_overflow` — `style.white-space: "nowrap"`/`"pre"` but natural width > box (there is no `wrap` field)
- `content_overflows_box` — wrapping text needs more room than its own box, e.g. a paragraph in a fixed-height card. Fires even when the box stays inside the frame
- `auto_scroll_disabled_overflow` — `auto_scroll: false` on a codeblock/terminal with content > box
- `animated_text_overflow` — an animated transform pushes the bbox out of the viewport at some sampled time (`--strict-anim` only)

See [rules/geometry-safety.md](geometry-safety.md) for the underlying mechanisms.

Expand All @@ -25,6 +27,6 @@ rustmotion validate -f /tmp/scenario.json --strict-anim # per-frame checks
rustmotion validate -f /tmp/scenario.json --lenient # warnings only
```

`--fix` may set `wrap: true` and `auto_scroll: true` automatically. Position/size issues are never auto-fixed.
`--fix` handles the two mechanical cases: it sets `auto_scroll: true` on `auto_scroll_disabled_overflow`, and removes `style.white-space` on `unwrappable_text_overflow` (falling back to the `normal` default, i.e. wrapping). Viewport and content-box overflows are never auto-fixed — they need a layout decision only you can make.

**FORBIDDEN:** Presenting JSON that has not been validated by `rustmotion validate`.
3 changes: 2 additions & 1 deletion .claude/skills/rustmotion/rules/wiggle-additive.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Wiggle offsets apply additively on top of keyframe animations and presets. Combi
"type": "icon",
"icon": "lucide:phone-off",
"style": {
"size": 64,
"width": 64,
"height": 64,
"color": "#FFFFFF",
"animation": [
{ "name": "scale_in" },
Expand Down
Loading
Loading