@a-company/atelier 0.29.0 → 0.37.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 (76) hide show
  1. package/dist/chunk-5QQESXI6.js +4432 -0
  2. package/dist/chunk-5QQESXI6.js.map +1 -0
  3. package/dist/cli.cjs +2391 -530
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +301 -429
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +2233 -38
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +584 -2
  10. package/dist/index.d.ts +584 -2
  11. package/dist/index.js +111 -3
  12. package/dist/mcp.cjs +1215 -365
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +1209 -365
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +20 -9
  17. package/src/web/inline-app.ts +867 -0
  18. package/src/web/tsconfig.json +9 -0
  19. package/templates/welcome.atelier +67 -0
  20. package/university/content/notes/N-atel-001-first-render.md +114 -0
  21. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  22. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  23. package/university/content/notes/N-atel-101-easings.md +97 -0
  24. package/university/content/notes/N-atel-101-layers.md +106 -0
  25. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  26. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  27. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  28. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  29. package/university/content/notes/N-atel-201-patterns.md +108 -0
  30. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  31. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  32. package/university/content/notes/N-atel-301-effects.md +136 -0
  33. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  34. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  35. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  36. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  37. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  38. package/university/content/notes/N-atel-401-transitions.md +94 -0
  39. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  40. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  41. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  42. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  43. package/university/content/notes/N-atel-601-carousel.md +71 -0
  44. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  45. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  46. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  47. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  48. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  49. package/university/content/notes/N-atel-701-vector.md +85 -0
  50. package/university/content/notes/N-atel-701-video.md +88 -0
  51. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  52. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  53. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  54. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  55. package/university/content/paths/LP-atel-001.yaml +21 -0
  56. package/university/content/paths/LP-atel-101.yaml +22 -0
  57. package/university/content/paths/LP-atel-201.yaml +23 -0
  58. package/university/content/paths/LP-atel-301.yaml +22 -0
  59. package/university/content/paths/LP-atel-401.yaml +22 -0
  60. package/university/content/paths/LP-atel-501.yaml +22 -0
  61. package/university/content/paths/LP-atel-601.yaml +22 -0
  62. package/university/content/paths/LP-atel-701.yaml +22 -0
  63. package/university/content/paths/LP-atel-801.yaml +22 -0
  64. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  65. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  66. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  67. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  68. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  69. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  70. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  71. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  72. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  73. package/university/index.yaml +720 -0
  74. package/university/pack.yaml +21 -0
  75. package/dist/chunk-JV7RGETS.js +0 -2292
  76. package/dist/chunk-JV7RGETS.js.map +0 -1
