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
@@ -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,13 +76,57 @@ 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
 
73
131
  // recordVelocity gates the 'inherit' velocity-sample recording for this write.
74
132
  // Default true (the pre-flag behavior); Android's JS layer passes false for
@@ -82,23 +140,57 @@ void setPresentation(
82
140
  bool overridePendingAnimation = false,
83
141
  bool recordVelocity = true);
84
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);
85
171
  int32_t animateTiming(
86
172
  uint64_t driverId,
87
173
  AnimationStart start,
88
174
  Presentation presentation,
89
- TimingAnimation animation);
175
+ TimingAnimation animation,
176
+ AnimationValidationMode validationMode =
177
+ AnimationValidationMode::ProtocolV2);
90
178
  int32_t animateSpring(
91
179
  uint64_t driverId,
92
180
  AnimationStart start,
93
181
  Presentation presentation,
94
- SpringAnimation animation);
182
+ SpringAnimation animation,
183
+ AnimationValidationMode validationMode =
184
+ AnimationValidationMode::ProtocolV2);
95
185
  int32_t animateKeyframes(
96
186
  uint64_t driverId,
97
187
  AnimationStart start,
98
188
  Presentation presentation,
99
189
  double durationMs,
100
190
  std::vector<Keyframe> keyframes,
101
- int32_t reduceMotion);
191
+ int32_t reduceMotion,
192
+ AnimationValidationMode validationMode =
193
+ AnimationValidationMode::ProtocolV2);
102
194
  int32_t rejectAnimation(uint64_t driverId);
