@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,428 @@
|
|
|
1
|
+
# FlatInk DSL — gotchas & best practices (appendix)
|
|
2
|
+
|
|
3
|
+
> **New here?** Start with the [documentation index](README.md) and the
|
|
4
|
+
> [getting-started guide](getting-started.md). This page is the **appendix**: hard-won pitfalls and
|
|
5
|
+
> sharp edges learned on a real production pipeline, then fixed in the engine or written down here.
|
|
6
|
+
> Skim it once you know the basics.
|
|
7
|
+
|
|
8
|
+
## Mental model
|
|
9
|
+
|
|
10
|
+
- A `.flatink` file splits in two: the **`scene { … }`** block (the VISUAL composition:
|
|
11
|
+
`path`/`circle`/`group`/`image`/`text`) and the **behavior** that follows (`object "Name"
|
|
12
|
+
{ … }`, `every frame`, timeline bindings). The two do NOT share the same grammar.
|
|
13
|
+
|
|
14
|
+
## `object "X"` addresses an ANIMATABLE item — a group, instance, text or image
|
|
15
|
+
|
|
16
|
+
- A **shape is baked material**: `rect 0 0 960 540 as "Eclat"` names it so `text … along "Eclat"` can
|
|
17
|
+
find it, but it carries **no pose**, so it can never be animated. Same for a **layer**. An `object`
|
|
18
|
+
block on either used to be dropped in **total silence** — the program compiled, ran, and the animation
|
|
19
|
+
simply did not exist. It is now a **compile error** naming what you actually hit. To animate a shape,
|
|
20
|
+
wrap it: `group "Eclat" { layer "art" { rect … } }`.
|
|
21
|
+
- Corollary — **opacities multiply**. Setting `opacity 0` on the shape to hide it at rest cancels the
|
|
22
|
+
group's animation even when the group lights up. Drive the *group's* opacity and leave the shape at 1.
|
|
23
|
+
|
|
24
|
+
## One action / one binding per line
|
|
25
|
+
|
|
26
|
+
- **Comments are `//`, never `#`.** `#` starts a COLOR (`#ffcc00`), so a `# note` inside a program is a
|
|
27
|
+
parse error, not a comment. (Reference listings in these docs annotate with `#`; runnable examples use `//`.)
|
|
28
|
+
- **One single action or assignment per line.** `x = 1 y = 2` raises a clear error
|
|
29
|
+
("one action per line — unexpected `=`"), with the column pointing at the second `=`.
|
|
30
|
+
A swallowed statement is named now — `score = score + 1 send "ok", 1` reports *"two statements on one
|
|
31
|
+
line — `send …` was swallowed into the expression before it"*, not `unexpected character """`.
|
|
32
|
+
- **`send` footgun (fixed)**: `send "evt", x = 1` (with a comma) used to capture `x = 1` as
|
|
33
|
+
the *payload*. It is now a dedicated error. A `send` carries at most one payload:
|
|
34
|
+
`send "evt"`, `send "evt", <expr>`, `send "evt", text("textId")`, or the record form
|
|
35
|
+
`send "evt", { a = <expr>, b }` — several *named* values in ONE event, not several payloads.
|
|
36
|
+
Inside a record the same rule holds: `{ x = y = 1 }` is the same error.
|
|
37
|
+
|
|
38
|
+
## Drawing (region / path)
|
|
39
|
+
|
|
40
|
+
- **`stroke` DOES exist on paths** (despite the folklore):
|
|
41
|
+
`path "…" fill #rrggbb stroke #rrggbb <width> [cap butt|round|square] [join …] [miter n] [dash a,b,…]`.
|
|
42
|
+
No need to draw ropes/threads as thin filled shapes.
|
|
43
|
+
- **`stroke` ALSO exists on text** (same grammar): `text "…" color #fff stroke #000 4 join round` outlines
|
|
44
|
+
the glyphs (stroke drawn behind the fill). No need to fake outlines by stacking two text objects.
|
|
45
|
+
- **`opacity` DOES exist on paths**: `path "…" fill #000 opacity 0.5`. (8-digit hex alpha
|
|
46
|
+
works too, but `opacity` reads better.)
|
|
47
|
+
- **Shape primitives** (sugar, normalized to `path` on save):
|
|
48
|
+
- `circle <cx> <cy> <r>`
|
|
49
|
+
- `ellipse <cx> <cy> <rx> <ry>`
|
|
50
|
+
- `rect <x> <y> <w> <h>` · `… <r>` (uniform rounded corners) · `… <rx> <ry>`
|
|
51
|
+
No more hand-computing the k = 0.5523·r Bézier constant.
|
|
52
|
+
- **`filter` on a path**: accepted (`path "…" fill #000 filter glow 6 #fff`). No need to wrap
|
|
53
|
+
in a `group` just to add a shadow/glow. (Also works on group/image/text.)
|
|
54
|
+
- **`linear(angle, …)` gradient**: `0` = → (left to right), `90` = ↓ (top to bottom).
|
|
55
|
+
- **Rings / holes = ONE path with multiple closed subpaths** — fill is **even-odd**, so a nested subpath
|
|
56
|
+
cuts a hole: `path "M-30 -30L30 -30L30 30L-30 30Z M-15 -15L15 -15L15 15L-15 15Z" fill #c33` is a solid
|
|
57
|
+
filled ring (frame + hole). No need to fake it with `nofill stroke`.
|
|
58
|
+
- **Stroke width SCALES with the group**: a child of a `group`/instance at `scale 0.4` (or a `pose scale`)
|
|
59
|
+
draws its stroke at 0.4× too (the stroke is drawn in the scaled space). So a thin ring stays
|
|
60
|
+
proportional when you shrink the group — don't compensate the width by hand.
|
|
61
|
+
- **`clip <x> <y> <w> <h>` on a group/instance**: cuts content to a rectangle (the container's LOCAL
|
|
62
|
+
coords) — e.g. hide the "feet" of an emerging shape: `group "Arc" clip -100 -60 200 60 { … }`. For an
|
|
63
|
+
arbitrary clip shape, use a `mask` layer instead. **Render-only**: hit-testing and the preview/auto-size
|
|
64
|
+
bbox ignore it (clipped-away area stays clickable / counts toward the framing) — it's a visual cut.
|
|
65
|
+
|
|
66
|
+
## Text
|
|
67
|
+
|
|
68
|
+
- **Word-wrap**: opt-in via `wrap`. `text "long sentence…" … box <W> <H> wrap` breaks at
|
|
69
|
+
spaces within `W`. Without `wrap`, text does NOT wrap on its own (it respects explicit
|
|
70
|
+
`\n`). Opt-in on purpose, to avoid breaking existing layouts.
|
|
71
|
+
- **Dynamic text (read-only)**: `text "Angle: {}°" … bind "round(aDeg)" decimals 1`. The
|
|
72
|
+
expression is evaluated every frame; its formatted value fills the **`{}`** slot in the
|
|
73
|
+
content (or is shown alone when there is no `{}`). `decimals` sets the decimal count.
|
|
74
|
+
No more gauges/needles just to display a measurement (angle, timer, score…).
|
|
75
|
+
- **Centering an `image`**: the origin is the top-left corner → center with `at -W/2,-H/2`.
|
|
76
|
+
|
|
77
|
+
## Animation (pose / cel / timeline)
|
|
78
|
+
|
|
79
|
+
> Full guide: **[Animating a symbol](animating-symbols.md)**. The sharp edges:
|
|
80
|
+
|
|
81
|
+
- **`pose` rotates/scales in human units**: `pose "G" rotate 90 scale 2` — **degrees** and multipliers,
|
|
82
|
+
**around the group's `pivot`**. No hand-written `matrix(cosθ, sinθ, …)`, no radians. `scaleX`/`scaleY`
|
|
83
|
+
for non-uniform. `matrix(…)` still works as an escape hatch.
|
|
84
|
+
- **A pose only overrides what it states** (patch, not replace): `pose "G" opacity 0.5` keeps G's
|
|
85
|
+
declared position/rotation/scale. Don't re-type `at x,y` in every cel.
|
|
86
|
+
- **Set the `pivot` to the visual center** or a part **orbits** instead of spinning in place. `pivot`
|
|
87
|
+
(local, on the roster item) = the center of rotation/scale; `at x,y` (on the pose) = where the local
|
|
88
|
+
origin lands. `spin cw|ccw` / `turns N` also turn around the pivot.
|
|
89
|
+
- **`expr rotation` is in RADIANS**, like `sin`/`cos`. Stay in degrees with the helpers: `rad(45)`,
|
|
90
|
+
`turns(time)` (one turn per second), `deg(r)`. e.g. `expr rotation "turns(time * 0.5)"`.
|
|
91
|
+
- **`time` WRAPS at `durationFrames`** (the timeline loops), so `sin(time * f)` with an arbitrary `f`
|
|
92
|
+
**jumps** every loop — and a `.flatink` with no `timeline` defaults to **60 frames (2.5 s @24fps)**, so
|
|
93
|
+
the jump is frequent. For free-running ambiance use **`clock`** (monotone, never wraps): `sin(clock * f)`.
|
|
94
|
+
Alternatively drive it by the loop phase `frame / <durationFrames>` (reboucles cleanly) or set a long
|
|
95
|
+
`timeline`. `--check` warns when a channel expression uses `time` under a short looping timeline.
|
|
96
|
+
- **`x`/`y` channels REPLACE the base transform — they are NOT a delta.** `object "G" { x = bump }` sets
|
|
97
|
+
G's local x to `bump`, **overwriting** the group's declared `at X,Y` (x snaps to `bump` ≈ 0 → the element
|
|
98
|
+
jumps to the left edge whenever the expression is small). The channel value is absolute, not added to `at` —
|
|
99
|
+
classic cause of "the animation appears in the wrong place." Two ways to keep it on the anchor:
|
|
100
|
+
- **Preferred — additive offsets `dx`/`dy`** (binding-only): `object "G" { dx = bump }` resolves to
|
|
101
|
+
`pos = at + (dx, dy)`, so `dx = 58*sin(clock)` oscillates **around** `at X,Y` with no base to re-type.
|
|
102
|
+
This is the natural "offset from the anchor" idiom (mirror of CSS `translate` / pivot). `dx`/`dy` compose
|
|
103
|
+
with an absolute `x`/`y` if both are bound (`pos = x + dx`); they have no keyframe/`spring`/`smooth` form.
|
|
104
|
+
- **Or re-inject the base** with the absolute channel: `x = $(X) + bump` (same for `y`/`rotation`/`scaleX`/…).
|
|
105
|
+
- **Channel `scaleX`/`scaleY`/`rotation` turn around the group's `pivot`** (like cel poses), so a panel
|
|
106
|
+
with `pivot <center>` driven by `object "P" { scaleX = s }` grows/spins **in place** — it no longer
|
|
107
|
+
shrinks toward the top-left corner. With no `pivot` (default `{0,0}`) it scales/rotates around the origin
|
|
108
|
+
as before. When a `pivot` is set, the `x`/`y` channels position **the pivot** (the object's anchor).
|
|
109
|
+
- **A layer WITH cels draws ONLY the cel's `matter` + the containers that cel poses.** A bare shape
|
|
110
|
+
written straight into such a layer is **silently never drawn** (it stays in the roster, which holds
|
|
111
|
+
bodies, not drawings). The drawing of a frame goes inside the cel: `cel 0 { matter { circle 0 0 30
|
|
112
|
+
fill #e33 } }` — that IS how you author **frame-by-frame** (a new `matter` per cel; it holds until the
|
|
113
|
+
next one, `morph` tweens the shape). A static element belongs on a **cel-less layer**. `--check` warns
|
|
114
|
+
on all three silent drops: a bare shape in a cel layer, a `pose "X"` matching no roster item, and a
|
|
115
|
+
roster item no cel poses. See [Animating a symbol](animating-symbols.md).
|
|
116
|
+
- **Render order**: in an animated layer the **matter draws BEHIND the posed containers**, and
|
|
117
|
+
declaration order between the matter and an animated `group` is NOT preserved. To put a static
|
|
118
|
+
shape in front of an animation, give it its own **layer above** (or wrap it in a group).
|
|
119
|
+
- **Gating a subtree by opacity is FREE**: a group whose resolved `opacity` is `<= 0.01` (e.g. the
|
|
120
|
+
off-phase branch of `opacity = phase == X ? 1 : 0`, even when smoothed toward ~0) is **pruned** — its
|
|
121
|
+
whole subtree is skipped for both draw AND expression eval, exactly like the hit-test already treats it
|
|
122
|
+
as click-through. So `opacity = phase==X ? 1 : 0` on the phase groups is the idiomatic, performant way to
|
|
123
|
+
show/hide whole acts; you don't need `visible`/`hidden` (which aren't animatable anyway).
|
|
124
|
+
- **Preview without clipping**: `flatc --preview` defaults to `--bbox all` (union over every frame, so
|
|
125
|
+
drifting/rotating/growing motion fits). `--bbox frame0` is the old frame-0 measure; `--pad N` adds a
|
|
126
|
+
margin.
|
|
127
|
+
- **Preview origin is STABLE across frames**: the canvas is auto-sized once (the union), independent of
|
|
128
|
+
`--frame`. So rendering several frames of an oscillating/translating symbol gives the SAME frame each
|
|
129
|
+
time — the object moves *inside* a fixed canvas, it does not "jump". No need for an invisible anchor.
|
|
130
|
+
- **Exposed interface** (`params {}` / `states {}`): a symbol can publish typed params (color/number/bool,
|
|
131
|
+
`fill <param>`) and named states (`states door { closed at 0 open at 24 }`, driven by `Name.param = open`).
|
|
132
|
+
See [Animating a symbol](animating-symbols.md). Restyle/tune without touching internals.
|
|
133
|
+
- **A container shows only on cels that pose it** — a cel is a full snapshot, so a container omitted from a
|
|
134
|
+
cel disappears there (that's how a symbol *exits*). For a container that stays put: declare it on its own
|
|
135
|
+
**cel-less layer** (always shown, declared once), or use **`cel N hold { … }`** to carry the previous
|
|
136
|
+
cel's poses forward (compile-time sugar — you only write what changes). It's per-*keyframe*, not per-frame.
|
|
137
|
+
- **`--preview --set` bakes the values into the flatpack** (onto the wrapped instance's `params`), so a
|
|
138
|
+
pre-themed `.flatpack` plays correctly in the browser too — provided the player bundle is current (a
|
|
139
|
+
stale bundle won't resolve `fill <param>` → black; rebuild after a `@flatkit/*` bump).
|
|
140
|
+
|
|
141
|
+
## Drag & drop
|
|
142
|
+
|
|
143
|
+
- **Drop semantics**: by default the **object's center** (its x/y channels) is tested against
|
|
144
|
+
the zone. Two levers to match human expectations:
|
|
145
|
+
- `when dropped on Zone at pointer { … }`: tests the **POINTER position** (not the center).
|
|
146
|
+
- `group "Zone" … hitbox <W> <H> { … }`: an **explicit drop rectangle** (centered on the
|
|
147
|
+
origin, ±W/2 × ±H/2) instead of the content bbox. Replaces invisible `#ffffff01` paths.
|
|
148
|
+
- **Locking a placed object**: `drag x, y { enabled <expr> }`. The drag is active only while
|
|
149
|
+
the expression is ≠ 0. No more `x = (p==1) ? Zone.x : xv` + `if p==0` guard patterns.
|
|
150
|
+
- **Event order on release**: `when released` fires **BEFORE** the drop test (useful to lower a
|
|
151
|
+
"hold" flag before the drop fires). A `link` interactor writes its outputs (`<target>` index,
|
|
152
|
+
end position) **before** `released` too, so a `when released` handler can read `<target>`
|
|
153
|
+
directly (consistent with `drag`, which writes its vars before `dragged`).
|
|
154
|
+
- **Several `when dropped on` per object**: evaluated in **declaration order**, without
|
|
155
|
+
short-circuit (the basis of the right-zone / wrong-zones pattern).
|
|
156
|
+
|
|
157
|
+
## Gestures beyond drag (interactors)
|
|
158
|
+
|
|
159
|
+
Inside an `object "Name" { … }`, besides `drag x, y` / `dragX` / `dragY`:
|
|
160
|
+
|
|
161
|
+
- **`turn <angle> around <x>,<y> [{ snap <deg> · enabled <expr> }]`**: pointer-driven
|
|
162
|
+
rotation. Writes into `<angle>` the pivot→cursor direction **in radians** (like the `rotation` channel
|
|
163
|
+
and `gesture.angle`), so `rotation = <angle>` wires directly. To work in **degrees**, use the twin
|
|
164
|
+
**`turnDeg`** (writes degrees) paired with the **`rotationDeg = <angle>`** channel (sugar for
|
|
165
|
+
`rotation = rad(<angle>)`). `snap <deg>` is authored in degrees on both. Great for clock hands, dials, knobs.
|
|
166
|
+
- **`trace <progress> along <TraceGroup> [{ tolerance <px> · enabled <expr> }]`**: follow a
|
|
167
|
+
path with the finger. While the pointer stays within `tolerance` of the trace (the regions
|
|
168
|
+
of the named group), `<progress>` rises from 0 to 1 (monotone, never goes back down).
|
|
169
|
+
Great for tracing a letter, a border, a constellation. (`tolerance` defaults to 24 px.)
|
|
170
|
+
- **`reveal <progress> [{ brush <px> · enabled <expr> }]`**: scratch / wipe. The grabbed
|
|
171
|
+
object IS the area to reveal; rubbing it ticks the cells of an internal grid (cell side =
|
|
172
|
+
`brush`) and `<progress>` rises from 0 to 1 (monotone, and **cumulative across separate grabs**
|
|
173
|
+
— a child rubbing in several short strokes keeps adding coverage, it does not reset). Drive a
|
|
174
|
+
cover's opacity with `opacity = 1 - <progress>`. Great for scratch cards, fogged glass, digging.
|
|
175
|
+
(`brush` defaults to 24 px; coverage model, no pixel mask.)
|
|
176
|
+
- **`link <endX>, <endY>, <target> to <TargetsGroup> [{ enabled <expr> }]`**: pull an elastic
|
|
177
|
+
thread toward a target. During the drag, `<endX>`/`<endY>` = pointer position (DRAW the
|
|
178
|
+
thread yourself with expressions, e.g. a region connecting the object to `endX,endY`). On
|
|
179
|
+
release, `<target>` = the **1..n** index of the named child of the group that was hit
|
|
180
|
+
(0 if none), and the thread end snaps to the linked target's center; off-target, the
|
|
181
|
+
author handles the "return" via `<target> == 0`. Several links coexist: one `link`
|
|
182
|
+
interactor per source object. Great for word↔image, prey↔predator, capital↔country.
|
|
183
|
+
**WORLD coords** (place sources and targets at the scene root so the thread lines up).
|
|
184
|
+
- Reminder: `{ enabled <expr> }` (dynamic lock) works on `turn`/`trace`/`reveal`/`link` too.
|
|
185
|
+
- **Output into an ARRAY element**: every gesture output accepts `name[<idx>]` (not just a
|
|
186
|
+
bare identifier) — `drag hx[i], hy[i]`, `turn ang[k] around …`, `reveal seen[2]`,
|
|
187
|
+
`link ex[i], ey[i], rel[i] to …`. This is the natural form **under `each`**:
|
|
188
|
+
`each "Handle" as i { drag hx[i], hy[i] }` attaches one drag per instance, each writing its
|
|
189
|
+
OWN slot. (The index is substituted by `each`; the array must exist: `var hx = fill(n, 0)`.)
|
|
190
|
+
|
|
191
|
+
## Feedback (reactions in one line)
|
|
192
|
+
|
|
193
|
+
An object's channel expressions can read **its own interaction state**: `self.hovered`,
|
|
194
|
+
`self.grabbed`, `self.pressed` (each `0`/`1`). So a hover-lift or a grab-squash is just an
|
|
195
|
+
expression — no mirror variable, no handler:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
object "Button" {
|
|
199
|
+
scaleX = self.hovered ? 1.06 : 1
|
|
200
|
+
opacity = self.hovered ? 0.85 : 1
|
|
201
|
+
scaleY = self.grabbed ? 0.94 : 1
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`self.hovered` tracks the pointer **handler-independently** (you do not need a `when enter/leave`),
|
|
206
|
+
and composes with `self.x`/`self.y` etc. (same `self`).
|
|
207
|
+
|
|
208
|
+
- **`feedback <tokens>` sugar** — the one-liner. Inside an `object` block,
|
|
209
|
+
`feedback lift tilt dim shake(<expr>)` unfolds into the channel bindings above (auto-injecting
|
|
210
|
+
`use "feedback"`), **composed per channel** so it never clashes with your `x`/`y` position
|
|
211
|
+
bindings. Tokens: `lift` (hover grow), `tilt` (grab squash), `dim` (hover opacity),
|
|
212
|
+
`shake(<expr>)` (refusal wobble — `<expr> ≠ 0` shakes the `rotation`). One line per element
|
|
213
|
+
instead of six — the biggest size/token saver across an activity.
|
|
214
|
+
```
|
|
215
|
+
object "Tile" {
|
|
216
|
+
x = tx y = ty // your position bindings, untouched
|
|
217
|
+
feedback lift dim shake(wrongZone)
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
- **`use "feedback"` functions** — the same reactions as plain helpers if you want to wire them by
|
|
221
|
+
hand: `lift(h)` · `dim(h)` · `tilt(g)` · `sink(g)` · `shake(bad, t)`. (Settle-bounce is not here
|
|
222
|
+
yet: it needs a release timestamp, so it is not stateless.)
|
|
223
|
+
- ⚠️ **Capture an instant with `clock`, never `time`.** `pulse`/`shake` ride the monotone `clock`, so
|
|
224
|
+
an instant captured on the looping `time` is compared against an axis it never shares: `clock - since`
|
|
225
|
+
grows without bound and **the ramp never fires**. Nothing jumps, nothing blinks — the failure is
|
|
226
|
+
entirely silent, which makes it far more expensive than a visible one. It is the classic leftover of
|
|
227
|
+
a pre-0.23 codebase, where `pulse` rode `time` too.
|
|
228
|
+
```
|
|
229
|
+
when wrong { shown = clock } // ✓ monotone — pulse can subtract it
|
|
230
|
+
when wrong { shown = time } // ✗ wraps every durationFrames — the ramp never starts
|
|
231
|
+
opacity = pulse(shown, 4)
|
|
232
|
+
```
|
|
233
|
+
`flatc --check` now names it: *"shown" is captured on `time` but read as an INSTANT by `pulse()`…*
|
|
234
|
+
|
|
235
|
+
## Factoring
|
|
236
|
+
|
|
237
|
+
- **Scene-level `repeat`**: generates N items.
|
|
238
|
+
```
|
|
239
|
+
scene {
|
|
240
|
+
layer "Stars" {
|
|
241
|
+
repeat i from 0 to 9 {
|
|
242
|
+
circle $(60 + i*40) 80 6 fill #ffd98a
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
The index is used inside numbers via the **`$(expr)`** interpolation (compile-time
|
|
248
|
+
arithmetic: `$(i*40)`, `$( (i+1)*20 )`…). Nested loops are fine (grids).
|
|
249
|
+
⚠️ Not to be confused with `repeat … times` / `repeat i from … to …` inside `object`
|
|
250
|
+
scripts (a RUNTIME loop, executed every frame).
|
|
251
|
+
- **`def <name> = <expr>`**: a named **compile-time** constant (columns, margins, counts…).
|
|
252
|
+
Declared anywhere (the line is removed at compile time), usable in any scene coordinate via
|
|
253
|
+
`$()`, including `repeat` bounds. A `def` may reference earlier ones.
|
|
254
|
+
```
|
|
255
|
+
def colL = 120
|
|
256
|
+
def gap = 40
|
|
257
|
+
def n = 2
|
|
258
|
+
scene { layer "L" {
|
|
259
|
+
repeat i from 0 to n { circle $(colL + i*gap) 80 6 fill #ffd98a }
|
|
260
|
+
} }
|
|
261
|
+
```
|
|
262
|
+
`def`s resolve **everywhere**: scene coordinates AND behavior expressions (`object`/`each`,
|
|
263
|
+
e.g. `x = 20 + t * $(vmax)`). ⚠️ Compile-time only: `def`s are NOT runtime variables (no
|
|
264
|
+
`var`), they vanish from the model (like `repeat`); for a value that changes at runtime,
|
|
265
|
+
use `var`. (Avoid naming a `def` like a symbol parameter — collision.)
|
|
266
|
+
- **`at center` anchor**: positions an item at the canvas center. `at center` (both axes),
|
|
267
|
+
`at center,540` (x centered, y = 540), `at 120,center` (x = 120, y centered). Sugar
|
|
268
|
+
resolved at parse from `size` (re-serialized as coords, like `def`). Composes with `$()`.
|
|
269
|
+
- **`align <point> of "Name" [offset dx,dy]` anchor**: puts an item's **origin** on a point
|
|
270
|
+
of **another item's bbox** (center inside a frame, hang a counter under a pit, snap a sign
|
|
271
|
+
to an edge). 9 points: `center`, `top`, `bottom`, `left`, `right`, `topleft`, `topright`,
|
|
272
|
+
`bottomleft`, `bottomright` (an edge point = centered on the cross axis). Optional
|
|
273
|
+
`offset dx,dy`.
|
|
274
|
+
```
|
|
275
|
+
group "Counter" align bottom of "Pit" offset 0,18 { … }
|
|
276
|
+
group "Tag" align top of "Bin" { … }
|
|
277
|
+
```
|
|
278
|
+
⚠️ **STATIC bbox** (scene transforms, WITHOUT expression channels — same as drop zones).
|
|
279
|
+
Anchoring only (NO adjacency/flow: deliberate — stacking is done with `repeat`+`$()`,
|
|
280
|
+
e.g. `at $(120 + i*84),540`). Positions in **root** space (place source and target at the
|
|
281
|
+
same level). Missing target = error. Sugar resolved at parse (re-serialized as coords).
|
|
282
|
+
- **Parameterized symbols**: `symbol "Name"(p, q = default) { … }` + `instance "Name"(args)
|
|
283
|
+
[as "X"] at x,y`. Reuse a VISUAL while varying values (label, tint, size). `$(param)`
|
|
284
|
+
substitution in the body — params can be **numbers AND text/color** (`text "$(label)"`,
|
|
285
|
+
`fill $(tint)`).
|
|
286
|
+
```
|
|
287
|
+
symbol "Card"(label, tint = "#ffffff") {
|
|
288
|
+
layer "c" { rect -40 -40 80 80 fill $(tint)
|
|
289
|
+
text "$(label)" as "lbl$(label)" font "sans-serif" size 20 align center line 1.2 color #000 box 80 80 }
|
|
290
|
+
}
|
|
291
|
+
scene { layer "L" {
|
|
292
|
+
repeat i from 0 to 4 { instance "Card"($(i+1)) as "C$(i)" at $(80 + i*90),200 }
|
|
293
|
+
} }
|
|
294
|
+
```
|
|
295
|
+
- **Defaults**: a param can gain a default without breaking existing calls (the signature
|
|
296
|
+
doubles as documentation).
|
|
297
|
+
- **Per-instance ids**: `as "lbl$(label)"` in the body → each card exposes its own
|
|
298
|
+
`text("lblFire")` for `send` payloads.
|
|
299
|
+
- Each `instance(...)` becomes a concrete **`group`** (substitution at PARSE time, **zero
|
|
300
|
+
runtime cost**).
|
|
301
|
+
- ⚠️ A param is **frozen** (≠ `var`: it is not a runtime variable). The body sees ONLY its
|
|
302
|
+
params (not the `i` of an outer `repeat` — pass it as an argument). Wrong arity / unknown
|
|
303
|
+
param = error.
|
|
304
|
+
- ⚠️ Compile-time sugar (re-serialized as groups, like `def`/`repeat`). For shared
|
|
305
|
+
BEHAVIOR, see `each` below.
|
|
306
|
+
- ⚠️ **`.flatink`-only**: a parameterized `symbol "X"(…)` lives in the **program** (`.flatink`), NOT in a
|
|
307
|
+
`.flat` library. `.flat` libs hold **non-parameterized** symbols, instanced **without** parens
|
|
308
|
+
(`instance "Hero" as "H"`); a parameterized one is instanced **with** args (`instance "Card"("A")`).
|
|
309
|
+
Putting a `(…)` symbol in a `.flat` — or letting `flatc` auto-discover such a `.flat` in the folder —
|
|
310
|
+
surfaces as a misleading `"{" expected, "("` (the `.flat` parser doesn't take parameters). Rule of
|
|
311
|
+
thumb: parens ⇔ parameterized ⇔ inline in the `.flatink`.
|
|
312
|
+
- **`each "Symbol" as i { … }`**: applies BEHAVIOR to every instance of a symbol, with
|
|
313
|
+
index `i`.
|
|
314
|
+
- **Channel bindings** on real instances (`each "Brick" as i { opacity = bricks[i] }`) →
|
|
315
|
+
resolved at runtime (`vals[i]`, variable arrays).
|
|
316
|
+
- **Handlers** on the instances of a **parameterized symbol** (`each "Key" as i { when
|
|
317
|
+
clicked { … } }`) → unrolled at parse into one `object` per generated instance (index `i`
|
|
318
|
+
substituted). The BEHAVIOR counterpart of parameterized symbols (the VISUAL): together
|
|
319
|
+
they make a full keypad in ~10 lines.
|
|
320
|
+
⚠️ Give instances distinct names (`as "K$(i)"`) so each handler targets ITS tile.
|
|
321
|
+
- **Indexed assignment**: `arr[<expr>] = <value>` (the `set` keyword is optional). The index
|
|
322
|
+
can be any expression, **including nested**: `occ[sl[i + 1]] = 0` works (balanced
|
|
323
|
+
brackets). An unclosed bracket is a **hard error** (no more silent truncation).
|
|
324
|
+
- **`else if`**: supported (`} else if cond {`); no need to nest `else { if … }`.
|
|
325
|
+
- **`match` — declarative pairing** (factors the drag+drop boilerplate). Unrolled into
|
|
326
|
+
`object` blocks.
|
|
327
|
+
```
|
|
328
|
+
match Word1, Word2, Word3 onto NounBin, VerbBin, AdjBin {
|
|
329
|
+
correct Word1 -> NounBin, Word2 -> VerbBin, Word3 -> AdjBin
|
|
330
|
+
lock on wrong // optional; absent = RETRYABLE (default)
|
|
331
|
+
on correct as it { send "found", text(it) } // optional GENERIC action hooks
|
|
332
|
+
on wrong as it { send "miss" } // `it` = the current item's name
|
|
333
|
+
on done { send "done" } // fires when all correct placements are in
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
Generates, per item: `drag <Item>_x, <Item>_y { enabled <Item>_placed == 0 }` + one
|
|
337
|
+
`dropped on` per zone that sets the **exposed state**: `<Item>_placed` (0/1), `<Item>_ok`
|
|
338
|
+
(0/1), `<Item>_zone` (index). **No event is imposed** (full host decoupling) — you decide
|
|
339
|
+
what to send via the hooks. The **visual stays yours**: declare `var <Item>_x`/`var
|
|
340
|
+
<Item>_y` (start positions) and write your expressions.
|
|
341
|
+
|
|
342
|
+
## Tooling (the generation loop)
|
|
343
|
+
|
|
344
|
+
- **`flatc --check <file>`**: semantic lint only. Exits ≠ 0 on ERROR; **warnings** print
|
|
345
|
+
without blocking. Also covers **layout** (approximate, no rendering): text (without `wrap`)
|
|
346
|
+
overflowing the canvas edge, image/text clipped at an edge, missing drop zone,
|
|
347
|
+
**overlapping hitboxes**, never-used global variable.
|
|
348
|
+
- **`flatc --watch <file>`**: recompiles on every change in the folder.
|
|
349
|
+
- **`flatc --render <file> -o out.png [--frame N] [--at k=v[,k2=v2]] [--steps N] [--scale S]`**:
|
|
350
|
+
renders a headless **PNG image** (skia backend, faithful to the browser: SVG, gradients,
|
|
351
|
+
glow/shadow filters). This is how you **see what you draw** before playing. `--at` forces
|
|
352
|
+
variables → capture a precise state (e.g. `--at step=2` for an escape-room stage); `--frame N`
|
|
353
|
+
targets a frame. **`--steps N`** runs N fixed simulation steps (`every frame`, 60 Hz) *before*
|
|
354
|
+
the capture, so a stateful act unfolds on its own — no need to force every derived ramp variable
|
|
355
|
+
by hand in `--at`. (Bounded to 10 000 steps.) Requires the optional `skia-canvas` dependency
|
|
356
|
+
(see the error message for the install steps).
|
|
357
|
+
- **`flatc --assets inline|external <file>`**: how media is baked. `inline` (default) embeds each
|
|
358
|
+
asset as a base64 `data:` URI inside the `.flatpack` — one portable file. `external` keeps
|
|
359
|
+
`asset.data` as a relative key and copies the files into a sidecar `<out>.assets/` folder; serve
|
|
360
|
+
that folder and play with `sameOriginAssetResolver(<flatpackUrl>)`. Use `external` for big media
|
|
361
|
+
(video, large audio) you do not want inflating the JSON.
|
|
362
|
+
- **`flatc --play <file.flatink|.flatpack> --script <gestures.json>`**: plays **headless**
|
|
363
|
+
(no canvas), replays a gesture script and prints `{ sends, vars }` as JSON. Great in CI.
|
|
364
|
+
Script format: an array of gestures. **Prefer SEMANTIC gestures** (by object NAME) —
|
|
365
|
+
robust (coordinate-independent), readable, and the engine resolves the position:
|
|
366
|
+
```json
|
|
367
|
+
[
|
|
368
|
+
{ "type": "drag", "source": "Card1", "target": "ZoneA" },
|
|
369
|
+
{ "type": "wait", "frames": 30 },
|
|
370
|
+
{ "type": "tap", "target": "Button" },
|
|
371
|
+
{ "type": "scratch", "target": "Cover1" },
|
|
372
|
+
{ "type": "connect", "source": "Word", "target": "Picture" }
|
|
373
|
+
]
|
|
374
|
+
```
|
|
375
|
+
`drag source→target` = the engine grabs `source` (at its RESOLVED position, expressions
|
|
376
|
+
included) and releases it at the center of `target`; `tap target` = a click at the object's
|
|
377
|
+
center; **`scratch target`** = the engine sweeps the `reveal` target's whole bbox for you
|
|
378
|
+
(boustrophedon at the brush spacing) so its coverage reaches ~1 — no more dozens of hand-typed
|
|
379
|
+
`move`s; **`connect source→target`** = pulls a `link` wire from `source` and releases over
|
|
380
|
+
`target`, resolving the target index. Unknown object → **hard error** (not a silent miss). The
|
|
381
|
+
generator describes the INTENT; the engine guarantees the interaction.
|
|
382
|
+
LOW-LEVEL gestures (scene coords) remain available for special cases:
|
|
383
|
+
```json
|
|
384
|
+
[
|
|
385
|
+
{ "type": "down", "x": 50, "y": 50 }, { "type": "move", "x": 60, "y": 60 }, { "type": "up", "x": 60, "y": 60 },
|
|
386
|
+
{ "type": "set", "name": "unlocked", "value": 1 }, { "type": "wait", "frames": 60 }
|
|
387
|
+
]
|
|
388
|
+
```
|
|
389
|
+
(`set` drives a variable from the "host"; `wait` lets the simulation run N fixed 60 Hz
|
|
390
|
+
steps — required to "wait out" `every frame` physics, time does not advance on its own in
|
|
391
|
+
headless mode.)
|
|
392
|
+
- **`flatc --play … --trace`**: instead of the final JSON, a **human-readable log per
|
|
393
|
+
gesture** — the emitted `send`s + the **variable diff** at each step. Step-by-step
|
|
394
|
+
inspection to understand/debug a script (a headless debug-player):
|
|
395
|
+
```
|
|
396
|
+
drag Word1→Bin1 sends:[found] vars{Word1_placed:undefined→1 Word1_ok:undefined→1}
|
|
397
|
+
wait 5
|
|
398
|
+
drag Word2→Bin2 sends:[found, win] vars{Word2_placed:undefined→1 Word2_ok:undefined→1}
|
|
399
|
+
```
|
|
400
|
+
- **`expect` — self-verification in CI**: a gesture `{ "type": "expect", "sends": ["done"],
|
|
401
|
+
"vars": { "score": 3 } }` in the script compares and makes **`flatc --play` exit ≠ 0** on
|
|
402
|
+
mismatch (each mismatch listed on stderr). `sends` = the **sequence of names** emitted
|
|
403
|
+
SINCE the last `expect`; `vars` = current state. No more eyeballing: the script becomes a
|
|
404
|
+
test. Works with or without `--trace`.
|
|
405
|
+
- **Gesture recording**: `player.startRecording()` / `stopRecording(): Gesture[]` capture
|
|
406
|
+
gestures played BY HAND (down/up/cancel + the `move`s during a drag, with `wait`s for
|
|
407
|
+
elapsed time) → a script **directly replayable by `--play`**. No more hand-written coords.
|
|
408
|
+
- **Canonical file name**: `.flatpack` (JSON inside; `.flatpack.json` is a legacy alias that
|
|
409
|
+
`--play` tolerates).
|
|
410
|
+
|
|
411
|
+
## `filter` performance
|
|
412
|
+
|
|
413
|
+
- A `filter glow`/`shadow` = **one offscreen canvas recomposited** per group carrying the
|
|
414
|
+
filter. The engine automatically **caches** *static* filtered subtrees (decor with no
|
|
415
|
+
expression or animation): they are only re-rendered on zoom/pan or asset load. So a filter
|
|
416
|
+
on **static decor** is nearly free in steady state.
|
|
417
|
+
- A filter on an **animated** element (channel expression, `bind`, timeline) is recomposited
|
|
418
|
+
every frame. Keep filters on animated elements **small**.
|
|
419
|
+
- Cheap alternative for decor shadows: a **"baked" shadow** = the same path offset by a few
|
|
420
|
+
px in `#00000028` UNDER the cutout (no offscreen canvas). Ideal for large planes.
|
|
421
|
+
- Drop-shadow cost ∝ area × blur: a large blurred plane is expensive when not cached.
|
|
422
|
+
|
|
423
|
+
## Audio
|
|
424
|
+
|
|
425
|
+
- `sound "assetId" at <frame> [gain <g>] [loop]` on the timeline, and the `sound "assetId"`
|
|
426
|
+
action in a handler, ARE wired (WebAudio). Declare the asset like images:
|
|
427
|
+
`asset "ding" "ding.mp3" sound`. Media (mp3/wav/ogg/m4a) are embedded as data URIs by
|
|
428
|
+
`flatc`. (No sample sounds ship for now — bring your own.)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Embedding the player: loading embedded fonts
|
|
2
|
+
|
|
3
|
+
A `.flatink` can **embed its fonts** so a deck is self-contained:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
asset "Archivo Black" "ArchivoBlack.woff2" font
|
|
7
|
+
scene { layer "L" { text "Déçu où ? ÇÀ" at 20,40 font "Archivo Black" size 48 } }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`flatc` inlines the face into the compiled doc as a base64 data-URI:
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
// doc.assets
|
|
14
|
+
[{ "id": "Archivo Black", "kind": "font", "mime": "font/woff2",
|
|
15
|
+
"data": "data:font/woff2;base64,…", "family": "Archivo Black" }]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The host owns font loading
|
|
19
|
+
|
|
20
|
+
`FlatPlayer` draws text with `ctx.font = "<family>"` on the canvas it was given. It does **not** register
|
|
21
|
+
the doc's `kind:'font'` assets itself — loading a face is environment-specific (a browser `FontFace`, a
|
|
22
|
+
skia `FontLibrary`, …) and asynchronous, while the player's render is synchronous. So **the host that embeds
|
|
23
|
+
the player is responsible for registering the embedded fonts before rendering.** Skip this and text silently
|
|
24
|
+
falls back to a system font.
|
|
25
|
+
|
|
26
|
+
> The `family` to register is **`asset.family || asset.id`** — it's exactly what the text targets via
|
|
27
|
+
> `font "<…>"`. Wrap loading in `try/catch`: a corrupt face should degrade to a fallback, never crash.
|
|
28
|
+
|
|
29
|
+
### Browser (`<canvas>`)
|
|
30
|
+
|
|
31
|
+
`@flatkit/player` exports the helper — `import` it, no need to reimplement:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { FlatPlayer, loadEmbeddedFonts } from '@flatkit/player'
|
|
35
|
+
|
|
36
|
+
await loadEmbeddedFonts(doc) // ← BEFORE new FlatPlayer
|
|
37
|
+
const player = new FlatPlayer(canvas, doc)
|
|
38
|
+
|
|
39
|
+
// Optional safety net (FOIT): redraw once any late face settles.
|
|
40
|
+
document.fonts?.ready.then(() => player.render())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`loadEmbeddedFonts(doc)` registers each `asset kind:'font'` (base64 data-URI) as a `FontFace` under
|
|
44
|
+
`asset.family || asset.id`, **no-ops outside a DOM** (SSR / Node), and **skips a corrupt face** (graceful
|
|
45
|
+
fallback, never throws). It's a tiny tree-shakeable export — it pulls in no extra dependency (uses the
|
|
46
|
+
browser's own `FontFace` / `document.fonts`). Calling it again (e.g. a remount) is harmless: a family
|
|
47
|
+
already on `document.fonts` is not re-registered.
|
|
48
|
+
|
|
49
|
+
> **Security.** Only embedded `data:` URIs are honored, and the bytes are decoded and handed to `FontFace`
|
|
50
|
+
> directly — `asset.data` is never spliced into a CSS `src` string. So an untrusted doc can neither point a
|
|
51
|
+
> face at a remote origin (no network fetch) nor inject extra CSS `src` descriptors via `url()`/`local()`.
|
|
52
|
+
> This is the same "no arbitrary fetch" contract the player's image/audio paths enforce.
|
|
53
|
+
|
|
54
|
+
### Node / headless (`skia-canvas`)
|
|
55
|
+
|
|
56
|
+
`FontLibrary` reads **file** paths, so materialize each base64 data-URI to a temp file, then register it.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { FlatPlayer } from '@flatkit/player'
|
|
60
|
+
import { FontLibrary } from 'skia-canvas'
|
|
61
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
62
|
+
import { tmpdir } from 'node:os'
|
|
63
|
+
import { join } from 'node:path'
|
|
64
|
+
|
|
65
|
+
function useEmbeddedFonts(doc) {
|
|
66
|
+
for (const a of doc.assets ?? []) {
|
|
67
|
+
if (a.kind !== 'font' || !/^data:[^;]*;base64,/.test(a.data ?? '')) continue
|
|
68
|
+
const base64 = a.data.slice(a.data.indexOf(',') + 1)
|
|
69
|
+
const ext = (a.mime || '').includes('woff2') ? 'woff2' : (a.mime || '').includes('woff') ? 'woff' : 'ttf'
|
|
70
|
+
const file = join(mkdtempSync(join(tmpdir(), 'fk-fonts-')), `${(a.family || a.id).replace(/[^a-z0-9]+/gi, '_')}.${ext}`)
|
|
71
|
+
writeFileSync(file, Buffer.from(base64, 'base64'))
|
|
72
|
+
try {
|
|
73
|
+
FontLibrary.use(a.family || a.id, [file]) // 2-arg form forces the family alias
|
|
74
|
+
} catch {
|
|
75
|
+
/* invalid font → fallback */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
useEmbeddedFonts(doc) // ← BEFORE the first render
|
|
81
|
+
const player = new FlatPlayer(canvasEl, doc, { input: false, audio: false })
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`FontLibrary` is a **global, idempotent** registry for the process — registering the same family twice is
|
|
85
|
+
harmless (you may memoize by family to avoid re-writing temp files).
|
|
86
|
+
|
|
87
|
+
## What's already handled for you
|
|
88
|
+
|
|
89
|
+
- **`flatc --render`** (the CLI, used for preview/OG images) registers embedded fonts **internally** before
|
|
90
|
+
drawing — CLI users and render pipelines get correct text for free. The host glue above is only needed when
|
|
91
|
+
you drive `@flatkit/player` **directly**.
|
|
92
|
+
|
|
93
|
+
- The **browser** path ships as `loadEmbeddedFonts` (exported above). The **skia/Node** path stays a snippet:
|
|
94
|
+
it would drag the optional `skia-canvas` dependency into the package, so it's left to the host (and the
|
|
95
|
+
`flatc --render` CLI already covers headless rendering).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Expressions & stdlib
|
|
2
|
+
|
|
3
|
+
Expressions appear in channel bindings (`rotation = …`), conditions (`if …`, `enabled …`), `send`
|
|
4
|
+
payloads, `bind` text, text-on-path `start`/`spacing` (`text … along "…" start "time*0.1"`), and `$()`
|
|
5
|
+
interpolation. They are **pure and numeric** — no statements, no loops, no side effects (so they can't
|
|
6
|
+
hang and only touch the values the runtime provides).
|
|
7
|
+
|
|
8
|
+
## Operators
|
|
9
|
+
|
|
10
|
+
From lowest to highest precedence:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
?: ternary a > 0 ? 1 : -1
|
|
14
|
+
|| && logical
|
|
15
|
+
== != < > <= >= comparison (a value ≠ 0 is "true")
|
|
16
|
+
+ - * / % arithmetic
|
|
17
|
+
- ! unary
|
|
18
|
+
. [] member / index mouse.x · slots[i]
|
|
19
|
+
fn(…) call
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
There are no booleans — comparisons and logic yield `1` / `0`.
|
|
23
|
+
|
|
24
|
+
## Built-in functions
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
sin cos tan asin acos atan atan2
|
|
28
|
+
abs sqrt pow exp log floor ceil round sign
|
|
29
|
+
min max hypot clamp(x, lo, hi) lerp(a, b, t) mod(a, b) between(x, lo, hi)
|
|
30
|
+
rad(deg) deg(rad) turns(n)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Constants: `PI`, `TAU` (2π), `E`.
|
|
34
|
+
|
|
35
|
+
> **`lerp` is the exponential smoother.** `lerp(v, target, k)` = `v + (target - v) * k` — so
|
|
36
|
+
> `niv = lerp(niv, target, 0.1)` in `every frame` eases `niv` toward `target` (no new helper needed; the
|
|
37
|
+
> target and rate stay explicit). For a timed 1→0 feedback ramp, see `pulse(since, dur)` in `use "feedback"`.
|
|
38
|
+
|
|
39
|
+
**Angles are RADIANS** (the `rotation` channel, `sin`/`cos`/`atan2`). Author in degrees with the helpers:
|
|
40
|
+
`rad(45)` (degrees → radians), `turns(n)` (n full turns → radians, e.g. `rotation = turns(time)` spins once
|
|
41
|
+
per second), `deg(r)` (the inverse, for readouts). Or bind the **`rotationDeg`** channel (sugar for
|
|
42
|
+
`rotation = rad(…)`) and use the **`turnDeg`** interactor — both work in degrees, for when that reads better.
|
|
43
|
+
|
|
44
|
+
## Reserved names
|
|
45
|
+
|
|
46
|
+
| Name | Meaning |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `time` | seconds elapsed — **resets to 0 every `durationFrames`** (the timeline loops). Fine for motion tuned to the loop; for free-running ambiance use `clock`. |
|
|
49
|
+
| `clock` | seconds elapsed, **monotone** (never wraps). Use for ambient motion in a looping/interactive scene so `sin(clock * f)` doesn't jump on each loop. |
|
|
50
|
+
| `frame` | current frame (0-based; also wraps at `durationFrames`) |
|
|
51
|
+
| `value` | the channel's current value (in a channel binding) |
|
|
52
|
+
| `mouse.x` `mouse.y` | pointer position (scene units) |
|
|
53
|
+
| `keys.<Key>` | `1` while a key is held, `0` otherwise — `<Key>` is the browser `KeyboardEvent.key` value (`keys.ArrowRight`, `keys.a`, `keys.Escape`), plus the alias `keys.Space` for the space bar. Naming a key here also makes the player **consume** it (no page scroll) — see [host integration](host-integration.md#keyboard) |
|
|
54
|
+
| `self.x` `self.y` `self.scaleX` … | the object's own current pose (in its channel bindings) |
|
|
55
|
+
| `self.hovered` `self.grabbed` `self.pressed` | the object's own interaction state (`0`/`1`) — see [feedback](behavior-and-interactions.md#feedback) |
|
|
56
|
+
| `<Name>.x` `<Name>.y` … | a named object's live channels (e.g. `Target.x`) |
|
|
57
|
+
|
|
58
|
+
## Arrays
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
var slots = [0, 0, 0]
|
|
62
|
+
object "P" { x = slots[i] } // computed index
|
|
63
|
+
slots[i + 1] = 1 // indexed assignment (in actions)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Functions (`fn`)
|
|
67
|
+
|
|
68
|
+
Define reusable helpers — a **value** function (an expression) or a **procedure** (actions):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
fn dist(ax, ay, bx, by) = hypot(ax - bx, ay - by) # value
|
|
72
|
+
fn reset() { score = 0 go to frame 0 } # procedure
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Stdlib packages
|
|
76
|
+
|
|
77
|
+
Import bundled helpers with `use "<name>"`. They're embedded (no network, no files), referenced in the
|
|
78
|
+
`.flatpack`, and resolved by the player. Functions are available **bare** and **qualified**
|
|
79
|
+
(`boxHit(…)` or `collision.boxHit(…)` — the qualified form disambiguates collisions).
|
|
80
|
+
|
|
81
|
+
> **The `use` line is optional**: calling a package function imports its package automatically. A package
|
|
82
|
+
> is a vocabulary, not a module to wire up — `pulse(…)` works because you wrote it. Write `use` when you
|
|
83
|
+
> want the dependency stated, or to pick a side when two packages share a bare name. Your own `fn` of the
|
|
84
|
+
> same name always wins.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
use "collision" # boxHit(ax,ay,bx,by,hw,hh) · dist(ax,ay,bx,by) · near(ax,ay,bx,by,r)
|
|
88
|
+
use "easing" # easeIn(t) · easeOut(t) · easeInOut(t) · smooth(t) (t in 0..1)
|
|
89
|
+
use "gesture" # snap(v,step) · snapTo(v,target,r) · railT/railX/railY(px,py,ax,ay,bx,by) · angle(cx,cy,px,py) · inZone(px,py,x,y,w,h)
|
|
90
|
+
use "feedback" # lift(h) · dim(h) · tilt(g) · sink(g) · shake(bad,t) · pulse(since,dur) (channel reactions)
|
|
91
|
+
# shake/pulse ride the MONOTONE `clock` → capture instants with `clock`, never `time`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
use "collision"
|
|
98
|
+
object "Ball" {
|
|
99
|
+
when dropped on Goal { won = near(self.x, self.y, Goal.x, Goal.y, 30) ? 1 : 0 }
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## See also
|
|
104
|
+
|
|
105
|
+
- Where expressions are used (channels, interactors, feedback) → **[Behavior & interactions](behavior-and-interactions.md)**
|
|
106
|
+
- The `feedback` sugar that writes channel expressions for you → **[Feedback](behavior-and-interactions.md#feedback)**
|
|
107
|
+
- Sending computed values out to the embedding app → **[Host integration](host-integration.md)**
|