@@ -0,0 +1,136 @@
1
+ ---
2
+ id: N-atel-301-effects
3
+ title: Effects — fills, strokes, shadows, blends, clips, motion paths, tints
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
+ - effects
13
+ - rendering
14
+ difficulty: intermediate
15
+ estimatedMinutes: 5
16
+ prerequisites:
17
+ - N-atel-301-images-and-video
18
+ summary: The non-geometry visual surface — paint (fills/strokes/gradients), depth (shadows), compositing (blends), masking (clip paths), trajectory (motion paths), color overlays (tints). What each does, when each animates well.
19
+ ---
20
+
21
+ ## Fills
22
+
23
+ ```yaml
24
+ fill:
25
+ type: color # or: linear-gradient | radial-gradient | pattern
26
+ color: "#3B82F6"
27
+ ```
28
+
29
+ Four fill types:
30
+
31
+ | `type` | Shape |
32
+ |---|---|
33
+ | `color` | `color: "#RRGGBB"` or `"rgba(...)"` |
34
+ | `linear-gradient` | `stops: [{offset, color}, ...]`, `angle: degrees` |
35
+ | `radial-gradient` | `stops: [{offset, color}, ...]`, `center: {x, y}`, `radius: number` |
36
+ | `pattern` | `assetId` (image), optional `repeat: 'repeat' \| 'no-repeat'` |
37
+
38
+ Apply via `atelier_set_fill({ documentId, layerId, fill })`. Animatable: stop colors and offsets via deltas on `fill.stops[N].color` and `fill.stops[N].offset`. Solid colors animate via `fill.color`.
39
+
40
+ ## Strokes
41
+
42
+ ```yaml
43
+ stroke:
44
+ color: "#F5F5F7"
45
+ width: 2
46
+ dash: [4, 4] # optional dash pattern
47
+ lineJoin: round # 'miter' | 'round' | 'bevel'
48
+ lineCap: round # 'butt' | 'round' | 'square'
49
+ ```
50
+
51
+ Animatable: `width`, `color`, `dash` (animate `dash[0]` and `dash[1]` for marching-ants effects).
52
+
53
+ ## Shadows
54
+
55
+ ```yaml
56
+ shadow:
57
+ color: "#000000AA"
58
+ blur: 20
59
+ offsetX: 0
60
+ offsetY: 8
61
+ spread: 0 # optional — extends the shadow before blur
62
+ ```
63
+
64
+ Drop shadows (`offsetX/Y > 0`) and glows (`offsetX/Y === 0` with high `blur`) both use the same primitive. Animate `blur` and `spread` for pulse effects.
65
+
66
+ Applied per-layer via `atelier_set_shadow({ documentId, layerId, shadow })`. Removing: pass `shadow: null`.
67
+
68
+ ## Blend modes
69
+
70
+ ```yaml
71
+ blendMode: multiply # or any Canvas composite operation
72
+ ```
73
+
74
+ Layer-level. The full list: `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`.
75
+
76
+ Most common in practice:
77
+ - `multiply` — color over background (good for grading photos)
78
+ - `screen` — lighten effect (good for flares, highlights)
79
+ - `overlay` — contrast boost
80
+ - `color` — preserve luminosity of base, take hue+saturation of overlay (good for color grading)
81
+
82
+ Animatable indirectly: animate the layer's `opacity` with the blend mode held constant. The blend mode itself is not interpolated (it's a discrete switch).
83
+
84
+ ## Clip paths
85
+
86
+ ```yaml
87
+ clipPath:
88
+ kind: ellipse # any Shape
89
+ radiusX: 200
90
+ radiusY: 200
91
+ ```
92
+
93
+ Restricts rendering to inside the clip shape. Useful for: avatar circles, ken-burns inside a non-rectangular frame, transition reveals.
94
+
95
+ The clip path is in the layer's local coordinate space (same as visual shapes). Animatable: animate the clip shape's properties (e.g. `clipPath.radiusX`) for reveal animations.
96
+
97
+ ## Motion paths
98
+
99
+ ```yaml
100
+ motionPath:
101
+ points:
102
+ - { x: 100, y: 400 }
103
+ - { x: 600, y: 100, outControl: { x: 800, y: 100 }}
104
+ - { x: 1100, y: 400, inControl: { x: 900, y: 400 }}
105
+ closed: false
106
+ autoRotate: true # layer rotates to follow path tangent
107
+ autoRotateOffset: 0 # degrees added to the auto-rotation
108
+ ```
109
+
110
+ The layer's `frame.x` / `frame.y` is overridden by the motion path. Deltas that animate `frame.x` or `frame.y` are ignored when a motion path is present (the path is the trajectory; the delta on a parametric position would conflict). To control speed along the path, animate `motionProgress` (0..1) with a delta.
111
+
112
+ If `autoRotate: true`, the layer's `rotation` follows the path's tangent — good for arrows tracing a curve, cars driving along a road.
113
+
114
+ ## Tints
115
+
116
+ ```yaml
117
+ tint:
118
+ color: "#3B82F6"
119
+ amount: 0.4 # 0..1, strength of the tint overlay
120
+ ```
121
+
122
+ A solid color overlay applied to the rendered layer. Animatable: `tint.amount` produces a tint fade-in/out. `tint.color` interpolates RGB → animatable for color-cycling.
123
+
124
+ Distinct from `blendMode: color` — tint is a per-layer overlay; blend mode is a composite operation between layers.
125
+
126
+ ## When to use what
127
+
128
+ - **Color emphasis on a shape:** fill.
129
+ - **Outline:** stroke.
130
+ - **Depth:** shadow.
131
+ - **Photo grading:** blendMode (`multiply`, `color`).
132
+ - **Reveal mask:** clipPath.
133
+ - **Curved trajectory:** motionPath.
134
+ - **Color cycle without changing assets:** tint.
135
+
136
+ For carousel social posts (the v1 headline use case): fills + a subtle `blendMode: multiply` on a dark overlay layer above the photo for caption legibility, with `tint` reserved for brand-color accents on overlay text.
@@ -0,0 +1,126 @@
1
+ ---
2
+ id: N-atel-301-images-and-video
3
+ title: Images and video — assets, cropping, spritesheets, playback
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
+ - images
13
+ - video
14
+ - assets
15
+ difficulty: intermediate
16
+ estimatedMinutes: 5
17
+ prerequisites:
18
+ - N-atel-301-shapes-and-text
19
+ summary: ImageVisual and VideoVisual. The asset system, cropping via sourceRect, spritesheet animation, video playback controls, and the objectFit rules that decide what "fitting" actually means.
20
+ ---
21
+
22
+ ## ImageVisual
23
+
24
+ ```yaml
25
+ visual:
26
+ type: image
27
+ assetId: hero-photo # registered via atelier_add_asset
28
+ src: assets/hero.jpg # optional — direct URL or data URL for inline use
29
+ sourceRect: # optional — crop region in source pixels
30
+ x: 100
31
+ y: 50
32
+ width: 800
33
+ height: 600
34
+ spritesheet: # optional — for spritesheet animation
35
+ columns: 4
36
+ rows: 4
37
+ frameWidth: 256
38
+ frameHeight: 256
39
+ frameCount: 16 # optional, defaults to columns*rows
40
+ frameIndex: 0 # animatable for spritesheet playback
41
+ ```
42
+
43
+ **Crop with `sourceRect`.** The 9-arg form of canvas `drawImage` is exposed directly — pick a sub-rectangle from the source image and draw it into the layer's bounds. The crop is in source-image pixels (not normalized 0–1). Animatable: deltas on `sourceRect.x`, `sourceRect.y`, `sourceRect.width`, `sourceRect.height` produce ken-burns-style pan/zoom.
44
+
45
+ **Spritesheet animation.** Set `spritesheet` and animate `frameIndex` with a `step()` easing for sprite-frame indexing:
46
+
47
+ ```yaml
48
+ deltas:
49
+ - layer: walking-character
50
+ property: frameIndex
51
+ from: 0
52
+ to: 15
53
+ startFrame: 0
54
+ endFrame: 120
55
+ easing:
56
+ type: step
57
+ count: 16
58
+ position: jump-none
59
+ ```
60
+
61
+ That walks through 16 spritesheet frames over 2 seconds at 60fps.
62
+
63
+ ## VideoVisual
64
+
65
+ ```yaml
66
+ visual:
67
+ type: video
68
+ assetId: hero-clip
69
+ src: assets/hero.mp4
70
+ startFrame: 30 # composition frame when clip begins playing
71
+ sourceOffset: 5.2 # seconds into source video to start from
72
+ sourceEnd: 12.7 # seconds into source to stop (undefined = play to end)
73
+ playbackRate: 1.0 # 1.0 = normal speed
74
+ volume: 0.8 # 0–1
75
+ muted: false
76
+ objectFit: contain # 'contain' | 'cover' | 'fill'
77
+ ```
78
+
79
+ - **`startFrame`** anchors the video to the composition timeline. Frame `0` of the composition shows the video frame at `sourceOffset` seconds.
80
+ - **`sourceOffset` / `sourceEnd`** trim the source. The `atelier trim` pipeline writes these values onto `clip-trim-N` layers (with `tags: ["silence-trim"]`) — the layer-tag isolation invariant covered in ATEL-501.
81
+ - **`playbackRate`** changes the source playback speed. `2.0` is 2x faster; `0.5` is half-speed. The audio is pitch-shifted accordingly (or muted if `muted: true`).
82
+ - **`objectFit`** decides what to do when the video's intrinsic aspect doesn't match the layer's bounds:
83
+ - `contain` — fit entirely inside bounds, possibly with letterboxing
84
+ - `cover` — fill bounds entirely, possibly cropping
85
+ - `fill` — stretch to fit (distorts aspect)
86
+
87
+ For most layouts: `cover`. For preserving aspect: `contain`. `fill` is rarely correct.
88
+
89
+ ## The asset system
90
+
91
+ ```
92
+ atelier_add_asset — register an asset with id + src + kind
93
+ atelier_list_assets — enumerate
94
+ atelier_remove_asset — remove (rejects if referenced by a layer)
95
+ ```
96
+
97
+ ```ts
98
+ await atelier_add_asset({ documentId,
99
+ asset: { id: "hero-photo", kind: "image", src: "assets/hero.jpg" }});
100
+ ```
101
+
102
+ `src` can be:
103
+ - A relative path (relative to the doc's directory). The studio's `/api/assets/:base64path` endpoint serves these with path-traversal protection.
104
+ - An absolute `file://` URL.
105
+ - A `data:` URL (inline base64) for portability — useful when shipping a doc as a single file.
106
+ - An `https://` URL (the renderer fetches it; not recommended for offline rendering).
107
+
108
+ **Why `assetId` + optional `src` (vs just `src`)?** Refactoring. Renaming an asset's source path means updating one entry in `atelier_add_asset`, not every layer that references it.
109
+
110
+ ## Cropping vs scaling — three knobs
111
+
112
+ Atelier gives you three independent ways to control how an image fits into a layer:
113
+
114
+ 1. **`sourceRect`** — pick a sub-rectangle from the source. Crop *what's drawn*.
115
+ 2. **`Layer.scale.x` / `Layer.scale.y`** — scale the layer's transform. Stretch *how it's drawn*.
116
+ 3. **`Layer.bounds.width` / `bounds.height`** — change the destination size. Crop *where it's drawn* (and the renderer interpolates fit per `objectFit` if it's a video).
117
+
118
+ For image carousels with consistent framing: set `bounds` to your target aspect (e.g. 1080×1080), use `sourceRect` to pick the focal area, leave `scale` at 1.0.
119
+
120
+ For ken-burns pan/zoom: animate `sourceRect.x`/`sourceRect.y`/`sourceRect.width`/`sourceRect.height` with deltas. The image stays in place; the visible crop pans and zooms.
121
+
122
+ ## Drag-and-drop in the studio (v1)
123
+
124
+ Dropping an image file onto the canvas creates an `ImageVisual` layer with the dropped file inlined as a `data:` URL (small files) or registered as an asset and referenced by `assetId` (larger files). The dropped image is centered on the canvas with bounds matching the image's intrinsic dimensions; you scale and crop from there.
125
+
126
+ This is the headline workflow for v1: drop a photo, add overlays (handle, page-number), tweak crop, export PNG. The studio's first-run scaffolds a `welcome.atelier` with a placeholder image you immediately replace.
@@ -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.