@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
  7. package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
  8. package/package.json +57 -0
  9. package/react-native.config.js +14 -0
  10. package/src/native/driver/instantWindow.ts +31 -0
  11. package/src/native/driver/strictModeReplay.ts +20 -0
  12. package/src/native/driver/uiLayoutEngine.ts +845 -0
  13. package/src/native/driver/uiLayoutGraph.ts +338 -0
  14. package/src/native/driver/uiValueChannel.ts +4640 -0
  15. package/src/native/driver/workletDriver.ts +3860 -0
  16. package/src/native/motion/AnimatePresence.tsx +1605 -0
  17. package/src/native/motion/LayoutGroup.tsx +163 -0
  18. package/src/native/motion/MotionConfig.tsx +400 -0
  19. package/src/native/motion/MotionRoot.tsx +250 -0
  20. package/src/native/motion/MotionView.tsx +6761 -0
  21. package/src/native/motion/addScaleCorrector.ts +97 -0
  22. package/src/native/motion/colorEndpointUiFeed.ts +32 -0
  23. package/src/native/motion/colorProjection.ts +39 -0
  24. package/src/native/motion/composeTransform.ts +271 -0
  25. package/src/native/motion/constraintMeasure.ts +157 -0
  26. package/src/native/motion/deferredPendingSnapshots.ts +25 -0
  27. package/src/native/motion/discreteProjection.ts +36 -0
  28. package/src/native/motion/dragAncestorPanContext.ts +28 -0
  29. package/src/native/motion/dragControls.ts +152 -0
  30. package/src/native/motion/dragGestureWiring.ts +1168 -0
  31. package/src/native/motion/dragHandoffBinding.ts +179 -0
  32. package/src/native/motion/dragHubStream.ts +756 -0
  33. package/src/native/motion/dragPropagationLock.ts +57 -0
  34. package/src/native/motion/driverValueChannel.ts +3288 -0
  35. package/src/native/motion/externalDragDriver.ts +214 -0
  36. package/src/native/motion/frameData.ts +31 -0
  37. package/src/native/motion/gestureBinding.ts +107 -0
  38. package/src/native/motion/gestureStateGate.ts +563 -0
  39. package/src/native/motion/gestureStateResolver.ts +286 -0
  40. package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
  41. package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
  42. package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
  43. package/src/native/motion/imperativeAnimate.ts +816 -0
  44. package/src/native/motion/keyframeTiming.ts +7 -0
  45. package/src/native/motion/layoutIdBinding.ts +261 -0
  46. package/src/native/motion/layoutIdFlightConfig.ts +51 -0
  47. package/src/native/motion/layoutProjection.ts +89 -0
  48. package/src/native/motion/layoutScroll.ts +204 -0
  49. package/src/native/motion/layoutTransition.ts +1302 -0
  50. package/src/native/motion/lengthLayoutContext.tsx +100 -0
  51. package/src/native/motion/lengthLayoutHost.ts +44 -0
  52. package/src/native/motion/mappedKeys.ts +184 -0
  53. package/src/native/motion/motionViewController.ts +3180 -0
  54. package/src/native/motion/nativeHostMarker.ts +22 -0
  55. package/src/native/motion/panSessionWiring.ts +127 -0
  56. package/src/native/motion/pathTransition.ts +101 -0
  57. package/src/native/motion/popLayout.ts +176 -0
  58. package/src/native/motion/presenceBinding.ts +382 -0
  59. package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
  60. package/src/native/motion/scrollValues.ts +260 -0
  61. package/src/native/motion/serializablePayload.ts +151 -0
  62. package/src/native/motion/severity.ts +16 -0
  63. package/src/native/motion/shippedSurface.ts +843 -0
  64. package/src/native/motion/staticLengthGate.ts +86 -0
  65. package/src/native/motion/styleBaseGate.ts +72 -0
  66. package/src/native/motion/styleValueBinding.ts +657 -0
  67. package/src/native/motion/systemGestureExclusion.ts +61 -0
  68. package/src/native/motion/tapGestureWiring.ts +215 -0
  69. package/src/native/motion/transformOrder.ts +38 -0
  70. package/src/native/motion/transformStringBinding.ts +200 -0
  71. package/src/native/motion/transformTemplateGate.ts +45 -0
  72. package/src/native/motion/transitionGate.ts +435 -0
  73. package/src/native/motion/useAnimate.ts +52 -0
  74. package/src/native/motion/useCycle.ts +44 -0
  75. package/src/native/motion/useInstantTransition.ts +68 -0
  76. package/src/native/motion/useReducedMotion.ts +58 -0
  77. package/src/native/motion/useScroll.ts +162 -0
  78. package/src/native/motion/useViewportScroll.ts +29 -0
  79. package/src/native/motion/valueChannel.ts +7121 -0
  80. package/src/native/motion/valueHooks.ts +901 -0
  81. package/src/native/motion/variantChildRegistry.ts +67 -0
  82. package/src/native/motion/variantContext.tsx +172 -0
  83. package/src/native/motion/variantProps.ts +598 -0
  84. package/src/native.ts +159 -0
  85. package/src/verification/harnessMetrics.ts +78 -0
  86. package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
  87. package/src/verification/probe/WorkletParityProbe.tsx +162 -0
  88. package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
  89. package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
  90. package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
  91. package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
  92. package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
  93. package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
  94. package/src/verification/screens/ConformanceScreen.tsx +573 -0
  95. package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
  96. package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
  97. package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
  98. package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
  99. package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
  100. package/src/verification/screens/GestureChecksScreen.tsx +685 -0
  101. package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
  102. package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
  103. package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
  104. package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
  105. package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
  106. package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
  107. package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
  108. package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
  109. package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
  110. package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
  111. package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
  112. package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
  113. package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
  114. package/src/verification/screens/conformanceBanner.ts +21 -0
  115. package/src/verification/screens/proofConsoleTap.ts +18 -0
  116. package/src/verification.ts +47 -0
  117. 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
+ }