@flatkit/compiler 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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)**