rustmotion 0.7.0

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.
Documentation
# Rule: Dynamic Depth — Multi-Element Parallax

Static depth (see [depth-layering.md](depth-layering.md)) places elements at different perceived distances. Dynamic depth *animates* each plane independently so the scene breathes and the spatial composition is felt over time, not just read visually.

Four independent mechanisms combine freely:

| Mechanism | Scope | Best for |
|---|---|---|
| **A. Per-element wiggle seeds** | One element | Uncorrelated floating per card/icon |
| **B. `float_3d` preset** | One element | Hero card with gentle 3D tilt loop |
| **C. Camera keyframes** | Whole scene | Cinematic zoom-in / slow pan |
| **D. `style.depth` (vraie parallaxe)** | Plans top-level | Parallaxe multi-plans pilotée par la caméra |

---

## Mechanism D — Vraie parallaxe caméra : `style.depth`

Quand la scène a une `camera`, chaque **enfant direct** de la scène est un plan. `style.depth` met à l'échelle l'effet caméra sur ce plan (pan × depth, zoom' = 1 + (zoom−1)×depth, rotation × depth, autour de `camera.origin`) :

| `depth` | Effet |
|---|---|
| `0` | Plan verrouillé — la caméra ne le bouge jamais (HUD, watermark, fond fixe) |
| `0.2–0.5` | Arrière-plan lointain — bouge peu (blobs, textures) |
| `1.0` | Plan normal (défaut — comportement identique sans depth) |
| `1.5–2.5` | Avant-plan amplifié — bouge plus que la caméra (profondeur dramatique) |

```json
{
  "duration": 6.0,
  "camera": {
    "keyframes": [
      { "property": "x", "values": [ { "time": 0, "value": 0 }, { "time": 5, "value": 200 } ], "easing": "ease_in_out" }
    ]
  },
  "children": [
    { "type": "shape", "shape": "circle", "position": "absolute", "x": 100, "y": 200,
      "fill": { "type": "radial", "colors": ["#6366F160", "#6366F100"] },
      "style": { "width": 600, "height": 600, "depth": 0.3 } },
    { "type": "card", "style": { "depth": 1.0, "width": 800, "height": 400 } },
    { "type": "badge", "text": "LIVE", "position": "absolute", "x": 80, "y": 100,
      "style": { "depth": 1.8 } }
  ]
}
```

Le fond (0.3) glisse lentement, la card suit la caméra, le badge file en avant-plan — parallaxe cinéma réelle, sans wiggle.

**Règles :**
- `depth` n'agit que sur les **enfants directs** de la scène (v1) ; il gouverne tout le sous-arbre du plan. Un `depth` sur un nœud imbriqué est sans effet caméra.
- Sans `camera` sur la scène, `depth` est inerte.
- `depth: 1.0` partout == aucun depth (byte-identique).
- Le point focal se règle avec `camera.origin` (`{ "x": px, "y": px }`, keyframable via `"origin.x"` / `"origin.y"`) — zoom vers un élément précis puis pan vers un autre.
- Combine avec le mécanisme A (wiggle) librement — la parallaxe caméra est déterministe, le wiggle ajoute la vie.

---

## Mechanism A — Wiggle seeds for per-element parallax

Each element gets a different `seed`. Because seeds produce different noise curves, elements move asynchronously even at the same frequency — that desynchronization is the parallax effect.

**Rules:**
- Background elements: `frequency` 0.3–0.6 (slow drift), `amplitude` 3–6px
- Mid-ground elements: `frequency` 0.6–1.0 (medium), `amplitude` 4–8px
- Foreground elements: `frequency` 1.0–1.5 (faster), `amplitude` 6–12px
- Never reuse the same seed on two elements in the same scene
- Wiggle is always additive — it doesn't conflict with entrance presets

```json
[
  {
    "type": "shape",
    "style": {
      "animation": [
        { "name": "fade_in", "duration": 0.6 },
        { "name": "wiggle", "property": "translate_y", "amplitude": 5, "frequency": 0.4, "seed": 7 },
        { "name": "wiggle", "property": "translate_x", "amplitude": 3, "frequency": 0.3, "seed": 91 }
      ]
    }
  },
  {
    "type": "card",
    "style": {
      "animation": [
        { "name": "fade_in_up", "duration": 0.6 },
        { "name": "wiggle", "property": "translate_y", "amplitude": 6, "frequency": 0.7, "seed": 42 }
      ]
    }
  },
  {
    "type": "badge",
    "style": {
      "animation": [
        { "name": "scale_in", "delay": 0.3, "duration": 0.5 },
        { "name": "wiggle", "property": "translate_y", "amplitude": 8, "frequency": 1.2, "seed": 13 },
        { "name": "wiggle", "property": "scale", "amplitude": 0.04, "frequency": 0.8, "seed": 55 }
      ]
    }
  }
]
```

---

## Mechanism B — `float_3d` preset for hero depth

`float_3d` is a continuous preset that combines a `translate_y` float with a gentle `rotate_x`/`rotate_y` wobble. It reads as "the object is hovering in 3D space". Use it on the scene's primary element (hero card, main icon, featured image).

**Always pair `float_3d` with `"loop": true`.**

```json
{
  "type": "card",
  "style": {
    "width": 800,
    "height": 480,
    "background": "#1E293B",
    "border-radius": 28,
    "box-shadow": [{ "color": "#00000060", "offset-y": 40, "blur": 80 }],
    "animation": [
      { "name": "scale_in", "duration": 0.7, "easing": "ease_out" },
      { "name": "float_3d", "loop": true }
    ]
  }
}
```

For mobile (larger scene), increase the `box-shadow` blur to 120 so the shadow movement on `float_3d` is visible.

**Do NOT apply `float_3d` to text components.** Rotating text mid-scene breaks readability.

---

## Mechanism C — Camera keyframes for cinematic parallax

The virtual camera transforms the entire scene uniformly. Animated `zoom` creates a "push in" that makes stationary elements feel they're being approached — the simplest form of cinematic parallax.

```json
{
  "duration": 5.0,
  "camera": {
    "keyframes": [
      { "property": "zoom", "values": [{ "time": 0, "value": 1.0 }, { "time": 4, "value": 1.08 }], "easing": "ease_in_out" }
    ]
  },
  "children": [...]
}
```

**Zoom range:** `1.0 → 1.05` (subtle) to `1.0 → 1.15` (dramatic). Beyond 1.2 elements exit the frame.

**Pan + zoom:**
```json
"keyframes": [
  { "property": "zoom", "values": [{ "time": 0, "value": 1.0 }, { "time": 4, "value": 1.1 }], "easing": "ease_in_out" },
  { "property": "x", "values": [{ "time": 0, "value": 0 }, { "time": 4, "value": -60 }], "easing": "ease_in_out" }
]
```

Camera applies **after** individual element animations — wiggle + float_3d still play on top.

---

## Combining all three: reference recipe

This is the "dynamism level 2" recipe for a hero scene on portrait 9:16 (1080×1920).

```json
{
  "duration": 6.0,
  "camera": {
    "keyframes": [
      { "property": "zoom", "values": [{ "time": 0, "value": 1.0 }, { "time": 5, "value": 1.06 }], "easing": "ease_in_out" }
    ]
  },
  "children": [
    {
      "type": "shape",
      "shape": "circle",
      "fill": "#6366F118",
      "style": {
        "width": 800,
        "height": 800,
        "z-index": 0,
        "animation": [
          { "name": "fade_in", "duration": 1.2 },
          { "name": "wiggle", "property": "scale", "amplitude": 0.06, "frequency": 0.25, "seed": 7 },
          { "name": "wiggle", "property": "translate_y", "amplitude": 12, "frequency": 0.2, "seed": 91 }
        ]
      }
    },
    {
      "type": "card",
      "style": {
        "width": 900,
        "height": 540,
        "background": "#1E293B",
        "border-radius": 32,
        "box-shadow": [{ "color": "#00000070", "offset-y": 48, "blur": 100 }],
        "z-index": 1,
        "animation": [
          { "name": "fade_in_up", "duration": 0.7 },
          { "name": "float_3d", "loop": true }
        ]
      },
      "children": [...]
    },
    {
      "type": "badge",
      "style": {
        "z-index": 2,
        "box-shadow": [{ "color": "#6366F180", "offset-y": 12, "blur": 32 }],
        "animation": [
          { "name": "scale_in", "delay": 0.4, "duration": 0.5 },
          { "name": "wiggle", "property": "translate_y", "amplitude": 10, "frequency": 1.1, "seed": 42 },
          { "name": "wiggle", "property": "scale", "amplitude": 0.05, "frequency": 0.9, "seed": 17 }
        ]
      }
    }
  ]
}
```

---

## Orbit for decorative background elements

`orbit` creates a circular/elliptical path with pseudo-3D scale modulation. Use it on non-content decorative elements (blobs, particles, icon decorations). Multiple elements with different `phase` values feel like a constellation.

```json
[
  { "style": { "animation": [{ "name": "orbit", "radius_x": 40, "radius_y": 24, "speed": 0.3, "depth": 0.12, "tilt": 15, "phase": 0.0 }] } },
  { "style": { "animation": [{ "name": "orbit", "radius_x": 40, "radius_y": 24, "speed": 0.3, "depth": 0.12, "tilt": 15, "phase": 0.33 }] } },
  { "style": { "animation": [{ "name": "orbit", "radius_x": 40, "radius_y": 24, "speed": 0.3, "depth": 0.12, "tilt": 15, "phase": 0.67 }] } }
]
```

Phase steps: evenly distributed (`0`, `1/N`, `2/N`, ...). Same speed + different phase = elements stay locked in formation.

---

## Frequency hierarchy cheatsheet

| Element role | Wiggle frequency | Wiggle amplitude | Seeds |
|---|---|---|---|
| Background blob/shape | 0.2–0.4 Hz | 8–15px (translate) | Any, just unique |
| Mid-ground card | 0.5–0.8 Hz | 5–10px | Different from bg |
| Foreground badge/icon | 0.9–1.4 Hz | 6–12px | Different from mid |
| Text (body) | **Never wiggle primary text** |||

---

## BAD: Same seed on every element

```json
{ "style": { "animation": [{ "name": "wiggle", "property": "translate_y", "amplitude": 6, "frequency": 0.8, "seed": 0 }] } },
{ "style": { "animation": [{ "name": "wiggle", "property": "translate_y", "amplitude": 6, "frequency": 0.8, "seed": 0 }] } }
```
Identical seed + frequency → elements move in perfect sync → no parallax, looks like a group animation bug. ✗

## BAD: `float_3d` on text

```json
{ "type": "text", "content": "Boost your sales", "style": { "animation": [{ "name": "float_3d", "loop": true }] } }
```
Rotating text mid-scene is disorienting and breaks readability. Use `float_3d` only on cards, icons, images, shapes. ✗