react-native-smooth-clip-view 0.3.0 → 0.4.1

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 (199) hide show
  1. package/README.md +135 -386
  2. package/android/build.gradle +4 -7
  3. package/android/proguard-rules.pro +2 -2
  4. package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +369 -486
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
  12. package/cpp/SmoothClipAnimationCurve.h +237 -346
  13. package/cpp/SmoothClipRegistry.h +24 -52
  14. package/cpp/SmoothClipSharedGeometry.h +64 -94
  15. package/cpp/SmoothClipVelocityTracker.h +5 -5
  16. package/ios/SmoothClipGeometry.h +23 -34
  17. package/ios/SmoothClipRegistry.mm +206 -711
  18. package/ios/SmoothClipTurboModule.cpp +217 -715
  19. package/ios/SmoothClipTurboModule.h +54 -297
  20. package/ios/SmoothClipView.mm +599 -505
  21. package/ios/SmoothClipViewRegistry.h +2 -7
  22. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  23. package/lib/module/SmoothClipView.ios.js +84 -17
  24. package/lib/module/SmoothClipView.ios.js.map +1 -1
  25. package/lib/module/SmoothClipView.js +1 -1
  26. package/lib/module/SmoothClipView.js.map +1 -1
  27. package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
  28. package/lib/module/capabilities.native.js +1 -19
  29. package/lib/module/capabilities.native.js.map +1 -1
  30. package/lib/module/capabilityTypes.js +0 -9
  31. package/lib/module/capabilityTypes.js.map +1 -1
  32. package/lib/module/controllerInternals.js +28 -0
  33. package/lib/module/controllerInternals.js.map +1 -0
  34. package/lib/module/controllerLifecycle.js +9 -0
  35. package/lib/module/controllerLifecycle.js.map +1 -0
  36. package/lib/module/controllerTypes.js +4 -0
  37. package/lib/module/controllerTypes.js.map +1 -0
  38. package/lib/module/controllers.android.js +4 -0
  39. package/lib/module/controllers.android.js.map +1 -0
  40. package/lib/module/controllers.ios.js +4 -0
  41. package/lib/module/controllers.ios.js.map +1 -0
  42. package/lib/module/controllers.js +4 -0
  43. package/lib/module/controllers.js.map +1 -0
  44. package/lib/module/controllers.native.js +89 -0
  45. package/lib/module/controllers.native.js.map +1 -0
  46. package/lib/module/geometry.js +71 -51
  47. package/lib/module/geometry.js.map +1 -1
  48. package/lib/module/groupTypes.js +4 -0
  49. package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
  50. package/lib/module/groups.android.js +4 -0
  51. package/lib/module/groups.android.js.map +1 -0
  52. package/lib/module/groups.ios.js +4 -0
  53. package/lib/module/groups.ios.js.map +1 -0
  54. package/lib/module/groups.js +4 -0
  55. package/lib/module/groups.js.map +1 -0
  56. package/lib/module/groups.native.js +399 -0
  57. package/lib/module/groups.native.js.map +1 -0
  58. package/lib/module/ids.js +8 -0
  59. package/lib/module/ids.js.map +1 -0
  60. package/lib/module/index.js +3 -3
  61. package/lib/module/index.js.map +1 -1
  62. package/lib/module/nativeCompletion.js +37 -16
  63. package/lib/module/nativeCompletion.js.map +1 -1
  64. package/lib/module/presentationCodec.js +81 -0
  65. package/lib/module/presentationCodec.js.map +1 -0
  66. package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
  67. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  68. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  69. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  70. package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
  71. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  72. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
  73. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  74. package/lib/typescript/src/capabilities.native.d.ts +1 -1
  75. package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
  76. package/lib/typescript/src/capabilityTypes.d.ts +0 -6
  77. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
  78. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  79. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  80. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  81. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  82. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  83. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/controllers.android.d.ts +2 -0
  85. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/controllers.d.ts +2 -0
  87. package/lib/typescript/src/controllers.d.ts.map +1 -0
  88. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  89. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/controllers.native.d.ts +3 -0
  91. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/geometry.d.ts +22 -12
  93. package/lib/typescript/src/geometry.d.ts.map +1 -1
  94. package/lib/typescript/src/groupTypes.d.ts +31 -0
  95. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  96. package/lib/typescript/src/groups.android.d.ts +2 -0
  97. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  98. package/lib/typescript/src/groups.d.ts +2 -0
  99. package/lib/typescript/src/groups.d.ts.map +1 -0
  100. package/lib/typescript/src/groups.ios.d.ts +2 -0
  101. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  102. package/lib/typescript/src/groups.native.d.ts +3 -0
  103. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  104. package/lib/typescript/src/ids.d.ts +2 -0
  105. package/lib/typescript/src/ids.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +6 -6
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  109. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  110. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  111. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  112. package/package.json +8 -3
  113. package/src/NativeSmoothClipModule.ts +41 -292
  114. package/src/SmoothClipView.ios.tsx +130 -23
  115. package/src/SmoothClipView.tsx +5 -1
  116. package/src/SmoothClipViewNativeComponent.ts +20 -26
  117. package/src/capabilities.native.ts +2 -52
  118. package/src/capabilityTypes.ts +0 -14
  119. package/src/controllerInternals.ts +52 -0
  120. package/src/controllerLifecycle.ts +6 -0
  121. package/src/controllerTypes.ts +87 -0
  122. package/src/controllers.android.ts +1 -0
  123. package/src/controllers.ios.ts +1 -0
  124. package/src/controllers.native.ts +104 -0
  125. package/src/controllers.ts +1 -0
  126. package/src/geometry.ts +118 -74
  127. package/src/groupTypes.ts +53 -0
  128. package/src/groups.android.ts +1 -0
  129. package/src/groups.ios.ts +1 -0
  130. package/src/groups.native.ts +604 -0
  131. package/src/groups.ts +1 -0
  132. package/src/ids.ts +6 -0
  133. package/src/index.ts +20 -28
  134. package/src/nativeCompletion.ts +67 -32
  135. package/src/presentationCodec.ts +134 -0
  136. package/lib/module/driverState.js +0 -82
  137. package/lib/module/driverState.js.map +0 -1
  138. package/lib/module/driverTypes.js +0 -4
  139. package/lib/module/driverTypes.js.map +0 -1
  140. package/lib/module/drivers.android.js +0 -6
  141. package/lib/module/drivers.android.js.map +0 -1
  142. package/lib/module/drivers.ios.js +0 -6
  143. package/lib/module/drivers.ios.js.map +0 -1
  144. package/lib/module/drivers.js +0 -4
  145. package/lib/module/drivers.js.map +0 -1
  146. package/lib/module/drivers.native.js +0 -756
  147. package/lib/module/drivers.native.js.map +0 -1
  148. package/lib/module/groupDriverTypes.js +0 -4
  149. package/lib/module/groupDrivers.android.js +0 -4
  150. package/lib/module/groupDrivers.android.js.map +0 -1
  151. package/lib/module/groupDrivers.ios.js +0 -4
  152. package/lib/module/groupDrivers.ios.js.map +0 -1
  153. package/lib/module/groupDrivers.js +0 -4
  154. package/lib/module/groupDrivers.js.map +0 -1
  155. package/lib/module/groupDrivers.native.js +0 -799
  156. package/lib/module/groupDrivers.native.js.map +0 -1
  157. package/lib/module/presentationProtocol.js +0 -15
  158. package/lib/module/presentationProtocol.js.map +0 -1
  159. package/lib/module/reactRequests.js +0 -56
  160. package/lib/module/reactRequests.js.map +0 -1
  161. package/lib/typescript/src/driverState.d.ts +0 -26
  162. package/lib/typescript/src/driverState.d.ts.map +0 -1
  163. package/lib/typescript/src/driverTypes.d.ts +0 -121
  164. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  165. package/lib/typescript/src/drivers.android.d.ts +0 -3
  166. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  167. package/lib/typescript/src/drivers.d.ts +0 -3
  168. package/lib/typescript/src/drivers.d.ts.map +0 -1
  169. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  170. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  171. package/lib/typescript/src/drivers.native.d.ts +0 -5
  172. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  173. package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
  174. package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
  175. package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
  176. package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
  177. package/lib/typescript/src/groupDrivers.d.ts +0 -2
  178. package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
  179. package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
  180. package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
  181. package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
  182. package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
  183. package/lib/typescript/src/presentationProtocol.d.ts +0 -4
  184. package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
  185. package/lib/typescript/src/reactRequests.d.ts +0 -9
  186. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  187. package/src/driverState.ts +0 -118
  188. package/src/driverTypes.ts +0 -171
  189. package/src/drivers.android.ts +0 -8
  190. package/src/drivers.ios.ts +0 -8
  191. package/src/drivers.native.ts +0 -1599
  192. package/src/drivers.ts +0 -6
  193. package/src/groupDriverTypes.ts +0 -117
  194. package/src/groupDrivers.android.ts +0 -1
  195. package/src/groupDrivers.ios.ts +0 -1
  196. package/src/groupDrivers.native.ts +0 -1189
  197. package/src/groupDrivers.ts +0 -1
  198. package/src/presentationProtocol.ts +0 -28
  199. package/src/reactRequests.ts +0 -76
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include "SmoothClipRegistry.h"
4
+ #include "SmoothClipSharedGeometry.h"
4
5
 
