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
@@ -21,8 +21,15 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
21
21
  var width: Double = 0.0,
22
22
  var height: Double = 0.0,
23
23
  var radius: Double = 0.0,
24
+ var presentationVersion: Int = 1,
25
+ var topLeftRadius: Double = 0.0,
26
+ var topRightRadius: Double = 0.0,
27
+ var bottomRightRadius: Double = 0.0,
28
+ var bottomLeftRadius: Double = 0.0,
29
+ var curveCode: Int = CLIP_CURVE_CIRCULAR,
24
30
  var contentTranslateX: Double = 0.0,
25
31
  var contentTranslateY: Double = 0.0,
32
+ var contentScale: Double = 1.0,
26
33
  var driverId: Double = 0.0,
27
34
  // Whether any prop setter ran in the current update transaction.
28
35
  var dirty: Boolean = false,
@@ -95,6 +102,36 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
95
102
  pending(view).radius = value
96
103
  }
97
104
 
105
+ @ReactProp(name = "presentationVersion")
106
+ override fun setPresentationVersion(view: SmoothClipView, value: Int) {
107
+ pending(view).presentationVersion = value
108
+ }
109
+
110
+ @ReactProp(name = "initialClipTopLeftRadius")
111
+ override fun setInitialClipTopLeftRadius(view: SmoothClipView, value: Double) {
112
+ pending(view).topLeftRadius = value
113
+ }
114
+
115
+ @ReactProp(name = "initialClipTopRightRadius")
116
+ override fun setInitialClipTopRightRadius(view: SmoothClipView, value: Double) {
117
+ pending(view).topRightRadius = value
118
+ }
119
+
120
+ @ReactProp(name = "initialClipBottomRightRadius")
121
+ override fun setInitialClipBottomRightRadius(view: SmoothClipView, value: Double) {
122
+ pending(view).bottomRightRadius = value
123
+ }
124
+
125
+ @ReactProp(name = "initialClipBottomLeftRadius")
126
+ override fun setInitialClipBottomLeftRadius(view: SmoothClipView, value: Double) {
127
+ pending(view).bottomLeftRadius = value
128
+ }
129
+
130
+ @ReactProp(name = "initialClipCurve")
131
+ override fun setInitialClipCurve(view: SmoothClipView, value: Int) {
132
+ pending(view).curveCode = value
133
+ }
134
+
98
135
  @ReactProp(name = "initialContentTranslateX")
99
136
  override fun setInitialContentTranslateX(view: SmoothClipView, value: Double) {
100
137
  pending(view).contentTranslateX = value
@@ -105,6 +142,11 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
105
142
  pending(view).contentTranslateY = value
106
143
  }
107
144
 
145
+ @ReactProp(name = "initialContentScale")
146
+ override fun setInitialContentScale(view: SmoothClipView, value: Double) {
147
+ pending(view).contentScale = value
148
+ }
149
+
108
150
  override fun setClipGeometry(
109
151
  view: SmoothClipView,
110
152
  x: Double,
@@ -139,6 +181,38 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
139
181
  )
140
182
  }
141
183
 
184
+ override fun setClipPresentationV2(
185
+ view: SmoothClipView,
186
+ x: Double,
187
+ y: Double,
188
+ width: Double,
189
+ height: Double,
190
+ topLeftRadius: Double,
191
+ topRightRadius: Double,
192
+ bottomRightRadius: Double,
193
+ bottomLeftRadius: Double,
194
+ curveCode: Int,
195
+ contentTranslateX: Double,
196
+ contentTranslateY: Double,
197
+ contentScale: Double,
198
+ ) {
199
+ view.commandIsAuthoritative = true
200
+ view.setClipPresentationV2Dip(
201
+ x,
202
+ y,
203
+ width,
204
+ height,
205
+ topLeftRadius,
206
+ topRightRadius,
207
+ bottomRightRadius,
208
+ bottomLeftRadius,
209
+ curveCode,
210
+ contentTranslateX,
211
+ contentTranslateY,
212
+ contentScale,
213
+ )
214
+ }
215
+
142
216
  override fun onAfterUpdateTransaction(view: SmoothClipView) {
143
217
  super.onAfterUpdateTransaction(view)
144
218
  val initial = pendingInitialClips[view]
@@ -154,33 +228,73 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
154
228
  }
