@a-company/atelier 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/{chunk-JPZ4F4PW.js → chunk-3ARBOSWY.js} +64 -5
  2. package/dist/chunk-3ARBOSWY.js.map +1 -0
  3. package/dist/cli.js +11469 -413
  4. package/dist/cli.js.map +1 -1
  5. package/dist/{dist-M67UZGFQ.js → dist-3YQK6PI6.js} +2 -2
  6. package/dist/index.cjs +3193 -227
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +701 -8
  9. package/dist/index.d.ts +701 -8
  10. package/dist/index.js +7237 -72
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.js +2898 -507
  13. package/dist/mcp.js.map +1 -1
  14. package/package.json +14 -9
  15. package/src/web/inline-app.ts +55 -4
  16. package/src/web/timeline-state-types.ts +28 -0
  17. package/src/web/timeline-view.test.ts +99 -0
  18. package/src/web/timeline-view.ts +339 -0
  19. package/src/web/workspace-app.ts +3146 -0
  20. package/templates/workspace/.claude/agents/atelier-iris.md +75 -0
  21. package/templates/workspace/.claude/agents/atelier-lux.md +67 -0
  22. package/templates/workspace/.claude/agents/atelier-quill.md +61 -0
  23. package/templates/workspace/.gitignore +30 -0
  24. package/templates/workspace/.paradigm/personas/_shared/cascade-merge.md +172 -0
  25. package/templates/workspace/CLAUDE.md +93 -0
  26. package/templates/workspace/README.md +75 -0
  27. package/templates/workspace/SETUP.md +127 -0
  28. package/templates/workspace/_brand/.atelier-brand.yaml +34 -0
  29. package/templates/workspace/_brand/DESIGN.md +56 -0
  30. package/templates/workspace/_brand/SCRIPT.md +41 -0
  31. package/templates/workspace/_brand/STORYBOARD.md +33 -0
  32. package/templates/workspace/_packs/README.md +54 -0
  33. package/templates/workspace/projects/README.md +49 -0
  34. package/templates/workspace/workspace.atelier +22 -0
  35. package/university/content/notes/N-atel-001-first-render.md +114 -0
  36. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  37. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  38. package/university/content/notes/N-atel-101-easings.md +97 -0
  39. package/university/content/notes/N-atel-101-layers.md +106 -0
  40. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  41. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  42. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  43. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  44. package/university/content/notes/N-atel-201-patterns.md +108 -0
  45. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  46. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  47. package/university/content/notes/N-atel-301-effects.md +136 -0
  48. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  49. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  50. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  51. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  52. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  53. package/university/content/notes/N-atel-401-transitions.md +94 -0
  54. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  55. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  56. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  57. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  58. package/university/content/notes/N-atel-601-carousel.md +71 -0
  59. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  60. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  61. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  62. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  63. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  64. package/university/content/notes/N-atel-701-vector.md +85 -0
  65. package/university/content/notes/N-atel-701-video.md +88 -0
  66. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  67. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  68. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  69. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  70. package/university/content/paths/LP-atel-001.yaml +21 -0
  71. package/university/content/paths/LP-atel-101.yaml +22 -0
  72. package/university/content/paths/LP-atel-201.yaml +23 -0
  73. package/university/content/paths/LP-atel-301.yaml +22 -0
  74. package/university/content/paths/LP-atel-401.yaml +22 -0
  75. package/university/content/paths/LP-atel-501.yaml +22 -0
  76. package/university/content/paths/LP-atel-601.yaml +22 -0
  77. package/university/content/paths/LP-atel-701.yaml +22 -0
  78. package/university/content/paths/LP-atel-801.yaml +22 -0
  79. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  80. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  81. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  82. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  83. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  84. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  85. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  86. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  87. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  88. package/university/index.yaml +720 -0
  89. package/university/pack.yaml +21 -0
  90. package/dist/chunk-5QQESXI6.js +0 -4432
  91. package/dist/chunk-5QQESXI6.js.map +0 -1
  92. package/dist/chunk-JPZ4F4PW.js.map +0 -1
  93. package/dist/cli.cjs +0 -6313
  94. package/dist/cli.cjs.map +0 -1
  95. package/dist/cli.d.cts +0 -1
  96. package/dist/cli.d.ts +0 -1
  97. package/dist/mcp.cjs +0 -5462
  98. package/dist/mcp.cjs.map +0 -1
  99. /package/dist/{dist-M67UZGFQ.js.map → dist-3YQK6PI6.js.map} +0 -0
