@a-company/atelier 0.36.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.
- package/package.json +13 -8
- package/university/content/notes/N-atel-001-first-render.md +114 -0
- package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
- package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
- package/university/content/notes/N-atel-101-easings.md +97 -0
- package/university/content/notes/N-atel-101-layers.md +106 -0
- package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
- package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
- package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
- package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
- package/university/content/notes/N-atel-201-patterns.md +108 -0
- package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
- package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
- package/university/content/notes/N-atel-301-effects.md +136 -0
- package/university/content/notes/N-atel-301-images-and-video.md +126 -0
- package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
- package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
- package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
- package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
- package/university/content/notes/N-atel-401-transitions.md +94 -0
- package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
- package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
- package/university/content/notes/N-atel-501-silence-trim.md +98 -0
- package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
- package/university/content/notes/N-atel-601-carousel.md +71 -0
- package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
- package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
- package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
- package/university/content/notes/N-atel-701-choosing-output.md +68 -0
- package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
- package/university/content/notes/N-atel-701-vector.md +85 -0
- package/university/content/notes/N-atel-701-video.md +88 -0
- package/university/content/notes/N-atel-801-editing-surface.md +69 -0
- package/university/content/notes/N-atel-801-live-bridge.md +84 -0
- package/university/content/notes/N-atel-801-studio-app.md +72 -0
- package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
- package/university/content/paths/LP-atel-001.yaml +21 -0
- package/university/content/paths/LP-atel-101.yaml +22 -0
- package/university/content/paths/LP-atel-201.yaml +23 -0
- package/university/content/paths/LP-atel-301.yaml +22 -0
- package/university/content/paths/LP-atel-401.yaml +22 -0
- package/university/content/paths/LP-atel-501.yaml +22 -0
- package/university/content/paths/LP-atel-601.yaml +22 -0
- package/university/content/paths/LP-atel-701.yaml +22 -0
- package/university/content/paths/LP-atel-801.yaml +22 -0
- package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
- package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
- package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
- package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
- package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
- package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
- package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
- package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
- package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
- package/university/index.yaml +720 -0
- package/university/pack.yaml +21 -0
|
@@ -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.
|