react-native-smooth-clip-view 0.2.9 → 0.4.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 (193) hide show
  1. package/README.md +132 -349
  2. package/android/build.gradle +8 -4
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +13 -6
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +605 -123
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +975 -209
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +192 -118
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +22 -4
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +81 -56
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +449 -240
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +150 -38
  12. package/cpp/SmoothClipAnimationCurve.h +317 -397
  13. package/cpp/SmoothClipRegistry.h +84 -20
  14. package/cpp/SmoothClipSharedGeometry.h +131 -22
  15. package/cpp/SmoothClipVelocityTracker.h +16 -12
  16. package/ios/SmoothClipGeometry.h +188 -21
  17. package/ios/SmoothClipRegistry.mm +869 -368
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +513 -156
  20. package/ios/SmoothClipTurboModule.h +123 -56
  21. package/ios/SmoothClipView.mm +1074 -449
  22. package/ios/SmoothClipViewRegistry.h +3 -7
  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 +94 -11
  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 +32 -12
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +18 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +2 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/controllerInternals.js +28 -0
  38. package/lib/module/controllerInternals.js.map +1 -0
  39. package/lib/module/controllerLifecycle.js +9 -0
  40. package/lib/module/controllerLifecycle.js.map +1 -0
  41. package/lib/module/controllerTypes.js +4 -0
  42. package/lib/module/controllerTypes.js.map +1 -0
  43. package/lib/module/controllers.android.js +4 -0
  44. package/lib/module/controllers.android.js.map +1 -0
  45. package/lib/module/controllers.ios.js +4 -0
  46. package/lib/module/controllers.ios.js.map +1 -0
  47. package/lib/module/controllers.js +4 -0
  48. package/lib/module/controllers.js.map +1 -0
  49. package/lib/module/controllers.native.js +89 -0
  50. package/lib/module/controllers.native.js.map +1 -0
  51. package/lib/module/geometry.js +180 -30
  52. package/lib/module/geometry.js.map +1 -1
  53. package/lib/module/groupTypes.js +4 -0
  54. package/lib/module/groupTypes.js.map +1 -0
  55. package/lib/module/groups.android.js +4 -0
  56. package/lib/module/groups.android.js.map +1 -0
  57. package/lib/module/groups.ios.js +4 -0
  58. package/lib/module/groups.ios.js.map +1 -0
  59. package/lib/module/groups.js +4 -0
  60. package/lib/module/groups.js.map +1 -0
  61. package/lib/module/groups.native.js +399 -0
  62. package/lib/module/groups.native.js.map +1 -0
  63. package/lib/module/ids.js +8 -0
  64. package/lib/module/ids.js.map +1 -0
  65. package/lib/module/index.js +4 -3
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/nativeCompletion.js +37 -16
  68. package/lib/module/nativeCompletion.js.map +1 -1
  69. package/lib/module/presentationCodec.js +81 -0
  70. package/lib/module/presentationCodec.js.map +1 -0
  71. package/lib/typescript/src/NativeSmoothClipModule.d.ts +20 -4
  72. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  73. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  74. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  75. package/lib/typescript/src/SmoothClipView.ios.d.ts +139 -4
  76. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  77. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +16 -3
  78. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  79. package/lib/typescript/src/capabilities.d.ts +3 -0
  80. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  81. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  82. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  83. package/lib/typescript/src/capabilityTypes.d.ts +4 -0
  84. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  85. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  86. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  87. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  88. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  89. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  90. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  91. package/lib/typescript/src/controllers.android.d.ts +2 -0
  92. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  93. package/lib/typescript/src/controllers.d.ts +2 -0
  94. package/lib/typescript/src/controllers.d.ts.map +1 -0
  95. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  96. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  97. package/lib/typescript/src/controllers.native.d.ts +3 -0
  98. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  99. package/lib/typescript/src/geometry.d.ts +49 -5
  100. package/lib/typescript/src/geometry.d.ts.map +1 -1
  101. package/lib/typescript/src/groupTypes.d.ts +31 -0
  102. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  103. package/lib/typescript/src/groups.android.d.ts +2 -0
  104. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  105. package/lib/typescript/src/groups.d.ts +2 -0
  106. package/lib/typescript/src/groups.d.ts.map +1 -0
  107. package/lib/typescript/src/groups.ios.d.ts +2 -0
  108. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  109. package/lib/typescript/src/groups.native.d.ts +3 -0
  110. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  111. package/lib/typescript/src/ids.d.ts +2 -0
  112. package/lib/typescript/src/ids.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +8 -5
  114. package/lib/typescript/src/index.d.ts.map +1 -1
  115. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  116. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  117. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  118. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  119. package/package.json +17 -13
  120. package/src/NativeSmoothClipModule.ts +80 -56
  121. package/src/SmoothClipView.ios.tsx +142 -16
  122. package/src/SmoothClipView.tsx +6 -76
  123. package/src/SmoothClipViewNativeComponent.ts +32 -12
  124. package/src/capabilities.native.ts +19 -0
  125. package/src/capabilities.ts +2 -0
  126. package/src/capabilityTypes.ts +3 -0
  127. package/src/controllerInternals.ts +52 -0
  128. package/src/controllerLifecycle.ts +6 -0
  129. package/src/controllerTypes.ts +87 -0
  130. package/src/controllers.android.ts +1 -0
  131. package/src/controllers.ios.ts +1 -0
  132. package/src/controllers.native.ts +104 -0
  133. package/src/controllers.ts +1 -0
  134. package/src/geometry.ts +293 -45
  135. package/src/groupTypes.ts +53 -0
  136. package/src/groups.android.ts +1 -0
  137. package/src/groups.ios.ts +1 -0
  138. package/src/groups.native.ts +604 -0
  139. package/src/groups.ts +1 -0
  140. package/src/ids.ts +6 -0
  141. package/src/index.ts +32 -12
  142. package/src/nativeCompletion.ts +67 -32
  143. package/src/presentationCodec.ts +134 -0
  144. package/lib/module/SmoothClipView.web.js +0 -54
  145. package/lib/module/SmoothClipView.web.js.map +0 -1
  146. package/lib/module/driverState.js +0 -110
  147. package/lib/module/driverState.js.map +0 -1
  148. package/lib/module/driverTypes.js +0 -4
  149. package/lib/module/driverTypes.js.map +0 -1
  150. package/lib/module/drivers.android.js +0 -6
  151. package/lib/module/drivers.android.js.map +0 -1
  152. package/lib/module/drivers.ios.js +0 -6
  153. package/lib/module/drivers.ios.js.map +0 -1
  154. package/lib/module/drivers.js +0 -284
  155. package/lib/module/drivers.js.map +0 -1
  156. package/lib/module/drivers.native.js +0 -556
  157. package/lib/module/drivers.native.js.map +0 -1
  158. package/lib/module/index.web.js +0 -8
  159. package/lib/module/index.web.js.map +0 -1
  160. package/lib/module/reactRequests.js +0 -56
  161. package/lib/module/reactRequests.js.map +0 -1
  162. package/lib/module/webClipPath.js +0 -25
  163. package/lib/module/webClipPath.js.map +0 -1
  164. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  165. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  166. package/lib/typescript/src/driverState.d.ts +0 -31
  167. package/lib/typescript/src/driverState.d.ts.map +0 -1
  168. package/lib/typescript/src/driverTypes.d.ts +0 -107
  169. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  170. package/lib/typescript/src/drivers.android.d.ts +0 -3
  171. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  172. package/lib/typescript/src/drivers.d.ts +0 -5
  173. package/lib/typescript/src/drivers.d.ts.map +0 -1
  174. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  175. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  176. package/lib/typescript/src/drivers.native.d.ts +0 -5
  177. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  178. package/lib/typescript/src/index.web.d.ts +0 -9
  179. package/lib/typescript/src/index.web.d.ts.map +0 -1
  180. package/lib/typescript/src/reactRequests.d.ts +0 -9
  181. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  182. package/lib/typescript/src/webClipPath.d.ts +0 -7
  183. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  184. package/src/SmoothClipView.web.tsx +0 -66
  185. package/src/driverState.ts +0 -158
  186. package/src/driverTypes.ts +0 -140
  187. package/src/drivers.android.ts +0 -8
  188. package/src/drivers.ios.ts +0 -8
  189. package/src/drivers.native.ts +0 -899
  190. package/src/drivers.ts +0 -427
  191. package/src/index.web.ts +0 -23
  192. package/src/reactRequests.ts +0 -76
  193. package/src/webClipPath.ts +0 -30
