@cssints/apg-specs 0.0.0-stage → 0.5.1
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/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- 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
|
+
}
|