@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,174 @@
|
|
|
1
|
+
import type { Generator } from "./types.cjs";
|
|
2
|
+
export type RepeatType = 'loop' | 'reverse' | 'mirror';
|
|
3
|
+
/**
|
|
4
|
+
* How an ODD iteration plays. `mirror` carries its own second generator rather than reflecting the
|
|
5
|
+
* first: the pin builds it from the same factory with REVERSED KEYFRAMES and a NEGATED seed
|
|
6
|
+
* velocity (`JSAnimation:147-153`), and `times`/`ease` pass through untouched — so a mirrored leg is
|
|
7
|
+
* genuinely a different trajectory, not a time-reflection of the forward one. Carrying the
|
|
8
|
+
* generator in the variant makes a mirror fold without its second generator UNREPRESENTABLE, which
|
|
9
|
+
* is what keeps this frame-path module free of a runtime guard it could only report by throwing on
|
|
10
|
+
* the UI runtime.
|
|
11
|
+
*/
|
|
12
|
+
export type RepeatLeg = {
|
|
13
|
+
readonly type: 'loop';
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: 'reverse';
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: 'mirror';
|
|
18
|
+
readonly mirrored: Generator;
|
|
19
|
+
};
|
|
20
|
+
export interface RepeatFoldConfig {
|
|
21
|
+
/** Additional plays after the first. A non-negative integer, or `Infinity` for endless. */
|
|
22
|
+
readonly repeat: number;
|
|
23
|
+
readonly leg: RepeatLeg;
|
|
24
|
+
/** Dead time appended to each iteration, in MILLISECONDS (the public seconds convert upstream). */
|
|
25
|
+
readonly repeatDelayMs: number;
|
|
26
|
+
/**
|
|
27
|
+
* One play's length in ms — the pin's `calculatedDuration`: a tween/keyframe array's authored
|
|
28
|
+
* duration, and for a PHYSICS spring the `calcIterationDurationMs` scan below.
|
|
29
|
+
*/
|
|
30
|
+
readonly iterationDurationMs: number;
|
|
31
|
+
}
|
|
32
|
+
export interface RepeatFoldGeometry {
|
|
33
|
+
/** One iteration INCLUDING its trailing repeat delay (the pin's `resolvedDuration`). */
|
|
34
|
+
readonly resolvedDurationMs: number;
|
|
35
|
+
/** The whole fold; `Infinity` for an endless repeat (the pin's `totalDuration`). */
|
|
36
|
+
readonly totalDurationMs: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const ITERATION_SCAN_STEP_MS = 50;
|
|
39
|
+
export declare const MAX_ITERATION_DURATION_MS = 20000;
|
|
40
|
+
/**
|
|
41
|
+
* The iteration length of a generator that does not know its own duration (a physics or
|
|
42
|
+
* visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
|
|
43
|
+
* grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
|
|
44
|
+
* forever. Safe to run against a live generator: core's generators are pure functions of elapsed
|
|
45
|
+
* time (they latch no state, only reuse their sample record).
|
|
46
|
+
*/
|
|
47
|
+
export declare function calcIterationDurationMs(generator: Generator): number;
|
|
48
|
+
/**
|
|
49
|
+
* Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
|
|
50
|
+
* length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
|
|
51
|
+
* pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
|
|
52
|
+
* the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
|
|
53
|
+
* `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
|
|
54
|
+
* EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
|
|
55
|
+
* F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
|
|
56
|
+
* way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
|
|
57
|
+
* divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
|
|
58
|
+
* crosses to the UI runtime, where a throw is a bare `std::terminate`).
|
|
59
|
+
*/
|
|
60
|
+
export declare function repeatIterationRefusal(iterationDurationMs: number, repeat: number, repeatDelayMs?: number): Error | null;
|
|
61
|
+
/**
|
|
62
|
+
* WHICH measurement decides a fold's iteration length. This is the rule that has now been
|
|
63
|
+
* re-derived, and got wrong, four times — three parallel copies of it exist by necessity
|
|
64
|
+
* (validation, the reference backend, the worklet backend), and every round a copy drifted:
|
|
65
|
+
*
|
|
66
|
+
* round 1 validation used `type==='tween' || duration || ease` -> measured springs as tweens
|
|
67
|
+
* round 2 validation used "physics keys present" -> measured tweens as springs
|
|
68
|
+
* round 3 validation added the tween short-circuit but stayed target-BLIND -> disagreed with
|
|
69
|
+
* both backends for every keyframe-ARRAY target, because the lane depends on the TARGET
|
|
70
|
+
* SHAPE as well as the transition
|
|
71
|
+
*
|
|
72
|
+
* So the decision itself lives here, once, over primitives — the three callers differ only in the
|
|
73
|
+
* types they hold (`Transition`, the driver's `PropTransition`, the UI runtime's plain-data
|
|
74
|
+
* `PlainTransition`), never in the rule. Worklet-safe by construction: primitives in, string out.
|
|
75
|
+
*
|
|
76
|
+
* `targetIsNumeric` is the load-bearing parameter validation cannot supply, because the boundary
|
|
77
|
+
* validator sees a transition and no target. That is not a gap to paper over — it is why validation
|
|
78
|
+
* must evaluate the rule for EVERY target shape and refuse only when they agree (REQ-TIMING-003).
|
|
79
|
+
*
|
|
80
|
+
* INERTIA is not a lane here. It has no repeatable iteration at all, and each caller must already
|
|
81
|
+
* narrow it off its own union before this point — so an `isInertia` parameter would have been dead
|
|
82
|
+
* at all three call sites while implying the rule covered a case it does not (round-5 review MINOR).
|
|
83
|
+
* Callers return early; this answers only the question it can actually decide.
|
|
84
|
+
*/
|
|
85
|
+
export type IterationMeasurementLane = 'keyframes' | 'timing' | 'spring';
|
|
86
|
+
export declare function iterationMeasurementLane(targetIsNumeric: boolean, isExplicitSpring: boolean, isTween: boolean): IterationMeasurementLane;
|
|
87
|
+
/**
|
|
88
|
+
* The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
|
|
89
|
+
*
|
|
90
|
+
* `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
|
|
91
|
+
* reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
|
|
92
|
+
* instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
|
|
93
|
+
* construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
|
|
94
|
+
* anyway, so the class identity could not survive the crossing even if it could be built.
|
|
95
|
+
*
|
|
96
|
+
* What CAN be identical across the crossing is exactly what the crossing carries — the name and the
|
|
97
|
+
* message. Both backends therefore build them here, so a developer sees one refusal whichever
|
|
98
|
+
* engine they are on, and `repeatRefusalParity` pins the two against each other.
|
|
99
|
+
*/
|
|
100
|
+
export declare const REPEAT_REFUSAL_NAME = "InvalidTransitionError";
|
|
101
|
+
/** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
|
|
102
|
+
export declare const REPEAT_REFUSAL_KEY = "repeat";
|
|
103
|
+
/**
|
|
104
|
+
* A fold refusal, as a CONSUMER can recognize it after any transport.
|
|
105
|
+
*
|
|
106
|
+
* The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
|
|
107
|
+
* which is decidable only on the reference backend: the UI runtime cannot construct a core class,
|
|
108
|
+
* and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
|
|
109
|
+
* and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
|
|
110
|
+
* available across the crossing, so the recognition contract cannot be built on it — it is
|
|
111
|
+
* STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
|
|
112
|
+
* whole identity is one thing that one floor can hold.
|
|
113
|
+
*
|
|
114
|
+
* `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
|
|
115
|
+
* marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
|
|
116
|
+
* consumer applies to the rethrown error rather than a second copy that can drift.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isRepeatFoldRefusalIdentity(name: unknown, key: unknown): boolean;
|
|
119
|
+
export declare function isRepeatFoldRefusal(error: unknown): error is Error & {
|
|
120
|
+
readonly key: string;
|
|
121
|
+
};
|
|
122
|
+
export declare function repeatRefusalMessage(repeat: number, reason: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
|
|
125
|
+
* delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
|
|
126
|
+
* the animation past its final one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function repeatFoldGeometry(config: {
|
|
129
|
+
readonly repeat: number;
|
|
130
|
+
readonly repeatDelayMs: number;
|
|
131
|
+
readonly iterationDurationMs: number;
|
|
132
|
+
}): RepeatFoldGeometry;
|
|
133
|
+
/**
|
|
134
|
+
* Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
|
|
135
|
+
* other — the driver steps it without knowing it repeats.
|
|
136
|
+
*
|
|
137
|
+
* Two laws diverge deliberately from a naive port of the pin's `tick`:
|
|
138
|
+
*
|
|
139
|
+
* - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
|
|
140
|
+
* velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
|
|
141
|
+
* whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
|
|
142
|
+
* construction even though its generator's internal velocity on a reversed leg is not.
|
|
143
|
+
* Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
|
|
144
|
+
* - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
|
|
145
|
+
* settles once per iteration; only the last one ends the animation, and an endless repeat is
|
|
146
|
+
* never done at all (packet L5 — such an element intentionally never quiesces).
|
|
147
|
+
*/
|
|
148
|
+
export declare function repeatGenerator(base: Generator, config: RepeatFoldConfig): Generator;
|
|
149
|
+
/**
|
|
150
|
+
* Build the folded generator for a repeated transition — the ONE implementation every driver seam
|
|
151
|
+
* calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
|
|
152
|
+
* skew: the fix-up review found both backends sharing it while both omitted it from retarget.
|
|
153
|
+
* Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
|
|
154
|
+
* only what it alone knows:
|
|
155
|
+
*
|
|
156
|
+
* - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
|
|
157
|
+
* the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
|
|
158
|
+
* mirror fold without one stays unrepresentable.
|
|
159
|
+
* - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
|
|
160
|
+
* a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
|
|
161
|
+
* null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
|
|
162
|
+
* is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
|
|
163
|
+
* zero-length plays (transition-default-selection F5) — scanning the base instead would invent
|
|
164
|
+
* a 50ms iteration the pin does not have.
|
|
165
|
+
*
|
|
166
|
+
* Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
|
|
167
|
+
* function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
|
|
168
|
+
* fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
|
|
169
|
+
*
|
|
170
|
+
* Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
|
|
171
|
+
* caller routes that refusal through its severity boundary — this never throws, because it runs on
|
|
172
|
+
* the UI runtime where a throw is a bare `std::terminate`.
|
|
173
|
+
*/
|
|
174
|
+
export declare function buildRepeatedGenerator(repeat: number, leg: RepeatLeg, repeatDelayMs: number, base: Generator, knownIterationDurationMs: number | null): Generator | Error;
|
package/dist/repeat.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Generator } from "./types.js";
|
|
2
|
+
export type RepeatType = 'loop' | 'reverse' | 'mirror';
|
|
3
|
+
/**
|
|
4
|
+
* How an ODD iteration plays. `mirror` carries its own second generator rather than reflecting the
|
|
5
|
+
* first: the pin builds it from the same factory with REVERSED KEYFRAMES and a NEGATED seed
|
|
6
|
+
* velocity (`JSAnimation:147-153`), and `times`/`ease` pass through untouched — so a mirrored leg is
|
|
7
|
+
* genuinely a different trajectory, not a time-reflection of the forward one. Carrying the
|
|
8
|
+
* generator in the variant makes a mirror fold without its second generator UNREPRESENTABLE, which
|
|
9
|
+
* is what keeps this frame-path module free of a runtime guard it could only report by throwing on
|
|
10
|
+
* the UI runtime.
|
|
11
|
+
*/
|
|
12
|
+
export type RepeatLeg = {
|
|
13
|
+
readonly type: 'loop';
|
|
14
|
+
} | {
|
|
15
|
+
readonly type: 'reverse';
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: 'mirror';
|
|
18
|
+
readonly mirrored: Generator;
|
|
19
|
+
};
|
|
20
|
+
export interface RepeatFoldConfig {
|
|
21
|
+
/** Additional plays after the first. A non-negative integer, or `Infinity` for endless. */
|
|
22
|
+
readonly repeat: number;
|
|
23
|
+
readonly leg: RepeatLeg;
|
|
24
|
+
/** Dead time appended to each iteration, in MILLISECONDS (the public seconds convert upstream). */
|
|
25
|
+
readonly repeatDelayMs: number;
|
|
26
|
+
/**
|
|
27
|
+
* One play's length in ms — the pin's `calculatedDuration`: a tween/keyframe array's authored
|
|
28
|
+
* duration, and for a PHYSICS spring the `calcIterationDurationMs` scan below.
|
|
29
|
+
*/
|
|
30
|
+
readonly iterationDurationMs: number;
|
|
31
|
+
}
|
|
32
|
+
export interface RepeatFoldGeometry {
|
|
33
|
+
/** One iteration INCLUDING its trailing repeat delay (the pin's `resolvedDuration`). */
|
|
34
|
+
readonly resolvedDurationMs: number;
|
|
35
|
+
/** The whole fold; `Infinity` for an endless repeat (the pin's `totalDuration`). */
|
|
36
|
+
readonly totalDurationMs: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const ITERATION_SCAN_STEP_MS = 50;
|
|
39
|
+
export declare const MAX_ITERATION_DURATION_MS = 20000;
|
|
40
|
+
/**
|
|
41
|
+
* The iteration length of a generator that does not know its own duration (a physics or
|
|
42
|
+
* visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
|
|
43
|
+
* grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
|
|
44
|
+
* forever. Safe to run against a live generator: core's generators are pure functions of elapsed
|
|
45
|
+
* time (they latch no state, only reuse their sample record).
|
|
46
|
+
*/
|
|
47
|
+
export declare function calcIterationDurationMs(generator: Generator): number;
|
|
48
|
+
/**
|
|
49
|
+
* Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
|
|
50
|
+
* length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
|
|
51
|
+
* pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
|
|
52
|
+
* the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
|
|
53
|
+
* `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
|
|
54
|
+
* EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
|
|
55
|
+
* F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
|
|
56
|
+
* way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
|
|
57
|
+
* divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
|
|
58
|
+
* crosses to the UI runtime, where a throw is a bare `std::terminate`).
|
|
59
|
+
*/
|
|
60
|
+
export declare function repeatIterationRefusal(iterationDurationMs: number, repeat: number, repeatDelayMs?: number): Error | null;
|
|
61
|
+
/**
|
|
62
|
+
* WHICH measurement decides a fold's iteration length. This is the rule that has now been
|
|
63
|
+
* re-derived, and got wrong, four times — three parallel copies of it exist by necessity
|
|
64
|
+
* (validation, the reference backend, the worklet backend), and every round a copy drifted:
|
|
65
|
+
*
|
|
66
|
+
* round 1 validation used `type==='tween' || duration || ease` -> measured springs as tweens
|
|
67
|
+
* round 2 validation used "physics keys present" -> measured tweens as springs
|
|
68
|
+
* round 3 validation added the tween short-circuit but stayed target-BLIND -> disagreed with
|
|
69
|
+
* both backends for every keyframe-ARRAY target, because the lane depends on the TARGET
|
|
70
|
+
* SHAPE as well as the transition
|
|
71
|
+
*
|
|
72
|
+
* So the decision itself lives here, once, over primitives — the three callers differ only in the
|
|
73
|
+
* types they hold (`Transition`, the driver's `PropTransition`, the UI runtime's plain-data
|
|
74
|
+
* `PlainTransition`), never in the rule. Worklet-safe by construction: primitives in, string out.
|
|
75
|
+
*
|
|
76
|
+
* `targetIsNumeric` is the load-bearing parameter validation cannot supply, because the boundary
|
|
77
|
+
* validator sees a transition and no target. That is not a gap to paper over — it is why validation
|
|
78
|
+
* must evaluate the rule for EVERY target shape and refuse only when they agree (REQ-TIMING-003).
|
|
79
|
+
*
|
|
80
|
+
* INERTIA is not a lane here. It has no repeatable iteration at all, and each caller must already
|
|
81
|
+
* narrow it off its own union before this point — so an `isInertia` parameter would have been dead
|
|
82
|
+
* at all three call sites while implying the rule covered a case it does not (round-5 review MINOR).
|
|
83
|
+
* Callers return early; this answers only the question it can actually decide.
|
|
84
|
+
*/
|
|
85
|
+
export type IterationMeasurementLane = 'keyframes' | 'timing' | 'spring';
|
|
86
|
+
export declare function iterationMeasurementLane(targetIsNumeric: boolean, isExplicitSpring: boolean, isTween: boolean): IterationMeasurementLane;
|
|
87
|
+
/**
|
|
88
|
+
* The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
|
|
89
|
+
*
|
|
90
|
+
* `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
|
|
91
|
+
* reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
|
|
92
|
+
* instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
|
|
93
|
+
* construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
|
|
94
|
+
* anyway, so the class identity could not survive the crossing even if it could be built.
|
|
95
|
+
*
|
|
96
|
+
* What CAN be identical across the crossing is exactly what the crossing carries — the name and the
|
|
97
|
+
* message. Both backends therefore build them here, so a developer sees one refusal whichever
|
|
98
|
+
* engine they are on, and `repeatRefusalParity` pins the two against each other.
|
|
99
|
+
*/
|
|
100
|
+
export declare const REPEAT_REFUSAL_NAME = "InvalidTransitionError";
|
|
101
|
+
/** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
|
|
102
|
+
export declare const REPEAT_REFUSAL_KEY = "repeat";
|
|
103
|
+
/**
|
|
104
|
+
* A fold refusal, as a CONSUMER can recognize it after any transport.
|
|
105
|
+
*
|
|
106
|
+
* The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
|
|
107
|
+
* which is decidable only on the reference backend: the UI runtime cannot construct a core class,
|
|
108
|
+
* and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
|
|
109
|
+
* and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
|
|
110
|
+
* available across the crossing, so the recognition contract cannot be built on it — it is
|
|
111
|
+
* STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
|
|
112
|
+
* whole identity is one thing that one floor can hold.
|
|
113
|
+
*
|
|
114
|
+
* `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
|
|
115
|
+
* marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
|
|
116
|
+
* consumer applies to the rethrown error rather than a second copy that can drift.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isRepeatFoldRefusalIdentity(name: unknown, key: unknown): boolean;
|
|
119
|
+
export declare function isRepeatFoldRefusal(error: unknown): error is Error & {
|
|
120
|
+
readonly key: string;
|
|
121
|
+
};
|
|
122
|
+
export declare function repeatRefusalMessage(repeat: number, reason: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
|
|
125
|
+
* delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
|
|
126
|
+
* the animation past its final one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function repeatFoldGeometry(config: {
|
|
129
|
+
readonly repeat: number;
|
|
130
|
+
readonly repeatDelayMs: number;
|
|
131
|
+
readonly iterationDurationMs: number;
|
|
132
|
+
}): RepeatFoldGeometry;
|
|
133
|
+
/**
|
|
134
|
+
* Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
|
|
135
|
+
* other — the driver steps it without knowing it repeats.
|
|
136
|
+
*
|
|
137
|
+
* Two laws diverge deliberately from a naive port of the pin's `tick`:
|
|
138
|
+
*
|
|
139
|
+
* - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
|
|
140
|
+
* velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
|
|
141
|
+
* whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
|
|
142
|
+
* construction even though its generator's internal velocity on a reversed leg is not.
|
|
143
|
+
* Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
|
|
144
|
+
* - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
|
|
145
|
+
* settles once per iteration; only the last one ends the animation, and an endless repeat is
|
|
146
|
+
* never done at all (packet L5 — such an element intentionally never quiesces).
|
|
147
|
+
*/
|
|
148
|
+
export declare function repeatGenerator(base: Generator, config: RepeatFoldConfig): Generator;
|
|
149
|
+
/**
|
|
150
|
+
* Build the folded generator for a repeated transition — the ONE implementation every driver seam
|
|
151
|
+
* calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
|
|
152
|
+
* skew: the fix-up review found both backends sharing it while both omitted it from retarget.
|
|
153
|
+
* Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
|
|
154
|
+
* only what it alone knows:
|
|
155
|
+
*
|
|
156
|
+
* - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
|
|
157
|
+
* the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
|
|
158
|
+
* mirror fold without one stays unrepresentable.
|
|
159
|
+
* - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
|
|
160
|
+
* a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
|
|
161
|
+
* null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
|
|
162
|
+
* is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
|
|
163
|
+
* zero-length plays (transition-default-selection F5) — scanning the base instead would invent
|
|
164
|
+
* a 50ms iteration the pin does not have.
|
|
165
|
+
*
|
|
166
|
+
* Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
|
|
167
|
+
* function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
|
|
168
|
+
* fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
|
|
169
|
+
*
|
|
170
|
+
* Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
|
|
171
|
+
* caller routes that refusal through its severity boundary — this never throws, because it runs on
|
|
172
|
+
* the UI runtime where a throw is a bare `std::terminate`.
|
|
173
|
+
*/
|
|
174
|
+
export declare function buildRepeatedGenerator(repeat: number, leg: RepeatLeg, repeatDelayMs: number, base: Generator, knownIterationDurationMs: number | null): Generator | Error;
|
package/dist/repeat.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'worklet';
|
|
2
|
+
// @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
|
|
3
|
+
// T18-a / REQ-TIMING-003 — the repeat elapsed-time FOLD. `repeat`/`repeatType`/`repeatDelay` are not
|
|
4
|
+
// generator features in the pinned Motion: they are a pure `t → t'` map wrapped AROUND a generator
|
|
5
|
+
// (motion-dom@12.42.2 `JSAnimation.initAnimation` + `tick`). This module is that map, so every
|
|
6
|
+
// generator core owns — spring, tween, keyframe array — repeats identically and none of them learns
|
|
7
|
+
// about repetition. Host-agnostic (REQ-CORE-003): relative imports only. Numerics are pinned by
|
|
8
|
+
// per-sample goldens in repeat.test.ts.
|
|
9
|
+
//
|
|
10
|
+
// The fold divides by the RESOLVED iteration length (play + repeatDelay), so it is TOTAL only
|
|
11
|
+
// when that is finite and positive: a zero-length play with no repeatDelay produces NaN at the
|
|
12
|
+
// pin and is refused upstream (`repeatIterationRefusal`), as is an unbounded one. Zero-length
|
|
13
|
+
// plays separated by a repeatDelay are the pin's executable shape (transition-default-selection
|
|
14
|
+
// F5) and fold fine.
|
|
15
|
+
// The pin's `calcGeneratorDuration` scan constants (motion-dom keyframes/calc-duration.ts). The
|
|
16
|
+
// 50ms grid is why a physics spring's iteration length is COARSE — it is the pin's number, not an
|
|
17
|
+
// analytic settle time, and reproducing it exactly is what keeps repeated springs in parity.
|
|
18
|
+
export const ITERATION_SCAN_STEP_MS = 50;
|
|
19
|
+
export const MAX_ITERATION_DURATION_MS = 20_000;
|
|
20
|
+
/**
|
|
21
|
+
* The iteration length of a generator that does not know its own duration (a physics or
|
|
22
|
+
* visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
|
|
23
|
+
* grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
|
|
24
|
+
* forever. Safe to run against a live generator: core's generators are pure functions of elapsed
|
|
25
|
+
* time (they latch no state, only reuse their sample record).
|
|
26
|
+
*/
|
|
27
|
+
// alloc-ok: lifecycle-edge — the scan runs once per animate command, never per frame.
|
|
28
|
+
export function calcIterationDurationMs(generator) {
|
|
29
|
+
let duration = 0;
|
|
30
|
+
let state = generator.sample(duration);
|
|
31
|
+
while (!state.done && duration < MAX_ITERATION_DURATION_MS) {
|
|
32
|
+
duration += ITERATION_SCAN_STEP_MS;
|
|
33
|
+
state = generator.sample(duration);
|
|
34
|
+
}
|
|
35
|
+
return duration >= MAX_ITERATION_DURATION_MS ? Infinity : duration;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
|
|
39
|
+
* length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
|
|
40
|
+
* pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
|
|
41
|
+
* the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
|
|
42
|
+
* `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
|
|
43
|
+
* EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
|
|
44
|
+
* F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
|
|
45
|
+
* way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
|
|
46
|
+
* divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
|
|
47
|
+
* crosses to the UI runtime, where a throw is a bare `std::terminate`).
|
|
48
|
+
*/
|
|
49
|
+
// alloc-ok: lifecycle-edge — a refusal built at command/validation time, never per frame.
|
|
50
|
+
export function repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs = 0) {
|
|
51
|
+
if (repeat <= 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (iterationDurationMs <= 0 && repeatDelayMs <= 0) {
|
|
54
|
+
return new Error('a repeated transition cannot fold a zero-length iteration — `repeat` divides by the ' +
|
|
55
|
+
"iteration's duration, so a zero-duration play repeats nothing (the pinned Motion " +
|
|
56
|
+
'silently holds the final keyframe instead). Give the transition a duration, or drop ' +
|
|
57
|
+
'`repeat` (REQ-TIMING-003, T18-a L3).');
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isFinite(iterationDurationMs)) {
|
|
60
|
+
return new Error(`a repeated transition cannot fold an iteration that never settles — this trajectory is ` +
|
|
61
|
+
`still moving after ${MAX_ITERATION_DURATION_MS}ms, past the point where the pinned ` +
|
|
62
|
+
'Motion stops measuring it and starts emitting NaN. Bound the transition (duration, or ' +
|
|
63
|
+
'stiffer physics), or drop `repeat` (REQ-TIMING-003, T18-a L3).');
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export function iterationMeasurementLane(targetIsNumeric, isExplicitSpring, isTween) {
|
|
68
|
+
// A keyframe ARRAY target is played by the keyframes generator unless the transition explicitly
|
|
69
|
+
// asked for a spring — so an untyped or tween-typed transition changes lane with the target.
|
|
70
|
+
if (!targetIsNumeric && !isExplicitSpring)
|
|
71
|
+
return 'keyframes';
|
|
72
|
+
if (isTween)
|
|
73
|
+
return 'timing';
|
|
74
|
+
return 'spring';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
|
|
78
|
+
*
|
|
79
|
+
* `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
|
|
80
|
+
* reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
|
|
81
|
+
* instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
|
|
82
|
+
* construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
|
|
83
|
+
* anyway, so the class identity could not survive the crossing even if it could be built.
|
|
84
|
+
*
|
|
85
|
+
* What CAN be identical across the crossing is exactly what the crossing carries — the name and the
|
|
86
|
+
* message. Both backends therefore build them here, so a developer sees one refusal whichever
|
|
87
|
+
* engine they are on, and `repeatRefusalParity` pins the two against each other.
|
|
88
|
+
*/
|
|
89
|
+
export const REPEAT_REFUSAL_NAME = 'InvalidTransitionError';
|
|
90
|
+
/** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
|
|
91
|
+
export const REPEAT_REFUSAL_KEY = 'repeat';
|
|
92
|
+
/**
|
|
93
|
+
* A fold refusal, as a CONSUMER can recognize it after any transport.
|
|
94
|
+
*
|
|
95
|
+
* The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
|
|
96
|
+
* which is decidable only on the reference backend: the UI runtime cannot construct a core class,
|
|
97
|
+
* and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
|
|
98
|
+
* and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
|
|
99
|
+
* available across the crossing, so the recognition contract cannot be built on it — it is
|
|
100
|
+
* STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
|
|
101
|
+
* whole identity is one thing that one floor can hold.
|
|
102
|
+
*
|
|
103
|
+
* `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
|
|
104
|
+
* marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
|
|
105
|
+
* consumer applies to the rethrown error rather than a second copy that can drift.
|
|
106
|
+
*/
|
|
107
|
+
export function isRepeatFoldRefusalIdentity(name, key) {
|
|
108
|
+
return name === REPEAT_REFUSAL_NAME && key === REPEAT_REFUSAL_KEY;
|
|
109
|
+
}
|
|
110
|
+
export function isRepeatFoldRefusal(error) {
|
|
111
|
+
if (typeof error !== 'object' || error === null)
|
|
112
|
+
return false;
|
|
113
|
+
const candidate = error;
|
|
114
|
+
return isRepeatFoldRefusalIdentity(candidate.name, candidate.key);
|
|
115
|
+
}
|
|
116
|
+
// Mirrors `InvalidTransitionError`'s message format for an anonymous component with a numeric
|
|
117
|
+
// value. It is duplicated rather than imported because `validate.ts` is JS-side-only (it reaches
|
|
118
|
+
// the subset registry and the value-type parsers) and this module crosses to the UI runtime. The
|
|
119
|
+
// duplication is held honest by the cross-backend floor, not by review.
|
|
120
|
+
// alloc-ok: lifecycle-edge — a refusal built at command time, never per frame.
|
|
121
|
+
export function repeatRefusalMessage(repeat, reason) {
|
|
122
|
+
return `<Motion.View>: invalid transition option 'repeat' = ${String(repeat)} — ${reason}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
|
|
126
|
+
* delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
|
|
127
|
+
* the animation past its final one.
|
|
128
|
+
*/
|
|
129
|
+
// alloc-ok: lifecycle-edge — geometry resolved once per animate command.
|
|
130
|
+
export function repeatFoldGeometry(config) {
|
|
131
|
+
const resolvedDurationMs = config.iterationDurationMs + config.repeatDelayMs;
|
|
132
|
+
return {
|
|
133
|
+
resolvedDurationMs,
|
|
134
|
+
totalDurationMs: resolvedDurationMs * (config.repeat + 1) - config.repeatDelayMs,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function clamp01(value) {
|
|
138
|
+
return value > 1 ? 1 : value < 0 ? 0 : value;
|
|
139
|
+
}
|
|
140
|
+
// DECLARATION ORDER IS LOAD-BEARING in this module: it ships FILE-tagged, so the workletizer
|
|
141
|
+
// emits every function as a non-hoisted const and a worklet may only reference worklets declared
|
|
142
|
+
// ABOVE it. `buildRepeatedGenerator` calls this, so this comes first. Pinned by
|
|
143
|
+
// check-worklet-forward-refs.test.ts.
|
|
144
|
+
/**
|
|
145
|
+
* Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
|
|
146
|
+
* other — the driver steps it without knowing it repeats.
|
|
147
|
+
*
|
|
148
|
+
* Two laws diverge deliberately from a naive port of the pin's `tick`:
|
|
149
|
+
*
|
|
150
|
+
* - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
|
|
151
|
+
* velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
|
|
152
|
+
* whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
|
|
153
|
+
* construction even though its generator's internal velocity on a reversed leg is not.
|
|
154
|
+
* Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
|
|
155
|
+
* - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
|
|
156
|
+
* settles once per iteration; only the last one ends the animation, and an endless repeat is
|
|
157
|
+
* never done at all (packet L5 — such an element intentionally never quiesces).
|
|
158
|
+
*/
|
|
159
|
+
// alloc-ok: lifecycle-edge — construction resolves geometry and builds the ONE reused sample
|
|
160
|
+
// record; sample() below mutates it and allocates nothing (REQ-DRIVER-021).
|
|
161
|
+
export function repeatGenerator(base, config) {
|
|
162
|
+
const { repeat, leg, repeatDelayMs, iterationDurationMs } = config;
|
|
163
|
+
const { resolvedDurationMs, totalDurationMs } = repeatFoldGeometry({
|
|
164
|
+
repeat,
|
|
165
|
+
repeatDelayMs,
|
|
166
|
+
iterationDurationMs,
|
|
167
|
+
});
|
|
168
|
+
const mirrored = leg.type === 'mirror' ? leg.mirrored : null;
|
|
169
|
+
const isReverse = leg.type === 'reverse';
|
|
170
|
+
// The pin's terminal keyframe rule (motion-dom keyframes/get-final.ts), reduced to the only case
|
|
171
|
+
// where it disagrees with where this fold naturally lands:
|
|
172
|
+
//
|
|
173
|
+
// useFirstKeyframe = speed < 0 || (repeat && repeatType !== 'loop' && repeat % 2 === 1)
|
|
174
|
+
//
|
|
175
|
+
// `repeat % 2 === 1` selects exactly the ODD INTEGERS, and `repeatType !== 'loop'` puts BOTH
|
|
176
|
+
// `reverse` and `mirror` in that branch. An integer fold already ENDS on the pin's keyframe by
|
|
177
|
+
// construction — a whole-number progress is corrected to `iterationProgress` 1; a `reverse` leg
|
|
178
|
+
// reflects that to 0 and a `mirror` leg samples the mirrored trajectory at ITS end, which is the
|
|
179
|
+
// base origin, so both odd cases report the pin's FIRST keyframe while a `loop` leg and every
|
|
180
|
+
// EVEN case hold at 1 and report the pin's LAST. So the integer path needs no snap and must not
|
|
181
|
+
// get one. (Measured on the pin, `[0,100]` and `[0,100,30]`, repeats 1-4 × all three types: odd
|
|
182
|
+
// reverse and odd mirror both land on FIRST, everything else on LAST.) A FRACTIONAL repeat is
|
|
183
|
+
// the gap: at forward speed it satisfies no branch of the rule, so the pin commits the LAST keyframe while this
|
|
184
|
+
// fold is mid-play. `speed < 0` has no native producer (drivers accumulate non-negative elapsed
|
|
185
|
+
// only), so that half of the rule is stated and not implemented.
|
|
186
|
+
const snapsAtTerminal = Number.isFinite(repeat) && repeat > 0 && !Number.isInteger(repeat);
|
|
187
|
+
// The pin's snap is NOT part of the fold: `JSAnimation.tick` computes the fold, then applies
|
|
188
|
+
// `getFinalKeyframe` under its `isAnimationFinished` gate
|
|
189
|
+
// (`holdTime === null && (state === 'finished' || (state === 'running' && done))`), and only then
|
|
190
|
+
// calls `finish()` — which never touches the value. A FRESH, non-autoplaying instance never
|
|
191
|
+
// satisfies that gate: it is `paused`, so `holdTime` is non-null and neither disjunct holds, and
|
|
192
|
+
// its `sample()` returns the raw fold at EVERY time. An already-FINISHED instance takes the
|
|
193
|
+
// override at ANY sampled time, mid-play included; a RUNNING one only at or past `totalDuration`,
|
|
194
|
+
// because that disjunct also requires `done`. Measured on `repeat: 1.5 reverse`, whose raw fold is
|
|
195
|
+
// 80 mid-play and 50 at the terminal — paused 80/50, running 80/100, finished 100/100.
|
|
196
|
+
//
|
|
197
|
+
// `gen-repeat-goldens.mjs` builds a FRESH `autoplay: false` instance per sample, so the golden
|
|
198
|
+
// records both layers: `samples` are raw-fold seeks (50 at the terminal) and `terminalValue` comes
|
|
199
|
+
// from playing to the end (100). Keeping `sample` seek-faithful and exposing the commit separately
|
|
200
|
+
// reproduces that split instead of collapsing it; putting the snap inside `sample` broke the
|
|
201
|
+
// pinned `tween-reverse-fractional` samples, which is the golden reporting the wrong layer (H2 F1).
|
|
202
|
+
//
|
|
203
|
+
// The value is the FORWARD base at its end, never the leg the partial play was on: the pin
|
|
204
|
+
// commits `resolvedKeyframes[length - 1]` whatever direction it was travelling, so a `reverse` or
|
|
205
|
+
// `mirror` fold descending toward the origin jumps UP to the target. Core's generators are pure
|
|
206
|
+
// functions of elapsed, so sampling one here is safe and allocation-free at the frame path.
|
|
207
|
+
const finalValue = snapsAtTerminal ? base.sample(resolvedDurationMs).value : undefined;
|
|
208
|
+
// The delay's share of one resolved iteration — the reverse leg's re-bias factor, hoisted so the
|
|
209
|
+
// frame path does no division beyond the progress one.
|
|
210
|
+
const delayFraction = repeatDelayMs / resolvedDurationMs;
|
|
211
|
+
const out = { value: 0, velocity: 0, done: false };
|
|
212
|
+
return {
|
|
213
|
+
finalValue,
|
|
214
|
+
sample(rawElapsedMs) {
|
|
215
|
+
// The pin clamps before folding (`JSAnimation.tick`: `Math.max(timeWithoutDelay, 0)`), and so
|
|
216
|
+
// must this: `floor` of a negative progress is -1, an ODD iteration, so a `reverse` leg would
|
|
217
|
+
// reflect and report the far endpoint at a moment the animation has not begun. Today's
|
|
218
|
+
// drivers only ever accumulate non-negative deltas, but the pin's one producer of negative
|
|
219
|
+
// elapsed is `currentTime - delay` — property-lane `delay`, this packet's successor rung.
|
|
220
|
+
// `Math.max` rather than `rawElapsedMs > 0 ? rawElapsedMs : 0` — they agree on every number
|
|
221
|
+
// and disagree on NaN, which the comparison silently maps to 0 (reporting the trajectory's
|
|
222
|
+
// ORIGIN for a broken clock) while `Math.max` propagates it. A NaN clock is a programmer
|
|
223
|
+
// error, and it must stay loud rather than resolve to a plausible-looking value (review
|
|
224
|
+
// MINOR).
|
|
225
|
+
const elapsedMs = Math.max(rawElapsedMs, 0);
|
|
226
|
+
let elapsed = elapsedMs;
|
|
227
|
+
let frameGenerator = base;
|
|
228
|
+
let reversedLeg = false;
|
|
229
|
+
if (repeat > 0) {
|
|
230
|
+
// Progress across the WHOLE fold in iteration units: 2.5 is halfway through the third play.
|
|
231
|
+
const progress = Math.min(elapsedMs, totalDurationMs) / resolvedDurationMs;
|
|
232
|
+
let currentIteration = Math.floor(progress);
|
|
233
|
+
let iterationProgress = progress % 1;
|
|
234
|
+
// A whole-number progress at or past the first boundary is the END of the PREVIOUS
|
|
235
|
+
// iteration, not the start of the next — otherwise every boundary flickers a frame of the
|
|
236
|
+
// successor's origin.
|
|
237
|
+
if (iterationProgress === 0 && progress >= 1) {
|
|
238
|
+
iterationProgress = 1;
|
|
239
|
+
currentIteration--;
|
|
240
|
+
}
|
|
241
|
+
if (currentIteration > repeat + 1)
|
|
242
|
+
currentIteration = repeat + 1;
|
|
243
|
+
if (currentIteration % 2 !== 0) {
|
|
244
|
+
if (isReverse) {
|
|
245
|
+
iterationProgress = 1 - iterationProgress;
|
|
246
|
+
// Reflecting progress would put the dead time at the START of the reversed play; this
|
|
247
|
+
// subtraction moves it back to the END, so the rule stays uniform across repeat types:
|
|
248
|
+
// every play holds on the value it FINISHED on. A reversed leg therefore begins
|
|
249
|
+
// descending immediately from the iteration's end value and pauses at its start value.
|
|
250
|
+
if (repeatDelayMs > 0)
|
|
251
|
+
iterationProgress -= delayFraction;
|
|
252
|
+
reversedLeg = true;
|
|
253
|
+
}
|
|
254
|
+
else if (mirrored !== null) {
|
|
255
|
+
frameGenerator = mirrored;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
elapsed = clamp01(iterationProgress) * resolvedDurationMs;
|
|
259
|
+
}
|
|
260
|
+
const sample = frameGenerator.sample(elapsed);
|
|
261
|
+
out.value = sample.value;
|
|
262
|
+
out.velocity = reversedLeg ? -sample.velocity : sample.velocity;
|
|
263
|
+
out.done = elapsedMs >= totalDurationMs;
|
|
264
|
+
return out;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build the folded generator for a repeated transition — the ONE implementation every driver seam
|
|
270
|
+
* calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
|
|
271
|
+
* skew: the fix-up review found both backends sharing it while both omitted it from retarget.
|
|
272
|
+
* Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
|
|
273
|
+
* only what it alone knows:
|
|
274
|
+
*
|
|
275
|
+
* - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
|
|
276
|
+
* the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
|
|
277
|
+
* mirror fold without one stays unrepresentable.
|
|
278
|
+
* - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
|
|
279
|
+
* a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
|
|
280
|
+
* null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
|
|
281
|
+
* is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
|
|
282
|
+
* zero-length plays (transition-default-selection F5) — scanning the base instead would invent
|
|
283
|
+
* a 50ms iteration the pin does not have.
|
|
284
|
+
*
|
|
285
|
+
* Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
|
|
286
|
+
* function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
|
|
287
|
+
* fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
|
|
288
|
+
*
|
|
289
|
+
* Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
|
|
290
|
+
* caller routes that refusal through its severity boundary — this never throws, because it runs on
|
|
291
|
+
* the UI runtime where a throw is a bare `std::terminate`.
|
|
292
|
+
*/
|
|
293
|
+
// alloc-ok: lifecycle-edge — one fold construction per animate command, never per frame.
|
|
294
|
+
export function buildRepeatedGenerator(repeat, leg, repeatDelayMs, base, knownIterationDurationMs) {
|
|
295
|
+
const iterationDurationMs = knownIterationDurationMs ?? calcIterationDurationMs(base);
|
|
296
|
+
const refusal = repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs);
|
|
297
|
+
if (refusal !== null)
|
|
298
|
+
return refusal;
|
|
299
|
+
return repeatGenerator(base, { repeat, leg, repeatDelayMs, iterationDurationMs });
|
|
300
|
+
}
|