@@ -0,0 +1,118 @@
1
+ ---
2
+ id: N-atel-301-shapes-and-text
3
+ title: Shapes and text — the foundational visuals
4
+ type: note
5
+ track: ATEL-301
6
+ author: atelier
7
+ created: '2026-05-18'
8
+ updated: '2026-05-18'
9
+ tags:
10
+ - course
11
+ - atel-301
12
+ - shapes
13
+ - text
14
+ - typography
15
+ difficulty: intermediate
16
+ estimatedMinutes: 5
17
+ prerequisites:
18
+ - N-atel-101-layers
19
+ summary: The four shape primitives (rect, ellipse, polygon, path), the typography stack, and the design rules that prevent 90% of "the text looks wrong" problems.
20
+ ---
21
+
22
+ ## ShapeVisual — four primitives
23
+
24
+ ```yaml
25
+ visual:
26
+ type: shape
27
+ shape:
28
+ kind: rect # or: ellipse | polygon | path
29
+ # ... kind-specific fields
30
+ ```
31
+
32
+ | `kind` | Required fields | Notes |
33
+ |---|---|---|
34
+ | `rect` | `width`, `height`, optional `cornerRadius` | The most common. `cornerRadius` accepts a number or `{tl, tr, br, bl}` for per-corner control. |
35
+ | `ellipse` | `radiusX`, `radiusY` | For circles, set `radiusX === radiusY`. |
36
+ | `polygon` | `points: [{x, y}, ...]` (≥ 3 points) | Closed path through the points. |
37
+ | `path` | `points: PathPoint[]`, optional `closed` | Bezier-capable. Each PathPoint has `{x, y}` and optional `inControl`/`outControl` for curves. |
38
+
39
+ Shapes are drawn at the layer's `frame` position with the layer's `bounds` as the local coordinate space. Shape coordinates are in that local space — `(0, 0)` is the bounds' top-left.
40
+
41
+ ## Fills and strokes — covered in the effects note
42
+
43
+ The shape `kind` defines geometry; `fill` and `stroke` (on the visual or set via `atelier_set_fill` / `atelier_set_stroke`) define paint. They're covered in N-atel-301-effects because they apply to text and image layers too, not just shapes.
44
+
45
+ ## TextVisual — the typography stack
46
+
47
+ ```yaml
48
+ visual:
49
+ type: text
50
+ content: "Hello"
51
+ style:
52
+ fontFamily: Inter, system-ui, sans-serif # required
53
+ fontSize: 48 # required
54
+ fontWeight: 600 # optional, defaults to "normal"
55
+ color: "#F5F5F7" # optional, defaults to canvas-appropriate
56
+ letterSpacing: 0 # optional, em units
57
+ lineHeight: 1.2 # optional, multiplier
58
+ textAlign: center # 'left' | 'center' | 'right' | 'justify'
59
+ fontStyle: normal # 'normal' | 'italic'
60
+ textDecoration: none # 'none' | 'underline' | 'line-through'
61
+ ```
62
+
63
+ `fontFamily` is treated as a CSS-style fallback chain. The renderer (canvas, SVG, Lottie) loads the first available font and falls back through the chain. **For consistent rendering across renderers and platforms, ship your fonts as assets** — register a font file with `atelier_add_asset({ kind: "font", src: "fonts/Inter-SemiBold.woff2" })` and reference it by the font's PostScript name in `fontFamily`.
64
+
65
+ Without font assets, the browser studio uses system fonts; the CLI's PNG export (node-canvas) uses whatever's installed on the machine. Renders diverge. Font assets fix this.
66
+
67
+ ## Text bounds
68
+
69
+ Text layers have `bounds` like every other layer. The renderer wraps text to fit the bounds' width. Lines that exceed the bounds' height are clipped (not pushed off-screen — clipped, like overflow:hidden).
70
+
71
+ If you want auto-sizing bounds: leave bounds at a generous default (e.g. 1200×400 for a title) and let the text size itself within. Tight bounds give precise layout but require you to know the text content in advance.
72
+
73
+ Animatable text properties via deltas:
74
+ - `opacity`, `rotation`, `scale`, `frame.x`/`frame.y`, `bounds.width`/`bounds.height` (layer-level — animate any text)
75
+ - `style.fontSize`, `style.color`, `style.letterSpacing`, `style.lineHeight` (style-level)
76
+ - `content` is **not** animatable — to "type" text on, use a series of TextVisual layers each appearing on a successive frame, or use the upcoming `atelier_set_typewriter` interaction (planned, not yet shipped)
77
+
78
+ ## The three rules that prevent most text problems
79
+
80
+ 1. **Always specify `fontSize`.** Defaults exist but vary by renderer. Pin it.
81
+ 2. **Always specify `fontFamily` with a fallback chain.** Even if you ship font assets, the chain handles "asset not loaded yet" gracefully.
82
+ 3. **Use `anchorPoint: {x: 0.5, y: 0.5}` for any text you'll animate.** Rotation and scale pivot from the anchor — centered text rotates around its center, top-left text rotates around the corner. The latter is almost never what you want.
83
+
84
+ ## Worked example — a title with subtitle
85
+
86
+ ```yaml
87
+ layers:
88
+ - id: title
89
+ visual:
90
+ type: text
91
+ content: Atelier
92
+ style:
93
+ fontFamily: Inter, system-ui, sans-serif
94
+ fontSize: 120
95
+ fontWeight: 700
96
+ color: "#F5F5F7"
97
+ textAlign: center
98
+ frame: { x: 960, y: 460 }
99
+ bounds: { width: 1200, height: 160 }
100
+ anchorPoint: { x: 0.5, y: 0.5 }
101
+
102
+ - id: subtitle
103
+ visual:
104
+ type: text
105
+ content: AI-native animation, locally launched
106
+ style:
107
+ fontFamily: Inter, system-ui, sans-serif
108
+ fontSize: 32
109
+ fontWeight: 400
110
+ color: "#9CA3AF"
111
+ textAlign: center
112
+ letterSpacing: 0.02
113
+ frame: { x: 960, y: 600 }
114
+ bounds: { width: 1400, height: 60 }
115
+ anchorPoint: { x: 0.5, y: 0.5 }
116
+ ```
117
+
118
+ Two layers, no animation — just composition. Open this in the studio and you have a still composition; add deltas and the title fades and the subtitle slides.
@@ -0,0 +1,71 @@
1
+ ---
2
+ id: N-atel-401-hierarchical-states
3
+ title: Hierarchical states — `parent`, inheritance, and merge
4
+ type: note
5
+ track: ATEL-401
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-401
12
+ - states
13
+ - hierarchy
14
+ - resolve-frame
15
+ difficulty: intermediate
16
+ estimatedMinutes: 6
17
+ prerequisites:
18
+ - N-atel-101-easings
19
+ summary: A state can name a `parent` and inherit its deltas. Inheritance is per `layer+property` group — the child replaces the whole group or leaves it untouched. Know that one rule and hierarchical states stop surprising you.
20
+ ---
21
+
22
+ ## Why states inherit
23
+
24
+ Interactive UI motion is variations on a theme. `hover` is `default` plus a lift. `pressed` is `hover` plus a sink. Re-authoring every shared delta in every state is duplication that drifts — change the entrance once and you have to remember to change it in four places.
25
+
26
+ A `parent` lets a state say "everything the parent does, plus my changes." You author the shared motion once on the base state and let `hover`, `pressed`, `disabled` extend it.
27
+
28
+ ```ts
29
+ atelier_add_state({ id, stateName: "hover", duration: 30 });
30
+ atelier_set_state_parent({ id, stateName: "hover", parent: "default" });
31
+ ```
32
+
33
+ `atelier_add_state` creates the state with an empty `deltas` array. `atelier_set_state_parent` wires the inheritance link (pass `parent: null` to clear it). The parent must already exist, and a state cannot be its own parent — the tool walks the parent chain and rejects cycles before writing.
34
+
35
+ ## When to use hierarchy (and when not)
36
+
37
+ Use a parent when states are **variations of the same composition** — the same layers, mostly the same motion, a few overrides. Interaction states (`default` → `hover` → `pressed`) are the canonical case.
38
+
39
+ Do **not** use a parent to splice unrelated segments of a narrative together. Two states that share no layers and no motion should just be two flat states. Hierarchy earns its keep only when there is real shared motion to inherit.
40
+
41
+ ## How `resolveFrame` merges parent + child
42
+
43
+ This is the one rule that matters. When a state has a `parent`, the resolver builds the **ancestor chain** (root → … → parent → self), then collects deltas with this algorithm:
44
+
45
+ 1. Group each state's deltas by a `layer+property` key (e.g. `title:opacity`, `title:scale.x`).
46
+ 2. Walk the chain from root to self. For each `layer+property` key, the **most-derived state's group wins outright**.
47
+ 3. Flatten the surviving groups into the merged delta list, and resolve the frame against that list.
48
+
49
+ The merge is **per group, not per field**. If the child declares any delta for `title:opacity`, the parent's `title:opacity` is dropped entirely — the child does not patch the parent's `from`/`easing` and keep the rest. Either the child owns that `layer+property` or it inherits the parent's.
50
+
51
+ | Key | `default` (parent) | `hover` (child) | Resolved on `hover` |
52
+ |---|---|---|---|
53
+ | `title:opacity` | fade 0→1 | fade 0→1, faster | **child** (parent dropped) |
54
+ | `title:scale.x` | — | 1→1.05 | **child** (new key) |
55
+ | `card:shadow.blur` | 4→8 | — | **parent** (inherited) |
56
+
57
+ So `hover` inherits the card's shadow animation untouched, overrides the title's fade, and adds a title scale the parent never had.
58
+
59
+ ## The trap
60
+
61
+ The surprising case is partial override. If `default` animates `title:opacity` with a nice spring and `hover` adds *any* `title:opacity` delta — even a tiny one — the spring is gone, replaced wholesale. If you want to keep the parent's motion and add something, add it on a **different property** (or a different layer). One `layer+property` key has exactly one owner in the resolved frame.
62
+
63
+ ## Inspecting the merge
64
+
65
+ ```bash
66
+ atelier preview button.atelier --frame 10 --state hover
67
+ ```
68
+
69
+ The resolved `ResolvedLayer[]` reflects the merged deltas — if a property looks wrong, it is almost always because a child group silently replaced a parent group you meant to keep. Compare against `--state default` at the same frame to see exactly which keys the child took over.
70
+
71
+ Next: transitions — how you move *between* these states with timing of their own.
@@ -0,0 +1,106 @@
1
+ ---
2
+ id: N-atel-401-motion-deep-dive
3
+ title: Motion deep dive — springs, expressions, motion paths
4
+ type: note
5
+ track: ATEL-401
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-401
12
+ - motion
13
+ - springs
14
+ - expressions
15
+ - motion-path
16
+ difficulty: intermediate
17
+ estimatedMinutes: 8
18
+ prerequisites:
19
+ - N-atel-101-easings
20
+ summary: Three ways to push past from→to-with-easing. Springs for physical feel, expression-valued deltas for math-driven motion, and motion paths for trajectories. When each earns its complexity.
21
+ ---
22
+
23
+ ## When a bezier isn't enough
24
+
25
+ A plain delta is `from → to` shaped by an easing. That covers most motion. This note is the other 10% — three escape hatches, each for a problem the basic delta can't express.
26
+
27
+ ## 1. Spring physics — tuning, not guessing
28
+
29
+ A spring easing is solved numerically per frame, so it can overshoot and settle in a way no bezier can fake. You set it as the delta's `easing`:
30
+
31
+ ```yaml
32
+ easing:
33
+ type: spring
34
+ stiffness: 170
35
+ damping: 20
36
+ mass: 1
37
+ ```
38
+
39
+ The three knobs:
40
+
41
+ | Knob | What it controls | Push it up → |
42
+ |---|---|---|
43
+ | `stiffness` | how hard the spring pulls toward the target | snappier, faster arrival |
44
+ | `damping` | how fast oscillation dies out | less bounce (high = no overshoot) |
45
+ | `mass` | inertia of the moving thing | slower, heavier, more lag |
46
+
47
+ The relationship that matters: **damping relative to stiffness** decides bounce. Low damping against high stiffness oscillates; high damping against the same stiffness glides in with no overshoot. Starting points: `{ stiffness: 170, damping: 26 }` for a clean snap, `{ stiffness: 100, damping: 10 }` for visible springiness. The delta's frame range still clips the solver — the spring is held at its resolved value after the range ends.
48
+
49
+ Use a spring when the thing should feel **tactile** — toggles, drag-release, dismissals. Use a bezier for crossfades and value-to-value moves where overshoot would be wrong.
50
+
51
+ ## 2. Expression-valued deltas — math as the curve
52
+
53
+ A delta's `from` and `to` normally hold concrete values. Either can instead hold an **expression object** `{ expr: "..." }` that the engine evaluates **per frame**:
54
+
55
+ ```yaml
56
+ - layer: badge
57
+ property: rotation
58
+ range: [0, 120]
59
+ from: 0
60
+ to: { expr: "sin(t * tau) * 8" }
61
+ ```
62
+
63
+ The evaluator is a safe recursive-descent parser — **no `eval`, no `Function`**, so an expression can never run arbitrary code. Inside the string you have:
64
+
65
+ - **Context variables:** `t` (eased progress 0→1), `progress` (raw progress before easing), `frame` (current frame number), `duration` (the delta's length in frames).
66
+ - **Functions:** `sin cos tan abs min max floor ceil round sqrt pow clamp sign log`.
67
+ - **Constants:** `pi`, `tau`, `e`. Operators `+ - * / % **`, comparisons, and a `cond ? a : b` ternary.
68
+
69
+ Reach for an expression when the motion is a **function**, not a tween — a continuous wobble (`sin(t * tau)`), a value derived from frame number, a conditional that changes behavior partway. For anything a bezier or spring can already do, use those; expressions are for genuinely procedural motion. Note the contract: an expression resolves to a **number**, so it's for numeric properties.
70
+
71
+ ## 3. Motion paths — trajectory instead of two axes
72
+
73
+ A motion path makes a layer travel a defined trajectory rather than animating `frame.x` and `frame.y` independently:
74
+
75
+ ```yaml
76
+ motionPath:
77
+ points: [ ... ] # PathPoint[] defining the curve
78
+ closed: false
79
+ autoRotate: true # turn the layer to follow the path tangent
80
+ autoRotateOffset: 0 # degrees of rotation offset when autoRotate is on
81
+ ```
82
+
83
+ `autoRotate` makes the layer **bank into the curve** — a paper plane that points where it's going, a label that rides along a route. `autoRotateOffset` corrects for art that doesn't point "right" at 0°.
84
+
85
+ You control **position along the path with progress**. When a motion path is present, the layer's `frame.x`/`frame.y` are overridden by the path, so you animate the `motionPath.progress` property (0→1) instead — that delta's easing now shapes *speed along the trajectory*:
86
+
87
+ ```yaml
88
+ - layer: plane
89
+ property: motionPath.progress
90
+ range: [0, 90]
91
+ from: 0
92
+ to: 1
93
+ easing: ease-in-out
94
+ ```
95
+
96
+ A `frame.x` delta on a layer that has a motion path is ignored at render time — the path won. Express speed and timing through `motionPath.progress` plus easings, not through frame deltas.
97
+
98
+ ## Choosing among the three
99
+
100
+ > Tactile, physical, should bounce or settle → **spring**.
101
+ > Continuous or procedural, a function of time/frame → **expression**.
102
+ > Should follow a defined route, maybe banking into it → **motion path** + `motionPath.progress`.
103
+
104
+ Everything else is a plain delta with an easing — and that's the right default. These three are tools for when the default genuinely can't say what you mean.
105
+
106
+ Next: presets and templates — packaging motion and structure for reuse.
@@ -0,0 +1,98 @@
1
+ ---
2
+ id: N-atel-401-presets-and-templates
3
+ title: Presets and templates — reusing motion and structure
4
+ type: note
5
+ track: ATEL-401
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-401
12
+ - presets
13
+ - templates
14
+ - reuse
15
+ difficulty: intermediate
16
+ estimatedMinutes: 7
17
+ prerequisites:
18
+ - N-atel-101-easings
19
+ summary: Two reuse mechanisms that are easy to confuse. Presets are bundles of deltas you apply to a layer; templates are layer subtrees you instantiate with variables. One rule keeps them straight.
20
+ ---
21
+
22
+ ## The one rule
23
+
24
+ > **Presets = delta bundles. Templates = layer subtrees.**
25
+
26
+ A preset is *motion* you stamp onto a layer that already exists. A template is *structure* — layers and their content — you instantiate to create new layers. If you keep that distinction, the rest of this note is detail.
27
+
28
+ ## Presets — define once, apply anywhere
29
+
30
+ Define a named bundle of deltas on the document. Each preset delta describes a property animation **without naming a layer or absolute frames** — those get filled in at apply time.
31
+
32
+ ```ts
33
+ atelier_define_preset({
34
+ id,
35
+ presetName: "fade-rise",
36
+ description: "Fade in while rising 20px",
37
+ deltas: [
38
+ { property: "opacity", from: 0, to: 1, offset: [0, 20], easing: "ease-out" },
39
+ { property: "frame.y", from: 20, to: 0, offset: [0, 20], easing: "ease-out" },
40
+ ],
41
+ });
42
+ ```
43
+
44
+ The `offset: [start, end]` on a preset delta is a **relative frame window** — measured from wherever you apply the preset, not from the document's frame 0. Then apply it to a concrete layer in a state:
45
+
46
+ ```ts
47
+ atelier_apply_preset({
48
+ id,
49
+ stateName: "intro",
50
+ presetName: "fade-rise",
51
+ layerId: "title",
52
+ startFrame: 0,
53
+ });
54
+ ```
55
+
56
+ Apply expands the preset into real deltas on that layer: each delta's `range` becomes `[startFrame + offset[0], startFrame + offset[1]]`. If a preset delta has no `offset`, it spans `startFrame` to `startFrame + duration` (where `duration` defaults to the state's duration). Expanded deltas go through the same no-overlap check as hand-authored ones — applying a preset that collides with existing motion on the same `layer+property` is rejected.
57
+
58
+ ## Two kinds of stagger
59
+
60
+ Stagger — the same motion rippling across a row of elements — comes from two different offsets, and it's worth knowing both:
61
+
62
+ 1. **Intra-preset sequencing** — give the deltas *inside* one preset stepped `offset` windows so they fire in sequence from a single apply (e.g. opacity at `[0, 12]`, then a label at `[6, 18]`).
63
+ 2. **Cross-instance stagger** — apply the *same* preset to several layers with stepped `startFrame` values. This is the classic stagger-cards pattern:
64
+
65
+ ```ts
66
+ atelier_apply_preset({ id, stateName: "intro", presetName: "fade-rise", layerId: "card-1", startFrame: 0 });
67
+ atelier_apply_preset({ id, stateName: "intro", presetName: "fade-rise", layerId: "card-2", startFrame: 5 });
68
+ atelier_apply_preset({ id, stateName: "intro", presetName: "fade-rise", layerId: "card-3", startFrame: 10 });
69
+ ```
70
+
71
+ Three identical applies, each five frames later than the last — the cards cascade in. Author the motion once; the stagger lives entirely in the stepped `startFrame`.
72
+
73
+ ## Templates — instantiate a layer subtree
74
+
75
+ A template is a document that uses `{{variableName}}` placeholders in its layers and content. `atelier_instantiate_template` resolves the placeholders against bindings you provide and **creates a new document** in the store:
76
+
77
+ ```ts
78
+ atelier_instantiate_template({
79
+ id: "lower-third-template",
80
+ bindings: { name: "Ada Lovelace", title: "Mathematician" },
81
+ });
82
+ ```
83
+
84
+ Use `atelier_find_variables` first to see what a template expects — it reports the `{{...}}` references found, which are declared, and which are undeclared or unused. Instantiation fails (with per-variable messages) if a required binding is missing, so you find gaps before you render.
85
+
86
+ A template gives you the *whole structure* — layers, text, the lock-up — parameterized. That is fundamentally different from a preset, which only carries motion and needs a layer to land on.
87
+
88
+ ## Choosing
89
+
90
+ | You have… | You want… | Reach for |
91
+ |---|---|---|
92
+ | A layer that exists | To animate it like other layers | **Preset** (`define` then `apply`) |
93
+ | A repeated structure (cards, lower-thirds) | To stamp out copies with different content | **Template** (`instantiate`) |
94
+ | A row of elements | The same motion, cascading | **Preset** applied N times with stepped `startFrame` |
95
+
96
+ Presets and templates compose: instantiate a template to build the structure, then apply presets to the layers it produced. Structure from one mechanism, motion from the other — the rule holds.
97
+
98
+ That closes ATEL-401. You can now build hierarchical, interactive, physically-tuned motion and package it for reuse.
@@ -0,0 +1,94 @@
1
+ ---
2
+ id: N-atel-401-transitions
3
+ title: Transitions and the interaction layer
4
+ type: note
5
+ track: ATEL-401
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-401
12
+ - transitions
13
+ - interactions
14
+ - state-machine
15
+ difficulty: intermediate
16
+ estimatedMinutes: 6
17
+ prerequisites:
18
+ - N-atel-101-easings
19
+ summary: A transition is the timed bridge from one state to another. Interactions are the triggers that fire those transitions. Together they turn a pile of states into an interactive state machine.
20
+ ---
21
+
22
+ ## A transition is a bridge with its own timing
23
+
24
+ States are destinations. A transition is *how you get from A to B* — with a duration and easing that belong to the move, not to either state.
25
+
26
+ ```ts
27
+ atelier_configure_transition({
28
+ id,
29
+ stateName: "default",
30
+ targetState: "hover",
31
+ transition: {
32
+ duration: 12,
33
+ easing: "ease-out",
34
+ },
35
+ });
36
+ ```
37
+
38
+ Read it as: "from `default`, the transition to `hover` takes 12 frames, eased-out." The `duration` is in frames (a positive integer) and the `easing` accepts the same forms you already know — `"ease-out"`, `{ type: "cubic-bezier", x1, y1, x2, y2 }`, `{ type: "spring", stiffness, damping }`, `{ type: "step", steps }`.
39
+
40
+ Transitions are **directional**. `default → hover` and `hover → default` are configured separately, and they usually differ — entrances ease-out, exits ease-in. Pass `transition: null` to remove a configured transition. A state cannot transition to itself, and the target state must already exist (the tool checks both).
41
+
42
+ ## Interactive motion: default → hover → pressed
43
+
44
+ The classic button is three states and the transitions between them:
45
+
46
+ ```ts
47
+ // states
48
+ atelier_add_state({ id, stateName: "hover", duration: 30 });
49
+ atelier_add_state({ id, stateName: "pressed", duration: 30 });
50
+ // (hover and pressed typically set parent: "default" — see hierarchical states)
51
+
52
+ // transitions, each with timing that fits the move
53
+ atelier_configure_transition({ id, stateName: "default", targetState: "hover", transition: { duration: 10, easing: "ease-out" } });
54
+ atelier_configure_transition({ id, stateName: "hover", targetState: "pressed", transition: { duration: 6, easing: "ease-in" } });
55
+ atelier_configure_transition({ id, stateName: "pressed", targetState: "hover", transition: { duration: 8, easing: "ease-out" } });
56
+ atelier_configure_transition({ id, stateName: "hover", targetState: "default", transition: { duration: 14, easing: "ease-in" } });
57
+ ```
58
+
59
+ At this point the document *describes* a state machine — but nothing fires the transitions yet. States and transitions are pure data. Something has to say "go."
60
+
61
+ ## The interaction layer — triggers that fire transitions
62
+
63
+ `atelier_add_interaction` attaches a **trigger → action** binding to a layer. The trigger is the event; the action is what happens.
64
+
65
+ ```ts
66
+ atelier_add_interaction({
67
+ id,
68
+ layerId: "button",
69
+ interactionId: "to-hover",
70
+ trigger: { type: "hover" },
71
+ action: { type: "go-to-state", state: "hover" },
72
+ description: "Lift the button when the pointer enters",
73
+ });
74
+ ```
75
+
76
+ Triggers: `click`, `hover`, `pointerdown`, `pointerup`, `timer` (needs a `delay` in ms), `signal` (needs a `signal` name). Actions: `go-to-state` (needs a `state`), `emit-signal`, `set-variable`, `toggle-visibility`. For the button you wire `hover → go-to-state hover`, `pointerdown → go-to-state pressed`, `pointerup → go-to-state hover`.
77
+
78
+ A `go-to-state` action does **not** snap. It runs the **transition** you configured from the current state to the target — with that transition's duration and easing. Interactions decide *when*; transitions decide *how it looks getting there*. If no transition is configured for that pair, the runtime cuts to the target's frame 0.
79
+
80
+ ## Two-trigger patterns worth knowing
81
+
82
+ - **Auto-advance:** a `timer` trigger with `delay` plus `go-to-state` — an intro state that walks itself to the main state after N milliseconds, no input required.
83
+ - **Decoupled choreography:** one layer's `click` action is `emit-signal`; another layer has a `signal` trigger that does `go-to-state`. The clicker doesn't know who's listening. This is how a single tap drives several layers into new states without hard-wiring them together.
84
+
85
+ ## Inspecting an interaction
86
+
87
+ ```ts
88
+ atelier_list_interactions({ id }); // every layer
89
+ atelier_list_interactions({ id, layerId: "button" }); // one layer
90
+ ```
91
+
92
+ The studio's interaction inspector shows each binding and lets you fire it manually to watch the transition play. If a click "does nothing," it is almost always a missing transition for that state pair — the action fired, but there was no bridge to animate across.
93
+
94
+ Next: the motion deep dive — springs, expressions, and motion paths.
@@ -0,0 +1,76 @@
1
+ ---
2
+ id: N-atel-501-detected-vs-user-edited
3
+ title: Detected vs. user-edited — why your fixes survive a re-run
4
+ type: note
5
+ track: ATEL-501
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-501
12
+ - video
13
+ - aspects
14
+ difficulty: intermediate
15
+ estimatedMinutes: 6
16
+ prerequisites:
17
+ - N-atel-101-layers
18
+ summary: The `~detected-vs-user-edited` aspect — detections are immutable, but your overrides (padding, userEdited/userAdded/hidden, corrected text) are re-attached across re-runs by matching on timing tolerance plus unchanged detected text.
19
+ ---
20
+
21
+ ## The problem this aspect solves
22
+
23
+ The video pipeline is meant to be re-run. You re-transcribe after swapping the audio; you re-detect after a re-cut. But every re-run regenerates the machine's findings from scratch. If a re-run wiped your corrections, you'd never trust it — you'd transcribe once, then refuse to touch it.
24
+
25
+ `~detected-vs-user-edited` is the invariant that makes re-running safe:
26
+
27
+ > AI-detected content is immutable. User overrides are preserved across re-runs by matching fresh detections to existing entries on timing tolerance **plus** unchanged detected text.
28
+
29
+ Detected and edited values live in *separate fields* so the two never collide. A transcript word carries both:
30
+
31
+ ```json
32
+ { "detected": "atelyer", "text": "atelier", "start": 3.40, "end": 3.72, "userEdited": true }
33
+ ```
34
+
35
+ `detected` is what Whisper heard — the machine's column. `text` is what renders — yours. The same split exists on a cut: `rawStart`/`rawEnd` are detected, `paddingPre`/`paddingPost` are yours.
36
+
37
+ ## How a re-run re-attaches your edits
38
+
39
+ When you re-run, the merge walks the **fresh** detections and asks, for each one: did the user override a matching old entry? "Matching" means two conditions, both required:
40
+
41
+ 1. the timing is within a tolerance, **and**
42
+ 2. the `detected` string is unchanged.
43
+
44
+ | Pipeline | Tolerance | Second condition |
45
+ |---|---|---|
46
+ | Transcript (`mergeTranscriptWithExisting`) | `0.3s` | `detected` string identical |
47
+ | Cuts (`mergeWithExisting`) | `0.5s` | both `rawStart` and `rawEnd` within tolerance |
48
+
49
+ If both hold, the old override (`text`, `userEdited`, `hidden`, or the cut's padding and label) is copied onto the fresh detection. If they don't, the fresh default wins — because the underlying audio genuinely changed there, and your old correction may no longer make sense.
50
+
51
+ Requiring **unchanged detected text** is the subtle part. Timing alone isn't enough: if Whisper now hears a different word at that timestamp, blindly carrying your old correction forward would graft a fix onto the wrong word. Matching the `detected` string guarantees you're editing the same finding you edited before.
52
+
53
+ ## userAdded words are orphans — and they're kept
54
+
55
+ A word you inserted with `atelier transcript add` has no fresh detection to match — Whisper never heard it. These `userAdded` words are preserved as orphans and re-inserted into the right segment by their timing. Your additions don't evaporate just because they aren't in the new Whisper output.
56
+
57
+ `hidden` words (from `atelier transcript delete`) follow the same rule as edits: matched on timing + unchanged `detected`, then the `hidden` flag rides along.
58
+
59
+ ## Concrete: fix a misheard word, re-transcribe, fix survives
60
+
61
+ ```
62
+ # Whisper hears "atelyer" at word 12
63
+ atelier transcript fix ./proj --word 12 --text 'atelier'
64
+ # → word 12 now { detected: "atelyer", text: "atelier", userEdited: true }
65
+
66
+ # later — re-transcribe (swapped a louder take of the same audio)
67
+ atelier transcribe ./proj
68
+ # → fresh detection at ~3.40s still hears "atelyer"
69
+ # → matches on 0.3s timing + unchanged detected "atelyer"
70
+ # → your text:"atelier" + userEdited:true are re-attached
71
+
72
+ atelier transcript list ./proj
73
+ # [ 12] 3.40s "atelier" ← "atelyer" [edited]
74
+ ```
75
+
76
+ The same shape holds for cuts: nudge `--cut 3 --pad-post 0.4`, re-run `atelier trim`, and as long as cut #3's raw boundaries land within `0.5s` of where they were, your `0.4s` of trailing padding rides through. Re-detect freely; the human's editorial layer is sticky by construction.
@@ -0,0 +1,62 @@
1
+ ---
2
+ id: N-atel-501-layer-tag-isolation
3
+ title: Layer-tag isolation — pipelines that never step on each other
4
+ type: note
5
+ track: ATEL-501
6
+ author: atelier
7
+ created: '2026-05-20'
8
+ updated: '2026-05-20'
9
+ tags:
10
+ - course
11
+ - atel-501
12
+ - video
13
+ - aspects
14
+ difficulty: intermediate
15
+ estimatedMinutes: 5
16
+ prerequisites:
17
+ - N-atel-101-layers
18
+ summary: The `~layer-tag-isolation` aspect — each content pipeline owns a tag namespace and on re-run drops ONLY its own tagged layers, never touching another pipeline's output or user-authored layers. Proven by tests.
19
+ ---
20
+
21
+ ## One document, many writers
22
+
23
+ A `project.atelier` is written by several actors at once: `atelier trim` owns the cut layers, `atelier transcribe` owns the caption layers, recipes own the overlay layers — and *you* hand-author whatever else the scene needs. They all live in the same `layers` array. Re-running any one of them must not disturb the others.
24
+
25
+ `~layer-tag-isolation` is the rule that makes that work:
26
+
27
+ > Every content pipeline owns a layer tag-namespace and, on re-run, drops ONLY its own tagged layers before re-adding them — never touching user-authored (untagged) layers or other pipelines' outputs.
28
+
29
+ This is the load-bearing invariant of the whole human-and-agent loop. Without it, every regenerate would be a gamble; with it, you can re-run a pipeline a hundred times and lose nothing you didn't ask it to touch.
30
+
31
+ ## The namespaces
32
+
33
+ | Tag | Owner | Status |
34
+ |---|---|---|
35
+ | `silence-trim` | `atelier trim` | Actively written |
36
+ | `caption` | `atelier transcribe` / `atelier captions regenerate` | Actively written |
37
+ | `caption-word` | reserved for per-word caption layers | Reserved in the isolation contract |
38
+ | `overlay` | Studio Recipes (handle, page-number, decoration) | Written by the recipe pipeline |
39
+
40
+ `silence-trim` and `caption` are what the commands in this track produce. `caption-word` is a reserved slot in the isolation contract — the namespace is claimed so that a future per-word caption mode can't collide with phrase-level `caption` layers. `overlay` belongs to the recipe pipeline (ATEL-301 covers it). Anything **untagged** is yours, and no pipeline ever touches it.
41
+
42
+ ## How a rewrite stays isolated
43
+
44
+ The mechanism is one line of intent: **filter by tag before rewriting.** When the caption builder rewrites, it partitions the document:
45
+
46
+ ```ts
47
+ // keep everything that ISN'T a caption layer
48
+ const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("caption"));
49
+ // build fresh caption layers from the current transcript
50
+ const { layers: captionLayers } = buildCaptionLayers(transcript, doc.canvas, options);
51
+ return { ...doc, layers: [...preserved, ...captionLayers] };
52
+ ```
53
+
54
+ `silence-trim` layers, `overlay` layers, and your untagged layers all fall into `preserved` and pass through unchanged. Only the `caption` set is dropped and rebuilt. `atelier trim` does the identical thing for its `silence-trim` tag. Two pipelines, two namespaces, zero overlap.
55
+
56
+ The same care extends to deltas: when captions are rebuilt, only the deltas keyed to the caption layers being replaced are dropped — every other delta in the state survives.
57
+
58
+ ## Proven by tests, not just by convention
59
+
60
+ Layer-tag isolation isn't an honor system. The test suite asserts the invariant directly: it seeds a document with a user-authored layer plus one pipeline's tagged layers, re-runs the *other* pipeline, and checks that the user layer and the sibling pipeline's layers are byte-for-byte present afterward. A regression that let `transcribe` clobber a `silence-trim` layer — or a hand-authored title — would turn the suite red.
61
+
62
+ That's the contract you get to lean on: tag a layer with a pipeline's namespace and that pipeline owns it; leave a layer untagged and it's untouchable. Regenerate as often as you like. The machine only ever cleans up after itself.