@flatkit/compiler 0.26.0 → 0.27.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/{chunk-RNQ2IQLD.js → chunk-S63MOX6G.js} +8 -7
- package/dist/chunk-S63MOX6G.js.map +1 -0
- package/dist/cli/flatc.js +1 -1
- 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 +1 -1
- 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,357 @@
|
|
|
1
|
+
# Animating a symbol (`.flat`)
|
|
2
|
+
|
|
3
|
+
> How a `.flat` symbol moves over time: the **timeline / cel / pose** model (Flash-style), the `pose`
|
|
4
|
+
> keywords (`rotate`, `scale`, `opacity`, `spin`…), pivots, tweens, and how to preview the result.
|
|
5
|
+
> If you only need static composition, see [Scene & drawing](scene-and-drawing.md).
|
|
6
|
+
|
|
7
|
+
## The model in one paragraph
|
|
8
|
+
|
|
9
|
+
A symbol owns a **timeline** (`timeline <fps> <durationFrames>`). Each animated **layer** is a time
|
|
10
|
+
track: a sequence of **cels** (layer-wide keyframes). A cel lists the **poses** of the containers
|
|
11
|
+
present at that frame (a `pose` per roster item) and, optionally, the **matter** (static drawing) at
|
|
12
|
+
that key. Between two cels the layer either **holds** the last key or **tweens** toward the next one.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
symbol "Wheel" {
|
|
16
|
+
timeline 24 24 ← 24 fps, 24 frames (loops once per second)
|
|
17
|
+
layer "spin" {
|
|
18
|
+
group "Rim" at 100,100 pivot 0,0 { ← the roster: declared ONCE, posed by the cels below
|
|
19
|
+
layer "art" { circle 0 0 40 nofill stroke #333 8 }
|
|
20
|
+
}
|
|
21
|
+
cel 0 tween { pose "Rim" rotate 0 }
|
|
22
|
+
cel 24 { pose "Rim" rotate 360 } ← one full turn around the pivot, in DEGREES
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Preview it without authoring a wrapper:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
flatc --preview Wheel.flat --render -o wheel.png # a PNG (frame 0)
|
|
31
|
+
flatc --preview Wheel.flat -o wheel.flatpack # a playable .flatpack for the browser player
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## `pose` — the keyframe of a container
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pose "Name" [at <x>,<y>] [rotate <deg>] [scale <s> | scaleX <sx> scaleY <sy>]
|
|
38
|
+
[opacity <o>] [tint <#color> <amount>] [spin cw|ccw] [turns <n>] [filter …]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **`rotate <deg>` and `scale`/`scaleX`/`scaleY` are in human units** (degrees, multipliers) and apply
|
|
42
|
+
**around the group's `pivot`** — no matrices, no radians, no trigonometry. `rotate 90` is a quarter
|
|
43
|
+
turn; `scale 2` is double size.
|
|
44
|
+
- **`at <x>,<y>`** places the container's local **origin** in parent space. `rotate`/`scale` then turn
|
|
45
|
+
and scale around the `pivot` point (see below), keeping it anchored.
|
|
46
|
+
- **`matrix(a,b,c,d,e,f)`** is still accepted as an escape hatch, but you almost never need it.
|
|
47
|
+
|
|
48
|
+
### Patch semantics — a pose only overrides what it states
|
|
49
|
+
|
|
50
|
+
A pose **inherits** every channel it does not mention from the container's resting pose (its declaration
|
|
51
|
+
in the roster) — position, rotation, scale, opacity, tint, filters. So:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pose "Boat" opacity 0.5 ← keeps the Boat's declared position/rotation/scale; only dims it
|
|
55
|
+
pose "Boat" rotate 3 ← keeps its position and scale; only tilts it 3°
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You do **not** re-state `at x,y` in every cel just to change opacity. (This is a change from older
|
|
59
|
+
builds where a partial pose snapped to `0,0`.)
|
|
60
|
+
|
|
61
|
+
## Pivot vs `at` — where things turn
|
|
62
|
+
|
|
63
|
+
- **`pivot <x>,<y>`** (set on the container in the roster, in its **local** coordinates) is the center
|
|
64
|
+
of rotation **and** scale **and** tween interpolation. Default is the local origin `0,0`.
|
|
65
|
+
- **`at <x>,<y>`** (on the pose) is where the local origin lands in the parent.
|
|
66
|
+
|
|
67
|
+
**Rule of thumb:** set the group's `pivot` to its visual center, then `rotate`/`scale`/`spin` turn it in
|
|
68
|
+
place. A wheel whose art is centered on its local origin needs no pivot; a wheel drawn off-origin must
|
|
69
|
+
set `pivot` to its hub, or it will **orbit** instead of spin.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
group "Hand" at 200,200 pivot 0,-60 { ← pivot at the clock center, 60px below the hand's tip
|
|
73
|
+
layer "art" { rect -4 -60 8 60 fill #111 }
|
|
74
|
+
}
|
|
75
|
+
…
|
|
76
|
+
cel 0 tween { pose "Hand" rotate 0 }
|
|
77
|
+
cel 60 { pose "Hand" rotate 360 } ← sweeps around the pivot, not its own middle
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Tweens, easing, spin
|
|
81
|
+
|
|
82
|
+
- **`cel N tween { … }`** interpolates this cel → the next for every container present in both. Without
|
|
83
|
+
`tween`, the cel **holds** until the next key.
|
|
84
|
+
- **`ease <curve>`** on the cel: `linear` · `easeIn` · `easeOut` · `easeInOut` · `cubic(a,b,c,d)`.
|
|
85
|
+
- **`spin cw|ccw`** + **`turns <n>`** force the rotation **direction** and add full turns across the
|
|
86
|
+
tween, so a 350° → 10° move can go the short way (`ccw`) or wind several times (`turns 2`). The spin
|
|
87
|
+
is **around the pivot**, like every other rotation.
|
|
88
|
+
- **`morph`** on a cel tweens the *shape* of the `matter` (drawing) toward the next key.
|
|
89
|
+
|
|
90
|
+
## Presence across cels — a cel is a full snapshot
|
|
91
|
+
|
|
92
|
+
Each `cel` is a **keyframe = the full set of containers present at that instant** (Flash style). A container
|
|
93
|
+
is shown only on the cels that **pose** it; one omitted from a cel **disappears** there. So a container
|
|
94
|
+
visible across a span must be posed on **each cel of that span** (it's per-*keyframe*, not per-frame — three
|
|
95
|
+
keyframes ⇒ three poses, not one per frame). This is also how a symbol **exits**: stop posing it.
|
|
96
|
+
|
|
97
|
+
Two ways to avoid re-typing an unchanged container:
|
|
98
|
+
|
|
99
|
+
- **Static element → its own layer WITHOUT cels.** A cel-less layer renders its items at every frame, so a
|
|
100
|
+
static base/background is declared **once** and never flickers. (Same idea as the render-order note below.)
|
|
101
|
+
- **`cel N hold { … }`** — carry the previous cel's poses forward for every container this cel does *not*
|
|
102
|
+
mention, then apply the stated overrides. Pure authoring sugar (the compiler expands it to full cels), so
|
|
103
|
+
you only write what changes:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
cel 0 tween { pose "Base" at 0,0 pose "Ring" scale 1 }
|
|
107
|
+
cel 30 hold tween { pose "Ring" scale 4 } # Base carried automatically
|
|
108
|
+
cel 60 hold { pose "Ring" scale 1 }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`hold` is opt-in per cel; without it the default (an omitted container is removed) is unchanged — so
|
|
112
|
+
exits still work.
|
|
113
|
+
|
|
114
|
+
## Frame-by-frame — a different DRAWING on each cel
|
|
115
|
+
|
|
116
|
+
A cel carries the poses **and** the layer's **matter**: the drawing at that key, written as a
|
|
117
|
+
`matter { … }` block. Change it from cel to cel and you get classic cel animation — one drawing per
|
|
118
|
+
frame, no tween:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
symbol "Blink" {
|
|
122
|
+
timeline 12 3
|
|
123
|
+
layer "draw" {
|
|
124
|
+
cel 0 { matter { circle 0 0 30 fill #e33 } }
|
|
125
|
+
cel 1 { matter { rect -30 -30 60 60 fill #3a3 } }
|
|
126
|
+
cel 2 { matter { path "M -30 30 L 0 -30 L 30 30 Z" fill #33e } }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- The matter is **held** until the next cel that defines one — a drawing kept over several frames is
|
|
132
|
+
written **once**, on the cel where it appears. An explicit `matter { }` (empty) blanks it.
|
|
133
|
+
- **`morph`** on the cel interpolates the *shape* toward the next cel's matter instead of cutting.
|
|
134
|
+
- A cel can carry **both**: a `matter { … }` for that frame's drawing *plus* `pose "…"` for the
|
|
135
|
+
containers animated on top of it (matter always draws **behind** the posed containers).
|
|
136
|
+
|
|
137
|
+
Two other ways to author the same thing, when the drawings already exist as objects:
|
|
138
|
+
|
|
139
|
+
- **Swap containers** — declare each drawing as a group in the roster, then pose only the one you want on
|
|
140
|
+
each cel (a container omitted from a cel disappears — see above).
|
|
141
|
+
- **Image sequence** — the same idiom with `image` items: `image "f1" 100 100 as "F1" at -50,-50` in the
|
|
142
|
+
roster, `cel 0 { pose "F1" }`. The media is declared in the `.flatink` (`asset "f1" "f1.png" image`).
|
|
143
|
+
|
|
144
|
+
> **The trap:** in a layer that **has cels**, a bare shape written directly in the layer is **never
|
|
145
|
+
> drawn** — such a layer renders the current cel's `matter` plus the containers that cel poses, and
|
|
146
|
+
> nothing else. Put the shape inside a `matter { … }`, or on a **cel-less layer** if it is static decor.
|
|
147
|
+
> `flatc --check` warns about it, and about the two sibling silent drops: a `pose "X"` naming no roster
|
|
148
|
+
> item, and a roster item that no cel ever poses.
|
|
149
|
+
|
|
150
|
+
## Driving a channel with an expression (`expr`)
|
|
151
|
+
|
|
152
|
+
Instead of keyframes you can bind a channel to an expression on the container itself:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
group "Fan" pivot 0,0 expr rotation "turns(time)" { … } ← one turn per second
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- The animatable channels are `x`, `y`, `scaleX`, `scaleY`, `rotation`, `opacity`.
|
|
159
|
+
- **`rotation` is in RADIANS** (like `sin`/`cos`/`atan2`). Use the helpers to stay in degrees:
|
|
160
|
+
- `rad(deg)` → radians, e.g. `expr rotation "rad(45)"`
|
|
161
|
+
- `turns(n)` → `n` full turns in radians, e.g. `expr rotation "turns(time)"` or `"turns(time * 0.5)"`
|
|
162
|
+
- `deg(rad)` → the inverse, for readouts.
|
|
163
|
+
|
|
164
|
+
## Stateful "feel": `spring` / `smooth`
|
|
165
|
+
|
|
166
|
+
`expr` is **pure** — it recomputes from `time`/params each frame, with no memory. When you want a channel to
|
|
167
|
+
*react over time* (lag behind a moving target, swing and settle), use a **modifier** instead. It carries
|
|
168
|
+
per-instance state that **integrates** toward a target each frame — so an asset's physical "feel" lives **in
|
|
169
|
+
the asset**, with no scene code:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
group "Suspente" spring rotation "crochetX" stiffness 0.08 damping 0.86 { … } # cable swings, then settles
|
|
173
|
+
group "Aiguille" smooth rotationDeg "valeur * 270" k 0.18 { … } # needle eases to its value
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `smooth <channel> "<target>" k <0..1>` — 1st-order lag: each step `value += (target − value) * k`. Small
|
|
177
|
+
`k` = slow/heavy; `k = 1` = instant (no lag).
|
|
178
|
+
- `spring <channel> "<target>" stiffness <0..1> damping <0..1>` — 2nd-order spring: overshoots then settles.
|
|
179
|
+
Lower `damping` = more bounce. (Both params are per fixed 60 Hz step; out-of-range values are clamped.)
|
|
180
|
+
- `<target>` is an ordinary expression (params, `time`, `self.x`, …) — the resting value the channel chases.
|
|
181
|
+
Authoring sugar like `expr`: `rotate` = `rotation`; `rotationDeg` reads degrees (wraps the target in `rad()`).
|
|
182
|
+
- A modifier **wins** over a plain `expr` / keyframes on the same channel.
|
|
183
|
+
|
|
184
|
+
**React to MOVEMENT, not value — `velocity(expr)`.** Inside a modifier target (only there), `velocity(x)` is
|
|
185
|
+
the per-second rate of change of `x`. It's **0 at rest** and while scrubbing/rendering, non-zero only while `x`
|
|
186
|
+
is actually moving — perfect for a pendulum on a moving pivot (a crane cable that swings when the trolley moves,
|
|
187
|
+
then hangs vertical again, with no scene code):
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
group "Suspente" spring rotation "rad(-velocity(crochetX) * 40)" stiffness 0.06 damping 0.22 { … }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
At rest `velocity = 0` → target `0` → vertical, automatically; on a scrub/`--render` it's also `0` → snaps to
|
|
194
|
+
rest. Composable in any target (`rad(-velocity(crochetX)*40 + 2*sin(time))`). `velocity()` is **only** valid in a
|
|
195
|
+
modifier target — `flatc --check` flags it elsewhere.
|
|
196
|
+
|
|
197
|
+
**Per instance.** State is keyed per instance, so two cranes side by side swing **independently** (even when
|
|
198
|
+
the spring is on a group *inside* the symbol).
|
|
199
|
+
|
|
200
|
+
**Live vs. static.** The spring animates during **playback** (gallery autoplay, an activity). On **random
|
|
201
|
+
access** — a timeline scrub, `--render`, a contact sheet — there is no time to integrate, so the channel
|
|
202
|
+
**snaps to its target** (the rest pose). So tune a spring by *playing* the preview, not by scrubbing.
|
|
203
|
+
|
|
204
|
+
**Also scene-side.** The same modifier works in a `.flatink` `object` block — the target is then an ordinary
|
|
205
|
+
(unquoted) FlatInk expression — for a one-off spring on a scene object, when the feel isn't baked into a `.flat`:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
object "Hero" {
|
|
209
|
+
spring rotation = crochetX { stiffness 0.08 damping 0.86 }
|
|
210
|
+
smooth opacity = lit { k 0.18 }
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Looping & instancing
|
|
215
|
+
|
|
216
|
+
The timeline loops over `[0, durationFrames)`. An `instance` of a symbol chooses **how its own timeline
|
|
217
|
+
advances** (Flash's symbol-instance models), written after the instance's attributes:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
instance "Walk" as "legs" # synced (default)
|
|
221
|
+
instance "Walk" as "legs" loop # independent (MovieClip)
|
|
222
|
+
instance "Splash" as "fx" once # play once, then hold the last frame
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| mode | clock | behavior |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| *(default)* / `synced` | the parent's frame | **Graphic symbol**: scrubbed and *truncated* by the parent — if an ancestor's timeline is shorter than (or not a multiple of) the sub-loop, it snaps mid-cycle. Best for lip-sync, deterministic scrub. |
|
|
228
|
+
| `loop` (`independent`) | the runtime's monotone clock | **MovieClip**: loops on its *own* duration, immune to any ancestor's loop length. Use for state-loops and idles that must keep their phase across the parent's wrap. |
|
|
229
|
+
| `once` | the monotone clock, clamped | plays through **once**, then **holds** the last frame — a one-shot (a splash, an explosion, a pose that stays). |
|
|
230
|
+
| `singleFrame` | — | frozen on a fixed frame. |
|
|
231
|
+
|
|
232
|
+
A `loop`/`once` instance runs on the global heartbeat, so it never needs its parent padded to a common
|
|
233
|
+
multiple ("LCM") of its sub-loops. In the **editor** it shows frame 0 (MovieClip-style authoring); it plays
|
|
234
|
+
at runtime — edit its keyframes by opening the symbol itself. `--preview` sizes its window to show a nested
|
|
235
|
+
`loop`/`once` looping cleanly, without touching the previewed symbol's own authored duration.
|
|
236
|
+
|
|
237
|
+
## Exposed parameters (`params`)
|
|
238
|
+
|
|
239
|
+
A symbol can publish a small, named **interface** instead of exposing its internals — useful for restyling
|
|
240
|
+
an asset (hull/sail colors), tuning an animation (amplitude, speed), or toggling a detail, including
|
|
241
|
+
"after the fact" by a small model.
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
symbol "Boat" {
|
|
245
|
+
params {
|
|
246
|
+
color hull = #c0392b "Hull color"
|
|
247
|
+
color sail = #2980b9 "Sail color"
|
|
248
|
+
number wave = 1 range 0 2 "Bob amplitude"
|
|
249
|
+
bool flag = true "Show the pennant"
|
|
250
|
+
}
|
|
251
|
+
layer "body" {
|
|
252
|
+
path "…" fill hull // a color param used as a fill
|
|
253
|
+
group "Deck" expr y "sin(clock*3) * wave" { … } // a number param read in an expression
|
|
254
|
+
group "Flag" expr opacity "flag ? 1 : 0" { … } // a bool param as a toggle
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- `params { <type> <name> = <default> [range <min> <max>] ["doc"] … }` — `<type>` is `color`, `number`,
|
|
260
|
+
or `bool`. The default, range, and doc string make the interface self-describing.
|
|
261
|
+
- **`color` params** are used as a paint — `fill hull`, `stroke hull <width>`, a **gradient stop**
|
|
262
|
+
(`0:hull@0.8`, optional `@alpha`), or a **`tint hull <amount>`** (anywhere a `#color` literal goes).
|
|
263
|
+
Resolved per instance at render; *not* available in numeric expressions.
|
|
264
|
+
- **`number` / `bool` params** become **variables in the symbol's expressions** (`wave`, `flag`). `bool`
|
|
265
|
+
reads as `1`/`0`. (`flatc --check` knows them — reading a declared param in an `expr` is not an "unknown
|
|
266
|
+
variable".)
|
|
267
|
+
|
|
268
|
+
> The `timeline`, `params`, and `states` header blocks may appear in **any order** before the layers.
|
|
269
|
+
|
|
270
|
+
Set params at the instance **call-site** (literals), in `--preview`, or — for `number`/`bool` — at
|
|
271
|
+
runtime (`Boat.wave = 1.5`, see below):
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
instance "Boat" as "Hero" at center { hull = #1a5f3a, wave = 1.5, flag = false }
|
|
275
|
+
flatc --preview Boat.flat --render --set hull=#1a5f3a,wave=1.5 -o boat.png
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
> A `state` (below) is just another exposed param — same call-site/preview/runtime surface.
|
|
279
|
+
|
|
280
|
+
## Named states (`states`)
|
|
281
|
+
|
|
282
|
+
A symbol can expose **named states** — points on its own timeline — and let a consumer switch between
|
|
283
|
+
them. A door is the canonical case: the symbol animates from `closed` (frame 0) to `open` (frame 24),
|
|
284
|
+
and exposes that as a single param.
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
symbol "Door" {
|
|
288
|
+
timeline 24 24
|
|
289
|
+
states door { closed at 0 open at 24 initial closed transition 12 ease easeInOut }
|
|
290
|
+
layer "panel" {
|
|
291
|
+
group "Panel" at 60,10 pivot 0,0 { layer "art" { rect 0 0 40 80 fill #884422 } }
|
|
292
|
+
cel 0 tween { pose "Panel" rotate 0 } // closed
|
|
293
|
+
cel 24 { pose "Panel" rotate 80 } // open
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- `states <param> { <name> at <frame> … }` declares the state machine. `<param>` is the exposed
|
|
299
|
+
variable (`door`); each `<name> at <frame>` anchors a state to a frame of the symbol's timeline.
|
|
300
|
+
- `initial <name>` is the resting state (default: the first). `transition <n> [ease <e>]` is the default
|
|
301
|
+
move between states.
|
|
302
|
+
- **The param drives the symbol's local playhead.** `door = 0` (or `closed`) → frame 0; `door = 1`
|
|
303
|
+
(or `open`) → frame 24; a fractional `door = 0.5` → frame 12, i.e. the authored in-between animation.
|
|
304
|
+
So **animating the variable from 0→1 plays the open animation** — states live inside the ordinary
|
|
305
|
+
variable system, no special runtime.
|
|
306
|
+
|
|
307
|
+
Select a state in a preview (a state name or a number):
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
flatc --preview Door.flat --render --set door=open -o open.png
|
|
311
|
+
flatc --preview Door.flat --render --set door=0.5 -o half.png # mid-transition
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Driving a state from a program (`set Name.param = state`)
|
|
315
|
+
|
|
316
|
+
In a `.flatink` program, address an instance **by name** and set its state — the player plays the
|
|
317
|
+
declared `transition` automatically. Each instance keeps its **own** state, so two doors are independent.
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
scene {
|
|
321
|
+
layer "stage" { instance "Door" as "FrontDoor" at 100,100 }
|
|
322
|
+
}
|
|
323
|
+
object "FrontDoor" {
|
|
324
|
+
when clicked { FrontDoor.door = open } // animates closed → open over `transition` frames
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The right-hand side is a **state name** (`open`) or an expression (`FrontDoor.door = score > 5 ? open : closed`
|
|
329
|
+
isn't valid — names aren't expressions; use a number there, e.g. `… ? 1 : 0`). `transition 0` snaps instantly.
|
|
330
|
+
|
|
331
|
+
> **Scope note:** the state value drives the instance's playhead and is visible to that instance's own
|
|
332
|
+
> expressions. Reading another object's state back by name (`FrontDoor.door` in an unrelated expression)
|
|
333
|
+
> and the broader typed `params {}` interface (colors/numbers/toggles, `fill hull`) are still to come.
|
|
334
|
+
|
|
335
|
+
## Render order (a real caveat)
|
|
336
|
+
|
|
337
|
+
Within **one animated layer**, the **matter (static drawing) always renders behind the posed
|
|
338
|
+
containers** — declaration order between a bare `path` and an animated `group` is **not** preserved,
|
|
339
|
+
because the cel model stores matter and the container roster separately.
|
|
340
|
+
|
|
341
|
+
**If a static shape must sit IN FRONT of an animated group**, give it its own group (so it becomes a
|
|
342
|
+
posed container too) or, simpler, **put it on its own layer** above. Layers always honor their stacking
|
|
343
|
+
order. This is the reliable way to control z-order around animation.
|
|
344
|
+
|
|
345
|
+
## Previewing without clipping
|
|
346
|
+
|
|
347
|
+
`flatc --preview` auto-sizes the stage to the symbol's bounds. By default (`--bbox all`) it measures the
|
|
348
|
+
**union over every frame** (sub-timelines unfrozen), so a part that drifts, rotates, or grows is **never
|
|
349
|
+
clipped**. Use `--bbox frame0` for the old frame-0-only measure, and `--pad N` to add a margin.
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
flatc --preview Boat.flat --render -o boat.png # union bbox (default) — full motion fits
|
|
353
|
+
flatc --preview Boat.flat --bbox frame0 --pad 40 -o b.png # frame-0 bounds + 40px margin
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
See also: [Tooling](tooling.md) for the full `flatc` reference, and
|
|
357
|
+
[Gotchas](dsl-gotchas.md) for sharp edges.
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Behavior & interactions
|
|
2
|
+
|
|
3
|
+
Everything after the `scene { … }` block is behavior. It attaches to named scene items via
|
|
4
|
+
`object "Name" { … }`, or runs scene-wide via `every frame { … }` and timeline hooks.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
var score = 0
|
|
8
|
+
|
|
9
|
+
object "Coin" {
|
|
10
|
+
when clicked { score = score + 1 } // an EVENT handler (actions)
|
|
11
|
+
rotation = clock * 90 // a CHANNEL binding (expression, every frame)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
every frame { if (score >= 10) { send "win" } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Events
|
|
18
|
+
|
|
19
|
+
Inside `object "Name" { … }`:
|
|
20
|
+
|
|
21
|
+
| Event | Fires when |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `when clicked` | the item is **tapped** — press + release with no drag (fires on release, within a few px) |
|
|
24
|
+
| `when hovered` / `when unhovered` | the pointer enters / leaves |
|
|
25
|
+
| `when pressed` / `when released` | pointer down / up on the item |
|
|
26
|
+
| `when dragged` | the item is being dragged (grab in progress) |
|
|
27
|
+
| `when held` | a long press |
|
|
28
|
+
| `when dropped on <Zone> [at pointer]` | released over a drop zone (see [drag & drop](#drag--drop)) |
|
|
29
|
+
|
|
30
|
+
Scene-wide: `when loaded { … }` (once), `every frame { … }` (each tick), `at frame <n> { … }`,
|
|
31
|
+
`label <frame> "name"`.
|
|
32
|
+
|
|
33
|
+
## Actions
|
|
34
|
+
|
|
35
|
+
In a handler body, one action per line:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
play · pause # timeline control
|
|
39
|
+
go to frame <n> [and play|and pause]
|
|
40
|
+
go to "<label>" [and play|and pause]
|
|
41
|
+
<name> = <expr> # set a variable (the `set` keyword is optional)
|
|
42
|
+
<arr>[<expr>] = <expr> # indexed assignment (nested indices ok: occ[sl[i]] = 0)
|
|
43
|
+
if <cond> { … } [else if <cond> { … }] [else { … }]
|
|
44
|
+
repeat <n> times { … } # runtime loop (bounded)
|
|
45
|
+
repeat i from <a> to <b> { … } # runtime range loop
|
|
46
|
+
<fn>(<args>) # call a function
|
|
47
|
+
send "<event>" [, <payload>] # emit an event to the host (see below)
|
|
48
|
+
sound "<assetId>" # one-shot audio
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `send` — talking to the host
|
|
52
|
+
|
|
53
|
+
`send` is the one-way channel from the scene to the page that embeds it. Four payload forms:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
send "win" # bare — just the event
|
|
57
|
+
send "score", lives * 100 # a NUMBER (any expression)
|
|
58
|
+
send "answer", text("txtCard") # the live TEXT of a text item
|
|
59
|
+
send "save", { x = px, y = py, doors } # a RECORD: named numbers (a state patch)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
In a record, `{ doors }` is shorthand for `{ doors = doors }` — handy when the field and the variable
|
|
63
|
+
share a name. Fields hold **numbers only**, at most 32 per `send`, and each name must be a plain
|
|
64
|
+
identifier (`[A-Za-z_]\w*`, 64 characters max, and never `__proto__`/`constructor`/`prototype`).
|
|
65
|
+
|
|
66
|
+
The host receives one object: `{ name, value?, fields? }` — `value` for the number/text forms, `fields`
|
|
67
|
+
for the record. Nothing comes back: `send` is fire-and-forget and never blocks the scene. See
|
|
68
|
+
**[Host integration](host-integration.md)** for the receiving end.
|
|
69
|
+
|
|
70
|
+
## Variables
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
var score = 0 # scalar (declared at the top of the file, Layer B state)
|
|
74
|
+
var slots = [0, 0, 0] # array literal
|
|
75
|
+
var seen = fill(8, 0) # array of 8 zeros
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Read/write them in expressions and actions. `var`s are runtime state — distinct from `def` (a
|
|
79
|
+
compile-time constant, see [factoring](#reuse--factoring)).
|
|
80
|
+
|
|
81
|
+
## Channel bindings
|
|
82
|
+
|
|
83
|
+
Drive an item's pose every frame with an expression. Channels: `x`, `y`, `scaleX`, `scaleY`,
|
|
84
|
+
`rotation`, `opacity` (absolute), plus the additive position offsets `dx`, `dy` (`pos = at + (dx, dy)`).
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
object "Needle" {
|
|
88
|
+
rotation = atan2(mouse.y - 160, mouse.x - 240) // point at the cursor (RADIANS)
|
|
89
|
+
opacity = lit ? 1 : 0.3
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`rotation` is in **radians** (like `sin`/`cos`/`atan2`). To author in **degrees**, bind **`rotationDeg`**
|
|
94
|
+
instead — sugar for `rotation = rad(<expr>)`: `rotationDeg = 45`, `rotationDeg = handAngle`.
|
|
95
|
+
|
|
96
|
+
`x`/`y` are **absolute** — they REPLACE the item's declared `at X,Y`. For motion **around** the anchor,
|
|
97
|
+
bind the additive offsets **`dx`/`dy`** instead: `pos = at + (dx, dy)`, so `dx = 30*sin(clock)` wobbles a
|
|
98
|
+
group `at 620,150` around 620 with no base to re-inject (and `dx`/`dy` add on top of `x`/`y` if both are
|
|
99
|
+
bound). Offsets are binding-only — no keyframe/`spring`/`smooth` form. See the
|
|
100
|
+
[absolute-vs-offset gotcha](dsl-gotchas.md).
|
|
101
|
+
|
|
102
|
+
`self.x`/`self.y`/… is the item's own current pose; `mouse.x`/`mouse.y` (and `mouse.wheel`, the per-frame
|
|
103
|
+
scroll delta), `time`, `clock`, `frame`, variables and named objects (`Target.x`) are all available — see
|
|
104
|
+
[Expressions](expressions-and-stdlib.md). Prefer **`clock`** (monotone) over `time` (restarts on every
|
|
105
|
+
timeline loop) for free-running motion and for any instant you capture and compare later.
|
|
106
|
+
|
|
107
|
+
## Drag & drop
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
object "Piece" {
|
|
111
|
+
drag px, py // follow the pointer, writing into px/py (use them: x = px, y = py)
|
|
112
|
+
x = px y = py
|
|
113
|
+
when dropped on Slot at pointer { placed = 1 }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `drag x, y` / `dragX x` / `dragY y` — the gesture writes the position into your variables.
|
|
118
|
+
- `{ confine to <Zone> }` clamp · `{ snap <grid> }` pixel-snap · `{ enabled <expr> }` active only while the expression ≠ 0 (a dynamic lock — no ternary needed).
|
|
119
|
+
- ⚠️ **`enabled` gates the GESTURE, not the handlers.** Once it is off the object stops being draggable, but
|
|
120
|
+
`when pressed` / `when released` / `when clicked` **still fire** on it. Guard the handler body yourself
|
|
121
|
+
(`when released { if done == 0 { … } }`) whenever it must run only while the gesture is live. (A `link`'s
|
|
122
|
+
target index is the exception: it resolves to `0` — "no target reached" — on a gated-off release, so it
|
|
123
|
+
can never hand you the previous gesture's answer.)
|
|
124
|
+
- **Drop zones**: by default the object's **center** is tested against the zone; `at pointer` tests the
|
|
125
|
+
pointer instead. Define an explicit rectangle with `group "Zone" … hitbox <w> <h> { … }`.
|
|
126
|
+
- Several `when dropped on` per object are evaluated in declaration order (the right-zone / wrong-zones pattern).
|
|
127
|
+
- **`match` sugar** factors the whole drag+drop boilerplate — see [factoring](#reuse--factoring).
|
|
128
|
+
|
|
129
|
+
## Interactors
|
|
130
|
+
|
|
131
|
+
Higher-level pointer behaviors (each writes into your variables; all accept `{ enabled <expr> }`):
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
turn <angle> around <x>,<y> [{ snap <deg> }] # dial / clock hand → angle in RADIANS → rotation = angle
|
|
135
|
+
turnDeg <angle> around <x>,<y> [{ snap <deg> }] # …in DEGREES → rotationDeg = angle (rotationDeg = sugar for rotation = rad(…))
|
|
136
|
+
trace <progress> along <Group> [{ tolerance <px> }]# follow a path → progress 0..1 (monotone)
|
|
137
|
+
reveal <progress> [{ brush <px> }] # scratch/wipe the grabbed area → fraction 0..1 (cumulative across grabs)
|
|
138
|
+
link <endX>, <endY>, <target> to <Group> # pull a thread → end follows the pointer; <target> = hit index 1..n on release (0 = none)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Each output also accepts an **array element** (`drag hx[i], hy[i]`, `reveal seen[2]`) — the natural form
|
|
142
|
+
under `each` (see below).
|
|
143
|
+
|
|
144
|
+
### Drawing the thread of a `link`
|
|
145
|
+
|
|
146
|
+
`link` gives you the end position and the target index; **the visible wire is yours to draw**. The idiom:
|
|
147
|
+
draw a horizontal bar of a known length, anchored at the source, then rotate and stretch it onto the end.
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
use "gesture" // angle(cx, cy, px, py) → radians
|
|
151
|
+
use "collision" // dist(ax, ay, bx, by) → length
|
|
152
|
+
|
|
153
|
+
scene {
|
|
154
|
+
layer "Fils" {
|
|
155
|
+
// A 100 px bar whose LEFT edge sits on the origin → scaling it stretches it away from the anchor.
|
|
156
|
+
group "Fil" at 120,300 { layer "c" { rect 0 -1 100 2 fill #3355ff } }
|
|
157
|
+
}
|
|
158
|
+
layer "Jeu" { group "Src" at 120,300 { layer "c" { circle 0 0 20 fill #3355ff } } }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
object "Src" { link ex, ey, hit to Cibles }
|
|
162
|
+
object "Fil" {
|
|
163
|
+
opacity = self.grabbed // only visible while the thread is being pulled
|
|
164
|
+
rotation = angle(120, 300, ex, ey)
|
|
165
|
+
scaleX = dist(120, 300, ex, ey) / 100 // 100 = the bar's DRAWN length
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Two things make it work: the bar is drawn **from its own origin** (so `scaleX` stretches the far end, not
|
|
170
|
+
both), and the divisor is the bar's drawn length. With the source at a variable position, replace the
|
|
171
|
+
literals with its coordinates.
|
|
172
|
+
|
|
173
|
+
## Pointer gestures (drag delta, finger-scroll, tap vs drag)
|
|
174
|
+
|
|
175
|
+
`mouse.x`/`mouse.y` hold the pointer position inside **any** handler — including `when pressed` /
|
|
176
|
+
`when clicked` / `when released` (the press/release point, on touch too), so you can capture a **grab
|
|
177
|
+
anchor**. A grab **keeps tracking the pointer after it leaves the object** (pointer capture), so a drag is
|
|
178
|
+
never lost at the object's edge.
|
|
179
|
+
|
|
180
|
+
**Relative drag / finger-scroll** — accumulate the delta from the press anchor (one action per line):
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
object "List" {
|
|
184
|
+
when pressed {
|
|
185
|
+
a = mouse.y
|
|
186
|
+
base = off
|
|
187
|
+
}
|
|
188
|
+
when dragged {
|
|
189
|
+
off = base + (mouse.y - a) // `off` scrolls by the finger delta
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Mouse-wheel scroll** (desktop) — `mouse.wheel` is the wheel delta accumulated **this frame** (0 when the
|
|
195
|
+
wheel is still), read in an `every frame` accumulator — the same idiom as the finger drag:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
every frame {
|
|
199
|
+
off = clamp(off + mouse.wheel, 0, max) // one notch ≈ tens of px; scale/clamp to taste
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The player consumes the wheel (keeps the page from scrolling over the canvas) **only when the scene reads
|
|
204
|
+
`mouse.wheel`** — a scene that ignores it lets the page scroll normally.
|
|
205
|
+
|
|
206
|
+
**Tap vs drag on the same element.** `when clicked` fires on **release**, and only if the pointer stayed
|
|
207
|
+
put — a press that travels past a few px is a **drag**, not a click. So the *same* element can be both
|
|
208
|
+
tappable and draggable: a tap fires `clicked`, a drag fires `dragged`/the interactor, with **no phantom
|
|
209
|
+
click** when you drag. That's what makes "tap a card to pick it, drag the list to scroll" work on one zone:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
object "Card" {
|
|
213
|
+
when clicked { picked = id } // fires on a TAP only
|
|
214
|
+
when dragged { off = base + (mouse.y - a) } // a DRAG scrolls — `clicked` does not fire
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Feedback
|
|
219
|
+
|
|
220
|
+
An object can read **its own interaction state** in channel expressions: `self.hovered`, `self.grabbed`,
|
|
221
|
+
`self.pressed` (each `0`/`1`). So hover-lift and grab-squash are just expressions — no mirror variable,
|
|
222
|
+
no handler:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
object "Button" {
|
|
226
|
+
scaleX = self.hovered ? 1.06 : 1
|
|
227
|
+
scaleY = self.grabbed ? 0.94 : 1
|
|
228
|
+
opacity = self.hovered ? 0.85 : 1
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The **`feedback` one-liner** generates these for you (auto-importing `use "feedback"`), composing per
|
|
233
|
+
channel so it never clashes with your `x`/`y` bindings:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
object "Tile" {
|
|
237
|
+
x = tx y = ty
|
|
238
|
+
feedback lift tilt dim shake(wrongZone) // lift=hover grow · tilt=grab squash · dim=hover opacity · shake=refusal wobble
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or call the [`feedback` stdlib](expressions-and-stdlib.md#stdlib-packages) functions by hand
|
|
243
|
+
(`lift`/`dim`/`tilt`/`sink`/`shake`).
|
|
244
|
+
|
|
245
|
+
**Timed feedback** (a message/flash that fades over a readable duration) — `pulse(since, dur)` from
|
|
246
|
+
`use "feedback"` is a linear `1→0` ramp over `dur` seconds since the instant `since`. Capture the instant
|
|
247
|
+
in a handler so nothing is hidden:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
var shown = -999
|
|
251
|
+
object "Hint" { opacity = pulse(shown, 4) } // visible 4 s after each trigger, then gone
|
|
252
|
+
object "Piece" { when dropped on Wrong { shown = clock } }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
(A multiplicative decay like `v * 0.86` is fine for a quick flash but vanishes before TEXT can be read —
|
|
256
|
+
`pulse` gives a duration you state.)
|
|
257
|
+
|
|
258
|
+
> ⚠️ **Capture the instant with `clock`, not `time`.** `pulse` and `shake` ride the monotone `clock`
|
|
259
|
+
> precisely because `time` **resets every `durationFrames`** (2.5 s by default). Timed on `time`, a
|
|
260
|
+
> one-shot end-of-game ramp *replays for ever* and a refusal wobble *skips* on every loop — with nothing
|
|
261
|
+
> on screen, and nothing at `--check`, to say so. `--check` now names any function of yours that reads
|
|
262
|
+
> `time` from inside a channel expression.
|
|
263
|
+
|
|
264
|
+
## Reuse / factoring
|
|
265
|
+
|
|
266
|
+
Cut repetition with compile-time sugar (all resolved at parse → zero runtime cost):
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
def gap = 70 // a compile-time constant (removed at parse), used via $(…)
|
|
270
|
+
scene { layer "L" {
|
|
271
|
+
repeat i from 0 to 4 { circle $(40 + i*gap) 80 6 fill #ffd98a } // generate N items; $(expr) interpolates
|
|
272
|
+
} }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Parameterized symbols** (a reusable visual) + **`each`** (shared behavior):
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
symbol "Key"(label) { layer "c" { rect -28 -28 56 56 fill #e8e8e8
|
|
279
|
+
text "$(label)" font "sans-serif" size 24 align center line 1.2 color #111 box 56 56 } }
|
|
280
|
+
|
|
281
|
+
scene { layer "Pad" {
|
|
282
|
+
repeat i from 0 to 8 { instance "Key"($(i+1)) as "K$(i)" at $(70 + (i%3)*80),$(80 + floor(i/3)*80) }
|
|
283
|
+
} }
|
|
284
|
+
|
|
285
|
+
each "Key" as i { when clicked { input = input * 10 + (i + 1) } } // one handler per generated key
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**`match`** — declarative pairing (factors drag+drop for a matching activity):
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
match Word1, Word2 onto Good, Bad {
|
|
292
|
+
correct Word1 -> Good, Word2 -> Bad
|
|
293
|
+
on correct as it { send "found", text(it) }
|
|
294
|
+
on done { send "win" }
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
It generates, per item, `<Item>_placed` / `<Item>_ok` / `<Item>_zone` state and the drag+drop handlers;
|
|
299
|
+
you keep the visual (`var <Item>_x`/`_y` + your channel expressions).
|
|
300
|
+
|
|
301
|
+
## See also
|
|
302
|
+
|
|
303
|
+
- The expression language and stdlib → **[Expressions & stdlib](expressions-and-stdlib.md)**
|
|
304
|
+
- Receive `send` events / drive variables from the page → **[Host integration](host-integration.md)**
|
|
305
|
+
- Test interactions headlessly (gesture scripts, `scratch`/`connect`) → **[Tooling](tooling.md)**
|
|
306
|
+
- Pitfalls (event order, monotone reveal, `$()` in `each`…) → **[Gotchas](dsl-gotchas.md)**
|