@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.
- package/dist/analysis.js +1 -1
- package/dist/{chunk-RNQ2IQLD.js → chunk-Z36EDKPW.js} +9 -8
- package/dist/chunk-Z36EDKPW.js.map +1 -0
- package/dist/{chunk-IYPM3CU5.js → chunk-ZAQUEPIX.js} +3 -2
- package/dist/{chunk-IYPM3CU5.js.map → chunk-ZAQUEPIX.js.map} +1 -1
- package/dist/cli/flatc.js +2 -2
- package/dist/cli/render.d.ts +36 -2
- package/dist/cli/render.js +76 -24
- package/dist/cli/render.js.map +1 -1
- package/dist/index.js +2 -2
- package/docs/README.md +55 -0
- package/docs/animating-symbols.md +357 -0
- package/docs/behavior-and-interactions.md +306 -0
- package/docs/dsl-gotchas.md +428 -0
- package/docs/embedding-fonts.md +95 -0
- package/docs/expressions-and-stdlib.md +107 -0
- package/docs/getting-started.md +107 -0
- package/docs/host-integration.md +162 -0
- package/docs/scene-and-drawing.md +168 -0
- package/docs/tooling.md +236 -0
- package/package.json +9 -7
- package/dist/chunk-RNQ2IQLD.js.map +0 -1
|
@@ -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)**
|
package/docs/tooling.md
ADDED
|
@@ -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)**
|