5
6
  #include <algorithm>
6
7
  #include <array>
@@ -9,9 +10,8 @@
9
10
  #include <utility>
10
11
  #include <vector>
11
12
 
12
- // Shared animation math for the Android integrator and the iOS CoreAnimation
13
- // restart paths: frame-clock anchoring, timing curves/remainders, spring
14
- // continuation, presentation blending and keyframes. Keeping the pure math out
13
+ // Shared animation math for the Android integrator: frame-clock anchoring,
14
+ // timing curves, spring validation, and presentation blending. Keeping the pure math out
15
15
  // of the registry translation units also lets the XCTest binary pin every rule
16
16
  // below — see ios/tests/SmoothClipAnimationCurveTests.mm.
17
17
  namespace smoothclip {
@@ -50,7 +50,7 @@ inline double clamp01(double value) {
50
50
  // parallel Reanimated animation by the callback's intra-frame offset for the
51
51
  // whole duration. Starts that carry the JS-captured stamp resolve through
52
52
  // resolveStartStamp() below and skip this anchor entirely; min() remains for
53
- // latch flushes and stamp-less callers, where its two branches are exact.
53
+ // pre-ready starts and unstamped callers, where its two branches are exact.
54
54
  inline double anchorStartTime(double startedAtS, double frameNowS) {
55
55
  return std::min(startedAtS, frameNowS);
56
56
  }
@@ -81,7 +81,7 @@ inline StartStamp resolveStartStamp(double jsStampS, double wallNowS) {
81
81
  return {wallNowS, false};
82
82
  }
83
83
 
84
- // Elapsed fraction of a timing/keyframe animation. A non-positive duration is
84
+ // Elapsed fraction of a timing animation. A non-positive duration is
85
85
  // complete on its first frame rather than dividing by zero.
86
86
  inline double timingFraction(
87
87
  double nowS,
@@ -91,20 +91,7 @@ inline double timingFraction(
91
91
  return clamp01((nowS - startedAtS) / durationS);
92
92
  }
93
93
 
94
- // --- Timing curve continuation -------------------------------------------
95
-
96
- struct CubicBezierPoint {
97
- double x;
98
- double y;
99
- };
100
-
101
- inline CubicBezierPoint mixBezierPoint(
102
- CubicBezierPoint from,
103
- CubicBezierPoint to,
104
- double progress) {
105
- return {from.x + (to.x - from.x) * progress,
106
- from.y + (to.y - from.y) * progress};
107
- }
94
+ // --- Timing curve --------------------------------------------------------
108
95
 
109
96
  inline double cubicBezierParameterForX(
110
97
  double x1,
@@ -141,157 +128,12 @@ inline double cubicBezier(
141
128
  3 * inverse * t * t * y2 + t * t * t;
142
129
  }
143
130
 
144
- struct TimingRemainder {
145
- TimingAnimation animation;
146
- double easedProgress;
147
- bool representable;
148
- };
149
-
150
- struct TimingContinuation {
151
- Presentation start;
152
- TimingAnimation animation;
153
- };
154
-
155
- // Exact right-hand segment of a CSS/CoreAnimation cubic Bezier after a raw
156
- // time cutoff. Restarting the original controls from a frozen presentation
157
- // resets its slope and creates a visible velocity cusp. De Casteljau splitting
158
- // preserves the original curve; normalizing the right segment to (0,0)->(1,1)
159
- // makes it directly reusable with a shorter BasicAnimation/integrator run.
160
- inline TimingRemainder timingRemainder(
161
- const TimingAnimation &original,
162
- double rawProgress) {
163
- const double progress = clamp01(rawProgress);
164
- if (progress <= 0) return {original, 0, true};
165
- if (progress >= 1) {
166
- TimingAnimation complete = original;
167
- complete.durationMs = 0;
168
- complete.controlPoint1X = 0;
169
- complete.controlPoint1Y = 0;
170
- complete.controlPoint2X = 1;
171
- complete.controlPoint2Y = 1;
172
- return {complete, 1, true};
173
- }
174
-
175
- const double t = cubicBezierParameterForX(
176
- original.controlPoint1X, original.controlPoint2X, progress, 30);
177
- const CubicBezierPoint p0{0, 0};
178
- const CubicBezierPoint p1{
179
- original.controlPoint1X, original.controlPoint1Y};
180
- const CubicBezierPoint p2{
181
- original.controlPoint2X, original.controlPoint2Y};
182
- const CubicBezierPoint p3{1, 1};
183
- const CubicBezierPoint a = mixBezierPoint(p0, p1, t);
184
- const CubicBezierPoint b = mixBezierPoint(p1, p2, t);
185
- const CubicBezierPoint c = mixBezierPoint(p2, p3, t);
186
- const CubicBezierPoint d = mixBezierPoint(a, b, t);
187
- const CubicBezierPoint e = mixBezierPoint(b, c, t);
188
- const CubicBezierPoint split = mixBezierPoint(d, e, t);
189
-
190
- TimingAnimation remainder = original;
191
- remainder.durationMs = original.durationMs * (1 - progress);
192
- const double remainingX = 1 - split.x;
193
- const double remainingY = 1 - split.y;
194
- // A curve whose y reaches exactly 1 before its time endpoint and later
195
- // departs cannot be represented as one endpoint-normalized BasicAnimation.
196
- // Keep the result finite; ordinary monotone easings never take this branch.
197
- if (std::abs(remainingX) <= 1e-12 || std::abs(remainingY) <= 1e-12) {
198
- remainder.controlPoint1X = 0;
199
- remainder.controlPoint1Y = 0;
200
- remainder.controlPoint2X = 1;
201
- remainder.controlPoint2Y = 1;
202
- return {remainder, split.y, false};
203
- }
204
- remainder.controlPoint1X = (e.x - split.x) / remainingX;
205
- remainder.controlPoint1Y = (e.y - split.y) / remainingY;
206
- remainder.controlPoint2X = (c.x - split.x) / remainingX;
207
- remainder.controlPoint2Y = (c.y - split.y) / remainingY;
208
- return {remainder, split.y, true};
209
- }
210
-
211
- // --- Spring continuation --------------------------------------------------
212
-
213
- struct NormalizedSpringState {
214
- // Remaining fraction of the original from->target displacement.
215
- double remaining;
216
- // Derivative of `remaining` in units per second.
217
- double velocity;
218
- };
219
-
220
- // Closed-form state of the same mass/spring/damper equation CoreAnimation
221
- // evaluates. If p is animation progress, remaining=1-p starts at 1 and its
222
- // derivative starts at -initialVelocity.
223
- inline NormalizedSpringState normalizedSpringState(
224
- const SpringAnimation &spring,
225
- double elapsedS) {
226
- if (!std::isfinite(elapsedS) || elapsedS <= 0) {
227
- return {1, -spring.initialVelocity};
228
- }
229
- const double mass = spring.mass;
230
- const double stiffness = spring.stiffness;
231
- const double damping = spring.damping;
232
- const double initialVelocity = spring.initialVelocity;
233
- if (!std::isfinite(mass) || !std::isfinite(stiffness) ||
234
- !std::isfinite(damping) || !std::isfinite(initialVelocity) ||
235
- mass <= 0 || stiffness <= 0 || damping < 0) {
236
- return {1, 0};
237
- }
238
-
239
- const double alpha = damping / (2 * mass);
240
- const double omegaSquared = stiffness / mass;
241
- const double discriminant = alpha * alpha - omegaSquared;
242
- const double criticalScale = std::max(1.0, omegaSquared);
243
- if (std::abs(discriminant) <= 1e-12 * criticalScale) {
244
- const double coefficient = alpha - initialVelocity;
245
- const double decay = std::exp(-alpha * elapsedS);
246
- const double remaining = decay * (1 + coefficient * elapsedS);
247
- const double velocity = decay *
248
- (coefficient - alpha * (1 + coefficient * elapsedS));
249
- return {remaining, velocity};
250
- }
251
- if (discriminant < 0) {
252
- const double omega = std::sqrt(-discriminant);
253
- const double coefficient = (alpha - initialVelocity) / omega;
254
- const double angle = omega * elapsedS;
255
- const double cosine = std::cos(angle);
256
- const double sine = std::sin(angle);
257
- const double basis = cosine + coefficient * sine;
258
- const double basisVelocity =
259
- -omega * sine + coefficient * omega * cosine;
260
- const double decay = std::exp(-alpha * elapsedS);
261
- return {decay * basis, decay * (basisVelocity - alpha * basis)};
262
- }
263
-
264
- const double root = std::sqrt(discriminant);
265
- const double firstRate = -alpha + root;
266
- const double secondRate = -alpha - root;
267
- const double firstCoefficient =
268
- (-initialVelocity - secondRate) / (firstRate - secondRate);
269
- const double secondCoefficient = 1 - firstCoefficient;
270
- const double first = firstCoefficient * std::exp(firstRate * elapsedS);
271
- const double second = secondCoefficient * std::exp(secondRate * elapsedS);
272
- return {first + second, firstRate * first + secondRate * second};
273
- }
274
-
275
- // CASpringAnimation.initialVelocity is normalized by the current remaining
276
- // distance. Re-expressing the old physical velocity against the frozen
277
- // current->target displacement preserves the derivative at a re-latch seam.
278
- inline double springContinuationVelocity(
279
- const SpringAnimation &spring,
280
- double elapsedS) {
281
- const NormalizedSpringState state = normalizedSpringState(spring, elapsedS);
282
- if (!std::isfinite(state.remaining) || !std::isfinite(state.velocity) ||
283
- std::abs(state.remaining) <= 1e-9) {
284
- return 0;
285
- }
286
- const double result = -state.velocity / state.remaining;
287
- return std::isfinite(result) ? result : 0;
288
- }
289
-
290
131
  // --- Presentation channels ------------------------------------------------
291
132
 
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;
133
+ // The first eleven channels are geometry/content; the final eight are shadow.
134
+ // Curve and shadow presence are categorical and stay outside the scalar array.
135
+ constexpr std::size_t kBaseChannelCount = 11;
136
+ constexpr std::size_t kChannelCount = 19;
295
137
  using Channels = std::array<double, kChannelCount>;
296
138
 
297
139
  inline double resolvedRadius(double overrideValue, double shorthand) {
@@ -326,42 +168,104 @@ inline Channels toChannels(const Presentation &presentation) {
326
168
  bottomLeft,
327
169
  presentation.contentTranslateX,
328
170
  presentation.contentTranslateY,
329
- presentation.contentScale};
171
+ presentation.contentScale,
172
+ presentation.shadow.red,
173
+ presentation.shadow.green,
174
+ presentation.shadow.blue,
175
+ presentation.shadow.alpha,
176
+ presentation.shadow.offsetX,
177
+ presentation.shadow.offsetY,
178
+ presentation.shadow.blurRadius,
179
+ presentation.shadow.spreadDistance};
330
180
  }
331
181
 
332
- // --- Protocol-V2 animation validation ------------------------------------
182
+ // --- Animation validation ---------------------------------------------------
333
183
  //
334
184
  // Keep the invariants at the native ownership boundary as well as in JS. OTA
335
185
  // callers and direct host-function users can bypass the TypeScript preflight;
336
186
  // rejecting here prevents an invalid request from dissolving an existing
337
187
  // animation group before the registry notices it cannot install the new plan.
338
188
 
339
- inline bool isProtocolV2Curve(ClipCurve curve) {
189
+ inline bool isSupportedCurve(ClipCurve curve) {
340
190
  return curve == ClipCurve::Circular || curve == ClipCurve::Continuous;
341
191
  }
342
192
 
343
- inline bool isFiniteProtocolV2Presentation(
193
+ inline bool isFinitePresentation(
344
194
  const Presentation &presentation) {
345
195
  const Channels channels = toChannels(presentation);
346
196
  return std::all_of(
347
197
  channels.begin(), channels.end(),
348
198
  [](double value) { return std::isfinite(value); }) &&
349
- isProtocolV2Curve(presentation.clip.curve) &&
350
- presentation.contentScale > 0;
199
+ isSupportedCurve(presentation.clip.curve) &&
200
+ presentation.contentScale > 0 &&
201
+ presentation.shadow.red >= 0 && presentation.shadow.red <= 1 &&
202
+ presentation.shadow.green >= 0 && presentation.shadow.green <= 1 &&
203
+ presentation.shadow.blue >= 0 && presentation.shadow.blue <= 1 &&
204
+ presentation.shadow.alpha >= 0 && presentation.shadow.alpha <= 1 &&
205
+ presentation.shadow.blurRadius >= 0;
351
206
  }
352
207
 
353
- inline bool protocolV2PresentationsEqual(
208
+ /** Canonicalizes registry geometry without consulting any attached host. */
209
+ inline bool canonicalizePresentation(Presentation &presentation) {
210
+ if (!isFinitePresentation(presentation)) return false;
211
+ CanonicalClip clip;
212
+ if (!SmoothClipCanonicalize(presentation.clip, clip)) return false;
213
+ presentation.clip = SmoothClipGeometry(clip);
214
+ return true;
215
+ }
216
+
217
+ inline bool presentationsEqual(
354
218
  const Presentation &first,
355
219
  const Presentation &second) {
356
220
  return first.clip.curve == second.clip.curve &&
221
+ first.shadow.enabled == second.shadow.enabled &&
357
222
  toChannels(first) == toChannels(second);
358
223
  }
359
224
 
225
+ inline bool hasVisibleShadow(const Presentation &presentation) {
226
+ return presentation.shadow.enabled && presentation.shadow.alpha > 0 &&
227
+ presentation.clip.width > 0 && presentation.clip.height > 0;
228
+ }
229
+
230
+ inline bool isAutonomousUniformCircular(
231
+ const Presentation &presentation) {
232
+ const Geometry &clip = presentation.clip;
233
+ return clip.curve == ClipCurve::Circular && radiiAreUniform(
234
+ resolvedRadius(clip.topLeftRadius, clip.radius),
235
+ resolvedRadius(clip.topRightRadius, clip.radius),
236
+ resolvedRadius(clip.bottomRightRadius, clip.radius),
237
+ resolvedRadius(clip.bottomLeftRadius, clip.radius));
238
+ }
239
+
240
+ /**
241
+ * Makes shadow appearance/disappearance a pure alpha fade. A disabled or
242
+ * transparent endpoint borrows the visible endpoint's style while retaining
243
+ * its own geometry, so path, blur, spread, and offset do not collapse during
244
+ * the fade. The public target remains unchanged in the registry; these copies
245
+ * are animation inputs only.
246
+ */
247
+ inline std::pair<Presentation, Presentation> normalizeShadowEndpoints(
248
+ Presentation from,
249
+ Presentation to) {
250
+ const bool fromVisible = hasVisibleShadow(from);
251
+ const bool toVisible = hasVisibleShadow(to);
252
+ if (!fromVisible) from.shadow.alpha = 0;
253
+ if (!toVisible) to.shadow.alpha = 0;
254
+ if (fromVisible == toVisible) return {from, to};
255
+
256
+ Shadow &invisible = fromVisible ? to.shadow : from.shadow;
257
+ const Shadow &visible = fromVisible ? from.shadow : to.shadow;
258
+ invisible = visible;
259
+ invisible.enabled = true;
260
+ invisible.alpha = 0;
261
+ return {from, to};
262
+ }
263
+
360
264
  inline bool isValidReduceMotionCode(int32_t value) {
361
265
  return value >= 0 && value <= 2;
362
266
  }
363
267
 
364
- inline bool isValidProtocolV2Timing(const TimingAnimation &animation) {
268
+ inline bool isValidTiming(const TimingAnimation &animation) {
365
269
  return std::isfinite(animation.durationMs) && animation.durationMs >= 0 &&
366
270
  std::isfinite(animation.controlPoint1X) &&
367
271
  std::isfinite(animation.controlPoint1Y) &&
@@ -372,59 +276,153 @@ inline bool isValidProtocolV2Timing(const TimingAnimation &animation) {
372
276
  isValidReduceMotionCode(animation.reduceMotion);
373
277
  }
374
278
 
375
- inline bool isValidProtocolV2Spring(const SpringAnimation &animation) {
279
+ inline bool isValidSpring(const SpringAnimation &animation) {
376
280
  return std::isfinite(animation.mass) && animation.mass > 0 &&
377
281
  std::isfinite(animation.stiffness) && animation.stiffness > 0 &&
378
282
  std::isfinite(animation.damping) && animation.damping >= 0 &&
379
283
  std::isfinite(animation.initialVelocity) &&
284
+ std::isfinite(animation.energyThreshold) &&
285
+ animation.energyThreshold > 0 &&
380
286
  isValidReduceMotionCode(animation.reduceMotion);
381
287
  }
382
288
 
383
- inline bool protocolV2SpringScaleIsProvablyPositive(
384
- const Presentation &start,
385
- const Presentation &target,
289
+ struct ScalarSpringState {
290
+ double position;
291
+ double velocity;
292
+ };
293
+
294
+ /** One analytic Reanimated-compatible spring step for normalized progress. */
295
+ inline ScalarSpringState advanceScalarSpring(
296
+ ScalarSpringState state,
297
+ const SpringAnimation &animation,
298
+ double deltaTimeS) {
299
+ const double dt = std::clamp(deltaTimeS, 0.0, 0.064);
300
+ if (dt == 0) return state;
301
+ const double omega0 = std::sqrt(animation.stiffness / animation.mass);
302
+ const double zeta = animation.damping /
303
+ (2.0 * std::sqrt(animation.stiffness * animation.mass));
304
+ const double displacement = state.position - 1.0;
305
+ const double velocity = state.velocity;
306
+ if (zeta < 1.0) {
307
+ const double omega1 = omega0 * std::sqrt(1.0 - zeta * zeta);
308
+ const double envelope = std::exp(-zeta * omega0 * dt);
309
+ const double sinTerm = std::sin(omega1 * dt);
310
+ const double cosTerm = std::cos(omega1 * dt);
311
+ const double coefficient =
312
+ (velocity + zeta * omega0 * displacement) / omega1;
313
+ const double nextDisplacement =
314
+ envelope * (displacement * cosTerm + coefficient * sinTerm);
315
+ const double nextVelocity = envelope *
316
+ (velocity * (cosTerm - zeta * omega0 / omega1 * sinTerm) -
317
+ displacement * (omega0 * omega0 / omega1) * sinTerm);
318
+ return {1.0 + nextDisplacement, nextVelocity};
319
+ }
320
+
321
+ // Reanimated deliberately uses its critical branch for zeta >= 1.
322
+ const double envelope = std::exp(-omega0 * dt);
323
+ const double coefficient = velocity + omega0 * displacement;
324
+ const double nextDisplacement =
325
+ envelope * (displacement + coefficient * dt);
326
+ const double nextVelocity =
327
+ envelope * (velocity - omega0 * coefficient * dt);
328
+ return {1.0 + nextDisplacement, nextVelocity};
329
+ }
330
+
331
+ inline double relativeSpringEnergy(
332
+ ScalarSpringState state,
386
333
  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;
334
+ const double displacement = state.position - 1.0;
335
+ const double currentEnergy =
336
+ animation.mass * state.velocity * state.velocity +
337
+ animation.stiffness * displacement * displacement;
338
+ const double initialEnergy =
339
+ animation.mass * animation.initialVelocity * animation.initialVelocity +
340
+ animation.stiffness;
341
+ return currentEnergy / initialEnergy;
395
342
  }
396
343
 
397
- inline bool isValidProtocolV2Keyframes(
398
- const std::vector<Keyframe> &keyframes,
399
- const Presentation &resolvedStart,
344
+ inline bool springScaleStaysPositive(
345
+ const Presentation &start,
400
346
  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;
347
+ const SpringAnimation &animation,
348
+ double initialVelocity) {
349
+ if (start.contentScale == target.contentScale) return true;
350
+ if (!std::isfinite(initialVelocity)) return false;
351
+
352
+ const double startScale = start.contentScale;
353
+ const double targetScale = target.contentScale;
354
+ const double delta = targetScale - startScale;
355
+ const auto scaleForDisplacement = [&](double displacement) {
356
+ // displacement is q = progress - 1, so scale = target + delta*q.
357
+ return targetScale + delta * displacement;
358
+ };
359
+ if (startScale <= 0 || targetScale <= 0) return false;
360
+
361
+ const double naturalFrequency =
362
+ std::sqrt(animation.stiffness / animation.mass);
363
+ const double decay = animation.damping / (2 * animation.mass);
364
+ const double discriminant = decay * decay -
365
+ naturalFrequency * naturalFrequency;
366
+ constexpr double epsilon = 1e-12;
367
+ constexpr double pi = 3.14159265358979323846;
368
+
369
+ const auto isPositiveAt = [&](double time, double displacement) {
370
+ return time <= epsilon || scaleForDisplacement(displacement) > 0;
371
+ };
372
+
373
+ if (discriminant < -epsilon) {
374
+ const double frequency = std::sqrt(-discriminant);
375
+ const double a = -1;
376
+ const double b = (initialVelocity - decay) / frequency;
377
+ const double derivativeCos = initialVelocity;
378
+ const double derivativeSin = frequency - decay * b;
379
+ double phase = std::atan2(-derivativeCos, derivativeSin);
380
+ while (phase <= epsilon) phase += pi;
381
+ // Extrema alternate and their envelope only decays, so the first two
382
+ // positive extrema cover both signs and therefore the global minimum.
383
+ for (int index = 0; index < 2; index += 1) {
384
+ const double time = phase / frequency;
385
+ const double displacement = std::exp(-decay * time) *
386
+ (a * std::cos(phase) + b * std::sin(phase));
387
+ if (!isPositiveAt(time, displacement)) return false;
388
+ phase += pi;
389
+ }
390
+ return true;
406
391
  }
407
- if (requireExplicitStart && !protocolV2PresentationsEqual(
408
- keyframes.front().presentation,
409
- resolvedStart)) {
410
- return false;
392
+
393
+ if (std::abs(discriminant) <= epsilon) {
394
+ const double b = initialVelocity - decay;
395
+ const double denominator = decay * b;
396
+ if (std::abs(denominator) <= epsilon) return true;
397
+ const double time = initialVelocity / denominator;
398
+ if (time <= epsilon) return true;
399
+ const double displacement = std::exp(-decay * time) * (-1 + b * time);
400
+ return isPositiveAt(time, displacement);
411
401
  }
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;
402
+
403
+ const double root = std::sqrt(discriminant);
404
+ const double slow = -decay + root;
405
+ const double fast = -decay - root;
406
+ const double slowCoefficient =
407
+ (initialVelocity + fast) / (slow - fast);
408
+ const double fastCoefficient = -1 - slowCoefficient;
409
+ const double numerator = -fastCoefficient * fast;
410
+ const double denominator = slowCoefficient * slow;
411
+ if (std::abs(denominator) <= epsilon || numerator / denominator <= 0) {
412
+ return true;
421
413
  }
422
- return resolvedStart.clip.curve == target.clip.curve;
414
+ const double time =
415
+ std::log(numerator / denominator) / (slow - fast);
416
+ if (time <= epsilon) return true;
417
+ const double displacement = slowCoefficient * std::exp(slow * time) +
418
+ fastCoefficient * std::exp(fast * time);
419
+ return isPositiveAt(time, displacement);
423
420
  }
424
421
 
425
422
  inline Presentation fromChannels(
426
423
  const Channels &channels,
427
- ClipCurve curve = ClipCurve::Circular) {
424
+ ClipCurve curve = ClipCurve::Circular,
425
+ bool shadowEnabled = false) {
428
426
  Geometry geometry{
429
427
  channels[0], channels[1], channels[2], channels[3], 0.0};
430
428
  geometry.topLeftRadius = channels[4];
@@ -436,7 +434,18 @@ inline Presentation fromChannels(
436
434
  ? channels[4]
437
435
  : 0.0;
438
436
  geometry.curve = curve;
439
- return Presentation{geometry, channels[8], channels[9], channels[10]};
437
+ Shadow shadow{
438
+ shadowEnabled,
439
+ std::clamp(channels[11], 0.0, 1.0),
440
+ std::clamp(channels[12], 0.0, 1.0),
441
+ std::clamp(channels[13], 0.0, 1.0),
442
+ std::clamp(channels[14], 0.0, 1.0),
443
+ channels[15],
444
+ channels[16],
445
+ std::max(0.0, channels[17]),
446
+ channels[18]};
447
+ return Presentation{
448
+ geometry, channels[8], channels[9], channels[10], shadow};
440
449
  }
441
450
 
442
451
  inline Presentation interpolate(
@@ -447,143 +456,25 @@ inline Presentation interpolate(
447
456
  return start + (end - start) * progress;
448
457
  };
449
458
  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) {
459
+ const auto normalized = normalizeShadowEndpoints(from, to);
460
+ const Channels fromChannelsValue = toChannels(normalized.first);
461
+ const Channels toChannelsValue = toChannels(normalized.second);
462
+ const bool rendersShadow = hasVisibleShadow(from) || hasVisibleShadow(to);
463
+ const std::size_t channelCount =
464
+ rendersShadow ? kChannelCount : kBaseChannelCount;
465
+ for (std::size_t index = 0; index < channelCount; index += 1) {
453
466
  blended[index] = mix(fromChannelsValue[index], toChannelsValue[index]);
454
467
  }
455
- return fromChannels(blended, from.clip.curve);
456
- }
457
-
458
- // Freezes a timing animation at one already-rendered raw-time phase and
459
- // returns the exact right-hand Bezier segment. Keeping the presentation and
460
- // residual curve in one helper prevents lifecycle code from sampling one
461
- // timestamp for the geometry and another for the duration.
462
- inline TimingContinuation timingContinuation(
463
- const TimingAnimation &original,
464
- const Presentation &start,
465
- const Presentation &target,
466
- double rawProgress) {
467
- const TimingRemainder remainder = timingRemainder(original, rawProgress);
468
- return {
469
- interpolate(start, target, remainder.easedProgress),
470
- remainder.animation};
471
- }
472
-
473
- // Android lifecycle callbacks can arrive long after the frame whose
474
- // presentation was actually delivered. Keep the clock inputs at this shared
475
- // boundary so callers cannot accidentally trim from callback wall time while
476
- // calculating geometry from the last rendered frame.
477
- inline TimingContinuation timingContinuationAtFrame(
478
- const TimingAnimation &original,
479
- const Presentation &start,
480
- const Presentation &target,
481
- double lastRenderedS,
482
- double startedAtS,
483
- double durationS) {
484
- return timingContinuation(
485
- original,
486
- start,
487
- target,
488
- timingFraction(lastRenderedS, startedAtS, durationS));
489
- }
490
-
491
- // --- Keyframe curve -------------------------------------------------------
492
-
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.
496
- class KeyframeCurve {
497
- public:
498
- void reset(std::vector<Keyframe> frames) {
499
- frames_ = std::move(frames);
500
- cursor_ = 1;
501
- }
502
-
503
- const std::vector<Keyframe> &frames() const { return frames_; }
504
- std::size_t size() const { return frames_.size(); }
505
- bool usable() const { return frames_.size() >= 2; }
506
-
507
- Presentation evaluate(double progress) {
508
- const std::size_t count = frames_.size();
509
- if (count == 0) return Presentation{{0, 0, 0, 0, 0}, 0, 0, 1};
510
- if (count == 1) return frames_[0].presentation;
511
-
512
- // The scan resumes from the previous segment instead of restarting at 1.
513
- // Progress is monotonic within an animation, so this is O(1) amortized
514
- // where the old restart-every-frame scan was O(keyframes) per frame; the
515
- // backward walk keeps it correct if progress ever moves the other way.
516
- std::size_t upper = std::min(std::max<std::size_t>(cursor_, 1), count - 1);
517
- while (upper > 1 && progress < frames_[upper - 1].offset) upper -= 1;
518
- while (upper < count - 1 && progress > frames_[upper].offset) upper += 1;
519
- cursor_ = upper;
520
-
521
- const Keyframe &lower = frames_[upper - 1];
522
- const Keyframe &higher = frames_[upper];
523
- const double span = higher.offset - lower.offset;
524
- if (span <= 0) return higher.presentation;
525
-
526
- const double t = clamp01((progress - lower.offset) / span);
527
- return interpolate(lower.presentation, higher.presentation, t);
528
- }
529
-
530
- private:
531
- std::vector<Keyframe> frames_;
532
- std::size_t cursor_ = 1;
533
- };
534
-
535
- struct KeyframeContinuation {
536
- Presentation start;
537
- std::vector<Keyframe> frames;
538
- double durationMs;
539
- };
540
-
541
- // Trims keyframes at the same raw-time phase that produced `frozen`. The new
542
- // zero frame is the rendered presentation, not an analytically re-sampled
543
- // neighbor, so a pause/re-latch cannot jump at the seam. Strictly increasing
544
- // remapped offsets avoid duplicate/division-by-zero segments after repeated
545
- // trims or floating-point collisions near the cutoff.
546
- inline KeyframeContinuation keyframeContinuation(
547
- const std::vector<Keyframe> &frames,
548
- const Presentation &frozen,
549
- const Presentation &target,
550
- double durationMs,
551
- double rawProgress) {
552
- const double progress = clamp01(rawProgress);
553
- KeyframeContinuation result{
554
- frozen, {{0.0, frozen}}, std::max(0.0, durationMs * (1 - progress))};
555
- if (progress < 1.0) {
556
- for (const Keyframe &frame : frames) {
557
- if (frame.offset <= progress) continue;
558
- const double offset = clamp01(
559
- (frame.offset - progress) / (1 - progress));
560
- if (offset <= result.frames.back().offset + 1e-12) continue;
561
- result.frames.push_back({offset, frame.presentation});
562
- }
563
- }
564
- if (result.frames.size() == 1 ||
565
- result.frames.back().offset < 1.0 - 1e-12) {
566
- result.frames.push_back({1.0, target});
567
- } else {
568
- result.frames.back().offset = 1.0;
569
- result.frames.back().presentation = target;
468
+ if (!rendersShadow) {
469
+ std::copy(
470
+ fromChannelsValue.begin() + kBaseChannelCount,
471
+ fromChannelsValue.end(),
472
+ blended.begin() + kBaseChannelCount);
570
473
  }
571
- return result;
572
- }
573
-
574
- inline KeyframeContinuation keyframeContinuationAtFrame(
575
- const std::vector<Keyframe> &frames,
576
- const Presentation &frozen,
577
- const Presentation &target,
578
- double lastRenderedS,
579
- double startedAtS,
580
- double durationS) {
581
- return keyframeContinuation(
582
- frames,
583
- frozen,
584
- target,
585
- std::max(0.0, durationS * 1000.0),
586
- timingFraction(lastRenderedS, startedAtS, durationS));
474
+ const bool shadowEnabled = progress >= 1
475
+ ? to.shadow.enabled
476
+ : rendersShadow;
477
+ return fromChannels(blended, from.clip.curve, shadowEnabled);
587
478
  }
588
479
 
589
480
  } // namespace smoothclip