@@ -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,420 +128,353 @@ 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
- };
131
+ // --- Presentation channels ------------------------------------------------
154
132
 
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
- }
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;
137
+ using Channels = std::array<double, kChannelCount>;
174
138
 
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};
139
+ inline double resolvedRadius(double overrideValue, double shorthand) {
140
+ return std::isfinite(overrideValue) ? overrideValue : shorthand;
209
141
  }
210
142
 
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};
143
+ inline bool radiiAreUniform(
144
+ double topLeft,
145
+ double topRight,
146
+ double bottomRight,
147
+ double bottomLeft) {
148
+ return topLeft == topRight && topLeft == bottomRight &&
149
+ topLeft == bottomLeft;
273
150
  }
274
151
 
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
- // --- Presentation channels ------------------------------------------------
291
-
292
- constexpr std::size_t kChannelCount = 7;
293
- using Channels = std::array<double, kChannelCount>;
294
-
295
152
  inline Channels toChannels(const Presentation &presentation) {
153
+ const double topLeft = resolvedRadius(
154
+ presentation.clip.topLeftRadius, presentation.clip.radius);
155
+ const double topRight = resolvedRadius(
156
+ presentation.clip.topRightRadius, presentation.clip.radius);
157
+ const double bottomRight = resolvedRadius(
158
+ presentation.clip.bottomRightRadius, presentation.clip.radius);
159
+ const double bottomLeft = resolvedRadius(
160
+ presentation.clip.bottomLeftRadius, presentation.clip.radius);
296
161
  return {presentation.clip.x,
297
162
  presentation.clip.y,
298
163
  presentation.clip.width,
299
164
  presentation.clip.height,
300
- presentation.clip.radius,
165
+ topLeft,
166
+ topRight,
167
+ bottomRight,
168
+ bottomLeft,
301
169
  presentation.contentTranslateX,
302
- presentation.contentTranslateY};
170
+ presentation.contentTranslateY,
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};
303
180
  }