103
195
  CancelResult cancelAnimation(
104
196
  uint64_t driverId,
@@ -1,5 +1,7 @@
1
1
  #pragma once
2
2
 
3
+ #include "SmoothClipRegistry.h"
4
+
3
5
  #include <algorithm>
4
6
  #include <cmath>
5
7
 
@@ -11,23 +13,110 @@ struct NormalizedClip {
11
13
  double right = 0;
12
14
  double bottom = 0;
13
15
  double radius = 0;
16
+ double topLeftRadius = 0;
17
+ double topRightRadius = 0;
18
+ double bottomRightRadius = 0;
19
+ double bottomLeftRadius = 0;
20
+ ClipCurve curve = ClipCurve::Circular;
14
21
  };
15
22
 
16
- // Shared clip normalization emitting edges (Android's Outline consumes
17
- // edges). Mirrors ios/SmoothClipGeometry.h (SmoothClipNormalizeGeometry) and
18
- // ClipGeometryNormalizer.kt (normalizeClipGeometryPx); a change here must
19
- // land in all three.
20
- inline bool SmoothClipNormalize(
23
+ inline double SmoothClipResolvedRadius(double overrideValue, double radius) {
24
+ return std::isfinite(overrideValue) ? overrideValue : radius;
25
+ }
26
+
27
+ /**
28
+ * Returns true only when host normalization is the identity for this geometry.
29
+ *
30
+ * Timing/keyframe renderers interpolate already-normalized native values on
31
+ * iOS, while Android interpolates presentation scalars and normalizes each
32
+ * delivered frame. Those operations commute exactly when every anchor stays
33
+ * inside one host and the CSS corner-overlap rule never activates. Because
34
+ * each condition below is linear, it also holds for every convex interpolation
35
+ * between accepted anchors.
36
+ *
37
+ * Static clipping deliberately does not use this gate: out-of-bounds geometry
38
+ * remains valid and is still clipped by SmoothClipNormalizeV2.
39
+ */
40
+ inline bool SmoothClipGeometryNormalizationIsIdentity(
41
+ const Geometry &geometry,
42
+ double hostWidth,
43
+ double hostHeight) {
44
+ const double topLeft = SmoothClipResolvedRadius(
45
+ geometry.topLeftRadius, geometry.radius);
46
+ const double topRight = SmoothClipResolvedRadius(
47
+ geometry.topRightRadius, geometry.radius);
48
+ const double bottomRight = SmoothClipResolvedRadius(
49
+ geometry.bottomRightRadius, geometry.radius);
50
+ const double bottomLeft = SmoothClipResolvedRadius(
51
+ geometry.bottomLeftRadius, geometry.radius);
52
+ const double right = geometry.x + geometry.width;
53
+ const double bottom = geometry.y + geometry.height;
54
+ const double topRadii = topLeft + topRight;
55
+ const double bottomRadii = bottomLeft + bottomRight;
56
+ const double leftRadii = topLeft + bottomLeft;
57
+ const double rightRadii = topRight + bottomRight;
58
+ const double values[] = {
59
+ geometry.x,
60
+ geometry.y,
61
+ geometry.width,
62
+ geometry.height,
63
+ geometry.radius,
64
+ topLeft,
65
+ topRight,
66
+ bottomRight,
67
+ bottomLeft,
68
+ right,
69
+ bottom,
70
+ topRadii,
71
+ bottomRadii,
72
+ leftRadii,
73
+ rightRadii,
74
+ hostWidth,
75
+ hostHeight,
76
+ };
77
+ for (const double value : values) {
78
+ if (!std::isfinite(value)) return false;
79
+ }
80
+ return hostWidth >= 0 && hostHeight >= 0 &&
81
+ geometry.x >= 0 && geometry.y >= 0 &&
82
+ geometry.width >= 0 && geometry.height >= 0 &&
83
+ right <= hostWidth && bottom <= hostHeight &&
84
+ topLeft >= 0 && topRight >= 0 &&
85
+ bottomRight >= 0 && bottomLeft >= 0 &&
86
+ topRadii <= geometry.width &&
87
+ bottomRadii <= geometry.width &&
88
+ leftRadii <= geometry.height &&
89
+ rightRadii <= geometry.height;
90
+ }
91
+
92
+ inline bool SmoothClipNormalizeV2(
21
93
  double x,
22
94
  double y,
23
95
  double width,
24
96
  double height,
25
97
  double radius,
98
+ double topLeftRadius,
99
+ double topRightRadius,
100
+ double bottomRightRadius,
101
+ double bottomLeftRadius,
102
+ ClipCurve curve,
26
103
  double hostWidth,
27
104
  double hostHeight,
28
105
  NormalizedClip &out) {
106
+ const double resolvedTopLeft =
107
+ SmoothClipResolvedRadius(topLeftRadius, radius);
108
+ const double resolvedTopRight =
109
+ SmoothClipResolvedRadius(topRightRadius, radius);
110
+ const double resolvedBottomRight =
111
+ SmoothClipResolvedRadius(bottomRightRadius, radius);
112
+ const double resolvedBottomLeft =
113
+ SmoothClipResolvedRadius(bottomLeftRadius, radius);
29
114
  if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(width) ||
30
115
  !std::isfinite(height) || !std::isfinite(radius) ||
116
+ !std::isfinite(resolvedTopLeft) ||
117
+ !std::isfinite(resolvedTopRight) ||
118
+ !std::isfinite(resolvedBottomRight) ||
119
+ !std::isfinite(resolvedBottomLeft) ||
31
120
  !std::isfinite(hostWidth) || !std::isfinite(hostHeight)) {
32
121
  return false;
33
122
  }
@@ -42,9 +131,59 @@ inline bool SmoothClipNormalize(
42
131
  out.bottom = std::min(boundedHostHeight, std::max(0.0, requestedBottom));
43
132
  const double visibleWidth = std::max(0.0, out.right - out.left);
44
133
  const double visibleHeight = std::max(0.0, out.bottom - out.top);
45
- out.radius = std::min(
46
- std::max(0.0, radius), std::min(visibleWidth, visibleHeight) / 2.0);
134
+
135
+ double topLeft = std::max(0.0, resolvedTopLeft);
136
+ double topRight = std::max(0.0, resolvedTopRight);
137
+ double bottomRight = std::max(0.0, resolvedBottomRight);
138
+ double bottomLeft = std::max(0.0, resolvedBottomLeft);
139
+ double scale = 1.0;
140
+ const auto includeLimit = [&scale](double available, double requested) {
141
+ if (requested > 0) scale = std::min(scale, available / requested);
142
+ };
143
+ includeLimit(visibleWidth, topLeft + topRight);
144
+ includeLimit(visibleWidth, bottomLeft + bottomRight);
145
+ includeLimit(visibleHeight, topLeft + bottomLeft);
146
+ includeLimit(visibleHeight, topRight + bottomRight);
147
+ scale = std::max(0.0, std::min(1.0, scale));
148
+ out.topLeftRadius = topLeft * scale;
149
+ out.topRightRadius = topRight * scale;
150
+ out.bottomRightRadius = bottomRight * scale;
151
+ out.bottomLeftRadius = bottomLeft * scale;
152
+ const bool uniform = out.topLeftRadius == out.topRightRadius &&
153
+ out.topLeftRadius == out.bottomRightRadius &&
154
+ out.topLeftRadius == out.bottomLeftRadius;
155
+ out.radius = uniform ? out.topLeftRadius : 0.0;
156
+ out.curve = curve;
47
157
  return true;
48
158
  }
49
159
 
160
+ // Shared clip normalization emitting edges (Android's Outline consumes
161
+ // edges). Mirrors ios/SmoothClipGeometry.h (SmoothClipNormalizeGeometry) and
162
+ // ClipGeometryNormalizer.kt (normalizeClipGeometryPx); a change here must
163
+ // land in all three.
164
+ inline bool SmoothClipNormalize(
165
+ double x,
166
+ double y,
167
+ double width,
168
+ double height,
169
+ double radius,
170
+ double hostWidth,
171
+ double hostHeight,
172
+ NormalizedClip &out) {
173
+ return SmoothClipNormalizeV2(
174
+ x,
175
+ y,
176
+ width,
177
+ height,
178
+ radius,
179
+ radius,
180
+ radius,
181
+ radius,
182
+ radius,
183
+ ClipCurve::Circular,
184
+ hostWidth,
185
+ hostHeight,
186
+ out);
187
+ }
188
+
50
189
  } // namespace smoothclip