155
229
  view.boundDriverId = initial.driverId
156
230
  if (initial.driverId != 0.0) {
157
- SmoothClipBindings.nativeRegisterView(
158
- initial.driverId,
159
- view,
160
- initial.x,
161
- initial.y,
162
- initial.width,
163
- initial.height,
164
- initial.radius,
165
- initial.contentTranslateX,
166
- initial.contentTranslateY,
167
- view.densityScale(),
168
- view.width.toDouble(),
169
- view.height.toDouble(),
170
- view.isHostLifecycleVisible(),
171
- )
231
+ if (initial.presentationVersion >= 2) {
232
+ SmoothClipBindings.nativeRegisterViewV2(
233
+ initial.driverId,
234
+ view,
235
+ initial.x,
236
+ initial.y,
237
+ initial.width,
238
+ initial.height,
239
+ initial.topLeftRadius,
240
+ initial.topRightRadius,
241
+ initial.bottomRightRadius,
242
+ initial.bottomLeftRadius,
243
+ initial.curveCode,
244
+ initial.contentTranslateX,
245
+ initial.contentTranslateY,
246
+ initial.contentScale,
247
+ view.densityScale(),
248
+ view.width.toDouble(),
249
+ view.height.toDouble(),
250
+ view.isHostLifecycleVisible(),
251
+ )
252
+ } else {
253
+ SmoothClipBindings.nativeRegisterView(
254
+ initial.driverId,
255
+ view,
256
+ initial.x,
257
+ initial.y,
258
+ initial.width,
259
+ initial.height,
260
+ initial.radius,
261
+ initial.contentTranslateX,
262
+ initial.contentTranslateY,
263
+ view.densityScale(),
264
+ view.width.toDouble(),
265
+ view.height.toDouble(),
266
+ view.isHostLifecycleVisible(),
267
+ )
268
+ }
172
269
  } else if (!view.commandIsAuthoritative) {
173
270
  // Command-driven mode (driverId 0): the initial props are the only
174
271
  // source of the first clip, so apply them directly.
175
- view.setClipPresentationDip(
176
- initial.x,
177
- initial.y,
178
- initial.width,
179
- initial.height,
180
- initial.radius,
181
- initial.contentTranslateX,
182
- initial.contentTranslateY,
183
- )
272
+ if (initial.presentationVersion >= 2) {
273
+ view.setClipPresentationV2Dip(
274
+ initial.x,
275
+ initial.y,
276
+ initial.width,
277
+ initial.height,
278
+ initial.topLeftRadius,
279
+ initial.topRightRadius,
280
+ initial.bottomRightRadius,
281
+ initial.bottomLeftRadius,
282
+ initial.curveCode,
283
+ initial.contentTranslateX,
284
+ initial.contentTranslateY,
285
+ initial.contentScale,
286
+ )
287
+ } else {
288
+ view.setClipPresentationDip(
289
+ initial.x,
290
+ initial.y,
291
+ initial.width,
292
+ initial.height,
293
+ initial.radius,
294
+ initial.contentTranslateX,
295
+ initial.contentTranslateY,
296
+ )
297
+ }
184
298
  } else {
185
299
  view.reapplyClipPresentation()
186
300
  }