304
181
 
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]};
182
+ // --- Animation validation ---------------------------------------------------
183
+ //
184
+ // Keep the invariants at the native ownership boundary as well as in JS. OTA
185
+ // callers and direct host-function users can bypass the TypeScript preflight;
186
+ // rejecting here prevents an invalid request from dissolving an existing
187
+ // animation group before the registry notices it cannot install the new plan.
188
+
189
+ inline bool isSupportedCurve(ClipCurve curve) {
190
+ return curve == ClipCurve::Circular || curve == ClipCurve::Continuous;
310
191
  }
311
192
 
312
- inline Presentation interpolate(
313
- const Presentation &from,
314
- const Presentation &to,
315
- double progress) {
316
- const auto mix = [progress](double start, double end) {
317
- return start + (end - start) * progress;
318
- };
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)};
193
+ inline bool isFinitePresentation(
194
+ const Presentation &presentation) {
195
+ const Channels channels = toChannels(presentation);
196
+ return std::all_of(
197
+ channels.begin(), channels.end(),
198
+ [](double value) { return std::isfinite(value); }) &&
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;
327
206
  }
328
207
 
329
- // Freezes a timing animation at one already-rendered raw-time phase and
330
- // returns the exact right-hand Bezier segment. Keeping the presentation and
331
- // residual curve in one helper prevents lifecycle code from sampling one
332
- // timestamp for the geometry and another for the duration.
333
- inline TimingContinuation timingContinuation(
334
- const TimingAnimation &original,
335
- const Presentation &start,
336
- const Presentation &target,
337
- double rawProgress) {
338
- const TimingRemainder remainder = timingRemainder(original, rawProgress);
339
- return {
340
- interpolate(start, target, remainder.easedProgress),
341
- remainder.animation};
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;
342
215
  }
343
216
 
