react-native-smooth-clip-view 0.2.8 → 0.3.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 (132) hide show
  1. package/README.md +86 -21
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +14 -2
  4. package/android/proguard-rules.pro +3 -3
  5. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  6. package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
  7. package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
  8. package/android/src/main/cpp/SmoothClipTrace.h +25 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  14. package/cpp/SmoothClipAnimationCurve.h +151 -122
  15. package/cpp/SmoothClipRegistry.h +102 -4
  16. package/cpp/SmoothClipSharedGeometry.h +146 -7
  17. package/cpp/SmoothClipVelocityTracker.h +13 -9
  18. package/ios/SmoothClipGeometry.h +184 -6
  19. package/ios/SmoothClipRegistry.mm +1033 -25
  20. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  21. package/ios/SmoothClipTurboModule.cpp +857 -2
  22. package/ios/SmoothClipTurboModule.h +310 -0
  23. package/ios/SmoothClipView.mm +714 -183
  24. package/ios/SmoothClipViewRegistry.h +1 -0
  25. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  26. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  27. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  28. package/lib/module/SmoothClipView.ios.js +22 -6
  29. package/lib/module/SmoothClipView.ios.js.map +1 -1
  30. package/lib/module/SmoothClipView.js +1 -46
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  33. package/lib/module/capabilities.js +4 -0
  34. package/lib/module/capabilities.js.map +1 -0
  35. package/lib/module/capabilities.native.js +36 -0
  36. package/lib/module/capabilities.native.js.map +1 -0
  37. package/lib/module/capabilityTypes.js +11 -0
  38. package/lib/module/capabilityTypes.js.map +1 -0
  39. package/lib/module/driverState.js +0 -28
  40. package/lib/module/driverState.js.map +1 -1
  41. package/lib/module/drivers.js +1 -281
  42. package/lib/module/drivers.js.map +1 -1
  43. package/lib/module/drivers.native.js +340 -75
  44. package/lib/module/drivers.native.js.map +1 -1
  45. package/lib/module/geometry.js +147 -17
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/groupDriverTypes.js +4 -0
  48. package/lib/module/groupDriverTypes.js.map +1 -0
  49. package/lib/module/groupDrivers.android.js +4 -0
  50. package/lib/module/groupDrivers.android.js.map +1 -0
  51. package/lib/module/groupDrivers.ios.js +4 -0
  52. package/lib/module/groupDrivers.ios.js.map +1 -0
  53. package/lib/module/groupDrivers.js +4 -0
  54. package/lib/module/groupDrivers.js.map +1 -0
  55. package/lib/module/groupDrivers.native.js +799 -0
  56. package/lib/module/groupDrivers.native.js.map +1 -0
  57. package/lib/module/index.js +3 -2
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/presentationProtocol.js +15 -0
  60. package/lib/module/presentationProtocol.js.map +1 -0
  61. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  62. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  67. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  68. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  69. package/lib/typescript/src/capabilities.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  72. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  73. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  74. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/driverState.d.ts +1 -6
  76. package/lib/typescript/src/driverState.d.ts.map +1 -1
  77. package/lib/typescript/src/driverTypes.d.ts +39 -11
  78. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.d.ts +2 -4
  80. package/lib/typescript/src/drivers.d.ts.map +1 -1
  81. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  82. package/lib/typescript/src/geometry.d.ts +36 -2
  83. package/lib/typescript/src/geometry.d.ts.map +1 -1
  84. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  85. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  91. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  92. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  93. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  94. package/lib/typescript/src/index.d.ts +6 -3
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  97. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  98. package/package.json +10 -11
  99. package/src/NativeSmoothClipModule.ts +275 -0
  100. package/src/SmoothClipView.ios.tsx +30 -11
  101. package/src/SmoothClipView.tsx +2 -76
  102. package/src/SmoothClipViewNativeComponent.ts +27 -1
  103. package/src/capabilities.native.ts +69 -0
  104. package/src/capabilities.ts +2 -0
  105. package/src/capabilityTypes.ts +17 -0
  106. package/src/driverState.ts +1 -41
  107. package/src/driverTypes.ts +56 -11
  108. package/src/drivers.native.ts +921 -151
  109. package/src/drivers.ts +2 -423
  110. package/src/geometry.ts +231 -27
  111. package/src/groupDriverTypes.ts +117 -0
  112. package/src/groupDrivers.android.ts +1 -0
  113. package/src/groupDrivers.ios.ts +1 -0
  114. package/src/groupDrivers.native.ts +1189 -0
  115. package/src/groupDrivers.ts +1 -0
  116. package/src/index.ts +30 -2
  117. package/src/presentationProtocol.ts +28 -0
  118. package/lib/module/SmoothClipView.web.js +0 -54
  119. package/lib/module/SmoothClipView.web.js.map +0 -1
  120. package/lib/module/index.web.js +0 -8
  121. package/lib/module/index.web.js.map +0 -1
  122. package/lib/module/webClipPath.js +0 -25
  123. package/lib/module/webClipPath.js.map +0 -1
  124. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  125. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  126. package/lib/typescript/src/index.web.d.ts +0 -9
  127. package/lib/typescript/src/index.web.d.ts.map +0 -1
  128. package/lib/typescript/src/webClipPath.d.ts +0 -7
  129. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  130. package/src/SmoothClipView.web.tsx +0 -66
  131. package/src/index.web.ts +0 -23
  132. package/src/webClipPath.ts +0 -30
@@ -1,12 +1,13 @@
1
- // The shared NATIVE driver: iOS and Android both execute this file, via the
1
+ // The shared native driver: iOS and Android both execute this file, via the
2
2
  // one-line re-exports in drivers.ios.ts / drivers.android.ts that Metro's
3
3
  // platform resolution picks up from index.ts's `./drivers` import. Only the
4
- // `./smoothClipNative` import below is platform-split. drivers.ts (no
5
- // suffix) is the web/Reanimated fallback, not this driver's sibling.
4
+ // `./smoothClipNative` import below is platform-split.
6
5
  import { useEffect, useRef } from 'react';
6
+ import { Platform } from 'react-native';
7
7
  import { useSharedValue, type SharedValue } from 'react-native-reanimated';
8
8
  import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
9
9
  import NativeSmoothClipModule from './smoothClipNative';