@@ -289,24 +289,154 @@ inline double springContinuationVelocity(
289
289
 
290
290
  // --- Presentation channels ------------------------------------------------
291
291
 
292
- constexpr std::size_t kChannelCount = 7;
292
+ // Eleven continuous V2 channels. The curve is categorical and is deliberately
293
+ // carried outside the scalar array.
294
+ constexpr std::size_t kChannelCount = 11;
293
295
  using Channels = std::array<double, kChannelCount>;
294
296
 
297
+ inline double resolvedRadius(double overrideValue, double shorthand) {
298
+ return std::isfinite(overrideValue) ? overrideValue : shorthand;
299
+ }
300
+
301
+ inline bool radiiAreUniform(
302
+ double topLeft,
303
+ double topRight,
304
+ double bottomRight,
305
+ double bottomLeft) {
306
+ return topLeft == topRight && topLeft == bottomRight &&
307
+ topLeft == bottomLeft;
308
+ }
309
+
295
310
  inline Channels toChannels(const Presentation &presentation) {
311
+ const double topLeft = resolvedRadius(
312
+ presentation.clip.topLeftRadius, presentation.clip.radius);
313
+ const double topRight = resolvedRadius(
314
+ presentation.clip.topRightRadius, presentation.clip.radius);
315
+ const double bottomRight = resolvedRadius(
316
+ presentation.clip.bottomRightRadius, presentation.clip.radius);
317
+ const double bottomLeft = resolvedRadius(
318
+ presentation.clip.bottomLeftRadius, presentation.clip.radius);
296
319
  return {presentation.clip.x,
297
320
  presentation.clip.y,
298
321
  presentation.clip.width,
299
322
  presentation.clip.height,
300
- presentation.clip.radius,
323
+ topLeft,
324
+ topRight,
325
+ bottomRight,
326
+ bottomLeft,
301
327
  presentation.contentTranslateX,
302
- presentation.contentTranslateY};
328
+ presentation.contentTranslateY,
329
+ presentation.contentScale};
330
+ }
331
+
332
+ // --- Protocol-V2 animation validation ------------------------------------
333
+ //
334
+ // Keep the invariants at the native ownership boundary as well as in JS. OTA
335
+ // callers and direct host-function users can bypass the TypeScript preflight;
336
+ // rejecting here prevents an invalid request from dissolving an existing
337
+ // animation group before the registry notices it cannot install the new plan.
338
+
339
+ inline bool isProtocolV2Curve(ClipCurve curve) {
340
+ return curve == ClipCurve::Circular || curve == ClipCurve::Continuous;
341
+ }
342
+
343
+ inline bool isFiniteProtocolV2Presentation(
344
+ const Presentation &presentation) {
345
+ const Channels channels = toChannels(presentation);
346
+ return std::all_of(
347
+ channels.begin(), channels.end(),
348
+ [](double value) { return std::isfinite(value); }) &&
349
+ isProtocolV2Curve(presentation.clip.curve) &&
350
+ presentation.contentScale > 0;
351
+ }
352
+
353
+ inline bool protocolV2PresentationsEqual(
354
+ const Presentation &first,
355
+ const Presentation &second) {
356
+ return first.clip.curve == second.clip.curve &&
357
+ toChannels(first) == toChannels(second);
358
+ }
359
+
360
+ inline bool isValidReduceMotionCode(int32_t value) {
361
+ return value >= 0 && value <= 2;
362
+ }
363
+
364
+ inline bool isValidProtocolV2Timing(const TimingAnimation &animation) {
365
+ return std::isfinite(animation.durationMs) && animation.durationMs >= 0 &&
366
+ std::isfinite(animation.controlPoint1X) &&
367
+ std::isfinite(animation.controlPoint1Y) &&
368
+ std::isfinite(animation.controlPoint2X) &&
369
+ std::isfinite(animation.controlPoint2Y) &&
370
+ animation.controlPoint1X >= 0 && animation.controlPoint1X <= 1 &&
371
+ animation.controlPoint2X >= 0 && animation.controlPoint2X <= 1 &&
372
+ isValidReduceMotionCode(animation.reduceMotion);
373
+ }
374
+
375
+ inline bool isValidProtocolV2Spring(const SpringAnimation &animation) {
376
+ return std::isfinite(animation.mass) && animation.mass > 0 &&
377
+ std::isfinite(animation.stiffness) && animation.stiffness > 0 &&
378
+ std::isfinite(animation.damping) && animation.damping >= 0 &&
379
+ std::isfinite(animation.initialVelocity) &&
380
+ isValidReduceMotionCode(animation.reduceMotion);
381
+ }
382
+
383
+ inline bool protocolV2SpringScaleIsProvablyPositive(
384
+ const Presentation &start,
385
+ const Presentation &target,
386
+ const SpringAnimation &animation) {
387
+ if (start.contentScale == target.contentScale) return true;
388
+ // With zero initial velocity, a critically/over-damped unit response is
389
+ // monotone, so it remains between two positive endpoints. Inherited or
390
+ // under-damped velocity can overshoot through zero and must be compiled to
391
+ // keyframes by the caller.
392
+ return !animation.inheritVelocity && animation.initialVelocity == 0 &&
393
+ animation.damping * animation.damping >=
394
+ 4 * animation.mass * animation.stiffness;
395
+ }
396
+
397
+ inline bool isValidProtocolV2Keyframes(
398
+ const std::vector<Keyframe> &keyframes,
399
+ const Presentation &resolvedStart,
400
+ const Presentation &target,
401
+ bool requireExplicitStart) {
402
+ if (keyframes.size() < 2 || keyframes.front().offset != 0 ||
403
+ keyframes.back().offset != 1 ||
404
+ !protocolV2PresentationsEqual(keyframes.back().presentation, target)) {
405
+ return false;
406
+ }
407
+ if (requireExplicitStart && !protocolV2PresentationsEqual(
408
+ keyframes.front().presentation,
409
+ resolvedStart)) {
410
+ return false;
411
+ }
412
+ double previousOffset = -1;
413
+ for (const Keyframe &keyframe : keyframes) {
414
+ if (!std::isfinite(keyframe.offset) || keyframe.offset <= previousOffset ||
415
+ keyframe.offset < 0 || keyframe.offset > 1 ||
416
+ !isFiniteProtocolV2Presentation(keyframe.presentation) ||
417
+ keyframe.presentation.clip.curve != target.clip.curve) {
418
+ return false;
419
+ }
420
+ previousOffset = keyframe.offset;
421
+ }
422
+ return resolvedStart.clip.curve == target.clip.curve;
303
423
  }
