@unrulysystems/native-motion-core 0.1.0-alpha.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/clock.cjs +71 -0
- package/dist/clock.d.cts +23 -0
- package/dist/clock.d.ts +23 -0
- package/dist/clock.js +66 -0
- package/dist/collect-reads.cjs +68 -0
- package/dist/collect-reads.d.cts +23 -0
- package/dist/collect-reads.d.ts +23 -0
- package/dist/collect-reads.js +62 -0
- package/dist/component/boundedArray.cjs +74 -0
- package/dist/component/boundedArray.d.cts +25 -0
- package/dist/component/boundedArray.d.ts +25 -0
- package/dist/component/boundedArray.js +68 -0
- package/dist/component/index.cjs +57 -0
- package/dist/component/index.d.cts +12 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/index.js +22 -0
- package/dist/component/orchestration.cjs +129 -0
- package/dist/component/orchestration.d.cts +66 -0
- package/dist/component/orchestration.d.ts +66 -0
- package/dist/component/orchestration.js +124 -0
- package/dist/component/resolve.cjs +104 -0
- package/dist/component/resolve.d.cts +11 -0
- package/dist/component/resolve.d.ts +11 -0
- package/dist/component/resolve.js +97 -0
- package/dist/component/transition.cjs +352 -0
- package/dist/component/transition.d.cts +33 -0
- package/dist/component/transition.d.ts +33 -0
- package/dist/component/transition.js +339 -0
- package/dist/component/types.cjs +114 -0
- package/dist/component/types.d.cts +60 -0
- package/dist/component/types.d.ts +60 -0
- package/dist/component/types.js +111 -0
- package/dist/component/validate.cjs +1015 -0
- package/dist/component/validate.d.cts +37 -0
- package/dist/component/validate.d.ts +37 -0
- package/dist/component/validate.js +1002 -0
- package/dist/component/variants.cjs +333 -0
- package/dist/component/variants.d.cts +106 -0
- package/dist/component/variants.d.ts +106 -0
- package/dist/component/variants.js +321 -0
- package/dist/config/constants.cjs +41 -0
- package/dist/config/constants.d.cts +29 -0
- package/dist/config/constants.d.ts +29 -0
- package/dist/config/constants.js +38 -0
- package/dist/delay.cjs +72 -0
- package/dist/delay.d.cts +26 -0
- package/dist/delay.d.ts +26 -0
- package/dist/delay.js +70 -0
- package/dist/derived.cjs +143 -0
- package/dist/derived.d.cts +41 -0
- package/dist/derived.d.ts +41 -0
- package/dist/derived.js +139 -0
- package/dist/driver/index.cjs +22 -0
- package/dist/driver/index.d.cts +7 -0
- package/dist/driver/index.d.ts +7 -0
- package/dist/driver/index.js +14 -0
- package/dist/driver/keyframeTiming.cjs +130 -0
- package/dist/driver/keyframeTiming.d.cts +18 -0
- package/dist/driver/keyframeTiming.d.ts +18 -0
- package/dist/driver/keyframeTiming.js +124 -0
- package/dist/driver/keyframeTimingConfig.cjs +24 -0
- package/dist/driver/keyframeTimingConfig.d.cts +5 -0
- package/dist/driver/keyframeTimingConfig.d.ts +5 -0
- package/dist/driver/keyframeTimingConfig.js +22 -0
- package/dist/driver/prepare.cjs +459 -0
- package/dist/driver/prepare.d.cts +4 -0
- package/dist/driver/prepare.d.ts +4 -0
- package/dist/driver/prepare.js +454 -0
- package/dist/driver/reference.cjs +762 -0
- package/dist/driver/reference.d.cts +53 -0
- package/dist/driver/reference.d.ts +53 -0
- package/dist/driver/reference.js +757 -0
- package/dist/driver/step.cjs +55 -0
- package/dist/driver/step.d.cts +9 -0
- package/dist/driver/step.d.ts +9 -0
- package/dist/driver/step.js +52 -0
- package/dist/driver/tiers.cjs +40 -0
- package/dist/driver/tiers.d.cts +2 -0
- package/dist/driver/tiers.d.ts +2 -0
- package/dist/driver/tiers.js +37 -0
- package/dist/driver/types.cjs +9 -0
- package/dist/driver/types.d.cts +68 -0
- package/dist/driver/types.d.ts +68 -0
- package/dist/driver/types.js +8 -0
- package/dist/external-animation-ledger.cjs +1250 -0
- package/dist/external-animation-ledger.d.cts +416 -0
- package/dist/external-animation-ledger.d.ts +416 -0
- package/dist/external-animation-ledger.js +1243 -0
- package/dist/gesture/directionLock.cjs +25 -0
- package/dist/gesture/directionLock.d.cts +8 -0
- package/dist/gesture/directionLock.d.ts +8 -0
- package/dist/gesture/directionLock.js +21 -0
- package/dist/gesture/dragConfig.cjs +634 -0
- package/dist/gesture/dragConfig.d.cts +298 -0
- package/dist/gesture/dragConfig.d.ts +298 -0
- package/dist/gesture/dragConfig.js +624 -0
- package/dist/gesture/elastic.cjs +44 -0
- package/dist/gesture/elastic.d.cts +4 -0
- package/dist/gesture/elastic.d.ts +4 -0
- package/dist/gesture/elastic.js +39 -0
- package/dist/gesture/handoffSession.cjs +161 -0
- package/dist/gesture/handoffSession.d.cts +35 -0
- package/dist/gesture/handoffSession.d.ts +35 -0
- package/dist/gesture/handoffSession.js +158 -0
- package/dist/gesture/index.cjs +37 -0
- package/dist/gesture/index.d.cts +12 -0
- package/dist/gesture/index.d.ts +12 -0
- package/dist/gesture/index.js +11 -0
- package/dist/gesture/projection.cjs +95 -0
- package/dist/gesture/projection.d.cts +7 -0
- package/dist/gesture/projection.d.ts +7 -0
- package/dist/gesture/projection.js +87 -0
- package/dist/gesture/session.cjs +162 -0
- package/dist/gesture/session.d.cts +29 -0
- package/dist/gesture/session.d.ts +29 -0
- package/dist/gesture/session.js +158 -0
- package/dist/gesture/types.cjs +5 -0
- package/dist/gesture/types.d.cts +13 -0
- package/dist/gesture/types.d.ts +13 -0
- package/dist/gesture/types.js +4 -0
- package/dist/gesture/viewportConstraints.cjs +38 -0
- package/dist/gesture/viewportConstraints.d.cts +6 -0
- package/dist/gesture/viewportConstraints.d.ts +6 -0
- package/dist/gesture/viewportConstraints.js +34 -0
- package/dist/graph.cjs +226 -0
- package/dist/graph.d.cts +2 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +223 -0
- package/dist/index.cjs +140 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +70 -0
- package/dist/inertia.cjs +214 -0
- package/dist/inertia.d.cts +53 -0
- package/dist/inertia.d.ts +53 -0
- package/dist/inertia.js +212 -0
- package/dist/instant.cjs +66 -0
- package/dist/instant.d.cts +16 -0
- package/dist/instant.d.ts +16 -0
- package/dist/instant.js +63 -0
- package/dist/internal-driver.cjs +81 -0
- package/dist/internal-driver.d.cts +19 -0
- package/dist/internal-driver.d.ts +19 -0
- package/dist/internal-driver.js +35 -0
- package/dist/keyframes.cjs +191 -0
- package/dist/keyframes.d.cts +13 -0
- package/dist/keyframes.d.ts +13 -0
- package/dist/keyframes.js +188 -0
- package/dist/layout/commitDetector.cjs +67 -0
- package/dist/layout/commitDetector.d.cts +21 -0
- package/dist/layout/commitDetector.d.ts +21 -0
- package/dist/layout/commitDetector.js +64 -0
- package/dist/layout/compose.cjs +67 -0
- package/dist/layout/compose.d.cts +30 -0
- package/dist/layout/compose.d.ts +30 -0
- package/dist/layout/compose.js +65 -0
- package/dist/layout/constants.cjs +13 -0
- package/dist/layout/constants.d.cts +6 -0
- package/dist/layout/constants.d.ts +6 -0
- package/dist/layout/constants.js +10 -0
- package/dist/layout/identity.cjs +700 -0
- package/dist/layout/identity.d.cts +67 -0
- package/dist/layout/identity.d.ts +67 -0
- package/dist/layout/identity.js +698 -0
- package/dist/layout/index.cjs +36 -0
- package/dist/layout/index.d.cts +18 -0
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.js +13 -0
- package/dist/layout/measure.cjs +81 -0
- package/dist/layout/measure.d.cts +43 -0
- package/dist/layout/measure.d.ts +43 -0
- package/dist/layout/measure.js +78 -0
- package/dist/layout/projection.cjs +99 -0
- package/dist/layout/projection.d.cts +23 -0
- package/dist/layout/projection.d.ts +23 -0
- package/dist/layout/projection.js +97 -0
- package/dist/layout/scroll.cjs +27 -0
- package/dist/layout/scroll.d.cts +13 -0
- package/dist/layout/scroll.d.ts +13 -0
- package/dist/layout/scroll.js +24 -0
- package/dist/layout/session.cjs +207 -0
- package/dist/layout/session.d.cts +73 -0
- package/dist/layout/session.d.ts +73 -0
- package/dist/layout/session.js +205 -0
- package/dist/layout/tree.cjs +826 -0
- package/dist/layout/tree.d.cts +70 -0
- package/dist/layout/tree.d.ts +70 -0
- package/dist/layout/tree.js +823 -0
- package/dist/layout/types.cjs +36 -0
- package/dist/layout/types.d.cts +31 -0
- package/dist/layout/types.d.ts +31 -0
- package/dist/layout/types.js +35 -0
- package/dist/motion-arc.cjs +184 -0
- package/dist/motion-arc.d.cts +78 -0
- package/dist/motion-arc.d.ts +78 -0
- package/dist/motion-arc.js +183 -0
- package/dist/motion-mix.cjs +205 -0
- package/dist/motion-mix.d.cts +3 -0
- package/dist/motion-mix.d.ts +3 -0
- package/dist/motion-mix.js +202 -0
- package/dist/motion-value-driver-port.cjs +636 -0
- package/dist/motion-value-driver-port.d.cts +406 -0
- package/dist/motion-value-driver-port.d.ts +406 -0
- package/dist/motion-value-driver-port.js +627 -0
- package/dist/motion-value.cjs +189 -0
- package/dist/motion-value.d.cts +51 -0
- package/dist/motion-value.d.ts +51 -0
- package/dist/motion-value.js +185 -0
- package/dist/presence/controller.cjs +657 -0
- package/dist/presence/controller.d.cts +6 -0
- package/dist/presence/controller.d.ts +6 -0
- package/dist/presence/controller.js +652 -0
- package/dist/presence/index.cjs +19 -0
- package/dist/presence/index.d.cts +4 -0
- package/dist/presence/index.d.ts +4 -0
- package/dist/presence/index.js +12 -0
- package/dist/presence/machine.cjs +50 -0
- package/dist/presence/machine.d.cts +10 -0
- package/dist/presence/machine.d.ts +10 -0
- package/dist/presence/machine.js +46 -0
- package/dist/presence/types.cjs +6 -0
- package/dist/presence/types.d.cts +32 -0
- package/dist/presence/types.d.ts +32 -0
- package/dist/presence/types.js +5 -0
- package/dist/repeat.cjs +311 -0
- package/dist/repeat.d.cts +174 -0
- package/dist/repeat.d.ts +174 -0
- package/dist/repeat.js +300 -0
- package/dist/spring.cjs +128 -0
- package/dist/spring.d.cts +18 -0
- package/dist/spring.d.ts +18 -0
- package/dist/spring.js +125 -0
- package/dist/subscriptions.cjs +74 -0
- package/dist/subscriptions.d.cts +19 -0
- package/dist/subscriptions.d.ts +19 -0
- package/dist/subscriptions.js +69 -0
- package/dist/subset/index.cjs +23 -0
- package/dist/subset/index.d.cts +6 -0
- package/dist/subset/index.d.ts +6 -0
- package/dist/subset/index.js +13 -0
- package/dist/subset/normalize.cjs +90 -0
- package/dist/subset/normalize.d.cts +10 -0
- package/dist/subset/normalize.d.ts +10 -0
- package/dist/subset/normalize.js +85 -0
- package/dist/subset/registry.cjs +259 -0
- package/dist/subset/registry.d.cts +25 -0
- package/dist/subset/registry.d.ts +25 -0
- package/dist/subset/registry.js +256 -0
- package/dist/subset/resolve.cjs +98 -0
- package/dist/subset/resolve.d.cts +24 -0
- package/dist/subset/resolve.d.ts +24 -0
- package/dist/subset/resolve.js +90 -0
- package/dist/timing.cjs +206 -0
- package/dist/timing.d.cts +17 -0
- package/dist/timing.d.ts +17 -0
- package/dist/timing.js +201 -0
- package/dist/transformTemplate.cjs +219 -0
- package/dist/transformTemplate.d.cts +29 -0
- package/dist/transformTemplate.d.ts +29 -0
- package/dist/transformTemplate.js +215 -0
- package/dist/transition.cjs +231 -0
- package/dist/transition.d.cts +89 -0
- package/dist/transition.d.ts +89 -0
- package/dist/transition.js +223 -0
- package/dist/types.cjs +4 -0
- package/dist/types.d.cts +175 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.js +3 -0
- package/dist/value-types/color.cjs +220 -0
- package/dist/value-types/color.d.cts +19 -0
- package/dist/value-types/color.d.ts +19 -0
- package/dist/value-types/color.js +218 -0
- package/dist/value-types/complex.cjs +160 -0
- package/dist/value-types/complex.d.cts +19 -0
- package/dist/value-types/complex.d.ts +19 -0
- package/dist/value-types/complex.js +155 -0
- package/dist/value-types/constants.cjs +15 -0
- package/dist/value-types/constants.d.cts +6 -0
- package/dist/value-types/constants.d.ts +6 -0
- package/dist/value-types/constants.js +12 -0
- package/dist/value-types/discrete.cjs +62 -0
- package/dist/value-types/discrete.d.cts +4 -0
- package/dist/value-types/discrete.d.ts +4 -0
- package/dist/value-types/discrete.js +56 -0
- package/dist/value-types/index.cjs +59 -0
- package/dist/value-types/index.d.cts +14 -0
- package/dist/value-types/index.d.ts +14 -0
- package/dist/value-types/index.js +24 -0
- package/dist/value-types/measure-resolve.cjs +325 -0
- package/dist/value-types/measure-resolve.d.cts +91 -0
- package/dist/value-types/measure-resolve.d.ts +91 -0
- package/dist/value-types/measure-resolve.js +313 -0
- package/dist/value-types/mix.cjs +90 -0
- package/dist/value-types/mix.d.cts +27 -0
- package/dist/value-types/mix.d.ts +27 -0
- package/dist/value-types/mix.js +85 -0
- package/dist/value-types/named-colors.cjs +61 -0
- package/dist/value-types/named-colors.d.cts +2 -0
- package/dist/value-types/named-colors.d.ts +2 -0
- package/dist/value-types/named-colors.js +58 -0
- package/dist/value-types/numeric.cjs +86 -0
- package/dist/value-types/numeric.d.cts +16 -0
- package/dist/value-types/numeric.d.ts +16 -0
- package/dist/value-types/numeric.js +79 -0
- package/dist/worklet-layout/config/constants.js +39 -0
- package/dist/worklet-layout/layout/constants.js +11 -0
- package/dist/worklet-layout/layout/identity.js +699 -0
- package/dist/worklet-layout/layout/projection.js +98 -0
- package/dist/worklet-layout/layout/session.js +206 -0
- package/dist/worklet-layout/layout/tree.js +824 -0
- package/dist/worklet-layout/layout/types.js +36 -0
- package/dist/worklet-layout/spring.js +125 -0
- package/dist/worklet-layout/timing.js +201 -0
- package/dist/worklet-layout/transition.js +223 -0
- package/dist/worklet-layout.cjs +47 -0
- package/dist/worklet-layout.d.cts +15 -0
- package/dist/worklet-layout.d.ts +15 -0
- package/dist/worklet-layout.js +28 -0
- package/dist/wrap.cjs +7 -0
- package/dist/wrap.d.cts +1 -0
- package/dist/wrap.d.ts +1 -0
- package/dist/wrap.js +4 -0
- package/package.json +45 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPEC-GESTURE §2 — the projection function, project-then-snap selection, velocity normalization, and the
|
|
3
|
+
// shared release-target selector. Pure functions over plain numbers (REQ-GESTURE-010/016/017/019); no graph,
|
|
4
|
+
// no clock, no host. Host-agnostic (REQ-CORE-003): only a type import.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PROJECTION_POWER = void 0;
|
|
7
|
+
exports.project = project;
|
|
8
|
+
exports.selectSnap = selectSnap;
|
|
9
|
+
exports.normalizeVelocity = normalizeVelocity;
|
|
10
|
+
exports.absoluteDragOrigin = absoluteDragOrigin;
|
|
11
|
+
exports.selectReleaseTarget = selectReleaseTarget;
|
|
12
|
+
const elastic_1 = require("./elastic.cjs");
|
|
13
|
+
// The default projection power = the pinned `motion` 12.42.2 inertia `power` default (verified `power = 0.8`
|
|
14
|
+
// in node_modules/motion-dom/dist). The M1 selection/monotonicity invariants only need `power > 0`, so this
|
|
15
|
+
// exact value binds cross-engine PARITY (scenario 12, M2), not the M1 scenarios. A different value is a
|
|
16
|
+
// recorded divergence (REQ-GESTURE-019).
|
|
17
|
+
exports.PROJECTION_POWER = 0.8;
|
|
18
|
+
// P(origin, v) = origin + power·v (REQ-GESTURE-019, Motion's `power · velocity` model). The projected landing
|
|
19
|
+
// point the snap selector targets; `v = 0` collapses `ideal` to `origin` (nearest-to-release).
|
|
20
|
+
function project(origin, velocity, power = exports.PROJECTION_POWER) {
|
|
21
|
+
return origin + power * velocity;
|
|
22
|
+
}
|
|
23
|
+
// argmin_{s ∈ points} |s − ideal| (REQ-GESTURE-017). Fails loud on an empty set (G-INV-8) — a snap drag with
|
|
24
|
+
// no targets is a config error, never a silent no-op. Exact-midpoint ties resolve toward the SMALLER snap
|
|
25
|
+
// value, ORDER-INDEPENDENT — matching the web mapping (`native-motion-web` nearestSnap) byte-for-byte so
|
|
26
|
+
// the two engines never disagree on a tie (G-INV-10 cross-engine parity; review major 2). An
|
|
27
|
+
// order-dependent tie-break would return the far snap on native and the near one on web for the same input.
|
|
28
|
+
function selectSnap(ideal, points) {
|
|
29
|
+
if (points.length === 0) {
|
|
30
|
+
throw new Error('selectSnap: empty snap set — a snap drag needs at least one snap point (fail-loud, never a silent no-op)');
|
|
31
|
+
}
|
|
32
|
+
let best = points[0];
|
|
33
|
+
let bestDist = Math.abs(best - ideal);
|
|
34
|
+
for (let i = 1; i < points.length; i++) {
|
|
35
|
+
const p = points[i];
|
|
36
|
+
const d = Math.abs(p - ideal);
|
|
37
|
+
// Strictly nearer wins; on an exact tie the SMALLER value wins (web-parity, order-independent).
|
|
38
|
+
if (d < bestDist || (d === bestDist && p < best)) {
|
|
39
|
+
best = p;
|
|
40
|
+
bestDist = d;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return best;
|
|
44
|
+
}
|
|
45
|
+
// Normalize a recognizer velocity to the canonical px/s unit (REQ-GESTURE-010, Decision §G). The platform
|
|
46
|
+
// velocity is already px/s, so this is a unit-contract pass-through that carries sign — explicitly NO ÷1000
|
|
47
|
+
// (a px/ms conversion is the defect this guards). Fails loud on a non-finite value; it never silently zeroes
|
|
48
|
+
// (a hold-then-release yields a genuine ~0 sample from the platform, which is finite and passes through).
|
|
49
|
+
function normalizeVelocity(velocity) {
|
|
50
|
+
if (!Number.isFinite(velocity)) {
|
|
51
|
+
throw new Error(`normalizeVelocity: velocity must be a finite px/s value, got ${velocity}`);
|
|
52
|
+
}
|
|
53
|
+
return velocity;
|
|
54
|
+
}
|
|
55
|
+
// Product dual-authority baseline (REQ-GESTURE-022 class): host drag callbacks report gesture-relative
|
|
56
|
+
// `info.offset` (Motion PanInfo / native translationX/Y). The engine handoff projects from the absolute
|
|
57
|
+
// live release origin. Controlled product `animate` retargets must rebuild absolute origin as
|
|
58
|
+
// grabOrigin (last committed snap / settle) + gesture-relative offset — never offset alone when grab≠0.
|
|
59
|
+
function absoluteDragOrigin(grabOrigin, gestureRelativeOffset) {
|
|
60
|
+
if (!Number.isFinite(grabOrigin) || !Number.isFinite(gestureRelativeOffset)) {
|
|
61
|
+
throw new Error(`absoluteDragOrigin: grabOrigin and gestureRelativeOffset must be finite, got ${grabOrigin}, ${gestureRelativeOffset}`);
|
|
62
|
+
}
|
|
63
|
+
return grabOrigin + gestureRelativeOffset;
|
|
64
|
+
}
|
|
65
|
+
// The release target (REQ-GESTURE-016/017/023), shared verbatim by the M1 session and the handoff session
|
|
66
|
+
// (REQ-GESTURE-022) so the terminal decision can never fork: a boundary spring-back when released past a
|
|
67
|
+
// constraint bound, else project-then-snap onto the snap set, else — free-drag — the projected ideal
|
|
68
|
+
// clamped into the constraints. Under R16 that clamped ideal is NOT a spring target (the retired R13
|
|
69
|
+
// clamp): it is the inertia descriptor's MIRROR terminal — the two-phase generator recomputes its own
|
|
70
|
+
// ideal from the seed and owns the walls, so the actual settle can diverge from it (an out-of-bounds
|
|
71
|
+
// origin settles AT the violated bound via the t=0 latch, never at a clamped ideal that lies elsewhere).
|
|
72
|
+
function selectReleaseTarget(current, platformVelocity, constraints, snap) {
|
|
73
|
+
if (constraints !== undefined) {
|
|
74
|
+
// Same polarity law as applyConstraints (review major 0d144ea2f856): refuse wrong-polarity
|
|
75
|
+
// open sentinels before any comparison that would return ±Infinity as a spring target.
|
|
76
|
+
(0, elastic_1.assertLegalConstraintBounds)(constraints, 'selectReleaseTarget');
|
|
77
|
+
if (Number.isFinite(constraints.max) && current > constraints.max)
|
|
78
|
+
return constraints.max;
|
|
79
|
+
if (Number.isFinite(constraints.min) && current < constraints.min)
|
|
80
|
+
return constraints.min;
|
|
81
|
+
}
|
|
82
|
+
if (snap !== undefined)
|
|
83
|
+
return selectSnap(project(current, platformVelocity), snap.points);
|
|
84
|
+
// Free-drag (REQ-GESTURE-023): the inertia descriptor's MIRROR terminal — Motion inertia
|
|
85
|
+
// ideal = origin + power · v, clamped into the constraints. The generator owns the actual
|
|
86
|
+
// settle (R16); this value never steers the curve.
|
|
87
|
+
const ideal = project(current, platformVelocity);
|
|
88
|
+
if (constraints === undefined)
|
|
89
|
+
return ideal;
|
|
90
|
+
if (Number.isFinite(constraints.max) && ideal > constraints.max)
|
|
91
|
+
return constraints.max;
|
|
92
|
+
if (Number.isFinite(constraints.min) && ideal < constraints.min)
|
|
93
|
+
return constraints.min;
|
|
94
|
+
return ideal;
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DragConstraints, SnapConfig } from "./types.cjs";
|
|
2
|
+
export declare const PROJECTION_POWER = 0.8;
|
|
3
|
+
export declare function project(origin: number, velocity: number, power?: number): number;
|
|
4
|
+
export declare function selectSnap(ideal: number, points: readonly number[]): number;
|
|
5
|
+
export declare function normalizeVelocity(velocity: number): number;
|
|
6
|
+
export declare function absoluteDragOrigin(grabOrigin: number, gestureRelativeOffset: number): number;
|
|
7
|
+
export declare function selectReleaseTarget(current: number, platformVelocity: number, constraints: DragConstraints | undefined, snap: SnapConfig | undefined): number;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DragConstraints, SnapConfig } from "./types.js";
|
|
2
|
+
export declare const PROJECTION_POWER = 0.8;
|
|
3
|
+
export declare function project(origin: number, velocity: number, power?: number): number;
|
|
4
|
+
export declare function selectSnap(ideal: number, points: readonly number[]): number;
|
|
5
|
+
export declare function normalizeVelocity(velocity: number): number;
|
|
6
|
+
export declare function absoluteDragOrigin(grabOrigin: number, gestureRelativeOffset: number): number;
|
|
7
|
+
export declare function selectReleaseTarget(current: number, platformVelocity: number, constraints: DragConstraints | undefined, snap: SnapConfig | undefined): number;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPEC-GESTURE §2 — the projection function, project-then-snap selection, velocity normalization, and the
|
|
2
|
+
// shared release-target selector. Pure functions over plain numbers (REQ-GESTURE-010/016/017/019); no graph,
|
|
3
|
+
// no clock, no host. Host-agnostic (REQ-CORE-003): only a type import.
|
|
4
|
+
import { assertLegalConstraintBounds } from "./elastic.js";
|
|
5
|
+
// The default projection power = the pinned `motion` 12.42.2 inertia `power` default (verified `power = 0.8`
|
|
6
|
+
// in node_modules/motion-dom/dist). The M1 selection/monotonicity invariants only need `power > 0`, so this
|
|
7
|
+
// exact value binds cross-engine PARITY (scenario 12, M2), not the M1 scenarios. A different value is a
|
|
8
|
+
// recorded divergence (REQ-GESTURE-019).
|
|
9
|
+
export const PROJECTION_POWER = 0.8;
|
|
10
|
+
// P(origin, v) = origin + power·v (REQ-GESTURE-019, Motion's `power · velocity` model). The projected landing
|
|
11
|
+
// point the snap selector targets; `v = 0` collapses `ideal` to `origin` (nearest-to-release).
|
|
12
|
+
export function project(origin, velocity, power = PROJECTION_POWER) {
|
|
13
|
+
return origin + power * velocity;
|
|
14
|
+
}
|
|
15
|
+
// argmin_{s ∈ points} |s − ideal| (REQ-GESTURE-017). Fails loud on an empty set (G-INV-8) — a snap drag with
|
|
16
|
+
// no targets is a config error, never a silent no-op. Exact-midpoint ties resolve toward the SMALLER snap
|
|
17
|
+
// value, ORDER-INDEPENDENT — matching the web mapping (`native-motion-web` nearestSnap) byte-for-byte so
|
|
18
|
+
// the two engines never disagree on a tie (G-INV-10 cross-engine parity; review major 2). An
|
|
19
|
+
// order-dependent tie-break would return the far snap on native and the near one on web for the same input.
|
|
20
|
+
export function selectSnap(ideal, points) {
|
|
21
|
+
if (points.length === 0) {
|
|
22
|
+
throw new Error('selectSnap: empty snap set — a snap drag needs at least one snap point (fail-loud, never a silent no-op)');
|
|
23
|
+
}
|
|
24
|
+
let best = points[0];
|
|
25
|
+
let bestDist = Math.abs(best - ideal);
|
|
26
|
+
for (let i = 1; i < points.length; i++) {
|
|
27
|
+
const p = points[i];
|
|
28
|
+
const d = Math.abs(p - ideal);
|
|
29
|
+
// Strictly nearer wins; on an exact tie the SMALLER value wins (web-parity, order-independent).
|
|
30
|
+
if (d < bestDist || (d === bestDist && p < best)) {
|
|
31
|
+
best = p;
|
|
32
|
+
bestDist = d;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
// Normalize a recognizer velocity to the canonical px/s unit (REQ-GESTURE-010, Decision §G). The platform
|
|
38
|
+
// velocity is already px/s, so this is a unit-contract pass-through that carries sign — explicitly NO ÷1000
|
|
39
|
+
// (a px/ms conversion is the defect this guards). Fails loud on a non-finite value; it never silently zeroes
|
|
40
|
+
// (a hold-then-release yields a genuine ~0 sample from the platform, which is finite and passes through).
|
|
41
|
+
export function normalizeVelocity(velocity) {
|
|
42
|
+
if (!Number.isFinite(velocity)) {
|
|
43
|
+
throw new Error(`normalizeVelocity: velocity must be a finite px/s value, got ${velocity}`);
|
|
44
|
+
}
|
|
45
|
+
return velocity;
|
|
46
|
+
}
|
|
47
|
+
// Product dual-authority baseline (REQ-GESTURE-022 class): host drag callbacks report gesture-relative
|
|
48
|
+
// `info.offset` (Motion PanInfo / native translationX/Y). The engine handoff projects from the absolute
|
|
49
|
+
// live release origin. Controlled product `animate` retargets must rebuild absolute origin as
|
|
50
|
+
// grabOrigin (last committed snap / settle) + gesture-relative offset — never offset alone when grab≠0.
|
|
51
|
+
export function absoluteDragOrigin(grabOrigin, gestureRelativeOffset) {
|
|
52
|
+
if (!Number.isFinite(grabOrigin) || !Number.isFinite(gestureRelativeOffset)) {
|
|
53
|
+
throw new Error(`absoluteDragOrigin: grabOrigin and gestureRelativeOffset must be finite, got ${grabOrigin}, ${gestureRelativeOffset}`);
|
|
54
|
+
}
|
|
55
|
+
return grabOrigin + gestureRelativeOffset;
|
|
56
|
+
}
|
|
57
|
+
// The release target (REQ-GESTURE-016/017/023), shared verbatim by the M1 session and the handoff session
|
|
58
|
+
// (REQ-GESTURE-022) so the terminal decision can never fork: a boundary spring-back when released past a
|
|
59
|
+
// constraint bound, else project-then-snap onto the snap set, else — free-drag — the projected ideal
|
|
60
|
+
// clamped into the constraints. Under R16 that clamped ideal is NOT a spring target (the retired R13
|
|
61
|
+
// clamp): it is the inertia descriptor's MIRROR terminal — the two-phase generator recomputes its own
|
|
62
|
+
// ideal from the seed and owns the walls, so the actual settle can diverge from it (an out-of-bounds
|
|
63
|
+
// origin settles AT the violated bound via the t=0 latch, never at a clamped ideal that lies elsewhere).
|
|
64
|
+
export function selectReleaseTarget(current, platformVelocity, constraints, snap) {
|
|
65
|
+
if (constraints !== undefined) {
|
|
66
|
+
// Same polarity law as applyConstraints (review major 0d144ea2f856): refuse wrong-polarity
|
|
67
|
+
// open sentinels before any comparison that would return ±Infinity as a spring target.
|
|
68
|
+
assertLegalConstraintBounds(constraints, 'selectReleaseTarget');
|
|
69
|
+
if (Number.isFinite(constraints.max) && current > constraints.max)
|
|
70
|
+
return constraints.max;
|
|
71
|
+
if (Number.isFinite(constraints.min) && current < constraints.min)
|
|
72
|
+
return constraints.min;
|
|
73
|
+
}
|
|
74
|
+
if (snap !== undefined)
|
|
75
|
+
return selectSnap(project(current, platformVelocity), snap.points);
|
|
76
|
+
// Free-drag (REQ-GESTURE-023): the inertia descriptor's MIRROR terminal — Motion inertia
|
|
77
|
+
// ideal = origin + power · v, clamped into the constraints. The generator owns the actual
|
|
78
|
+
// settle (R16); this value never steers the curve.
|
|
79
|
+
const ideal = project(current, platformVelocity);
|
|
80
|
+
if (constraints === undefined)
|
|
81
|
+
return ideal;
|
|
82
|
+
if (Number.isFinite(constraints.max) && ideal > constraints.max)
|
|
83
|
+
return constraints.max;
|
|
84
|
+
if (Number.isFinite(constraints.min) && ideal < constraints.min)
|
|
85
|
+
return constraints.min;
|
|
86
|
+
return ideal;
|
|
87
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPEC-GESTURE §2 — the gesture SESSION: the total begin→active→end/cancel state machine that ingests a
|
|
3
|
+
// recognizer sample stream, drives a motion value continuously with grab continuity + live constraints, and
|
|
4
|
+
// on release hands off `(live value, platform velocity)` into the pinned two-phase inertia generator
|
|
5
|
+
// (free-drag, REQ-GESTURE-023 R16) or a velocity-seeded spring toward a project-then-snap target (snap).
|
|
6
|
+
// Drives the exit off the injected MotionGraph + the built generators — no forked math, no velocity
|
|
7
|
+
// estimator (Decision §G), no driver/React import. Host-agnostic (REQ-CORE-003): relative imports only.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GESTURE_RELEASE_SPRING = void 0;
|
|
10
|
+
exports.createGestureSession = createGestureSession;
|
|
11
|
+
const transition_1 = require("../transition.cjs");
|
|
12
|
+
const inertia_1 = require("../inertia.cjs");
|
|
13
|
+
const elastic_1 = require("./elastic.cjs");
|
|
14
|
+
const projection_1 = require("./projection.cjs");
|
|
15
|
+
// First-spike release spring (physics ⇒ carries the seeded velocity, REQ-SPRING-007). A near-critical snap
|
|
16
|
+
// spring — minimal overshoot for a settling sheet. Config constant, not an inline literal; refinable (the
|
|
17
|
+
// exact stiffness/damping binds feel + cross-engine parity at M2, not the M1 continuity/settle invariants).
|
|
18
|
+
// Exported: the component's driver-integrated release (REQ-GESTURE-022) seeds the SAME default into the
|
|
19
|
+
// driver's spring — one constant, never a duplicated literal. Typed with REQUIRED fields (satisfies, not
|
|
20
|
+
// annotation) so consumers can lift it into shapes with required numbers under exactOptionalPropertyTypes.
|
|
21
|
+
exports.GESTURE_RELEASE_SPRING = {
|
|
22
|
+
stiffness: 400,
|
|
23
|
+
damping: 40,
|
|
24
|
+
};
|
|
25
|
+
// Wrap a generator factory so the spring seeds from the LIVE value (C0) but with an EXPLICIT velocity (the
|
|
26
|
+
// platform release velocity, §G) rather than the value's own tracked velocity. The release spring is physics-
|
|
27
|
+
// resolved (carryVelocity true), so the seeded velocity is honored.
|
|
28
|
+
function seedWith(factory, velocity) {
|
|
29
|
+
return (seed) => factory({ from: seed.from, velocity });
|
|
30
|
+
}
|
|
31
|
+
function createGestureSession(options) {
|
|
32
|
+
const { graph } = options;
|
|
33
|
+
const constraints = options.constraints;
|
|
34
|
+
const snap = options.snap;
|
|
35
|
+
const releaseSpring = options.releaseSpring ?? exports.GESTURE_RELEASE_SPRING;
|
|
36
|
+
const value = graph.motionValue(options.initial ?? 0);
|
|
37
|
+
let state = 'idle';
|
|
38
|
+
let grabValue = 0; // the value at the last grab (P0)
|
|
39
|
+
let activationTranslation = 0; // recognizer translation captured at activation (dead-zone subtraction)
|
|
40
|
+
let preGrabValue = 0; // the value before the current grab — the cancel target (REQ-GESTURE-018)
|
|
41
|
+
let animation; // the in-flight release/cancel spring, if any
|
|
42
|
+
let lastHandoffVelocity; // the velocity seeded at the last release/cancel (§G)
|
|
43
|
+
// A finger being down is unsettled work with no animation, so the graph would otherwise report settled
|
|
44
|
+
// mid-drag. Hold an activity handle for the duration of the active gesture (G-INV-7 / REQ-DRIVER-002).
|
|
45
|
+
let dragHandle;
|
|
46
|
+
function releaseDragHandle() {
|
|
47
|
+
dragHandle?.release();
|
|
48
|
+
dragHandle = undefined;
|
|
49
|
+
}
|
|
50
|
+
// Reconcile the internal state to the graph: once the release/cancel spring has settled, the session is
|
|
51
|
+
// idle again. The settle is asynchronous (a later frame step), so the session cannot mutate `state` at the
|
|
52
|
+
// moment it happens — every state-dependent entry point reconciles first. Without this the field stayed
|
|
53
|
+
// stale at 'settling' after settle, and a subsequent cancel()/fail() rewound to the PREVIOUS gesture's
|
|
54
|
+
// pre-grab value (the "idle cancel rewind"). The state() getter reconciled for reads but the mutators did
|
|
55
|
+
// not, so the public state and the behavior disagreed.
|
|
56
|
+
function sync() {
|
|
57
|
+
if (state === 'settling' && graph.isSettled())
|
|
58
|
+
state = 'idle';
|
|
59
|
+
}
|
|
60
|
+
// Map a recognizer translation to the constrained motion value: P0 + (translation − activation), then
|
|
61
|
+
// hard-clamp / rubber-band per constraints (REQ-GESTURE-011/015). Delta-since-activation gives C0 across a
|
|
62
|
+
// non-zero activeOffset dead zone.
|
|
63
|
+
function applyToValue(translation) {
|
|
64
|
+
const raw = grabValue + (translation - activationTranslation);
|
|
65
|
+
value.set(constraints === undefined ? raw : (0, elastic_1.applyConstraints)(raw, constraints));
|
|
66
|
+
}
|
|
67
|
+
function startSpring(target, seedVelocity) {
|
|
68
|
+
animation = graph.animate(value, seedWith((0, transition_1.resolveSpringGenerator)(target, releaseSpring), seedVelocity));
|
|
69
|
+
state = 'settling';
|
|
70
|
+
}
|
|
71
|
+
// R16 (REQ-GESTURE-023): the free-drag release runs the pinned two-phase inertia generator on
|
|
72
|
+
// the SAME graph — the decay + boundary-spring curve, never the projected-landing spring. The
|
|
73
|
+
// generator recomputes the ideal from the seed and owns the walls; the drag-layer physics are
|
|
74
|
+
// the pin's (timeConstant 750, power 0.8, restDelta 1, restSpeed 10, elastic-conditional bounce).
|
|
75
|
+
function startInertia(seedVelocity) {
|
|
76
|
+
const elastic = constraints?.elastic;
|
|
77
|
+
const bounce = (0, transition_1.dragInertiaBounce)(elastic);
|
|
78
|
+
animation = graph.animate(value, seedWith((0, inertia_1.inertiaGenerator)({
|
|
79
|
+
power: transition_1.DRAG_INERTIA_DEFAULTS.power,
|
|
80
|
+
timeConstant: transition_1.DRAG_INERTIA_DEFAULTS.timeConstant,
|
|
81
|
+
bounceStiffness: bounce.bounceStiffness,
|
|
82
|
+
bounceDamping: bounce.bounceDamping,
|
|
83
|
+
restDelta: transition_1.DRAG_INERTIA_DEFAULTS.restDelta,
|
|
84
|
+
restSpeed: transition_1.DRAG_INERTIA_DEFAULTS.restSpeed,
|
|
85
|
+
...(constraints === undefined ? {} : { min: constraints.min, max: constraints.max }),
|
|
86
|
+
}), seedVelocity));
|
|
87
|
+
state = 'settling';
|
|
88
|
+
}
|
|
89
|
+
function cancelTo(target) {
|
|
90
|
+
sync(); // reconcile first — a settled release is 'idle', not a stale 'settling' (else the guard misfires)
|
|
91
|
+
if (state !== 'active' && state !== 'settling') {
|
|
92
|
+
// idle ⇒ no gesture in flight. Fail loud, consistent with the total state machine (G-INV-8:
|
|
93
|
+
// illegal transitions throw). NOT a silent no-op (which hides recognizer/caller misuse) and NOT a
|
|
94
|
+
// rewind — the throw fires before any mutation, so the settled value is untouched.
|
|
95
|
+
throw new Error(`gesture: cancel() in state '${state}' — no active or settling gesture to cancel`);
|
|
96
|
+
}
|
|
97
|
+
// state is 'active' (cancel the drag) or 'settling' (redirect the in-flight release) — both cancel to target.
|
|
98
|
+
animation?.stop();
|
|
99
|
+
lastHandoffVelocity = 0;
|
|
100
|
+
startSpring(target, 0); // cancel is a zero-velocity return to the cancel target (REQ-GESTURE-018)
|
|
101
|
+
releaseDragHandle(); // the spring now holds the graph unsettled; drop the drag handle
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
begin(activation) {
|
|
105
|
+
sync(); // a settled prior release is idle, not 'settling' — a fresh grab is legal
|
|
106
|
+
if (state === 'active') {
|
|
107
|
+
throw new Error('gesture: begin() during an active gesture — end() or cancel() first');
|
|
108
|
+
}
|
|
109
|
+
animation?.stop(); // re-grab: stop any in-flight release (decrements activeAnimationCount, REQ-GESTURE-012)
|
|
110
|
+
animation = undefined;
|
|
111
|
+
releaseDragHandle(); // defensive: never stack handles across a re-grab
|
|
112
|
+
grabValue = value.get();
|
|
113
|
+
preGrabValue = value.get();
|
|
114
|
+
activationTranslation = activation;
|
|
115
|
+
dragHandle = graph.registerActivity(); // a finger is down ⇒ the graph is unsettled (G-INV-7)
|
|
116
|
+
state = 'active';
|
|
117
|
+
},
|
|
118
|
+
active(sample) {
|
|
119
|
+
sync();
|
|
120
|
+
if (state !== 'active') {
|
|
121
|
+
throw new Error(`gesture: active() in state '${state}' — begin() a gesture first`);
|
|
122
|
+
}
|
|
123
|
+
applyToValue(sample.translation);
|
|
124
|
+
},
|
|
125
|
+
end(sample) {
|
|
126
|
+
sync();
|
|
127
|
+
if (state !== 'active') {
|
|
128
|
+
throw new Error(`gesture: end() in state '${state}' — no active gesture to release`);
|
|
129
|
+
}
|
|
130
|
+
applyToValue(sample.translation); // commit the final active position (C0 seam)
|
|
131
|
+
const platformVelocity = (0, projection_1.normalizeVelocity)(sample.velocity);
|
|
132
|
+
if (snap === undefined) {
|
|
133
|
+
// Free-drag (REQ-GESTURE-023, R16): the two-phase inertia generator owns the curve.
|
|
134
|
+
lastHandoffVelocity = platformVelocity;
|
|
135
|
+
startInertia(platformVelocity);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Resolve the target BEFORE mutating settle state so a config error leaves the session
|
|
139
|
+
// untouched rather than half-released. The selector is the SHARED pure seam (REQ-GESTURE-022 parity).
|
|
140
|
+
const target = (0, projection_1.selectReleaseTarget)(value.get(), platformVelocity, constraints, snap);
|
|
141
|
+
lastHandoffVelocity = platformVelocity; // the velocity handed off, UNSCALED (§G / REQ-GESTURE-013)
|
|
142
|
+
startSpring(target, platformVelocity);
|
|
143
|
+
}
|
|
144
|
+
releaseDragHandle(); // release AFTER the spring starts so there is no spurious settled frame
|
|
145
|
+
},
|
|
146
|
+
cancel() {
|
|
147
|
+
cancelTo(preGrabValue);
|
|
148
|
+
},
|
|
149
|
+
fail() {
|
|
150
|
+
cancelTo(preGrabValue); // a recognizer fail is a cancel to the pre-grab value (REQ-GESTURE-018)
|
|
151
|
+
},
|
|
152
|
+
value: () => value.get(),
|
|
153
|
+
velocity: () => value.getVelocity(),
|
|
154
|
+
isSettled: () => graph.isSettled(),
|
|
155
|
+
// Reconcile the post-release state: once the release/cancel spring has settled, the session is idle.
|
|
156
|
+
state: () => {
|
|
157
|
+
sync();
|
|
158
|
+
return state;
|
|
159
|
+
},
|
|
160
|
+
handoffVelocity: () => lastHandoffVelocity,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SpringConfig } from "../transition.cjs";
|
|
2
|
+
import type { MotionGraph } from "../types.cjs";
|
|
3
|
+
import type { DragConstraints, GestureSample, SnapConfig } from "./types.cjs";
|
|
4
|
+
export declare const GESTURE_RELEASE_SPRING: {
|
|
5
|
+
readonly stiffness: 400;
|
|
6
|
+
readonly damping: 40;
|
|
7
|
+
};
|
|
8
|
+
export type GestureSessionState = 'idle' | 'active' | 'settling';
|
|
9
|
+
export interface GestureSessionOptions {
|
|
10
|
+
readonly graph: MotionGraph;
|
|
11
|
+
readonly initial?: number;
|
|
12
|
+
readonly constraints?: DragConstraints;
|
|
13
|
+
readonly snap?: SnapConfig;
|
|
14
|
+
readonly releaseSpring?: SpringConfig;
|
|
15
|
+
}
|
|
16
|
+
export interface GestureSession {
|
|
17
|
+
begin(activationTranslation: number): void;
|
|
18
|
+
active(sample: GestureSample): void;
|
|
19
|
+
end(sample: GestureSample): void;
|
|
20
|
+
cancel(): void;
|
|
21
|
+
fail(): void;
|
|
22
|
+
value(): number;
|
|
23
|
+
velocity(): number;
|
|
24
|
+
isSettled(): boolean;
|
|
25
|
+
state(): GestureSessionState;
|
|
26
|
+
/** The velocity (px/s) seeded into the following animation at the last release/cancel; `undefined` before any. */
|
|
27
|
+
handoffVelocity(): number | undefined;
|
|
28
|
+
}
|
|
29
|
+
export declare function createGestureSession(options: GestureSessionOptions): GestureSession;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SpringConfig } from "../transition.js";
|
|
2
|
+
import type { MotionGraph } from "../types.js";
|
|
3
|
+
import type { DragConstraints, GestureSample, SnapConfig } from "./types.js";
|
|
4
|
+
export declare const GESTURE_RELEASE_SPRING: {
|
|
5
|
+
readonly stiffness: 400;
|
|
6
|
+
readonly damping: 40;
|
|
7
|
+
};
|
|
8
|
+
export type GestureSessionState = 'idle' | 'active' | 'settling';
|
|
9
|
+
export interface GestureSessionOptions {
|
|
10
|
+
readonly graph: MotionGraph;
|
|
11
|
+
readonly initial?: number;
|
|
12
|
+
readonly constraints?: DragConstraints;
|
|
13
|
+
readonly snap?: SnapConfig;
|
|
14
|
+
readonly releaseSpring?: SpringConfig;
|
|
15
|
+
}
|
|
16
|
+
export interface GestureSession {
|
|
17
|
+
begin(activationTranslation: number): void;
|
|
18
|
+
active(sample: GestureSample): void;
|
|
19
|
+
end(sample: GestureSample): void;
|
|
20
|
+
cancel(): void;
|
|
21
|
+
fail(): void;
|
|
22
|
+
value(): number;
|
|
23
|
+
velocity(): number;
|
|
24
|
+
isSettled(): boolean;
|
|
25
|
+
state(): GestureSessionState;
|
|
26
|
+
/** The velocity (px/s) seeded into the following animation at the last release/cancel; `undefined` before any. */
|
|
27
|
+
handoffVelocity(): number | undefined;
|
|
28
|
+
}
|
|
29
|
+
export declare function createGestureSession(options: GestureSessionOptions): GestureSession;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPEC-GESTURE §2 — the gesture SESSION: the total begin→active→end/cancel state machine that ingests a
|
|
2
|
+
// recognizer sample stream, drives a motion value continuously with grab continuity + live constraints, and
|
|
3
|
+
// on release hands off `(live value, platform velocity)` into the pinned two-phase inertia generator
|
|
4
|
+
// (free-drag, REQ-GESTURE-023 R16) or a velocity-seeded spring toward a project-then-snap target (snap).
|
|
5
|
+
// Drives the exit off the injected MotionGraph + the built generators — no forked math, no velocity
|
|
6
|
+
// estimator (Decision §G), no driver/React import. Host-agnostic (REQ-CORE-003): relative imports only.
|
|
7
|
+
import { DRAG_INERTIA_DEFAULTS, dragInertiaBounce, resolveSpringGenerator, } from "../transition.js";
|
|
8
|
+
import { inertiaGenerator } from "../inertia.js";
|
|
9
|
+
import { applyConstraints } from "./elastic.js";
|
|
10
|
+
import { normalizeVelocity, selectReleaseTarget } from "./projection.js";
|
|
11
|
+
// First-spike release spring (physics ⇒ carries the seeded velocity, REQ-SPRING-007). A near-critical snap
|
|
12
|
+
// spring — minimal overshoot for a settling sheet. Config constant, not an inline literal; refinable (the
|
|
13
|
+
// exact stiffness/damping binds feel + cross-engine parity at M2, not the M1 continuity/settle invariants).
|
|
14
|
+
// Exported: the component's driver-integrated release (REQ-GESTURE-022) seeds the SAME default into the
|
|
15
|
+
// driver's spring — one constant, never a duplicated literal. Typed with REQUIRED fields (satisfies, not
|
|
16
|
+
// annotation) so consumers can lift it into shapes with required numbers under exactOptionalPropertyTypes.
|
|
17
|
+
export const GESTURE_RELEASE_SPRING = {
|
|
18
|
+
stiffness: 400,
|
|
19
|
+
damping: 40,
|
|
20
|
+
};
|
|
21
|
+
// Wrap a generator factory so the spring seeds from the LIVE value (C0) but with an EXPLICIT velocity (the
|
|
22
|
+
// platform release velocity, §G) rather than the value's own tracked velocity. The release spring is physics-
|
|
23
|
+
// resolved (carryVelocity true), so the seeded velocity is honored.
|
|
24
|
+
function seedWith(factory, velocity) {
|
|
25
|
+
return (seed) => factory({ from: seed.from, velocity });
|
|
26
|
+
}
|
|
27
|
+
export function createGestureSession(options) {
|
|
28
|
+
const { graph } = options;
|
|
29
|
+
const constraints = options.constraints;
|
|
30
|
+
const snap = options.snap;
|
|
31
|
+
const releaseSpring = options.releaseSpring ?? GESTURE_RELEASE_SPRING;
|
|
32
|
+
const value = graph.motionValue(options.initial ?? 0);
|
|
33
|
+
let state = 'idle';
|
|
34
|
+
let grabValue = 0; // the value at the last grab (P0)
|
|
35
|
+
let activationTranslation = 0; // recognizer translation captured at activation (dead-zone subtraction)
|
|
36
|
+
let preGrabValue = 0; // the value before the current grab — the cancel target (REQ-GESTURE-018)
|
|
37
|
+
let animation; // the in-flight release/cancel spring, if any
|
|
38
|
+
let lastHandoffVelocity; // the velocity seeded at the last release/cancel (§G)
|
|
39
|
+
// A finger being down is unsettled work with no animation, so the graph would otherwise report settled
|
|
40
|
+
// mid-drag. Hold an activity handle for the duration of the active gesture (G-INV-7 / REQ-DRIVER-002).
|
|
41
|
+
let dragHandle;
|
|
42
|
+
function releaseDragHandle() {
|
|
43
|
+
dragHandle?.release();
|
|
44
|
+
dragHandle = undefined;
|
|
45
|
+
}
|
|
46
|
+
// Reconcile the internal state to the graph: once the release/cancel spring has settled, the session is
|
|
47
|
+
// idle again. The settle is asynchronous (a later frame step), so the session cannot mutate `state` at the
|
|
48
|
+
// moment it happens — every state-dependent entry point reconciles first. Without this the field stayed
|
|
49
|
+
// stale at 'settling' after settle, and a subsequent cancel()/fail() rewound to the PREVIOUS gesture's
|
|
50
|
+
// pre-grab value (the "idle cancel rewind"). The state() getter reconciled for reads but the mutators did
|
|
51
|
+
// not, so the public state and the behavior disagreed.
|
|
52
|
+
function sync() {
|
|
53
|
+
if (state === 'settling' && graph.isSettled())
|
|
54
|
+
state = 'idle';
|
|
55
|
+
}
|
|
56
|
+
// Map a recognizer translation to the constrained motion value: P0 + (translation − activation), then
|
|
57
|
+
// hard-clamp / rubber-band per constraints (REQ-GESTURE-011/015). Delta-since-activation gives C0 across a
|
|
58
|
+
// non-zero activeOffset dead zone.
|
|
59
|
+
function applyToValue(translation) {
|
|
60
|
+
const raw = grabValue + (translation - activationTranslation);
|
|
61
|
+
value.set(constraints === undefined ? raw : applyConstraints(raw, constraints));
|
|
62
|
+
}
|
|
63
|
+
function startSpring(target, seedVelocity) {
|
|
64
|
+
animation = graph.animate(value, seedWith(resolveSpringGenerator(target, releaseSpring), seedVelocity));
|
|
65
|
+
state = 'settling';
|
|
66
|
+
}
|
|
67
|
+
// R16 (REQ-GESTURE-023): the free-drag release runs the pinned two-phase inertia generator on
|
|
68
|
+
// the SAME graph — the decay + boundary-spring curve, never the projected-landing spring. The
|
|
69
|
+
// generator recomputes the ideal from the seed and owns the walls; the drag-layer physics are
|
|
70
|
+
// the pin's (timeConstant 750, power 0.8, restDelta 1, restSpeed 10, elastic-conditional bounce).
|
|
71
|
+
function startInertia(seedVelocity) {
|
|
72
|
+
const elastic = constraints?.elastic;
|
|
73
|
+
const bounce = dragInertiaBounce(elastic);
|
|
74
|
+
animation = graph.animate(value, seedWith(inertiaGenerator({
|
|
75
|
+
power: DRAG_INERTIA_DEFAULTS.power,
|
|
76
|
+
timeConstant: DRAG_INERTIA_DEFAULTS.timeConstant,
|
|
77
|
+
bounceStiffness: bounce.bounceStiffness,
|
|
78
|
+
bounceDamping: bounce.bounceDamping,
|
|
79
|
+
restDelta: DRAG_INERTIA_DEFAULTS.restDelta,
|
|
80
|
+
restSpeed: DRAG_INERTIA_DEFAULTS.restSpeed,
|
|
81
|
+
...(constraints === undefined ? {} : { min: constraints.min, max: constraints.max }),
|
|
82
|
+
}), seedVelocity));
|
|
83
|
+
state = 'settling';
|
|
84
|
+
}
|
|
85
|
+
function cancelTo(target) {
|
|
86
|
+
sync(); // reconcile first — a settled release is 'idle', not a stale 'settling' (else the guard misfires)
|
|
87
|
+
if (state !== 'active' && state !== 'settling') {
|
|
88
|
+
// idle ⇒ no gesture in flight. Fail loud, consistent with the total state machine (G-INV-8:
|
|
89
|
+
// illegal transitions throw). NOT a silent no-op (which hides recognizer/caller misuse) and NOT a
|
|
90
|
+
// rewind — the throw fires before any mutation, so the settled value is untouched.
|
|
91
|
+
throw new Error(`gesture: cancel() in state '${state}' — no active or settling gesture to cancel`);
|
|
92
|
+
}
|
|
93
|
+
// state is 'active' (cancel the drag) or 'settling' (redirect the in-flight release) — both cancel to target.
|
|
94
|
+
animation?.stop();
|
|
95
|
+
lastHandoffVelocity = 0;
|
|
96
|
+
startSpring(target, 0); // cancel is a zero-velocity return to the cancel target (REQ-GESTURE-018)
|
|
97
|
+
releaseDragHandle(); // the spring now holds the graph unsettled; drop the drag handle
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
begin(activation) {
|
|
101
|
+
sync(); // a settled prior release is idle, not 'settling' — a fresh grab is legal
|
|
102
|
+
if (state === 'active') {
|
|
103
|
+
throw new Error('gesture: begin() during an active gesture — end() or cancel() first');
|
|
104
|
+
}
|
|
105
|
+
animation?.stop(); // re-grab: stop any in-flight release (decrements activeAnimationCount, REQ-GESTURE-012)
|
|
106
|
+
animation = undefined;
|
|
107
|
+
releaseDragHandle(); // defensive: never stack handles across a re-grab
|
|
108
|
+
grabValue = value.get();
|
|
109
|
+
preGrabValue = value.get();
|
|
110
|
+
activationTranslation = activation;
|
|
111
|
+
dragHandle = graph.registerActivity(); // a finger is down ⇒ the graph is unsettled (G-INV-7)
|
|
112
|
+
state = 'active';
|
|
113
|
+
},
|
|
114
|
+
active(sample) {
|
|
115
|
+
sync();
|
|
116
|
+
if (state !== 'active') {
|
|
117
|
+
throw new Error(`gesture: active() in state '${state}' — begin() a gesture first`);
|
|
118
|
+
}
|
|
119
|
+
applyToValue(sample.translation);
|
|
120
|
+
},
|
|
121
|
+
end(sample) {
|
|
122
|
+
sync();
|
|
123
|
+
if (state !== 'active') {
|
|
124
|
+
throw new Error(`gesture: end() in state '${state}' — no active gesture to release`);
|
|
125
|
+
}
|
|
126
|
+
applyToValue(sample.translation); // commit the final active position (C0 seam)
|
|
127
|
+
const platformVelocity = normalizeVelocity(sample.velocity);
|
|
128
|
+
if (snap === undefined) {
|
|
129
|
+
// Free-drag (REQ-GESTURE-023, R16): the two-phase inertia generator owns the curve.
|
|
130
|
+
lastHandoffVelocity = platformVelocity;
|
|
131
|
+
startInertia(platformVelocity);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Resolve the target BEFORE mutating settle state so a config error leaves the session
|
|
135
|
+
// untouched rather than half-released. The selector is the SHARED pure seam (REQ-GESTURE-022 parity).
|
|
136
|
+
const target = selectReleaseTarget(value.get(), platformVelocity, constraints, snap);
|
|
137
|
+
lastHandoffVelocity = platformVelocity; // the velocity handed off, UNSCALED (§G / REQ-GESTURE-013)
|
|
138
|
+
startSpring(target, platformVelocity);
|
|
139
|
+
}
|
|
140
|
+
releaseDragHandle(); // release AFTER the spring starts so there is no spurious settled frame
|
|
141
|
+
},
|
|
142
|
+
cancel() {
|
|
143
|
+
cancelTo(preGrabValue);
|
|
144
|
+
},
|
|
145
|
+
fail() {
|
|
146
|
+
cancelTo(preGrabValue); // a recognizer fail is a cancel to the pre-grab value (REQ-GESTURE-018)
|
|
147
|
+
},
|
|
148
|
+
value: () => value.get(),
|
|
149
|
+
velocity: () => value.getVelocity(),
|
|
150
|
+
isSettled: () => graph.isSettled(),
|
|
151
|
+
// Reconcile the post-release state: once the release/cancel spring has settled, the session is idle.
|
|
152
|
+
state: () => {
|
|
153
|
+
sync();
|
|
154
|
+
return state;
|
|
155
|
+
},
|
|
156
|
+
handoffVelocity: () => lastHandoffVelocity,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPEC-GESTURE §2 — the gesture ingest + constraint types. Host-agnostic (REQ-CORE-003): plain data only,
|
|
3
|
+
// no react/react-native/Gesture-Handler import. A recognizer stream is modeled as a sequence of these
|
|
4
|
+
// samples; the real Gesture Handler / motion-react bindings that produce them are Milestone 2 (deferred).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GestureSample {
|
|
2
|
+
readonly t: number;
|
|
3
|
+
readonly translation: number;
|
|
4
|
+
readonly velocity: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DragConstraints {
|
|
7
|
+
readonly min: number;
|
|
8
|
+
readonly max: number;
|
|
9
|
+
readonly elastic?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SnapConfig {
|
|
12
|
+
readonly points: readonly number[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GestureSample {
|
|
2
|
+
readonly t: number;
|
|
3
|
+
readonly translation: number;
|
|
4
|
+
readonly velocity: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DragConstraints {
|
|
7
|
+
readonly min: number;
|
|
8
|
+
readonly max: number;
|
|
9
|
+
readonly elastic?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SnapConfig {
|
|
12
|
+
readonly points: readonly number[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// SPEC-GESTURE §2 — the gesture ingest + constraint types. Host-agnostic (REQ-CORE-003): plain data only,
|
|
2
|
+
// no react/react-native/Gesture-Handler import. A recognizer stream is modeled as a sequence of these
|
|
3
|
+
// samples; the real Gesture Handler / motion-react bindings that produce them are Milestone 2 (deferred).
|
|
4
|
+
export {};
|