react-native-smooth-clip-view 0.4.6 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +47 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +18 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
- package/cpp/SmoothClipAnimationCurve.h +36 -11
- package/cpp/SmoothClipRegistry.h +11 -4
- package/ios/SmoothClipBackdropView.h +17 -0
- package/ios/SmoothClipBackdropView.mm +267 -0
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +723 -22
- package/ios/SmoothClipViewRegistry.h +14 -0
- package/lib/module/SmoothClipBackdropView.js +40 -0
- package/lib/module/SmoothClipBackdropView.js.map +1 -0
- package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/lib/module/SmoothClipView.ios.js +17 -1
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
- package/lib/module/geometry.js +44 -6
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groups.native.js +10 -3
- package/lib/module/groups.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationCodec.js +6 -2
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
- package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +19 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groups.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/SmoothClipBackdropView.tsx +53 -0
- package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/src/SmoothClipView.android.tsx +4 -1
- package/src/SmoothClipView.ios.tsx +24 -1
- package/src/SmoothClipView.tsx +4 -1
- package/src/SmoothClipViewNativeComponent.ts +4 -0
- package/src/geometry.ts +59 -4
- package/src/groups.native.ts +9 -3
- package/src/index.ts +7 -1
- package/src/presentationCodec.ts +8 -2
package/ios/SmoothClipView.mm
CHANGED
|
@@ -136,6 +136,8 @@ static bool SmoothClipBuildPresentation(
|
|
|
136
136
|
double shadowSpreadDistance,
|
|
137
137
|
double rotation,
|
|
138
138
|
double opacity,
|
|
139
|
+
double backdropTranslateX,
|
|
140
|
+
double backdropTranslateY,
|
|
139
141
|
smoothclip::Presentation *result) {
|
|
140
142
|
if (result == nullptr || !isfinite(x) || !isfinite(y) ||
|
|
141
143
|
!isfinite(width) || !isfinite(height) ||
|
|
@@ -148,6 +150,7 @@ static bool SmoothClipBuildPresentation(
|
|
|
148
150
|
!isfinite(shadowOffsetX) || !isfinite(shadowOffsetY) ||
|
|
149
151
|
!isfinite(shadowBlurRadius) || !isfinite(shadowSpreadDistance) ||
|
|
150
152
|
!isfinite(rotation) || !isfinite(opacity) ||
|
|
153
|
+
!isfinite(backdropTranslateX) || !isfinite(backdropTranslateY) ||
|
|
151
154
|
contentScale <= 0 || shadowRed < 0 || shadowRed > 1 ||
|
|
152
155
|
shadowGreen < 0 || shadowGreen > 1 ||
|
|
153
156
|
shadowBlue < 0 || shadowBlue > 1 ||
|
|
@@ -179,7 +182,8 @@ static bool SmoothClipBuildPresentation(
|
|
|
179
182
|
shadowSpreadDistance};
|
|
180
183
|
*result = {
|
|
181
184
|
geometry, contentTranslateX, contentTranslateY, contentScale, shadow,
|
|
182
|
-
rotation, smoothclip::clamp01(opacity)
|
|
185
|
+
rotation, smoothclip::clamp01(opacity), backdropTranslateX,
|
|
186
|
+
backdropTranslateY};
|
|
183
187
|
return true;
|
|
184
188
|
}
|
|
185
189
|
|
|
@@ -381,6 +385,324 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
381
385
|
return presentation;
|
|
382
386
|
}
|
|
383
387
|
|
|
388
|
+
// MARK: - Baked shadow tiles
|
|
389
|
+
|
|
390
|
+
// A rounded-rect shadow's pixels depend only on corner radius, blur, spread
|
|
391
|
+
// and colour, so one square tile holds every distinct pixel such a shadow can
|
|
392
|
+
// show: four corners and a band between them that the compositor stretches
|
|
393
|
+
// (`contentsCenter`) to any aperture, with no blur per frame. The margin on
|
|
394
|
+
// each side of the shape is 3σ (1.5 × the CSS blur radius): it holds the tail
|
|
395
|
+
// outside the shape and separates the band from the corners, so a stretched
|
|
396
|
+
// edge is a true straight-edge profile.
|
|
397
|
+
static const CGFloat kSmoothClipShadowTileBand = 2;
|
|
398
|
+
// Tile radii round up to this step, so a drag ramping the radius bakes a
|
|
399
|
+
// handful of tiles rather than one per frame, and a tile corner is never
|
|
400
|
+
// tighter than the clip corner over it (that would read as a dark sliver).
|
|
401
|
+
static const CGFloat kSmoothClipShadowTileRadiusStep = 4;
|
|
402
|
+
|
|
403
|
+
@interface SmoothClipShadowTile : NSObject
|
|
404
|
+
@property(nonatomic, readonly) CGImageRef image;
|
|
405
|
+
/** Shape corner radius the tile was baked for, in points. */
|
|
406
|
+
@property(nonatomic, readonly) CGFloat radius;
|
|
407
|
+
/** Tile border beyond the shape rect on each side, in points. */
|
|
408
|
+
@property(nonatomic, readonly) CGFloat margin;
|
|
409
|
+
@property(nonatomic, readonly) CGFloat side;
|
|
410
|
+
@property(nonatomic, readonly) CGFloat scale;
|
|
411
|
+
@property(nonatomic, readonly) CGRect contentsCenter;
|
|
412
|
+
@end
|
|
413
|
+
|
|
414
|
+
@implementation SmoothClipShadowTile
|
|
415
|
+
- (instancetype)initWithImage:(CGImageRef)image
|
|
416
|
+
radius:(CGFloat)radius
|
|
417
|
+
margin:(CGFloat)margin
|
|
418
|
+
side:(CGFloat)side
|
|
419
|
+
scale:(CGFloat)scale {
|
|
420
|
+
if (self = [super init]) {
|
|
421
|
+
_image = CGImageRetain(image);
|
|
422
|
+
_radius = radius;
|
|
423
|
+
_margin = margin;
|
|
424
|
+
_side = side;
|
|
425
|
+
_scale = scale;
|
|
426
|
+
const CGFloat corner = 2 * margin + radius;
|
|
427
|
+
_contentsCenter = CGRectMake(
|
|
428
|
+
corner / side, corner / side,
|
|
429
|
+
kSmoothClipShadowTileBand / side, kSmoothClipShadowTileBand / side);
|
|
430
|
+
}
|
|
431
|
+
return self;
|
|
432
|
+
}
|
|
433
|
+
- (void)dealloc {
|
|
434
|
+
CGImageRelease(_image);
|
|
435
|
+
}
|
|
436
|
+
@end
|
|
437
|
+
|
|
438
|
+
static CGFloat SmoothClipBakedShadowTileRadius(CGFloat radius) {
|
|
439
|
+
return ceil(MAX(0, radius) / kSmoothClipShadowTileRadiusStep) *
|
|
440
|
+
kSmoothClipShadowTileRadiusStep;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
static CGFloat SmoothClipBakedShadowMargin(const smoothclip::Shadow &shadow) {
|
|
444
|
+
return ceil(1.5 * MAX(0, shadow.blurRadius));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Baked once per (radius step, curve, blur, colour, scale) and cached for the
|
|
448
|
+
// process. The tile is the pure blurred shape the compositor draws for a
|
|
449
|
+
// `shadowPath` layer, with nothing filled in on top of it; colour alpha is
|
|
450
|
+
// applied as layer opacity. It is drawn with a Core Graphics shadow, not by
|
|
451
|
+
// rendering a layer: `renderInContext:` draws a layer's shadow only where the
|
|
452
|
+
// layer has content of its own, so a shadowPath-only layer renders nothing.
|
|
453
|
+
// A Core Graphics shadow of `b` has sigma b / 2, the same sigma Core
|
|
454
|
+
// Animation gives `shadowRadius` b / 2 (the blur path's setting), so the two
|
|
455
|
+
// tails match sample for sample. The context comes from a UIKit image
|
|
456
|
+
// renderer, whose base transform scales the blur with the tile's scale; a
|
|
457
|
+
// hand-scaled CGBitmapContext leaves the shadow in pixel space.
|
|
458
|
+
static NSCache<NSString *, SmoothClipShadowTile *> *SmoothClipShadowTileCache(void) {
|
|
459
|
+
static NSCache<NSString *, SmoothClipShadowTile *> *cache;
|
|
460
|
+
static dispatch_once_t once;
|
|
461
|
+
dispatch_once(&once, ^{
|
|
462
|
+
cache = [NSCache new];
|
|
463
|
+
cache.countLimit = 24;
|
|
464
|
+
});
|
|
465
|
+
return cache;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
static NSString *SmoothClipShadowTileKey(
|
|
469
|
+
CGFloat tileRadius,
|
|
470
|
+
SmoothClipCornerCurve curve,
|
|
471
|
+
const smoothclip::Shadow &shadow,
|
|
472
|
+
CGFloat scale) {
|
|
473
|
+
return [NSString stringWithFormat:@"%g|%d|%g|%g|%g|%g|%g",
|
|
474
|
+
tileRadius, (int)curve, MAX(0, shadow.blurRadius),
|
|
475
|
+
smoothclip::clamp01(shadow.red), smoothclip::clamp01(shadow.green),
|
|
476
|
+
smoothclip::clamp01(shadow.blue), scale];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// The cached tile for this radius step, or nil. Never bakes.
|
|
480
|
+
static SmoothClipShadowTile *SmoothClipCachedShadowTile(
|
|
481
|
+
CGFloat tileRadius,
|
|
482
|
+
SmoothClipCornerCurve curve,
|
|
483
|
+
const smoothclip::Shadow &shadow,
|
|
484
|
+
CGFloat scale) {
|
|
485
|
+
return [SmoothClipShadowTileCache()
|
|
486
|
+
objectForKey:SmoothClipShadowTileKey(tileRadius, curve, shadow, scale)];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Radius steps searched on either side of a missing tile for one to show
|
|
490
|
+
// while it bakes.
|
|
491
|
+
static const int kSmoothClipShadowTileSearchSteps = 16;
|
|
492
|
+
|
|
493
|
+
// The cached tile nearest to `tileRadius`: the exact one, else at each
|
|
494
|
+
// distance the rounder neighbour before the tighter one (a tile corner
|
|
495
|
+
// tighter than the clip corner over it reads as a dark sliver; a rounder one
|
|
496
|
+
// only recedes under the clip). Never bakes.
|
|
497
|
+
static SmoothClipShadowTile *SmoothClipNearestCachedShadowTile(
|
|
498
|
+
CGFloat tileRadius,
|
|
499
|
+
SmoothClipCornerCurve curve,
|
|
500
|
+
const smoothclip::Shadow &shadow,
|
|
501
|
+
CGFloat scale) {
|
|
502
|
+
for (int distance = 0; distance <= kSmoothClipShadowTileSearchSteps;
|
|
503
|
+
distance += 1) {
|
|
504
|
+
const CGFloat delta = distance * kSmoothClipShadowTileRadiusStep;
|
|
505
|
+
SmoothClipShadowTile *tile =
|
|
506
|
+
SmoothClipCachedShadowTile(tileRadius + delta, curve, shadow, scale);
|
|
507
|
+
if (tile != nil) return tile;
|
|
508
|
+
if (distance == 0 || tileRadius - delta < 0) continue;
|
|
509
|
+
tile = SmoothClipCachedShadowTile(tileRadius - delta, curve, shadow, scale);
|
|
510
|
+
if (tile != nil) return tile;
|
|
511
|
+
}
|
|
512
|
+
return nil;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Bakes on the calling thread and caches the result: about a millisecond for
|
|
516
|
+
// a 16 pt blur at 3x, growing with blur² × scale². The callers reach for it
|
|
517
|
+
// only when nothing else can be shown (a cold cache, at mount) and for a
|
|
518
|
+
// run's resting target, which must be exact from the run's last frame on;
|
|
519
|
+
// every other tile bakes through SmoothClipRequestShadowTile, off the frame.
|
|
520
|
+
static NSUInteger gSmoothClipShadowTileBakeCount = 0;
|
|
521
|
+
static SmoothClipShadowTile *SmoothClipBakeShadowTile(
|
|
522
|
+
CGFloat tileRadius,
|
|
523
|
+
SmoothClipCornerCurve curve,
|
|
524
|
+
const smoothclip::Shadow &shadow,
|
|
525
|
+
CGFloat scale) {
|
|
526
|
+
NSString *key = SmoothClipShadowTileKey(tileRadius, curve, shadow, scale);
|
|
527
|
+
SmoothClipShadowTile *tile = [SmoothClipShadowTileCache() objectForKey:key];
|
|
528
|
+
if (tile != nil) return tile;
|
|
529
|
+
gSmoothClipShadowTileBakeCount += 1;
|
|
530
|
+
const CGFloat margin = SmoothClipBakedShadowMargin(shadow);
|
|
531
|
+
|
|
532
|
+
const CGFloat side = 2 * (2 * margin + tileRadius) + kSmoothClipShadowTileBand;
|
|
533
|
+
const CGRect shape = CGRectInset(CGRectMake(0, 0, side, side), margin, margin);
|
|
534
|
+
const SmoothClipCornerRadii radii = {
|
|
535
|
+
tileRadius, tileRadius, tileRadius, tileRadius};
|
|
536
|
+
CGPathRef path = SmoothClipCreateRoundedRectPath(shape, radii, curve);
|
|
537
|
+
CGColorRef color = [UIColor colorWithRed:smoothclip::clamp01(shadow.red)
|
|
538
|
+
green:smoothclip::clamp01(shadow.green)
|
|
539
|
+
blue:smoothclip::clamp01(shadow.blue)
|
|
540
|
+
alpha:1].CGColor;
|
|
541
|
+
const CGFloat blur = MAX(0, shadow.blurRadius);
|
|
542
|
+
UIGraphicsImageRendererFormat *format =
|
|
543
|
+
[UIGraphicsImageRendererFormat defaultFormat];
|
|
544
|
+
format.scale = scale;
|
|
545
|
+
format.opaque = NO;
|
|
546
|
+
format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
|
|
547
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
|
|
548
|
+
initWithSize:CGSizeMake(side, side) format:format];
|
|
549
|
+
UIImage *rendered = [renderer imageWithActions:^(
|
|
550
|
+
UIGraphicsImageRendererContext *rendererContext) {
|
|
551
|
+
CGContextRef context = rendererContext.CGContext;
|
|
552
|
+
// Only the shadow lands on the tile: the shape itself is filled far
|
|
553
|
+
// outside the bitmap and its shadow offset back by the same distance,
|
|
554
|
+
// so the interior keeps the blurred shape's own profile (0.5 at the
|
|
555
|
+
// edge) instead of a hard fill.
|
|
556
|
+
const CGFloat away = 4 * side;
|
|
557
|
+
CGContextSetShadowWithColor(context, CGSizeMake(0, -away), blur, color);
|
|
558
|
+
CGContextTranslateCTM(context, 0, away);
|
|
559
|
+
CGContextAddPath(context, path);
|
|
560
|
+
CGContextSetFillColorWithColor(context, color);
|
|
561
|
+
CGContextFillPath(context);
|
|
562
|
+
}];
|
|
563
|
+
CGPathRelease(path);
|
|
564
|
+
CGImageRef image = rendered.CGImage;
|
|
565
|
+
if (image == NULL) return nil;
|
|
566
|
+
tile = [[SmoothClipShadowTile alloc] initWithImage:image
|
|
567
|
+
radius:tileRadius
|
|
568
|
+
margin:margin
|
|
569
|
+
side:side
|
|
570
|
+
scale:scale];
|
|
571
|
+
[SmoothClipShadowTileCache() setObject:tile forKey:key];
|
|
572
|
+
return tile;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
typedef void (^SmoothClipShadowTileCompletion)(SmoothClipShadowTile *tile);
|
|
576
|
+
|
|
577
|
+
@interface SmoothClipShadowTileRequest : NSObject
|
|
578
|
+
@property(nonatomic) CGFloat tileRadius;
|
|
579
|
+
@property(nonatomic) SmoothClipCornerCurve curve;
|
|
580
|
+
@property(nonatomic) smoothclip::Shadow shadow;
|
|
581
|
+
@property(nonatomic) CGFloat scale;
|
|
582
|
+
@property(nonatomic, readonly)
|
|
583
|
+
NSMutableArray<SmoothClipShadowTileCompletion> *completions;
|
|
584
|
+
@end
|
|
585
|
+
|
|
586
|
+
@implementation SmoothClipShadowTileRequest
|
|
587
|
+
- (instancetype)init {
|
|
588
|
+
if (self = [super init]) {
|
|
589
|
+
_completions = [NSMutableArray array];
|
|
590
|
+
}
|
|
591
|
+
return self;
|
|
592
|
+
}
|
|
593
|
+
@end
|
|
594
|
+
|
|
595
|
+
// Tiles requested off the frame's critical path, in request order, one per
|
|
596
|
+
// main-queue drain: a block enqueued from inside a drain runs in the next
|
|
597
|
+
// one, so a burst of missing steps costs one bake per run-loop turn rather
|
|
598
|
+
// than all of them inside the transaction that asked for them.
|
|
599
|
+
static NSMutableArray<SmoothClipShadowTileRequest *> *SmoothClipPendingShadowTiles(void) {
|
|
600
|
+
static NSMutableArray<SmoothClipShadowTileRequest *> *pending;
|
|
601
|
+
static dispatch_once_t once;
|
|
602
|
+
dispatch_once(&once, ^{
|
|
603
|
+
pending = [NSMutableArray array];
|
|
604
|
+
});
|
|
605
|
+
return pending;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
static NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *
|
|
609
|
+
SmoothClipPendingShadowTilesByKey(void) {
|
|
610
|
+
static NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *pending;
|
|
611
|
+
static dispatch_once_t once;
|
|
612
|
+
dispatch_once(&once, ^{
|
|
613
|
+
pending = [NSMutableDictionary dictionary];
|
|
614
|
+
});
|
|
615
|
+
return pending;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
static BOOL gSmoothClipShadowTileBakeScheduled = NO;
|
|
619
|
+
static void SmoothClipScheduleShadowTileBake(void);
|
|
620
|
+
|
|
621
|
+
static void SmoothClipBakeNextShadowTile(void) {
|
|
622
|
+
gSmoothClipShadowTileBakeScheduled = NO;
|
|
623
|
+
NSMutableArray<SmoothClipShadowTileRequest *> *pending =
|
|
624
|
+
SmoothClipPendingShadowTiles();
|
|
625
|
+
SmoothClipShadowTileRequest *request = pending.firstObject;
|
|
626
|
+
if (request == nil) return;
|
|
627
|
+
[pending removeObjectAtIndex:0];
|
|
628
|
+
[SmoothClipPendingShadowTilesByKey() removeObjectForKey:SmoothClipShadowTileKey(
|
|
629
|
+
request.tileRadius, request.curve, request.shadow, request.scale)];
|
|
630
|
+
SmoothClipShadowTile *tile = SmoothClipBakeShadowTile(
|
|
631
|
+
request.tileRadius, request.curve, request.shadow, request.scale);
|
|
632
|
+
for (SmoothClipShadowTileCompletion completion in request.completions) {
|
|
633
|
+
completion(tile);
|
|
634
|
+
}
|
|
635
|
+
if (pending.count > 0) SmoothClipScheduleShadowTileBake();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
static void SmoothClipScheduleShadowTileBake(void) {
|
|
639
|
+
if (gSmoothClipShadowTileBakeScheduled) return;
|
|
640
|
+
gSmoothClipShadowTileBakeScheduled = YES;
|
|
641
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
642
|
+
SmoothClipBakeNextShadowTile();
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Asks for the tile without baking it now. `completion` runs on the main
|
|
647
|
+
// thread once the tile is cached, at once when it already is.
|
|
648
|
+
static void SmoothClipRequestShadowTile(
|
|
649
|
+
CGFloat tileRadius,
|
|
650
|
+
SmoothClipCornerCurve curve,
|
|
651
|
+
const smoothclip::Shadow &shadow,
|
|
652
|
+
CGFloat scale,
|
|
653
|
+
SmoothClipShadowTileCompletion completion) {
|
|
654
|
+
SmoothClipShadowTile *cached =
|
|
655
|
+
SmoothClipCachedShadowTile(tileRadius, curve, shadow, scale);
|
|
656
|
+
if (cached != nil) {
|
|
657
|
+
if (completion != nil) completion(cached);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
NSString *key = SmoothClipShadowTileKey(tileRadius, curve, shadow, scale);
|
|
661
|
+
NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *byKey =
|
|
662
|
+
SmoothClipPendingShadowTilesByKey();
|
|
663
|
+
SmoothClipShadowTileRequest *request = byKey[key];
|
|
664
|
+
if (request == nil) {
|
|
665
|
+
request = [SmoothClipShadowTileRequest new];
|
|
666
|
+
request.tileRadius = tileRadius;
|
|
667
|
+
request.curve = curve;
|
|
668
|
+
request.shadow = shadow;
|
|
669
|
+
request.scale = scale;
|
|
670
|
+
byKey[key] = request;
|
|
671
|
+
[SmoothClipPendingShadowTiles() addObject:request];
|
|
672
|
+
}
|
|
673
|
+
if (completion != nil) [request.completions addObject:[completion copy]];
|
|
674
|
+
SmoothClipScheduleShadowTileBake();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
NSUInteger SmoothClipShadowTileBakeCountForTesting(void) {
|
|
678
|
+
return gSmoothClipShadowTileBakeCount;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
NSUInteger SmoothClipPendingShadowTileCountForTesting(void) {
|
|
682
|
+
return SmoothClipPendingShadowTiles().count;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
void SmoothClipBakePendingShadowTilesForTesting(void) {
|
|
686
|
+
while (SmoothClipPendingShadowTiles().count > 0) {
|
|
687
|
+
SmoothClipBakeNextShadowTile();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// The shadow layer's frame for a baked tile: the shape rect (aperture grown by
|
|
692
|
+
// the spread, then offset) plus the tile margin on every side.
|
|
693
|
+
static CGRect SmoothClipBakedShadowLayerRect(
|
|
694
|
+
SmoothClipCanonicalGeometry geometry,
|
|
695
|
+
const smoothclip::Shadow &shadow,
|
|
696
|
+
CGFloat margin) {
|
|
697
|
+
const CGFloat spread = shadow.spreadDistance;
|
|
698
|
+
CGRect rect = CGRectInset(geometry.rect, -spread, -spread);
|
|
699
|
+
if (CGRectGetWidth(rect) <= 0 || CGRectGetHeight(rect) <= 0) {
|
|
700
|
+
return CGRectZero;
|
|
701
|
+
}
|
|
702
|
+
rect = CGRectOffset(rect, shadow.offsetX, shadow.offsetY);
|
|
703
|
+
return CGRectInset(rect, -margin, -margin);
|
|
704
|
+
}
|
|
705
|
+
|
|
384
706
|
@implementation SmoothClipView {
|
|
385
707
|
CALayer *_shadowLayer;
|
|
386
708
|
SmoothClipPresentationContainerView *_presentationContainer;
|
|
@@ -395,6 +717,9 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
395
717
|
BOOL _hasShadowModel;
|
|
396
718
|
SmoothClipCanonicalGeometry _shadowModelGeometry;
|
|
397
719
|
smoothclip::Shadow _shadowModel;
|
|
720
|
+
// `shadowRendering="baked"`: stretch one pre-blurred tile instead of
|
|
721
|
+
// blurring a path every frame. See SmoothClipBakedShadowTile.
|
|
722
|
+
BOOL _bakedShadows;
|
|
398
723
|
|
|
399
724
|
SmoothClipContainerView *_clipContainer;
|
|
400
725
|
SmoothClipContainerView *_contentContainer;
|
|
@@ -485,6 +810,7 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
485
810
|
|
|
486
811
|
_shadowLayer = nil;
|
|
487
812
|
// Created lazily so the common clipping-only path owns no shadow layer.
|
|
813
|
+
_bakedShadows = NO;
|
|
488
814
|
|
|
489
815
|
_unequalCornerMask = [CAShapeLayer layer];
|
|
490
816
|
_unequalCornerMask.actions = @{
|
|
@@ -600,31 +926,175 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
600
926
|
@"shadowOffset": [NSNull null],
|
|
601
927
|
@"bounds": [NSNull null],
|
|
602
928
|
@"position": [NSNull null],
|
|
929
|
+
@"opacity": [NSNull null],
|
|
930
|
+
@"contents": [NSNull null],
|
|
931
|
+
@"contentsCenter": [NSNull null],
|
|
932
|
+
@"contentsScale": [NSNull null],
|
|
603
933
|
};
|
|
604
934
|
[_presentationContainer.layer insertSublayer:_shadowLayer below:_clipContainer.layer];
|
|
605
935
|
return _shadowLayer;
|
|
606
936
|
}
|
|
607
937
|
|
|
938
|
+
- (CGFloat)bakedShadowScale {
|
|
939
|
+
const CGFloat scale = self.traitCollection.displayScale;
|
|
940
|
+
return scale > 0 ? scale : UIScreen.mainScreen.scale;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Radius step of the tile a geometry's shadow draws from.
|
|
944
|
+
- (CGFloat)bakedShadowTileRadiusForGeometry:(SmoothClipCanonicalGeometry)geometry
|
|
945
|
+
shadow:(const smoothclip::Shadow &)shadow {
|
|
946
|
+
return SmoothClipBakedShadowTileRadius(SmoothClipAdjustedShadowRadius(
|
|
947
|
+
geometry.radii.topLeft, shadow.spreadDistance));
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
- (BOOL)usesBakedShadowForGeometry:(SmoothClipCanonicalGeometry)geometry
|
|
951
|
+
shadow:(const smoothclip::Shadow &)shadow {
|
|
952
|
+
// Unequal radii have no single tile; they keep the blur path. So does a
|
|
953
|
+
// shape too small for two corner slices on a side, 2 x (margin + radius):
|
|
954
|
+
// the compositor would squash the slices rather than overlap the blur
|
|
955
|
+
// tails the way the true shadow does.
|
|
956
|
+
if (!_bakedShadows || !SmoothClipCornerRadiiAreUniform(geometry.radii)) {
|
|
957
|
+
return NO;
|
|
958
|
+
}
|
|
959
|
+
const CGFloat minimumSide = 2 *
|
|
960
|
+
(SmoothClipBakedShadowMargin(shadow) +
|
|
961
|
+
[self bakedShadowTileRadiusForGeometry:geometry shadow:shadow]);
|
|
962
|
+
const CGFloat spread = 2 * shadow.spreadDistance;
|
|
963
|
+
return CGRectGetWidth(geometry.rect) + spread >= minimumSide &&
|
|
964
|
+
CGRectGetHeight(geometry.rect) + spread >= minimumSide;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// A requested tile is cached: draw it, if the model still wants it and
|
|
968
|
+
// nothing else owns the layer (a run in flight swaps tiles from its own
|
|
969
|
+
// keyframes, and its completion writes the model again).
|
|
970
|
+
- (void)shadowTileDidLand:(SmoothClipShadowTile *)tile {
|
|
971
|
+
if (tile == nil || _activeAnimationId != 0 || !_hasShadowModel ||
|
|
972
|
+
_shadowLayer == nil ||
|
|
973
|
+
![self usesBakedShadowForGeometry:_shadowModelGeometry
|
|
974
|
+
shadow:_shadowModel]) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const CGFloat wanted = [self bakedShadowTileRadiusForGeometry:_shadowModelGeometry
|
|
978
|
+
shadow:_shadowModel];
|
|
979
|
+
if (SmoothClipCachedShadowTile(wanted, _shadowModelGeometry.curve,
|
|
980
|
+
_shadowModel, [self bakedShadowScale]) != tile ||
|
|
981
|
+
_shadowLayer.contents == (__bridge id)tile.image) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
[CATransaction begin];
|
|
985
|
+
[CATransaction setDisableActions:YES];
|
|
986
|
+
[self writeBakedShadowModel:_shadowModel
|
|
987
|
+
geometry:_shadowModelGeometry
|
|
988
|
+
visible:SmoothBoxShadowVisible(_shadowModelGeometry, _shadowModel)
|
|
989
|
+
nearestTile:NO
|
|
990
|
+
layer:_shadowLayer];
|
|
991
|
+
[CATransaction commit];
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// `allowNearest`: a missing tile may be stood in for by the nearest cached
|
|
995
|
+
// step until the exact one lands (a drag crossing a step, a model write); a
|
|
996
|
+
// run's resting target passes NO and bakes now, so its last frame is exact.
|
|
997
|
+
- (void)writeBakedShadowModel:(const smoothclip::Shadow &)shadow
|
|
998
|
+
geometry:(SmoothClipCanonicalGeometry)geometry
|
|
999
|
+
visible:(BOOL)visible
|
|
1000
|
+
nearestTile:(BOOL)allowNearest
|
|
1001
|
+
layer:(CALayer *)shadowLayer {
|
|
1002
|
+
SmoothClipShadowTile *tile = nil;
|
|
1003
|
+
if (visible) {
|
|
1004
|
+
const CGFloat tileRadius =
|
|
1005
|
+
[self bakedShadowTileRadiusForGeometry:geometry shadow:shadow];
|
|
1006
|
+
const CGFloat scale = [self bakedShadowScale];
|
|
1007
|
+
tile = SmoothClipCachedShadowTile(tileRadius, geometry.curve, shadow, scale);
|
|
1008
|
+
if (tile == nil && allowNearest) {
|
|
1009
|
+
tile = SmoothClipNearestCachedShadowTile(
|
|
1010
|
+
tileRadius, geometry.curve, shadow, scale);
|
|
1011
|
+
if (tile != nil) {
|
|
1012
|
+
// A neighbouring step for now (one step off is invisible under the
|
|
1013
|
+
// blur the tile was made for); the exact tile lands off this frame.
|
|
1014
|
+
__weak SmoothClipView *weakSelf = self;
|
|
1015
|
+
SmoothClipRequestShadowTile(
|
|
1016
|
+
tileRadius, geometry.curve, shadow, scale,
|
|
1017
|
+
^(SmoothClipShadowTile *exact) {
|
|
1018
|
+
[weakSelf shadowTileDidLand:exact];
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (tile == nil) {
|
|
1023
|
+
// Nothing near enough to show meanwhile (a cold cache, at mount), or a
|
|
1024
|
+
// resting target that must be exact.
|
|
1025
|
+
tile = SmoothClipBakeShadowTile(tileRadius, geometry.curve, shadow, scale);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Leave no blur-path state behind: a tile layer that kept a shadowPath
|
|
1029
|
+
// would draw both.
|
|
1030
|
+
if (shadowLayer.shadowPath != NULL) shadowLayer.shadowPath = NULL;
|
|
1031
|
+
shadowLayer.shadowOpacity = 0;
|
|
1032
|
+
if (tile == nil) {
|
|
1033
|
+
shadowLayer.opacity = 0;
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (shadowLayer.contents != (__bridge id)tile.image) {
|
|
1037
|
+
shadowLayer.contents = (__bridge id)tile.image;
|
|
1038
|
+
shadowLayer.contentsCenter = tile.contentsCenter;
|
|
1039
|
+
shadowLayer.contentsScale = tile.scale;
|
|
1040
|
+
shadowLayer.contentsGravity = kCAGravityResize;
|
|
1041
|
+
}
|
|
1042
|
+
const CGRect rect =
|
|
1043
|
+
SmoothClipBakedShadowLayerRect(geometry, shadow, tile.margin);
|
|
1044
|
+
shadowLayer.bounds = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
|
1045
|
+
shadowLayer.position =
|
|
1046
|
+
CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
|
|
1047
|
+
shadowLayer.opacity = smoothclip::clamp01(shadow.alpha);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
608
1050
|
- (void)writeShadowModel:(const smoothclip::Shadow &)shadow
|
|
609
1051
|
geometry:(SmoothClipCanonicalGeometry)geometry {
|
|
1052
|
+
[self writeShadowModel:shadow
|
|
1053
|
+
geometry:geometry
|
|
1054
|
+
baked:[self usesBakedShadowForGeometry:geometry shadow:shadow]
|
|
1055
|
+
nearestTile:YES];
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
- (void)writeShadowModel:(const smoothclip::Shadow &)shadow
|
|
1059
|
+
geometry:(SmoothClipCanonicalGeometry)geometry
|
|
1060
|
+
baked:(BOOL)baked
|
|
1061
|
+
nearestTile:(BOOL)allowNearest {
|
|
610
1062
|
const smoothclip::Shadow effectiveShadow =
|
|
611
1063
|
SmoothBoxShadowWithEffectiveAlpha(geometry, shadow);
|
|
612
1064
|
const BOOL visible = SmoothBoxShadowVisible(geometry, effectiveShadow);
|
|
613
1065
|
if (_shadowLayer == nil && !visible) return;
|
|
614
1066
|
|
|
615
1067
|
CALayer *shadowLayer = visible ? [self ensureShadowLayer] : _shadowLayer;
|
|
1068
|
+
const BOOL pathInputChanged = !_hasShadowModel ||
|
|
1069
|
+
!SmoothBoxShadowPathInputEqual(
|
|
1070
|
+
_shadowModelGeometry, _shadowModel, geometry, effectiveShadow);
|
|
1071
|
+
_hasShadowModel = YES;
|
|
1072
|
+
_shadowModelGeometry = geometry;
|
|
1073
|
+
_shadowModel = effectiveShadow;
|
|
1074
|
+
if (baked) {
|
|
1075
|
+
[self writeBakedShadowModel:effectiveShadow
|
|
1076
|
+
geometry:geometry
|
|
1077
|
+
visible:visible
|
|
1078
|
+
nearestTile:allowNearest
|
|
1079
|
+
layer:shadowLayer];
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
BOOL leftTile = NO;
|
|
1083
|
+
if (shadowLayer.contents != nil) {
|
|
1084
|
+
// Back from a baked tile: the blur path draws at layer opacity 1 with
|
|
1085
|
+
// the alpha in shadowColor, and needs its path again.
|
|
1086
|
+
shadowLayer.contents = nil;
|
|
1087
|
+
shadowLayer.opacity = 1;
|
|
1088
|
+
leftTile = YES;
|
|
1089
|
+
}
|
|
616
1090
|
shadowLayer.bounds = self.bounds;
|
|
617
1091
|
shadowLayer.position = CGPointMake(
|
|
618
1092
|
CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
|
619
|
-
if (
|
|
620
|
-
_shadowModelGeometry, _shadowModel, geometry, effectiveShadow)) {
|
|
1093
|
+
if (pathInputChanged || leftTile) {
|
|
621
1094
|
CGPathRef path = SmoothClipCreateShadowPath(geometry, effectiveShadow);
|
|
622
1095
|
shadowLayer.shadowPath = path;
|
|
623
1096
|
CGPathRelease(path);
|
|
624
1097
|
}
|
|
625
|
-
_hasShadowModel = YES;
|
|
626
|
-
_shadowModelGeometry = geometry;
|
|
627
|
-
_shadowModel = effectiveShadow;
|
|
628
1098
|
shadowLayer.shadowColor = [self colorForShadow:effectiveShadow];
|
|
629
1099
|
shadowLayer.shadowOpacity = visible ? 1 : 0;
|
|
630
1100
|
shadowLayer.shadowRadius = MAX(0, effectiveShadow.blurRadius) / 2.0;
|
|
@@ -632,6 +1102,178 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
632
1102
|
effectiveShadow.offsetX, effectiveShadow.offsetY);
|
|
633
1103
|
}
|
|
634
1104
|
|
|
1105
|
+
// Animations for a baked shadow run. Bounds, position and opacity
|
|
1106
|
+
// interpolate; the tile (radius step, blur, colour) is categorical, so it steps
|
|
1107
|
+
// at the key times where the run's progress crosses each radius step, with the
|
|
1108
|
+
// target's blur and colour from the first swap on. Steps are one radius step
|
|
1109
|
+
// apart, invisible under any blur the tile was made for.
|
|
1110
|
+
- (NSArray<CAAnimation *> *)bakedShadowAnimationsFromGeometry:
|
|
1111
|
+
(SmoothClipCanonicalGeometry)fromGeometry
|
|
1112
|
+
fromShadow:(smoothclip::Shadow)fromShadow
|
|
1113
|
+
toGeometry:(SmoothClipCanonicalGeometry)toGeometry
|
|
1114
|
+
toShadow:(smoothclip::Shadow)toShadow
|
|
1115
|
+
timing:(CAMediaTimingFunction *)timing
|
|
1116
|
+
spring:(BOOL)spring
|
|
1117
|
+
velocity:(double)velocity
|
|
1118
|
+
duration:(CFTimeInterval)duration {
|
|
1119
|
+
const CGFloat scale = [self bakedShadowScale];
|
|
1120
|
+
const CGFloat fromRadius =
|
|
1121
|
+
[self bakedShadowTileRadiusForGeometry:fromGeometry shadow:fromShadow];
|
|
1122
|
+
const CGFloat toRadius =
|
|
1123
|
+
[self bakedShadowTileRadiusForGeometry:toGeometry shadow:toShadow];
|
|
1124
|
+
// The start is whatever the model shows (its step or a stand-in); the
|
|
1125
|
+
// target was baked by the model write just before this and must be exact.
|
|
1126
|
+
SmoothClipShadowTile *fromTile = SmoothClipNearestCachedShadowTile(
|
|
1127
|
+
fromRadius, fromGeometry.curve, fromShadow, scale);
|
|
1128
|
+
if (fromTile == nil) {
|
|
1129
|
+
fromTile = SmoothClipBakeShadowTile(
|
|
1130
|
+
fromRadius, fromGeometry.curve, fromShadow, scale);
|
|
1131
|
+
}
|
|
1132
|
+
SmoothClipShadowTile *toTile = SmoothClipBakeShadowTile(
|
|
1133
|
+
toRadius, toGeometry.curve, toShadow, scale);
|
|
1134
|
+
const CGFloat fromMargin = fromTile != nil
|
|
1135
|
+
? fromTile.margin : SmoothClipBakedShadowMargin(fromShadow);
|
|
1136
|
+
const CGFloat toMargin = toTile != nil
|
|
1137
|
+
? toTile.margin : SmoothClipBakedShadowMargin(toShadow);
|
|
1138
|
+
const CGRect fromRect =
|
|
1139
|
+
SmoothClipBakedShadowLayerRect(fromGeometry, fromShadow, fromMargin);
|
|
1140
|
+
const CGRect toRect =
|
|
1141
|
+
SmoothClipBakedShadowLayerRect(toGeometry, toShadow, toMargin);
|
|
1142
|
+
NSMutableArray<CAAnimation *> *animations = [NSMutableArray array];
|
|
1143
|
+
const auto add = [&](NSString *keyPath, id from, id to) {
|
|
1144
|
+
[animations addObject:spring
|
|
1145
|
+
? [self springAnimationForKeyPath:keyPath fromValue:from toValue:to
|
|
1146
|
+
velocity:velocity duration:duration]
|
|
1147
|
+
: [self basicAnimationForKeyPath:keyPath fromValue:from toValue:to
|
|
1148
|
+
timingFunction:timing]];
|
|
1149
|
+
};
|
|
1150
|
+
if (!CGSizeEqualToSize(fromRect.size, toRect.size)) {
|
|
1151
|
+
add(@"bounds",
|
|
1152
|
+
[NSValue valueWithCGRect:CGRectMake(
|
|
1153
|
+
0, 0, fromRect.size.width, fromRect.size.height)],
|
|
1154
|
+
[NSValue valueWithCGRect:CGRectMake(
|
|
1155
|
+
0, 0, toRect.size.width, toRect.size.height)]);
|
|
1156
|
+
}
|
|
1157
|
+
const CGPoint fromCenter =
|
|
1158
|
+
CGPointMake(CGRectGetMidX(fromRect), CGRectGetMidY(fromRect));
|
|
1159
|
+
const CGPoint toCenter =
|
|
1160
|
+
CGPointMake(CGRectGetMidX(toRect), CGRectGetMidY(toRect));
|
|
1161
|
+
if (!CGPointEqualToPoint(fromCenter, toCenter)) {
|
|
1162
|
+
add(@"position",
|
|
1163
|
+
[NSValue valueWithCGPoint:fromCenter],
|
|
1164
|
+
[NSValue valueWithCGPoint:toCenter]);
|
|
1165
|
+
}
|
|
1166
|
+
if (fromShadow.alpha != toShadow.alpha) {
|
|
1167
|
+
add(@"opacity", @(smoothclip::clamp01(fromShadow.alpha)),
|
|
1168
|
+
@(smoothclip::clamp01(toShadow.alpha)));
|
|
1169
|
+
}
|
|
1170
|
+
if (fromTile == nil || toTile == nil || fromTile == toTile) {
|
|
1171
|
+
return animations;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
NSMutableArray *images = [NSMutableArray arrayWithObject:(__bridge id)fromTile.image];
|
|
1175
|
+
NSMutableArray<NSValue *> *centers =
|
|
1176
|
+
[NSMutableArray arrayWithObject:[NSValue valueWithCGRect:fromTile.contentsCenter]];
|
|
1177
|
+
NSMutableArray<NSNumber *> *keyTimes = [NSMutableArray arrayWithObject:@0];
|
|
1178
|
+
const CGFloat rawFrom = SmoothClipAdjustedShadowRadius(
|
|
1179
|
+
fromGeometry.radii.topLeft, fromShadow.spreadDistance);
|
|
1180
|
+
const CGFloat rawTo = SmoothClipAdjustedShadowRadius(
|
|
1181
|
+
toGeometry.radii.topLeft, toShadow.spreadDistance);
|
|
1182
|
+
// Normalised time at which the run's progress first reaches `target`.
|
|
1183
|
+
const auto timeForProgress = [&](double target) -> double {
|
|
1184
|
+
if (target <= 0) return 0;
|
|
1185
|
+
if (target >= 1) return 1;
|
|
1186
|
+
constexpr double step = 1.0 / 240.0;
|
|
1187
|
+
if (spring) {
|
|
1188
|
+
smoothclip::ScalarSpringState state{0, velocity};
|
|
1189
|
+
double elapsed = 0;
|
|
1190
|
+
while (elapsed < duration) {
|
|
1191
|
+
state = smoothclip::advanceScalarSpring(state, _springAnimation, step);
|
|
1192
|
+
elapsed += step;
|
|
1193
|
+
if (state.position >= target) break;
|
|
1194
|
+
}
|
|
1195
|
+
return duration > 0 ? std::min(1.0, elapsed / duration) : 1.0;
|
|
1196
|
+
}
|
|
1197
|
+
double low = 0, high = 1;
|
|
1198
|
+
for (int iteration = 0; iteration < 20; iteration += 1) {
|
|
1199
|
+
const double middle = (low + high) / 2;
|
|
1200
|
+
const double progress = smoothclip::cubicBezier(
|
|
1201
|
+
_timingAnimation.controlPoint1X, _timingAnimation.controlPoint1Y,
|
|
1202
|
+
_timingAnimation.controlPoint2X, _timingAnimation.controlPoint2Y,
|
|
1203
|
+
middle);
|
|
1204
|
+
if (progress < target) low = middle; else high = middle;
|
|
1205
|
+
}
|
|
1206
|
+
return (low + high) / 2;
|
|
1207
|
+
};
|
|
1208
|
+
if (fromTile.radius == toTile.radius || rawTo == rawFrom) {
|
|
1209
|
+
// Same radius step, a different blur or colour: one swap, mid-run.
|
|
1210
|
+
[images addObject:(__bridge id)toTile.image];
|
|
1211
|
+
[centers addObject:[NSValue valueWithCGRect:toTile.contentsCenter]];
|
|
1212
|
+
[keyTimes addObject:@0.5];
|
|
1213
|
+
} else {
|
|
1214
|
+
const BOOL growing = toTile.radius > fromTile.radius;
|
|
1215
|
+
const CGFloat step = kSmoothClipShadowTileRadiusStep * (growing ? 1 : -1);
|
|
1216
|
+
// Key time a missing step hands to the next cached tile on a growing run.
|
|
1217
|
+
double carriedTime = -1;
|
|
1218
|
+
for (CGFloat tileRadius = fromTile.radius + step;
|
|
1219
|
+
growing ? tileRadius <= toTile.radius : tileRadius >= toTile.radius;
|
|
1220
|
+
tileRadius += step) {
|
|
1221
|
+
// Rounding up: a growing radius takes tile T once it passes T - step,
|
|
1222
|
+
// a shrinking one once it reaches T.
|
|
1223
|
+
const CGFloat threshold = growing
|
|
1224
|
+
? tileRadius - kSmoothClipShadowTileRadiusStep : tileRadius;
|
|
1225
|
+
const double progress = (threshold - rawFrom) / (rawTo - rawFrom);
|
|
1226
|
+
SmoothClipShadowTile *tile = SmoothClipCachedShadowTile(
|
|
1227
|
+
tileRadius, toGeometry.curve, toShadow, scale);
|
|
1228
|
+
if (tile == nil) {
|
|
1229
|
+
// Not baked inside this transaction: it lands for the next run. A
|
|
1230
|
+
// growing run swaps to the next cached (rounder) tile at this step's
|
|
1231
|
+
// time instead, so the shown corner is never tighter than the clip's;
|
|
1232
|
+
// a shrinking run simply keeps the rounder tile a step longer.
|
|
1233
|
+
SmoothClipRequestShadowTile(
|
|
1234
|
+
tileRadius, toGeometry.curve, toShadow, scale, nil);
|
|
1235
|
+
if (growing && carriedTime < 0) carriedTime = timeForProgress(progress);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const double ownTime = timeForProgress(progress);
|
|
1239
|
+
const double time = std::max(
|
|
1240
|
+
keyTimes.lastObject.doubleValue,
|
|
1241
|
+
carriedTime >= 0 ? std::min(carriedTime, ownTime) : ownTime);
|
|
1242
|
+
carriedTime = -1;
|
|
1243
|
+
if (time <= keyTimes.lastObject.doubleValue + 1e-6) {
|
|
1244
|
+
// Same instant as the previous swap (leaving a step value at t = 0):
|
|
1245
|
+
// the later tile is the one shown; key times must strictly increase.
|
|
1246
|
+
images[images.count - 1] = (__bridge id)tile.image;
|
|
1247
|
+
centers[centers.count - 1] = [NSValue valueWithCGRect:tile.contentsCenter];
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
[images addObject:(__bridge id)tile.image];
|
|
1251
|
+
[centers addObject:[NSValue valueWithCGRect:tile.contentsCenter]];
|
|
1252
|
+
[keyTimes addObject:@(time)];
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (images.count < 2) return animations;
|
|
1256
|
+
// Discrete key times bracket their values: one more entry than values, the
|
|
1257
|
+
// last at 1 (CAKeyframeAnimation.keyTimes). Without the closing entry Core
|
|
1258
|
+
// Animation ignores the array and spaces the swaps evenly over the run.
|
|
1259
|
+
[keyTimes addObject:@1];
|
|
1260
|
+
CAKeyframeAnimation *contents =
|
|
1261
|
+
[CAKeyframeAnimation animationWithKeyPath:@"contents"];
|
|
1262
|
+
contents.values = images;
|
|
1263
|
+
contents.keyTimes = keyTimes;
|
|
1264
|
+
contents.calculationMode = kCAAnimationDiscrete;
|
|
1265
|
+
contents.duration = duration;
|
|
1266
|
+
[animations addObject:contents];
|
|
1267
|
+
CAKeyframeAnimation *contentsCenter =
|
|
1268
|
+
[CAKeyframeAnimation animationWithKeyPath:@"contentsCenter"];
|
|
1269
|
+
contentsCenter.values = centers;
|
|
1270
|
+
contentsCenter.keyTimes = keyTimes;
|
|
1271
|
+
contentsCenter.calculationMode = kCAAnimationDiscrete;
|
|
1272
|
+
contentsCenter.duration = duration;
|
|
1273
|
+
[animations addObject:contentsCenter];
|
|
1274
|
+
return animations;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
635
1277
|
- (void)writeShadow:(const smoothclip::Shadow &)shadow
|
|
636
1278
|
geometry:(SmoothClipCanonicalGeometry)geometry {
|
|
637
1279
|
_canonicalShadow = shadow;
|
|
@@ -964,6 +1606,15 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
964
1606
|
if (shadowLayer == nil) return SmoothClipCanonicalSnapshot({
|
|
965
1607
|
geometry, transform.tx, transform.ty, contentScale, shadow,
|
|
966
1608
|
[self visibleRotation], [self visibleOpacity]});
|
|
1609
|
+
if (shadowLayer.contents != nil) {
|
|
1610
|
+
// A baked tile: colour, blur, offset and spread never move on the layer
|
|
1611
|
+
// (a run swaps tiles); the layer's opacity carries the alpha.
|
|
1612
|
+
shadow.enabled = _canonicalShadow.enabled || shadowLayer.opacity > 0;
|
|
1613
|
+
shadow.alpha = shadowLayer.opacity;
|
|
1614
|
+
return SmoothClipCanonicalSnapshot({
|
|
1615
|
+
geometry, transform.tx, transform.ty, contentScale, shadow,
|
|
1616
|
+
[self visibleRotation], [self visibleOpacity]});
|
|
1617
|
+
}
|
|
967
1618
|
shadow.enabled = _canonicalShadow.enabled || shadowLayer.shadowOpacity > 0;
|
|
968
1619
|
shadow.blurRadius = shadowLayer.shadowRadius * 2.0;
|
|
969
1620
|
shadow.offsetX = shadowLayer.shadowOffset.width;
|
|
@@ -1087,16 +1738,7 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1087
1738
|
// Every key path shares one normalized trajectory. Derive the terminal
|
|
1088
1739
|
// duration from the same relative-energy rule as Android/Reanimated instead
|
|
1089
1740
|
// of asking each Core Animation property for an independent settle time.
|
|
1090
|
-
smoothclip::
|
|
1091
|
-
constexpr double step = 1.0 / 120.0;
|
|
1092
|
-
double elapsed = 0;
|
|
1093
|
-
while (elapsed < 10.0 &&
|
|
1094
|
-
smoothclip::relativeSpringEnergy(state, _springAnimation) >
|
|
1095
|
-
_springAnimation.energyThreshold) {
|
|
1096
|
-
state = smoothclip::advanceScalarSpring(state, _springAnimation, step);
|
|
1097
|
-
elapsed += step;
|
|
1098
|
-
}
|
|
1099
|
-
return elapsed;
|
|
1741
|
+
return smoothclip::springSettleDuration(_springAnimation, velocity);
|
|
1100
1742
|
}
|
|
1101
1743
|
|
|
1102
1744
|
- (CASpringAnimation *)springAnimationForKeyPath:(NSString *)keyPath
|
|
@@ -1174,11 +1816,22 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1174
1816
|
_canonicalShadow = toShadow;
|
|
1175
1817
|
SmoothBoxShadowNormalizeAnimationEndpoints(
|
|
1176
1818
|
fromGeometry, &fromShadow, toGeometry, &toShadow);
|
|
1819
|
+
// A baked run animates the tile layer's frame and opacity and swaps tiles
|
|
1820
|
+
// in steps (bakedShadowAnimationsFromGeometry:...). One non-uniform
|
|
1821
|
+
// endpoint has no tile, so such a run takes the blur path throughout.
|
|
1822
|
+
const BOOL bakedShadow =
|
|
1823
|
+
(SmoothBoxShadowVisible(fromGeometry, fromShadow) ||
|
|
1824
|
+
SmoothBoxShadowVisible(toGeometry, toShadow)) &&
|
|
1825
|
+
[self usesBakedShadowForGeometry:fromGeometry shadow:fromShadow] &&
|
|
1826
|
+
[self usesBakedShadowForGeometry:toGeometry shadow:toShadow];
|
|
1177
1827
|
// The target model layer is complete before any presentation animations are
|
|
1178
1828
|
// attached. Core Animation may remove the terminal presentation animation
|
|
1179
1829
|
// before its delegate runs, so correctness cannot depend on completion-time
|
|
1180
1830
|
// cleanup hiding a stale path or opaque color.
|
|
1181
|
-
[self writeShadowModel:toShadow
|
|
1831
|
+
[self writeShadowModel:toShadow
|
|
1832
|
+
geometry:toGeometry
|
|
1833
|
+
baked:bakedShadow
|
|
1834
|
+
nearestTile:NO];
|
|
1182
1835
|
if (usesMask) {
|
|
1183
1836
|
// Keep one mask representation for the full interval, including an
|
|
1184
1837
|
// unequal→uniform transition. Switching layer.mask mid-animation would
|
|
@@ -1217,12 +1870,13 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1217
1870
|
[self ensureShadowLayer];
|
|
1218
1871
|
// Reveal opacity is encoded in shadowColor alpha. Animating layer opacity
|
|
1219
1872
|
// too would multiply both curves and produce a quadratic-looking fade.
|
|
1220
|
-
|
|
1873
|
+
// (A baked tile has no shadowColor; its alpha is the layer opacity.)
|
|
1874
|
+
if (!bakedShadow) _shadowLayer.shadowOpacity = 1;
|
|
1221
1875
|
}
|
|
1222
|
-
CGPathRef fromShadowPath = animatesShadowPath
|
|
1876
|
+
CGPathRef fromShadowPath = animatesShadowPath && !bakedShadow
|
|
1223
1877
|
? SmoothClipCreateShadowPath(fromGeometry, fromShadow)
|
|
1224
1878
|
: nil;
|
|
1225
|
-
CGPathRef toShadowPath = animatesShadowPath
|
|
1879
|
+
CGPathRef toShadowPath = animatesShadowPath && !bakedShadow
|
|
1226
1880
|
? SmoothClipCreateShadowPath(toGeometry, toShadow)
|
|
1227
1881
|
: nil;
|
|
1228
1882
|
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
@@ -1292,7 +1946,18 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1292
1946
|
}
|
|
1293
1947
|
contentGroup.animations = contentAnimations;
|
|
1294
1948
|
contentGroup.duration = duration;
|
|
1295
|
-
if (animatesShadow) {
|
|
1949
|
+
if (animatesShadow && bakedShadow) {
|
|
1950
|
+
shadowGroup.animations = [self
|
|
1951
|
+
bakedShadowAnimationsFromGeometry:fromGeometry
|
|
1952
|
+
fromShadow:fromShadow
|
|
1953
|
+
toGeometry:toGeometry
|
|
1954
|
+
toShadow:toShadow
|
|
1955
|
+
timing:timing
|
|
1956
|
+
spring:NO
|
|
1957
|
+
velocity:0
|
|
1958
|
+
duration:duration];
|
|
1959
|
+
shadowGroup.duration = duration;
|
|
1960
|
+
} else if (animatesShadow) {
|
|
1296
1961
|
NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
|
|
1297
1962
|
if (animatesShadowPath) [shadowAnimations addObject:
|
|
1298
1963
|
[self basicAnimationForKeyPath:@"shadowPath"
|
|
@@ -1435,7 +2100,18 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1435
2100
|
}
|
|
1436
2101
|
contentGroup.animations = contentAnimations;
|
|
1437
2102
|
contentGroup.duration = springDuration;
|
|
1438
|
-
if (animatesShadow) {
|
|
2103
|
+
if (animatesShadow && bakedShadow) {
|
|
2104
|
+
shadowGroup.animations = [self
|
|
2105
|
+
bakedShadowAnimationsFromGeometry:fromGeometry
|
|
2106
|
+
fromShadow:fromShadow
|
|
2107
|
+
toGeometry:toGeometry
|
|
2108
|
+
toShadow:toShadow
|
|
2109
|
+
timing:nil
|
|
2110
|
+
spring:YES
|
|
2111
|
+
velocity:velocity
|
|
2112
|
+
duration:springDuration];
|
|
2113
|
+
shadowGroup.duration = springDuration;
|
|
2114
|
+
} else if (animatesShadow) {
|
|
1439
2115
|
NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
|
|
1440
2116
|
if (animatesShadowPath) [shadowAnimations addObject:
|
|
1441
2117
|
[self springAnimationForKeyPath:@"shadowPath"
|
|
@@ -1527,6 +2203,14 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1527
2203
|
convertTime:sharedBeginTime
|
|
1528
2204
|
fromLayer:nil];
|
|
1529
2205
|
}
|
|
2206
|
+
// A stamped epoch may sit up to one frame ahead of this commit. Until it
|
|
2207
|
+
// arrives the run presents its first frame rather than the model values
|
|
2208
|
+
// the layers already hold, which are the target.
|
|
2209
|
+
appearanceGroup.fillMode = kCAFillModeBackwards;
|
|
2210
|
+
group.fillMode = kCAFillModeBackwards;
|
|
2211
|
+
contentGroup.fillMode = kCAFillModeBackwards;
|
|
2212
|
+
if (animatesShadow) shadowGroup.fillMode = kCAFillModeBackwards;
|
|
2213
|
+
if (maskAnimation != nil) maskAnimation.fillMode = kCAFillModeBackwards;
|
|
1530
2214
|
}
|
|
1531
2215
|
|
|
1532
2216
|
_animationDelegate = [SmoothClipAnimationDelegate new];
|
|
@@ -1859,6 +2543,17 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1859
2543
|
[super updateProps:props oldProps:oldProps];
|
|
1860
2544
|
self.clipsToBounds = YES;
|
|
1861
2545
|
|
|
2546
|
+
const BOOL bakedShadows = newProps.shadowRendering == 1;
|
|
2547
|
+
if (bakedShadows != _bakedShadows) {
|
|
2548
|
+
_bakedShadows = bakedShadows;
|
|
2549
|
+
// Re-render the current shadow in the new mode. A run in flight keeps its
|
|
2550
|
+
// renderer to the end; its completion writes the model again.
|
|
2551
|
+
if (_activeAnimationId == 0 && _hasShadowModel) {
|
|
2552
|
+
[self writeShadow:_requestedShadow
|
|
2553
|
+
geometry:[self canonicalGeometryValue]];
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
1862
2557
|
const uint64_t nextDriverId =
|
|
1863
2558
|
isfinite(newProps.driverId) && newProps.driverId > 0
|
|
1864
2559
|
? static_cast<uint64_t>(newProps.driverId)
|
|
@@ -1888,6 +2583,8 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1888
2583
|
newProps.initialClipBoxShadowSpreadDistance,
|
|
1889
2584
|
newProps.initialRotation,
|
|
1890
2585
|
newProps.initialOpacity,
|
|
2586
|
+
newProps.initialBackdropTranslateX,
|
|
2587
|
+
newProps.initialBackdropTranslateY,
|
|
1891
2588
|
&initial)) {
|
|
1892
2589
|
// The initial presentation is atomic. Never apply a valid subset when one
|
|
1893
2590
|
// field rejects.
|
|
@@ -2013,6 +2710,10 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
2013
2710
|
shadowSpreadDistance,
|
|
2014
2711
|
rotation,
|
|
2015
2712
|
opacity,
|
|
2713
|
+
// The command protocol carries no backdrop channel; a command-driven
|
|
2714
|
+
// host keeps its backdrops where they are.
|
|
2715
|
+
0,
|
|
2716
|
+
0,
|
|
2016
2717
|
&presentation)) {
|
|
2017
2718
|
return;
|
|
2018
2719
|
}
|