react-native-smooth-clip-view 0.2.9 → 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 (130) hide show
  1. package/README.md +60 -21
  2. package/android/build.gradle +6 -2
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +890 -4
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +1056 -44
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +371 -18
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  12. package/cpp/SmoothClipAnimationCurve.h +151 -122
  13. package/cpp/SmoothClipRegistry.h +95 -3
  14. package/cpp/SmoothClipSharedGeometry.h +146 -7
  15. package/cpp/SmoothClipVelocityTracker.h +13 -9
  16. package/ios/SmoothClipGeometry.h +184 -6
  17. package/ios/SmoothClipRegistry.mm +1028 -22
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +857 -2
  20. package/ios/SmoothClipTurboModule.h +310 -0
  21. package/ios/SmoothClipView.mm +714 -183
  22. package/ios/SmoothClipViewRegistry.h +1 -0
  23. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  24. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  26. package/lib/module/SmoothClipView.ios.js +22 -6
  27. package/lib/module/SmoothClipView.ios.js.map +1 -1
  28. package/lib/module/SmoothClipView.js +1 -46
  29. package/lib/module/SmoothClipView.js.map +1 -1
  30. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +36 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +11 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/driverState.js +0 -28
  38. package/lib/module/driverState.js.map +1 -1
  39. package/lib/module/drivers.js +1 -281
  40. package/lib/module/drivers.js.map +1 -1
  41. package/lib/module/drivers.native.js +263 -63
  42. package/lib/module/drivers.native.js.map +1 -1
  43. package/lib/module/geometry.js +147 -17
  44. package/lib/module/geometry.js.map +1 -1
  45. package/lib/module/groupDriverTypes.js +4 -0
  46. package/lib/module/groupDriverTypes.js.map +1 -0
  47. package/lib/module/groupDrivers.android.js +4 -0
  48. package/lib/module/groupDrivers.android.js.map +1 -0
  49. package/lib/module/groupDrivers.ios.js +4 -0
  50. package/lib/module/groupDrivers.ios.js.map +1 -0
  51. package/lib/module/groupDrivers.js +4 -0
  52. package/lib/module/groupDrivers.js.map +1 -0
  53. package/lib/module/groupDrivers.native.js +799 -0
  54. package/lib/module/groupDrivers.native.js.map +1 -0
  55. package/lib/module/index.js +3 -2
  56. package/lib/module/index.js.map +1 -1
  57. package/lib/module/presentationProtocol.js +15 -0
  58. package/lib/module/presentationProtocol.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  61. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  62. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  64. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  66. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  67. package/lib/typescript/src/capabilities.d.ts +3 -0
  68. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  69. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  72. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  73. package/lib/typescript/src/driverState.d.ts +1 -6
  74. package/lib/typescript/src/driverState.d.ts.map +1 -1
  75. package/lib/typescript/src/driverTypes.d.ts +25 -11
  76. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  77. package/lib/typescript/src/drivers.d.ts +2 -4
  78. package/lib/typescript/src/drivers.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  80. package/lib/typescript/src/geometry.d.ts +36 -2
  81. package/lib/typescript/src/geometry.d.ts.map +1 -1
  82. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  83. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  85. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  91. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/index.d.ts +6 -3
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  95. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  96. package/package.json +10 -11
  97. package/src/NativeSmoothClipModule.ts +275 -0
  98. package/src/SmoothClipView.ios.tsx +30 -11
  99. package/src/SmoothClipView.tsx +2 -76
  100. package/src/SmoothClipViewNativeComponent.ts +27 -1
  101. package/src/capabilities.native.ts +69 -0
  102. package/src/capabilities.ts +2 -0
  103. package/src/capabilityTypes.ts +17 -0
  104. package/src/driverState.ts +1 -41
  105. package/src/driverTypes.ts +42 -11
  106. package/src/drivers.native.ts +824 -124
  107. package/src/drivers.ts +2 -423
  108. package/src/geometry.ts +231 -27
  109. package/src/groupDriverTypes.ts +117 -0
  110. package/src/groupDrivers.android.ts +1 -0
  111. package/src/groupDrivers.ios.ts +1 -0
  112. package/src/groupDrivers.native.ts +1189 -0
  113. package/src/groupDrivers.ts +1 -0
  114. package/src/index.ts +30 -2
  115. package/src/presentationProtocol.ts +28 -0
  116. package/lib/module/SmoothClipView.web.js +0 -54
  117. package/lib/module/SmoothClipView.web.js.map +0 -1
  118. package/lib/module/index.web.js +0 -8
  119. package/lib/module/index.web.js.map +0 -1
  120. package/lib/module/webClipPath.js +0 -25
  121. package/lib/module/webClipPath.js.map +0 -1
  122. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  123. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  124. package/lib/typescript/src/index.web.d.ts +0 -9
  125. package/lib/typescript/src/index.web.d.ts.map +0 -1
  126. package/lib/typescript/src/webClipPath.d.ts +0 -7
  127. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  128. package/src/SmoothClipView.web.tsx +0 -66
  129. package/src/index.web.ts +0 -23
  130. package/src/webClipPath.ts +0 -30