10
+ import { getSmoothClipCapabilities } from './capabilities';
10
11
  import type {
11
12
  ClipReduceMotion,
12
13
  KeyframedClipAnimation,
@@ -29,9 +30,11 @@ import {
29
30
  synchronizeNativeCompletion,
30
31
  } from './nativeCompletion';
31
32
  import {
33
+ canonicalizeClipPresentation,
32
34
  clipPresentationEquals,
33
35
  createClipPresentation,
34
36
  isFiniteClipPresentation,
37
+ type CanonicalSmoothClipPresentation,
35
38
  type ClipGeometry,
36
39
  type SmoothClipPresentation,
37
40
  } from './geometry';
@@ -44,7 +47,63 @@ import {
44
47
  const INTERACTIVE = 0;
45
48
  const NATIVE = 1;
46
49
 
47
- const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
50
+ // Only Android consumes the Reanimated start stamp (its frame-clock anchor —
51
+ // docs/android-frame-clock-anchor.md). iOS's TurboModule reads its declared
52
+ // parameters positionally and drops the trailing argument, so capturing the
53
+ // stamp there is a wasted native _getAnimationTimestamp() call per animateTo.
54
+ // Resolved once at module scope; the worklet captures the primitive.
55
+ const NEEDS_START_STAMP = Platform.OS === 'android';
56
+ const PRESENTATION_PROTOCOL_VERSION =
57
+ getSmoothClipCapabilities().presentationProtocolVersion;
58
+
59
+ type NativeV2HostFunctions = Readonly<{
60
+ setClipPresentationV2(
61
+ driverId: number,
62
+ x: number,
63
+ y: number,
64
+ width: number,
65
+ height: number,
66
+ topLeftRadius: number,
67
+ topRightRadius: number,
68
+ bottomRightRadius: number,
69
+ bottomLeftRadius: number,
70
+ curveCode: number,
71
+ contentTranslateX: number,
72
+ contentTranslateY: number,
73
+ contentScale: number,
74
+ takeOwnership: boolean,
75
+ overridePendingAnimation: boolean,
76
+ recordVelocity?: boolean
77
+ ): void;
78
+ beginInteractionV2(driverId: number): readonly number[];
79
+ snapshotCurrentV2(driverId: number): readonly number[];
80
+ animateTimingV2(...args: readonly unknown[]): number;
81
+ animateSpringV2(...args: readonly unknown[]): number;
82
+ animateKeyframesV2(...args: readonly unknown[]): number;
83
+ animateTimingFromV2(...args: readonly unknown[]): number;
84
+ animateSpringFromV2(...args: readonly unknown[]): number;
85
+ animateKeyframesFromV2(...args: readonly unknown[]): number;
86
+ cancelAnimationV2(
87
+ driverId: number,
88
+ animationId: number,
89
+ behavior: number
90
+ ): readonly number[];
91
+ }>;
92
+
93
+ const NativeSmoothClipModuleV2 =
94
+ NativeSmoothClipModule as unknown as Partial<NativeV2HostFunctions>;
95
+
96
+ // Widened like the animate* functions below: one trailing argument beyond the
97
+ // TurboModule spec — whether this write should record an `'inherit'` velocity
98
+ // sample. Android's JSI bindings read it (absent means record, the pre-flag
99
+ // behavior); the iOS TurboModule reads its declared parameters positionally
100
+ // and drops it, so iOS always records.
101
+ type WithRecordVelocity<F> = F extends (...args: infer A) => infer R
102
+ ? (...args: [...A, recordVelocity?: boolean]) => R
103
+ : never;
104
+ const setPresentationHostFunction: WithRecordVelocity<
105
+ typeof NativeSmoothClipModule.setClipPresentation
106
+ > = NativeSmoothClipModule.setClipPresentation;
48
107
  const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
49
108
 
50
109
  // The animate* host functions take one argument beyond the TurboModule spec:
@@ -68,15 +127,35 @@ const animateKeyframesHostFunction: WithStartStamp<
68
127
  const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
69
128
  const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
70
129
  const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
130
+ const setPresentationV2HostFunction =
131
+ NativeSmoothClipModuleV2.setClipPresentationV2;
132
+ const beginInteractionV2HostFunction =
133
+ NativeSmoothClipModuleV2.beginInteractionV2;
134
+ const snapshotCurrentV2HostFunction =
135
+ NativeSmoothClipModuleV2.snapshotCurrentV2;
136
+ const animateTimingV2HostFunction = NativeSmoothClipModuleV2.animateTimingV2;
137
+ const animateSpringV2HostFunction = NativeSmoothClipModuleV2.animateSpringV2;
138
+ const animateKeyframesV2HostFunction =
139
+ NativeSmoothClipModuleV2.animateKeyframesV2;
140
+ const animateTimingFromV2HostFunction =
141
+ NativeSmoothClipModuleV2.animateTimingFromV2;
142
+ const animateSpringFromV2HostFunction =
143
+ NativeSmoothClipModuleV2.animateSpringFromV2;
144
+ const animateKeyframesFromV2HostFunction =
145
+ NativeSmoothClipModuleV2.animateKeyframesFromV2;
146
+ const cancelAnimationV2HostFunction =
147
+ NativeSmoothClipModuleV2.cancelAnimationV2;
71
148
 
72
- // Workaround for react-native-worklets 0.10: the RN-side serialization cache
149
+ // Workaround for react-native-worklets 0.10.0 (fixed upstream in >=0.10.1,
150
+ // which drops the registry/proxy design; kept while the peer range still
151
+ // allows 0.10.0 consumers): the RN-side serialization cache
73
152
  // hands out one remote-function registry id per function forever, but the
74
153
  // registry entry is deleted as soon as any transient UI-side proxy of that
75
154
  // function is garbage collected. A later scheduleOnRN for the same function
76
155
  // then reads `undefined` from the registry and aborts in Value::getObject.
77
156
  // Pinning one UI-side reference for the app lifetime keeps the proxy — and
78
- // with it the registry entry — alive. Remove once worklets decouples registry
79
- // cleanup from proxy lifetime.
157
+ // with it the registry entry — alive. Remove once the peer range requires
158
+ // worklets >=0.10.1.
80
159
  scheduleOnUI((resolver: typeof resolveReactRequest) => {
81
160
  'worklet';
82
161
  (globalThis as Record<string, unknown>).__smoothClipPinnedResolver = resolver;
@@ -94,8 +173,8 @@ function reduceMotionCode(value: ClipReduceMotion): number {
94
173
  }
95
174
 
96
175
  function animationIsFinite(
97
- target: SmoothClipPresentation,
98
- animation: SmoothClipAnimation
176
+ animation: SmoothClipAnimation,
177
+ strictV2: boolean
99
178
  ): boolean {
100
179
  'worklet';
101
180
  if (
@@ -110,62 +189,67 @@ function animationIsFinite(
110
189
  const [x1, , x2] = animation.controlPoints;
111
190
  return (
112
191
  Number.isFinite(animation.duration) &&
192
+ (!strictV2 || animation.duration >= 0) &&
113
193
  animation.controlPoints.every(Number.isFinite) &&
114
194
  // A cubic-bezier easing is only defined for x within [0,1] (the CSS /
115
195
  // CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
116
196
  // makes the Android parameter solve meaningless while CoreAnimation
117
197
  // silently clamps — reject like any other invalid input instead of
118
198
  // diverging per platform.
119
- x1 >= 0 &&
120
- x1 <= 1 &&
121
- x2 >= 0 &&
122
- x2 <= 1
199
+ (!strictV2 || (x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1))
123
200
  );
124
201
  }
125
202
  if (animation.type === 'spring') {
126
- return [
127
- animation.mass ?? 1,
128
- animation.stiffness ?? 100,
129
- animation.damping ?? 10,
203
+ const mass = animation.mass ?? 1;
204
+ const stiffness = animation.stiffness ?? 100;
205
+ const damping = animation.damping ?? 10;
206
+ const initialVelocity =
130
207
  animation.initialVelocity === 'inherit'
131
208
  ? 0
132
- : (animation.initialVelocity ?? 0),
133
- ].every(Number.isFinite);
134
- }
135
- if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
136
- return false;
137
- }
138
- let previousOffset = -1;
139
- for (const frame of animation.frames) {
140
- if (
141
- !Number.isFinite(frame.offset) ||
142
- frame.offset < 0 ||
143
- frame.offset > 1 ||
144
- frame.offset <= previousOffset ||
145
- !isFiniteClipPresentation(frame.presentation)
146
- ) {
147
- return false;
148
- }
149
- previousOffset = frame.offset;
209
+ : (animation.initialVelocity ?? 0);
210
+ return (
211
+ [mass, stiffness, damping, initialVelocity].every(Number.isFinite) &&
212
+ (!strictV2 || (mass > 0 && stiffness > 0 && damping >= 0))
213
+ );
150
214
  }
215
+ // Keyframe frames validate inside flattenFiniteKeyframes — one traversal
216
+ // both checks and flattens, instead of walking every frame twice. The type
217
+ // check is statically always-true for the TS union but load-bearing for JS
218
+ // callers: an unrecognized type must reject here, or it would skip the
219
+ // keyframe gate entirely and hand native a null frames array.
151
220
  return (
152
- animation.frames[0]?.offset === 0 &&
153
- previousOffset === 1 &&
154
- clipPresentationEquals(
155
- animation.frames[animation.frames.length - 1]?.presentation ?? null,
156
- target
157
- )
221
+ animation.type === 'keyframes' && (!strictV2 || animation.duration >= 0)
158
222
  );
159
223
  }
160
224
 
161
225
  function presentationFromNative(
162
226
  values: ReadonlyArray<number>,
163
- fallback: SmoothClipPresentation,
227
+ fallback: CanonicalSmoothClipPresentation,
164
228
  offset = 0
165
- ): SmoothClipPresentation {
229
+ ): CanonicalSmoothClipPresentation {
166
230
  'worklet';
231
+ if (values.length >= offset + 12) {
232
+ const presentation = canonicalizeClipPresentation({
233
+ clip: {
234
+ x: values[offset] as number,
235
+ y: values[offset + 1] as number,
236
+ width: values[offset + 2] as number,
237
+ height: values[offset + 3] as number,
238
+ radius: 0,
239
+ topLeftRadius: values[offset + 4] as number,
240
+ topRightRadius: values[offset + 5] as number,
241
+ bottomRightRadius: values[offset + 6] as number,
242
+ bottomLeftRadius: values[offset + 7] as number,
243
+ curve: values[offset + 8] === 1 ? 'continuous' : 'circular',
244
+ },
245
+ contentTranslateX: values[offset + 9] as number,
246
+ contentTranslateY: values[offset + 10] as number,
247
+ contentScale: values[offset + 11] as number,
248
+ });
249
+ return presentation ?? fallback;
250
+ }
167
251
  if (values.length < offset + 7) return fallback;
168
- const presentation = {
252
+ const presentation = canonicalizeClipPresentation({
169
253
  clip: {
170
254
  x: values[offset] as number,
171
255
  y: values[offset + 1] as number,
@@ -175,24 +259,133 @@ function presentationFromNative(
175
259
  },
176
260
  contentTranslateX: values[offset + 5] as number,
177
261
  contentTranslateY: values[offset + 6] as number,
178
- };
179
- return isFiniteClipPresentation(presentation) ? presentation : fallback;
262
+ });
263
+ return presentation ?? fallback;
264
+ }
265
+
266
+ function curveCode(presentation: CanonicalSmoothClipPresentation): number {
267
+ 'worklet';
268
+ return presentation.clip.curve === 'continuous' ? 1 : 0;
180
269
  }
181
270
 
182
- function flattenedKeyframes(animation: KeyframedClipAnimation): number[] {
271
+ function isV1Compatible(
272
+ presentation: CanonicalSmoothClipPresentation
273
+ ): boolean {
183
274
  'worklet';
275
+ const clip = presentation.clip;
276
+ return (
277
+ clip.curve === 'circular' &&
278
+ clip.topLeftRadius === clip.radius &&
279
+ clip.topRightRadius === clip.radius &&
280
+ clip.bottomRightRadius === clip.radius &&
281
+ clip.bottomLeftRadius === clip.radius &&
282
+ presentation.contentScale === 1
283
+ );
284
+ }
285
+
286
+ function springScaleIsProvablyPositive(
287
+ start: CanonicalSmoothClipPresentation,
288
+ target: CanonicalSmoothClipPresentation,
289
+ animation: SpringClipAnimation
290
+ ): boolean {
291
+ 'worklet';
292
+ if (start.contentScale === target.contentScale) return true;
293
+
294
+ // A zero-velocity critically/over-damped step is monotonic, so its sampled
295
+ // scale remains between two already-positive endpoints. Inherited or
296
+ // under-damped configurations can overshoot; callers must compile those
297
+ // plans to exact keyframes instead of risking a non-positive transform.
298
+ const mass = animation.mass ?? 1;
299
+ const stiffness = animation.stiffness ?? 100;
300
+ const damping = animation.damping ?? 10;
301
+ return (
302
+ animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness
303
+ );
304
+ }
305
+
306
+ // Validates and flattens the keyframe list in one traversal (the flat number
307
+ // array is the wire format the native side reads back positionally). Returns
308
+ // null when any frame is invalid — same rejection contract the two-pass
309
+ // validate-then-flatten had.
310
+ function flattenFiniteKeyframes(
311
+ target: CanonicalSmoothClipPresentation,
312
+ animation: KeyframedClipAnimation
313
+ ): {
314
+ frames: number[];
315
+ canonicalFrames: CanonicalSmoothClipPresentation[];
316
+ } | null {
317
+ 'worklet';
318
+ if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
319
+ return null;
320
+ }
184
321
  const values: number[] = [];
322
+ const canonicalFrames: CanonicalSmoothClipPresentation[] = [];
323
+ let previousOffset = -1;
185
324
  for (const frame of animation.frames) {
186
- const { clip, contentTranslateX, contentTranslateY } = frame.presentation;
325
+ const { offset } = frame;
326
+ const presentation = canonicalizeClipPresentation(frame.presentation);
327
+ if (
328
+ !Number.isFinite(offset) ||
329
+ offset < 0 ||
330
+ offset > 1 ||
331
+ offset <= previousOffset ||
332
+ presentation === null ||
333
+ presentation.clip.curve !== target.clip.curve
334
+ ) {
335
+ return null;
336
+ }
337
+ previousOffset = offset;
338
+ canonicalFrames.push(presentation);
339
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
340
+ presentation;
187
341
  values.push(
188
- frame.offset,
342
+ offset,
189
343
  clip.x,
190
344
  clip.y,
191
345
  clip.width,
192
346
  clip.height,
193
- clip.radius,
347
+ clip.topLeftRadius,
348
+ clip.topRightRadius,
349
+ clip.bottomRightRadius,
350
+ clip.bottomLeftRadius,
351
+ curveCode(presentation),
194
352
  contentTranslateX,
195
- contentTranslateY
353
+ contentTranslateY,
354
+ contentScale
355
+ );
356
+ }
357
+ if (
358
+ animation.frames[0]?.offset !== 0 ||
359
+ previousOffset !== 1 ||
360
+ !clipPresentationEquals(
361
+ canonicalFrames[canonicalFrames.length - 1] ?? null,
362
+ target
363
+ )
364
+ ) {
365
+ return null;
366
+ }
367
+ return { frames: values, canonicalFrames };
368
+ }
369
+
370
+ function flattenV1Keyframes(
371
+ frames: readonly CanonicalSmoothClipPresentation[],
372
+ offsets: readonly number[]
373
+ ): number[] {
374
+ 'worklet';
375
+ const values: number[] = [];
376
+ for (let index = 0; index < frames.length; index += 1) {
377
+ const presentation = frames[index];
378
+ const offset = offsets[index];
379
+ if (presentation === undefined || offset === undefined) continue;
380
+ values.push(
381
+ offset,
382
+ presentation.clip.x,
383
+ presentation.clip.y,
384
+ presentation.clip.width,
385
+ presentation.clip.height,
386
+ presentation.clip.radius,
387
+ presentation.contentTranslateX,
388
+ presentation.contentTranslateY
196
389
  );
197
390
  }
198
391
  return values;
@@ -209,11 +402,18 @@ export function useSmoothClipDriver(
209
402
  initialValue: ClipGeometry | SmoothClipPresentation,
210
403
  options: SmoothClipDriverOptions = {}
211
404
  ): SmoothClipDriver {
212
- const initialPresentation =
405
+ const requestedInitialPresentation =
213
406
  'clip' in initialValue
214
407
  ? initialValue
215
408
  : createClipPresentation(initialValue);
216
- const presentation = useSharedValue(initialPresentation);
409
+ const initialPresentation = canonicalizeClipPresentation(
410
+ requestedInitialPresentation
411
+ );
412
+ if (initialPresentation === null) {
413
+ throw new Error('[SmoothClipView] Initial presentation must be finite.');
414
+ }
415
+ const presentation =
416
+ useSharedValue<SmoothClipPresentation>(initialPresentation);
217
417
  const ownership = useSharedValue(INTERACTIVE);
218
418
  const suppressDeliveries = useSharedValue(0);
219
419
  const activeAnimationId = useSharedValue(0);
@@ -236,8 +436,12 @@ export function useSmoothClipDriver(
236
436
  if (driverRef.current === null) {
237
437
  const driverId = allocateDriverId();
238
438
  const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
239
-
240
- const seedPresentation = (next: SmoothClipPresentation) => {
439
+ // Captured once, like reduceMotion. Only the per-frame setScalars stream
440
+ // consults it; set(), the fused animateTo `from` seed, and the effect's
441
+ // declarative deliveries always record, so every once-per-call channel
442
+ // keeps its pre-flag 'inherit' behavior.
443
+ const velocityTracking = options.velocityTracking === true;
444
+ const seedPresentation = (next: CanonicalSmoothClipPresentation) => {
241
445
  'worklet';
242
446
  if (clipPresentationEquals(presentation.value, next)) return;
243
447
  suppressDeliveries.value += 1;
@@ -251,12 +455,16 @@ export function useSmoothClipDriver(
251
455
  if (suppressDeliveries.value > 0) suppressDeliveries.value -= 1;
252
456
  };
253
457
 
254
- const beginOnUI = (): SmoothClipPresentation => {
458
+ const beginOnUI = (): CanonicalSmoothClipPresentation => {
255
459
  'worklet';
256
- if (disposed.value !== 0) return presentation.value;
460
+ const fallback =
461
+ canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
462
+ if (disposed.value !== 0) return fallback;
257
463
  const current = presentationFromNative(
258
- beginInteractionHostFunction(driverId),
259
- presentation.value
464
+ PRESENTATION_PROTOCOL_VERSION === 2 && beginInteractionV2HostFunction
465
+ ? beginInteractionV2HostFunction(driverId)
466
+ : beginInteractionHostFunction(driverId),
467
+ fallback
260
468
  );
261
469
  activeAnimationId.value = 0;
262
470
  ownership.value = INTERACTIVE;
@@ -267,24 +475,55 @@ export function useSmoothClipDriver(
267
475
 
268
476
  const setOnUI = (next: SmoothClipPresentation): void => {
269
477
  'worklet';
270
- if (disposed.value !== 0 || !isFiniteClipPresentation(next)) return;
478
+ const canonical = canonicalizeClipPresentation(next);
479
+ if (disposed.value !== 0 || canonical === null) return;
271
480
  activeAnimationId.value = 0;
272
481
  ownership.value = INTERACTIVE;
273
482
  scalarsStale.value = 0;
274
- const { clip, contentTranslateX, contentTranslateY } = next;
275
- setPresentationHostFunction(
276
- driverId,
277
- clip.x,
278
- clip.y,
279
- clip.width,
280
- clip.height,
281
- clip.radius,
282
- contentTranslateX,
283
- contentTranslateY,
284
- true,
285
- false
286
- );
287
- seedPresentation(next);
483
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
484
+ canonical;
485
+ // Always records: only the per-frame setScalars hot path is gated by
486
+ // velocityTracking. set() already pays full validation and a SharedValue
487
+ // seed per call, so the sample recording is noise here.
488
+ if (
489
+ PRESENTATION_PROTOCOL_VERSION === 2 &&
490
+ setPresentationV2HostFunction
491
+ ) {
492
+ setPresentationV2HostFunction(
493
+ driverId,
494
+ clip.x,
495
+ clip.y,
496
+ clip.width,
497
+ clip.height,
498
+ clip.topLeftRadius,
499
+ clip.topRightRadius,
500
+ clip.bottomRightRadius,
501
+ clip.bottomLeftRadius,
502
+ curveCode(canonical),
503
+ contentTranslateX,
504
+ contentTranslateY,
505
+ contentScale,
506
+ true,
507
+ false,
508
+ true
509
+ );
510
+ } else if (isV1Compatible(canonical)) {
511
+ setPresentationHostFunction(
512
+ driverId,
513
+ clip.x,
514
+ clip.y,
515
+ clip.width,
516
+ clip.height,
517
+ clip.radius,
518
+ contentTranslateX,
519
+ contentTranslateY,
520
+ true,
521
+ false
522
+ );
523
+ } else {
524
+ return;
525
+ }
526
+ seedPresentation(canonical);
288
527
  };
289
528
 
290
529
  const setScalarsOnUI = (
@@ -295,9 +534,24 @@ export function useSmoothClipDriver(
295
534
  radius: number,
296
535
  contentTranslateX: number,
297
536
  contentTranslateY: number,
298
- overridePendingAnimation = false
537
+ overridePendingAnimation = false,
538
+ forceRecordVelocity = false
299
539
  ): void => {
300
540
  'worklet';
541
+ // Resolved in the body, not as a default-parameter initializer: the
542
+ // worklets Babel plugin only captures closure variables referenced
543
+ // inside the function body, so `= velocityTracking` up in the parameter
544
+ // list would throw "Property 'velocityTracking' doesn't exist" on the
545
+ // UI runtime.
546
+ const recordVelocity = forceRecordVelocity || velocityTracking;
547
+ // Number.isFinite (not a fused arithmetic gate): it also rejects
548
+ // non-number types, which arithmetic would coerce past the gate and
549
+ // into a UI-runtime throw from the binding's asNumber. The gate must
550
+ // stay on this side of the bridge: the native binding drops non-finite
551
+ // writes silently, and flipping the ownership SharedValues below for a
552
+ // write native never applied would let the deliver listener record
553
+ // values native has not observed — a later identical declarative write
554
+ // then dedupes against them and is swallowed for good.
301
555
  if (
302
556
  disposed.value !== 0 ||
303
557
  !Number.isFinite(x) ||
@@ -323,7 +577,96 @@ export function useSmoothClipDriver(
323
577
  contentTranslateX,
324
578
  contentTranslateY,
325
579
  true,
326
- overridePendingAnimation
580
+ overridePendingAnimation,
581
+ recordVelocity
582
+ );
583
+ };
584
+
585
+ const setPresentationScalarsOnUI = (
586
+ x: number,
587
+ y: number,
588
+ width: number,
589
+ height: number,
590
+ topLeftRadius: number,
591
+ topRightRadius: number,
592
+ bottomRightRadius: number,
593
+ bottomLeftRadius: number,
594
+ nextCurveCode: number,
595
+ contentTranslateX: number,
596
+ contentTranslateY: number,
597
+ contentScale: number,
598
+ overridePendingAnimation = false,
599
+ forceRecordVelocity = false
600
+ ): void => {
601
+ 'worklet';
602
+ const values = [
603
+ x,
604
+ y,
605
+ width,
606
+ height,
607
+ topLeftRadius,
608
+ topRightRadius,
609
+ bottomRightRadius,
610
+ bottomLeftRadius,
611
+ nextCurveCode,
612
+ contentTranslateX,
613
+ contentTranslateY,
614
+ contentScale,
615
+ ];
616
+ if (
617
+ disposed.value !== 0 ||
618
+ !values.every(Number.isFinite) ||
619
+ (nextCurveCode !== 0 && nextCurveCode !== 1) ||
620
+ contentScale <= 0
621
+ ) {
622
+ return;
623
+ }
624
+ const recordVelocity = forceRecordVelocity || velocityTracking;
625
+ if (
626
+ PRESENTATION_PROTOCOL_VERSION !== 2 ||
627
+ !setPresentationV2HostFunction
628
+ ) {
629
+ if (
630
+ nextCurveCode === 0 &&
631
+ contentScale === 1 &&
632
+ topLeftRadius === topRightRadius &&
633
+ topLeftRadius === bottomRightRadius &&
634
+ topLeftRadius === bottomLeftRadius
635
+ ) {
636
+ setScalarsOnUI(
637
+ x,
638
+ y,
639
+ width,
640
+ height,
641
+ topLeftRadius,
642
+ contentTranslateX,
643
+ contentTranslateY,
644
+ overridePendingAnimation,
645
+ forceRecordVelocity
646
+ );
647
+ }
648
+ return;
649
+ }
650
+ if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
651
+ if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
652
+ if (scalarsStale.value === 0) scalarsStale.value = 1;
653
+ setPresentationV2HostFunction(
654
+ driverId,
655
+ x,
656
+ y,
657
+ width,
658
+ height,
659
+ topLeftRadius,
660
+ topRightRadius,
661
+ bottomRightRadius,
662
+ bottomLeftRadius,
663
+ nextCurveCode,
664
+ contentTranslateX,
665
+ contentTranslateY,
666
+ contentScale,
667
+ true,
668
+ overridePendingAnimation,
669
+ recordVelocity
327
670
  );
328
671
  };
329
672
 
@@ -336,9 +679,89 @@ export function useSmoothClipDriver(
336
679
  // invalid stale call cannot mint a native rejection id/completion against
337
680
  // a driver whose listener and registry entry are already gone.
338
681
  if (disposed.value !== 0) return 0;
682
+ const canonicalTarget = canonicalizeClipPresentation(target);
683
+ const canonicalFrom =
684
+ animation.from === undefined
685
+ ? undefined
686
+ : canonicalizeClipPresentation(animation.from);
687
+ const keyframeResult =
688
+ animation.type === 'keyframes'
689
+ ? canonicalTarget === null
690
+ ? null
691
+ : flattenFiniteKeyframes(canonicalTarget, animation)
692
+ : null;
693
+ if (
694
+ canonicalTarget === null ||
695
+ (animation.from !== undefined && canonicalFrom === null) ||
696
+ (animation.type === 'keyframes' && keyframeResult === null)
697
+ ) {
698
+ return rejectAnimationHostFunction(driverId);
699
+ }
700
+ const resolvedFrom = canonicalFrom ?? undefined;
701
+ const v1Compatible =
702
+ isV1Compatible(canonicalTarget) &&
703
+ (resolvedFrom === undefined || isV1Compatible(resolvedFrom)) &&
704
+ (animation.type !== 'keyframes' ||
705
+ (keyframeResult?.canonicalFrames.every(isV1Compatible) ?? false));
706
+ // V1-representable plans deliberately stay on the original seven-scalar
707
+ // bridge even when V2 is installed. Besides preserving bridge shape,
708
+ // this retains V1's finite-value/clamping and host-normalization rules.
709
+ const useV2 = !v1Compatible;
710
+ if (!animationIsFinite(animation, useV2)) {
711
+ return rejectAnimationHostFunction(driverId);
712
+ }
713
+ const hasV2Protocol =
714
+ PRESENTATION_PROTOCOL_VERSION === 2 &&
715
+ snapshotCurrentV2HostFunction !== undefined &&
716
+ animateTimingV2HostFunction !== undefined &&
717
+ animateSpringV2HostFunction !== undefined &&
718
+ animateKeyframesV2HostFunction !== undefined;
719
+ const hasAtomicFromMethod =
720
+ resolvedFrom === undefined ||
721
+ (animation.type === 'timing'
722
+ ? animateTimingFromV2HostFunction !== undefined
723
+ : animation.type === 'spring'
724
+ ? animateSpringFromV2HostFunction !== undefined
725
+ : animateKeyframesFromV2HostFunction !== undefined);
726
+ // This is intentionally before sampling or any ownership/group/
727
+ // presentation mutation. New JS against old native must fail closed,
728
+ // and an explicit V2 start must never degrade to set()+animate().
729
+ if (useV2 && (!hasV2Protocol || !hasAtomicFromMethod)) {
730
+ return rejectAnimationHostFunction(driverId);
731
+ }
339
732
  if (
340
- !isFiniteClipPresentation(target) ||
341
- !animationIsFinite(target, animation)
733
+ useV2 &&
734
+ resolvedFrom !== undefined &&
735
+ animation.type === 'keyframes' &&
736
+ !clipPresentationEquals(
737
+ keyframeResult?.canonicalFrames[0] ?? null,
738
+ resolvedFrom
739
+ )
740
+ ) {
741
+ return rejectAnimationHostFunction(driverId);
742
+ }
743
+ const currentPresentation =
744
+ canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
745
+ const sampledStart =
746
+ resolvedFrom ??
747
+ (useV2
748
+ ? presentationFromNative(
749
+ snapshotCurrentV2HostFunction!(driverId),
750
+ currentPresentation
751
+ )
752
+ : currentPresentation);
753
+ // setPresentationScalars intentionally leaves the public SharedValue
754
+ // stale to keep the hot path allocation-free. Validate an implicit
755
+ // retarget against the sampled native-visible start, not that stale JS
756
+ // value; otherwise a streamed curve transition cannot settle natively on
757
+ // its new (now stable) curve.
758
+ if (canonicalTarget.clip.curve !== sampledStart.clip.curve) {
759
+ return rejectAnimationHostFunction(driverId);
760
+ }
761
+ if (
762
+ useV2 &&
763
+ animation.type === 'spring' &&
764
+ !springScaleIsProvablyPositive(sampledStart, canonicalTarget, animation)
342
765
  ) {
343
766
  return rejectAnimationHostFunction(driverId);
344
767
  }
@@ -348,13 +771,18 @@ export function useSmoothClipDriver(
348
771
  // rejectAnimation, which would mint an id and a completion for a driver
349
772
  // whose JS state is already detached. This is the "unsupported dispatch"
350
773
  // arm of the documented 0 contract.
351
- const from = animation.from;
352
- if (from !== undefined) {
774
+ const from = resolvedFrom;
775
+ if (from !== undefined && !useV2) {
353
776
  // Fused hot write: exactly setScalars(from…) issued immediately
354
777
  // before the handoff, so native's latest value — the animation
355
778
  // start below — is the caller's explicit presentation. Also
356
779
  // re-grabs from a running animation, which the implicit
357
- // interactive-start path would silently skip.
780
+ // interactive-start path would silently skip. Always records a
781
+ // velocity sample regardless of velocityTracking: this is one write
782
+ // per animateTo, not the per-frame stream the flag exists to keep
783
+ // cheap, and an unrecorded seed would invalidate the history a
784
+ // set()-driven drag just recorded (the tracker's coalesce and
785
+ // identical-re-record rules were designed for exactly this write).
358
786
  setScalarsOnUI(
359
787
  from.clip.x,
360
788
  from.clip.y,
@@ -363,6 +791,7 @@ export function useSmoothClipDriver(
363
791
  from.clip.radius,
364
792
  from.contentTranslateX,
365
793
  from.contentTranslateY,
794
+ true,
366
795
  true
367
796
  );
368
797
  }
@@ -374,12 +803,18 @@ export function useSmoothClipDriver(
374
803
  const scalarsWereStale = scalarsStale.value !== 0;
375
804
  const hasInteractiveStart =
376
805
  ownership.value === INTERACTIVE && !scalarsWereStale;
377
- const start = presentation.value;
378
- ownership.value = NATIVE;
379
- presentation.value = target;
380
- scalarsStale.value = 0;
381
- const startClip = start.clip;
382
- const targetClip = target.clip;
806
+ const start =
807
+ canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
808
+ // V1 preserves the old optimistic SharedValue ownership transition.
809
+ // V2 commits it only after native has atomically accepted the plan.
810
+ if (!useV2) {
811
+ ownership.value = NATIVE;
812
+ presentation.value = canonicalTarget;
813
+ scalarsStale.value = 0;
814
+ }
815
+ const dispatchStart = useV2 && from !== undefined ? from : start;
816
+ const startClip = dispatchStart.clip;
817
+ const targetClip = canonicalTarget.clip;
383
818
  // Reanimated stamps a parallel animation's t0 on this same runtime as
384
819
  // `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
385
820
  // Capturing the identical value here and handing it to the integrator
@@ -390,19 +825,95 @@ export function useSmoothClipDriver(
390
825
  // its rAF flush where __frameTimestamp is cleared, keeps the wall
391
826
  // clock — a lead lasting the whole animation. NaN when the globals are
392
827
  // absent (tests, non-worklets runtimes): native then falls back to its
393
- // own clock plus the min() anchor, the pre-stamp behavior.
828
+ // own clock plus the min() anchor, the pre-stamp behavior. On iOS the
829
+ // stamp is always NaN: Core Animation anchors at commit time and the
830
+ // TurboModule discards the argument, so the capture would only cost.
394
831
  const workletGlobal = globalThis as {
395
832
  __frameTimestamp?: number;
396
833
  _getAnimationTimestamp?: () => number;
397
834
  };
398
- const startedAtMs =
399
- workletGlobal.__frameTimestamp ||
400
- (typeof workletGlobal._getAnimationTimestamp === 'function'
401
- ? workletGlobal._getAnimationTimestamp()
402
- : Number.NaN);
835
+ const startedAtMs = !NEEDS_START_STAMP
836
+ ? Number.NaN
837
+ : workletGlobal.__frameTimestamp ||
838
+ (typeof workletGlobal._getAnimationTimestamp === 'function'
839
+ ? workletGlobal._getAnimationTimestamp()
840
+ : Number.NaN);
403
841
  let animationId: number;
404
842
 
405
- if (animation.type === 'timing') {
843
+ if (animation.type === 'timing' && useV2) {
844
+ const [x1, y1, x2, y2] = animation.controlPoints;
845
+ animationId =
846
+ from !== undefined
847
+ ? animateTimingFromV2HostFunction!(
848
+ driverId,
849
+ startClip.x,
850
+ startClip.y,
851
+ startClip.width,
852
+ startClip.height,
853
+ startClip.topLeftRadius,
854
+ startClip.topRightRadius,
855
+ startClip.bottomRightRadius,
856
+ startClip.bottomLeftRadius,
857
+ curveCode(dispatchStart),
858
+ dispatchStart.contentTranslateX,
859
+ dispatchStart.contentTranslateY,
860
+ dispatchStart.contentScale,
861
+ targetClip.x,
862
+ targetClip.y,
863
+ targetClip.width,
864
+ targetClip.height,
865
+ targetClip.topLeftRadius,
866
+ targetClip.topRightRadius,
867
+ targetClip.bottomRightRadius,
868
+ targetClip.bottomLeftRadius,
869
+ curveCode(canonicalTarget),
870
+ canonicalTarget.contentTranslateX,
871
+ canonicalTarget.contentTranslateY,
872
+ canonicalTarget.contentScale,
873
+ animation.duration,
874
+ x1,
875
+ y1,
876
+ x2,
877
+ y2,
878
+ reduceMotion,
879
+ startedAtMs
880
+ )
881
+ : animateTimingV2HostFunction!(
882
+ driverId,
883
+ hasInteractiveStart,
884
+ startClip.x,
885
+ startClip.y,
886
+ startClip.width,
887
+ startClip.height,
888
+ startClip.topLeftRadius,
889
+ startClip.topRightRadius,
890
+ startClip.bottomRightRadius,
891
+ startClip.bottomLeftRadius,
892
+ curveCode(dispatchStart),
893
+ dispatchStart.contentTranslateX,
894
+ dispatchStart.contentTranslateY,
895
+ dispatchStart.contentScale,
896
+ targetClip.x,
897
+ targetClip.y,
898
+ targetClip.width,
899
+ targetClip.height,
900
+ targetClip.topLeftRadius,
901
+ targetClip.topRightRadius,
902
+ targetClip.bottomRightRadius,
903
+ targetClip.bottomLeftRadius,
904
+ curveCode(canonicalTarget),
905
+ canonicalTarget.contentTranslateX,
906
+ canonicalTarget.contentTranslateY,
907
+ canonicalTarget.contentScale,
908
+ animation.duration,
909
+ x1,
910
+ y1,
911
+ x2,
912
+ y2,
913
+ reduceMotion,
914
+ startedAtMs
915
+ );
916
+ } else if (animation.type === 'timing') {
406
917
  const [x1, y1, x2, y2] = animation.controlPoints;
407
918
  animationId = animateTimingHostFunction(
408
919
  driverId,
@@ -419,8 +930,8 @@ export function useSmoothClipDriver(
419
930
  targetClip.width,
420
931
  targetClip.height,
421
932
  targetClip.radius,
422
- target.contentTranslateX,
423
- target.contentTranslateY,
933
+ canonicalTarget.contentTranslateX,
934
+ canonicalTarget.contentTranslateY,
424
935
  Math.max(0, animation.duration),
425
936
  x1,
426
937
  y1,
@@ -429,6 +940,87 @@ export function useSmoothClipDriver(
429
940
  reduceMotion,
430
941
  startedAtMs
431
942
  );
943
+ } else if (animation.type === 'spring' && useV2) {
944
+ const inheritVelocity =
945
+ animation.initialVelocity === undefined ||
946
+ animation.initialVelocity === 'inherit';
947
+ const mass = animation.mass ?? 1;
948
+ const stiffness = animation.stiffness ?? 100;
949
+ const damping = animation.damping ?? 10;
950
+ const initialVelocity = inheritVelocity
951
+ ? 0
952
+ : (animation.initialVelocity as number);
953
+ animationId =
954
+ from !== undefined
955
+ ? animateSpringFromV2HostFunction!(
956
+ driverId,
957
+ startClip.x,
958
+ startClip.y,
959
+ startClip.width,
960
+ startClip.height,
961
+ startClip.topLeftRadius,
962
+ startClip.topRightRadius,
963
+ startClip.bottomRightRadius,
964
+ startClip.bottomLeftRadius,
965
+ curveCode(dispatchStart),
966
+ dispatchStart.contentTranslateX,
967
+ dispatchStart.contentTranslateY,
968
+ dispatchStart.contentScale,
969
+ targetClip.x,
970
+ targetClip.y,
971
+ targetClip.width,
972
+ targetClip.height,
973
+ targetClip.topLeftRadius,
974
+ targetClip.topRightRadius,
975
+ targetClip.bottomRightRadius,
976
+ targetClip.bottomLeftRadius,
977
+ curveCode(canonicalTarget),
978
+ canonicalTarget.contentTranslateX,
979
+ canonicalTarget.contentTranslateY,
980
+ canonicalTarget.contentScale,
981
+ mass,
982
+ stiffness,
983
+ damping,
984
+ initialVelocity,
985
+ inheritVelocity,
986
+ reduceMotion,
987
+ startedAtMs
988
+ )
989
+ : animateSpringV2HostFunction!(
990
+ driverId,
991
+ hasInteractiveStart,
992
+ startClip.x,
993
+ startClip.y,
994
+ startClip.width,
995
+ startClip.height,
996
+ startClip.topLeftRadius,
997
+ startClip.topRightRadius,
998
+ startClip.bottomRightRadius,
999
+ startClip.bottomLeftRadius,
1000
+ curveCode(dispatchStart),
1001
+ dispatchStart.contentTranslateX,
1002
+ dispatchStart.contentTranslateY,
1003
+ dispatchStart.contentScale,
1004
+ targetClip.x,
1005
+ targetClip.y,
1006
+ targetClip.width,
1007
+ targetClip.height,
1008
+ targetClip.topLeftRadius,
1009
+ targetClip.topRightRadius,
1010
+ targetClip.bottomRightRadius,
1011
+ targetClip.bottomLeftRadius,
1012
+ curveCode(canonicalTarget),
1013
+ canonicalTarget.contentTranslateX,
1014
+ canonicalTarget.contentTranslateY,
1015
+ canonicalTarget.contentScale,
1016
+ mass,
1017
+ stiffness,
1018
+ damping,
1019
+ initialVelocity,
1020
+ inheritVelocity,
1021
+ reduceMotion,
1022
+ startedAtMs
1023
+ );
432
1024
  } else if (animation.type === 'spring') {
433
1025
  const inheritVelocity =
434
1026
  animation.initialVelocity === undefined ||
@@ -448,8 +1040,8 @@ export function useSmoothClipDriver(
448
1040
  targetClip.width,
449
1041
  targetClip.height,
450
1042
  targetClip.radius,
451
- target.contentTranslateX,
452
- target.contentTranslateY,
1043
+ canonicalTarget.contentTranslateX,
1044
+ canonicalTarget.contentTranslateY,
453
1045
  Math.max(0.0001, animation.mass ?? 1),
454
1046
  Math.max(0.0001, animation.stiffness ?? 100),
455
1047
  Math.max(0, animation.damping ?? 10),
@@ -458,6 +1050,74 @@ export function useSmoothClipDriver(
458
1050
  reduceMotion,
459
1051
  startedAtMs
460
1052
  );
1053
+ } else if (useV2) {
1054
+ const frames = (keyframeResult as NonNullable<typeof keyframeResult>)
1055
+ .frames;
1056
+ animationId =
1057
+ from !== undefined
1058
+ ? animateKeyframesFromV2HostFunction!(
1059
+ driverId,
1060
+ startClip.x,
1061
+ startClip.y,
1062
+ startClip.width,
1063
+ startClip.height,
1064
+ startClip.topLeftRadius,
1065
+ startClip.topRightRadius,
1066
+ startClip.bottomRightRadius,
1067
+ startClip.bottomLeftRadius,
1068
+ curveCode(dispatchStart),
1069
+ dispatchStart.contentTranslateX,
1070
+ dispatchStart.contentTranslateY,
1071
+ dispatchStart.contentScale,
1072
+ targetClip.x,
1073
+ targetClip.y,
1074
+ targetClip.width,
1075
+ targetClip.height,
1076
+ targetClip.topLeftRadius,
1077
+ targetClip.topRightRadius,
1078
+ targetClip.bottomRightRadius,
1079
+ targetClip.bottomLeftRadius,
1080
+ curveCode(canonicalTarget),
1081
+ canonicalTarget.contentTranslateX,
1082
+ canonicalTarget.contentTranslateY,
1083
+ canonicalTarget.contentScale,
1084
+ animation.duration,
1085
+ frames,
1086
+ reduceMotion,
1087
+ startedAtMs
1088
+ )
1089
+ : animateKeyframesV2HostFunction!(
1090
+ driverId,
1091
+ hasInteractiveStart,
1092
+ startClip.x,
1093
+ startClip.y,
1094
+ startClip.width,
1095
+ startClip.height,
1096
+ startClip.topLeftRadius,
1097
+ startClip.topRightRadius,
1098
+ startClip.bottomRightRadius,
1099
+ startClip.bottomLeftRadius,
1100
+ curveCode(dispatchStart),
1101
+ dispatchStart.contentTranslateX,
1102
+ dispatchStart.contentTranslateY,
1103
+ dispatchStart.contentScale,
1104
+ targetClip.x,
1105
+ targetClip.y,
1106
+ targetClip.width,
1107
+ targetClip.height,
1108
+ targetClip.topLeftRadius,
1109
+ targetClip.topRightRadius,
1110
+ targetClip.bottomRightRadius,
1111
+ targetClip.bottomLeftRadius,
1112
+ curveCode(canonicalTarget),
1113
+ canonicalTarget.contentTranslateX,
1114
+ canonicalTarget.contentTranslateY,
1115
+ canonicalTarget.contentScale,
1116
+ animation.duration,
1117
+ frames,
1118
+ reduceMotion,
1119
+ startedAtMs
1120
+ );
461
1121
  } else {
462
1122
  animationId = animateKeyframesHostFunction(
463
1123
  driverId,
@@ -474,10 +1134,16 @@ export function useSmoothClipDriver(
474
1134
  targetClip.width,
475
1135
  targetClip.height,
476
1136
  targetClip.radius,
477
- target.contentTranslateX,
478
- target.contentTranslateY,
1137
+ canonicalTarget.contentTranslateX,
1138
+ canonicalTarget.contentTranslateY,
479
1139
  Math.max(0, animation.duration),
480
- flattenedKeyframes(animation),
1140
+ // Non-null here: the validation gate above rejected null before any
1141
+ // side effect.
1142
+ flattenV1Keyframes(
1143
+ (keyframeResult as NonNullable<typeof keyframeResult>)
1144
+ .canonicalFrames,
1145
+ animation.frames.map((frame) => frame.offset)
1146
+ ),
481
1147
  reduceMotion,
482
1148
  startedAtMs
483
1149
  );
@@ -491,12 +1157,19 @@ export function useSmoothClipDriver(
491
1157
  // effect, with a fresh id and one finished:false completion once the
492
1158
  // native entry exists — in the pre-seed window rejectAnimation has no
493
1159
  // entry and returns the bare 0 sentinel with no completion instead.
494
- ownership.value = INTERACTIVE;
495
- presentation.value = start;
496
- if (scalarsWereStale) scalarsStale.value = 1;
497
- activeAnimationId.value = 0;
1160
+ if (!useV2) {
1161
+ ownership.value = INTERACTIVE;
1162
+ presentation.value = start;
1163
+ if (scalarsWereStale) scalarsStale.value = 1;
1164
+ activeAnimationId.value = 0;
1165
+ }
498
1166
  return rejectAnimationHostFunction(driverId);
499
1167
  }
1168
+ if (useV2) {
1169
+ ownership.value = NATIVE;
1170
+ presentation.value = canonicalTarget;
1171
+ scalarsStale.value = 0;
1172
+ }
500
1173
  activeAnimationId.value = animationId;
501
1174
  return animationId;
502
1175
  };
@@ -504,19 +1177,28 @@ export function useSmoothClipDriver(
504
1177
  const cancelOnUI = (
505
1178
  animationId = 0,
506
1179
  behavior: 'current' | 'target' = 'current'
507
- ): SmoothClipPresentation => {
1180
+ ): CanonicalSmoothClipPresentation => {
508
1181
  'worklet';
509
1182
  // Past the hook's cleanup the native entry is gone; the call would fail
510
1183
  // defined (handled=false) but it is the last driver.ui path that still
511
1184
  // crosses into native after the tombstone. Guard it like the rest.
512
- if (disposed.value !== 0) return presentation.value;
513
- const values = cancelAnimationHostFunction(
514
- driverId,
515
- animationId,
516
- behavior === 'target' ? 1 : 0
517
- );
518
- if (values.length < 8 || values[0] !== 1) return presentation.value;
519
- const current = presentationFromNative(values, presentation.value, 1);
1185
+ const fallback =
1186
+ canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
1187
+ if (disposed.value !== 0) return fallback;
1188
+ const values =
1189
+ PRESENTATION_PROTOCOL_VERSION === 2 && cancelAnimationV2HostFunction
1190
+ ? cancelAnimationV2HostFunction(
1191
+ driverId,
1192
+ animationId,
1193
+ behavior === 'target' ? 1 : 0
1194
+ )
1195
+ : cancelAnimationHostFunction(
1196
+ driverId,
1197
+ animationId,
1198
+ behavior === 'target' ? 1 : 0
1199
+ );
1200
+ if (values.length < 8 || values[0] !== 1) return fallback;
1201
+ const current = presentationFromNative(values, fallback, 1);
520
1202
  activeAnimationId.value = 0;
521
1203
  ownership.value = INTERACTIVE;
522
1204
  scalarsStale.value = 0;
@@ -559,6 +1241,37 @@ export function useSmoothClipDriver(
559
1241
  contentTranslateY
560
1242
  );
561
1243
  },
1244
+ setPresentationScalars(
1245
+ x,
1246
+ y,
1247
+ width,
1248
+ height,
1249
+ topLeftRadius,
1250
+ topRightRadius,
1251
+ bottomRightRadius,
1252
+ bottomLeftRadius,
1253
+ nextCurveCode,
1254
+ contentTranslateX,
1255
+ contentTranslateY,
1256
+ contentScale
1257
+ ) {
1258
+ 'worklet';
1259
+ if (isRNRuntime()) return uiOnly();
1260
+ setPresentationScalarsOnUI(
1261
+ x,
1262
+ y,
1263
+ width,
1264
+ height,
1265
+ topLeftRadius,
1266
+ topRightRadius,
1267
+ bottomRightRadius,
1268
+ bottomLeftRadius,
1269
+ nextCurveCode,
1270
+ contentTranslateX,
1271
+ contentTranslateY,
1272
+ contentScale
1273
+ );
1274
+ },
562
1275
  animateTo(target, animation) {
563
1276
  'worklet';
564
1277
  if (isRNRuntime()) return uiOnly();
@@ -573,7 +1286,7 @@ export function useSmoothClipDriver(
573
1286
  react: {
574
1287
  beginInteraction() {
575
1288
  const { requestId, promise } =
576
- createReactRequest<SmoothClipPresentation>(driverId);
1289
+ createReactRequest<CanonicalSmoothClipPresentation>(driverId);
577
1290
  scheduleOnUI(() => {
578
1291
  'worklet';
579
1292
  const result = beginOnUI();
@@ -620,7 +1333,7 @@ export function useSmoothClipDriver(
620
1333
  },
621
1334
  cancel(animationId = 0, behavior = 'current') {
622
1335
  const { requestId, promise } =
623
- createReactRequest<SmoothClipPresentation>(driverId, true);
1336
+ createReactRequest<CanonicalSmoothClipPresentation>(driverId, true);
624
1337
  scheduleOnUI(() => {
625
1338
  'worklet';
626
1339
  const result = cancelOnUI(animationId, behavior);
@@ -635,6 +1348,13 @@ export function useSmoothClipDriver(
635
1348
  return promise;
636
1349
  },
637
1350
  },
1351
+ __smoothClipHandle: {
1352
+ driverId,
1353
+ presentation,
1354
+ ownership,
1355
+ activeAnimationId,
1356
+ disposed,
1357
+ },
638
1358
  };
639
1359
 
640
1360
  setDriverState(
@@ -686,7 +1406,9 @@ export function useSmoothClipDriver(
686
1406
  active: SharedValue<number>,
687
1407
  stale: SharedValue<number>,
688
1408
  gone: SharedValue<number>,
689
- setter: typeof setPresentationHostFunction
1409
+ setter: typeof setPresentationHostFunction,
1410
+ setterV2: typeof setPresentationV2HostFunction,
1411
+ protocolVersion: number
690
1412
  ) => {
691
1413
  'worklet';
692
1414
  // First, before the listener or the seed: this effect run owns the
@@ -695,10 +1417,8 @@ export function useSmoothClipDriver(
695
1417
  gone.value = 0;
696
1418
  let last: SmoothClipPresentation | null = null;
697
1419
  const deliver = (next: SmoothClipPresentation) => {
698
- if (
699
- !isFiniteClipPresentation(next) ||
700
- clipPresentationEquals(last, next)
701
- ) {
1420
+ const canonical = canonicalizeClipPresentation(next);
1421
+ if (canonical === null || clipPresentationEquals(last, canonical)) {
702
1422
  return;
703
1423
  }
704
1424
  // Record `last` only for values native has actually observed:
@@ -707,24 +1427,51 @@ export function useSmoothClipDriver(
707
1427
  if (owner.value !== INTERACTIVE) return;
708
1428
  if (suppressed.value > 0) {
709
1429
  suppressed.value -= 1;
710
- last = next;
1430
+ last = canonical;
1431
+ return;
1432
+ }
1433
+ if (protocolVersion !== 2 && !isV1Compatible(canonical)) {
711
1434
  return;
712
1435
  }
713
- last = next;
1436
+ last = canonical;
714
1437
  if (stale.value !== 0) stale.value = 0;
715
- const { clip, contentTranslateX, contentTranslateY } = next;
716
- setter(
717
- nativeDriverId,
718
- clip.x,
719
- clip.y,
720
- clip.width,
721
- clip.height,
722
- clip.radius,
723
- contentTranslateX,
724
- contentTranslateY,
725
- false,
726
- false
727
- );
1438
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
1439
+ canonical;
1440
+ // Declarative deliveries always record velocity samples — only the
1441
+ // setScalars hot path is gated by the velocityTracking option.
1442
+ if (protocolVersion === 2 && setterV2) {
1443
+ setterV2(
1444
+ nativeDriverId,
1445
+ clip.x,
1446
+ clip.y,
1447
+ clip.width,
1448
+ clip.height,
1449
+ clip.topLeftRadius,
1450
+ clip.topRightRadius,
1451
+ clip.bottomRightRadius,
1452
+ clip.bottomLeftRadius,
1453
+ curveCode(canonical),
1454
+ contentTranslateX,
1455
+ contentTranslateY,
1456
+ contentScale,
1457
+ false,
1458
+ false,
1459
+ true
1460
+ );
1461
+ } else {
1462
+ setter(
1463
+ nativeDriverId,
1464
+ clip.x,
1465
+ clip.y,
1466
+ clip.width,
1467
+ clip.height,
1468
+ clip.radius,
1469
+ contentTranslateX,
1470
+ contentTranslateY,
1471
+ false,
1472
+ false
1473
+ );
1474
+ }
728
1475
  };
729
1476
  source.addListener(listenerId, deliver);
730
1477
  // Authoritative take-ownership seed. Creates the native entry before
@@ -733,24 +1480,45 @@ export function useSmoothClipDriver(
733
1480
  // An animation worklet can win the scheduling race and create a latch
734
1481
  // first; Native ownership then marks that intent as newer, so this
735
1482
  // passive seed must not clear its active id or replay the target value.
736
- const current = source.value;
737
- if (owner.value === INTERACTIVE && isFiniteClipPresentation(current)) {
1483
+ const current = canonicalizeClipPresentation(source.value);
1484
+ if (owner.value === INTERACTIVE && current !== null) {
738
1485
  active.value = 0;
739
1486
  owner.value = INTERACTIVE;
740
1487
  stale.value = 0;
741
1488
  last = current;
742
- setter(
743
- nativeDriverId,
744
- current.clip.x,
745
- current.clip.y,
746
- current.clip.width,
747
- current.clip.height,
748
- current.clip.radius,
749
- current.contentTranslateX,
750
- current.contentTranslateY,
751
- true,
752
- false
753
- );
1489
+ if (protocolVersion === 2 && setterV2) {
1490
+ setterV2(
1491
+ nativeDriverId,
1492
+ current.clip.x,
1493
+ current.clip.y,
1494
+ current.clip.width,
1495
+ current.clip.height,
1496
+ current.clip.topLeftRadius,
1497
+ current.clip.topRightRadius,
1498
+ current.clip.bottomRightRadius,
1499
+ current.clip.bottomLeftRadius,
1500
+ curveCode(current),
1501
+ current.contentTranslateX,
1502
+ current.contentTranslateY,
1503
+ current.contentScale,
1504
+ true,
1505
+ false,
1506
+ true
1507
+ );
1508
+ } else if (isV1Compatible(current)) {
1509
+ setter(
1510
+ nativeDriverId,
1511
+ current.clip.x,
1512
+ current.clip.y,
1513
+ current.clip.width,
1514
+ current.clip.height,
1515
+ current.clip.radius,
1516
+ current.contentTranslateX,
1517
+ current.contentTranslateY,
1518
+ true,
1519
+ false
1520
+ );
1521
+ }
754
1522
  }
755
1523
  },
756
1524
  presentation,
@@ -761,7 +1529,9 @@ export function useSmoothClipDriver(
761
1529
  activeAnimationId,
762
1530
  scalarsStale,
763
1531
  disposed,
764
- setPresentationHostFunction
1532
+ setPresentationHostFunction,
1533
+ setPresentationV2HostFunction,
1534
+ PRESENTATION_PROTOCOL_VERSION
765
1535
  );
766
1536
 
767
1537
  return () => {