304
424
 
305
- inline Presentation fromChannels(const Channels &channels) {
306
- return Presentation{
307
- {channels[0], channels[1], channels[2], channels[3], channels[4]},
308
- channels[5],
309
- channels[6]};
425
+ inline Presentation fromChannels(
426
+ const Channels &channels,
427
+ ClipCurve curve = ClipCurve::Circular) {
428
+ Geometry geometry{
429
+ channels[0], channels[1], channels[2], channels[3], 0.0};
430
+ geometry.topLeftRadius = channels[4];
431
+ geometry.topRightRadius = channels[5];
432
+ geometry.bottomRightRadius = channels[6];
433
+ geometry.bottomLeftRadius = channels[7];
434
+ geometry.radius = radiiAreUniform(
435
+ channels[4], channels[5], channels[6], channels[7])
436
+ ? channels[4]
437
+ : 0.0;
438
+ geometry.curve = curve;
439
+ return Presentation{geometry, channels[8], channels[9], channels[10]};
310
440
  }
311
441
 
312
442
  inline Presentation interpolate(
@@ -316,14 +446,13 @@ inline Presentation interpolate(
316
446
  const auto mix = [progress](double start, double end) {
317
447
  return start + (end - start) * progress;
318
448
  };
319
- return Presentation{
320
- {mix(from.clip.x, to.clip.x),
321
- mix(from.clip.y, to.clip.y),
322
- mix(from.clip.width, to.clip.width),
323
- mix(from.clip.height, to.clip.height),
324
- mix(from.clip.radius, to.clip.radius)},
325
- mix(from.contentTranslateX, to.contentTranslateX),
326
- mix(from.contentTranslateY, to.contentTranslateY)};
449
+ Channels blended{};
450
+ const Channels fromChannelsValue = toChannels(from);
451
+ const Channels toChannelsValue = toChannels(to);
452
+ for (std::size_t index = 0; index < kChannelCount; index += 1) {
453
+ blended[index] = mix(fromChannelsValue[index], toChannelsValue[index]);
454
+ }
455
+ return fromChannels(blended, from.clip.curve);
327
456
  }
328
457
 
329
458
  // Freezes a timing animation at one already-rendered raw-time phase and
@@ -361,95 +490,14 @@ inline TimingContinuation timingContinuationAtFrame(
361
490
 
362
491
  // --- Keyframe curve -------------------------------------------------------
363
492
 
364
- // Keyframes sampled from a smooth path, evaluated with monotone cubic Hermite
365
- // interpolation (Fritsch-Carlson).
366
- //
367
- // Straight lerping between keyframes is exact at every keyframe and wrong in
368
- // between in a way that matters more than its position error suggests: the
369
- // interpolated VELOCITY is a staircase that steps at every keyframe boundary,
370
- // beside content whose Reanimated curve is continuous. Consumers bake dozens of
371
- // keyframes precisely because their geometry path is not affine in progress, so
372
- // reconstructing that path smoothly is the honest reading of their intent.
373
- //
374
- // Monotone rather than plain Catmull-Rom because the alternative can overshoot,
375
- // and these channels do not tolerate it: width, height and radius must never
376
- // dip below zero or bulge past the values the consumer asked for. The
377
- // Fritsch-Carlson tangent clamp keeps every segment inside the range of its own
378
- // endpoints. Two keyframes degenerate to the old straight line (both tangents
379
- // equal the secant) — equal in real arithmetic, within one ulp (~1e-13 of the
380
- // travel) in doubles because the Hermite blend orders its operations
381
- // differently than the lerp it replaced. The jfloat delivery cast erases the
382
- // difference long before a pixel could see it.
383
- //
384
- // PARITY NOTE: iOS builds a CAKeyframeAnimation with `kCAAnimationLinear`
385
- // (SmoothClipView.mm) and cannot run this evaluator — CoreAnimation interpolates
386
- // off-thread. The platforms therefore differ mid-segment by the linearization
387
- // error this removes, sub-pixel at any realistic keyframe density and zero at
388
- // every keyframe. Closing it needs either `kCAAnimationCubic` (a different
389
- // spline that CAN overshoot, so no) or resampling the keyframes densely through
390
- // this curve before handing them to CA.
493
+ // Keyframes interpolate segment-wise linearly on every platform. This is a
494
+ // public V2 contract: consumers compile unsupported springs/easings into exact
495
+ // scalar samples, and native must not reinterpret those samples as a spline.
391
496
  class KeyframeCurve {
392
497
  public:
393
498
  void reset(std::vector<Keyframe> frames) {
394
499
  frames_ = std::move(frames);
395
500
  cursor_ = 1;
396
- tangents_.assign(frames_.size(), Channels{});
397
- const std::size_t count = frames_.size();
398
- if (count < 2) return;
399
-
400
- // Secant slope of every segment, per channel. A non-increasing offset pair
401
- // (the re-latch remap can produce one) contributes no slope rather than
402
- // dividing by zero; evaluate() short-circuits those segments too.
403
- std::vector<Channels> secants(count - 1);
404
- for (std::size_t index = 0; index + 1 < count; index += 1) {
405
- const double span = frames_[index + 1].offset - frames_[index].offset;
406
- const Channels lower = toChannels(frames_[index].presentation);
407
- const Channels upper = toChannels(frames_[index + 1].presentation);
408
- for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
409
- secants[index][channel] =
410
- span > 0 ? (upper[channel] - lower[channel]) / span : 0.0;
411
- }
412
- }
413
-
414
- // One-sided at the ends, averaged inside.
415
- for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
416
- tangents_[0][channel] = secants[0][channel];
417
- tangents_[count - 1][channel] = secants[count - 2][channel];
418
- for (std::size_t index = 1; index + 1 < count; index += 1) {
419
- tangents_[index][channel] =
420
- (secants[index - 1][channel] + secants[index][channel]) / 2.0;
421
- }
422
- }
423
-
424
- // Fritsch-Carlson clamp: this is what makes the curve non-overshooting.
425
- for (std::size_t index = 0; index + 1 < count; index += 1) {
426
- for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
427
- const double secant = secants[index][channel];
428
- if (secant == 0.0) {
429
- // Flat segment: pin both ends flat so the curve cannot bulge off a
430
- // pair of equal keyframes.
431
- tangents_[index][channel] = 0.0;
432
- tangents_[index + 1][channel] = 0.0;
433
- continue;
434
- }
435
- double alpha = tangents_[index][channel] / secant;
436
- double beta = tangents_[index + 1][channel] / secant;
437
- if (alpha < 0.0) {
438
- tangents_[index][channel] = 0.0;
439
- alpha = 0.0;
440
- }
441
- if (beta < 0.0) {
442
- tangents_[index + 1][channel] = 0.0;
443
- beta = 0.0;
444
- }
445
- const double magnitude = alpha * alpha + beta * beta;
446
- if (magnitude > 9.0) {
447
- const double scale = 3.0 / std::sqrt(magnitude);
448
- tangents_[index][channel] = scale * alpha * secant;
449
- tangents_[index + 1][channel] = scale * beta * secant;
450
- }
451
- }
452
- }
453
501
  }
454
502
 
455
503
  const std::vector<Keyframe> &frames() const { return frames_; }
@@ -458,7 +506,7 @@ class KeyframeCurve {
458
506
 
459
507
  Presentation evaluate(double progress) {
460
508
  const std::size_t count = frames_.size();
461
- if (count == 0) return Presentation{{0, 0, 0, 0, 0}, 0, 0};
509
+ if (count == 0) return Presentation{{0, 0, 0, 0, 0}, 0, 0, 1};
462
510
  if (count == 1) return frames_[0].presentation;
463
511
 
464
512
  // The scan resumes from the previous segment instead of restarting at 1.
@@ -476,30 +524,11 @@ class KeyframeCurve {
476
524
  if (span <= 0) return higher.presentation;
477
525
 
478
526
  const double t = clamp01((progress - lower.offset) / span);
479
- const double t2 = t * t;
480
- const double t3 = t2 * t;
481
- // Cubic Hermite basis.
482
- const double h00 = 2 * t3 - 3 * t2 + 1;
483
- const double h10 = t3 - 2 * t2 + t;
484
- const double h01 = -2 * t3 + 3 * t2;
485
- const double h11 = t3 - t2;
486
-
487
- const Channels from = toChannels(lower.presentation);
488
- const Channels to = toChannels(higher.presentation);
489
- const Channels &fromSlope = tangents_[upper - 1];
490
- const Channels &toSlope = tangents_[upper];
491
- Channels blended{};
492
- for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
493
- blended[channel] = h00 * from[channel] +
494
- h10 * span * fromSlope[channel] + h01 * to[channel] +
495
- h11 * span * toSlope[channel];
496
- }
497
- return fromChannels(blended);
527
+ return interpolate(lower.presentation, higher.presentation, t);
498
528
  }
499
529
 
500
- private:
530
+ private:
501
531
  std::vector<Keyframe> frames_;
502
- std::vector<Channels> tangents_;
503
532
  std::size_t cursor_ = 1;
504
533
  };
505
534
 
@@ -7,18 +7,32 @@
7
7
 
8
8
  namespace smoothclip {
9
9
 
10
+ enum class ClipCurve : int32_t {
11
+ Circular = 0,
12
+ Continuous = 1,
13
+ };
14
+
10
15
  struct Geometry {
11
16
  double x;
12
17
  double y;
13
18
  double width;
14
19
  double height;
15
20
  double radius;
21
+ // NaN means "use radius". Keeping the overrides at the end preserves every
22
+ // existing five-value aggregate initializer used by the V1 implementation
23
+ // and its native tests.
24
+ double topLeftRadius = std::numeric_limits<double>::quiet_NaN();
25
+ double topRightRadius = std::numeric_limits<double>::quiet_NaN();
26
+ double bottomRightRadius = std::numeric_limits<double>::quiet_NaN();
27
+ double bottomLeftRadius = std::numeric_limits<double>::quiet_NaN();
28
+ ClipCurve curve = ClipCurve::Circular;
16
29
  };
17
30
 
18
31
  struct Presentation {
19
32
  Geometry clip;
20
33
  double contentTranslateX;
21
34
  double contentTranslateY;
35
+ double contentScale = 1.0;
22
36
  };
23
37
 
24
38
  struct Keyframe {
@@ -62,37 +76,121 @@ struct CancelResult {
62
76
  Presentation presentation;
63
77
  };
64
78
 
79
+ struct DriverSnapshot {
80
+ uint64_t driverId;
81
+ Presentation presentation;
82
+ bool ready;
83
+ };
84
+
85
+ struct BatchEntry {
86
+ uint64_t driverId;
87
+ Presentation presentation;
88
+ };
89
+
90
+ struct GroupMotionEntry {
91
+ uint64_t driverId;
92
+ bool hasFrom;
93
+ Presentation from;
94
+ Presentation target;
95
+ std::vector<Keyframe> keyframes;
96
+ };
97
+
98
+ enum class GroupSuspensionPolicy : int32_t {
99
+ Pause = 0,
100
+ Finish = 1,
101
+ };
102
+
103
+ enum class GroupCancelBehavior : int32_t {
104
+ Freeze = 0,
105
+ Finish = 1,
106
+ };
107
+
108
+ // The seven-scalar V1 bridge predates protocol-V2 host-normalization
109
+ // preflight and strict spring/duration rejection. Keep that wire contract
110
+ // selectable without widening any V1 entry point; V2 and grouped calls retain
111
+ // the conservative validation required for portable native ownership.
112
+ enum class AnimationValidationMode : int32_t {
113
+ LegacyV1 = 1,
114
+ ProtocolV2 = 2,
115
+ };
116
+
65
117
  using CompletionCallback =
66
118
  std::function<void(uint64_t, int32_t, bool)>;
119
+ using GroupCompletionCallback =
120
+ std::function<void(uint64_t, int32_t, bool, std::vector<uint64_t>)>;
67
121
 
68
122
  void setCompletionCallback(
69
123
  const void *owner,
70
124
  CompletionCallback callback);
71
125
  void clearCompletionCallback(const void *owner);
126
+ void setGroupCompletionCallback(
127
+ const void *owner,
128
+ GroupCompletionCallback callback);
129
+ void clearGroupCompletionCallback(const void *owner);
72
130
 
131
+ // recordVelocity gates the 'inherit' velocity-sample recording for this write.
132
+ // Default true (the pre-flag behavior); Android's JS layer passes false for
133
+ // setScalars hot writes on drivers without SmoothClipDriverOptions
134
+ // .velocityTracking, so the per-frame drag stream skips the clock read and
135
+ // channel copies. iOS callers never pass it and keep recording always.
73
136
  void setPresentation(
74
137
  uint64_t driverId,
75
138
  Presentation presentation,
76
139
  bool takeOwnership,
77
- bool overridePendingAnimation = false);
140
+ bool overridePendingAnimation = false,
141
+ bool recordVelocity = true);
78
142
  Presentation beginInteraction(uint64_t driverId);
143
+ Presentation snapshotCurrent(uint64_t driverId);
144
+ std::vector<DriverSnapshot> beginGroupInteraction(
145
+ const std::vector<uint64_t> &driverIds);
146
+ std::vector<DriverSnapshot> snapshotGroup(
147
+ const std::vector<uint64_t> &driverIds);
148
+ bool setPresentationBatch(const std::vector<BatchEntry> &entries);
149
+ int32_t animateTimingGroup(
150
+ uint64_t controllerId,
151
+ std::vector<GroupMotionEntry> entries,
152
+ TimingAnimation animation,
153
+ GroupSuspensionPolicy suspensionPolicy,
154
+ double startedAtHintS = std::numeric_limits<double>::quiet_NaN());
155
+ int32_t animateSpringGroup(
156
+ uint64_t controllerId,
157
+ std::vector<GroupMotionEntry> entries,
158
+ SpringAnimation animation,
159
+ GroupSuspensionPolicy suspensionPolicy,
160
+ double startedAtHintS = std::numeric_limits<double>::quiet_NaN());
161
+ int32_t animateKeyframesGroup(
162
+ uint64_t controllerId,
163
+ std::vector<GroupMotionEntry> entries,
164
+ double durationMs,
165
+ int32_t reduceMotion,
166
+ GroupSuspensionPolicy suspensionPolicy,
167
+ double startedAtHintS = std::numeric_limits<double>::quiet_NaN());
168
+ std::vector<DriverSnapshot> cancelAnimationGroup(
169
+ int32_t groupId,
170
+ GroupCancelBehavior behavior);
79
171
  int32_t animateTiming(
80
172
  uint64_t driverId,
81
173
  AnimationStart start,
82
174
  Presentation presentation,
83
- TimingAnimation animation);
175
+ TimingAnimation animation,
176
+ AnimationValidationMode validationMode =
177
+ AnimationValidationMode::ProtocolV2);
84
178
  int32_t animateSpring(
85
179
  uint64_t driverId,
86
180
  AnimationStart start,
87
181
  Presentation presentation,
88
- SpringAnimation animation);
182
+ SpringAnimation animation,
183
+ AnimationValidationMode validationMode =
184
+ AnimationValidationMode::ProtocolV2);
89
185
  int32_t animateKeyframes(
90
186
  uint64_t driverId,
91
187
  AnimationStart start,
92
188
  Presentation presentation,
93
189
  double durationMs,
94
190
  std::vector<Keyframe> keyframes,
95
- int32_t reduceMotion);
191
+ int32_t reduceMotion,
192
+ AnimationValidationMode validationMode =
193
+ AnimationValidationMode::ProtocolV2);
96
194
  int32_t rejectAnimation(uint64_t driverId);
97
195
  CancelResult cancelAnimation(
98
196
  uint64_t driverId,