@@ -1,13 +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';
7
6
  import { Platform } from 'react-native';
8
7
  import { useSharedValue, type SharedValue } from 'react-native-reanimated';
9
8
  import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
10
9
  import NativeSmoothClipModule from './smoothClipNative';
10
+ import { getSmoothClipCapabilities } from './capabilities';
11
11
  import type {
12
12
  ClipReduceMotion,
13
13
  KeyframedClipAnimation,
@@ -30,9 +30,11 @@ import {
30
30
  synchronizeNativeCompletion,
31
31
  } from './nativeCompletion';
32
32
  import {
33
+ canonicalizeClipPresentation,
33
34
  clipPresentationEquals,
34
35
  createClipPresentation,
35
36
  isFiniteClipPresentation,
37
+ type CanonicalSmoothClipPresentation,
36
38
  type ClipGeometry,
37
39
  type SmoothClipPresentation,
38
40
  } from './geometry';
@@ -51,6 +53,45 @@ const NATIVE = 1;
51
53
  // stamp there is a wasted native _getAnimationTimestamp() call per animateTo.
52
54
  // Resolved once at module scope; the worklet captures the primitive.
53
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>;
54
95
 
55
96
  // Widened like the animate* functions below: one trailing argument beyond the
56
97
  // TurboModule spec — whether this write should record an `'inherit'` velocity
@@ -86,15 +127,35 @@ const animateKeyframesHostFunction: WithStartStamp<
86
127
  const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
87
128
  const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
88
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;
89
148
 
90
- // 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
91
152
  // hands out one remote-function registry id per function forever, but the
92
153
  // registry entry is deleted as soon as any transient UI-side proxy of that
93
154
  // function is garbage collected. A later scheduleOnRN for the same function
94
155
  // then reads `undefined` from the registry and aborts in Value::getObject.
95
156
  // Pinning one UI-side reference for the app lifetime keeps the proxy — and
96
- // with it the registry entry — alive. Remove once worklets decouples registry
97
- // cleanup from proxy lifetime.
157
+ // with it the registry entry — alive. Remove once the peer range requires
158
+ // worklets >=0.10.1.
98
159
  scheduleOnUI((resolver: typeof resolveReactRequest) => {
99
160
  'worklet';
100
161
  (globalThis as Record<string, unknown>).__smoothClipPinnedResolver = resolver;
@@ -111,7 +172,10 @@ function reduceMotionCode(value: ClipReduceMotion): number {
111
172
  }
112
173
  }
113
174
 
114
- function animationIsFinite(animation: SmoothClipAnimation): boolean {
175
+ function animationIsFinite(
176
+ animation: SmoothClipAnimation,
177
+ strictV2: boolean
178
+ ): boolean {
115
179
  'worklet';
116
180
  if (
117
181
  animation.from !== undefined &&
@@ -125,44 +189,67 @@ function animationIsFinite(animation: SmoothClipAnimation): boolean {
125
189
  const [x1, , x2] = animation.controlPoints;
126
190
  return (
127
191
  Number.isFinite(animation.duration) &&
192
+ (!strictV2 || animation.duration >= 0) &&
128
193
  animation.controlPoints.every(Number.isFinite) &&
129
194
  // A cubic-bezier easing is only defined for x within [0,1] (the CSS /
130
195
  // CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
131
196
  // makes the Android parameter solve meaningless while CoreAnimation
132
197
  // silently clamps — reject like any other invalid input instead of
133
198
  // diverging per platform.
134
- x1 >= 0 &&
135
- x1 <= 1 &&
136
- x2 >= 0 &&
137
- x2 <= 1
199
+ (!strictV2 || (x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1))
138
200
  );
139
201
  }
140
202
  if (animation.type === 'spring') {
141
- return [
142
- animation.mass ?? 1,
143
- animation.stiffness ?? 100,
144
- animation.damping ?? 10,
203
+ const mass = animation.mass ?? 1;
204
+ const stiffness = animation.stiffness ?? 100;
205
+ const damping = animation.damping ?? 10;
206
+ const initialVelocity =
145
207
  animation.initialVelocity === 'inherit'
146
208
  ? 0
147
- : (animation.initialVelocity ?? 0),
148
- ].every(Number.isFinite);
209
+ : (animation.initialVelocity ?? 0);
210
+ return (
211
+ [mass, stiffness, damping, initialVelocity].every(Number.isFinite) &&
212
+ (!strictV2 || (mass > 0 && stiffness > 0 && damping >= 0))
213
+ );
149
214
  }
150
215
  // Keyframe frames validate inside flattenFiniteKeyframes — one traversal
151
216
  // both checks and flattens, instead of walking every frame twice. The type
152
217
  // check is statically always-true for the TS union but load-bearing for JS
153
218
  // callers: an unrecognized type must reject here, or it would skip the
154
219
  // keyframe gate entirely and hand native a null frames array.
155
- return animation.type === 'keyframes';
220
+ return (
221
+ animation.type === 'keyframes' && (!strictV2 || animation.duration >= 0)
222
+ );
156
223
  }
157
224
 
158
225
  function presentationFromNative(
159
226
  values: ReadonlyArray<number>,
160
- fallback: SmoothClipPresentation,
227
+ fallback: CanonicalSmoothClipPresentation,
161
228
  offset = 0
162
- ): SmoothClipPresentation {
229
+ ): CanonicalSmoothClipPresentation {
163
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
+ }
164
251
  if (values.length < offset + 7) return fallback;
165
- const presentation = {
252
+ const presentation = canonicalizeClipPresentation({
166
253
  clip: {
167
254
  x: values[offset] as number,
168
255
  y: values[offset + 1] as number,
@@ -172,8 +259,48 @@ function presentationFromNative(
172
259
  },
173
260
  contentTranslateX: values[offset + 5] as number,
174
261
  contentTranslateY: values[offset + 6] as number,
175
- };
176
- 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;
269
+ }
270
+
271
+ function isV1Compatible(
272
+ presentation: CanonicalSmoothClipPresentation
273
+ ): boolean {
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
+ );
177
304
  }
178
305
 
179
306
  // Validates and flattens the keyframe list in one traversal (the flat number
@@ -181,49 +308,86 @@ function presentationFromNative(
181
308
  // null when any frame is invalid — same rejection contract the two-pass
182
309
  // validate-then-flatten had.
183
310
  function flattenFiniteKeyframes(
184
- target: SmoothClipPresentation,
311
+ target: CanonicalSmoothClipPresentation,
185
312
  animation: KeyframedClipAnimation
186
- ): number[] | null {
313
+ ): {
314
+ frames: number[];
315
+ canonicalFrames: CanonicalSmoothClipPresentation[];
316
+ } | null {
187
317
  'worklet';
188
318
  if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
189
319
  return null;
190
320
  }
191
321
  const values: number[] = [];
322
+ const canonicalFrames: CanonicalSmoothClipPresentation[] = [];
192
323
  let previousOffset = -1;
193
324
  for (const frame of animation.frames) {
194
- const { offset, presentation } = frame;
325
+ const { offset } = frame;
326
+ const presentation = canonicalizeClipPresentation(frame.presentation);
195
327
  if (
196
328
  !Number.isFinite(offset) ||
197
329
  offset < 0 ||
198
330
  offset > 1 ||
199
331
  offset <= previousOffset ||
200
- !isFiniteClipPresentation(presentation)
332
+ presentation === null ||
333
+ presentation.clip.curve !== target.clip.curve
201
334
  ) {
202
335
  return null;
203
336
  }
204
337
  previousOffset = offset;
205
- const { clip, contentTranslateX, contentTranslateY } = presentation;
338
+ canonicalFrames.push(presentation);
339
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
340
+ presentation;
206
341
  values.push(
207
342
  offset,
208
343
  clip.x,
209
344
  clip.y,
210
345
  clip.width,
211
346
  clip.height,
212
- clip.radius,
347
+ clip.topLeftRadius,
348
+ clip.topRightRadius,
349
+ clip.bottomRightRadius,
350
+ clip.bottomLeftRadius,
351
+ curveCode(presentation),
213
352
  contentTranslateX,
214
- contentTranslateY
353
+ contentTranslateY,
354
+ contentScale
215
355
  );
216
356
  }
217
357
  if (
218
358
  animation.frames[0]?.offset !== 0 ||
219
359
  previousOffset !== 1 ||
220
360
  !clipPresentationEquals(
221
- animation.frames[animation.frames.length - 1]?.presentation ?? null,
361
+ canonicalFrames[canonicalFrames.length - 1] ?? null,
222
362
  target
223
363
  )
224
364
  ) {
225
365
  return null;
226
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
389
+ );
390
+ }
227
391
  return values;
228
392
  }
229
393
 
@@ -238,11 +402,18 @@ export function useSmoothClipDriver(
238
402
  initialValue: ClipGeometry | SmoothClipPresentation,
239
403
  options: SmoothClipDriverOptions = {}
240
404
  ): SmoothClipDriver {
241
- const initialPresentation =
405
+ const requestedInitialPresentation =
242
406
  'clip' in initialValue
243
407
  ? initialValue
244
408
  : createClipPresentation(initialValue);
245
- 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);
246
417
  const ownership = useSharedValue(INTERACTIVE);
247
418
  const suppressDeliveries = useSharedValue(0);
248
419
  const activeAnimationId = useSharedValue(0);
@@ -270,7 +441,7 @@ export function useSmoothClipDriver(
270
441
  // declarative deliveries always record, so every once-per-call channel
271
442
  // keeps its pre-flag 'inherit' behavior.
272
443
  const velocityTracking = options.velocityTracking === true;
273
- const seedPresentation = (next: SmoothClipPresentation) => {
444
+ const seedPresentation = (next: CanonicalSmoothClipPresentation) => {
274
445
  'worklet';
275
446
  if (clipPresentationEquals(presentation.value, next)) return;
276
447
  suppressDeliveries.value += 1;
@@ -284,12 +455,16 @@ export function useSmoothClipDriver(
284
455
  if (suppressDeliveries.value > 0) suppressDeliveries.value -= 1;
285
456
  };
286
457
 
287
- const beginOnUI = (): SmoothClipPresentation => {
458
+ const beginOnUI = (): CanonicalSmoothClipPresentation => {
288
459
  'worklet';
289
- if (disposed.value !== 0) return presentation.value;
460
+ const fallback =
461
+ canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
462
+ if (disposed.value !== 0) return fallback;
290
463
  const current = presentationFromNative(
291
- beginInteractionHostFunction(driverId),
292
- presentation.value
464
+ PRESENTATION_PROTOCOL_VERSION === 2 && beginInteractionV2HostFunction
465
+ ? beginInteractionV2HostFunction(driverId)
466
+ : beginInteractionHostFunction(driverId),
467
+ fallback
293
468
  );
294
469
  activeAnimationId.value = 0;
295
470
  ownership.value = INTERACTIVE;
@@ -300,27 +475,55 @@ export function useSmoothClipDriver(
300
475
 
301
476
  const setOnUI = (next: SmoothClipPresentation): void => {
302
477
  'worklet';
303
- if (disposed.value !== 0 || !isFiniteClipPresentation(next)) return;
478
+ const canonical = canonicalizeClipPresentation(next);
479
+ if (disposed.value !== 0 || canonical === null) return;
304
480
  activeAnimationId.value = 0;
305
481
  ownership.value = INTERACTIVE;
306
482
  scalarsStale.value = 0;
307
- const { clip, contentTranslateX, contentTranslateY } = next;
483
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
484
+ canonical;
308
485
  // Always records: only the per-frame setScalars hot path is gated by
309
486
  // velocityTracking. set() already pays full validation and a SharedValue
310
487
  // seed per call, so the sample recording is noise here.
311
- setPresentationHostFunction(
312
- driverId,
313
- clip.x,
314
- clip.y,
315
- clip.width,
316
- clip.height,
317
- clip.radius,
318
- contentTranslateX,
319
- contentTranslateY,
320
- true,
321
- false
322
- );
323
- seedPresentation(next);
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);
324
527
  };
325
528
 
326
529
  const setScalarsOnUI = (
@@ -379,6 +582,94 @@ export function useSmoothClipDriver(
379
582
  );
380
583
  };
381
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
670
+ );
671
+ };
672
+
382
673
  const animateOnUI = (
383
674
  target: SmoothClipPresentation,
384
675
  animation: SmoothClipAnimation
@@ -388,14 +679,89 @@ export function useSmoothClipDriver(
388
679
  // invalid stale call cannot mint a native rejection id/completion against
389
680
  // a driver whose listener and registry entry are already gone.
390
681
  if (disposed.value !== 0) return 0;
391
- const keyframeValues =
682
+ const canonicalTarget = canonicalizeClipPresentation(target);
683
+ const canonicalFrom =
684
+ animation.from === undefined
685
+ ? undefined
686
+ : canonicalizeClipPresentation(animation.from);
687
+ const keyframeResult =
392
688
  animation.type === 'keyframes'
393
- ? flattenFiniteKeyframes(target, animation)
689
+ ? canonicalTarget === null
690
+ ? null
691
+ : flattenFiniteKeyframes(canonicalTarget, animation)
394
692
  : null;
395
693
  if (
396
- !isFiniteClipPresentation(target) ||
397
- !animationIsFinite(animation) ||
398
- (animation.type === 'keyframes' && keyframeValues === null)
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
+ }
732
+ if (
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)
399
765
  ) {
400
766
  return rejectAnimationHostFunction(driverId);
401
767
  }
@@ -405,8 +771,8 @@ export function useSmoothClipDriver(
405
771
  // rejectAnimation, which would mint an id and a completion for a driver
406
772
  // whose JS state is already detached. This is the "unsupported dispatch"
407
773
  // arm of the documented 0 contract.
408
- const from = animation.from;
409
- if (from !== undefined) {
774
+ const from = resolvedFrom;
775
+ if (from !== undefined && !useV2) {
410
776
  // Fused hot write: exactly setScalars(from…) issued immediately
411
777
  // before the handoff, so native's latest value — the animation
412
778
  // start below — is the caller's explicit presentation. Also
@@ -437,12 +803,18 @@ export function useSmoothClipDriver(
437
803
  const scalarsWereStale = scalarsStale.value !== 0;
438
804
  const hasInteractiveStart =
439
805
  ownership.value === INTERACTIVE && !scalarsWereStale;
440
- const start = presentation.value;
441
- ownership.value = NATIVE;
442
- presentation.value = target;
443
- scalarsStale.value = 0;
444
- const startClip = start.clip;
445
- 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;
446
818
  // Reanimated stamps a parallel animation's t0 on this same runtime as
447
819
  // `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
448
820
  // Capturing the identical value here and handing it to the integrator
@@ -468,7 +840,80 @@ export function useSmoothClipDriver(
468
840
  : Number.NaN);
469
841
  let animationId: number;
470
842
 
471
- 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') {
472
917
  const [x1, y1, x2, y2] = animation.controlPoints;
473
918
  animationId = animateTimingHostFunction(
474
919
  driverId,
@@ -485,8 +930,8 @@ export function useSmoothClipDriver(
485
930
  targetClip.width,
486
931
  targetClip.height,
487
932
  targetClip.radius,
488
- target.contentTranslateX,
489
- target.contentTranslateY,
933
+ canonicalTarget.contentTranslateX,
934
+ canonicalTarget.contentTranslateY,
490
935
  Math.max(0, animation.duration),
491
936
  x1,
492
937
  y1,
@@ -495,6 +940,87 @@ export function useSmoothClipDriver(
495
940
  reduceMotion,
496
941
  startedAtMs
497
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
+ );
498
1024
  } else if (animation.type === 'spring') {
499
1025
  const inheritVelocity =
500
1026
  animation.initialVelocity === undefined ||
@@ -514,8 +1040,8 @@ export function useSmoothClipDriver(
514
1040
  targetClip.width,
515
1041
  targetClip.height,
516
1042
  targetClip.radius,
517
- target.contentTranslateX,
518
- target.contentTranslateY,
1043
+ canonicalTarget.contentTranslateX,
1044
+ canonicalTarget.contentTranslateY,
519
1045
  Math.max(0.0001, animation.mass ?? 1),
520
1046
  Math.max(0.0001, animation.stiffness ?? 100),
521
1047
  Math.max(0, animation.damping ?? 10),
@@ -524,6 +1050,74 @@ export function useSmoothClipDriver(
524
1050
  reduceMotion,
525
1051
  startedAtMs
526
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
+ );
527
1121
  } else {
528
1122
  animationId = animateKeyframesHostFunction(
529
1123
  driverId,
@@ -540,12 +1134,16 @@ export function useSmoothClipDriver(
540
1134
  targetClip.width,
541
1135
  targetClip.height,
542
1136
  targetClip.radius,
543
- target.contentTranslateX,
544
- target.contentTranslateY,
1137
+ canonicalTarget.contentTranslateX,
1138
+ canonicalTarget.contentTranslateY,
545
1139
  Math.max(0, animation.duration),
546
1140
  // Non-null here: the validation gate above rejected null before any
547
1141
  // side effect.
548
- keyframeValues as number[],
1142
+ flattenV1Keyframes(
1143
+ (keyframeResult as NonNullable<typeof keyframeResult>)
1144
+ .canonicalFrames,
1145
+ animation.frames.map((frame) => frame.offset)
1146
+ ),
549
1147
  reduceMotion,
550
1148
  startedAtMs
551
1149
  );
@@ -559,12 +1157,19 @@ export function useSmoothClipDriver(
559
1157
  // effect, with a fresh id and one finished:false completion once the
560
1158
  // native entry exists — in the pre-seed window rejectAnimation has no
561
1159
  // entry and returns the bare 0 sentinel with no completion instead.
562
- ownership.value = INTERACTIVE;
563
- presentation.value = start;
564
- if (scalarsWereStale) scalarsStale.value = 1;
565
- 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
+ }
566
1166
  return rejectAnimationHostFunction(driverId);
567
1167
  }
1168
+ if (useV2) {
1169
+ ownership.value = NATIVE;
1170
+ presentation.value = canonicalTarget;
1171
+ scalarsStale.value = 0;
1172
+ }
568
1173
  activeAnimationId.value = animationId;
569
1174
  return animationId;
570
1175
  };
@@ -572,19 +1177,28 @@ export function useSmoothClipDriver(
572
1177
  const cancelOnUI = (
573
1178
  animationId = 0,
574
1179
  behavior: 'current' | 'target' = 'current'
575
- ): SmoothClipPresentation => {
1180
+ ): CanonicalSmoothClipPresentation => {
576
1181
  'worklet';
577
1182
  // Past the hook's cleanup the native entry is gone; the call would fail
578
1183
  // defined (handled=false) but it is the last driver.ui path that still
579
1184
  // crosses into native after the tombstone. Guard it like the rest.
580
- if (disposed.value !== 0) return presentation.value;
581
- const values = cancelAnimationHostFunction(
582
- driverId,
583
- animationId,
584
- behavior === 'target' ? 1 : 0
585
- );
586
- if (values.length < 8 || values[0] !== 1) return presentation.value;
587
- 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);
588
1202
  activeAnimationId.value = 0;
589
1203
  ownership.value = INTERACTIVE;
590
1204
  scalarsStale.value = 0;
@@ -627,6 +1241,37 @@ export function useSmoothClipDriver(
627
1241
  contentTranslateY
628
1242
  );
629
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
+ },
630
1275
  animateTo(target, animation) {
631
1276
  'worklet';
632
1277
  if (isRNRuntime()) return uiOnly();
@@ -641,7 +1286,7 @@ export function useSmoothClipDriver(
641
1286
  react: {
642
1287
  beginInteraction() {
643
1288
  const { requestId, promise } =
644
- createReactRequest<SmoothClipPresentation>(driverId);
1289
+ createReactRequest<CanonicalSmoothClipPresentation>(driverId);
645
1290
  scheduleOnUI(() => {
646
1291
  'worklet';
647
1292
  const result = beginOnUI();
@@ -688,7 +1333,7 @@ export function useSmoothClipDriver(
688
1333
  },
689
1334
  cancel(animationId = 0, behavior = 'current') {
690
1335
  const { requestId, promise } =
691
- createReactRequest<SmoothClipPresentation>(driverId, true);
1336
+ createReactRequest<CanonicalSmoothClipPresentation>(driverId, true);
692
1337
  scheduleOnUI(() => {
693
1338
  'worklet';
694
1339
  const result = cancelOnUI(animationId, behavior);
@@ -703,6 +1348,13 @@ export function useSmoothClipDriver(
703
1348
  return promise;
704
1349
  },
705
1350
  },
1351
+ __smoothClipHandle: {
1352
+ driverId,
1353
+ presentation,
1354
+ ownership,
1355
+ activeAnimationId,
1356
+ disposed,
1357
+ },
706
1358
  };
707
1359
 
708
1360
  setDriverState(
@@ -754,7 +1406,9 @@ export function useSmoothClipDriver(
754
1406
  active: SharedValue<number>,
755
1407
  stale: SharedValue<number>,
756
1408
  gone: SharedValue<number>,
757
- setter: typeof setPresentationHostFunction
1409
+ setter: typeof setPresentationHostFunction,
1410
+ setterV2: typeof setPresentationV2HostFunction,
1411
+ protocolVersion: number
758
1412
  ) => {
759
1413
  'worklet';
760
1414
  // First, before the listener or the seed: this effect run owns the
@@ -763,10 +1417,8 @@ export function useSmoothClipDriver(
763
1417
  gone.value = 0;
764
1418
  let last: SmoothClipPresentation | null = null;
765
1419
  const deliver = (next: SmoothClipPresentation) => {
766
- if (
767
- !isFiniteClipPresentation(next) ||
768
- clipPresentationEquals(last, next)
769
- ) {
1420
+ const canonical = canonicalizeClipPresentation(next);
1421
+ if (canonical === null || clipPresentationEquals(last, canonical)) {
770
1422
  return;
771
1423
  }
772
1424
  // Record `last` only for values native has actually observed:
@@ -775,26 +1427,51 @@ export function useSmoothClipDriver(
775
1427
  if (owner.value !== INTERACTIVE) return;
776
1428
  if (suppressed.value > 0) {
777
1429
  suppressed.value -= 1;
778
- last = next;
1430
+ last = canonical;
1431
+ return;
1432
+ }
1433
+ if (protocolVersion !== 2 && !isV1Compatible(canonical)) {
779
1434
  return;
780
1435
  }
781
- last = next;
1436
+ last = canonical;
782
1437
  if (stale.value !== 0) stale.value = 0;
783
- const { clip, contentTranslateX, contentTranslateY } = next;
1438
+ const { clip, contentTranslateX, contentTranslateY, contentScale } =
1439
+ canonical;
784
1440
  // Declarative deliveries always record velocity samples — only the
785
1441
  // setScalars hot path is gated by the velocityTracking option.
786
- setter(
787
- nativeDriverId,
788
- clip.x,
789
- clip.y,
790
- clip.width,
791
- clip.height,
792
- clip.radius,
793
- contentTranslateX,
794
- contentTranslateY,
795
- false,
796
- false
797
- );
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
+ }
798
1475
  };
799
1476
  source.addListener(listenerId, deliver);
800
1477
  // Authoritative take-ownership seed. Creates the native entry before
@@ -803,24 +1480,45 @@ export function useSmoothClipDriver(
803
1480
  // An animation worklet can win the scheduling race and create a latch
804
1481
  // first; Native ownership then marks that intent as newer, so this
805
1482
  // passive seed must not clear its active id or replay the target value.
806
- const current = source.value;
807
- if (owner.value === INTERACTIVE && isFiniteClipPresentation(current)) {
1483
+ const current = canonicalizeClipPresentation(source.value);
1484
+ if (owner.value === INTERACTIVE && current !== null) {
808
1485
  active.value = 0;
809
1486
  owner.value = INTERACTIVE;
810
1487
  stale.value = 0;
811
1488
  last = current;
812
- setter(
813
- nativeDriverId,
814
- current.clip.x,
815
- current.clip.y,
816
- current.clip.width,
817
- current.clip.height,
818
- current.clip.radius,
819
- current.contentTranslateX,
820
- current.contentTranslateY,
821
- true,
822
- false
823
- );
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
+ }
824
1522
  }
825
1523
  },
826
1524
  presentation,
@@ -831,7 +1529,9 @@ export function useSmoothClipDriver(
831
1529
  activeAnimationId,
832
1530
  scalarsStale,
833
1531
  disposed,
834
- setPresentationHostFunction
1532
+ setPresentationHostFunction,
1533
+ setPresentationV2HostFunction,
1534
+ PRESENTATION_PROTOCOL_VERSION
835
1535
  );
836
1536
 
837
1537
  return () => {