@flatkit/compiler 0.38.0 → 0.39.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.
@@ -27,7 +27,7 @@ Inside `object "Name" { … }`:
27
27
  | `when held` | a long press |
28
28
  | `when dropped on <Zone> [at pointer]` | released over a drop zone (see [drag & drop](#drag--drop)) |
29
29
 
30
- Scene-wide: `when loaded { … }` (once), `every frame { … }` (each tick), `at frame <n> { … }`,
30
+ Scene-wide: `when loaded { … }` (once), `every frame { … }` (each simulation step, 60 Hz — see [how a frame runs](#how-a-frame-runs)), `at frame <n> { … }`,
31
31
  `label <frame> "name"`. These live at the TOP LEVEL of the program, outside any `object` block — inside
32
32
  one they do nothing, and `--check` says so.
33
33
 
@@ -501,6 +501,45 @@ match Word1, Word2 onto Good, Bad {
501
501
  It generates, per item, `<Item>_placed` / `<Item>_ok` / `<Item>_zone` state and the drag+drop handlers;
502
502
  you keep the visual (`var <Item>_x`/`_y` + your channel expressions).
503
503
 
504
+ ## How a frame runs
505
+
506
+ Three things happen, always in this order, and knowing it removes most "it reads the old value" surprises.
507
+
508
+ **1. Events, as they arrive.** A press, a move, a release or a key runs its handlers **at once**, between
509
+ two displays — not at the next step. At a release the order is: the gesture's outputs are written (drag
510
+ position, `link` target), then `when released`, then `when dropped on …` (in declaration order), then
511
+ `when clicked` if the press stayed a tap.
512
+
513
+ **2. Steps of the simulation.** `every frame { … }` runs at a **fixed 60 Hz**: one run is one step of
514
+ exactly 1/60 s, **whatever the `timeline` fps and whatever the display**. `timeline 30 …` only sets the
515
+ speed of the playhead: after 60 steps `clock` has advanced by 1 and `frame` by 30. The step has a name,
516
+ **`DT`** (= 1/60, in seconds), so an integration is written `v = v + a * DT` — never measure it from
517
+ `clock`. In a browser `clock`, `time` and `frame` follow REAL time, once per display, so two steps run in
518
+ the same display read the same `clock`; `clock - previous` is then the display's duration on the first and
519
+ `0` on the second. (`flatc --play` advances them by 1/60 per step, which is why a replay is exact.)
520
+
521
+ How many steps run before each display depends on the display: none or one at 120 Hz, one at 60 Hz, two at
522
+ 30. When the display stalls (a tab in the background, a slow device) the player does **not** catch up: it
523
+ counts at most 0.25 s per display and runs at most 30 steps, dropping the rest. The simulation then runs
524
+ slower than the wall clock; it never jumps. Within a step, the scene's `every frame` runs first, then
525
+ those of the active symbols; `at frame <n>` scripts come after, in the same step.
526
+
527
+ **3. The picture.** Channel bindings (`x = px`, `opacity = lit`) are not statements that run: they are
528
+ read whenever something looks at the object — when it is drawn, when it is hit-tested, when a handler
529
+ reads `Target.x` — and always give the value of NOW. What is drawn between two steps is interpolated
530
+ between them, for smoothness; variables are never changed by that.
531
+
532
+ What follows from it:
533
+
534
+ - **A handler reads a derived value as the last step left it.** If `every frame { double = count * 2 }`
535
+ and a handler does `count = count + 1` then reads `double`, it reads the value from BEFORE its own
536
+ write. Two events between two steps: the second sees what the first *wrote*, not what `every frame`
537
+ derives from it. Derive in the handler what the handler needs, or make it a function (`fn`).
538
+ - **`flatc --play` gives every pointer event one step** (see [tooling](tooling.md#headless-play----play)),
539
+ as a real pointer does; `"settle": 0` replays two events with no step in between, which is the case above.
540
+ - **The step is guaranteed**; the number of steps per display is not. A rule that counts steps counts
541
+ sixtieths of a second of simulated time.
542
+
504
543
  ## See also
505
544
 
506
545
  - The expression language and stdlib → **[Expressions & stdlib](expressions-and-stdlib.md)**
@@ -10,6 +10,11 @@
10
10
  - A `.flatink` file splits in two: the **`scene { … }`** block (the VISUAL composition:
11
11
  `path`/`circle`/`group`/`image`/`text`) and the **behavior** that follows (`object "Name"
12
12
  { … }`, `every frame`, timeline bindings). The two do NOT share the same grammar.
13
+ - **`every frame` is a 60 Hz simulation step, not a timeline frame.** One run is exactly 1/60 s —
14
+ the constant **`DT`** — whatever `timeline <fps>` says and whatever the display does; integrate with
15
+ `v = v + a * DT`, never with a `dt` measured from `clock`. Handlers run when their event arrives,
16
+ BEFORE the next step: a handler that reads a value derived in `every frame` reads the one of the last
17
+ step. The full order is in [how a frame runs](behavior-and-interactions.md#how-a-frame-runs).
13
18
 
14
19
  ## `object "X"` addresses an ANIMATABLE item — a group, instance, text or image
15
20
 
@@ -30,7 +30,8 @@ min max hypot clamp(x, lo, hi) lerp(a, b, t) mod(a, b) between(x, lo, hi)
30
30
  rad(deg) deg(rad) turns(n)
31
31
  ```
32
32
 
33
- Constants: `PI`, `TAU` (2π), `E`.
33
+ Constants: `PI`, `TAU` (2π), `E`, and **`DT`** — the duration of one `every frame` step, in seconds: 1/60,
34
+ whatever the timeline's fps (`v = v + a * DT`). See [how a frame runs](behavior-and-interactions.md#how-a-frame-runs).
34
35
 
35
36
  > **`lerp` is the exponential smoother.** `lerp(v, target, k)` = `v + (target - v) * k` — so
36
37
  > `niv = lerp(niv, target, 0.1)` in `every frame` eases `niv` toward `target` (no new helper needed; the
@@ -70,6 +71,38 @@ its index are expressions, only the element picked is evaluated, and it indexes
70
71
  index is rounded; outside the table it is `NaN`, so the binding keeps its fallback). It is not a value on
71
72
  its own: `[1, 2, 3]` without an index is an error — to keep a table, declare `var t = [1, 2, 3]`.
72
73
 
74
+ ## Determinism
75
+
76
+ For someone replaying a simulation elsewhere (a Python replica, a test that compares to the bit), what can
77
+ be counted on. Numbers are IEEE 754 doubles, and an expression is evaluated in the order it is written,
78
+ one rounded operation at a time (no fused multiply-add).
79
+
80
+ - **Exact** — the same bits on every engine: the operators `+ - * / %` and the comparisons, and `abs`
81
+ `floor` `ceil` `round` `sign` `min` `max` `sqrt` `clamp` `lerp` `mod` `between` `rad` `deg` `turns`.
82
+ (`sqrt` is the IEEE square root, correctly rounded on every engine in use, although ECMAScript does not
83
+ formally demand it. `rad`, `deg` and `turns` multiply by the double `PI`: exact, provided the replica
84
+ does the same operations in the same order — `rad(d)` is `d * PI / 180`.)
85
+ - **Engine-dependent** — ECMAScript leaves the last bits to the implementation: `sin` `cos` `tan` `asin`
86
+ `acos` `atan` `atan2` `pow` `exp` `log` `hypot`. Two browsers usually agree, and nothing guarantees
87
+ it; a Python or C library need not agree with either. Integrated over thousands of steps, one bit
88
+ becomes a visible gap. A replica that must match to the bit uses its own polynomial for these, on both
89
+ sides.
90
+
91
+ Three spellings that differ from other languages:
92
+
93
+ - **`%`** is the remainder of the truncated division: it takes the sign of the LEFT operand (`-1 % 3` is
94
+ `-1`), and works on decimals (`3.25 % 12` is `3.25`). It is C's `fmod`, Python's `math.fmod` — not
95
+ Python's `%`. **`mod(a, b)`** is the positive one (`mod(-1, 3)` is `2`), Python's `%` for `b > 0`.
96
+ - **`round`** sends a half UP, toward +∞: `round(2.5)` is `3`, `round(-2.5)` is `-2`. Python's `round`
97
+ sends it to the even neighbour. An array index is rounded the same way.
98
+ - **`random()`** draws from a seeded generator made of integer operations only: the same seed gives the
99
+ same sequence on every engine. A replay is always seeded (`flatc --play`, seed `1`, or `--seed N`); a
100
+ player in a page draws from the browser unless its host passes `seed`.
101
+
102
+ Time: one step of `every frame` is exactly **`DT`** = 1/60 s — integrate with it. `clock`, `time` and
103
+ `frame` follow real time in a browser and are exact only in a replay (see
104
+ [how a frame runs](behavior-and-interactions.md#how-a-frame-runs)).
105
+
73
106
  ## Functions (`fn`)
74
107
 
75
108
  Define reusable helpers — a **value** function (an expression) or a **procedure** (actions):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkit/compiler",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "The FlatInk language (parser + AST) and compiler (.flatink → .flatpack). Ships the flatc CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Zwyk Studio",
@@ -57,9 +57,9 @@
57
57
  "docs"
58
58
  ],
59
59
  "dependencies": {
60
- "@flatkit/engine": "0.38.0",
61
- "@flatkit/player": "0.38.0",
62
- "@flatkit/types": "0.38.0"
60
+ "@flatkit/player": "0.39.0",
61
+ "@flatkit/engine": "0.39.0",
62
+ "@flatkit/types": "0.39.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "skia-canvas": "^3.0.8 || ^4.0.0-rc7"
@@ -288,7 +288,9 @@ align top of "Bin" [offset dx,dy] // pin origin onto ano
288
288
  Pure & numeric (no booleans: comparisons/logic yield `1`/`0`). Operators: `?: || && == != < > <= >=
289
289
  + - * / % - ! . [] fn()`. A lookup table can be written in place and indexed at once: `[10, 20, 30][i]`.
290
290
  Built-ins: `sin cos tan asin acos atan atan2 abs sqrt pow exp log floor ceil round sign min max hypot
291
- clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Constants `PI TAU E`.
291
+ clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Constants `PI TAU E`, and `DT` = 1/60 s: `every frame` is a
292
+ fixed 60 Hz step whatever the timeline's fps, so integrate with `v = v + a * DT`. Handlers run when their event
293
+ arrives, before the next step.
292
294
  Reserved: `time` (seconds, **wraps** every `durationFrames`), `clock` (seconds, **monotone**), `frame`,
293
295
  `value`, `mouse.x/y`, `keys.<Key>`, `self.*`, `<Name>.*`.
294
296
  Packages: `use "collision" | "easing" | "gesture" | "feedback"`; functions are available bare and
@@ -104,7 +104,7 @@ State/funcs: `var a = 0` · `var arr = fill(8,0)` (also as an ASSIGNMENT: `arr =
104
104
  ## Expressions
105
105
  Pure numeric, no booleans (compare/logic → 1/0). Ops: `?: || && == != < > <= >= + - * / % - ! . [] fn()` · inline table `[10, 20, 30][i]`.
106
106
  Funcs: `sin cos tan atan2 abs sqrt pow floor ceil round sign min max hypot clamp(x,lo,hi) lerp(a,b,t)
107
- mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Const `PI TAU E`.
107
+ mod(a,b) between(x,lo,hi) rad(deg) deg(rad) turns(n)`. Const `PI TAU E` · `DT` = 1/60 s (the `every frame` step, fixed 60 Hz: `v = v + a * DT`).
108
108
  Reserved: `time`(s, **wraps**) `clock`(s, **monotone**) `frame` `value` `mouse.x/y` `keys.<Key>` `self.*` `<Name>.*`.
109
109
  Channels: `x y scaleX scaleY rotation opacity` (absolute) + `dx dy` (additive: `pos = at + (dx, dy)`).
110
110
  Stateful easing: `spring <ch> "<target>" stiffness <0..1> damping <0..1>` · `smooth <ch> "<target>" k <0..1>`
@@ -68,7 +68,7 @@ object "Dial" { spring rotation = aim { stiffness 0.08 damping 0.86 } } // smo
68
68
  Pure numeric expressions (no booleans — logic/compares yield `1`/`0`). Operators `?: || && == != < >
69
69
  <= >= + - * / % - ! . [] fn()`. Built-ins: `sin cos tan atan2 abs sqrt pow exp log floor ceil round
70
70
  sign min max hypot clamp(x,lo,hi) lerp(a,b,t) mod(a,b) between(x,lo,hi) rad deg turns`. Constants
71
- `PI TAU E`. Reserved: `time` (s), `frame`, `value`, `mouse.x/y`, `keys.<Key>`, `self.*`, `<Name>.*`.
71
+ `PI TAU E`, `DT` (= 1/60 s: `every frame` is a fixed 60 Hz step, integrate with `v = v + a * DT`). Reserved: `time` (s), `frame`, `value`, `mouse.x/y`, `keys.<Key>`, `self.*`, `<Name>.*`.
72
72
 
73
73
  Functions: `fn dist(ax,ay,bx,by) = hypot(ax-bx, ay-by)` (value) · `fn reset() { score = 0 }` (procedure).
74
74