@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,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-601-recipe-tools-and-apply
|
|
3
|
+
title: Recipe tooling — CLI verbs, MCP tools, and resolution
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-601
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-601
|
|
12
|
+
- cli
|
|
13
|
+
- mcp
|
|
14
|
+
difficulty: intermediate
|
|
15
|
+
estimatedMinutes: 6
|
|
16
|
+
prerequisites:
|
|
17
|
+
- N-atel-301-composition-and-overlays
|
|
18
|
+
summary: The recipe surface — CLI verbs (recipe new/validate/show, apply-recipe, the --recipe flag on trim/transcribe/captions regenerate) and MCP tools (atelier_recipe_list/get/validate/save/apply). save refuses invalid recipes; apply wires only overlay_rules. The two-tier resolution chain and CLI-wins precedence.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## CLI: the recipe family
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
atelier recipe new <name> scaffold a starter YAML with every default filled in
|
|
25
|
+
atelier recipe validate <path> validate against the schema (^valid-recipe gate)
|
|
26
|
+
atelier recipe show <path> print a recipe; --with-defaults overlays code defaults
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**`recipe new`** scaffolds. It writes a starter `.recipe.yaml` (into `./.atelier/recipes/` by default, or `--dir`) with every Phase 1 field present and commented, so you learn the shape by editing. It refuses to overwrite an existing file, and it guards the footgun where `recipe new foo.recipe.yaml` would otherwise produce `foo.recipe.yaml.recipe.yaml`. Note: `new` only writes a template — it does *not* validate, because the scaffold is always valid by construction.
|
|
30
|
+
|
|
31
|
+
**`recipe validate`** runs the recipe through `validateRecipe` and reports `PASS`/`FAIL`. It also surfaces forward-compat warnings for reserved Phase 3 fields. Add `--json` for machine-readable output.
|
|
32
|
+
|
|
33
|
+
**`recipe show`** prints the recipe back as YAML. With `--with-defaults` it overlays the code defaults onto every omitted field, so you see the fully-resolved effective recipe.
|
|
34
|
+
|
|
35
|
+
## CLI: applying a recipe
|
|
36
|
+
|
|
37
|
+
A recipe touches a project two ways.
|
|
38
|
+
|
|
39
|
+
**The `--recipe <name>` flag** on the individual pipelines:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
atelier trim <project> --recipe my-style silence_policy as the baseline
|
|
43
|
+
atelier transcribe <project> --recipe my-style caption_style + caption_grouping
|
|
44
|
+
atelier captions regenerate <project> --recipe my-style caption_style + caption_grouping
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each flag pulls the relevant section from the recipe and uses it as the baseline for that pipeline. (The caption verb is `captions regenerate`, not a bare `captions`.)
|
|
48
|
+
|
|
49
|
+
**The `apply-recipe` convenience verb** runs trim and transcribe against the same recipe in one shot:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
atelier apply-recipe <project> <recipe>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In order: `atelier trim`, then `atelier transcribe`, then — if the recipe has `overlay_rules` — the overlay translator rewrites the project's `overlay`-tagged layers. Flags: `--reset` (destructive — discards existing user padding and transcript edits on both pipelines), `--no-trim`, `--no-transcribe`. Because `apply-recipe` is a single-frame apply with no carousel context, the page-number layer is silently skipped; the handle is applied if present.
|
|
56
|
+
|
|
57
|
+
## MCP: the conversational surface
|
|
58
|
+
|
|
59
|
+
Five tools let an agent view, edit, and apply recipes without shelling out:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
atelier_recipe_list enumerate discoverable recipes (project + user library)
|
|
63
|
+
atelier_recipe_get load a recipe; returns BOTH the parsed object and its YAML
|
|
64
|
+
atelier_recipe_validate validate a path OR inline YAML text
|
|
65
|
+
atelier_recipe_save write a recipe — refuses to write if invalid
|
|
66
|
+
atelier_recipe_apply apply overlay_rules to an in-store document
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Two behaviors are worth internalizing.
|
|
70
|
+
|
|
71
|
+
**`atelier_recipe_save` validates before writing.** It runs `validateRecipe` first and *refuses* to write an invalid recipe, returning the validation issues instead. This is the deliberate contrast with `recipe new`: `new` scaffolds an always-valid template; `save` is how an agent persists an *edited* recipe, and it guards the write so a malformed edit never lands on disk. It creates parent directories as needed and accepts either a recipe object or raw YAML text.
|
|
72
|
+
|
|
73
|
+
**`atelier_recipe_apply` only applies `overlay_rules`.** This is the key agent mental-model point. The apply tool wires up *only* the handle + page-number overlays onto an in-store document (the result is stored back with `source: "llm"`). Silence-trim, transcribe, and caption application stay **CLI-only** — there is no MCP path for them. If the recipe requests a `page_number` but `currentIndex`/`totalCount` aren't passed, that layer is skipped and a warning is returned (same rule as the CLI translator).
|
|
74
|
+
|
|
75
|
+
## Resolution chain and precedence
|
|
76
|
+
|
|
77
|
+
A bare recipe name resolves through a two-tier chain (`resolveRecipePath` in `packages/cli/src/lib/recipe.ts`), checking these extensions at each tier — `.recipe.yaml`, `.recipe.json`, `.yaml`, `.yml`, `.json`:
|
|
78
|
+
|
|
79
|
+
1. **Project-local:** `<projectDir>/.atelier/recipes/<name>.<ext>`
|
|
80
|
+
2. **User library:** `~/.atelier/recipes/<name>.<ext>`
|
|
81
|
+
|
|
82
|
+
The project directory is searched first, so a project can shadow a personal recipe of the same name. A path that is absolute or contains a slash skips resolution entirely and loads literally. `recipe new`, `recipe validate`, and `recipe show` all share this chain, so a name authored by `new` is found by `validate` and `show`.
|
|
83
|
+
|
|
84
|
+
**CLI-wins precedence.** When a recipe is combined with explicit CLI flags, the CLI flag wins. In `applyRecipeToTrimOptions`, each field is `cliOptions.x ?? policy.x` — the recipe only fills in what the CLI left unspecified. So `atelier trim <project> --recipe my-style --noise -40dB` uses the recipe's silence policy as the baseline but honors the `-40dB` you typed. The recipe is a baseline; the command line is an override.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-601-studio-recipe
|
|
3
|
+
title: The Studio Recipe — style as a reusable preset
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-601
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-601
|
|
12
|
+
- recipe
|
|
13
|
+
- presets
|
|
14
|
+
difficulty: intermediate
|
|
15
|
+
estimatedMinutes: 5
|
|
16
|
+
prerequisites:
|
|
17
|
+
- N-atel-301-composition-and-overlays
|
|
18
|
+
summary: A Studio Recipe is a reusable bundle of style defaults — silence policy, caption style, phrase grouping — authored once and applied to many projects. Phase 1 ships three implemented sections; partial recipes fall back to code defaults; six Phase 3 fields parse as opaque values for forward-compat.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The value play
|
|
22
|
+
|
|
23
|
+
A Studio Recipe is *your style, saved as a preset.* You spend an afternoon dialing in how your shorts look — how aggressively silence gets trimmed, what font and size the captions render at, how words group into phrases — and then you never tune it by hand again. You write those decisions into one YAML file and apply it to the next project, and the one after that.
|
|
24
|
+
|
|
25
|
+
That's the whole pitch: **a recipe makes a look reproducible.** The same defaults, every project, until you decide to change them in one place.
|
|
26
|
+
|
|
27
|
+
The type lives in `packages/types/src/recipe.ts` as `StudioRecipe`. The top of the file is metadata:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
version: "1.0" # recipe format version — current is "1.0"
|
|
31
|
+
name: "my-shorts-style"
|
|
32
|
+
description: "Punchy captions, tight silence trim"
|
|
33
|
+
author: "@me"
|
|
34
|
+
tags: [shorts, vertical]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## The three Phase 1 sections
|
|
38
|
+
|
|
39
|
+
Phase 1 implements three sections, each consumed by a different pipeline:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
silence_policy: # consumed by `atelier trim`
|
|
43
|
+
noise: "-30dB"
|
|
44
|
+
min_silence: 0.35
|
|
45
|
+
default_padding_pre: 0.08
|
|
46
|
+
default_padding_post: 0.12
|
|
47
|
+
match_tolerance: 0.5
|
|
48
|
+
|
|
49
|
+
caption_style: # consumed by `atelier transcribe` / `captions regenerate`
|
|
50
|
+
font_family: "Inter"
|
|
51
|
+
font_size: 84
|
|
52
|
+
font_weight: "bold" # normal | bold | numeric 100..900
|
|
53
|
+
text_align: "center" # left | center | right
|
|
54
|
+
color: "#FFFFFF"
|
|
55
|
+
y_ratio: 0.85 # 0=top, 1=bottom
|
|
56
|
+
width_ratio: 0.9
|
|
57
|
+
fade_seconds: 0.05
|
|
58
|
+
|
|
59
|
+
caption_grouping: # consumed by `atelier transcribe` phrase grouping
|
|
60
|
+
max_words: 5
|
|
61
|
+
pause_gap: 0.4
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**`silence_policy`** is the silence-trim baseline. Every field maps one-to-one onto an `atelier trim` option — `noise` is the `silencedetect` threshold, `min_silence` is the minimum gap that registers as a cut, the two `default_padding_*` values are the leading/trailing pad on new cuts, and `match_tolerance` is the timing window that preserves your hand-tuned padding when you re-run the trim (the detected-vs-user-edited invariant from ATEL-501).
|
|
65
|
+
|
|
66
|
+
**`caption_style`** is the caption layer's visual presentation — font, color, vertical position as a ratio of canvas height, width as a ratio of canvas width, fade duration. The recipe is the canonical source for caption styling; the CLI doesn't expose per-invocation caption-style flags.
|
|
67
|
+
|
|
68
|
+
**`caption_grouping`** controls how transcribed words clump into on-screen phrases: at most `max_words` per phrase, with a forced break whenever the gap between words exceeds `pause_gap` seconds.
|
|
69
|
+
|
|
70
|
+
## Partial recipes fall back to code defaults
|
|
71
|
+
|
|
72
|
+
Every section — and every field within a section — is optional. A recipe that only sets caption color is completely valid:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
version: "1.0"
|
|
76
|
+
name: "just-the-color"
|
|
77
|
+
caption_style:
|
|
78
|
+
color: "#FFD60A"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Omitted fields fall back to the code defaults baked into `renderRecipeWithDefaults` (in `packages/cli/src/lib/recipe.ts`). Those defaults are the same values the scaffold writes: `noise: -30dB`, `min_silence: 0.35`, caption `font_size: 84`, `max_words: 5`, and so on. The merge is shallow-per-section: your recipe's section overlays the defaults section, so setting one caption field doesn't wipe out the rest.
|
|
82
|
+
|
|
83
|
+
To see the effective values — your recipe overlaid on the defaults — run `atelier recipe show <name> --with-defaults`. It prints the fully-resolved recipe so there's no guessing about what an omitted field will become.
|
|
84
|
+
|
|
85
|
+
## Reserved Phase 3 fields parse as opaque values
|
|
86
|
+
|
|
87
|
+
Five fields are declared in the type but not yet implemented:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
caption_highlight # per-word highlight color animation
|
|
91
|
+
transition_kit # transition defaults between clips
|
|
92
|
+
palette # brand colors / font set
|
|
93
|
+
audio_policy # ducking, music lane
|
|
94
|
+
aspect_targets # multi-aspect render targets
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
These are **reserved for Phase 3** (extraction-of-style-from-a-reference-video). They parse cleanly as opaque values — the schema accepts them as `unknown` — so a recipe that includes them today stays forward-compatible. Phase 1 simply ignores their contents.
|
|
98
|
+
|
|
99
|
+
The one caveat: `atelier recipe validate` emits a forward-compat *warning* (not an error) when a reserved field is present, so you're never caught off-guard that a section you authored isn't doing anything yet. (`overlay_rules`, covered in the next note, was promoted out of this reserved set into first-class Phase 1.5 — it does *not* warn.)
|
|
100
|
+
|
|
101
|
+
## Why a flat preset and not a template engine
|
|
102
|
+
|
|
103
|
+
A recipe is deliberately *data, not behavior.* It carries no logic, no conditionals, no per-layer targeting — just a flat bag of style values that three pipelines read. That keeps recipes diffable, shareable, and safe to validate before they ever touch a document. The richer machinery (templates, presets, variables from ATEL-201) is what the pipelines use internally; the recipe is the human-facing dial.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-701-choosing-output
|
|
3
|
+
title: Choosing an output — a decision guide by destination
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-701
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-701
|
|
12
|
+
- delivery
|
|
13
|
+
- decision-guide
|
|
14
|
+
difficulty: intermediate
|
|
15
|
+
estimatedMinutes: 6
|
|
16
|
+
prerequisites:
|
|
17
|
+
- N-atel-101-the-atelier-format
|
|
18
|
+
summary: One decision guide across every exporter — pick the format by destination (social post, web embed, motion-design handoff, preview), hit the right aspect target, and know when to use the CLI versus the studio Export button.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Start from the destination, not the format
|
|
22
|
+
|
|
23
|
+
The mistake is asking "what can Atelier export?" The right question is "where is this going?" — and the destination picks the exporter for you.
|
|
24
|
+
|
|
25
|
+
| Destination | Format | Command / surface |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Social image post | PNG | `atelier export-image` (or `atelier carousel` for a set) |
|
|
28
|
+
| Open Graph / thumbnail / poster | PNG | `atelier export-image --width …` |
|
|
29
|
+
| Web logo / icon that must scale | SVG | `atelier export-svg` |
|
|
30
|
+
| Motion-design handoff (runtime / AE) | Lottie JSON | `atelier export-lottie` |
|
|
31
|
+
| Social / web video | MP4 | `atelier render` or studio Export |
|
|
32
|
+
| README / chat loop | GIF | `atelier render -f gif` |
|
|
33
|
+
| Web video, modern browsers, small files | WebM | studio Export (WebCodecs) |
|
|
34
|
+
| Frame inspection / debugging | JSON | `atelier still` (default format) |
|
|
35
|
+
|
|
36
|
+
## The four common destinations, decided
|
|
37
|
+
|
|
38
|
+
**Social post.** A still image, fixed pixel size, often square or portrait. → **PNG** via `atelier export-image`. Set your aspect with `--width`/`--height` (or design the canvas to the target). For a whole *set* of posts, see the carousel note below — don't loop the single-frame command.
|
|
39
|
+
|
|
40
|
+
**Web embed.** Two cases. If it's a logo, icon, or any artwork that has to stay crisp at arbitrary sizes → **SVG** via `atelier export-svg`, inlined into markup (drop the `--xml-declaration`). If it's video → **MP4** for reach, **WebM** (studio) when the audience is modern browsers and file size matters.
|
|
41
|
+
|
|
42
|
+
**Motion-design handoff.** You're giving the animation to a design or front-end team to drop into a product → **Lottie** via `atelier export-lottie`. It's the whole document as vector animation, tiny, and playable in every major runtime. Watch the stderr warnings: a clean export means the Lottie matches; a warning means a feature dropped and you should preview it in a player.
|
|
43
|
+
|
|
44
|
+
**Preview.** You just want to see the current frame. → `atelier still` for the resolved-frame JSON (what the renderer sees), or `atelier still --format png` / `atelier export-image` for a quick pixel look. No FFmpeg, no canvas build — both raster previews run on a plain install.
|
|
45
|
+
|
|
46
|
+
## Aspect targets
|
|
47
|
+
|
|
48
|
+
Design the document canvas to the destination's aspect when you can — it's lossless. Common targets:
|
|
49
|
+
|
|
50
|
+
- **1:1 (square)** — 1080×1080. Feed posts, carousels.
|
|
51
|
+
- **4:5 (portrait)** — 1080×1350. Maximizes feed real estate.
|
|
52
|
+
- **9:16 (vertical)** — 1080×1920. Stories, Reels, Shorts, TikTok.
|
|
53
|
+
- **16:9 (landscape)** — 1920×1080. YouTube, web hero video, presentations.
|
|
54
|
+
|
|
55
|
+
On a still export you can also retarget at export time: `atelier export-image hero.atelier --out card.png --width 1200` derives the height to preserve aspect. Override **both** `--width` and `--height` only when you deliberately want a non-aspect-preserving size — it will squash otherwise. For MP4, remember H.264 needs **even** dimensions; an odd canvas is rejected with the next even size suggested.
|
|
56
|
+
|
|
57
|
+
## CLI or the studio Export button?
|
|
58
|
+
|
|
59
|
+
Same artwork, two ways to get a file out:
|
|
60
|
+
|
|
61
|
+
- **The studio Export button** encodes in the browser via WebCodecs — no FFmpeg, no node-canvas, nothing to install. Best for one-off exports while you're editing, and the only path that reaches WebM. Interactive, per-document.
|
|
62
|
+
- **The CLI** (`export-image`, `export-svg`, `export-lottie`, `render`) is scriptable and batchable — wire it into a build, a script, a CI job. Raster and vector exports run on a plain install; **video (`atelier render`) additionally needs FFmpeg on `$PATH` and the `canvas` package** with its cairo/pango stack (see N-atel-701-video).
|
|
63
|
+
|
|
64
|
+
Rule of thumb: editing and exporting one thing → studio. Automating or batching → CLI.
|
|
65
|
+
|
|
66
|
+
## Batch image delivery — see ATEL-601
|
|
67
|
+
|
|
68
|
+
When the job is *many* images at once — a folder of shots composed into a uniform set of posts with consistent overlays — that's `atelier carousel`, covered in **ATEL-601 (Studio Recipes)**. It drives the same single-frame raster path across every input and applies a recipe's handle + page-number overlays per image. This note doesn't re-teach it; reach for ATEL-601 when you need batch delivery rather than a single export.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-701-png-and-frames
|
|
3
|
+
title: PNG and frames — the raster export path
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-701
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-701
|
|
12
|
+
- png
|
|
13
|
+
- raster
|
|
14
|
+
difficulty: intermediate
|
|
15
|
+
estimatedMinutes: 6
|
|
16
|
+
prerequisites:
|
|
17
|
+
- N-atel-101-the-atelier-format
|
|
18
|
+
summary: Single-frame raster output via `atelier export-image` and `atelier still`. The @napi-rs/canvas rasterizer ships prebuilt platform binaries — PNG export works on a plain `npm install`, no brew, no cairo, no node-gyp build.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## One frame, one PNG
|
|
22
|
+
|
|
23
|
+
A whole animation is a sequence of resolved frames. To pull a single one out as an image:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
atelier export-image hello.atelier --out frame.png
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That renders the document's first state at frame `0` to `frame.png`. To pick a different moment, name the state and the frame:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
atelier export-image hello.atelier --out mid-fade.png --frame 15 --state intro
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The flags are exactly:
|
|
36
|
+
|
|
37
|
+
- `-o, --out <path>` — output PNG path (required)
|
|
38
|
+
- `-s, --state <name>` — state name (defaults to the first state)
|
|
39
|
+
- `-f, --frame <number>` — frame within the state (defaults to `0`)
|
|
40
|
+
- `--width <number>` / `--height <number>` — output dimension overrides
|
|
41
|
+
|
|
42
|
+
Pick a frame past the end of the state's `duration` and the command refuses it with `Frame N is out of range for state "X" (duration D)`. The renderer validates the selection before it touches a canvas.
|
|
43
|
+
|
|
44
|
+
## Resizing on export
|
|
45
|
+
|
|
46
|
+
Override one dimension and the other is derived to preserve the document's aspect ratio:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
atelier export-image hero.atelier --out thumb.png --width 400
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A 1920×1080 document exported with `--width 400` comes out 400×225. Override *both* and the renderer uses both verbatim — which can squash the image if the ratio doesn't match. That's deliberate: sometimes you genuinely want a non-uniform target size. Most of the time, set one and let the aspect ratio carry the other.
|
|
53
|
+
|
|
54
|
+
## `still` — the same frame, two formats
|
|
55
|
+
|
|
56
|
+
`atelier still` resolves a single frame too, but it can emit either the resolved-frame JSON or a PNG:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
atelier still hello.atelier --frame 15 # JSON to stdout
|
|
60
|
+
atelier still hello.atelier --frame 15 --format png -o f.png
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `--format json` (default) prints the `resolveFrame` output — every layer's interpolated position, opacity, scale, and resolved visual at that frame. This is the inspection tool: see exactly what the renderer sees before it draws.
|
|
64
|
+
- `--format png` rasterizes that resolved frame. With no `-o, --output`, the PNG bytes go to stdout (pipe it somewhere); with `-o`, it writes a file.
|
|
65
|
+
|
|
66
|
+
So `export-image` is the dedicated "give me a PNG" command with resize flags; `still` is the "show me this frame" command that happens to also rasterize. When you want a sized image file, reach for `export-image`. When you're debugging interpolation, reach for `still --format json`.
|
|
67
|
+
|
|
68
|
+
## The rasterizer: @napi-rs/canvas
|
|
69
|
+
|
|
70
|
+
Both raster paths go through the same backend — `@napi-rs/canvas`, a Canvas2D implementation shipped as **prebuilt platform binaries**. This matters more than it sounds:
|
|
71
|
+
|
|
72
|
+
- No `node-gyp` compile step.
|
|
73
|
+
- No system libraries — no `brew install cairo pango libpng`, no `apt install libcairo2-dev`.
|
|
74
|
+
- It's a hard dependency, so PNG export works the moment `npm install` finishes, on any platform with a prebuilt binary.
|
|
75
|
+
|
|
76
|
+
The renderer pre-loads every `ImageVisual` source first (file path, `data:` URL, or buffer all work server-side), wires them into an `ImageCache`, then renders one resolved frame scaled to the requested output dimensions. If the binary somehow fails to load — a corrupt install, an unsupported platform — you get a typed `CanvasUnavailableError` telling you to reinstall, not a cryptic native crash.
|
|
77
|
+
|
|
78
|
+
> Hold onto this distinction: the **single-frame raster** path (`export-image`, `still --format png`, and `carousel`) needs nothing beyond `npm install`. The **video** path (`atelier render`, covered in N-atel-701-video) is a different backend with different requirements. PNG-clean install does not imply MP4-clean install.
|
|
79
|
+
|
|
80
|
+
## When raster is the right call
|
|
81
|
+
|
|
82
|
+
A PNG is the right output when the destination is a still image: a thumbnail, an Open Graph card, a poster frame, a single social image, a print proof. It's the wrong output for anything that needs to scale crisply at arbitrary sizes (a logo destined for a stylesheet) — that's the vector path in N-atel-701-vector — or anything that moves (the video path in N-atel-701-video).
|
|
83
|
+
|
|
84
|
+
For batch raster delivery — a whole folder of images composed into a uniform set of posts — don't loop `export-image` by hand. ATEL-601's `atelier carousel` drives the same `renderDocumentToPng` path across many inputs with recipe overlays applied. See that track; this note stays on the single-frame primitives.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-701-vector
|
|
3
|
+
title: Vector export — SVG per frame, Lottie per document
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-701
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-701
|
|
12
|
+
- svg
|
|
13
|
+
- lottie
|
|
14
|
+
- vector
|
|
15
|
+
difficulty: intermediate
|
|
16
|
+
estimatedMinutes: 6
|
|
17
|
+
prerequisites:
|
|
18
|
+
- N-atel-101-the-atelier-format
|
|
19
|
+
summary: '`atelier export-svg` emits one frame as a resolution-independent SVG string; `atelier export-lottie` emits the whole animated document as Lottie JSON. When to choose vector over raster — web embeds and motion-design handoff.'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Two vector exporters, two scopes
|
|
23
|
+
|
|
24
|
+
Raster gives you pixels at a fixed size. Vector gives you shapes that stay crisp at any size. Atelier ships two vector exporters, and the scope difference is the thing to internalize:
|
|
25
|
+
|
|
26
|
+
- `atelier export-svg` exports **one frame** as an SVG document.
|
|
27
|
+
- `atelier export-lottie` exports the **whole animated document** as Lottie JSON.
|
|
28
|
+
|
|
29
|
+
One is a still snapshot in vector form; the other is the animation itself, handed to a runtime that plays it.
|
|
30
|
+
|
|
31
|
+
## `export-svg` — a frame as SVG
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
atelier export-svg lockup.atelier --frame 0 --out logo.svg
|
|
35
|
+
atelier export-svg lockup.atelier --state hero --frame 12 # to stdout
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Flags:
|
|
39
|
+
|
|
40
|
+
- `-s, --state <name>` — state name (defaults to the first state)
|
|
41
|
+
- `-f, --frame <number>` — frame within the state (defaults to `0`)
|
|
42
|
+
- `-o, --output <path>` — output path (defaults to stdout)
|
|
43
|
+
- `--xml-declaration` — prepend the `<?xml ...?>` declaration
|
|
44
|
+
|
|
45
|
+
Like the raster path, it resolves a single frame — but instead of rasterizing it, it serializes the resolved layers as SVG elements. Shapes become `<path>` / `<rect>` / `<ellipse>`, text becomes `<text>`, fills and strokes and gradients map to SVG paint. The output scales to any size with no quality loss, which is the whole point: a logo lock-up exported as SVG drops straight into a stylesheet or a React component and renders sharp on a retina display and a billboard alike.
|
|
46
|
+
|
|
47
|
+
Use `--xml-declaration` when the SVG is a standalone file consumed by tooling that wants the prolog; omit it when you're inlining the markup into HTML (where the declaration is noise).
|
|
48
|
+
|
|
49
|
+
## `export-lottie` — the document as Lottie
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
atelier export-lottie intro.atelier --out intro.json
|
|
53
|
+
atelier export-lottie intro.atelier --state hero -o hero.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Flags:
|
|
57
|
+
|
|
58
|
+
- `-s, --state <name>` — state name (defaults to the first state)
|
|
59
|
+
- `-o, --output <path>` — output path (defaults to stdout)
|
|
60
|
+
|
|
61
|
+
Notice what's **missing**: there's no `--frame`. Lottie is an animation format, so the exporter takes the whole timeline, not a single moment. It walks the document's deltas and writes them as Lottie keyframes — opacity tracks, transform tracks, the lot. The result is a JSON file a Lottie player (lottie-web, the iOS/Android runtimes, After Effects via Bodymovin) plays back as vector animation.
|
|
62
|
+
|
|
63
|
+
The exporter is honest about its limits. Anything it can't faithfully translate — an effect with no Lottie equivalent, a visual type outside Lottie's model — comes back as a **warning on stderr**, not a silent omission:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Warning: <unsupported feature> — skipped in Lottie output
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Read those warnings. A clean export means the Lottie matches the document; a warning means a feature dropped on the way out and you should check the result in a player before shipping it.
|
|
70
|
+
|
|
71
|
+
## Vector vs raster — choosing
|
|
72
|
+
|
|
73
|
+
Reach for **vector** when:
|
|
74
|
+
|
|
75
|
+
- **The output embeds in the web and must scale.** An SVG logo or icon stays crisp at every density and size. No `@2x`/`@3x` asset matrix.
|
|
76
|
+
- **You're handing off to motion design.** Lottie is the lingua franca between Atelier and design/runtime teams — tiny files, vector-crisp, playable in every major runtime. Export the document as Lottie and a front-end engineer drops it into a screen with three lines of code.
|
|
77
|
+
- **File size and scalability beat pixel fidelity.** Vector files are small and resolution-independent.
|
|
78
|
+
|
|
79
|
+
Reach for **raster** (N-atel-701-png-and-frames) when:
|
|
80
|
+
|
|
81
|
+
- The artwork is photographic or has effects vector can't express cleanly (complex blurs, blends, image content).
|
|
82
|
+
- The destination wants a fixed-size pixel image (an OG card, a thumbnail).
|
|
83
|
+
- You need a single still and the consumer expects a PNG.
|
|
84
|
+
|
|
85
|
+
The rule of thumb: **SVG for a still that must scale, Lottie for motion handed to a player, raster for everything photographic or pixel-final.** When a Lottie export warns, treat that as a signal that the document leans on something raster (or video) would carry better.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-701-video
|
|
3
|
+
title: Video export — MP4 and GIF via FFmpeg, WebM in the studio
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-701
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-701
|
|
12
|
+
- video
|
|
13
|
+
- ffmpeg
|
|
14
|
+
- webcodecs
|
|
15
|
+
difficulty: intermediate
|
|
16
|
+
estimatedMinutes: 7
|
|
17
|
+
prerequisites:
|
|
18
|
+
- N-atel-101-the-atelier-format
|
|
19
|
+
summary: '`atelier render` writes MP4 or GIF via FFmpeg from the CLI; the browser studio exports via WebCodecs with no FFmpeg dependency. The two paths use different backends with different install requirements — know which one you''re on.'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## `atelier render` — the CLI video path
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
atelier render animation.atelier # → animation.mp4
|
|
26
|
+
atelier render animation.atelier -f gif # → animation.gif
|
|
27
|
+
atelier render animation.atelier -o out/clip.mp4 # custom output path
|
|
28
|
+
atelier render animation.atelier -s intro outro # specific states, in order
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Flags:
|
|
32
|
+
|
|
33
|
+
- `-o, --output <path>` — output path (format inferred from the extension if `--format` is absent)
|
|
34
|
+
- `-f, --format <type>` — `mp4` or `gif`
|
|
35
|
+
- `-s, --state <names...>` — one or more states to render, **in order** (defaults to all states, in document order). This is variadic — `-s intro outro` renders intro then outro into one file.
|
|
36
|
+
|
|
37
|
+
With no flags it renders every state in order to `<name>.mp4`. With `-o foo.gif` and no `--format`, it infers GIF from the extension. The CLI renders **MP4 and GIF only** — there is no `webm` here. (WebM lives in the studio path, below.)
|
|
38
|
+
|
|
39
|
+
The render loop resolves each frame, draws it to a canvas, and pipes the raw frames straight into FFmpeg over stdin — MP4 via `libx264` (CRF 18, `+faststart` so it streams), GIF via single-pass palette generation. A live progress line on stderr reports `frame N/total`, percent, current state, and an ETA.
|
|
40
|
+
|
|
41
|
+
## Two requirements the CLI path enforces
|
|
42
|
+
|
|
43
|
+
**FFmpeg must be on `$PATH`.** Before rendering anything, `atelier render` shells out to `ffmpeg -version`. If it's missing you get an install hint and a non-zero exit, not a half-written file:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
FFmpeg is not installed or not in PATH.
|
|
47
|
+
Install it:
|
|
48
|
+
macOS: brew install ffmpeg
|
|
49
|
+
Ubuntu: sudo apt install ffmpeg
|
|
50
|
+
Windows: https://ffmpeg.org/download.html
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**H.264 requires even dimensions.** MP4 export refuses an odd-width or odd-height canvas with a message that tells you the next even size to try. GIF has no such constraint.
|
|
54
|
+
|
|
55
|
+
## The backend distinction — read this carefully
|
|
56
|
+
|
|
57
|
+
The single-frame raster path (N-atel-701-png-and-frames) uses `@napi-rs/canvas` — prebuilt binaries, no system libraries, works on a plain `npm install`. **The video path does not.**
|
|
58
|
+
|
|
59
|
+
`atelier render` rasterizes frames with the `canvas` (node-canvas) package, which is a separate, optional native dependency that **does** need a toolchain and system libraries:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
The 'canvas' package is not installed.
|
|
63
|
+
Install it with:
|
|
64
|
+
npm install canvas
|
|
65
|
+
Prerequisites vary by OS:
|
|
66
|
+
macOS: brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman
|
|
67
|
+
Ubuntu: sudo apt install build-essential libcairo2-dev libpango1.0-dev ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
So "PNG export works on a clean install" is true — and it does **not** imply "MP4 export works on a clean install." Video from the CLI asks for two things the raster path never does: FFmpeg on `$PATH` and the `canvas` package with its cairo/pango stack. If you only ever need stills, you never touch any of it.
|
|
71
|
+
|
|
72
|
+
## The studio path — WebCodecs, no FFmpeg
|
|
73
|
+
|
|
74
|
+
The browser studio doesn't shell out to FFmpeg and can't install cairo. Its Export button encodes video with **WebCodecs**, the browser's native video-encode API. That's the headline tradeoff:
|
|
75
|
+
|
|
76
|
+
- **No external dependency.** A user with a modern browser exports video with nothing installed — no FFmpeg, no node-canvas, no toolchain. The encoder is in the browser already.
|
|
77
|
+
- **Different format reach.** WebCodecs handles the codecs the browser ships (commonly MP4 and WebM). FFmpeg, on the CLI, reaches whatever your local FFmpeg build supports.
|
|
78
|
+
- **Different scale point.** The studio path is interactive and per-document — you hit Export and get a file. The CLI path is scriptable and batchable — you wire `atelier render` into a build, a Makefile, a CI job.
|
|
79
|
+
|
|
80
|
+
## Choosing a video target
|
|
81
|
+
|
|
82
|
+
- **Social / web video** → MP4 (H.264). Universal playback, good compression. `atelier render clip.atelier` or the studio Export button.
|
|
83
|
+
- **Short looping animation, chat/README embed** → GIF. Larger files, no audio, but plays everywhere with no `<video>` tag. `atelier render clip.atelier -f gif`.
|
|
84
|
+
- **Web video where size matters and the audience is modern browsers** → WebM, via the studio.
|
|
85
|
+
- **Automated/batch rendering** → the CLI (`atelier render`), so it scripts.
|
|
86
|
+
- **One-off export while editing** → the studio, so you skip the FFmpeg/canvas install entirely.
|
|
87
|
+
|
|
88
|
+
The destination decides the format; the *how* (CLI vs studio) decides the dependencies. N-atel-701-choosing-output ties this together with the still and vector paths into one decision guide.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: N-atel-801-editing-surface
|
|
3
|
+
title: The editing surface — layers, image drop, and slider commit-gating
|
|
4
|
+
type: note
|
|
5
|
+
track: ATEL-801
|
|
6
|
+
author: atelier
|
|
7
|
+
created: '2026-05-20'
|
|
8
|
+
updated: '2026-05-20'
|
|
9
|
+
tags:
|
|
10
|
+
- course
|
|
11
|
+
- atel-801
|
|
12
|
+
- studio
|
|
13
|
+
- editing
|
|
14
|
+
- undo
|
|
15
|
+
difficulty: advanced
|
|
16
|
+
estimatedMinutes: 5
|
|
17
|
+
prerequisites:
|
|
18
|
+
- N-atel-201-mcp-overview
|
|
19
|
+
summary: The studio's hands-on surface — the Text/Shape/Image/Handle/Page Number layer-type picker, image drag-drop via createImageLayerFromFile (aspect-fit, centered, first drop replaces the `background` placeholder), the canvas/layer/property/yaml panels, and slider commit-gating (onInput live preview vs onChange commit) so one drag is one undo.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The four panels
|
|
23
|
+
|
|
24
|
+
The studio is a two-column layout: the canvas on the left, and a right panel that hosts the **layer panel**, the **property panel**, and the **yaml panel** (the studio opens on the YAML tab by default). The canvas renders the resolved frame; the layer panel lists and reorders layers; the property panel edits the selected layer's properties; the yaml panel shows the live `.atelier` source. Every panel reads from one document — edit in any of them and the others refresh.
|
|
25
|
+
|
|
26
|
+
## The layer-type picker
|
|
27
|
+
|
|
28
|
+
The layer panel's Add menu offers five variants, each a one-glyph icon plus a label:
|
|
29
|
+
|
|
30
|
+
| Variant | Icon | What it adds |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `text` | T | a TextVisual layer |
|
|
33
|
+
| `shape` | ■ | a ShapeVisual layer |
|
|
34
|
+
| `image` | ▣ | an ImageVisual layer (opens a file picker) |
|
|
35
|
+
| `handle` | @ | an overlay-tagged handle (`@username`, credit, watermark) |
|
|
36
|
+
| `page-number` | # | an overlay-tagged page-number (`{current}/{total}`) |
|
|
37
|
+
|
|
38
|
+
`Handle` and `Page Number` carry `tags: ["overlay"]` so the layer-tag isolation invariant (ATEL-301, ATEL-501) protects them across recipe re-runs — the studio's Add menu generates the same overlay shape the MCP convenience tools and recipes do. The other three are plain content layers.
|
|
39
|
+
|
|
40
|
+
## Image drag-drop
|
|
41
|
+
|
|
42
|
+
The headline image workflow is drag-and-drop. Drop one or more image files onto the canvas and each becomes an ImageVisual layer. The same path runs whether you drop on the canvas or pick a file via the layer panel's Add → Image menu; both call `createImageLayerFromFile`.
|
|
43
|
+
|
|
44
|
+
The geometry is fixed for you by `fitImageToCanvas`:
|
|
45
|
+
|
|
46
|
+
- **Aspect-fit.** A landscape-relative image fits to canvas width; a portrait-relative image fits to canvas height. Each dimension is capped at the canvas extent, so a square image into a square canvas fills exactly.
|
|
47
|
+
- **Centered.** The frame is `{ x: canvas.width / 2, y: canvas.height / 2 }`, combined with a `0.5 / 0.5` anchor, so the image's centerpoint sits at canvas center.
|
|
48
|
+
|
|
49
|
+
Two placement rules govern where the new layer lands in the z-stack:
|
|
50
|
+
|
|
51
|
+
1. **First-drop-replaces.** When the FIRST image of a drop batch lands and `doc.layers[0]?.id === "background"`, it swaps that placeholder layer **in place**. That's how the welcome scaffold's `background` placeholder gets filled by your first image. `createImageLayerFromFile` is called with `replaceBackground: true` only for the first file in the batch.
|
|
52
|
+
2. **Everything else unshifts.** Subsequent images are `unshift`-ed onto the layers array. The renderer paints index 0 first (bottom of the z-stack), so new images sit behind existing overlays like a handle or page-number — the desired composition order.
|
|
53
|
+
|
|
54
|
+
Each layer also gets a matching `doc.assets` entry registered alongside it, keyed by `${id}-asset`. Non-image files are silently filtered out before the helper runs.
|
|
55
|
+
|
|
56
|
+
## Slider commit-gating — one drag, one undo
|
|
57
|
+
|
|
58
|
+
The property panel's sliders distinguish two events, and the difference is what keeps undo sane:
|
|
59
|
+
|
|
60
|
+
- **`onInput`** (continuous, during a drag) → `onPropertyPreview`: a **cheap canvas re-render only**. No undo snapshot. No host notification. You get live feedback as you drag.
|
|
61
|
+
- **`onChange`** (once, on release) → `onPropertyChange`: the **commit**. Snapshot undo history, then notify the host (which triggers autosave).
|
|
62
|
+
|
|
63
|
+
So dragging a slider produces exactly **one** undo entry, not one per pixel of travel. If a control doesn't distinguish the two events, the preview hook degrades to a full commit, so behavior is never worse than committing on every tick — but the sliders that matter (opacity, transforms) split them.
|
|
64
|
+
|
|
65
|
+
This was the **slider-commit-flood** bug: in the old un-typechecked string form of the client, the commit fired on every `input` event, flooding the undo stack and re-saving on every frame of a drag. Like the autosave save-race, it only became fixable once the client moved into a real, compiler-visible module.
|
|
66
|
+
|
|
67
|
+
## Why this surface exists
|
|
68
|
+
|
|
69
|
+
Everything here is something a human does directly — pick a layer type, drop a photo, nudge a slider. The next note introduces the other editor at the same canvas: an agent, mutating the same document over a WebSocket while you watch.
|