@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,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)**
|