react-native-smooth-clip-view 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -21
- package/android/build.gradle +6 -2
- package/android/proguard-rules.pro +3 -3
- package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +890 -4
- package/android/src/main/cpp/SmoothClipRegistry.cpp +1056 -44
- 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 +371 -18
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
- package/cpp/SmoothClipAnimationCurve.h +151 -122
- package/cpp/SmoothClipRegistry.h +95 -3
- package/cpp/SmoothClipSharedGeometry.h +146 -7
- package/cpp/SmoothClipVelocityTracker.h +13 -9
- package/ios/SmoothClipGeometry.h +184 -6
- package/ios/SmoothClipRegistry.mm +1028 -22
- 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 +263 -63
- 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 +25 -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 +42 -11
- package/src/drivers.native.ts +824 -124
- 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 ||
|
|
@@ -815,6 +1286,16 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
815
1286
|
return state.latest;
|
|
816
1287
|
}
|
|
817
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
|
+
|
|
818
1299
|
const int32_t animationId = state.animation->animationId;
|
|
819
1300
|
const Presentation canonical = canonicalFrozenPresentation(state);
|
|
820
1301
|
state.latest = canonical;
|
|
@@ -824,19 +1305,443 @@ Presentation beginInteraction(uint64_t driverId) {
|
|
|
824
1305
|
return canonical;
|
|
825
1306
|
}
|
|
826
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
|
+
|
|
827
1693
|
int32_t animateTiming(
|
|
828
1694
|
uint64_t driverId,
|
|
829
1695
|
AnimationStart start,
|
|
830
1696
|
Presentation presentation,
|
|
831
|
-
TimingAnimation animation
|
|
1697
|
+
TimingAnimation animation,
|
|
1698
|
+
AnimationValidationMode validationMode) {
|
|
832
1699
|
if (!NSThread.isMainThread) {
|
|
833
1700
|
// See setPresentation: blocking off-main risks a deadlock. 0 is the
|
|
834
1701
|
// documented rejection sentinel.
|
|
835
1702
|
return 0;
|
|
836
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
|
+
}
|
|
837
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
|
+
}
|
|
838
1744
|
if (iterator == registry().end()) {
|
|
839
|
-
if (!start.hasInteractiveStart) return 0;
|
|
840
1745
|
iterator = registry().try_emplace(driverId).first;
|
|
841
1746
|
iterator->second.latest = start.interactiveStart;
|
|
842
1747
|
iterator->second.hasLatest = true;
|
|
@@ -844,10 +1749,10 @@ int32_t animateTiming(
|
|
|
844
1749
|
auto &state = iterator->second;
|
|
845
1750
|
state.destroyed = false;
|
|
846
1751
|
const int32_t animationId = allocateAnimationId();
|
|
847
|
-
const Presentation resolvedStart =
|
|
1752
|
+
const Presentation resolvedStart = preflightStart;
|
|
848
1753
|
prepareAnimation(driverId, state, start, presentation);
|
|
849
1754
|
|
|
850
|
-
if (
|
|
1755
|
+
if (reduced) {
|
|
851
1756
|
applyPresentation(state, presentation, false);
|
|
852
1757
|
emitCompletion(driverId, state, animationId, true);
|
|
853
1758
|
return animationId;
|
|
@@ -891,14 +1796,49 @@ int32_t animateSpring(
|
|
|
891
1796
|
uint64_t driverId,
|
|
892
1797
|
AnimationStart start,
|
|
893
1798
|
Presentation presentation,
|
|
894
|
-
SpringAnimation animation
|
|
1799
|
+
SpringAnimation animation,
|
|
1800
|
+
AnimationValidationMode validationMode) {
|
|
895
1801
|
if (!NSThread.isMainThread) {
|
|
896
1802
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
897
1803
|
return 0;
|
|
898
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
|
+
}
|
|
899
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
|
+
}
|
|
900
1841
|
if (iterator == registry().end()) {
|
|
901
|
-
if (!start.hasInteractiveStart) return 0;
|
|
902
1842
|
iterator = registry().try_emplace(driverId).first;
|
|
903
1843
|
iterator->second.latest = start.interactiveStart;
|
|
904
1844
|
iterator->second.hasLatest = true;
|
|
@@ -906,7 +1846,7 @@ int32_t animateSpring(
|
|
|
906
1846
|
auto &state = iterator->second;
|
|
907
1847
|
state.destroyed = false;
|
|
908
1848
|
const int32_t animationId = allocateAnimationId();
|
|
909
|
-
const Presentation resolvedStart =
|
|
1849
|
+
const Presentation resolvedStart = preflightStart;
|
|
910
1850
|
prepareAnimation(driverId, state, start, presentation);
|
|
911
1851
|
|
|
912
1852
|
if (shouldReduceMotion(animation.reduceMotion)) {
|
|
@@ -949,14 +1889,57 @@ int32_t animateKeyframes(
|
|
|
949
1889
|
Presentation presentation,
|
|
950
1890
|
double durationMs,
|
|
951
1891
|
std::vector<Keyframe> keyframes,
|
|
952
|
-
int32_t reduceMotion
|
|
1892
|
+
int32_t reduceMotion,
|
|
1893
|
+
AnimationValidationMode validationMode) {
|
|
953
1894
|
if (!NSThread.isMainThread) {
|
|
954
1895
|
// See setPresentation: blocking off-main risks a deadlock.
|
|
955
1896
|
return 0;
|
|
956
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
|
+
}
|
|
957
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
|
+
}
|
|
958
1942
|
if (iterator == registry().end()) {
|
|
959
|
-
if (!start.hasInteractiveStart) return 0;
|
|
960
1943
|
iterator = registry().try_emplace(driverId).first;
|
|
961
1944
|
iterator->second.latest = start.interactiveStart;
|
|
962
1945
|
iterator->second.hasLatest = true;
|
|
@@ -964,14 +1947,23 @@ int32_t animateKeyframes(
|
|
|
964
1947
|
auto &state = iterator->second;
|
|
965
1948
|
state.destroyed = false;
|
|
966
1949
|
const int32_t animationId = allocateAnimationId();
|
|
967
|
-
const Presentation resolvedStart =
|
|
1950
|
+
const Presentation resolvedStart = preflightStart;
|
|
968
1951
|
prepareAnimation(driverId, state, start, presentation);
|
|
969
|
-
if (
|
|
1952
|
+
if (reduced) {
|
|
970
1953
|
applyPresentation(state, presentation, false);
|
|
971
1954
|
emitCompletion(driverId, state, animationId, true);
|
|
972
1955
|
return animationId;
|
|
973
1956
|
}
|
|
974
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
|
+
|
|
975
1967
|
ActiveAnimation active{animationId};
|
|
976
1968
|
active.kind = AnimationKind::Keyframes;
|
|
977
1969
|
active.start = resolvedStart;
|
|
@@ -1022,6 +2014,20 @@ CancelResult cancelAnimation(
|
|
|
1022
2014
|
(animationId > 0 && animationId != state.animation->animationId)) {
|
|
1023
2015
|
return {false, state.latest};
|
|
1024
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
|
+
}
|
|
1025
2031
|
|
|
1026
2032
|
#if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
|
|
1027
2033
|
os_signpost_event_emit(
|