react-native-smooth-clip-view 0.2.4 → 0.2.6
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.
- package/README.md +62 -25
- package/android/CMakeLists.txt +0 -1
- package/android/src/main/cpp/SmoothClipAndroid.h +5 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +28 -8
- package/android/src/main/cpp/SmoothClipRegistry.cpp +198 -189
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +20 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +105 -34
- package/cpp/SmoothClipAnimationCurve.h +468 -0
- package/cpp/SmoothClipAnimationId.h +32 -0
- package/cpp/SmoothClipRegistry.h +11 -1
- package/cpp/SmoothClipVelocityTracker.h +32 -15
- package/ios/SmoothClipRegistry.mm +246 -97
- package/ios/SmoothClipTurboModule.cpp +4 -2
- package/ios/SmoothClipTurboModule.h +2 -1
- package/ios/SmoothClipView.mm +73 -16
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/drivers.android.js +3 -1
- package/lib/module/drivers.android.js.map +1 -1
- package/lib/module/drivers.ios.js +3 -391
- package/lib/module/drivers.ios.js.map +1 -1
- package/lib/module/drivers.native.js +470 -0
- package/lib/module/drivers.native.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +6 -3
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.android.d.ts +2 -2
- package/lib/typescript/src/drivers.android.d.ts.map +1 -1
- package/lib/typescript/src/drivers.ios.d.ts +2 -4
- package/lib/typescript/src/drivers.ios.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts +5 -0
- package/lib/typescript/src/drivers.native.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/NativeSmoothClipModule.ts +2 -1
- package/src/driverTypes.ts +6 -3
- package/src/drivers.android.ts +4 -2
- package/src/drivers.ios.ts +4 -687
- package/src/drivers.native.ts +805 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "SmoothClipRegistry.h"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <array>
|
|
7
|
+
#include <cmath>
|
|
8
|
+
#include <cstddef>
|
|
9
|
+
#include <utility>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
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
|
|
15
|
+
// of the registry translation units also lets the XCTest binary pin every rule
|
|
16
|
+
// below — see ios/tests/SmoothClipAnimationCurveTests.mm.
|
|
17
|
+
namespace smoothclip {
|
|
18
|
+
|
|
19
|
+
inline double clamp01(double value) {
|
|
20
|
+
return std::min(1.0, std::max(0.0, value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- Frame clock ----------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// Moves a wall-clock start stamp onto the choreographer frame-time axis by
|
|
26
|
+
// taking the EARLIER of the two stamps.
|
|
27
|
+
//
|
|
28
|
+
// This is Reanimated's own rule, which is what keeps a parallel `withTiming`
|
|
29
|
+
// and the native clip at the same phase: `valueSetter.ts` stamps `startTime`
|
|
30
|
+
// with `global.__frameTimestamp || _getAnimationTimestamp()`, and `min()`
|
|
31
|
+
// reproduces both branches — a call issued between frames keeps its wall stamp
|
|
32
|
+
// (the dispatching frame is later, so the first fraction is already positive
|
|
33
|
+
// and no start frame is duplicated), while a call issued inside the frame that
|
|
34
|
+
// dispatches us adopts that frame's stamp.
|
|
35
|
+
//
|
|
36
|
+
// Deliberately NOT a wall-elapsed rebase (`frameNow - (wallNow - startedAt)`):
|
|
37
|
+
// that bakes the first frame's intra-frame dispatch latency into the curve for
|
|
38
|
+
// the animation's whole duration.
|
|
39
|
+
//
|
|
40
|
+
// Both arguments must be CLOCK_MONOTONIC seconds. This compares them
|
|
41
|
+
// absolutely, so a shared epoch is load-bearing — see nowSeconds() in the
|
|
42
|
+
// Android registry for what breaks if that ever stops holding.
|
|
43
|
+
//
|
|
44
|
+
// min() is an APPROXIMATION of Reanimated's rule that reads no JS state, and
|
|
45
|
+
// it has one wrong branch: a start issued from an earlier phase of a frame
|
|
46
|
+
// already in flight (CALLBACK_INPUT — batched gesture moves) is dispatched in
|
|
47
|
+
// that same doFrame, whose stamp is EARLIER than the call. min() adopts the
|
|
48
|
+
// frame stamp while Reanimated — outside its rAF flush, where
|
|
49
|
+
// __frameTimestamp is cleared — keeps its wall stamp, so the clip led a
|
|
50
|
+
// parallel Reanimated animation by the callback's intra-frame offset for the
|
|
51
|
+
// whole duration. Starts that carry the JS-captured stamp resolve through
|
|
52
|
+
// resolveStartStamp() below and skip this anchor entirely; min() remains for
|
|
53
|
+
// latch flushes and stamp-less callers, where its two branches are exact.
|
|
54
|
+
inline double anchorStartTime(double startedAtS, double frameNowS) {
|
|
55
|
+
return std::min(startedAtS, frameNowS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A resolved start stamp for a new animation.
|
|
59
|
+
struct StartStamp {
|
|
60
|
+
double startedAtS;
|
|
61
|
+
// True when startedAtS already lives on Reanimated's own start axis (the
|
|
62
|
+
// JS side captured `__frameTimestamp || _getAnimationTimestamp()` in the
|
|
63
|
+
// same worklet that issued the animateTo), so the first advance() must NOT
|
|
64
|
+
// re-anchor it with min() — see the CALLBACK_INPUT note on
|
|
65
|
+
// anchorStartTime().
|
|
66
|
+
bool frameClockAnchored;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// How far a JS-captured stamp may sit from the native wall clock and still be
|
|
70
|
+
// trusted. The two reads happen microseconds apart in the same synchronous
|
|
71
|
+
// call stack, and a __frameTimestamp hint can lag by at most one stalled
|
|
72
|
+
// frame; a full second means a broken epoch (a caller stamping from the wrong
|
|
73
|
+
// clock), where falling back to the native clock is strictly safer than
|
|
74
|
+
// completing every animation on its first frame.
|
|
75
|
+
inline constexpr double kStartStampSanityWindowS = 1.0;
|
|
76
|
+
|
|
77
|
+
inline StartStamp resolveStartStamp(double jsStampS, double wallNowS) {
|
|
78
|
+
const bool usable = std::isfinite(jsStampS) &&
|
|
79
|
+
std::abs(jsStampS - wallNowS) <= kStartStampSanityWindowS;
|
|
80
|
+
if (usable) return {jsStampS, true};
|
|
81
|
+
return {wallNowS, false};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Elapsed fraction of a timing/keyframe animation. A non-positive duration is
|
|
85
|
+
// complete on its first frame rather than dividing by zero.
|
|
86
|
+
inline double timingFraction(
|
|
87
|
+
double nowS,
|
|
88
|
+
double startedAtS,
|
|
89
|
+
double durationS) {
|
|
90
|
+
if (durationS <= 0) return 1.0;
|
|
91
|
+
return clamp01((nowS - startedAtS) / durationS);
|
|
92
|
+
}
|
|
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
|
+
}
|
|
108
|
+
|
|
109
|
+
inline double cubicBezierParameterForX(
|
|
110
|
+
double x1,
|
|
111
|
+
double x2,
|
|
112
|
+
double input,
|
|
113
|
+
int iterations = 14) {
|
|
114
|
+
double low = 0;
|
|
115
|
+
double high = 1;
|
|
116
|
+
// Keep the runtime evaluator's established precision/cost: this same solve
|
|
117
|
+
// runs on every Android timing frame.
|
|
118
|
+
for (int iteration = 0; iteration < iterations; iteration += 1) {
|
|
119
|
+
const double t = (low + high) / 2;
|
|
120
|
+
const double inverse = 1 - t;
|
|
121
|
+
const double x = 3 * inverse * inverse * t * x1 +
|
|
122
|
+
3 * inverse * t * t * x2 + t * t * t;
|
|
123
|
+
if (x < input) {
|
|
124
|
+
low = t;
|
|
125
|
+
} else {
|
|
126
|
+
high = t;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return (low + high) / 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
inline double cubicBezier(
|
|
133
|
+
double x1,
|
|
134
|
+
double y1,
|
|
135
|
+
double x2,
|
|
136
|
+
double y2,
|
|
137
|
+
double input) {
|
|
138
|
+
const double t = cubicBezierParameterForX(x1, x2, clamp01(input));
|
|
139
|
+
const double inverse = 1 - t;
|
|
140
|
+
return 3 * inverse * inverse * t * y1 +
|
|
141
|
+
3 * inverse * t * t * y2 + t * t * t;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct TimingRemainder {
|
|
145
|
+
TimingAnimation animation;
|
|
146
|
+
double easedProgress;
|
|
147
|
+
bool representable;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Exact right-hand segment of a CSS/CoreAnimation cubic Bezier after a raw
|
|
151
|
+
// time cutoff. Restarting the original controls from a frozen presentation
|
|
152
|
+
// resets its slope and creates a visible velocity cusp. De Casteljau splitting
|
|
153
|
+
// preserves the original curve; normalizing the right segment to (0,0)->(1,1)
|
|
154
|
+
// makes it directly reusable with a shorter BasicAnimation/integrator run.
|
|
155
|
+
inline TimingRemainder timingRemainder(
|
|
156
|
+
const TimingAnimation &original,
|
|
157
|
+
double rawProgress) {
|
|
158
|
+
const double progress = clamp01(rawProgress);
|
|
159
|
+
if (progress <= 0) return {original, 0, true};
|
|
160
|
+
if (progress >= 1) {
|
|
161
|
+
TimingAnimation complete = original;
|
|
162
|
+
complete.durationMs = 0;
|
|
163
|
+
complete.controlPoint1X = 0;
|
|
164
|
+
complete.controlPoint1Y = 0;
|
|
165
|
+
complete.controlPoint2X = 1;
|
|
166
|
+
complete.controlPoint2Y = 1;
|
|
167
|
+
return {complete, 1, true};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const double t = cubicBezierParameterForX(
|
|
171
|
+
original.controlPoint1X, original.controlPoint2X, progress, 30);
|
|
172
|
+
const CubicBezierPoint p0{0, 0};
|
|
173
|
+
const CubicBezierPoint p1{
|
|
174
|
+
original.controlPoint1X, original.controlPoint1Y};
|
|
175
|
+
const CubicBezierPoint p2{
|
|
176
|
+
original.controlPoint2X, original.controlPoint2Y};
|
|
177
|
+
const CubicBezierPoint p3{1, 1};
|
|
178
|
+
const CubicBezierPoint a = mixBezierPoint(p0, p1, t);
|
|
179
|
+
const CubicBezierPoint b = mixBezierPoint(p1, p2, t);
|
|
180
|
+
const CubicBezierPoint c = mixBezierPoint(p2, p3, t);
|
|
181
|
+
const CubicBezierPoint d = mixBezierPoint(a, b, t);
|
|
182
|
+
const CubicBezierPoint e = mixBezierPoint(b, c, t);
|
|
183
|
+
const CubicBezierPoint split = mixBezierPoint(d, e, t);
|
|
184
|
+
|
|
185
|
+
TimingAnimation remainder = original;
|
|
186
|
+
remainder.durationMs = original.durationMs * (1 - progress);
|
|
187
|
+
const double remainingX = 1 - split.x;
|
|
188
|
+
const double remainingY = 1 - split.y;
|
|
189
|
+
// A curve whose y reaches exactly 1 before its time endpoint and later
|
|
190
|
+
// departs cannot be represented as one endpoint-normalized BasicAnimation.
|
|
191
|
+
// Keep the result finite; ordinary monotone easings never take this branch.
|
|
192
|
+
if (std::abs(remainingX) <= 1e-12 || std::abs(remainingY) <= 1e-12) {
|
|
193
|
+
remainder.controlPoint1X = 0;
|
|
194
|
+
remainder.controlPoint1Y = 0;
|
|
195
|
+
remainder.controlPoint2X = 1;
|
|
196
|
+
remainder.controlPoint2Y = 1;
|
|
197
|
+
return {remainder, split.y, false};
|
|
198
|
+
}
|
|
199
|
+
remainder.controlPoint1X = (e.x - split.x) / remainingX;
|
|
200
|
+
remainder.controlPoint1Y = (e.y - split.y) / remainingY;
|
|
201
|
+
remainder.controlPoint2X = (c.x - split.x) / remainingX;
|
|
202
|
+
remainder.controlPoint2Y = (c.y - split.y) / remainingY;
|
|
203
|
+
return {remainder, split.y, true};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Spring continuation --------------------------------------------------
|
|
207
|
+
|
|
208
|
+
struct NormalizedSpringState {
|
|
209
|
+
// Remaining fraction of the original from->target displacement.
|
|
210
|
+
double remaining;
|
|
211
|
+
// Derivative of `remaining` in units per second.
|
|
212
|
+
double velocity;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Closed-form state of the same mass/spring/damper equation CoreAnimation
|
|
216
|
+
// evaluates. If p is animation progress, remaining=1-p starts at 1 and its
|
|
217
|
+
// derivative starts at -initialVelocity.
|
|
218
|
+
inline NormalizedSpringState normalizedSpringState(
|
|
219
|
+
const SpringAnimation &spring,
|
|
220
|
+
double elapsedS) {
|
|
221
|
+
if (!std::isfinite(elapsedS) || elapsedS <= 0) {
|
|
222
|
+
return {1, -spring.initialVelocity};
|
|
223
|
+
}
|
|
224
|
+
const double mass = spring.mass;
|
|
225
|
+
const double stiffness = spring.stiffness;
|
|
226
|
+
const double damping = spring.damping;
|
|
227
|
+
const double initialVelocity = spring.initialVelocity;
|
|
228
|
+
if (!std::isfinite(mass) || !std::isfinite(stiffness) ||
|
|
229
|
+
!std::isfinite(damping) || !std::isfinite(initialVelocity) ||
|
|
230
|
+
mass <= 0 || stiffness <= 0 || damping < 0) {
|
|
231
|
+
return {1, 0};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const double alpha = damping / (2 * mass);
|
|
235
|
+
const double omegaSquared = stiffness / mass;
|
|
236
|
+
const double discriminant = alpha * alpha - omegaSquared;
|
|
237
|
+
const double criticalScale = std::max(1.0, omegaSquared);
|
|
238
|
+
if (std::abs(discriminant) <= 1e-12 * criticalScale) {
|
|
239
|
+
const double coefficient = alpha - initialVelocity;
|
|
240
|
+
const double decay = std::exp(-alpha * elapsedS);
|
|
241
|
+
const double remaining = decay * (1 + coefficient * elapsedS);
|
|
242
|
+
const double velocity = decay *
|
|
243
|
+
(coefficient - alpha * (1 + coefficient * elapsedS));
|
|
244
|
+
return {remaining, velocity};
|
|
245
|
+
}
|
|
246
|
+
if (discriminant < 0) {
|
|
247
|
+
const double omega = std::sqrt(-discriminant);
|
|
248
|
+
const double coefficient = (alpha - initialVelocity) / omega;
|
|
249
|
+
const double angle = omega * elapsedS;
|
|
250
|
+
const double cosine = std::cos(angle);
|
|
251
|
+
const double sine = std::sin(angle);
|
|
252
|
+
const double basis = cosine + coefficient * sine;
|
|
253
|
+
const double basisVelocity =
|
|
254
|
+
-omega * sine + coefficient * omega * cosine;
|
|
255
|
+
const double decay = std::exp(-alpha * elapsedS);
|
|
256
|
+
return {decay * basis, decay * (basisVelocity - alpha * basis)};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const double root = std::sqrt(discriminant);
|
|
260
|
+
const double firstRate = -alpha + root;
|
|
261
|
+
const double secondRate = -alpha - root;
|
|
262
|
+
const double firstCoefficient =
|
|
263
|
+
(-initialVelocity - secondRate) / (firstRate - secondRate);
|
|
264
|
+
const double secondCoefficient = 1 - firstCoefficient;
|
|
265
|
+
const double first = firstCoefficient * std::exp(firstRate * elapsedS);
|
|
266
|
+
const double second = secondCoefficient * std::exp(secondRate * elapsedS);
|
|
267
|
+
return {first + second, firstRate * first + secondRate * second};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// CASpringAnimation.initialVelocity is normalized by the current remaining
|
|
271
|
+
// distance. Re-expressing the old physical velocity against the frozen
|
|
272
|
+
// current->target displacement preserves the derivative at a re-latch seam.
|
|
273
|
+
inline double springContinuationVelocity(
|
|
274
|
+
const SpringAnimation &spring,
|
|
275
|
+
double elapsedS) {
|
|
276
|
+
const NormalizedSpringState state = normalizedSpringState(spring, elapsedS);
|
|
277
|
+
if (!std::isfinite(state.remaining) || !std::isfinite(state.velocity) ||
|
|
278
|
+
std::abs(state.remaining) <= 1e-9) {
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
const double result = -state.velocity / state.remaining;
|
|
282
|
+
return std::isfinite(result) ? result : 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Presentation channels ------------------------------------------------
|
|
286
|
+
|
|
287
|
+
constexpr std::size_t kChannelCount = 7;
|
|
288
|
+
using Channels = std::array<double, kChannelCount>;
|
|
289
|
+
|
|
290
|
+
inline Channels toChannels(const Presentation &presentation) {
|
|
291
|
+
return {presentation.clip.x,
|
|
292
|
+
presentation.clip.y,
|
|
293
|
+
presentation.clip.width,
|
|
294
|
+
presentation.clip.height,
|
|
295
|
+
presentation.clip.radius,
|
|
296
|
+
presentation.contentTranslateX,
|
|
297
|
+
presentation.contentTranslateY};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
inline Presentation fromChannels(const Channels &channels) {
|
|
301
|
+
return Presentation{
|
|
302
|
+
{channels[0], channels[1], channels[2], channels[3], channels[4]},
|
|
303
|
+
channels[5],
|
|
304
|
+
channels[6]};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
inline Presentation interpolate(
|
|
308
|
+
const Presentation &from,
|
|
309
|
+
const Presentation &to,
|
|
310
|
+
double progress) {
|
|
311
|
+
const auto mix = [progress](double start, double end) {
|
|
312
|
+
return start + (end - start) * progress;
|
|
313
|
+
};
|
|
314
|
+
return Presentation{
|
|
315
|
+
{mix(from.clip.x, to.clip.x),
|
|
316
|
+
mix(from.clip.y, to.clip.y),
|
|
317
|
+
mix(from.clip.width, to.clip.width),
|
|
318
|
+
mix(from.clip.height, to.clip.height),
|
|
319
|
+
mix(from.clip.radius, to.clip.radius)},
|
|
320
|
+
mix(from.contentTranslateX, to.contentTranslateX),
|
|
321
|
+
mix(from.contentTranslateY, to.contentTranslateY)};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- Keyframe curve -------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
// Keyframes sampled from a smooth path, evaluated with monotone cubic Hermite
|
|
327
|
+
// interpolation (Fritsch-Carlson).
|
|
328
|
+
//
|
|
329
|
+
// Straight lerping between keyframes is exact at every keyframe and wrong in
|
|
330
|
+
// between in a way that matters more than its position error suggests: the
|
|
331
|
+
// interpolated VELOCITY is a staircase that steps at every keyframe boundary,
|
|
332
|
+
// beside content whose Reanimated curve is continuous. Consumers bake dozens of
|
|
333
|
+
// keyframes precisely because their geometry path is not affine in progress, so
|
|
334
|
+
// reconstructing that path smoothly is the honest reading of their intent.
|
|
335
|
+
//
|
|
336
|
+
// Monotone rather than plain Catmull-Rom because the alternative can overshoot,
|
|
337
|
+
// and these channels do not tolerate it: width, height and radius must never
|
|
338
|
+
// dip below zero or bulge past the values the consumer asked for. The
|
|
339
|
+
// Fritsch-Carlson tangent clamp keeps every segment inside the range of its own
|
|
340
|
+
// endpoints. Two keyframes degenerate to the old straight line (both tangents
|
|
341
|
+
// equal the secant) — equal in real arithmetic, within one ulp (~1e-13 of the
|
|
342
|
+
// travel) in doubles because the Hermite blend orders its operations
|
|
343
|
+
// differently than the lerp it replaced. The jfloat delivery cast erases the
|
|
344
|
+
// difference long before a pixel could see it.
|
|
345
|
+
//
|
|
346
|
+
// PARITY NOTE: iOS builds a CAKeyframeAnimation with `kCAAnimationLinear`
|
|
347
|
+
// (SmoothClipView.mm) and cannot run this evaluator — CoreAnimation interpolates
|
|
348
|
+
// off-thread. The platforms therefore differ mid-segment by the linearization
|
|
349
|
+
// error this removes, sub-pixel at any realistic keyframe density and zero at
|
|
350
|
+
// every keyframe. Closing it needs either `kCAAnimationCubic` (a different
|
|
351
|
+
// spline that CAN overshoot, so no) or resampling the keyframes densely through
|
|
352
|
+
// this curve before handing them to CA.
|
|
353
|
+
class KeyframeCurve {
|
|
354
|
+
public:
|
|
355
|
+
void reset(std::vector<Keyframe> frames) {
|
|
356
|
+
frames_ = std::move(frames);
|
|
357
|
+
cursor_ = 1;
|
|
358
|
+
tangents_.assign(frames_.size(), Channels{});
|
|
359
|
+
const std::size_t count = frames_.size();
|
|
360
|
+
if (count < 2) return;
|
|
361
|
+
|
|
362
|
+
// Secant slope of every segment, per channel. A non-increasing offset pair
|
|
363
|
+
// (the re-latch remap can produce one) contributes no slope rather than
|
|
364
|
+
// dividing by zero; evaluate() short-circuits those segments too.
|
|
365
|
+
std::vector<Channels> secants(count - 1);
|
|
366
|
+
for (std::size_t index = 0; index + 1 < count; index += 1) {
|
|
367
|
+
const double span = frames_[index + 1].offset - frames_[index].offset;
|
|
368
|
+
const Channels lower = toChannels(frames_[index].presentation);
|
|
369
|
+
const Channels upper = toChannels(frames_[index + 1].presentation);
|
|
370
|
+
for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
|
|
371
|
+
secants[index][channel] =
|
|
372
|
+
span > 0 ? (upper[channel] - lower[channel]) / span : 0.0;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// One-sided at the ends, averaged inside.
|
|
377
|
+
for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
|
|
378
|
+
tangents_[0][channel] = secants[0][channel];
|
|
379
|
+
tangents_[count - 1][channel] = secants[count - 2][channel];
|
|
380
|
+
for (std::size_t index = 1; index + 1 < count; index += 1) {
|
|
381
|
+
tangents_[index][channel] =
|
|
382
|
+
(secants[index - 1][channel] + secants[index][channel]) / 2.0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fritsch-Carlson clamp: this is what makes the curve non-overshooting.
|
|
387
|
+
for (std::size_t index = 0; index + 1 < count; index += 1) {
|
|
388
|
+
for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
|
|
389
|
+
const double secant = secants[index][channel];
|
|
390
|
+
if (secant == 0.0) {
|
|
391
|
+
// Flat segment: pin both ends flat so the curve cannot bulge off a
|
|
392
|
+
// pair of equal keyframes.
|
|
393
|
+
tangents_[index][channel] = 0.0;
|
|
394
|
+
tangents_[index + 1][channel] = 0.0;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
double alpha = tangents_[index][channel] / secant;
|
|
398
|
+
double beta = tangents_[index + 1][channel] / secant;
|
|
399
|
+
if (alpha < 0.0) {
|
|
400
|
+
tangents_[index][channel] = 0.0;
|
|
401
|
+
alpha = 0.0;
|
|
402
|
+
}
|
|
403
|
+
if (beta < 0.0) {
|
|
404
|
+
tangents_[index + 1][channel] = 0.0;
|
|
405
|
+
beta = 0.0;
|
|
406
|
+
}
|
|
407
|
+
const double magnitude = alpha * alpha + beta * beta;
|
|
408
|
+
if (magnitude > 9.0) {
|
|
409
|
+
const double scale = 3.0 / std::sqrt(magnitude);
|
|
410
|
+
tangents_[index][channel] = scale * alpha * secant;
|
|
411
|
+
tangents_[index + 1][channel] = scale * beta * secant;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const std::vector<Keyframe> &frames() const { return frames_; }
|
|
418
|
+
std::size_t size() const { return frames_.size(); }
|
|
419
|
+
bool usable() const { return frames_.size() >= 2; }
|
|
420
|
+
|
|
421
|
+
Presentation evaluate(double progress) {
|
|
422
|
+
const std::size_t count = frames_.size();
|
|
423
|
+
if (count == 0) return Presentation{{0, 0, 0, 0, 0}, 0, 0};
|
|
424
|
+
if (count == 1) return frames_[0].presentation;
|
|
425
|
+
|
|
426
|
+
// The scan resumes from the previous segment instead of restarting at 1.
|
|
427
|
+
// Progress is monotonic within an animation, so this is O(1) amortized
|
|
428
|
+
// where the old restart-every-frame scan was O(keyframes) per frame; the
|
|
429
|
+
// backward walk keeps it correct if progress ever moves the other way.
|
|
430
|
+
std::size_t upper = std::min(std::max<std::size_t>(cursor_, 1), count - 1);
|
|
431
|
+
while (upper > 1 && progress < frames_[upper - 1].offset) upper -= 1;
|
|
432
|
+
while (upper < count - 1 && progress > frames_[upper].offset) upper += 1;
|
|
433
|
+
cursor_ = upper;
|
|
434
|
+
|
|
435
|
+
const Keyframe &lower = frames_[upper - 1];
|
|
436
|
+
const Keyframe &higher = frames_[upper];
|
|
437
|
+
const double span = higher.offset - lower.offset;
|
|
438
|
+
if (span <= 0) return higher.presentation;
|
|
439
|
+
|
|
440
|
+
const double t = clamp01((progress - lower.offset) / span);
|
|
441
|
+
const double t2 = t * t;
|
|
442
|
+
const double t3 = t2 * t;
|
|
443
|
+
// Cubic Hermite basis.
|
|
444
|
+
const double h00 = 2 * t3 - 3 * t2 + 1;
|
|
445
|
+
const double h10 = t3 - 2 * t2 + t;
|
|
446
|
+
const double h01 = -2 * t3 + 3 * t2;
|
|
447
|
+
const double h11 = t3 - t2;
|
|
448
|
+
|
|
449
|
+
const Channels from = toChannels(lower.presentation);
|
|
450
|
+
const Channels to = toChannels(higher.presentation);
|
|
451
|
+
const Channels &fromSlope = tangents_[upper - 1];
|
|
452
|
+
const Channels &toSlope = tangents_[upper];
|
|
453
|
+
Channels blended{};
|
|
454
|
+
for (std::size_t channel = 0; channel < kChannelCount; channel += 1) {
|
|
455
|
+
blended[channel] = h00 * from[channel] +
|
|
456
|
+
h10 * span * fromSlope[channel] + h01 * to[channel] +
|
|
457
|
+
h11 * span * toSlope[channel];
|
|
458
|
+
}
|
|
459
|
+
return fromChannels(blended);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private:
|
|
463
|
+
std::vector<Keyframe> frames_;
|
|
464
|
+
std::vector<Channels> tangents_;
|
|
465
|
+
std::size_t cursor_ = 1;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
} // namespace smoothclip
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <atomic>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <limits>
|
|
6
|
+
|
|
7
|
+
namespace smoothclip {
|
|
8
|
+
|
|
9
|
+
// Completion delivery is asynchronous with respect to registry teardown. A
|
|
10
|
+
// per-driver counter is therefore not an identity: destroying the last host
|
|
11
|
+
// erases that counter, and an effect replay can recreate the same driver and
|
|
12
|
+
// allocate the same id before the old completion reaches JS. Keep ids unique
|
|
13
|
+
// for the practical lifetime of the process instead. The supported call paths
|
|
14
|
+
// are main-thread confined, but the atomic keeps this header safe if a future
|
|
15
|
+
// rejection path allocates before reaching that guard.
|
|
16
|
+
inline int32_t allocateAnimationId() {
|
|
17
|
+
static std::atomic<int32_t> next{0};
|
|
18
|
+
int32_t observed = next.load(std::memory_order_relaxed);
|
|
19
|
+
while (true) {
|
|
20
|
+
const int32_t desired =
|
|
21
|
+
observed == std::numeric_limits<int32_t>::max() ? 1 : observed + 1;
|
|
22
|
+
if (next.compare_exchange_weak(
|
|
23
|
+
observed,
|
|
24
|
+
desired,
|
|
25
|
+
std::memory_order_relaxed,
|
|
26
|
+
std::memory_order_relaxed)) {
|
|
27
|
+
return desired;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
} // namespace smoothclip
|
package/cpp/SmoothClipRegistry.h
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cstdint>
|
|
4
4
|
#include <functional>
|
|
5
|
+
#include <limits>
|
|
5
6
|
#include <vector>
|
|
6
7
|
|
|
7
8
|
namespace smoothclip {
|
|
@@ -46,6 +47,14 @@ struct SpringAnimation {
|
|
|
46
47
|
struct AnimationStart {
|
|
47
48
|
bool hasInteractiveStart;
|
|
48
49
|
Presentation interactiveStart;
|
|
50
|
+
// Reanimated-rule start stamp, captured on the UI runtime at animateTo time
|
|
51
|
+
// as `__frameTimestamp || _getAnimationTimestamp()` and converted to
|
|
52
|
+
// CLOCK_MONOTONIC seconds. NaN means no stamp was captured (older callers,
|
|
53
|
+
// tests, platforms that ignore it): the integrator then falls back to its
|
|
54
|
+
// own clock plus the min() frame-clock anchor. iOS constructs this struct
|
|
55
|
+
// without the member and inherits the NaN default — CoreAnimation anchors
|
|
56
|
+
// its own animations, so the hint is Android-only by design.
|
|
57
|
+
double startedAtHintS = std::numeric_limits<double>::quiet_NaN();
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
struct CancelResult {
|
|
@@ -64,7 +73,8 @@ void clearCompletionCallback(const void *owner);
|
|
|
64
73
|
void setPresentation(
|
|
65
74
|
uint64_t driverId,
|
|
66
75
|
Presentation presentation,
|
|
67
|
-
bool takeOwnership
|
|
76
|
+
bool takeOwnership,
|
|
77
|
+
bool overridePendingAnimation = false);
|
|
68
78
|
Presentation beginInteraction(uint64_t driverId);
|
|
69
79
|
int32_t animateTiming(
|
|
70
80
|
uint64_t driverId,
|
|
@@ -27,9 +27,16 @@ struct VelocitySampleHistory {
|
|
|
27
27
|
// batch spacing and below the 8.33 ms 120 Hz frame, so legitimate
|
|
28
28
|
// consecutive-frame samples never coalesce.
|
|
29
29
|
inline constexpr double kSampleCoalesceWindowS = 0.004;
|
|
30
|
-
// A latest sample
|
|
31
|
-
// yields zero
|
|
30
|
+
// A latest sample this old no longer describes the finger at all; 'inherit'
|
|
31
|
+
// yields zero rather than replaying stale motion. It is the far end of the
|
|
32
|
+
// decay below, not a cliff.
|
|
32
33
|
inline constexpr double kVelocityStalenessS = 0.1;
|
|
34
|
+
// Below this age the sample is still "now": an ordinary fling calls animateTo
|
|
35
|
+
// in the same input batch as, or one frame after, the last drag write, and
|
|
36
|
+
// must inherit its motion undiminished. Decaying from zero age instead would
|
|
37
|
+
// shave ~17% off every normal handoff at 60 Hz — a regression in the case that
|
|
38
|
+
// matters most, in exchange for fixing the case that barely happens.
|
|
39
|
+
inline constexpr double kVelocityFullCreditS = 1.0 / 60.0;
|
|
33
40
|
|
|
34
41
|
inline void recordVelocitySample(
|
|
35
42
|
VelocitySampleHistory &history,
|
|
@@ -60,22 +67,31 @@ inline void recordVelocitySample(
|
|
|
60
67
|
history.latestTimeS = nowS;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
// beginInteraction
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
history.hasLatest = true;
|
|
71
|
-
history.latest = channels;
|
|
72
|
-
history.latestTimeS = nowS;
|
|
73
|
-
}
|
|
70
|
+
// beginInteraction on both platforms records the frozen presentation as a
|
|
71
|
+
// plain sample: a grab mid-animation pairs the frozen value with the last
|
|
72
|
+
// real sample, so an instant refling inherits bounded recent motion instead
|
|
73
|
+
// of launching dead, and a grab at an unchanged value dedupes to a no-op
|
|
74
|
+
// whose staleness keeps aging. There is deliberately no reset primitive —
|
|
75
|
+
// the staleness decay below is the forgetting mechanism, and it forgets
|
|
76
|
+
// gradually rather than all at once.
|
|
74
77
|
|
|
75
78
|
inline void clearVelocitySamples(VelocitySampleHistory &history) {
|
|
76
79
|
history = VelocitySampleHistory{};
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// Age of the newest sample expressed as a credit multiplier: full inside the
|
|
83
|
+
// grace window, decaying linearly to zero at the staleness bound. The old
|
|
84
|
+
// binary cutoff meant a release after a 99 ms still-hold launched at the full
|
|
85
|
+
// drag velocity while 101 ms launched dead — a step the hand can feel, and one
|
|
86
|
+
// RNGH's own windowed event.velocity* does not have. Continuous and monotone
|
|
87
|
+
// in holdS, so no hold duration produces a discontinuity in launch speed.
|
|
88
|
+
inline double velocityStalenessCredit(double holdS) {
|
|
89
|
+
if (holdS <= kVelocityFullCreditS) return 1;
|
|
90
|
+
if (holdS >= kVelocityStalenessS) return 0;
|
|
91
|
+
return (kVelocityStalenessS - holdS) /
|
|
92
|
+
(kVelocityStalenessS - kVelocityFullCreditS);
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
// Normalized velocity of the recorded motion projected onto the
|
|
80
96
|
// latest→target trajectory, in units of remaining-distance per second
|
|
81
97
|
// (1 covers the remaining distance in one second) — the CASpringAnimation
|
|
@@ -87,7 +103,8 @@ inline double inheritedVelocity(
|
|
|
87
103
|
double nowS) {
|
|
88
104
|
if (!history.hasPrevious || !history.hasLatest) return 0;
|
|
89
105
|
const double elapsed = history.latestTimeS - history.previousTimeS;
|
|
90
|
-
|
|
106
|
+
const double credit = velocityStalenessCredit(nowS - history.latestTimeS);
|
|
107
|
+
if (elapsed <= 0 || credit <= 0) {
|
|
91
108
|
return 0;
|
|
92
109
|
}
|
|
93
110
|
double numerator = 0;
|
|
@@ -99,7 +116,7 @@ inline double inheritedVelocity(
|
|
|
99
116
|
denominator += destinationDelta * destinationDelta;
|
|
100
117
|
}
|
|
101
118
|
if (denominator <= 1e-12) return 0;
|
|
102
|
-
const double result = numerator / elapsed / denominator;
|
|
119
|
+
const double result = numerator / elapsed / denominator * credit;
|
|
103
120
|
return std::isfinite(result) ? result : 0;
|
|
104
121
|
}
|
|
105
122
|
|