@unrulysystems/native-motion 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 +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
|
|
2
|
+
// The production UI-runtime graph adapter for the layout-identity engine
|
|
3
|
+
// (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md §The UI-runtime graph adapter). The registry/session
|
|
4
|
+
// need a MotionGraph host for their rect channels + crossfade scalar; the class-based core graph
|
|
5
|
+
// must not cross onto the UI runtime (spike Residual Segfault Risks), so this plain-object graph
|
|
6
|
+
// is constructed and stepped entirely UI-side by the driver's frame loop. RN-free on purpose: the
|
|
7
|
+
// 'worklet' directives are inert on the JS thread, so vitest exercises the SAME functions the UI
|
|
8
|
+
// runtime executes. Function-level directives are babel-workletized per-function in app source
|
|
9
|
+
// (the motion/colorProjection.ts precedent, device-proven).
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DT_CLAMP_MS,
|
|
13
|
+
MAX_VELOCITY_DELTA_MS,
|
|
14
|
+
type ActivityHandle,
|
|
15
|
+
type Animation,
|
|
16
|
+
type Generator,
|
|
17
|
+
type GeneratorFactory,
|
|
18
|
+
type MotionGraph,
|
|
19
|
+
type MotionValue,
|
|
20
|
+
type MotionValueEvent,
|
|
21
|
+
type ValueChangeListener,
|
|
22
|
+
} from '@unrulysystems/native-motion-core/worklet-layout'
|
|
23
|
+
|
|
24
|
+
interface UiLayoutAnimation extends Animation {
|
|
25
|
+
done: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Ledger/ownership teardown with or without the end-of-animation velocity cleanup (r19):
|
|
28
|
+
* interruption carries velocity into the successor seed, so it finishes WITHOUT cleanup;
|
|
29
|
+
* a direct stop() cleans up (r17).
|
|
30
|
+
*/
|
|
31
|
+
__finish(runEndCleanup: boolean): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface UiLayoutMotionValue extends MotionValue<number> {
|
|
35
|
+
__active: UiLayoutAnimation | null
|
|
36
|
+
__applyAnimationSample(value: number, velocity: number): void
|
|
37
|
+
__endAnimation(): void
|
|
38
|
+
/** Fire a lifecycle event on this value (r15 e18b6f40a2d7 — the union is EMITTED, not just accepted). */
|
|
39
|
+
__emit(event: 'animationStart' | 'animationComplete' | 'animationCancel' | 'destroy'): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface UiLayoutActiveAnimation {
|
|
43
|
+
readonly value: UiLayoutMotionValue
|
|
44
|
+
readonly generator: Generator
|
|
45
|
+
readonly handle: UiLayoutAnimation
|
|
46
|
+
elapsedMs: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The graph surface the layout engine steps: core's MotionGraph plus the driver-owned frame
|
|
50
|
+
// advance (the driver's useFrameCallback calls frame(dt); there is no scheduler in this graph by
|
|
51
|
+
// design — REQ-DRIVER-011 keeps the frame source singular and driver-owned).
|
|
52
|
+
export interface UiLayoutGraph extends MotionGraph {
|
|
53
|
+
frame(dtMs: number): void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// alloc-ok: construction — one motion value per projection channel, created at pairing time;
|
|
57
|
+
// the per-frame writers (__applyAnimationSample/set) run indexed with zero allocation.
|
|
58
|
+
function createUiLayoutMotionValue(initial: number, now: () => number): UiLayoutMotionValue {
|
|
59
|
+
'worklet'
|
|
60
|
+
let value = initial
|
|
61
|
+
// Core MotionValueImpl's EXACT sample-window model (r20 26d0a09db7f4 — a manual-write
|
|
62
|
+
// timestamp alone missed same-frame compounding, identical-write suppression, and the
|
|
63
|
+
// clock velocity of listener writes): the animated velocity takes precedence while a
|
|
64
|
+
// flight drives the value; manual writes roll the frame window only when time moved.
|
|
65
|
+
let animatedVelocity: number | null = null
|
|
66
|
+
let prev: number | null = null
|
|
67
|
+
let prevUpdatedAtMs: number | null = null
|
|
68
|
+
let updatedAtMs = now()
|
|
69
|
+
const listeners: Array<ValueChangeListener<number>> = []
|
|
70
|
+
// 'change' listeners take the latest value (the pinned payload); lifecycle listeners take
|
|
71
|
+
// none — one storage shape, lifecycle emission casts to the payload-free call.
|
|
72
|
+
const eventListeners: Record<MotionValueEvent, Array<(latest: number) => void>> = {
|
|
73
|
+
change: [],
|
|
74
|
+
animationStart: [],
|
|
75
|
+
animationComplete: [],
|
|
76
|
+
animationCancel: [],
|
|
77
|
+
destroy: [],
|
|
78
|
+
}
|
|
79
|
+
const mv: UiLayoutMotionValue = {
|
|
80
|
+
__active: null,
|
|
81
|
+
__applyAnimationSample(next, nextVelocity) {
|
|
82
|
+
value = next
|
|
83
|
+
animatedVelocity = nextVelocity
|
|
84
|
+
// Indexed: this runs per animation per vsync — for..of allocates its iterator on Hermes.
|
|
85
|
+
for (let i = 0; i < listeners.length; i++) listeners[i]!(value)
|
|
86
|
+
for (let i = 0; i < eventListeners.change.length; i++) eventListeners.change[i]!(value)
|
|
87
|
+
},
|
|
88
|
+
__endAnimation() {
|
|
89
|
+
// Core MotionValueImpl.endAnimation() verbatim (r20): clears the animated velocity AND
|
|
90
|
+
// resets the clock window — the registry's EPSILON_VEL settle floor clears, and no
|
|
91
|
+
// stale manual sample can synthesize a velocity against the flight's final value.
|
|
92
|
+
animatedVelocity = null
|
|
93
|
+
prev = null
|
|
94
|
+
prevUpdatedAtMs = null
|
|
95
|
+
updatedAtMs = now()
|
|
96
|
+
},
|
|
97
|
+
get: () => value,
|
|
98
|
+
set(next) {
|
|
99
|
+
// Core set() verbatim (r20): a manual write reverts to clock velocity; the frame window
|
|
100
|
+
// rolls only when time moved (same-frame writes COMPOUND); identical writes do not
|
|
101
|
+
// notify. The layout channels are session-driven — no gesture-lane write rides this.
|
|
102
|
+
animatedVelocity = null
|
|
103
|
+
const nowMs = now()
|
|
104
|
+
if (nowMs !== updatedAtMs) {
|
|
105
|
+
prev = value
|
|
106
|
+
prevUpdatedAtMs = updatedAtMs
|
|
107
|
+
updatedAtMs = nowMs
|
|
108
|
+
}
|
|
109
|
+
const previous = value
|
|
110
|
+
value = next
|
|
111
|
+
if (value !== previous) {
|
|
112
|
+
for (let i = 0; i < listeners.length; i++) listeners[i]!(value)
|
|
113
|
+
for (let i = 0; i < eventListeners.change.length; i++) eventListeners.change[i]!(value)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
jump(next) {
|
|
117
|
+
// Core jump() VERBATIM (r16/r17/r20): set (window semantics + identical-write
|
|
118
|
+
// suppression + clock velocity for the change listener) -> window reset -> stop whose
|
|
119
|
+
// cancellation reads 0 with the animation still observable.
|
|
120
|
+
mv.set(next)
|
|
121
|
+
prev = null
|
|
122
|
+
prevUpdatedAtMs = null
|
|
123
|
+
updatedAtMs = now()
|
|
124
|
+
mv.stop()
|
|
125
|
+
},
|
|
126
|
+
// alloc-ok: lifecycle-edge — listener registration; the unsubscriber's splice runs at detach.
|
|
127
|
+
subscribe(listener) {
|
|
128
|
+
listeners.push(listener)
|
|
129
|
+
// alloc-ok: lifecycle-edge — the unsubscriber runs at holder detach, never per frame.
|
|
130
|
+
return () => {
|
|
131
|
+
const at = listeners.indexOf(listener)
|
|
132
|
+
if (at >= 0) listeners.splice(at, 1)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
getVelocity: () => {
|
|
136
|
+
// Core getVelocity() verbatim: the animated velocity wins while a flight drives the
|
|
137
|
+
// value; otherwise the clock formula over the manual-write window, stale -> 0.
|
|
138
|
+
if (animatedVelocity !== null) return animatedVelocity
|
|
139
|
+
if (prev === null || prevUpdatedAtMs === null) return 0
|
|
140
|
+
if (now() - updatedAtMs > MAX_VELOCITY_DELTA_MS) return 0
|
|
141
|
+
const dtMs = Math.min(updatedAtMs - prevUpdatedAtMs, MAX_VELOCITY_DELTA_MS)
|
|
142
|
+
if (dtMs <= 0) return 0
|
|
143
|
+
return ((value - prev) * 1000) / dtMs
|
|
144
|
+
},
|
|
145
|
+
// alloc-ok: lifecycle-edge — event-listener registration; the unsubscriber runs at detach.
|
|
146
|
+
on(event, listener) {
|
|
147
|
+
eventListeners[event].push(listener)
|
|
148
|
+
// alloc-ok: lifecycle-edge — the unsubscriber runs at holder detach, never per frame.
|
|
149
|
+
return () => {
|
|
150
|
+
const at = eventListeners[event].indexOf(listener)
|
|
151
|
+
if (at >= 0) eventListeners[event].splice(at, 1)
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
// Liveness IS the __active handle (set at animate, nulled at stop and natural completion).
|
|
155
|
+
isAnimating: () => mv.__active !== null,
|
|
156
|
+
// Lifecycle emission (r15 e18b6f40a2d7): indexed — completion emits inside the vsync loop.
|
|
157
|
+
__emit(event) {
|
|
158
|
+
const list = eventListeners[event]
|
|
159
|
+
for (let i = 0; i < list.length; i++) (list[i] as () => void)()
|
|
160
|
+
},
|
|
161
|
+
stop() {
|
|
162
|
+
if (mv.__active !== null) {
|
|
163
|
+
// Pinned ORDER (r17): the cancellation is notified while the animation is still
|
|
164
|
+
// observable; the handle's ledger/liveness/velocity cleanup runs after.
|
|
165
|
+
mv.__emit('animationCancel')
|
|
166
|
+
mv.__active.stop()
|
|
167
|
+
mv.__active = null
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
destroy() {
|
|
171
|
+
// Pinned teardown ORDER (r16 major 1): destroy announces FIRST (the listener observes
|
|
172
|
+
// the live animation), then the SILENT stop — never an animationCancel from teardown.
|
|
173
|
+
mv.__emit('destroy')
|
|
174
|
+
if (mv.__active !== null) {
|
|
175
|
+
mv.__active.stop()
|
|
176
|
+
mv.__active = null
|
|
177
|
+
}
|
|
178
|
+
listeners.length = 0
|
|
179
|
+
eventListeners.change.length = 0
|
|
180
|
+
eventListeners.animationStart.length = 0
|
|
181
|
+
eventListeners.animationComplete.length = 0
|
|
182
|
+
eventListeners.animationCancel.length = 0
|
|
183
|
+
eventListeners.destroy.length = 0
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
return mv
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// alloc-ok: construction — one graph per layout slot; the per-frame surface is frame()/
|
|
190
|
+
// isSettled()/activeAnimationCount(), which allocate nothing.
|
|
191
|
+
export function createUiLayoutGraph(): UiLayoutGraph {
|
|
192
|
+
'worklet'
|
|
193
|
+
const animations: UiLayoutActiveAnimation[] = []
|
|
194
|
+
let openActivities = 0
|
|
195
|
+
// The graph's frame clock (r19): RAW frame time accumulated like core's ManualClock — the
|
|
196
|
+
// manual-write velocity window reads it; animations still advance by CLAMPED dt.
|
|
197
|
+
let nowMs = 0
|
|
198
|
+
// Live (not-done) animation count, maintained at the edges that flip `done` — the ONLY two
|
|
199
|
+
// writers are handle.stop() and frame()'s natural completion, both latched on !done so a
|
|
200
|
+
// stop-then-frame or double-stop decrements exactly once. isSettled() is the hottest read on
|
|
201
|
+
// the frame path (per active id and per painted holder through the session's sync()), so it
|
|
202
|
+
// must be O(1) with zero allocation — a scan here made settle checks O(animations) each and
|
|
203
|
+
// the frame quadratic across concurrent identities (REQ-DRIVER-015, review round 6 major 1).
|
|
204
|
+
let liveAnimations = 0
|
|
205
|
+
|
|
206
|
+
const graph: UiLayoutGraph = {
|
|
207
|
+
motionValue<T = number>(initial: T): MotionValue<T> {
|
|
208
|
+
// alloc-ok: lifecycle-edge — the clock-read closure is created once per VALUE at
|
|
209
|
+
// pairing time (the factory never runs on the vsync path).
|
|
210
|
+
return createUiLayoutMotionValue(initial as number, () => nowMs) as unknown as MotionValue<T>
|
|
211
|
+
},
|
|
212
|
+
derived() {
|
|
213
|
+
// ui-throw-ok: assert — a programmer-error refusal for an API outside the layout-engine
|
|
214
|
+
// crossing; no production caller (a dev-only trap, never a UI delivery).
|
|
215
|
+
throw new Error(
|
|
216
|
+
'uiLayoutGraph: derived values are not part of the layout engine crossing (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md).',
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
// alloc-ok: lifecycle-edge — flight start/retarget (session.springTo), never per frame.
|
|
220
|
+
animate(value: MotionValue<number>, factory: GeneratorFactory): Animation {
|
|
221
|
+
const motionValue = value as UiLayoutMotionValue
|
|
222
|
+
// Interruption ORDER (r19/r20, core-graph parity): the outgoing handle LATCHES first
|
|
223
|
+
// (core's markFinished-before-notify — a reentrant listener stop() hits the
|
|
224
|
+
// exactly-once latch, never a null dereference), the cancellation is notified while
|
|
225
|
+
// liveness is still observable (a valid listener may write the value), ownership
|
|
226
|
+
// clears, and only THEN does the successor seed read the CURRENT state
|
|
227
|
+
// (REQ-CORE-002/REQ-LAYOUT-018 continuity — including the listener write's clock
|
|
228
|
+
// velocity, which core's set() law now computes here too).
|
|
229
|
+
const outgoing = motionValue.__active
|
|
230
|
+
if (outgoing !== null && !outgoing.done) {
|
|
231
|
+
// Core's markFinished shape EXACTLY (r21): only the exactly-once latch flips before
|
|
232
|
+
// the notification — the listener still observes a LIVE graph (isAnimating true,
|
|
233
|
+
// count 1, unsettled). Ledger and ownership clear AFTER, in a finally (r22): a
|
|
234
|
+
// THROWING listener propagates loud but must never wedge the settle edge — frame()
|
|
235
|
+
// compacts done handles by ledger, so an unrepaired decrement is permanent.
|
|
236
|
+
outgoing.done = true
|
|
237
|
+
try {
|
|
238
|
+
motionValue.__emit('animationCancel')
|
|
239
|
+
} finally {
|
|
240
|
+
liveAnimations -= 1
|
|
241
|
+
if (motionValue.__active === outgoing) motionValue.__active = null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const generator = factory({ from: motionValue.get(), velocity: motionValue.getVelocity() })
|
|
245
|
+
const handle: UiLayoutAnimation = {
|
|
246
|
+
done: false,
|
|
247
|
+
__finish(runEndCleanup) {
|
|
248
|
+
if (handle.done) return // exactly-once: a second finish must not skew the ledger
|
|
249
|
+
handle.done = true
|
|
250
|
+
liveAnimations -= 1
|
|
251
|
+
// Ownership coherence (r16 major 3): __active clears either way; the
|
|
252
|
+
// end-of-animation velocity cleanup (r17) runs ONLY outside interruption — a
|
|
253
|
+
// retarget carries velocity into the successor seed (r18/r19).
|
|
254
|
+
if (motionValue.__active === handle) {
|
|
255
|
+
motionValue.__active = null
|
|
256
|
+
if (runEndCleanup) motionValue.__endAnimation()
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
stop() {
|
|
260
|
+
handle.__finish(true)
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
motionValue.__active = handle
|
|
264
|
+
animations.push({ value: motionValue, generator, handle, elapsedMs: 0 })
|
|
265
|
+
liveAnimations += 1
|
|
266
|
+
// Core parity (graph.animate): the start event fires after the stopper is installed.
|
|
267
|
+
motionValue.__emit('animationStart')
|
|
268
|
+
return handle
|
|
269
|
+
},
|
|
270
|
+
activeAnimationCount: () => liveAnimations,
|
|
271
|
+
// alloc-ok: lifecycle-edge — measure-wait handle creation at the fail-closed edge.
|
|
272
|
+
registerActivity(): ActivityHandle {
|
|
273
|
+
openActivities += 1
|
|
274
|
+
let released = false
|
|
275
|
+
return {
|
|
276
|
+
release() {
|
|
277
|
+
if (released) return
|
|
278
|
+
released = true
|
|
279
|
+
openActivities -= 1
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
isSettled: () => liveAnimations === 0 && openActivities === 0,
|
|
284
|
+
whenSettled() {
|
|
285
|
+
// ui-throw-ok: assert — a programmer-error refusal for an API outside the layout-engine
|
|
286
|
+
// crossing; no production caller (a dev-only trap, never a UI delivery).
|
|
287
|
+
throw new Error(
|
|
288
|
+
'uiLayoutGraph: whenSettled is not part of the layout engine crossing — the driver frame loop owns the settle edge (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md).',
|
|
289
|
+
)
|
|
290
|
+
},
|
|
291
|
+
frame(dtMs) {
|
|
292
|
+
// REQ-CLOCK-003 (core graph semantics, packages/core/src/graph.ts): clamp per-frame elapsed
|
|
293
|
+
// so a large gap after backgrounding does not jump the flight forward unbounded — the live
|
|
294
|
+
// driver feeds real vsync deltas here (review round 2026-07-09: frame(5000) completed a
|
|
295
|
+
// spring in one step while element-driver props stayed clamped inside stepProp).
|
|
296
|
+
// Hot-path discipline (REQ-DRIVER-015): completion handled inline (an animation's end only
|
|
297
|
+
// touches its own value) and finished entries compacted by write index — no completed
|
|
298
|
+
// snapshot array, no splice garbage, zero allocation per vsync (review round 4 major 6).
|
|
299
|
+
nowMs += dtMs
|
|
300
|
+
const clampedDt = dtMs > DT_CLAMP_MS ? DT_CLAMP_MS : dtMs
|
|
301
|
+
let anyDone = false
|
|
302
|
+
// Indexed loop: for..of allocates its iterator on Hermes — banned on the vsync path.
|
|
303
|
+
for (let i = 0; i < animations.length; i++) {
|
|
304
|
+
const animation = animations[i]!
|
|
305
|
+
if (animation.handle.done) {
|
|
306
|
+
anyDone = true // a stop() between frames leaves a done entry to compact below
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
animation.elapsedMs += clampedDt
|
|
310
|
+
const sample = animation.generator.sample(animation.elapsedMs)
|
|
311
|
+
animation.value.__applyAnimationSample(sample.value, sample.velocity)
|
|
312
|
+
if (sample.done) {
|
|
313
|
+
animation.handle.done = true // this branch only reaches !done entries — one decrement
|
|
314
|
+
liveAnimations -= 1
|
|
315
|
+
if (animation.value.__active === animation.handle) {
|
|
316
|
+
animation.value.__active = null
|
|
317
|
+
animation.value.__endAnimation()
|
|
318
|
+
animation.value.__emit('animationComplete')
|
|
319
|
+
}
|
|
320
|
+
anyDone = true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Compact finished entries so a long-lived driver graph does not grow per flight.
|
|
324
|
+
if (anyDone) {
|
|
325
|
+
let w = 0
|
|
326
|
+
for (let i = 0; i < animations.length; i++) {
|
|
327
|
+
const animation = animations[i]!
|
|
328
|
+
if (!animation.handle.done) {
|
|
329
|
+
animations[w] = animation
|
|
330
|
+
w += 1
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
animations.length = w
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
return graph
|
|
338
|
+
}
|