344
- // Android lifecycle callbacks can arrive long after the frame whose
345
- // presentation was actually delivered. Keep the clock inputs at this shared
346
- // boundary so callers cannot accidentally trim from callback wall time while
347
- // calculating geometry from the last rendered frame.
348
- inline TimingContinuation timingContinuationAtFrame(
349
- const TimingAnimation &original,
350
- const Presentation &start,
351
- const Presentation &target,
352
- double lastRenderedS,
353
- double startedAtS,
354
- double durationS) {
355
- return timingContinuation(
356
- original,
357
- start,
358
- target,
359
- timingFraction(lastRenderedS, startedAtS, durationS));
217
+ inline bool presentationsEqual(
218
+ const Presentation &first,
219
+ const Presentation &second) {
220
+ return first.clip.curve == second.clip.curve &&
221
+ first.shadow.enabled == second.shadow.enabled &&
222
+ toChannels(first) == toChannels(second);
360
223
  }
361
224
 
362
- // --- Keyframe curve -------------------------------------------------------
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
+ }
363
229
 
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.
391
- class KeyframeCurve {
392
- public:
393
- void reset(std::vector<Keyframe> frames) {
394
- frames_ = std::move(frames);
395
- 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
- }
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
+ }
413
239
 
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
- }
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
+ }
423
263
 
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
- }
264
+ inline bool isValidReduceMotionCode(int32_t value) {
265
+ return value >= 0 && value <= 2;
266
+ }
454
267
 
455
- const std::vector<Keyframe> &frames() const { return frames_; }
456
- std::size_t size() const { return frames_.size(); }
457
- bool usable() const { return frames_.size() >= 2; }
458
-
459
- Presentation evaluate(double progress) {
460
- const std::size_t count = frames_.size();
461
- if (count == 0) return Presentation{{0, 0, 0, 0, 0}, 0, 0};
462
- if (count == 1) return frames_[0].presentation;
463
-
464
- // The scan resumes from the previous segment instead of restarting at 1.
465
- // Progress is monotonic within an animation, so this is O(1) amortized
466
- // where the old restart-every-frame scan was O(keyframes) per frame; the
467
- // backward walk keeps it correct if progress ever moves the other way.
468
- std::size_t upper = std::min(std::max<std::size_t>(cursor_, 1), count - 1);
469
- while (upper > 1 && progress < frames_[upper - 1].offset) upper -= 1;
470
- while (upper < count - 1 && progress > frames_[upper].offset) upper += 1;
471
- cursor_ = upper;
472
-
473
- const Keyframe &lower = frames_[upper - 1];
474
- const Keyframe &higher = frames_[upper];
475
- const double span = higher.offset - lower.offset;
476
- if (span <= 0) return higher.presentation;
477
-
478
- 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);
498
- }
268
+ inline bool isValidTiming(const TimingAnimation &animation) {
269
+ return std::isfinite(animation.durationMs) && animation.durationMs >= 0 &&
270
+ std::isfinite(animation.controlPoint1X) &&
271
+ std::isfinite(animation.controlPoint1Y) &&
272
+ std::isfinite(animation.controlPoint2X) &&
273
+ std::isfinite(animation.controlPoint2Y) &&
274
+ animation.controlPoint1X >= 0 && animation.controlPoint1X <= 1 &&
275
+ animation.controlPoint2X >= 0 && animation.controlPoint2X <= 1 &&
276
+ isValidReduceMotionCode(animation.reduceMotion);
277
+ }
499
278
 
500
- private:
501
- std::vector<Keyframe> frames_;
502
- std::vector<Channels> tangents_;
503
- std::size_t cursor_ = 1;
504
- };
279
+ inline bool isValidSpring(const SpringAnimation &animation) {
280
+ return std::isfinite(animation.mass) && animation.mass > 0 &&
281
+ std::isfinite(animation.stiffness) && animation.stiffness > 0 &&
282
+ std::isfinite(animation.damping) && animation.damping >= 0 &&
283
+ std::isfinite(animation.initialVelocity) &&
284
+ std::isfinite(animation.energyThreshold) &&
285
+ animation.energyThreshold > 0 &&
286
+ isValidReduceMotionCode(animation.reduceMotion);
287
+ }
505
288
 
