@flatkit/compiler 0.26.0 → 0.28.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.
@@ -0,0 +1,107 @@
1
+ # Getting started
2
+
3
+ > Prerequisite: the `flatc` CLI — `pnpm add -D @flatkit/compiler` (or run it from this repo with
4
+ > `pnpm flatc …`). See [Tooling](tooling.md) for the full CLI.
5
+
6
+ ## 1. Your first scene
7
+
8
+ Create `hello.flatink`:
9
+
10
+ ```
11
+ size 320 240
12
+ background #0a0e1c
13
+
14
+ scene {
15
+ layer "main" {
16
+ circle 160 120 48 fill #ffcc00
17
+ text "Hello, FlatInk" at 0,190 font "sans-serif" size 24 align center line 1.2 color #ffffff box 320 40
18
+ }
19
+ }
20
+ ```
21
+
22
+ - `size` is **required and must come first** (the canvas, in scene units).
23
+ - `scene { … }` holds **layers**; layers hold **items** (`circle`, `text`, `path`, `image`, `group`…).
24
+ - Coordinates are plain numbers; the origin is the top-left of the canvas.
25
+ - On `text` (and `image`), `at x,y` comes **right after the content**, before `font`/`box`/`fill` — not at
26
+ the end. `text "…" box W H at x,y` fails; write `text "…" at x,y box W H`.
27
+
28
+ Compile and look at it:
29
+
30
+ ```sh
31
+ flatc hello.flatink -o hello.flatpack # → a single playable file
32
+ flatc hello.flatink --render -o hello.png # → a PNG, to see what you drew (needs skia-canvas)
33
+ ```
34
+
35
+ ## 2. Make it move
36
+
37
+ Animation comes from **channel expressions** in a behavior block, which target a **named object**
38
+ (`group`/`instance`/`text`). A bare shape can't carry channels — its `as "<id>"` name only makes it
39
+ addressable for **text-on-path** — so wrap it in a named **group** (the name lives on the group), then
40
+ drive a channel:
41
+
42
+ ```
43
+ scene {
44
+ layer "main" {
45
+ group "Sun" at 160,120 pivot 0,0 { layer "art" { circle 0 0 48 fill #ffcc00 } }
46
+ }
47
+ }
48
+
49
+ object "Sun" {
50
+ scaleX = 1 + sin(clock * 3) * 0.1 // gentle pulse
51
+ scaleY = 1 + sin(clock * 3) * 0.1
52
+ }
53
+ ```
54
+
55
+ `clock` is seconds elapsed, **monotone**; `sin`/`cos` and friends are built in (see
56
+ [Expressions](expressions-and-stdlib.md)). Channels you can bind: `x`, `y`, `scaleX`, `scaleY`,
57
+ `rotation`, `opacity`.
58
+
59
+ > **`clock`, not `time`.** `time` restarts at 0 every `durationFrames` (2.5 s by default), so free-running
60
+ > motion **jumps** on each loop and a one-shot ramp **replays**. Use `time` only for motion you tuned to
61
+ > the loop itself; use `clock` for ambience and for any instant you capture and compare later. `--check`
62
+ > warns when a channel reads `time` under a short timeline — including through a function you wrote.
63
+
64
+ ## 3. Make it react
65
+
66
+ Add state (`var`) and an event handler:
67
+
68
+ ```
69
+ var score = 0
70
+
71
+ scene {
72
+ layer "main" {
73
+ group "Sun" at 160,120 pivot 0,0 { layer "art" { circle 0 0 48 fill #ffcc00 } }
74
+ text "Score: {}" at 0,10 bind "score" box 320 40 font "sans-serif" size 20 align center line 1.2 color #fff
75
+ }
76
+ }
77
+
78
+ object "Sun" {
79
+ when clicked { score = score + 1 }
80
+ scaleY = self.grabbed ? 0.92 : 1 // squash while pressed
81
+ }
82
+ ```
83
+
84
+ - `var score = 0` declares interactive state.
85
+ - `when clicked { … }` runs actions on click.
86
+ - `text "… {}" bind "score"` shows the live value (the `{}` slot).
87
+ - `self.grabbed` is this object's own interaction state — see [feedback](behavior-and-interactions.md#feedback).
88
+
89
+ ## 4. Play it
90
+
91
+ In a web page:
92
+
93
+ ```js
94
+ import { FlatPlayer } from '@flatkit/player'
95
+
96
+ const canvas = document.querySelector('canvas')
97
+ const doc = await fetch('hello.flatpack').then((r) => r.json())
98
+ const player = new FlatPlayer(canvas, doc, { autoplay: true })
99
+ ```
100
+
101
+ Or verify it headlessly (great in CI) without a browser — see [Tooling → headless play](tooling.md#headless-play--play).
102
+
103
+ ## Where next
104
+
105
+ - Draw richer scenes → **[Scene & drawing](scene-and-drawing.md)**
106
+ - Drag/drop, interactors, feedback → **[Behavior & interactions](behavior-and-interactions.md)**
107
+ - The full `flatc` CLI → **[Tooling](tooling.md)**
@@ -0,0 +1,162 @@
1
+ # Host integration — embedding the player in an app
2
+
3
+ A `.flatpack` is not a video: it has **state** and it can **talk back**. This guide is the receiving
4
+ end — how the page that mounts `@flatkit/player` listens to a scene, drives it, and tears it down.
5
+
6
+ The scene side of the contract lives in
7
+ [Behavior & interactions](behavior-and-interactions.md#send--talking-to-the-host).
8
+
9
+ ## Mount
10
+
11
+ ```js
12
+ import { FlatPlayer, loadEmbeddedFonts } from '@flatkit/player'
13
+
14
+ const doc = await fetch('activity.flatpack').then((r) => r.json())
15
+ await loadEmbeddedFonts(doc) // BEFORE mounting (see embedding-fonts.md)
16
+
17
+ const player = new FlatPlayer(canvas, doc, {
18
+ autoplay: true,
19
+ onEvent: (e) => handle(e), // the `send` channel
20
+ })
21
+ ```
22
+
23
+ | Option | Default | What it does |
24
+ |---|---|---|
25
+ | `autoplay` | `false` | starts the timeline on mount |
26
+ | `loop` | `true` | loops the timeline |
27
+ | `padding` | `0` | margin around the page, in CSS px |
28
+ | `audio` | `true` | `false` mutes `sound "…"` and audio tracks |
29
+ | `input` | `true` | `false` = non-interactive preview: it animates but ignores pointer **and keyboard** |
30
+ | `render` | `true` | `false` = headless (logic + `send`s only, no Canvas API needed) |
31
+ | `resolveAsset` | embedded only | maps an asset to a URL. Default: embedded `data:` URIs only — see [Security](#security) |
32
+ | `onEvent` | — | called on every `send` |
33
+
34
+ ## Receiving events (`send` → `onEvent`)
35
+
36
+ `onEvent` gets **one object per `send`**, synchronously, during the tick that fired it. The type is
37
+ exported — `import type { SendEvent } from '@flatkit/player'`:
38
+
39
+ ```ts
40
+ type SendEvent = {
41
+ name: string // the event name, e.g. "save"
42
+ value?: number | string // number payload, or text("…") content
43
+ fields?: Record<string, number> // record payload — named numbers
44
+ }
45
+ ```
46
+
47
+ Which key is present depends on the payload form the scene used:
48
+
49
+ | In the scene | The host receives |
50
+ |---|---|
51
+ | `send "win"` | `{ name: 'win' }` |
52
+ | `send "score", lives * 100` | `{ name: 'score', value: 300 }` |
53
+ | `send "answer", text("txtCard")` | `{ name: 'answer', value: 'Bonjour' }` |
54
+ | `send "save", { x = px, y = py, doors }` | `{ name: 'save', fields: { x: 12, y: 40, doors: 3 } }` |
55
+
56
+ `value` and `fields` are mutually exclusive today — a record carries no positional `value`. Write the
57
+ handler so an unknown/absent key is simply ignored, and it stays forward-compatible:
58
+
59
+ ```js
60
+ function handle(e) {
61
+ switch (e.name) {
62
+ case 'score': setScore(e.value ?? 0); break
63
+ case 'save': setState((s) => ({ ...s, ...e.fields })); break // a record IS a state patch
64
+ case 'win': finish(); break
65
+ }
66
+ }
67
+ ```
68
+
69
+ Three properties worth relying on:
70
+
71
+ - **Fire-and-forget.** Nothing is returned to the scene; the return value of `onEvent` is ignored.
72
+ - **Exception-safe.** If your callback throws, the player catches, logs, and keeps playing — a broken
73
+ host handler never freezes the activity. (So do your own error reporting inside it.)
74
+ - **Vetted.** Everything crossing the boundary is validated by the player, not trusted from the
75
+ document: the event name matches `[A-Za-z_][A-Za-z0-9_-]{0,63}`, numbers are finite (`NaN`/`Infinity`
76
+ → `0`), text is truncated at 4096 characters, and a record carries at most 32 fields whose names are
77
+ plain identifiers — never `__proto__`, `constructor` or `prototype`. Spreading `e.fields` into your
78
+ own state cannot pollute a prototype.
79
+
80
+ ## Driving the scene from the host
81
+
82
+ The state variables (`var` in the DSL, "Layer B") are readable and writable both ways:
83
+
84
+ ```js
85
+ player.setVar('difficulty', 2) // host → scene (redraws immediately; arrays are cloned)
86
+ player.getVar('score') // scene → host: number | number[] | undefined (arrays copied)
87
+ player.allVars() // snapshot of everything, for debugging/save states
88
+ ```
89
+
90
+ `getVar`/`allVars` return **copies**: mutating the result never touches the running scene. Symmetrically
91
+ `setVar` clones what you pass in.
92
+
93
+ Playback control mirrors the DSL actions: `play()`, `pause()`, `toggle()`, `stop()`, `seek(frame)`,
94
+ plus the read-only `currentFrame`, `isPlaying`, `fps`, `duration`. `load(doc)` swaps the document in
95
+ place, and `render()` forces a repaint (useful after a late font settles).
96
+
97
+ ## Keyboard
98
+
99
+ `keys.<Key>` in an expression is `1` while the key is held. The name is the browser's
100
+ `KeyboardEvent.key` value — `keys.ArrowRight`, `keys.a`, `keys.Escape` — plus one alias: the space bar
101
+ (`key === ' '`) is also exposed as **`keys.Space`**.
102
+
103
+ The listeners are attached to the **window** (a scene reacts immediately, with no click-to-focus step),
104
+ but the player is a good citizen about it — you should not have to do anything:
105
+
106
+ - **It never steals what you are typing.** A keystroke headed to an `<input>`, `<textarea>`, `<select>`
107
+ or any `contenteditable` element of the host page is ignored by the scene.
108
+ - **It only consumes the keys the scene actually declares.** The player scans the document for
109
+ `keys.<Name>` and calls `preventDefault()` on those alone: an activity bound to the arrows stops
110
+ scrolling the page under it, while every other key keeps its native behavior. Browser/OS shortcuts
111
+ (any `Ctrl`/`Cmd`/`Alt` combination), `Tab` and the function keys are never consumed, whatever the
112
+ scene declares.
113
+ - **A key never stays stuck.** Losing the window (alt-tab, an iframe taking the focus) releases the
114
+ held keys, even though the browser delivers no `keyup` in that case.
115
+
116
+ `input: false` remains the total opt-out: no pointer and no keyboard listener at all.
117
+
118
+ **On-screen controls.** There is no keyboard on a phone, so a key can also be driven programmatically —
119
+ wire your own D-pad to `setKey`, and the scene cannot tell the difference:
120
+
121
+ ```js
122
+ btn.addEventListener('pointerdown', () => player.setKey('ArrowRight', true))
123
+ btn.addEventListener('pointerup', () => player.setKey('ArrowRight', false))
124
+ ```
125
+
126
+ A key stays held until released, so pair every `true` with a `false` (a `pointercancel`/`pointerleave`
127
+ handler too, or a finger sliding off the button leaves the scene running).
128
+
129
+ ## Teardown
130
+
131
+ ```js
132
+ player.destroy() // pauses, releases the window/canvas listeners and the pending timers
133
+ ```
134
+
135
+ Always call it when unmounting (a React `useEffect` cleanup, a route change…). Skipping it leaves
136
+ `keydown`/`resize` listeners attached to the window.
137
+
138
+ ## Security
139
+
140
+ A `.flatpack` is **untrusted input** — treat it like third-party HTML, not like your own code. The
141
+ player is built for that: no `eval`, bounded per-tick work, and **no network access by default** (only
142
+ the assets embedded as `data:` URIs are loaded). To serve external assets, pass an explicit resolver so
143
+ the *host* picks the origin:
144
+
145
+ ```js
146
+ import { FlatPlayer, sameOriginAssetResolver } from '@flatkit/player'
147
+ new FlatPlayer(canvas, doc, { resolveAsset: sameOriginAssetResolver('/activities/42/') })
148
+ ```
149
+
150
+ Read [SECURITY.md](../SECURITY.md) for the full threat model.
151
+
152
+ ## Testing the integration without a browser
153
+
154
+ `flatc … --play --script gestures.json` replays a gesture script headlessly and prints the `sends` (with
155
+ their `value`/`fields`) plus the final variables — the same objects your `onEvent` would receive. It is
156
+ the cheapest way to lock the host contract in CI. See [Tooling](tooling.md).
157
+
158
+ ## See also
159
+
160
+ - What the scene can emit → **[Behavior & interactions](behavior-and-interactions.md#send--talking-to-the-host)**
161
+ - Registering the doc's embedded fonts → **[Embedding fonts](embedding-fonts.md)**
162
+ - Headless replay and CI → **[Tooling](tooling.md)**
@@ -0,0 +1,168 @@
1
+ # Scene & drawing
2
+
3
+ Everything inside `scene { … }` is composition. The tree is **layers → items**; items can be shapes,
4
+ text, images, or **groups** (which nest their own layers).
5
+
6
+ ```
7
+ scene {
8
+ layer "bg" { rect 0 0 480 320 fill #0a0e1c }
9
+ layer "game" {
10
+ group "Hero" at 240,160 { layer "c" { circle 0 0 30 fill #ffcc00 } }
11
+ }
12
+ }
13
+ ```
14
+
15
+ Layers stack bottom-to-top. A `layer` takes `"name"` and options (`opacity <n>`, `locked`, `hidden`).
16
+
17
+ ## Shapes
18
+
19
+ Sugar primitives (normalized to a `path` on save — no need to hand-compute Beziers):
20
+
21
+ ```
22
+ circle <cx> <cy> <r>
23
+ ellipse <cx> <cy> <rx> <ry>
24
+ rect <x> <y> <w> <h> # · <r> for uniform rounded corners · <rx> <ry> for distinct
25
+ path "M0 0 L10 0 L10 10 Z" # raw SVG path data
26
+ circle 100 100 40 as "Ring" # name a shape (right after the geometry) → addressable, e.g. text `along "Ring"`
27
+ ```
28
+
29
+ ### Fill, stroke, opacity
30
+
31
+ ```
32
+ circle 0 0 20 fill #ff3366
33
+ path "…" fill #000 stroke #ffffff 3 cap round join round # stroke: <color> <width> [cap] [join] [miter n] [dash a,b]
34
+ path "…" nofill stroke #888 2 # outline only (a line, a thread)
35
+ rect 0 0 40 40 fill #00aaff opacity 0.5 # 0..1 (8-digit hex alpha also works)
36
+ ```
37
+
38
+ ### Paints (gradients)
39
+
40
+ ```
41
+ fill linear(90, 0:#bdecff, 1:#2f8fe0) # angle: 0 = →, 90 = ↓ ; stops are offset:color
42
+ fill radial(0.5, 0.5, 0.5, 0:#fff, 1:#000) # cx, cy, r (0..1), then stops
43
+ ```
44
+
45
+ A stop's color can be a symbol **`color` param** instead of a literal — so a gradient (a halo, a glow) is
46
+ recolorable per instance, like `fill <param>` is for a solid. An optional `@alpha` (0..1) sets the stop's
47
+ alpha, since a param is a 6-digit hue:
48
+
49
+ ```
50
+ params { color teinte = #ffe9a8 }
51
+ circle 0 0 60 fill radial(0.5, 0.5, 0.5, 0:teinte@0.8, 1:teinte@0) # same hue, alpha fading 0.8 → 0
52
+ ```
53
+
54
+ Param and literal stops mix freely (`0:teinte@0.8, 0.5:#3366ffcc, 1:teinte@0`). See
55
+ [exposed parameters](animating-symbols.md#exposed-parameters-params).
56
+
57
+ ### Filters
58
+
59
+ `filter` works on any item (shape, text, image, group) — no need to wrap in a group:
60
+
61
+ ```
62
+ filter glow <blur> <color>
63
+ filter shadow <dx> <dy> <blur> <color>
64
+ filter blur <radius>
65
+ filter adjust <brightness> <contrast> <saturate> <hue>
66
+ ```
67
+
68
+ Filters on **static** decor are cached (nearly free); on **animated** elements they recomposite every
69
+ frame — keep those small (see the [gotchas](dsl-gotchas.md) for the perf details).
70
+
71
+ ## Clipping & masks
72
+
73
+ Cut a container's content to a viewport — scroll panes, reveal windows, framed cards:
74
+
75
+ ```
76
+ group "Viewport" at 20,20 clip 0 0 120 80 { layer "c" { … } } // rectangular clip, in the group's LOCAL coords
77
+ mask layer "Window" { circle 60 60 50 fill #fff layer "c" { … } } // arbitrary clip shape (the mask's matter clips its child layers)
78
+ ```
79
+
80
+ - **`clip <x> <y> <w> <h>`** on a `group`/`instance` cuts everything outside the rectangle. **Render-only**:
81
+ hit-testing and the auto-size bbox ignore it (the clipped-away area stays clickable / counts toward
82
+ framing) — it's a visual cut, not a hit/layout change.
83
+ - For an **arbitrary** clip shape, use a **`mask` layer**: its material (the shapes drawn directly in it)
84
+ clips its **child layers**. See the [gotchas](dsl-gotchas.md) for the clip/mask details.
85
+
86
+ ## Text
87
+
88
+ ```
89
+ text "Hello" font "sans-serif" size 24 align center line 1.2 color #ffffff box 200 40
90
+ text "OUTLINE" font "sans-serif" size 64 color #ffd23f stroke #e23b3b 6 join round # outlined text
91
+ ```
92
+
93
+ - `box <w> <h>` sets the text box; `align left|center|right`; `line` = line-height; `bold` / `italic`.
94
+ - `stroke <color> <width> [cap …] [join …] [miter n] [dash a,b]` outlines the glyphs (same grammar as
95
+ paths). The stroke is drawn **behind** the fill, so the fill keeps its full weight. Accepts a gradient
96
+ paint too (`stroke linear(…) 4`).
97
+ - **Word-wrap is opt-in**: add `wrap` to break at spaces within the box width (otherwise only explicit `\n` wraps).
98
+ - **Live text**: `text "Angle: {}°" bind "round(a)" decimals 1` evaluates the expression every frame and
99
+ fills the `{}` slot (or replaces the whole string if there's no `{}`).
100
+ - **Stable id**: `text "…" as "myId"` lets behavior reference it via `text("myId")` (e.g. in a `send`
101
+ payload). Without `as`, the id is auto-generated and not referenceable.
102
+
103
+ ### Text on a path
104
+
105
+ Lay glyphs **along a curve** instead of a straight baseline — banners, badges, ribbons, dials:
106
+
107
+ ```
108
+ text "SURF CLUB" along "Banner" align center # follow a NAMED shape's outline
109
+ text "loop" along path "M0 80 C120 0 360 0 480 80" # …or inline SVG path data
110
+ ```
111
+
112
+ - **`along "<id>"`** follows a **named shape** (`circle`/`rect`/`ellipse`/`path … as "<id>"`). A *closed*
113
+ named shape (circle/ellipse) anchors the run **upright, centered over the top** by default. **`along path
114
+ "<d>"`** takes inline path data instead — baked **literally**, so you own its start/direction.
115
+ - **`align`** reuses the text alignment: `left` starts the run at the anchor, `center` centers on it,
116
+ `right` ends on it. **`start <0..1>`** moves the anchor along the curve (fraction of its length). On an
117
+ **open** path the anchor defaults to the start (0), so to center a label *on the path* use `align center
118
+ start 0.5` — with `start 0`, `center`/`right` push the run off the near end and those glyphs are dropped.
119
+ (Closed paths wrap, so `start 0` already centers over the top.)
120
+ - **`side over|under`** — which side of the curve the run sits on: `over` = outside (default), `under` = inside.
121
+ - **`spacing <px>`** — extra tracking per glyph (may be negative; the effective advance is floored at 1px).
122
+ - **Animate** by quoting the value (it becomes an expression, same scope as `bind`: `time`, `frame`,
123
+ `clock`, vars): `start "time * 0.1"` scrolls the run along the path (**marquee** — wraps on a closed
124
+ shape); `spacing "sin(clock) * 4"` eases the tracking.
125
+ - `along` replaces `at`/`box`/`wrap` (a path-laid run is not box-wrapped). A run longer than the path drops
126
+ its trailing glyphs, and `flatc` warns (`… overflows its path (~Npx > Lpx)`).
127
+
128
+ ## Images
129
+
130
+ ```
131
+ asset "logo" "logo.svg" image // declare the media (top of file) — embedded by flatc
132
+ scene { layer "c" { image "logo" 80 80 at -40,-40 } } // the origin is the top-left → center with at -w/2,-h/2
133
+ ```
134
+
135
+ ## Transforms & placement
136
+
137
+ On any group / instance / text / image:
138
+
139
+ ```
140
+ at <x>,<y> # translation
141
+ matrix(a,b,c,d,e,f) # full affine
142
+ at center · at center,540 · at 120,center # canvas-relative anchor (resolved from `size`)
143
+ align <point> of "Name" [offset dx,dy] # pin this item's origin onto another item's bbox
144
+ ```
145
+
146
+ > **Placement & naming gotchas.** The order is fixed: **content → `as "…"` → `at …` / `matrix(…)` →
147
+ > style attributes** (`font`/`box`/`fill`/…). So `text "…" box W H at x,y` fails — write `text "…" at x,y
148
+ > box W H`; and `image "logo" 80 80 at 0,0 as "L"` fails — write `image "logo" 80 80 as "L" at 0,0`
149
+ > (same for `instance "Sym" as "hero" at x,y`). **Shapes** name themselves with `as
150
+ > "<id>"` **right after the geometry** (`circle cx cy r as "Ring" fill …`); a named shape is addressable by
151
+ > **text-on-path** (`along "<id>"`). To drive a shape from *behavior* (clicks, `send`, drop zones), still
152
+ > wrap it in a `group "Name"` — the region name addresses geometry, the group name an interactive object.
153
+
154
+ `align` points: `center`, `top`, `bottom`, `left`, `right`, `topleft`, `topright`, `bottomleft`,
155
+ `bottomright`. It uses the target's **static** bbox (no expression channels) — placement only, no flow
156
+ layout (stack with `repeat` + `$()`, see [factoring](behavior-and-interactions.md#reuse--factoring)).
157
+
158
+ ## Other item attributes
159
+
160
+ - `pivot <x>,<y>` — origin offset (the rotation/scale center).
161
+ - `tint <color> <amount>` — Flash-style tint, `amount` 0..1. `<color>` may be a symbol `color` param (`tint teinte 0.4`), recolorable per instance.
162
+ - `nohit` — stays **drawn** but ignored by hit-testing (clicks pass through). On a group, applies to the
163
+ whole subtree. Ideal for a decorative full-screen veil.
164
+
165
+ ## See also
166
+
167
+ - Animate and react → **[Behavior & interactions](behavior-and-interactions.md)**
168
+ - Reuse shapes (symbols, `repeat`, `each`) → **[Behavior & interactions → Reuse](behavior-and-interactions.md#reuse--factoring)**
@@ -0,0 +1,236 @@
1
+ # Tooling — the `flatc` CLI
2
+
3
+ `flatc` compiles `.flatink` text into a single `.flatpack`, and helps you **see**, **test**, and
4
+ **ship** scenes. Install with `pnpm add -D @flatkit/compiler` (or `pnpm flatc …` in this repo).
5
+ `flatc --help` lists everything.
6
+
7
+ ## Files
8
+
9
+ | File | What |
10
+ |---|---|
11
+ | `.flatink` | the program (composition + behavior, the DSL) |
12
+ | `.flat` | a visual asset library (symbols), exported by the editor |
13
+ | `.flatpack` | the baked, playable `Doc` (JSON) — what the player runs |
14
+
15
+ ```
16
+ flatc game.flatink hero.flat -o game.flatpack
17
+ ```
18
+
19
+ `.flat` libs in the program's folder are discovered automatically; media declared by
20
+ `asset "id" "path" kind` are embedded as `data:` URIs.
21
+
22
+ > **The extension decides how a file is read**, so it decides what `--check` verifies. A `.flat` is read
23
+ > as a bag of symbols: a whole *program* saved under that name has no symbols in it, so `--check` used to
24
+ > report "0 symbol(s)" and exit 0 with nothing verified. `flatc` now **refuses** a `.flat` that contains a
25
+ > `scene { … }` block or `object` blocks. When checking a program, make sure it is named `.flatink`.
26
+ >
27
+ > **`--no-libs`** turns off the folder auto-discovery. In a working folder a neighbouring scratch file is
28
+ > not a dependency — and when one of them fails to parse, the error now names the file.
29
+
30
+ ## Compile & check
31
+
32
+ ```
33
+ flatc <program.flatink> [-o out.flatpack]
34
+ flatc <program.flatink> --check # semantic lint only (exits ≠0 on ERROR; warnings don't block)
35
+ flatc <program.flatink> --check --no-libs # …without pulling in the .flat files sitting next to it
36
+ flatc <program.flatink> --watch # recompile on every change in the folder
37
+ flatc <library.flat> [more.flat …] --check # lint an asset LIB per-symbol (several .flat are merged)
38
+ ```
39
+
40
+ `--check` lints a program **or** an asset library (`.flat`): the same per-symbol checks (params-in-`expr`,
41
+ undeclared color param in a paint, unknown functions/objects) run on a lib's symbols — no need to compile a
42
+ preview first.
43
+
44
+ `--check` also covers approximate **layout** warnings: text overflowing the canvas, **wrapped text taller
45
+ than its box** (or holding a single word too wide to break), clipped items, missing/overlapping drop zones,
46
+ never-used variables, and a `color` param used as a paint (a `fill`/`stroke`, a gradient stop `0:teinte@…`,
47
+ or a `tint`) that the symbol doesn't declare — a silent "dead recolor". The layout passes descend **into
48
+ groups** and measure in world coordinates, and they skip anything positioned at runtime (a bound or dragged
49
+ item, and everything nested under one, has no meaningful static position).
50
+
51
+ It flags an instant **captured on `time` and read by `pulse`/`shake`** — both ride the monotone `clock`, so
52
+ the two axes never meet and the ramp never fires, with nothing on screen to say so. The costliest kind of
53
+ bug in a codebase migrated from 0.21; see the
54
+ [gotchas](dsl-gotchas.md#feedback-reactions-in-one-line).
55
+
56
+ ### Rendering from code — one frame, or three hundred
57
+
58
+ `renderDocToPng(doc, opts)` draws one image. For a GIF, an MP4, a contact sheet or a loop check, hold a
59
+ renderer OPEN instead: the expensive setup — the `skia-canvas` import, writing and registering the
60
+ embedded fonts, decoding every image asset, building the player, installing the Node globals — is paid
61
+ once rather than per frame.
62
+
63
+ ```ts
64
+ import { createRenderer } from '@flatkit/compiler/render'
65
+
66
+ const r = await createRenderer(doc, { scale: 2, params: { door: 'open' } })
67
+ try {
68
+ for (let f = 0; f < 300; f++) writeFileSync(`out/${f}.png`, await r.frame(f))
69
+ } finally {
70
+ r.close() // restores the globals and drops the temp fonts — not optional
71
+ }
72
+ ```
73
+
74
+ `params` sets a SYMBOL's exposed params before rendering — a state NAME or a number, the same spelling
75
+ `--set` accepts, and now available on `--render` too (`flatc --render p.flatink --set door=open`). It was
76
+ `--preview`-only, which is why anyone wanting to render a program in a given state wrote their own
77
+ harness.
78
+
79
+ > **Driving the player yourself under Node?** You need a `document` shim, or every `filter` and `tint` is
80
+ > dropped — the frame still draws, the effect is simply gone. The player warns once when that happens.
81
+ > `createRenderer` installs the shims for you; prefer it.
82
+
83
+ ### The same pass, from code — `checkProgram`
84
+
85
+ The compiled Doc is **not enough** to validate a program. A text that is not FlatInk at all compiles to an
86
+ empty Doc nothing distinguishes from a valid one, and an `object` block that binds to nothing leaves no
87
+ trace either: both errors live in the SOURCE. `checkProgram` runs the whole `--check` pass on a string, so
88
+ validating in a service or a browser no longer means spawning `flatc`:
89
+
90
+ ```ts
91
+ import { checkProgram } from '@flatkit/compiler'
92
+
93
+ const { ok, errors, warnings, report, doc } = checkProgram(srcFromAnLLM, { assetSrcs: [libText] })
94
+ if (!ok) regenerate(report) // the report doubles as the repair prompt
95
+ ```
96
+
97
+ Never throws: a source the parser rejects outright comes back as a diagnostic with `doc: null`. The CLI
98
+ calls the same function, so the two verdicts cannot drift.
99
+
100
+ It also flags the three **silent drops of a cel layer** (such a layer draws only the current cel's
101
+ `matter` and the containers that cel poses): a bare shape left in the layer, a `pose "X"` naming no roster
102
+ item, and a roster item no cel ever poses. Each renders an empty frame with no other signal — see
103
+ [frame-by-frame](animating-symbols.md#frame-by-frame--a-different-drawing-on-each-cel).
104
+
105
+ ## See what you draw — `--render`
106
+
107
+ Render a PNG (skia backend, faithful to the browser). Needs the optional `skia-canvas` dep
108
+ (`npm i -D skia-canvas`).
109
+
110
+ ```
111
+ flatc <file> --render -o out.png [--frame N] [--at k=v[,k2=v2]] [--steps N] [--scale S]
112
+ ```
113
+
114
+ - `--frame N` target frame · `--scale S` resolution factor.
115
+ - `--at score=3,step=2` forces variables → capture a precise state.
116
+ - **`--steps N`** runs N fixed simulation steps (`every frame`, 60 Hz) *before* capture, so a stateful
117
+ act unfolds on its own — no need to force every derived ramp variable by hand.
118
+ - **Embedded fonts render too**: any `asset "id" "font.woff2" font` is registered with skia before
119
+ capture, so text uses the authored face (matched by the font's intrinsic family name — the same name
120
+ you put in `text … font "…"`) instead of a host fallback. `.woff2/.woff/.ttf/.otf` are all supported;
121
+ flatc prints the registered families to stderr.
122
+ - **Font family alias**: add a quoted name after `font` — `asset "id" "font.woff2" font "Quicksand"` —
123
+ to register the face under *that* family instead of the file's intrinsic one. Use it when a font's
124
+ name table is wrong (e.g. a variable-font static export skia reads as `… Thin/Light`), so the alias
125
+ matches the `text … font "Quicksand"` you authored. Browsers ignore it (they bind families via
126
+ `FontFace`); it only steers headless `--render`.
127
+
128
+ ## Preview a `.flat` symbol — `--preview`
129
+
130
+ Wrap ONE symbol of a `.flat` library into a playable, auto-sized Doc — no hand-authored wrapper. Outputs a
131
+ `.flatpack` (drop it in the browser player) or, with `--render`, a PNG.
132
+
133
+ ```
134
+ flatc --preview <library.flat> [--symbol NAME] [-o out.flatpack | --render -o out.png]
135
+ [--bbox all|frame0] [--pad N] [--set p=v[,p2=v2]] [--frame N] [--scale S]
136
+ ```
137
+
138
+ - `--symbol NAME` picks the symbol (default: the first; others are listed on stderr).
139
+ - **`--bbox all`** (default) auto-sizes to the UNION of bounds over every frame (sub-timelines unfrozen),
140
+ so drifting/rotating/growing motion is never clipped. `--bbox frame0` is the old frame-0-only measure;
141
+ `--pad N` adds a margin (default 24).
142
+ - **`--set p=v`** sets the symbol's exposed [params](animating-symbols.md#exposed-parameters-params): a
143
+ `color` (`hull=#1a5`), a `number`/`bool` (`wave=1.5`), or a `state` by name (`door=open`). Baked into the
144
+ preview (flatpack + render).
145
+ - **`--scale auto`** picks the render factor from the symbol's size — enlarges small/thin assets (so a fine
146
+ filament stays legible) and leaves large ones at 1×. Otherwise `--scale S` (default 2).
147
+
148
+ ## Media packing — `--assets`
149
+
150
+ ```
151
+ flatc <file> --assets inline # default: media embedded as data: URIs → one portable .flatpack
152
+ flatc <file> --assets external # asset.data = relative key + a sidecar <out>.assets/ folder
153
+ ```
154
+
155
+ Use `external` for big media you don't want inflating the JSON; serve the folder and play with
156
+ `sameOriginAssetResolver(<flatpackUrl>)` (see `@flatkit/player`).
157
+
158
+ ## Headless play — `--play`
159
+
160
+ Run a scene **without a canvas**, replay a gesture script, and print `{ sends, vars }` — great in CI.
161
+
162
+ ```
163
+ flatc <file> --play --script gestures.json [--trace]
164
+ ```
165
+
166
+ **Prefer semantic gestures** (by object NAME — robust, the engine resolves coordinates):
167
+
168
+ ```json
169
+ [
170
+ { "type": "drag", "source": "Card1", "target": "ZoneA" },
171
+ { "type": "tap", "target": "Button" },
172
+ { "type": "scratch", "target": "Cover1" },
173
+ { "type": "connect", "source": "Word", "target": "Picture" },
174
+ { "type": "key", "name": "ArrowRight", "frames": 10 },
175
+ { "type": "wait", "frames": 30 },
176
+ { "type": "expect", "sends": ["win"], "vars": { "score": 3 } }
177
+ ]
178
+ ```
179
+
180
+ - `drag` / `tap` / `scratch` (sweeps a `reveal` zone) / `connect` (pulls a `link` wire) — by name.
181
+ - `set` drives a variable from the host; `wait` runs N fixed 60 Hz steps (advances `every frame` physics).
182
+ - **`key`** holds a key down (`keys.<name>` reads `1`) for `frames` steps — default `1` — then releases
183
+ it: the way to test a keyboard-driven scene in CI. Use the authored name (`"ArrowRight"`, `"Space"`).
184
+ - **`expect`** turns the script into a test: it compares the `send`s emitted since the last `expect` and
185
+ the current vars, and makes `--play` **exit ≠0** on mismatch. No more eyeballing. It matches the
186
+ **sequence of event names**; to assert a payload, read `sends` from the JSON output (each entry is the
187
+ object the host would receive: `{ name, value?, fields? }` — see
188
+ [host integration](host-integration.md#receiving-events-send--onevent)).
189
+ - Low-level gestures (`down`/`move`/`up`/`cancel` with `x,y`) remain for special cases.
190
+ - `--trace` prints a human-readable log per gesture (emitted sends + variable diff) instead of JSON —
191
+ a `send` shows as `name`, `name=value`, or `name{a=1, b=2}` for a record payload.
192
+
193
+ ### Recording
194
+
195
+ In the player, `player.startRecording()` / `stopRecording(): Gesture[]` capture gestures you play by
196
+ hand into a script that `--play` replays. (Authoring/CI helpers live in `@flatkit/player/debug`.)
197
+ Pointer only — key presses are not captured; add the `key` gestures to the recorded script yourself.
198
+
199
+ ## Teaching the language to a model
200
+
201
+ Three pieces, all pure functions — no filesystem, no canvas — so they work in a browser as well as in CI:
202
+
203
+ ```ts
204
+ import { languageCard, drawingCard, docToManifest, llmContext, manifestObjects, manifestEvents } from '@flatkit/compiler'
205
+
206
+ languageCard() // BEHAVIOR: events, channels, expressions (function/constant lists interpolated
207
+ // from the engine, so they cannot go stale)
208
+ drawingCard() // COMPOSITION: shapes, paints, filters, text, clipping, and the word order that
209
+ // breaks most often. Its examples are compiled by a test — a copied reference drifts
210
+ docToManifest(doc) // the SCENE MAP alone: the names this scene can reference, and the contract below
211
+ llmContext(doc) // the bundle: both cards + the map. `{ drawing: false }` drops the drawing half
212
+ ```
213
+
214
+ Pick `docToManifest` when you already inject the references yourself — `llmContext` is the everything-included
215
+ bundle, and calling it in that case ships the cards twice.
216
+
217
+ The scene map (`docToManifest`) ends with the **binding contract**: for each named object, the events the
218
+ logic handles on it, whether it is dragged, whether it is a drop zone, the channels the logic drives, and
219
+ the state it reads — plus the events the program emits. Deliberately **no coordinates**: that is what lets
220
+ a second skin honour the same logic with a completely different composition.
221
+
222
+ ```
223
+ contract (honour these; the layout is yours):
224
+ Flask - zone
225
+ TileH - drag, on drop, driven: x y opacity rotation, reads: xh yh rmix inO
226
+ events: correct, completed, incorrect
227
+ ```
228
+
229
+ Longer, hand-written prompts (a full reference plus one file per role — asset creator, motion designer,
230
+ coder) ship with the package under `prompts/`; see [its README](../packages/compiler/prompts/README.md).
231
+
232
+ ## See also
233
+
234
+ - The language itself → **[Scene & drawing](scene-and-drawing.md)** · **[Behavior & interactions](behavior-and-interactions.md)**
235
+ - Wiring a scene into an app → **[Host integration](host-integration.md)**
236
+ - Pitfalls & best practices → **[Gotchas](dsl-gotchas.md)**