@cssints/apg-specs 0.0.0-stage → 0.5.2

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.
Files changed (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -2
  3. package/dist/apg/accordion.bridge.d.mts +35 -0
  4. package/dist/apg/accordion.bridge.mjs +326 -0
  5. package/dist/apg/accordion.model.json +8397 -0
  6. package/dist/apg/alert.bridge.d.mts +22 -0
  7. package/dist/apg/alert.bridge.mjs +86 -0
  8. package/dist/apg/alert.model.json +871 -0
  9. package/dist/apg/alertdialog.bridge.d.mts +21 -0
  10. package/dist/apg/alertdialog.bridge.mjs +221 -0
  11. package/dist/apg/alertdialog.model.json +5232 -0
  12. package/dist/apg/breadcrumb.bridge.d.mts +26 -0
  13. package/dist/apg/breadcrumb.bridge.mjs +120 -0
  14. package/dist/apg/breadcrumb.model.json +574 -0
  15. package/dist/apg/button.bridge.d.mts +35 -0
  16. package/dist/apg/button.bridge.mjs +208 -0
  17. package/dist/apg/button.model.json +5814 -0
  18. package/dist/apg/carousel.bridge.d.mts +44 -0
  19. package/dist/apg/carousel.bridge.mjs +262 -0
  20. package/dist/apg/carousel.model.json +8963 -0
  21. package/dist/apg/checkbox-group.bridge.d.mts +25 -0
  22. package/dist/apg/checkbox-group.bridge.mjs +276 -0
  23. package/dist/apg/checkbox-group.model.json +3863 -0
  24. package/dist/apg/checkbox.bridge.d.mts +34 -0
  25. package/dist/apg/checkbox.bridge.mjs +253 -0
  26. package/dist/apg/checkbox.model.json +3158 -0
  27. package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
  28. package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
  29. package/dist/apg/combobox-autocomplete.model.json +11586 -0
  30. package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
  31. package/dist/apg/combobox-dialog.bridge.mjs +250 -0
  32. package/dist/apg/combobox-dialog.model.json +8981 -0
  33. package/dist/apg/combobox-grid.bridge.d.mts +17 -0
  34. package/dist/apg/combobox-grid.bridge.mjs +227 -0
  35. package/dist/apg/combobox-grid.model.json +8554 -0
  36. package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
  37. package/dist/apg/combobox-select-only.bridge.mjs +304 -0
  38. package/dist/apg/combobox-select-only.model.json +7846 -0
  39. package/dist/apg/combobox-tree.bridge.d.mts +17 -0
  40. package/dist/apg/combobox-tree.bridge.mjs +211 -0
  41. package/dist/apg/combobox-tree.model.json +9345 -0
  42. package/dist/apg/dialog-modal.bridge.d.mts +35 -0
  43. package/dist/apg/dialog-modal.bridge.mjs +186 -0
  44. package/dist/apg/dialog-modal.model.json +7580 -0
  45. package/dist/apg/disclosure.bridge.d.mts +25 -0
  46. package/dist/apg/disclosure.bridge.mjs +202 -0
  47. package/dist/apg/disclosure.model.json +4703 -0
  48. package/dist/apg/feed.bridge.d.mts +31 -0
  49. package/dist/apg/feed.bridge.mjs +246 -0
  50. package/dist/apg/feed.model.json +4523 -0
  51. package/dist/apg/grid-selection.bridge.d.mts +13 -0
  52. package/dist/apg/grid-selection.bridge.mjs +160 -0
  53. package/dist/apg/grid-selection.model.json +6974 -0
  54. package/dist/apg/grid-sort.bridge.d.mts +12 -0
  55. package/dist/apg/grid-sort.bridge.mjs +138 -0
  56. package/dist/apg/grid-sort.model.json +3799 -0
  57. package/dist/apg/grid.bridge.d.mts +54 -0
  58. package/dist/apg/grid.bridge.mjs +441 -0
  59. package/dist/apg/grid.model.json +3802 -0
  60. package/dist/apg/link.bridge.d.mts +24 -0
  61. package/dist/apg/link.bridge.mjs +126 -0
  62. package/dist/apg/link.model.json +694 -0
  63. package/dist/apg/listbox-multi.bridge.d.mts +10 -0
  64. package/dist/apg/listbox-multi.bridge.mjs +261 -0
  65. package/dist/apg/listbox-multi.model.json +10893 -0
  66. package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
  67. package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
  68. package/dist/apg/listbox-rearrangeable.model.json +17592 -0
  69. package/dist/apg/listbox-shared.d.mts +58 -0
  70. package/dist/apg/listbox-shared.mjs +171 -0
  71. package/dist/apg/listbox-single.bridge.d.mts +10 -0
  72. package/dist/apg/listbox-single.bridge.mjs +196 -0
  73. package/dist/apg/listbox-single.model.json +8024 -0
  74. package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
  75. package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
  76. package/dist/apg/listbox-typeahead.model.json +5615 -0
  77. package/dist/apg/menu-button.bridge.d.mts +29 -0
  78. package/dist/apg/menu-button.bridge.mjs +259 -0
  79. package/dist/apg/menu-button.model.json +5706 -0
  80. package/dist/apg/menu-checkable.bridge.d.mts +25 -0
  81. package/dist/apg/menu-checkable.bridge.mjs +226 -0
  82. package/dist/apg/menu-checkable.model.json +4277 -0
  83. package/dist/apg/menu-menubar.bridge.d.mts +27 -0
  84. package/dist/apg/menu-menubar.bridge.mjs +273 -0
  85. package/dist/apg/menu-menubar.model.json +9540 -0
  86. package/dist/apg/meter.bridge.d.mts +28 -0
  87. package/dist/apg/meter.bridge.mjs +159 -0
  88. package/dist/apg/meter.model.json +1185 -0
  89. package/dist/apg/player.bridge.d.mts +24 -0
  90. package/dist/apg/player.bridge.mjs +342 -0
  91. package/dist/apg/player.model.json +11013 -0
  92. package/dist/apg/radio.bridge.d.mts +35 -0
  93. package/dist/apg/radio.bridge.mjs +371 -0
  94. package/dist/apg/radio.model.json +5011 -0
  95. package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
  96. package/dist/apg/slider-multithumb.bridge.mjs +365 -0
  97. package/dist/apg/slider-multithumb.model.json +5774 -0
  98. package/dist/apg/slider.bridge.d.mts +54 -0
  99. package/dist/apg/slider.bridge.mjs +359 -0
  100. package/dist/apg/slider.model.json +4881 -0
  101. package/dist/apg/spinbutton.bridge.d.mts +41 -0
  102. package/dist/apg/spinbutton.bridge.mjs +327 -0
  103. package/dist/apg/spinbutton.model.json +7571 -0
  104. package/dist/apg/switch.bridge.d.mts +30 -0
  105. package/dist/apg/switch.bridge.mjs +243 -0
  106. package/dist/apg/switch.model.json +3159 -0
  107. package/dist/apg/tabs.bridge.d.mts +41 -0
  108. package/dist/apg/tabs.bridge.mjs +294 -0
  109. package/dist/apg/tabs.model.json +7659 -0
  110. package/dist/apg/timeline.bridge.d.mts +36 -0
  111. package/dist/apg/timeline.bridge.mjs +265 -0
  112. package/dist/apg/timeline.model.json +6189 -0
  113. package/dist/apg/toolbar.bridge.d.mts +25 -0
  114. package/dist/apg/toolbar.bridge.mjs +307 -0
  115. package/dist/apg/toolbar.model.json +6616 -0
  116. package/dist/apg/tooltip.bridge.d.mts +36 -0
  117. package/dist/apg/tooltip.bridge.mjs +208 -0
  118. package/dist/apg/tooltip.model.json +8295 -0
  119. package/dist/apg/treegrid.bridge.d.mts +28 -0
  120. package/dist/apg/treegrid.bridge.mjs +311 -0
  121. package/dist/apg/treegrid.model.json +17358 -0
  122. package/dist/apg/treeview.bridge.d.mts +26 -0
  123. package/dist/apg/treeview.bridge.mjs +396 -0
  124. package/dist/apg/treeview.model.json +13745 -0
  125. package/dist/apg/windowsplitter.bridge.d.mts +36 -0
  126. package/dist/apg/windowsplitter.bridge.mjs +242 -0
  127. package/dist/apg/windowsplitter.model.json +6253 -0
  128. package/dist/canvas/camera.model.json +6568 -0
  129. package/dist/canvas/canvas.bridge.d.mts +190 -0
  130. package/dist/canvas/canvas.bridge.mjs +729 -0
  131. package/dist/canvas/pointer.model.json +17095 -0
  132. package/dist/scenario.d.mts +233 -0
  133. package/dist/scenario.mjs +313 -0
  134. package/package.json +57 -4
  135. package/scenarios/apg/README.md +221 -0
  136. package/scenarios/apg/accordion.base-ui.json +1005 -0
  137. package/scenarios/apg/accordion.react-aria.json +929 -0
  138. package/scenarios/apg/alert.base-ui.json +81 -0
  139. package/scenarios/apg/alert.react-aria.json +98 -0
  140. package/scenarios/apg/alertdialog.base-ui.json +331 -0
  141. package/scenarios/apg/alertdialog.react-aria.json +225 -0
  142. package/scenarios/apg/breadcrumb.react-aria.json +312 -0
  143. package/scenarios/apg/button.ariakit.json +136 -0
  144. package/scenarios/apg/button.base-ui.json +747 -0
  145. package/scenarios/apg/button.react-aria.json +443 -0
  146. package/scenarios/apg/carousel.apg.json +909 -0
  147. package/scenarios/apg/checkbox.ariakit.json +299 -0
  148. package/scenarios/apg/checkbox.base-ui.json +1126 -0
  149. package/scenarios/apg/checkbox.react-aria.json +518 -0
  150. package/scenarios/apg/combobox.ariakit.json +945 -0
  151. package/scenarios/apg/combobox.base-ui.json +2444 -0
  152. package/scenarios/apg/combobox.react-aria.json +845 -0
  153. package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
  154. package/scenarios/apg/dialog-modal.base-ui.json +775 -0
  155. package/scenarios/apg/dialog-modal.react-aria.json +638 -0
  156. package/scenarios/apg/disclosure.apg.json +349 -0
  157. package/scenarios/apg/disclosure.ariakit.json +163 -0
  158. package/scenarios/apg/disclosure.base-ui.json +451 -0
  159. package/scenarios/apg/disclosure.react-aria.json +643 -0
  160. package/scenarios/apg/feed.apg.json +442 -0
  161. package/scenarios/apg/grid.ariakit.json +197 -0
  162. package/scenarios/apg/grid.base-ui.json +720 -0
  163. package/scenarios/apg/grid.react-aria.json +969 -0
  164. package/scenarios/apg/landmarks.ariakit.json +93 -0
  165. package/scenarios/apg/landmarks.react-aria.json +330 -0
  166. package/scenarios/apg/link.ariakit.json +83 -0
  167. package/scenarios/apg/link.base-ui.json +88 -0
  168. package/scenarios/apg/link.react-aria.json +186 -0
  169. package/scenarios/apg/listbox.ariakit.json +78 -0
  170. package/scenarios/apg/listbox.base-ui.json +163 -0
  171. package/scenarios/apg/listbox.react-aria.json +1118 -0
  172. package/scenarios/apg/menu-button.ariakit.json +924 -0
  173. package/scenarios/apg/menu-button.base-ui.json +1155 -0
  174. package/scenarios/apg/menu-button.react-aria.json +1080 -0
  175. package/scenarios/apg/menubar.ariakit.json +758 -0
  176. package/scenarios/apg/menubar.base-ui.json +673 -0
  177. package/scenarios/apg/meter.base-ui.json +243 -0
  178. package/scenarios/apg/meter.react-aria.json +201 -0
  179. package/scenarios/apg/radio.ariakit.json +459 -0
  180. package/scenarios/apg/radio.base-ui.json +581 -0
  181. package/scenarios/apg/radio.react-aria.json +576 -0
  182. package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
  183. package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
  184. package/scenarios/apg/slider.base-ui.json +544 -0
  185. package/scenarios/apg/slider.react-aria.json +456 -0
  186. package/scenarios/apg/spinbutton.base-ui.json +1688 -0
  187. package/scenarios/apg/spinbutton.react-aria.json +426 -0
  188. package/scenarios/apg/switch.base-ui.json +456 -0
  189. package/scenarios/apg/switch.react-aria.json +303 -0
  190. package/scenarios/apg/table.ariakit.json +65 -0
  191. package/scenarios/apg/table.react-aria.json +221 -0
  192. package/scenarios/apg/tabs.ariakit.json +329 -0
  193. package/scenarios/apg/tabs.base-ui.json +875 -0
  194. package/scenarios/apg/tabs.react-aria.json +644 -0
  195. package/scenarios/apg/toolbar.ariakit.json +595 -0
  196. package/scenarios/apg/toolbar.base-ui.json +1189 -0
  197. package/scenarios/apg/toolbar.react-aria.json +383 -0
  198. package/scenarios/apg/tooltip.ariakit.json +441 -0
  199. package/scenarios/apg/tooltip.base-ui.json +411 -0
  200. package/scenarios/apg/tooltip.react-aria.json +454 -0
  201. package/scenarios/apg/treegrid.apg.json +2558 -0
  202. package/scenarios/apg/treegrid.react-aria.json +5094 -0
  203. package/scenarios/apg/treeview.apg.json +928 -0
  204. package/scenarios/apg/windowsplitter.apg.json +557 -0
  205. package/scenarios/canvas/README.md +69 -0
  206. package/scenarios/canvas/camera.contract.json +237 -0
  207. package/scenarios/canvas/camera.excalidraw.json +57 -0
  208. package/scenarios/canvas/marquee.excalidraw.json +122 -0
  209. package/scenarios/canvas/pan.excalidraw.json +46 -0
  210. package/scenarios/canvas/pointer.contract.json +528 -0
  211. package/scenarios/parts.json +291 -0
  212. package/shared/aria.qnt +186 -0
  213. package/shared/details-group.qnt +50 -0
  214. package/shared/focusgroup.qnt +152 -0
  215. package/shared/invokers.qnt +81 -0
  216. package/shared/popover.qnt +71 -0
  217. package/specs/apg/STATIC.md +103 -0
  218. package/specs/apg/accordion.qnt +578 -0
  219. package/specs/apg/alert.qnt +131 -0
  220. package/specs/apg/alertdialog.qnt +455 -0
  221. package/specs/apg/breadcrumb.qnt +88 -0
  222. package/specs/apg/button.qnt +445 -0
  223. package/specs/apg/carousel.qnt +732 -0
  224. package/specs/apg/checkbox-group.qnt +388 -0
  225. package/specs/apg/checkbox.qnt +394 -0
  226. package/specs/apg/combobox-autocomplete.qnt +764 -0
  227. package/specs/apg/combobox-dialog.qnt +627 -0
  228. package/specs/apg/combobox-grid.qnt +632 -0
  229. package/specs/apg/combobox-select-only.qnt +626 -0
  230. package/specs/apg/combobox-tree.qnt +633 -0
  231. package/specs/apg/dialog-modal.qnt +499 -0
  232. package/specs/apg/disclosure.qnt +309 -0
  233. package/specs/apg/feed.qnt +485 -0
  234. package/specs/apg/grid-selection.qnt +512 -0
  235. package/specs/apg/grid-sort.qnt +330 -0
  236. package/specs/apg/grid.qnt +360 -0
  237. package/specs/apg/link.qnt +119 -0
  238. package/specs/apg/listbox-multi.qnt +799 -0
  239. package/specs/apg/listbox-rearrangeable.qnt +801 -0
  240. package/specs/apg/listbox-single.qnt +511 -0
  241. package/specs/apg/listbox-typeahead.qnt +396 -0
  242. package/specs/apg/menu-button.qnt +363 -0
  243. package/specs/apg/menu-checkable.qnt +304 -0
  244. package/specs/apg/menu-menubar.qnt +531 -0
  245. package/specs/apg/meter.qnt +129 -0
  246. package/specs/apg/player.qnt +1081 -0
  247. package/specs/apg/radio.qnt +394 -0
  248. package/specs/apg/slider-multithumb.qnt +441 -0
  249. package/specs/apg/slider.qnt +396 -0
  250. package/specs/apg/spinbutton.qnt +494 -0
  251. package/specs/apg/switch.qnt +389 -0
  252. package/specs/apg/tabs.qnt +412 -0
  253. package/specs/apg/timeline.qnt +612 -0
  254. package/specs/apg/toolbar.qnt +364 -0
  255. package/specs/apg/tooltip.qnt +553 -0
  256. package/specs/apg/treegrid.qnt +983 -0
  257. package/specs/apg/treeview.qnt +1004 -0
  258. package/specs/apg/windowsplitter.qnt +487 -0
  259. package/specs/canvas/camera.qnt +407 -0
  260. package/specs/canvas/pointer.qnt +841 -0
@@ -0,0 +1,407 @@
1
+ // Infinite canvas: the camera machine (viewport transform, zoom, tween).
2
+ // Implements docs/research/canvas-prior-art.md (Decision 2, items 1-3 and 9). Bean e9jm.
3
+ //
4
+ // CONVENTIONS (the tracer / adapter must reproduce these exactly)
5
+ // - Integer fixed point, power-of-two denominators, so TS doubles and Quint ints agree
6
+ // bit-for-bit (every intermediate stays below 2^53; pan products reach ~2^43):
7
+ // zoom z = zoom * ZONE, ZONE = 4096 (2^12) [ZMIN = 1/16, ZMAX = 8]
8
+ // camera tx = (world units) * TONE, TONE = 256 (2^8) (sub-unit offsets keep zoom smooth)
9
+ // World coordinates of persisted nodes are whole integers (atproto has no floats); the camera
10
+ // and pointer-derived world points are in 1/256 units ("fp").
11
+ // - World-offset form (tldraw / Excalidraw / panzoom): screen = (world + t) * zoom.
12
+ // screenToWorld(s) = rdiv(s * TZ, z) - t worldToScreen(w) = rdiv((w + t) * z, TZ)
13
+ // with TZ = ZONE * TONE = 2^20.
14
+ // - ROUNDING RULE for every division: round half up = floor(a / b + 1/2) (rdiv); fdiv is floor
15
+ // division. Quint "/" truncates toward zero, so fdiv is built from "/" on non-negative
16
+ // operands only. In TS use Math.floor on integers (exact below 2^53) or BigInt.
17
+ // - Zoom about an anchor s: t' = t + rdiv(s * TZ, z') - rdiv(s * TZ, z), with z' = clamp(request).
18
+ // The translation is ALWAYS solved with the clamped z', i.e. a limit clamp re-anchors instead
19
+ // of drifting (tldraw preserveFocalPoint). With this rounding the anchor's world point is
20
+ // preserved EXACTLY (0 fp units); the invariant allows < 1 world unit (TONE fp units) so
21
+ // other roundings still pass.
22
+ // - WORLD BOUND: |tx|, |ty| <= WB * TONE with WB = 1e6 units: the viewport ORIGIN (top-left)
23
+ // stays inside +-1e6 (Excalidraw's own flag, the float32 spacing at 1e6). The far corner can
24
+ // exceed it by vw / ZMIN (<= 65536 units); a host that needs the whole viewport inside +-1e6
25
+ // passes a smaller WB. A move that would leave the bound is CLAMPED; the anchor guarantee is
26
+ // therefore "within 1 unit, unless the origin is pinned at the bound on that axis".
27
+ // - RESIZE ANCHOR POLICY: top-left fixed. Resizing changes only vw / vh; tx, ty, z are
28
+ // untouched (what tldraw / Excalidraw / xyflow do; no division, nothing to round). A
29
+ // centre-anchored policy (Figma) would be t' = t + rdiv((vw' - vw) * TZ, 2 * z): a different
30
+ // def, not modelled.
31
+ // - TWEENS: a request is an output with a duration the adapter may set to zero. Under reduced
32
+ // motion (rm) the camera jumps to the target at once. Otherwise TN = 4 ticks interpolate
33
+ // linearly from the start camera to the target: lerp(a, b, p) = a + fdiv((b - a) * p, TN),
34
+ // which is exactly b at p = TN and always between a and b (so every in-between camera is
35
+ // inside the zoom limits and the world bound). Easing and viewport-box interpolation are
36
+ // adapter policy. ANY gesture (pan, zoom) or cancel stops the tween where the camera is.
37
+ // At rest, camera == target == start (RestIsOnTarget); a finished tween and an instant jump
38
+ // therefore end in the same state.
39
+ //
40
+ // EVENTS (one nondet per action, so selectors are packed ints): init, resize, set_motion,
41
+ // pan_by, zoom_at, zoom_limit, request_tween, tween_tick, cancel_tween.
42
+ //
43
+ // INVARIANTS ACHIEVED: ViewportInRange, ZoomInRange (ZMIN > 0 = invertibility), OriginInBound,
44
+ // TweenEndpointsValid, ScreenWorldRoundTrip, AnchorFixed, TweenFollowsLerp, RestIsOnTarget,
45
+ // ReducedMotionNeverTweens. No NaN / non-finite state exists by construction (integers only).
46
+ //
47
+ // NOT MODELLED (adapter / host policy, per the research): wheel / pinch normalisation, device
48
+ // pixel snapping, easing curves, fit-to-bounds, inertia, rendering. No focus / keyboard model:
49
+ // "reveal the focused node" is a request_tween goal, so either focus model (real DOM focus or
50
+ // virtual focus + live region) can drive it; announcements live in pointer.qnt as outputs.
51
+ //
52
+ // GENERATOR GAPS HIT: no collections / const (constants are zero-arg defs); one nondet per
53
+ // action (packed selectors); no let in defs (long arg lists, repeated subexpressions); every
54
+ // action assigns every var (this file's frame lines were emitted by a throwaway script, the
55
+ // output is plain Quint); bool helper vals would be misclassified as invariants (helpers are
56
+ // defs); no bitwise ops; defs are not callable from the reference model, so the exact-double
57
+ // claim is checked against an independent TS implementation in test/canvas/camera.test.ts.
58
+ module canvas_camera {
59
+ var vw: int // viewport width, screen px
60
+ var vh: int // viewport height, screen px
61
+ var tx: int // camera x: world units * TONE (world-offset: screen = (world + t) * z)
62
+ var ty: int // camera y: world units * TONE
63
+ var z: int // camera zoom: zoom * ZONE
64
+ var rm: bool // reduced motion: a tween request jumps to its target at once
65
+ var active: bool // a tween is running
66
+ var tp: int // ticks done; TN = at rest
67
+ var stx: int // tween start x (== target at rest)
68
+ var sty: int // tween start y
69
+ var sz: int // tween start zoom
70
+ var ttx: int // tween target x (== camera at rest)
71
+ var tty: int // tween target y
72
+ var tz: int // tween target zoom
73
+ var anch: bool // witness: the last camera move was an anchored zoom
74
+ var asx: int // that zoom's anchor, screen px
75
+ var asy: int
76
+ var awx: int // the world point (fp) under the anchor BEFORE the zoom
77
+ var awy: int
78
+
79
+ // ---- constants ----------------------------------------------------------
80
+ def ZONE: int = 4096
81
+ def TONE: int = 256
82
+ def TZ: int = ZONE * TONE
83
+ def ZMIN: int = 256
84
+ def ZMAX: int = 32768
85
+ def WB: int = 1000000
86
+ def WT: int = WB * TONE
87
+ def TN: int = 4
88
+
89
+ // ---- integer helpers ----------------------------------------------------
90
+ def absI(a: int): int = if (a < 0) -a else a
91
+ def clampI(v: int, lo: int, hi: int): int = if (v < lo) lo else if (v > hi) hi else v
92
+ // floor division, b > 0
93
+ def fdiv(a: int, b: int): int = if (a >= 0) a / b else -((-a + b - 1) / b)
94
+ // round half up, b > 0
95
+ def rdiv(a: int, b: int): int = fdiv(2 * a + b, 2 * b)
96
+
97
+ // ---- the transform ------------------------------------------------------
98
+ def screenToWorld(s: int, t: int, zf: int): int = rdiv(s * TZ, zf) - t
99
+ def worldToScreen(w: int, t: int, zf: int): int = rdiv((w + t) * zf, TZ)
100
+ def clampZ(zf: int): int = clampI(zf, ZMIN, ZMAX)
101
+ def clampT(t: int): int = clampI(t, -WT, WT)
102
+ // zoom to zf2 about screen point s; the translation is solved for the CLAMPED zoom
103
+ def anchoredT(t: int, zf: int, zf2: int, s: int): int =
104
+ clampT(t + rdiv(s * TZ, zf2) - rdiv(s * TZ, zf))
105
+ def panT(t: int, zf: int, d: int): int = clampT(t + rdiv(d * TZ, zf))
106
+ def zoomRequest(zf: int, f: int): int = clampZ(rdiv(zf * f, ZONE))
107
+ def lerp(a: int, b: int, p: int): int = a + fdiv((b - a) * p, TN)
108
+
109
+ // ---- packed selectors ---------------------------------------------------
110
+ def VW(i: int): int =
111
+ if (i == 0) 320 else if (i == 1) 800 else if (i == 2) 1024 else if (i == 3) 1920 else 4096
112
+ def VH(i: int): int =
113
+ if (i == 0) 240 else if (i == 1) 600 else if (i == 2) 768 else if (i == 3) 1080 else 2160
114
+ // 0 top-left, 1 centre, 2 bottom-right, 3 (1/4, 3/4)
115
+ def anchorX(a: int, w: int): int = if (a == 0) 0 else if (a == 1) w / 2 else if (a == 2) w else w / 4
116
+ def anchorY(a: int, h: int): int = if (a == 0) 0 else if (a == 1) h / 2 else if (a == 2) h else 3 * h / 4
117
+ // x0.5 x0.75 x1.25 x1.5 x2 x16 x1/16 (ZONE = x1)
118
+ def zoomFactor(k: int): int =
119
+ if (k == 0) 2048 else if (k == 1) 3072 else if (k == 2) 5120 else if (k == 3) 6144
120
+ else if (k == 4) 8192 else if (k == 5) 65536 else 256
121
+ // screen deltas in px: far beyond the world bound on purpose
122
+ def panDelta(k: int): int =
123
+ if (k == 0) -9000000 else if (k == 1) -400 else if (k == 2) 0 else if (k == 3) 5 else 9000000
124
+ // world points (units) a tween can centre on; the last is near the bound
125
+ def pointX(i: int): int = if (i == 0) 0 else if (i == 1) 1000 else if (i == 2) -250000 else 999000
126
+ def pointY(i: int): int = if (i == 0) 0 else if (i == 1) 500 else if (i == 2) 400000 else -999000
127
+ // tween goals: kind = goal / 4 (0 ZMIN, 1 ZMAX, 2 100%, 3 centre on a point)
128
+ def tgtZ(g: int, zf: int): int =
129
+ if (g / 4 == 0) ZMIN else if (g / 4 == 1) ZMAX else if (g / 4 == 2) ZONE else zf
130
+ def tgtX(g: int, t: int, zf: int, w: int): int =
131
+ if (g / 4 == 3) clampT(rdiv((w / 2) * TZ, zf) - pointX(g % 4) * TONE)
132
+ else anchoredT(t, zf, tgtZ(g, zf), anchorX(g % 4, w))
133
+ def tgtY(g: int, t: int, zf: int, h: int): int =
134
+ if (g / 4 == 3) clampT(rdiv((h / 2) * TZ, zf) - pointY(g % 4) * TONE)
135
+ else anchoredT(t, zf, tgtZ(g, zf), anchorY(g % 4, h))
136
+
137
+ // ---- predicates used by the invariants ----------------------------------
138
+ def zoomOk(zf: int): bool = and(ZMIN > 0, zf >= ZMIN, zf <= ZMAX)
139
+ def tOk(t: int): bool = absI(t) <= WT
140
+ def axisAnchored(s: int, w0: int, t: int, zf: int): bool =
141
+ or(absI(t) == WT, and(zf > 0, absI(screenToWorld(s, t, zf) - w0) < TONE))
142
+
143
+ action init = all {
144
+ vw' = 800,
145
+ vh' = 600,
146
+ tx' = 0,
147
+ ty' = 0,
148
+ z' = 4096,
149
+ rm' = false,
150
+ active' = false,
151
+ tp' = 4,
152
+ stx' = 0,
153
+ sty' = 0,
154
+ sz' = 4096,
155
+ ttx' = 0,
156
+ tty' = 0,
157
+ tz' = 4096,
158
+ anch' = false,
159
+ asx' = 0,
160
+ asy' = 0,
161
+ awx' = 0,
162
+ awy' = 0
163
+ }
164
+
165
+ // host resize: the TOP-LEFT stays fixed (tx, ty, z untouched); `vp` picks one of 5 viewports
166
+ action resize = {
167
+ nondet vp = Set(0, 1, 2, 3, 4).oneOf()
168
+ all {
169
+ vw' = VW(vp),
170
+ vh' = VH(vp),
171
+ tx' = tx,
172
+ ty' = ty,
173
+ z' = z,
174
+ rm' = rm,
175
+ active' = active,
176
+ tp' = tp,
177
+ stx' = stx,
178
+ sty' = sty,
179
+ sz' = sz,
180
+ ttx' = ttx,
181
+ tty' = tty,
182
+ tz' = tz,
183
+ anch' = anch,
184
+ asx' = asx,
185
+ asy' = asy,
186
+ awx' = awx,
187
+ awy' = awy
188
+ }
189
+ }
190
+
191
+ // reduced-motion preference; turning it on settles a running tween on its target
192
+ action set_motion = {
193
+ nondet reduced = Set(true, false).oneOf()
194
+ all {
195
+ vw' = vw,
196
+ vh' = vh,
197
+ tx' = if (and(reduced, active)) ttx else tx,
198
+ ty' = if (and(reduced, active)) tty else ty,
199
+ z' = if (and(reduced, active)) tz else z,
200
+ rm' = reduced,
201
+ active' = if (reduced) false else active,
202
+ tp' = if (and(reduced, active)) TN else tp,
203
+ stx' = if (and(reduced, active)) ttx else stx,
204
+ sty' = if (and(reduced, active)) tty else sty,
205
+ sz' = if (and(reduced, active)) tz else sz,
206
+ ttx' = ttx,
207
+ tty' = tty,
208
+ tz' = tz,
209
+ anch' = if (and(reduced, active)) false else anch,
210
+ asx' = asx,
211
+ asy' = asy,
212
+ awx' = awx,
213
+ awy' = awy
214
+ }
215
+ }
216
+
217
+ // pan by a screen delta (5 x 5 pool: dx = delta % 5, dy = delta / 5); a gesture cancels any tween
218
+ action pan_by = {
219
+ nondet delta = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24).oneOf()
220
+ all {
221
+ vw' = vw,
222
+ vh' = vh,
223
+ tx' = panT(tx, z, panDelta(delta % 5)),
224
+ ty' = panT(ty, z, panDelta(delta / 5)),
225
+ z' = z,
226
+ rm' = rm,
227
+ active' = false,
228
+ tp' = TN,
229
+ stx' = panT(tx, z, panDelta(delta % 5)),
230
+ sty' = panT(ty, z, panDelta(delta / 5)),
231
+ sz' = z,
232
+ ttx' = panT(tx, z, panDelta(delta % 5)),
233
+ tty' = panT(ty, z, panDelta(delta / 5)),
234
+ tz' = z,
235
+ anch' = false,
236
+ asx' = asx,
237
+ asy' = asy,
238
+ awx' = awx,
239
+ awy' = awy
240
+ }
241
+ }
242
+
243
+ // zoom by a factor about an anchor: gesture = anchor (% 4) + 4 * factor (7 factors)
244
+ action zoom_at = {
245
+ nondet gesture = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27).oneOf()
246
+ all {
247
+ vw' = vw,
248
+ vh' = vh,
249
+ tx' = anchoredT(tx, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorX(gesture % 4, vw)),
250
+ ty' = anchoredT(ty, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorY(gesture % 4, vh)),
251
+ z' = zoomRequest(z, zoomFactor(gesture / 4)),
252
+ rm' = rm,
253
+ active' = false,
254
+ tp' = TN,
255
+ stx' = anchoredT(tx, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorX(gesture % 4, vw)),
256
+ sty' = anchoredT(ty, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorY(gesture % 4, vh)),
257
+ sz' = zoomRequest(z, zoomFactor(gesture / 4)),
258
+ ttx' = anchoredT(tx, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorX(gesture % 4, vw)),
259
+ tty' = anchoredT(ty, z, zoomRequest(z, zoomFactor(gesture / 4)), anchorY(gesture % 4, vh)),
260
+ tz' = zoomRequest(z, zoomFactor(gesture / 4)),
261
+ anch' = true,
262
+ asx' = anchorX(gesture % 4, vw),
263
+ asy' = anchorY(gesture % 4, vh),
264
+ awx' = screenToWorld(anchorX(gesture % 4, vw), tx, z),
265
+ awy' = screenToWorld(anchorY(gesture % 4, vh), ty, z)
266
+ }
267
+ }
268
+
269
+ // zoom straight to a limit about an anchor: limit = anchor (% 4) + 4 * (0 = ZMIN, 1 = ZMAX)
270
+ action zoom_limit = {
271
+ nondet limit = Set(0, 1, 2, 3, 4, 5, 6, 7).oneOf()
272
+ all {
273
+ vw' = vw,
274
+ vh' = vh,
275
+ tx' = anchoredT(tx, z, if (limit < 4) ZMIN else ZMAX, anchorX(limit % 4, vw)),
276
+ ty' = anchoredT(ty, z, if (limit < 4) ZMIN else ZMAX, anchorY(limit % 4, vh)),
277
+ z' = if (limit < 4) ZMIN else ZMAX,
278
+ rm' = rm,
279
+ active' = false,
280
+ tp' = TN,
281
+ stx' = anchoredT(tx, z, if (limit < 4) ZMIN else ZMAX, anchorX(limit % 4, vw)),
282
+ sty' = anchoredT(ty, z, if (limit < 4) ZMIN else ZMAX, anchorY(limit % 4, vh)),
283
+ sz' = if (limit < 4) ZMIN else ZMAX,
284
+ ttx' = anchoredT(tx, z, if (limit < 4) ZMIN else ZMAX, anchorX(limit % 4, vw)),
285
+ tty' = anchoredT(ty, z, if (limit < 4) ZMIN else ZMAX, anchorY(limit % 4, vh)),
286
+ tz' = if (limit < 4) ZMIN else ZMAX,
287
+ anch' = true,
288
+ asx' = anchorX(limit % 4, vw),
289
+ asy' = anchorY(limit % 4, vh),
290
+ awx' = screenToWorld(anchorX(limit % 4, vw), tx, z),
291
+ awy' = screenToWorld(anchorY(limit % 4, vh), ty, z)
292
+ }
293
+ }
294
+
295
+ // request a camera move to `goal` (0..11: zoom to ZMIN / ZMAX / 100% about anchor goal % 4;
296
+ // 12..15: centre world point goal - 12 at the current zoom). Under reduced motion the camera
297
+ // jumps to the target at once (the same state a finished tween reaches); otherwise a tween starts.
298
+ action request_tween = {
299
+ nondet goal = Set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).oneOf()
300
+ all {
301
+ vw' = vw,
302
+ vh' = vh,
303
+ tx' = if (rm) tgtX(goal, tx, z, vw) else tx,
304
+ ty' = if (rm) tgtY(goal, ty, z, vh) else ty,
305
+ z' = if (rm) tgtZ(goal, z) else z,
306
+ rm' = rm,
307
+ active' = not(rm),
308
+ tp' = if (rm) TN else 0,
309
+ stx' = if (rm) tgtX(goal, tx, z, vw) else tx,
310
+ sty' = if (rm) tgtY(goal, ty, z, vh) else ty,
311
+ sz' = if (rm) tgtZ(goal, z) else z,
312
+ ttx' = tgtX(goal, tx, z, vw),
313
+ tty' = tgtY(goal, ty, z, vh),
314
+ tz' = tgtZ(goal, z),
315
+ anch' = false,
316
+ asx' = asx,
317
+ asy' = asy,
318
+ awx' = awx,
319
+ awy' = awy
320
+ }
321
+ }
322
+
323
+ // one animation frame: advance the tween; the last tick lands exactly on the target
324
+ action tween_tick = all {
325
+ vw' = vw,
326
+ vh' = vh,
327
+ tx' = if (active) lerp(stx, ttx, tp + 1) else tx,
328
+ ty' = if (active) lerp(sty, tty, tp + 1) else ty,
329
+ z' = if (active) lerp(sz, tz, tp + 1) else z,
330
+ rm' = rm,
331
+ active' = and(active, tp + 1 < TN),
332
+ tp' = if (active) tp + 1 else tp,
333
+ stx' = if (and(active, tp + 1 >= TN)) ttx else stx,
334
+ sty' = if (and(active, tp + 1 >= TN)) tty else sty,
335
+ sz' = if (and(active, tp + 1 >= TN)) tz else sz,
336
+ ttx' = ttx,
337
+ tty' = tty,
338
+ tz' = tz,
339
+ anch' = and(anch, not(active)),
340
+ asx' = asx,
341
+ asy' = asy,
342
+ awx' = awx,
343
+ awy' = awy
344
+ }
345
+
346
+ // stop where the camera is: the target becomes the camera (rest)
347
+ action cancel_tween = all {
348
+ vw' = vw,
349
+ vh' = vh,
350
+ tx' = tx,
351
+ ty' = ty,
352
+ z' = z,
353
+ rm' = rm,
354
+ active' = false,
355
+ tp' = TN,
356
+ stx' = tx,
357
+ sty' = ty,
358
+ sz' = z,
359
+ ttx' = tx,
360
+ tty' = ty,
361
+ tz' = z,
362
+ anch' = anch,
363
+ asx' = asx,
364
+ asy' = asy,
365
+ awx' = awx,
366
+ awy' = awy
367
+ }
368
+
369
+ action step = any { init, resize, set_motion, pan_by, zoom_at, zoom_limit, request_tween, tween_tick, cancel_tween }
370
+
371
+ // viewport sizes come from the pool
372
+ val ViewportInRange: bool = and(vw >= 320, vw <= 4096, vh >= 240, vh <= 2160)
373
+ // zoom stays in [ZMIN, ZMAX] and ZMIN > 0: screenToWorld is invertible
374
+ val ZoomInRange: bool = zoomOk(z)
375
+ // the viewport origin never leaves the hard world bound
376
+ val OriginInBound: bool = and(tOk(tx), tOk(ty))
377
+ // the tween's start and target are themselves valid cameras
378
+ val TweenEndpointsValid: bool =
379
+ and(zoomOk(sz), zoomOk(tz), tOk(stx), tOk(sty), tOk(ttx), tOk(tty))
380
+ // screen -> world -> screen is the identity on whole pixels (zoom <= 128 keeps it exact)
381
+ val ScreenWorldRoundTrip: bool = and(
382
+ z > 0,
383
+ worldToScreen(screenToWorld(0, tx, z), tx, z) == 0,
384
+ worldToScreen(screenToWorld(vw, tx, z), tx, z) == vw,
385
+ worldToScreen(screenToWorld(vw / 2, tx, z), tx, z) == vw / 2,
386
+ worldToScreen(screenToWorld(0, ty, z), ty, z) == 0,
387
+ worldToScreen(screenToWorld(vh, ty, z), ty, z) == vh,
388
+ worldToScreen(screenToWorld(vh / 3, ty, z), ty, z) == vh / 3
389
+ )
390
+ // zoom about an anchor keeps the anchor's world point within one unit (a limit clamp
391
+ // re-anchors; only a pinned world bound may move it)
392
+ val AnchorFixed: bool =
393
+ or(not(anch), and(axisAnchored(asx, awx, tx, z), axisAnchored(asy, awy, ty, z)))
394
+ // while a tween runs the camera is exactly the interpolation of start -> target at tick tp
395
+ val TweenFollowsLerp: bool =
396
+ or(not(active), and(
397
+ tp >= 0, tp < TN,
398
+ tx == lerp(stx, ttx, tp), ty == lerp(sty, tty, tp), z == lerp(sz, tz, tp)
399
+ ))
400
+ // at rest the camera IS the target: tweened and instant moves end in the same state
401
+ val RestIsOnTarget: bool =
402
+ or(active, and(
403
+ tp == TN, tx == ttx, ty == tty, z == tz, stx == ttx, sty == tty, sz == tz
404
+ ))
405
+ // reduced motion never leaves a tween running
406
+ val ReducedMotionNeverTweens: bool = or(not(rm), not(active))
407
+ }