506
- struct KeyframeContinuation {
507
- Presentation start;
508
- std::vector<Keyframe> frames;
509
- double durationMs;
289
+ struct ScalarSpringState {
290
+ double position;
291
+ double velocity;
510
292
  };
511
293
 
512
- // Trims keyframes at the same raw-time phase that produced `frozen`. The new
513
- // zero frame is the rendered presentation, not an analytically re-sampled
514
- // neighbor, so a pause/re-latch cannot jump at the seam. Strictly increasing
515
- // remapped offsets avoid duplicate/division-by-zero segments after repeated
516
- // trims or floating-point collisions near the cutoff.
517
- inline KeyframeContinuation keyframeContinuation(
518
- const std::vector<Keyframe> &frames,
519
- const Presentation &frozen,
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,
333
+ const SpringAnimation &animation) {
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;
342
+ }
343
+
344
+ inline bool springScaleStaysPositive(
345
+ const Presentation &start,
520
346
  const Presentation &target,
521
- double durationMs,
522
- double rawProgress) {
523
- const double progress = clamp01(rawProgress);
524
- KeyframeContinuation result{
525
- frozen, {{0.0, frozen}}, std::max(0.0, durationMs * (1 - progress))};
526
- if (progress < 1.0) {
527
- for (const Keyframe &frame : frames) {
528
- if (frame.offset <= progress) continue;
529
- const double offset = clamp01(
530
- (frame.offset - progress) / (1 - progress));
531
- if (offset <= result.frames.back().offset + 1e-12) continue;
532
- result.frames.push_back({offset, frame.presentation});
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;
533
389
  }
390
+ return true;
391
+ }
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);
534
401
  }
535
- if (result.frames.size() == 1 ||
536
- result.frames.back().offset < 1.0 - 1e-12) {
537
- result.frames.push_back({1.0, target});
538
- } else {
539
- result.frames.back().offset = 1.0;
540
- result.frames.back().presentation = target;
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;
541
413
  }
542
- return result;
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);
543
420
  }
544
421
 
545
- inline KeyframeContinuation keyframeContinuationAtFrame(
546
- const std::vector<Keyframe> &frames,
547
- const Presentation &frozen,
548
- const Presentation &target,
549
- double lastRenderedS,
550
- double startedAtS,
551
- double durationS) {
552
- return keyframeContinuation(
553
- frames,
554
- frozen,
555
- target,
556
- std::max(0.0, durationS * 1000.0),
557
- timingFraction(lastRenderedS, startedAtS, durationS));
422
+ inline Presentation fromChannels(
423
+ const Channels &channels,
424
+ ClipCurve curve = ClipCurve::Circular,
425
+ bool shadowEnabled = false) {
426
+ Geometry geometry{
427
+ channels[0], channels[1], channels[2], channels[3], 0.0};
428
+ geometry.topLeftRadius = channels[4];
429
+ geometry.topRightRadius = channels[5];
430
+ geometry.bottomRightRadius = channels[6];
431
+ geometry.bottomLeftRadius = channels[7];
432
+ geometry.radius = radiiAreUniform(
433
+ channels[4], channels[5], channels[6], channels[7])
434
+ ? channels[4]
435
+ : 0.0;
436
+ geometry.curve = curve;
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};
449
+ }
450
+
451
+ inline Presentation interpolate(
452
+ const Presentation &from,
453
+ const Presentation &to,
454
+ double progress) {
455
+ const auto mix = [progress](double start, double end) {
456
+ return start + (end - start) * progress;
457
+ };
458
+ Channels blended{};
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) {
466
+ blended[index] = mix(fromChannelsValue[index], toChannelsValue[index]);
467
+ }
468
+ if (!rendersShadow) {
469
+ std::copy(
470
+ fromChannelsValue.begin() + kBaseChannelCount,
471
+ fromChannelsValue.end(),
472
+ blended.begin() + kBaseChannelCount);
473
+ }
474
+ const bool shadowEnabled = progress >= 1
475
+ ? to.shadow.enabled
476
+ : rendersShadow;
477
+ return fromChannels(blended, from.clip.curve, shadowEnabled);
558
478
  }
559
479
 
560
480
  } // namespace smoothclip