react-native-smooth-clip-view 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -21
- package/android/CMakeLists.txt +7 -0
- package/android/build.gradle +14 -2
- package/android/proguard-rules.pro +3 -3
- package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
- package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
- package/android/src/main/cpp/SmoothClipTrace.h +25 -0
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
- package/cpp/SmoothClipAnimationCurve.h +151 -122
- package/cpp/SmoothClipRegistry.h +102 -4
- package/cpp/SmoothClipSharedGeometry.h +146 -7
- package/cpp/SmoothClipVelocityTracker.h +13 -9
- package/ios/SmoothClipGeometry.h +184 -6
- package/ios/SmoothClipRegistry.mm +1033 -25
- package/ios/SmoothClipRegistrySnapshot.h +13 -0
- package/ios/SmoothClipTurboModule.cpp +857 -2
- package/ios/SmoothClipTurboModule.h +310 -0
- package/ios/SmoothClipView.mm +714 -183
- package/ios/SmoothClipViewRegistry.h +1 -0
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +22 -6
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -46
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
- package/lib/module/capabilities.js +4 -0
- package/lib/module/capabilities.js.map +1 -0
- package/lib/module/capabilities.native.js +36 -0
- package/lib/module/capabilities.native.js.map +1 -0
- package/lib/module/capabilityTypes.js +11 -0
- package/lib/module/capabilityTypes.js.map +1 -0
- package/lib/module/driverState.js +0 -28
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.js +1 -281
- package/lib/module/drivers.js.map +1 -1
- package/lib/module/drivers.native.js +340 -75
- package/lib/module/drivers.native.js.map +1 -1
- package/lib/module/geometry.js +147 -17
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupDriverTypes.js +4 -0
- package/lib/module/groupDriverTypes.js.map +1 -0
- package/lib/module/groupDrivers.android.js +4 -0
- package/lib/module/groupDrivers.android.js.map +1 -0
- package/lib/module/groupDrivers.ios.js +4 -0
- package/lib/module/groupDrivers.ios.js.map +1 -0
- package/lib/module/groupDrivers.js +4 -0
- package/lib/module/groupDrivers.js.map +1 -0
- package/lib/module/groupDrivers.native.js +799 -0
- package/lib/module/groupDrivers.native.js.map +1 -0
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationProtocol.js +15 -0
- package/lib/module/presentationProtocol.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +2 -10
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.d.ts +3 -0
- package/lib/typescript/src/capabilities.d.ts.map +1 -0
- package/lib/typescript/src/capabilities.native.d.ts +3 -0
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
- package/lib/typescript/src/capabilityTypes.d.ts +10 -0
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
- package/lib/typescript/src/driverState.d.ts +1 -6
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +39 -11
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.d.ts +2 -4
- package/lib/typescript/src/drivers.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +36 -2
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
- package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
- package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationProtocol.d.ts +4 -0
- package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
- package/package.json +10 -11
- package/src/NativeSmoothClipModule.ts +275 -0
- package/src/SmoothClipView.ios.tsx +30 -11
- package/src/SmoothClipView.tsx +2 -76
- package/src/SmoothClipViewNativeComponent.ts +27 -1
- package/src/capabilities.native.ts +69 -0
- package/src/capabilities.ts +2 -0
- package/src/capabilityTypes.ts +17 -0
- package/src/driverState.ts +1 -41
- package/src/driverTypes.ts +56 -11
- package/src/drivers.native.ts +921 -151
- package/src/drivers.ts +2 -423
- package/src/geometry.ts +231 -27
- package/src/groupDriverTypes.ts +117 -0
- package/src/groupDrivers.android.ts +1 -0
- package/src/groupDrivers.ios.ts +1 -0
- package/src/groupDrivers.native.ts +1189 -0
- package/src/groupDrivers.ts +1 -0
- package/src/index.ts +30 -2
- package/src/presentationProtocol.ts +28 -0
- package/lib/module/SmoothClipView.web.js +0 -54
- package/lib/module/SmoothClipView.web.js.map +0 -1
- package/lib/module/index.web.js +0 -8
- package/lib/module/index.web.js.map +0 -1
- package/lib/module/webClipPath.js +0 -25
- package/lib/module/webClipPath.js.map +0 -1
- package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
- package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
- package/lib/typescript/src/index.web.d.ts +0 -9
- package/lib/typescript/src/index.web.d.ts.map +0 -1
- package/lib/typescript/src/webClipPath.d.ts +0 -7
- package/lib/typescript/src/webClipPath.d.ts.map +0 -1
- package/src/SmoothClipView.web.tsx +0 -66
- package/src/index.web.ts +0 -23
- package/src/webClipPath.ts +0 -30
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
#include "SmoothClipAnimationId.h"
|
|
23
23
|
#include "SmoothClipAnimationCurve.h"
|
|
24
|
+
#include "SmoothClipSharedGeometry.h"
|
|
24
25
|
|
|
25
26
|
@interface SmoothClipView (Registry)
|
|
26
27
|
- (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation;
|
|
@@ -31,13 +32,26 @@
|
|
|
31
32
|
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
32
33
|
animation:(smoothclip::TimingAnimation)animation
|
|
33
34
|
animationId:(int32_t)animationId;
|
|
35
|
+
- (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
|
|
36
|
+
animation:(smoothclip::TimingAnimation)animation
|
|
37
|
+
animationId:(int32_t)animationId
|
|
38
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
34
39
|
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
35
40
|
animation:(smoothclip::SpringAnimation)animation
|
|
36
41
|
animationId:(int32_t)animationId;
|
|
42
|
+
- (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
|
|
43
|
+
animation:(smoothclip::SpringAnimation)animation
|
|
44
|
+
animationId:(int32_t)animationId
|
|
45
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
37
46
|
- (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
|
|
38
47
|
keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
|
|
39
48
|
durationMs:(double)durationMs
|
|
40
49
|
animationId:(int32_t)animationId;
|
|
50
|
+
- (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
|
|
51
|
+
keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
|
|
52
|
+
durationMs:(double)durationMs
|
|
53
|
+
animationId:(int32_t)animationId
|
|
54
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime;
|
|
41
55
|
- (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget;
|
|
42
56
|
- (BOOL)smoothClipIsJoinable;
|
|
43
57
|
- (BOOL)smoothClipCanDisplay;
|
|
@@ -56,6 +70,9 @@ enum class AnimationKind { Timing, Spring, Keyframes };
|
|
|
56
70
|
|
|
57
71
|
struct ActiveAnimation {
|
|
58
72
|
int32_t animationId;
|
|
73
|
+
// Zero identifies a legacy/single-driver animation. A non-zero value binds
|
|
74
|
+
// the driver to an immutable group whose completion is aggregated once.
|
|
75
|
+
int32_t groupId = 0;
|
|
59
76
|
std::unordered_set<ViewKey> participants;
|
|
60
77
|
// Hosts that installed this id and temporarily lost displayability. They
|
|
61
78
|
// may rejoin the same remainder; completion is false if any remain
|
|
@@ -98,11 +115,31 @@ struct CompletionSink {
|
|
|
98
115
|
CompletionCallback callback;
|
|
99
116
|
};
|
|
100
117
|
|
|
118
|
+
struct GroupCompletionSink {
|
|
119
|
+
const void *owner = nullptr;
|
|
120
|
+
GroupCompletionCallback callback;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
struct GroupState {
|
|
124
|
+
uint64_t controllerId;
|
|
125
|
+
int32_t groupId;
|
|
126
|
+
std::vector<uint64_t> driverIds;
|
|
127
|
+
std::unordered_set<uint64_t> remainingDriverIds;
|
|
128
|
+
GroupSuspensionPolicy suspensionPolicy = GroupSuspensionPolicy::Pause;
|
|
129
|
+
bool finished = true;
|
|
130
|
+
bool mutating = false;
|
|
131
|
+
};
|
|
132
|
+
|
|
101
133
|
std::unordered_map<uint64_t, DriverState> ®istry() {
|
|
102
134
|
static std::unordered_map<uint64_t, DriverState> value;
|
|
103
135
|
return value;
|
|
104
136
|
}
|
|
105
137
|
|
|
138
|
+
std::unordered_map<int32_t, GroupState> &groupRegistry() {
|
|
139
|
+
static std::unordered_map<int32_t, GroupState> value;
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
// The transitions must be observed for the whole process lifetime, not per
|
|
107
144
|
// view: the state below is process-global, and a notification delivered while
|
|
108
145
|
// no SmoothClipView happens to be alive (the last host unmounted during
|
|
@@ -143,6 +180,11 @@ CompletionSink &completionSink() {
|
|
|
143
180
|
return sink;
|
|
144
181
|
}
|
|
145
182
|
|
|
183
|
+
GroupCompletionSink &groupCompletionSink() {
|
|
184
|
+
static GroupCompletionSink sink;
|
|
185
|
+
return sink;
|
|
186
|
+
}
|
|
187
|
+
|
|
146
188
|
// The sink is written from the TurboModule ctor/dtor on the JS thread and
|
|
147
189
|
// invoked on main. Invoking under the lock also keeps the owning module
|
|
148
190
|
// alive for the duration of the callback.
|
|
@@ -151,6 +193,18 @@ std::mutex &completionSinkMutex() {
|
|
|
151
193
|
return value;
|
|
152
194
|
}
|
|
153
195
|
|
|
196
|
+
|
|
197
|
+
void groupMemberCompleted(
|
|
198
|
+
int32_t groupId,
|
|
199
|
+
uint64_t driverId,
|
|
200
|
+
bool finished);
|
|
201
|
+
std::vector<DriverSnapshot> cancelGroupInternal(
|
|
202
|
+
int32_t groupId,
|
|
203
|
+
GroupCancelBehavior behavior,
|
|
204
|
+
bool completionFinished);
|
|
205
|
+
void tryStartGroup(int32_t groupId);
|
|
206
|
+
void suspendGroup(int32_t groupId);
|
|
207
|
+
|
|
154
208
|
Presentation unavailablePresentation() {
|
|
155
209
|
// Non-finite geometry makes the JS side fall back to its current value
|
|
156
210
|
// instead of applying zeros.
|
|
@@ -183,7 +237,14 @@ void emitCompletion(
|
|
|
183
237
|
DriverState &state,
|
|
184
238
|
int32_t animationId,
|
|
185
239
|
bool finished) {
|
|
240
|
+
const int32_t groupId = state.animation.has_value()
|
|
241
|
+
? state.animation->groupId
|
|
242
|
+
: 0;
|
|
186
243
|
state.animation.reset();
|
|
244
|
+
if (groupId != 0) {
|
|
245
|
+
groupMemberCompleted(groupId, driverId, finished);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
187
248
|
{
|
|
188
249
|
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
189
250
|
if (completionSink().callback) {
|
|
@@ -198,6 +259,37 @@ void emitCompletion(
|
|
|
198
259
|
#endif
|
|
199
260
|
}
|
|
200
261
|
|
|
262
|
+
void emitGroupCompletion(
|
|
263
|
+
uint64_t controllerId,
|
|
264
|
+
int32_t groupId,
|
|
265
|
+
bool finished,
|
|
266
|
+
const std::vector<uint64_t> &driverIds) {
|
|
267
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
268
|
+
if (groupCompletionSink().callback) {
|
|
269
|
+
groupCompletionSink().callback(
|
|
270
|
+
controllerId, groupId, finished, driverIds);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
void groupMemberCompleted(
|
|
275
|
+
int32_t groupId,
|
|
276
|
+
uint64_t driverId,
|
|
277
|
+
bool finished) {
|
|
278
|
+
auto iterator = groupRegistry().find(groupId);
|
|
279
|
+
if (iterator == groupRegistry().end()) return;
|
|
280
|
+
GroupState &group = iterator->second;
|
|
281
|
+
if (group.remainingDriverIds.erase(driverId) == 0) return;
|
|
282
|
+
group.finished = group.finished && finished;
|
|
283
|
+
if (!group.remainingDriverIds.empty() || group.mutating) return;
|
|
284
|
+
|
|
285
|
+
const uint64_t controllerId = group.controllerId;
|
|
286
|
+
const bool aggregateFinished = group.finished;
|
|
287
|
+
const std::vector<uint64_t> driverIds = group.driverIds;
|
|
288
|
+
groupRegistry().erase(iterator);
|
|
289
|
+
emitGroupCompletion(
|
|
290
|
+
controllerId, groupId, aggregateFinished, driverIds);
|
|
291
|
+
}
|
|
292
|
+
|
|
201
293
|
void emitStandaloneCompletion(
|
|
202
294
|
uint64_t driverId,
|
|
203
295
|
int32_t animationId,
|
|
@@ -258,11 +350,184 @@ Presentation canonicalFrozenPresentation(
|
|
|
258
350
|
return canonical;
|
|
259
351
|
}
|
|
260
352
|
|
|
353
|
+
Presentation canonicalVisiblePresentation(const DriverState &state) {
|
|
354
|
+
if (state.animation.has_value() && !state.animation->started) {
|
|
355
|
+
return state.animation->start;
|
|
356
|
+
}
|
|
357
|
+
for (const ViewKey key : state.views) {
|
|
358
|
+
SmoothClipView *candidate = viewForKey(key);
|
|
359
|
+
if ([candidate smoothClipIsJoinable]) {
|
|
360
|
+
return [candidate smoothClipCurrentPresentation];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return state.latest;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
bool driverReady(const DriverState &state) {
|
|
367
|
+
if (state.destroyed) return false;
|
|
368
|
+
for (const ViewKey key : state.views) {
|
|
369
|
+
if ([viewForKey(key) smoothClipCanDisplay]) return true;
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
bool presentationFitsEveryKnownHostWithoutNormalization(
|
|
375
|
+
const DriverState &state,
|
|
376
|
+
const Presentation &presentation) {
|
|
377
|
+
for (const ViewKey key : state.views) {
|
|
378
|
+
SmoothClipView *view = viewForKey(key);
|
|
379
|
+
// A pre-layout host has no authoritative bounds yet. It remains covered by
|
|
380
|
+
// the existing readiness latch; fixed-host promotion reaches this path
|
|
381
|
+
// only after its participating host has laid out.
|
|
382
|
+
if (![view smoothClipIsJoinable]) continue;
|
|
383
|
+
const CGSize hostSize = view.bounds.size;
|
|
384
|
+
if (!SmoothClipGeometryNormalizationIsIdentity(
|
|
385
|
+
presentation.clip, hostSize.width, hostSize.height)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
bool timingPlanPreservesLinearHostNormalization(
|
|
393
|
+
const DriverState &state,
|
|
394
|
+
const Presentation &start,
|
|
395
|
+
const Presentation &target,
|
|
396
|
+
const TimingAnimation &animation) {
|
|
397
|
+
// A cubic Bezier stays inside the convex hull of its Y control points. The
|
|
398
|
+
// bound is deliberately conservative: if an overshooting easing still fits
|
|
399
|
+
// at both extrema, every presentation it can generate fits too because all
|
|
400
|
+
// host/radius eligibility inequalities are linear.
|
|
401
|
+
const double minimumProgress = std::min(
|
|
402
|
+
{0.0, 1.0, animation.controlPoint1Y, animation.controlPoint2Y});
|
|
403
|
+
const double maximumProgress = std::max(
|
|
404
|
+
{0.0, 1.0, animation.controlPoint1Y, animation.controlPoint2Y});
|
|
405
|
+
const Presentation minimum =
|
|
406
|
+
interpolate(start, target, minimumProgress);
|
|
407
|
+
const Presentation maximum =
|
|
408
|
+
interpolate(start, target, maximumProgress);
|
|
409
|
+
return isFiniteProtocolV2Presentation(minimum) &&
|
|
410
|
+
isFiniteProtocolV2Presentation(maximum) &&
|
|
411
|
+
presentationFitsEveryKnownHostWithoutNormalization(state, minimum) &&
|
|
412
|
+
presentationFitsEveryKnownHostWithoutNormalization(state, maximum);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
bool keyframePlanPreservesLinearHostNormalization(
|
|
416
|
+
const DriverState &state,
|
|
417
|
+
const Presentation &resolvedStart,
|
|
418
|
+
const std::vector<Keyframe> &keyframes) {
|
|
419
|
+
if (!presentationFitsEveryKnownHostWithoutNormalization(
|
|
420
|
+
state, resolvedStart)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
// Frame zero is replaced by resolvedStart for an implicit native retarget.
|
|
424
|
+
// Every later anchor must be identity-normalized; convex segment-wise linear
|
|
425
|
+
// interpolation then cannot cross a host boundary or activate radius scaling.
|
|
426
|
+
for (std::size_t index = 1; index < keyframes.size(); index += 1) {
|
|
427
|
+
if (!presentationFitsEveryKnownHostWithoutNormalization(
|
|
428
|
+
state, keyframes[index].presentation)) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
DriverSnapshot snapshotForDriver(uint64_t driverId) {
|
|
436
|
+
const auto iterator = registry().find(driverId);
|
|
437
|
+
if (iterator == registry().end() || iterator->second.destroyed) {
|
|
438
|
+
return {driverId, unavailablePresentation(), false};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
driverId,
|
|
442
|
+
canonicalVisiblePresentation(iterator->second),
|
|
443
|
+
driverReady(iterator->second),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
std::vector<DriverSnapshot> cancelGroupInternal(
|
|
448
|
+
int32_t groupId,
|
|
449
|
+
GroupCancelBehavior behavior,
|
|
450
|
+
bool completionFinished) {
|
|
451
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
452
|
+
if (groupIterator == groupRegistry().end()) return {};
|
|
453
|
+
GroupState &group = groupIterator->second;
|
|
454
|
+
if (group.mutating) return {};
|
|
455
|
+
group.mutating = true;
|
|
456
|
+
|
|
457
|
+
const std::vector<uint64_t> driverIds = group.driverIds;
|
|
458
|
+
std::vector<DriverSnapshot> snapshots;
|
|
459
|
+
snapshots.reserve(driverIds.size());
|
|
460
|
+
for (const uint64_t driverId : driverIds) {
|
|
461
|
+
auto stateIterator = registry().find(driverId);
|
|
462
|
+
if (stateIterator == registry().end()) {
|
|
463
|
+
snapshots.push_back({driverId, unavailablePresentation(), false});
|
|
464
|
+
groupMemberCompleted(groupId, driverId, false);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
DriverState &state = stateIterator->second;
|
|
468
|
+
if (!state.animation.has_value() ||
|
|
469
|
+
state.animation->groupId != groupId) {
|
|
470
|
+
snapshots.push_back(snapshotForDriver(driverId));
|
|
471
|
+
groupMemberCompleted(groupId, driverId, false);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const int32_t animationId = state.animation->animationId;
|
|
476
|
+
Presentation resolved;
|
|
477
|
+
if (behavior == GroupCancelBehavior::Finish) {
|
|
478
|
+
resolved = state.latest;
|
|
479
|
+
const bool wasLatched = !state.animation->started;
|
|
480
|
+
for (const ViewKey key : state.views) {
|
|
481
|
+
[viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
|
|
482
|
+
}
|
|
483
|
+
if (wasLatched) {
|
|
484
|
+
for (const ViewKey key : state.views) {
|
|
485
|
+
[viewForKey(key) smoothClipApplyPresentation:resolved
|
|
486
|
+
recordVelocitySample:NO];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
resolved = canonicalFrozenPresentation(state);
|
|
491
|
+
}
|
|
492
|
+
state.latest = resolved;
|
|
493
|
+
state.hasLatest = true;
|
|
494
|
+
state.ownership = Ownership::Interactive;
|
|
495
|
+
const bool ready = driverReady(state);
|
|
496
|
+
emitCompletion(
|
|
497
|
+
driverId, state, animationId, completionFinished);
|
|
498
|
+
snapshots.push_back({driverId, resolved, ready});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
groupIterator = groupRegistry().find(groupId);
|
|
502
|
+
if (groupIterator != groupRegistry().end()) {
|
|
503
|
+
GroupState &remainingGroup = groupIterator->second;
|
|
504
|
+
remainingGroup.mutating = false;
|
|
505
|
+
if (remainingGroup.remainingDriverIds.empty()) {
|
|
506
|
+
const uint64_t controllerId = remainingGroup.controllerId;
|
|
507
|
+
const bool finished = remainingGroup.finished;
|
|
508
|
+
const std::vector<uint64_t> completedDrivers =
|
|
509
|
+
remainingGroup.driverIds;
|
|
510
|
+
groupRegistry().erase(groupIterator);
|
|
511
|
+
emitGroupCompletion(
|
|
512
|
+
controllerId, groupId, finished, completedDrivers);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return snapshots;
|
|
516
|
+
}
|
|
517
|
+
|
|
261
518
|
void cancelActive(
|
|
262
519
|
uint64_t driverId,
|
|
263
520
|
DriverState &state,
|
|
264
521
|
bool useTarget) {
|
|
265
522
|
if (!state.animation.has_value()) return;
|
|
523
|
+
if (state.animation->groupId != 0) {
|
|
524
|
+
cancelGroupInternal(
|
|
525
|
+
state.animation->groupId,
|
|
526
|
+
useTarget ? GroupCancelBehavior::Finish
|
|
527
|
+
: GroupCancelBehavior::Freeze,
|
|
528
|
+
false);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
266
531
|
const int32_t animationId = state.animation->animationId;
|
|
267
532
|
// Deferred views are deliberately not completion participants, but they do
|
|
268
533
|
// hold a pending install and requested target. Cancellation must still reach
|
|
@@ -310,8 +575,10 @@ void prepareAnimation(
|
|
|
310
575
|
DriverState &state,
|
|
311
576
|
AnimationStart start,
|
|
312
577
|
Presentation target) {
|
|
313
|
-
|
|
314
|
-
|
|
578
|
+
// An explicit `from` is part of the new animation's start transaction. It
|
|
579
|
+
// intentionally overrides a sampled native frame even when this operation
|
|
580
|
+
// is replacing another native-owned animation.
|
|
581
|
+
const bool acceptsInteractiveStart = start.hasInteractiveStart;
|
|
315
582
|
cancelActive(driverId, state, false);
|
|
316
583
|
if (acceptsInteractiveStart) {
|
|
317
584
|
applyPresentation(state, start.interactiveStart);
|
|
@@ -326,7 +593,7 @@ void prepareAnimation(
|
|
|
326
593
|
Presentation resolvedAnimationStart(
|
|
327
594
|
const DriverState &state,
|
|
328
595
|
const AnimationStart &start) {
|
|
329
|
-
if (start.hasInteractiveStart
|
|
596
|
+
if (start.hasInteractiveStart) {
|
|
330
597
|
return start.interactiveStart;
|
|
331
598
|
}
|
|
332
599
|
// Replacing a latched animation (e.g. open→close before the host mounted):
|
|
@@ -335,7 +602,9 @@ Presentation resolvedAnimationStart(
|
|
|
335
602
|
if (state.animation.has_value() && !state.animation->started) {
|
|
336
603
|
return state.animation->start;
|
|
337
604
|
}
|
|
338
|
-
return state.
|
|
605
|
+
return state.animation.has_value()
|
|
606
|
+
? canonicalVisiblePresentation(state)
|
|
607
|
+
: state.latest;
|
|
339
608
|
}
|
|
340
609
|
|
|
341
610
|
// Installs the remaining part of the active animation on a view that joins
|
|
@@ -346,9 +615,12 @@ void dispatchActiveAnimationJoin(
|
|
|
346
615
|
SmoothClipView *view,
|
|
347
616
|
ViewKey key,
|
|
348
617
|
const Presentation &visible,
|
|
349
|
-
SmoothClipView *reference = nil
|
|
618
|
+
SmoothClipView *reference = nil,
|
|
619
|
+
CFTimeInterval sharedBeginTime = 0) {
|
|
350
620
|
auto &active = *state.animation;
|
|
351
|
-
const double elapsedMs =
|
|
621
|
+
const double elapsedMs = sharedBeginTime > 0
|
|
622
|
+
? 0
|
|
623
|
+
: (CACurrentMediaTime() - active.startedAt) * 1000.0;
|
|
352
624
|
const double remainingMs = MAX(0, active.durationMs - elapsedMs);
|
|
353
625
|
BOOL installed = NO;
|
|
354
626
|
if (active.kind == AnimationKind::Timing) {
|
|
@@ -358,7 +630,8 @@ void dispatchActiveAnimationJoin(
|
|
|
358
630
|
TimingAnimation timing = timingRemainder(active.timing, progress).animation;
|
|
359
631
|
installed = [view smoothClipAnimateTiming:state.latest
|
|
360
632
|
animation:timing
|
|
361
|
-
animationId:active.animationId
|
|
633
|
+
animationId:active.animationId
|
|
634
|
+
sharedBeginTime:sharedBeginTime];
|
|
362
635
|
} else if (active.kind == AnimationKind::Spring) {
|
|
363
636
|
SpringAnimation spring = active.spring;
|
|
364
637
|
if (reference != nil) {
|
|
@@ -371,7 +644,8 @@ void dispatchActiveAnimationJoin(
|
|
|
371
644
|
}
|
|
372
645
|
installed = [view smoothClipAnimateSpring:state.latest
|
|
373
646
|
animation:spring
|
|
374
|
-
animationId:active.animationId
|
|
647
|
+
animationId:active.animationId
|
|
648
|
+
sharedBeginTime:sharedBeginTime];
|
|
375
649
|
} else {
|
|
376
650
|
const double progress = active.durationMs <= 0
|
|
377
651
|
? 1
|
|
@@ -387,7 +661,8 @@ void dispatchActiveAnimationJoin(
|
|
|
387
661
|
installed = [view smoothClipAnimateKeyframes:state.latest
|
|
388
662
|
keyframes:remaining
|
|
389
663
|
durationMs:remainingMs
|
|
390
|
-
animationId:active.animationId
|
|
664
|
+
animationId:active.animationId
|
|
665
|
+
sharedBeginTime:sharedBeginTime];
|
|
391
666
|
}
|
|
392
667
|
if (installed) {
|
|
393
668
|
active.participants.insert(key);
|
|
@@ -485,6 +760,10 @@ void relatchActiveAnimation(
|
|
|
485
760
|
// view. Views that still cannot display defer their install per-view and
|
|
486
761
|
// resume via a later positive displayability update.
|
|
487
762
|
void startLatchedAnimation(DriverState &state) {
|
|
763
|
+
if (state.animation->groupId != 0) {
|
|
764
|
+
tryStartGroup(state.animation->groupId);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
488
767
|
state.animation->startedAt = CACurrentMediaTime();
|
|
489
768
|
state.animation->started = true;
|
|
490
769
|
// Participants describe live CA delegates, not registered views. Anything
|
|
@@ -498,6 +777,86 @@ void startLatchedAnimation(DriverState &state) {
|
|
|
498
777
|
}
|
|
499
778
|
}
|
|
500
779
|
|
|
780
|
+
void tryStartGroup(int32_t groupId) {
|
|
781
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
782
|
+
if (groupIterator == groupRegistry().end() ||
|
|
783
|
+
groupIterator->second.mutating) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const std::vector<uint64_t> driverIds = groupIterator->second.driverIds;
|
|
787
|
+
for (const uint64_t driverId : driverIds) {
|
|
788
|
+
const auto stateIterator = registry().find(driverId);
|
|
789
|
+
if (stateIterator == registry().end() ||
|
|
790
|
+
!stateIterator->second.animation.has_value() ||
|
|
791
|
+
stateIterator->second.animation->groupId != groupId ||
|
|
792
|
+
!driverReady(stateIterator->second)) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const CFTimeInterval sharedBeginTime = CACurrentMediaTime();
|
|
798
|
+
[CATransaction begin];
|
|
799
|
+
[CATransaction setDisableActions:YES];
|
|
800
|
+
for (const uint64_t driverId : driverIds) {
|
|
801
|
+
DriverState &state = registry().find(driverId)->second;
|
|
802
|
+
ActiveAnimation &active = *state.animation;
|
|
803
|
+
active.startedAt = sharedBeginTime;
|
|
804
|
+
active.started = true;
|
|
805
|
+
active.participants.clear();
|
|
806
|
+
const Presentation start = active.start;
|
|
807
|
+
for (const ViewKey key : state.views) {
|
|
808
|
+
SmoothClipView *participant = viewForKey(key);
|
|
809
|
+
[participant smoothClipApplyPresentation:start recordVelocitySample:NO];
|
|
810
|
+
dispatchActiveAnimationJoin(
|
|
811
|
+
state,
|
|
812
|
+
participant,
|
|
813
|
+
key,
|
|
814
|
+
start,
|
|
815
|
+
nil,
|
|
816
|
+
sharedBeginTime);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
[CATransaction commit];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
void suspendGroup(int32_t groupId) {
|
|
823
|
+
auto groupIterator = groupRegistry().find(groupId);
|
|
824
|
+
if (groupIterator == groupRegistry().end() ||
|
|
825
|
+
groupIterator->second.mutating) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (groupIterator->second.suspensionPolicy ==
|
|
829
|
+
GroupSuspensionPolicy::Finish) {
|
|
830
|
+
cancelGroupInternal(
|
|
831
|
+
groupId, GroupCancelBehavior::Finish, true);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
groupIterator->second.mutating = true;
|
|
836
|
+
const std::vector<uint64_t> driverIds = groupIterator->second.driverIds;
|
|
837
|
+
for (const uint64_t driverId : driverIds) {
|
|
838
|
+
auto stateIterator = registry().find(driverId);
|
|
839
|
+
if (stateIterator != registry().end() &&
|
|
840
|
+
stateIterator->second.animation.has_value() &&
|
|
841
|
+
stateIterator->second.animation->groupId == groupId) {
|
|
842
|
+
relatchActiveAnimation(driverId, stateIterator->second);
|
|
843
|
+
} else {
|
|
844
|
+
groupMemberCompleted(groupId, driverId, false);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
groupIterator = groupRegistry().find(groupId);
|
|
849
|
+
if (groupIterator == groupRegistry().end()) return;
|
|
850
|
+
GroupState &group = groupIterator->second;
|
|
851
|
+
group.mutating = false;
|
|
852
|
+
if (!group.remainingDriverIds.empty()) return;
|
|
853
|
+
const uint64_t controllerId = group.controllerId;
|
|
854
|
+
const bool finished = group.finished;
|
|
855
|
+
const std::vector<uint64_t> completedDrivers = group.driverIds;
|
|
856
|
+
groupRegistry().erase(groupIterator);
|
|
857
|
+
emitGroupCompletion(controllerId, groupId, finished, completedDrivers);
|
|
858
|
+
}
|
|
859
|
+
|
|
501
860
|
} // namespace
|
|
502
861
|
|
|
503
862
|
bool applicationIsActive() {
|
|
@@ -508,8 +867,19 @@ void applicationWillResignActive() {
|
|
|
508
867
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
509
868
|
if (!applicationActiveState()) return;
|
|
510
869
|
applicationActiveState() = false;
|
|
870
|
+
std::vector<int32_t> groupIds;
|
|
871
|
+
groupIds.reserve(groupRegistry().size());
|
|
872
|
+
for (const auto &[groupId, group] : groupRegistry()) {
|
|
873
|
+
(void)group;
|
|
874
|
+
groupIds.push_back(groupId);
|
|
875
|
+
}
|
|
876
|
+
for (const int32_t groupId : groupIds) {
|
|
877
|
+
suspendGroup(groupId);
|
|
878
|
+
}
|
|
511
879
|
for (auto &[driverId, state] : registry()) {
|
|
512
|
-
|
|
880
|
+
if (!state.animation.has_value() || state.animation->groupId == 0) {
|
|
881
|
+
relatchActiveAnimation(driverId, state);
|
|
882
|
+
}
|
|
513
883
|
}
|
|
514
884
|
}
|
|
515
885
|
|
|
@@ -517,10 +887,19 @@ void applicationDidBecomeActive() {
|
|
|
517
887
|
NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
|
|
518
888
|
if (applicationActiveState()) return;
|
|
519
889
|
applicationActiveState() = true;
|
|
890
|
+
std::vector<int32_t> groupIds;
|
|
891
|
+
groupIds.reserve(groupRegistry().size());
|
|
892
|
+
for (const auto &[groupId, group] : groupRegistry()) {
|
|
893
|
+
(void)group;
|
|
894
|
+
groupIds.push_back(groupId);
|
|
895
|
+
}
|
|
896
|
+
for (const int32_t groupId : groupIds) {
|
|
897
|
+
tryStartGroup(groupId);
|
|
898
|
+
}
|
|
520
899
|
for (auto &[driverId, state] : registry()) {
|
|
521
900
|
(void)driverId;
|
|
522
901
|
if (state.animation.has_value() && !state.animation->started &&
|
|
523
|
-
anyDisplayableView(state)) {
|
|
902
|
+
state.animation->groupId == 0 && anyDisplayableView(state)) {
|
|
524
903
|
startLatchedAnimation(state);
|
|
525
904
|
}
|
|
526
905
|
}
|
|
@@ -541,6 +920,21 @@ void clearCompletionCallback(const void *owner) {
|
|
|
541
920
|
completionSink().callback = nullptr;
|
|
542
921
|
}
|
|
543
922
|
|
|
923
|
+
void setGroupCompletionCallback(
|
|
924
|
+
const void *owner,
|
|
925
|
+
GroupCompletionCallback callback) {
|
|
926
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
927
|
+
groupCompletionSink().owner = owner;
|
|
928
|
+
groupCompletionSink().callback = std::move(callback);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
void clearGroupCompletionCallback(const void *owner) {
|
|
932
|
+
std::lock_guard<std::mutex> lock(completionSinkMutex());
|
|
933
|
+
if (groupCompletionSink().owner != owner) return;
|
|
934
|
+
groupCompletionSink().owner = nullptr;
|
|
935
|
+
groupCompletionSink().callback = nullptr;
|
|
936
|
+
}
|
|
937
|
+
|
|
544
938
|
void registerView(
|
|
545
939
|
uint64_t driverId,
|
|
546
940
|
SmoothClipView *view,
|
|
@@ -614,6 +1008,38 @@ void viewDisplayabilityChanged(uint64_t driverId, SmoothClipView *view) {
|
|
|
614
1008
|
state.views.end()) {
|
|
615
1009
|
return;
|
|
616
1010
|
}
|
|
1011
|
+
const int32_t groupId = state.animation->groupId;
|
|
1012
|
+
if (groupId != 0) {
|
|
1013
|
+
if (![view smoothClipCanDisplay]) {
|
|
1014
|
+
if (!state.animation->started) {
|
|
1015
|
+
if (!driverReady(state)) suspendGroup(groupId);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (state.animation->participants.count(key) == 0) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (!driverReady(state)) {
|
|
1022
|
+
suspendGroup(groupId);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
[view smoothClipFreezePresentation];
|
|
1026
|
+
if (state.animation.has_value() &&
|
|
1027
|
+
state.animation->groupId == groupId) {
|
|
1028
|
+
state.animation->participants.erase(key);
|
|
1029
|
+
state.animation->suspendedParticipants.insert(key);
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!state.animation->started) {
|
|
1034
|
+
tryStartGroup(groupId);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if ([view smoothClipHasPendingInstall] ||
|
|
1038
|
+
state.animation->suspendedParticipants.count(key) > 0) {
|
|
1039
|
+
joinActiveAnimation(driverId, view);
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
617
1043
|
if (![view smoothClipCanDisplay]) {
|
|
618
1044
|
if (!state.animation->started) return;
|
|
619
1045
|
if (state.animation->participants.count(key) == 0) return;
|
|
@@ -663,6 +1089,11 @@ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
|
|
|
663
1089
|
// instead of falling through.
|
|
664
1090
|
if (!state.animation->started) {
|
|
665
1091
|
if (![view smoothClipCanDisplay]) return false;
|
|
1092
|
+
if (state.animation->groupId != 0) {
|
|
1093
|
+
const int32_t groupId = state.animation->groupId;
|
|
1094
|
+
tryStartGroup(groupId);
|
|
1095
|
+
return state.animation.has_value() && state.animation->started;
|
|
1096
|
+
}
|
|
666
1097
|
startLatchedAnimation(state);
|
|
667
1098
|
return true;
|
|
668
1099
|
}
|
|
@@ -692,6 +1123,46 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
|
|
|
692
1123
|
|
|
693
1124
|
auto &state = iterator->second;
|
|
694
1125
|
const ViewKey key = keyForView(view);
|
|
1126
|
+
const int32_t groupId = state.animation.has_value()
|
|
1127
|
+
? state.animation->groupId
|
|
1128
|
+
: 0;
|
|
1129
|
+
if (groupId != 0) {
|
|
1130
|
+
const bool installed =
|
|
1131
|
+
state.animation->participants.count(key) > 0 ||
|
|
1132
|
+
state.animation->suspendedParticipants.count(key) > 0;
|
|
1133
|
+
bool otherDisplayable = false;
|
|
1134
|
+
for (const ViewKey candidate : state.views) {
|
|
1135
|
+
if (candidate != key && [viewForKey(candidate) smoothClipCanDisplay]) {
|
|
1136
|
+
otherDisplayable = true;
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (!otherDisplayable) {
|
|
1141
|
+
suspendGroup(groupId);
|
|
1142
|
+
} else if (state.animation.has_value() && state.animation->started &&
|
|
1143
|
+
state.animation->participants.count(key) > 0) {
|
|
1144
|
+
[view smoothClipFreezePresentation];
|
|
1145
|
+
}
|
|
1146
|
+
state.views.erase(
|
|
1147
|
+
std::remove(state.views.begin(), state.views.end(), key),
|
|
1148
|
+
state.views.end());
|
|
1149
|
+
if (state.animation.has_value() &&
|
|
1150
|
+
state.animation->groupId == groupId) {
|
|
1151
|
+
state.animation->participants.erase(key);
|
|
1152
|
+
state.animation->suspendedParticipants.erase(key);
|
|
1153
|
+
// A duplicate rendering host may leave without poisoning the immutable
|
|
1154
|
+
// driver participant. If the driver lost its only host, the group's
|
|
1155
|
+
// suspension policy above owns the result.
|
|
1156
|
+
if (installed && otherDisplayable &&
|
|
1157
|
+
state.animation->participants.empty()) {
|
|
1158
|
+
suspendGroup(groupId);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (state.destroyed && state.views.empty()) {
|
|
1162
|
+
registry().erase(iterator);
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
695
1166
|
bool installed = false;
|
|
696
1167
|
if (state.animation.has_value()) {
|
|
697
1168
|
installed = state.animation->participants.count(key) > 0 ||
|
|
@@ -731,7 +1202,8 @@ void setPresentation(
|
|
|
731
1202
|
uint64_t driverId,
|
|
732
1203
|
Presentation presentation,
|
|
733
1204
|
bool takeOwnership,
|
|
734
|
-
bool overridePendingAnimation
|
|
1205
|
+
bool overridePendingAnimation,
|
|
1206
|
+
bool recordVelocity) {
|
|
735
1207
|
// CALayer writes require the main thread (not main-queue drain context).
|
|
736
1208
|
if (!NSThread.isMainThread) {
|
|
737
1209
|
// Never block an off-main caller: a synchronous hop can deadlock against
|
|
@@ -740,7 +1212,8 @@ void setPresentation(
|
|
|
740
1212
|
// same mutex when draining scheduled worklets).
|
|
741
1213
|
RCTExecuteOnMainQueue(^{
|
|
742
1214
|
setPresentation(
|
|
743
|
-
driverId, presentation, takeOwnership, overridePendingAnimation
|
|
1215
|
+
driverId, presentation, takeOwnership, overridePendingAnimation,
|
|
1216
|
+
recordVelocity);
|
|
744
1217
|
});
|
|
745
1218
|
return;
|
|
746
1219
|
}
|
|
@@ -789,7 +1262,7 @@ void setPresentation(
|
|
|
789
1262
|
}
|
|
790
1263
|
cancelActive(driverId, state, false);
|
|
791
1264
|
}
|
|
792
|
-
applyPresentation(state, presentation);
|
|
1265
|
+
applyPresentation(state, presentation, recordVelocity);
|
|
793
1266
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
794
1267
|
if (signpostsEnabled) {
|
|
795
1268
|
os_signpost_interval_end(
|
|
@@ -813,6 +1286,16 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
813
1286
|
return state.latest;
|
|
814
1287
|
}
|
|
815
1288
|
|
|
1289
|
+
if (state.animation->groupId != 0) {
|
|
1290
|
+
const int32_t groupId = state.animation->groupId;
|
|
1291
|
+
cancelGroupInternal(
|
|
1292
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
1293
|
+
const auto current = registry().find(driverId);
|
|
1294
|
+
return current == registry().end()
|
|
1295
|
+
? unavailablePresentation()
|
|
1296
|
+
: current->second.latest;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
816
1299
|
const int32_t animationId = state.animation->animationId;
|
|
817
1300
|
const Presentation canonical = canonicalFrozenPresentation(state);
|
|
818
1301
|
state.latest = canonical;
|
|
@@ -822,19 +1305,443 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
822
1305
|
return canonical;
|
|
823
1306
|
}
|
|
824
1307
|
|
|
1308
|
+
Presentation snapshotCurrent(uint64_t driverId) {
|
|
1309
|
+
if (!NSThread.isMainThread) return unavailablePresentation();
|
|
1310
|
+
const auto iterator = registry().find(driverId);
|
|
1311
|
+
if (iterator == registry().end() || iterator->second.destroyed) {
|
|
1312
|
+
return unavailablePresentation();
|
|
1313
|
+
}
|
|
1314
|
+
return canonicalVisiblePresentation(iterator->second);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
std::vector<DriverSnapshot> snapshotGroup(
|
|
1318
|
+
const std::vector<uint64_t> &driverIds) {
|
|
1319
|
+
if (!NSThread.isMainThread) return {};
|
|
1320
|
+
std::vector<DriverSnapshot> snapshots;
|
|
1321
|
+
snapshots.reserve(driverIds.size());
|
|
1322
|
+
for (const uint64_t driverId : driverIds) {
|
|
1323
|
+
snapshots.push_back(snapshotForDriver(driverId));
|
|
1324
|
+
}
|
|
1325
|
+
return snapshots;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
std::vector<DriverSnapshot> beginGroupInteraction(
|
|
1329
|
+
const std::vector<uint64_t> &driverIds) {
|
|
1330
|
+
if (!NSThread.isMainThread) return {};
|
|
1331
|
+
std::unordered_set<uint64_t> unique;
|
|
1332
|
+
for (const uint64_t driverId : driverIds) {
|
|
1333
|
+
const auto iterator = registry().find(driverId);
|
|
1334
|
+
if (driverId == 0 || !unique.insert(driverId).second ||
|
|
1335
|
+
iterator == registry().end() || iterator->second.destroyed) {
|
|
1336
|
+
std::vector<DriverSnapshot> unavailable;
|
|
1337
|
+
unavailable.reserve(driverIds.size());
|
|
1338
|
+
for (const uint64_t requested : driverIds) {
|
|
1339
|
+
unavailable.push_back(
|
|
1340
|
+
{requested, unavailablePresentation(), false});
|
|
1341
|
+
}
|
|
1342
|
+
return unavailable;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1347
|
+
for (const uint64_t driverId : driverIds) {
|
|
1348
|
+
DriverState &state = registry().find(driverId)->second;
|
|
1349
|
+
if (!state.animation.has_value()) continue;
|
|
1350
|
+
const int32_t groupId = state.animation->groupId;
|
|
1351
|
+
if (groupId != 0) {
|
|
1352
|
+
if (cancelledGroups.insert(groupId).second) {
|
|
1353
|
+
cancelGroupInternal(
|
|
1354
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const int32_t animationId = state.animation->animationId;
|
|
1359
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1360
|
+
state.latest = frozen;
|
|
1361
|
+
state.hasLatest = true;
|
|
1362
|
+
state.ownership = Ownership::Interactive;
|
|
1363
|
+
emitCompletion(driverId, state, animationId, false);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
std::vector<DriverSnapshot> snapshots;
|
|
1367
|
+
snapshots.reserve(driverIds.size());
|
|
1368
|
+
for (const uint64_t driverId : driverIds) {
|
|
1369
|
+
DriverState &state = registry().find(driverId)->second;
|
|
1370
|
+
state.destroyed = false;
|
|
1371
|
+
state.ownership = Ownership::Interactive;
|
|
1372
|
+
snapshots.push_back(
|
|
1373
|
+
{driverId, state.latest, driverReady(state)});
|
|
1374
|
+
}
|
|
1375
|
+
return snapshots;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
bool setPresentationBatch(const std::vector<BatchEntry> &entries) {
|
|
1379
|
+
if (!NSThread.isMainThread) return false;
|
|
1380
|
+
std::unordered_set<uint64_t> unique;
|
|
1381
|
+
for (const BatchEntry &entry : entries) {
|
|
1382
|
+
const auto iterator = registry().find(entry.driverId);
|
|
1383
|
+
if (entry.driverId == 0 || !unique.insert(entry.driverId).second ||
|
|
1384
|
+
iterator == registry().end() || iterator->second.destroyed ||
|
|
1385
|
+
!isFiniteProtocolV2Presentation(entry.presentation)) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1391
|
+
for (const BatchEntry &entry : entries) {
|
|
1392
|
+
DriverState &state = registry().find(entry.driverId)->second;
|
|
1393
|
+
if (!state.animation.has_value()) continue;
|
|
1394
|
+
const int32_t groupId = state.animation->groupId;
|
|
1395
|
+
if (groupId != 0) {
|
|
1396
|
+
if (cancelledGroups.insert(groupId).second) {
|
|
1397
|
+
cancelGroupInternal(
|
|
1398
|
+
groupId, GroupCancelBehavior::Freeze, false);
|
|
1399
|
+
}
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
const int32_t animationId = state.animation->animationId;
|
|
1403
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1404
|
+
state.latest = frozen;
|
|
1405
|
+
state.hasLatest = true;
|
|
1406
|
+
state.ownership = Ownership::Interactive;
|
|
1407
|
+
emitCompletion(entry.driverId, state, animationId, false);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
[CATransaction begin];
|
|
1411
|
+
[CATransaction setDisableActions:YES];
|
|
1412
|
+
for (const BatchEntry &entry : entries) {
|
|
1413
|
+
applyPresentation(
|
|
1414
|
+
registry().find(entry.driverId)->second, entry.presentation, true);
|
|
1415
|
+
}
|
|
1416
|
+
[CATransaction commit];
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
namespace {
|
|
1421
|
+
|
|
1422
|
+
bool preflightGroupEntries(
|
|
1423
|
+
const std::vector<GroupMotionEntry> &entries,
|
|
1424
|
+
AnimationKind kind,
|
|
1425
|
+
const TimingAnimation &timing,
|
|
1426
|
+
const SpringAnimation &spring,
|
|
1427
|
+
double durationMs,
|
|
1428
|
+
int32_t reduceMotion,
|
|
1429
|
+
bool &reduced,
|
|
1430
|
+
std::vector<Presentation> &resolvedStarts) {
|
|
1431
|
+
if (entries.empty()) return false;
|
|
1432
|
+
if (kind == AnimationKind::Timing &&
|
|
1433
|
+
!isValidProtocolV2Timing(timing)) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
if (kind == AnimationKind::Spring &&
|
|
1437
|
+
!isValidProtocolV2Spring(spring)) {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
if (kind == AnimationKind::Keyframes &&
|
|
1441
|
+
(!std::isfinite(durationMs) || durationMs < 0 ||
|
|
1442
|
+
!isValidReduceMotionCode(reduceMotion))) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
reduced = shouldReduceMotion(reduceMotion) ||
|
|
1446
|
+
(kind != AnimationKind::Spring && durationMs <= 0);
|
|
1447
|
+
std::unordered_set<uint64_t> unique;
|
|
1448
|
+
resolvedStarts.clear();
|
|
1449
|
+
resolvedStarts.reserve(entries.size());
|
|
1450
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1451
|
+
if (entry.driverId == 0 || !unique.insert(entry.driverId).second) {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
const auto iterator = registry().find(entry.driverId);
|
|
1455
|
+
if ((iterator == registry().end() || iterator->second.destroyed ||
|
|
1456
|
+
!iterator->second.hasLatest) && !entry.hasFrom) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
const Presentation resolvedStart = entry.hasFrom
|
|
1460
|
+
? entry.from
|
|
1461
|
+
: canonicalVisiblePresentation(iterator->second);
|
|
1462
|
+
if (!isFiniteProtocolV2Presentation(resolvedStart) ||
|
|
1463
|
+
!isFiniteProtocolV2Presentation(entry.target) ||
|
|
1464
|
+
resolvedStart.clip.curve != entry.target.clip.curve) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
if (kind == AnimationKind::Spring &&
|
|
1468
|
+
!protocolV2SpringScaleIsProvablyPositive(
|
|
1469
|
+
resolvedStart, entry.target, spring)) {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
if (kind == AnimationKind::Keyframes) {
|
|
1473
|
+
if (!isValidProtocolV2Keyframes(
|
|
1474
|
+
entry.keyframes,
|
|
1475
|
+
resolvedStart,
|
|
1476
|
+
entry.target,
|
|
1477
|
+
entry.hasFrom)) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
} else if (!entry.keyframes.empty()) {
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
if (!reduced && iterator != registry().end()) {
|
|
1484
|
+
if (kind == AnimationKind::Timing &&
|
|
1485
|
+
!timingPlanPreservesLinearHostNormalization(
|
|
1486
|
+
iterator->second, resolvedStart, entry.target, timing)) {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
if (kind == AnimationKind::Keyframes &&
|
|
1490
|
+
!keyframePlanPreservesLinearHostNormalization(
|
|
1491
|
+
iterator->second, resolvedStart, entry.keyframes)) {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
resolvedStarts.push_back(resolvedStart);
|
|
1496
|
+
}
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
void freezeReplacedAnimations(
|
|
1501
|
+
const std::vector<GroupMotionEntry> &entries) {
|
|
1502
|
+
std::unordered_set<int32_t> cancelledGroups;
|
|
1503
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1504
|
+
auto iterator = registry().find(entry.driverId);
|
|
1505
|
+
if (iterator == registry().end() ||
|
|
1506
|
+
!iterator->second.animation.has_value()) {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
DriverState &state = iterator->second;
|
|
1510
|
+
const int32_t oldGroupId = state.animation->groupId;
|
|
1511
|
+
if (oldGroupId != 0) {
|
|
1512
|
+
if (cancelledGroups.insert(oldGroupId).second) {
|
|
1513
|
+
cancelGroupInternal(
|
|
1514
|
+
oldGroupId, GroupCancelBehavior::Freeze, false);
|
|
1515
|
+
}
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
const int32_t animationId = state.animation->animationId;
|
|
1519
|
+
const Presentation frozen = canonicalFrozenPresentation(state);
|
|
1520
|
+
state.latest = frozen;
|
|
1521
|
+
state.hasLatest = true;
|
|
1522
|
+
state.ownership = Ownership::Interactive;
|
|
1523
|
+
emitCompletion(entry.driverId, state, animationId, false);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
int32_t createGroupAnimation(
|
|
1528
|
+
uint64_t controllerId,
|
|
1529
|
+
std::vector<GroupMotionEntry> entries,
|
|
1530
|
+
AnimationKind kind,
|
|
1531
|
+
TimingAnimation timing,
|
|
1532
|
+
SpringAnimation spring,
|
|
1533
|
+
double durationMs,
|
|
1534
|
+
int32_t reduceMotion,
|
|
1535
|
+
GroupSuspensionPolicy suspensionPolicy) {
|
|
1536
|
+
std::vector<Presentation> resolvedStarts;
|
|
1537
|
+
bool reduced = false;
|
|
1538
|
+
if (!NSThread.isMainThread || controllerId == 0 ||
|
|
1539
|
+
!preflightGroupEntries(
|
|
1540
|
+
entries,
|
|
1541
|
+
kind,
|
|
1542
|
+
timing,
|
|
1543
|
+
spring,
|
|
1544
|
+
durationMs,
|
|
1545
|
+
reduceMotion,
|
|
1546
|
+
reduced,
|
|
1547
|
+
resolvedStarts)) {
|
|
1548
|
+
return 0;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// No state changes occur before every entry is known to be viable. Once
|
|
1552
|
+
// preflight succeeds, replacement freezes each overlapping group at most
|
|
1553
|
+
// once and reports its stale completion before installing the new epoch.
|
|
1554
|
+
freezeReplacedAnimations(entries);
|
|
1555
|
+
const int32_t groupId = allocateAnimationId();
|
|
1556
|
+
std::vector<uint64_t> driverIds;
|
|
1557
|
+
driverIds.reserve(entries.size());
|
|
1558
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1559
|
+
driverIds.push_back(entry.driverId);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (reduced) {
|
|
1563
|
+
[CATransaction begin];
|
|
1564
|
+
[CATransaction setDisableActions:YES];
|
|
1565
|
+
for (const GroupMotionEntry &entry : entries) {
|
|
1566
|
+
auto iterator = registry().find(entry.driverId);
|
|
1567
|
+
if (iterator == registry().end()) {
|
|
1568
|
+
iterator = registry().try_emplace(entry.driverId).first;
|
|
1569
|
+
}
|
|
1570
|
+
DriverState &state = iterator->second;
|
|
1571
|
+
state.destroyed = false;
|
|
1572
|
+
applyPresentation(state, entry.target, false);
|
|
1573
|
+
}
|
|
1574
|
+
[CATransaction commit];
|
|
1575
|
+
emitGroupCompletion(controllerId, groupId, true, driverIds);
|
|
1576
|
+
return groupId;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
GroupState group{
|
|
1580
|
+
controllerId,
|
|
1581
|
+
groupId,
|
|
1582
|
+
driverIds,
|
|
1583
|
+
std::unordered_set<uint64_t>(driverIds.begin(), driverIds.end()),
|
|
1584
|
+
suspensionPolicy,
|
|
1585
|
+
};
|
|
1586
|
+
groupRegistry().emplace(groupId, std::move(group));
|
|
1587
|
+
|
|
1588
|
+
for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
|
|
1589
|
+
GroupMotionEntry &entry = entries[entryIndex];
|
|
1590
|
+
auto iterator = registry().find(entry.driverId);
|
|
1591
|
+
if (iterator == registry().end()) {
|
|
1592
|
+
iterator = registry().try_emplace(entry.driverId).first;
|
|
1593
|
+
}
|
|
1594
|
+
DriverState &state = iterator->second;
|
|
1595
|
+
state.destroyed = false;
|
|
1596
|
+
if (entry.hasFrom) {
|
|
1597
|
+
applyPresentation(state, entry.from, true);
|
|
1598
|
+
}
|
|
1599
|
+
const Presentation start = resolvedStarts[entryIndex];
|
|
1600
|
+
if (kind == AnimationKind::Keyframes && !entry.keyframes.empty()) {
|
|
1601
|
+
// An omitted `from` is a native-to-native retarget: sample the visible
|
|
1602
|
+
// presentation before replacement and substitute that exact value as
|
|
1603
|
+
// frame zero. Explicit endpoints were already validated and are never
|
|
1604
|
+
// silently corrected by native.
|
|
1605
|
+
if (!entry.hasFrom) entry.keyframes.front().presentation = start;
|
|
1606
|
+
}
|
|
1607
|
+
state.latest = entry.target;
|
|
1608
|
+
state.hasLatest = true;
|
|
1609
|
+
state.ownership = Ownership::Native;
|
|
1610
|
+
|
|
1611
|
+
ActiveAnimation active{groupId};
|
|
1612
|
+
active.groupId = groupId;
|
|
1613
|
+
active.kind = kind;
|
|
1614
|
+
active.start = start;
|
|
1615
|
+
active.timing = timing;
|
|
1616
|
+
active.spring = spring;
|
|
1617
|
+
active.keyframes = std::move(entry.keyframes);
|
|
1618
|
+
active.durationMs = durationMs;
|
|
1619
|
+
active.started = false;
|
|
1620
|
+
state.animation = std::move(active);
|
|
1621
|
+
}
|
|
1622
|
+
tryStartGroup(groupId);
|
|
1623
|
+
return groupId;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
} // namespace
|
|
1627
|
+
|
|
1628
|
+
int32_t animateTimingGroup(
|
|
1629
|
+
uint64_t controllerId,
|
|
1630
|
+
std::vector<GroupMotionEntry> entries,
|
|
1631
|
+
TimingAnimation animation,
|
|
1632
|
+
GroupSuspensionPolicy suspensionPolicy,
|
|
1633
|
+
double startedAtHintS) {
|
|
1634
|
+
(void)startedAtHintS;
|
|
1635
|
+
return createGroupAnimation(
|
|
1636
|
+
controllerId,
|
|
1637
|
+
std::move(entries),
|
|
1638
|
+
AnimationKind::Timing,
|
|
1639
|
+
animation,
|
|
1640
|
+
{},
|
|
1641
|
+
animation.durationMs,
|
|
1642
|
+
animation.reduceMotion,
|
|
1643
|
+
suspensionPolicy);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
int32_t animateSpringGroup(
|
|
1647
|
+
uint64_t controllerId,
|
|
1648
|
+
std::vector<GroupMotionEntry> entries,
|
|
1649
|
+
SpringAnimation animation,
|
|
1650
|
+
GroupSuspensionPolicy suspensionPolicy,
|
|
1651
|
+
double startedAtHintS) {
|
|
1652
|
+
(void)startedAtHintS;
|
|
1653
|
+
return createGroupAnimation(
|
|
1654
|
+
controllerId,
|
|
1655
|
+
std::move(entries),
|
|
1656
|
+
AnimationKind::Spring,
|
|
1657
|
+
{},
|
|
1658
|
+
animation,
|
|
1659
|
+
0,
|
|
1660
|
+
animation.reduceMotion,
|
|
1661
|
+
suspensionPolicy);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
int32_t animateKeyframesGroup(
|
|
1665
|
+
uint64_t controllerId,
|
|
1666
|
+
std::vector<GroupMotionEntry> entries,
|
|
1667
|
+
double durationMs,
|
|
1668
|
+
int32_t reduceMotion,
|
|
1669
|
+
GroupSuspensionPolicy suspensionPolicy,
|
|
1670
|
+
double startedAtHintS) {
|
|
1671
|
+
(void)startedAtHintS;
|
|
1672
|
+
return createGroupAnimation(
|
|
1673
|
+
controllerId,
|
|
1674
|
+
std::move(entries),
|
|
1675
|
+
AnimationKind::Keyframes,
|
|
1676
|
+
{},
|
|
1677
|
+
{},
|
|
1678
|
+
durationMs,
|
|
1679
|
+
reduceMotion,
|
|
1680
|
+
suspensionPolicy);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
std::vector<DriverSnapshot> cancelAnimationGroup(
|
|
1684
|
+
int32_t groupId,
|
|
1685
|
+
GroupCancelBehavior behavior) {
|
|
1686
|
+
if (!NSThread.isMainThread || groupId <= 0) return {};
|
|
1687
|
+
return cancelGroupInternal(
|
|
1688
|
+
groupId,
|
|
1689
|
+
behavior,
|
|
1690
|
+
behavior == GroupCancelBehavior::Finish);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
825
1693
|
int32_t animateTiming(
|
|
826
1694
|
uint64_t driverId,
|
|
827
1695
|
AnimationStart start,
|
|
828
1696
|
Presentation presentation,
|
|
829
|
-
TimingAnimation animation
|
|
1697
|
+
TimingAnimation animation,
|
|
1698
|
+
AnimationValidationMode validationMode) {
|
|
830
1699
|
if (!NSThread.isMainThread) {
|
|
831
1700
|
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
832
1701
|
// documented rejection sentinel.
|
|
833
1702
|
return 0;
|
|
834
1703
|
}
|
|
1704
|
+
const bool strictV2 =
|
|
1705
|
+
validationMode == AnimationValidationMode::ProtocolV2;
|
|
1706
|
+
const bool validAnimation = strictV2
|
|
1707
|
+
? isValidProtocolV2Timing(animation)
|
|
1708
|
+
: std::isfinite(animation.durationMs) &&
|
|
1709
|
+
std::isfinite(animation.controlPoint1X) &&
|
|
1710
|
+
std::isfinite(animation.controlPoint1Y) &&
|
|
1711
|
+
std::isfinite(animation.controlPoint2X) &&
|
|
1712
|
+
std::isfinite(animation.controlPoint2Y) &&
|
|
1713
|
+
isValidReduceMotionCode(animation.reduceMotion);
|
|
1714
|
+
if (!strictV2 && validAnimation) {
|
|
1715
|
+
animation.durationMs = std::max(0.0, animation.durationMs);
|
|
1716
|
+
}
|
|
1717
|
+
if (driverId == 0 || !isFiniteProtocolV2Presentation(presentation) ||
|
|
1718
|
+
!validAnimation) {
|
|
1719
|
+
return 0;
|
|
1720
|
+
}
|
|
835
1721
|
auto iterator = registry().find(driverId);
|
|
1722
|
+
Presentation preflightStart;
|
|
1723
|
+
if (iterator == registry().end()) {
|
|
1724
|
+
if (!start.hasInteractiveStart ||
|
|
1725
|
+
!isFiniteProtocolV2Presentation(start.interactiveStart)) {
|
|
1726
|
+
return 0;
|
|
1727
|
+
}
|
|
1728
|
+
preflightStart = start.interactiveStart;
|
|
1729
|
+
} else {
|
|
1730
|
+
if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
|
|
1731
|
+
preflightStart = resolvedAnimationStart(iterator->second, start);
|
|
1732
|
+
}
|
|
1733
|
+
if (!isFiniteProtocolV2Presentation(preflightStart) ||
|
|
1734
|
+
preflightStart.clip.curve != presentation.clip.curve) {
|
|
1735
|
+
return 0;
|
|
1736
|
+
}
|
|
1737
|
+
const bool reduced = shouldReduceMotion(animation.reduceMotion) ||
|
|
1738
|
+
animation.durationMs <= 0;
|
|
1739
|
+
if (strictV2 && !reduced && iterator != registry().end() &&
|
|
1740
|
+
!timingPlanPreservesLinearHostNormalization(
|
|
1741
|
+
iterator->second, preflightStart, presentation, animation)) {
|
|
1742
|
+
return 0;
|
|
1743
|
+
}
|
|
836
1744
|
if (iterator == registry().end()) {
|
|
837
|
-
if (!start.hasInteractiveStart) return 0;
|
|
838
1745
|
iterator = registry().try_emplace(driverId).first;
|
|
839
1746
|
iterator->second.latest = start.interactiveStart;
|
|
840
1747
|
iterator->second.hasLatest = true;
|
|
@@ -842,10 +1749,10 @@ int32_t animateTiming(
|
|
|
842
1749
|
auto &state = iterator->second;
|
|
843
1750
|
state.destroyed = false;
|
|
844
1751
|
const int32_t animationId = allocateAnimationId();
|
|
845
|
-
const Presentation resolvedStart =
|
|
1752
|
+
const Presentation resolvedStart = preflightStart;
|
|
846
1753
|
prepareAnimation(driverId, state, start, presentation);
|
|
847
1754
|
|
|
848
|
-
if (
|
|
1755
|
+
if (reduced) {
|
|
849
1756
|
applyPresentation(state, presentation, false);
|
|
850
1757
|
emitCompletion(driverId, state, animationId, true);
|
|
851
1758
|
return animationId;
|
|
@@ -889,14 +1796,49 @@ int32_t animateSpring(
|
|
|
889
1796
|
uint64_t driverId,
|
|
890
1797
|
AnimationStart start,
|
|
891
1798
|
Presentation presentation,
|
|
892
|
-
SpringAnimation animation
|
|
1799
|
+
SpringAnimation animation,
|
|
1800
|
+
AnimationValidationMode validationMode) {
|
|
893
1801
|
if (!NSThread.isMainThread) {
|
|
894
1802
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
895
1803
|
return 0;
|
|
896
1804
|
}
|
|
1805
|
+
const bool strictV2 =
|
|
1806
|
+
validationMode == AnimationValidationMode::ProtocolV2;
|
|
1807
|
+
const bool validAnimation = strictV2
|
|
1808
|
+
? isValidProtocolV2Spring(animation)
|
|
1809
|
+
: std::isfinite(animation.mass) &&
|
|
1810
|
+
std::isfinite(animation.stiffness) &&
|
|
1811
|
+
std::isfinite(animation.damping) &&
|
|
1812
|
+
std::isfinite(animation.initialVelocity) &&
|
|
1813
|
+
isValidReduceMotionCode(animation.reduceMotion);
|
|
1814
|
+
if (!strictV2 && validAnimation) {
|
|
1815
|
+
animation.mass = std::max(0.0001, animation.mass);
|
|
1816
|
+
animation.stiffness = std::max(0.0001, animation.stiffness);
|
|
1817
|
+
animation.damping = std::max(0.0, animation.damping);
|
|
1818
|
+
}
|
|
1819
|
+
if (driverId == 0 || !isFiniteProtocolV2Presentation(presentation) ||
|
|
1820
|
+
!validAnimation) {
|
|
1821
|
+
return 0;
|
|
1822
|
+
}
|
|
897
1823
|
auto iterator = registry().find(driverId);
|
|
1824
|
+
Presentation preflightStart;
|
|
1825
|
+
if (iterator == registry().end()) {
|
|
1826
|
+
if (!start.hasInteractiveStart ||
|
|
1827
|
+
!isFiniteProtocolV2Presentation(start.interactiveStart)) {
|
|
1828
|
+
return 0;
|
|
1829
|
+
}
|
|
1830
|
+
preflightStart = start.interactiveStart;
|
|
1831
|
+
} else {
|
|
1832
|
+
if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
|
|
1833
|
+
preflightStart = resolvedAnimationStart(iterator->second, start);
|
|
1834
|
+
}
|
|
1835
|
+
if (!isFiniteProtocolV2Presentation(preflightStart) ||
|
|
1836
|
+
preflightStart.clip.curve != presentation.clip.curve ||
|
|
1837
|
+
(strictV2 && !protocolV2SpringScaleIsProvablyPositive(
|
|
1838
|
+
preflightStart, presentation, animation))) {
|
|
1839
|
+
return 0;
|
|
1840
|
+
}
|
|
898
1841
|
if (iterator == registry().end()) {
|
|
899
|
-
if (!start.hasInteractiveStart) return 0;
|
|
900
1842
|
iterator = registry().try_emplace(driverId).first;
|
|
901
1843
|
iterator->second.latest = start.interactiveStart;
|
|
902
1844
|
iterator->second.hasLatest = true;
|
|
@@ -904,7 +1846,7 @@ int32_t animateSpring(
|
|
|
904
1846
|
auto &state = iterator->second;
|
|
905
1847
|
state.destroyed = false;
|
|
906
1848
|
const int32_t animationId = allocateAnimationId();
|
|
907
|
-
const Presentation resolvedStart =
|
|
1849
|
+
const Presentation resolvedStart = preflightStart;
|
|
908
1850
|
prepareAnimation(driverId, state, start, presentation);
|
|
909
1851
|
|
|
910
1852
|
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
@@ -947,14 +1889,57 @@ int32_t animateKeyframes(
|
|
|
947
1889
|
Presentation presentation,
|
|
948
1890
|
double durationMs,
|
|
949
1891
|
std::vector<Keyframe> keyframes,
|
|
950
|
-
int32_t reduceMotion
|
|
1892
|
+
int32_t reduceMotion,
|
|
1893
|
+
AnimationValidationMode validationMode) {
|
|
951
1894
|
if (!NSThread.isMainThread) {
|
|
952
1895
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
953
1896
|
return 0;
|
|
954
1897
|
}
|
|
1898
|
+
const bool strictV2 =
|
|
1899
|
+
validationMode == AnimationValidationMode::ProtocolV2;
|
|
1900
|
+
const bool finiteDuration = std::isfinite(durationMs);
|
|
1901
|
+
if (!strictV2 && finiteDuration) durationMs = std::max(0.0, durationMs);
|
|
1902
|
+
if (driverId == 0 || !isFiniteProtocolV2Presentation(presentation) ||
|
|
1903
|
+
!finiteDuration || (strictV2 && durationMs < 0) ||
|
|
1904
|
+
!isValidReduceMotionCode(reduceMotion)) {
|
|
1905
|
+
return 0;
|
|
1906
|
+
}
|
|
955
1907
|
auto iterator = registry().find(driverId);
|
|
1908
|
+
Presentation preflightStart;
|
|
1909
|
+
if (iterator == registry().end()) {
|
|
1910
|
+
if (!start.hasInteractiveStart ||
|
|
1911
|
+
!isFiniteProtocolV2Presentation(start.interactiveStart)) {
|
|
1912
|
+
return 0;
|
|
1913
|
+
}
|
|
1914
|
+
preflightStart = start.interactiveStart;
|
|
1915
|
+
} else {
|
|
1916
|
+
if (iterator->second.destroyed && !start.hasInteractiveStart) return 0;
|
|
1917
|
+
preflightStart = resolvedAnimationStart(iterator->second, start);
|
|
1918
|
+
}
|
|
1919
|
+
bool validKeyframes = keyframes.size() >= 2 &&
|
|
1920
|
+
keyframes.front().offset == 0 && keyframes.back().offset == 1;
|
|
1921
|
+
double previousOffset = -1;
|
|
1922
|
+
for (const Keyframe &keyframe : keyframes) {
|
|
1923
|
+
validKeyframes = validKeyframes && std::isfinite(keyframe.offset) &&
|
|
1924
|
+
keyframe.offset > previousOffset && keyframe.offset >= 0 &&
|
|
1925
|
+
keyframe.offset <= 1 &&
|
|
1926
|
+
isFiniteProtocolV2Presentation(keyframe.presentation);
|
|
1927
|
+
previousOffset = keyframe.offset;
|
|
1928
|
+
}
|
|
1929
|
+
if (strictV2) {
|
|
1930
|
+
validKeyframes = isValidProtocolV2Keyframes(
|
|
1931
|
+
keyframes, preflightStart, presentation, start.hasInteractiveStart);
|
|
1932
|
+
}
|
|
1933
|
+
if (!isFiniteProtocolV2Presentation(preflightStart) || !validKeyframes) {
|
|
1934
|
+
return 0;
|
|
1935
|
+
}
|
|
1936
|
+
const bool reduced = shouldReduceMotion(reduceMotion) || durationMs <= 0;
|
|
1937
|
+
if (strictV2 && !reduced && iterator != registry().end() &&
|
|
1938
|
+
!keyframePlanPreservesLinearHostNormalization(
|
|
1939
|
+
iterator->second, preflightStart, keyframes)) {
|
|
1940
|
+
return 0;
|
|
1941
|
+
}
|
|
956
1942
|
if (iterator == registry().end()) {
|
|
957
|
-
if (!start.hasInteractiveStart) return 0;
|
|
958
1943
|
iterator = registry().try_emplace(driverId).first;
|
|
959
1944
|
iterator->second.latest = start.interactiveStart;
|
|
960
1945
|
iterator->second.hasLatest = true;
|
|
@@ -962,14 +1947,23 @@ int32_t animateKeyframes(
|
|
|
962
1947
|
auto &state = iterator->second;
|
|
963
1948
|
state.destroyed = false;
|
|
964
1949
|
const int32_t animationId = allocateAnimationId();
|
|
965
|
-
const Presentation resolvedStart =
|
|
1950
|
+
const Presentation resolvedStart = preflightStart;
|
|
966
1951
|
prepareAnimation(driverId, state, start, presentation);
|
|
967
|
-
if (
|
|
1952
|
+
if (reduced) {
|
|
968
1953
|
applyPresentation(state, presentation, false);
|
|
969
1954
|
emitCompletion(driverId, state, animationId, true);
|
|
970
1955
|
return animationId;
|
|
971
1956
|
}
|
|
972
1957
|
|
|
1958
|
+
// Frame zero is a placeholder when the caller omits `from`. Resolve it from
|
|
1959
|
+
// the presentation layer/native-visible state so native-to-native retargets
|
|
1960
|
+
// cannot jump back to a stale compiled sample. With an explicit `from`,
|
|
1961
|
+
// protocol validation requires the same value, so this assignment is a
|
|
1962
|
+
// harmless canonicalization in both cases.
|
|
1963
|
+
if (strictV2 && !start.hasInteractiveStart) {
|
|
1964
|
+
keyframes.front().presentation = resolvedStart;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
973
1967
|
ActiveAnimation active{animationId};
|
|
974
1968
|
active.kind = AnimationKind::Keyframes;
|
|
975
1969
|
active.start = resolvedStart;
|
|
@@ -1020,6 +2014,20 @@ CancelResult cancelAnimation(
|
|
|
1020
2014
|
(animationId > 0 && animationId != state.animation->animationId)) {
|
|
1021
2015
|
return {false, state.latest};
|
|
1022
2016
|
}
|
|
2017
|
+
if (state.animation->groupId != 0) {
|
|
2018
|
+
const int32_t groupId = state.animation->groupId;
|
|
2019
|
+
const std::vector<DriverSnapshot> snapshots = cancelGroupInternal(
|
|
2020
|
+
groupId,
|
|
2021
|
+
useTarget ? GroupCancelBehavior::Finish
|
|
2022
|
+
: GroupCancelBehavior::Freeze,
|
|
2023
|
+
useTarget);
|
|
2024
|
+
for (const DriverSnapshot &snapshot : snapshots) {
|
|
2025
|
+
if (snapshot.driverId == driverId) {
|
|
2026
|
+
return {true, snapshot.presentation};
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return {false, state.latest};
|
|
2030
|
+
}
|
|
1023
2031
|
|
|
1024
2032
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
1025
2033
|
os_signpost_event_emit(
|