react-native-smooth-clip-view 0.4.5 → 0.5.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/CHANGELOG.md +66 -0
- package/README.md +52 -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 +209 -10
- 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/SmoothClipGeometry.h +8 -7
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +713 -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,314 @@ 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. Rendered through Core Animation's own shadow path so the tile is
|
|
449
|
+
// the pure blurred shape the compositor draws for a `shadowPath` layer, with
|
|
450
|
+
// nothing filled in on top of it; colour alpha is applied as layer opacity.
|
|
451
|
+
static NSCache<NSString *, SmoothClipShadowTile *> *SmoothClipShadowTileCache(void) {
|
|
452
|
+
static NSCache<NSString *, SmoothClipShadowTile *> *cache;
|
|
453
|
+
static dispatch_once_t once;
|
|
454
|
+
dispatch_once(&once, ^{
|
|
455
|
+
cache = [NSCache new];
|
|
456
|
+
cache.countLimit = 24;
|
|
457
|
+
});
|
|
458
|
+
return cache;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
static NSString *SmoothClipShadowTileKey(
|
|
462
|
+
CGFloat tileRadius,
|
|
463
|
+
SmoothClipCornerCurve curve,
|
|
464
|
+
const smoothclip::Shadow &shadow,
|
|
465
|
+
CGFloat scale) {
|
|
466
|
+
return [NSString stringWithFormat:@"%g|%d|%g|%g|%g|%g|%g",
|
|
467
|
+
tileRadius, (int)curve, MAX(0, shadow.blurRadius),
|
|
468
|
+
smoothclip::clamp01(shadow.red), smoothclip::clamp01(shadow.green),
|
|
469
|
+
smoothclip::clamp01(shadow.blue), scale];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The cached tile for this radius step, or nil. Never bakes.
|
|
473
|
+
static SmoothClipShadowTile *SmoothClipCachedShadowTile(
|
|
474
|
+
CGFloat tileRadius,
|
|
475
|
+
SmoothClipCornerCurve curve,
|
|
476
|
+
const smoothclip::Shadow &shadow,
|
|
477
|
+
CGFloat scale) {
|
|
478
|
+
return [SmoothClipShadowTileCache()
|
|
479
|
+
objectForKey:SmoothClipShadowTileKey(tileRadius, curve, shadow, scale)];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Radius steps searched on either side of a missing tile for one to show
|
|
483
|
+
// while it bakes.
|
|
484
|
+
static const int kSmoothClipShadowTileSearchSteps = 16;
|
|
485
|
+
|
|
486
|
+
// The cached tile nearest to `tileRadius`: the exact one, else at each
|
|
487
|
+
// distance the rounder neighbour before the tighter one (a tile corner
|
|
488
|
+
// tighter than the clip corner over it reads as a dark sliver; a rounder one
|
|
489
|
+
// only recedes under the clip). Never bakes.
|
|
490
|
+
static SmoothClipShadowTile *SmoothClipNearestCachedShadowTile(
|
|
491
|
+
CGFloat tileRadius,
|
|
492
|
+
SmoothClipCornerCurve curve,
|
|
493
|
+
const smoothclip::Shadow &shadow,
|
|
494
|
+
CGFloat scale) {
|
|
495
|
+
for (int distance = 0; distance <= kSmoothClipShadowTileSearchSteps;
|
|
496
|
+
distance += 1) {
|
|
497
|
+
const CGFloat delta = distance * kSmoothClipShadowTileRadiusStep;
|
|
498
|
+
SmoothClipShadowTile *tile =
|
|
499
|
+
SmoothClipCachedShadowTile(tileRadius + delta, curve, shadow, scale);
|
|
500
|
+
if (tile != nil) return tile;
|
|
501
|
+
if (distance == 0 || tileRadius - delta < 0) continue;
|
|
502
|
+
tile = SmoothClipCachedShadowTile(tileRadius - delta, curve, shadow, scale);
|
|
503
|
+
if (tile != nil) return tile;
|
|
504
|
+
}
|
|
505
|
+
return nil;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Bakes on the calling thread and caches the result: about a millisecond for
|
|
509
|
+
// a 16 pt blur at 3x, growing with blur² × scale². The callers reach for it
|
|
510
|
+
// only when nothing else can be shown (a cold cache, at mount) and for a
|
|
511
|
+
// run's resting target, which must be exact from the run's last frame on;
|
|
512
|
+
// every other tile bakes through SmoothClipRequestShadowTile, off the frame.
|
|
513
|
+
static NSUInteger gSmoothClipShadowTileBakeCount = 0;
|
|
514
|
+
static SmoothClipShadowTile *SmoothClipBakeShadowTile(
|
|
515
|
+
CGFloat tileRadius,
|
|
516
|
+
SmoothClipCornerCurve curve,
|
|
517
|
+
const smoothclip::Shadow &shadow,
|
|
518
|
+
CGFloat scale) {
|
|
519
|
+
NSString *key = SmoothClipShadowTileKey(tileRadius, curve, shadow, scale);
|
|
520
|
+
SmoothClipShadowTile *tile = [SmoothClipShadowTileCache() objectForKey:key];
|
|
521
|
+
if (tile != nil) return tile;
|
|
522
|
+
gSmoothClipShadowTileBakeCount += 1;
|
|
523
|
+
const CGFloat margin = SmoothClipBakedShadowMargin(shadow);
|
|
524
|
+
|
|
525
|
+
const CGFloat side = 2 * (2 * margin + tileRadius) + kSmoothClipShadowTileBand;
|
|
526
|
+
const size_t pixels = (size_t)MAX(1, ceil(side * scale));
|
|
527
|
+
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
|
|
528
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
529
|
+
NULL, pixels, pixels, 8, 0, space,
|
|
530
|
+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
|
531
|
+
CGColorSpaceRelease(space);
|
|
532
|
+
if (context == NULL) return nil;
|
|
533
|
+
CGContextScaleCTM(context, scale, scale);
|
|
534
|
+
CALayer *layer = [CALayer layer];
|
|
535
|
+
layer.frame = CGRectMake(0, 0, side, side);
|
|
536
|
+
const CGRect shape = CGRectInset(layer.bounds, margin, margin);
|
|
537
|
+
const SmoothClipCornerRadii radii = {
|
|
538
|
+
tileRadius, tileRadius, tileRadius, tileRadius};
|
|
539
|
+
CGPathRef path = SmoothClipCreateRoundedRectPath(shape, radii, curve);
|
|
540
|
+
layer.shadowPath = path;
|
|
541
|
+
CGPathRelease(path);
|
|
542
|
+
layer.shadowColor = [UIColor colorWithRed:smoothclip::clamp01(shadow.red)
|
|
543
|
+
green:smoothclip::clamp01(shadow.green)
|
|
544
|
+
blue:smoothclip::clamp01(shadow.blue)
|
|
545
|
+
alpha:1].CGColor;
|
|
546
|
+
layer.shadowOpacity = 1;
|
|
547
|
+
layer.shadowOffset = CGSizeZero;
|
|
548
|
+
layer.shadowRadius = MAX(0, shadow.blurRadius) / 2.0;
|
|
549
|
+
// The bitmap's y axis is flipped relative to the layer; the tile is
|
|
550
|
+
// symmetric (uniform radii, no offset), so that is harmless.
|
|
551
|
+
[layer renderInContext:context];
|
|
552
|
+
CGImageRef image = CGBitmapContextCreateImage(context);
|
|
553
|
+
CGContextRelease(context);
|
|
554
|
+
if (image == NULL) return nil;
|
|
555
|
+
tile = [[SmoothClipShadowTile alloc] initWithImage:image
|
|
556
|
+
radius:tileRadius
|
|
557
|
+
margin:margin
|
|
558
|
+
side:side
|
|
559
|
+
scale:scale];
|
|
560
|
+
CGImageRelease(image);
|
|
561
|
+
[SmoothClipShadowTileCache() setObject:tile forKey:key];
|
|
562
|
+
return tile;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
typedef void (^SmoothClipShadowTileCompletion)(SmoothClipShadowTile *tile);
|
|
566
|
+
|
|
567
|
+
@interface SmoothClipShadowTileRequest : NSObject
|
|
568
|
+
@property(nonatomic) CGFloat tileRadius;
|
|
569
|
+
@property(nonatomic) SmoothClipCornerCurve curve;
|
|
570
|
+
@property(nonatomic) smoothclip::Shadow shadow;
|
|
571
|
+
@property(nonatomic) CGFloat scale;
|
|
572
|
+
@property(nonatomic, readonly)
|
|
573
|
+
NSMutableArray<SmoothClipShadowTileCompletion> *completions;
|
|
574
|
+
@end
|
|
575
|
+
|
|
576
|
+
@implementation SmoothClipShadowTileRequest
|
|
577
|
+
- (instancetype)init {
|
|
578
|
+
if (self = [super init]) {
|
|
579
|
+
_completions = [NSMutableArray array];
|
|
580
|
+
}
|
|
581
|
+
return self;
|
|
582
|
+
}
|
|
583
|
+
@end
|
|
584
|
+
|
|
585
|
+
// Tiles requested off the frame's critical path, in request order, one per
|
|
586
|
+
// main-queue drain: a block enqueued from inside a drain runs in the next
|
|
587
|
+
// one, so a burst of missing steps costs one bake per run-loop turn rather
|
|
588
|
+
// than all of them inside the transaction that asked for them.
|
|
589
|
+
static NSMutableArray<SmoothClipShadowTileRequest *> *SmoothClipPendingShadowTiles(void) {
|
|
590
|
+
static NSMutableArray<SmoothClipShadowTileRequest *> *pending;
|
|
591
|
+
static dispatch_once_t once;
|
|
592
|
+
dispatch_once(&once, ^{
|
|
593
|
+
pending = [NSMutableArray array];
|
|
594
|
+
});
|
|
595
|
+
return pending;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
static NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *
|
|
599
|
+
SmoothClipPendingShadowTilesByKey(void) {
|
|
600
|
+
static NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *pending;
|
|
601
|
+
static dispatch_once_t once;
|
|
602
|
+
dispatch_once(&once, ^{
|
|
603
|
+
pending = [NSMutableDictionary dictionary];
|
|
604
|
+
});
|
|
605
|
+
return pending;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
static BOOL gSmoothClipShadowTileBakeScheduled = NO;
|
|
609
|
+
static void SmoothClipScheduleShadowTileBake(void);
|
|
610
|
+
|
|
611
|
+
static void SmoothClipBakeNextShadowTile(void) {
|
|
612
|
+
gSmoothClipShadowTileBakeScheduled = NO;
|
|
613
|
+
NSMutableArray<SmoothClipShadowTileRequest *> *pending =
|
|
614
|
+
SmoothClipPendingShadowTiles();
|
|
615
|
+
SmoothClipShadowTileRequest *request = pending.firstObject;
|
|
616
|
+
if (request == nil) return;
|
|
617
|
+
[pending removeObjectAtIndex:0];
|
|
618
|
+
[SmoothClipPendingShadowTilesByKey() removeObjectForKey:SmoothClipShadowTileKey(
|
|
619
|
+
request.tileRadius, request.curve, request.shadow, request.scale)];
|
|
620
|
+
SmoothClipShadowTile *tile = SmoothClipBakeShadowTile(
|
|
621
|
+
request.tileRadius, request.curve, request.shadow, request.scale);
|
|
622
|
+
for (SmoothClipShadowTileCompletion completion in request.completions) {
|
|
623
|
+
completion(tile);
|
|
624
|
+
}
|
|
625
|
+
if (pending.count > 0) SmoothClipScheduleShadowTileBake();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
static void SmoothClipScheduleShadowTileBake(void) {
|
|
629
|
+
if (gSmoothClipShadowTileBakeScheduled) return;
|
|
630
|
+
gSmoothClipShadowTileBakeScheduled = YES;
|
|
631
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
632
|
+
SmoothClipBakeNextShadowTile();
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Asks for the tile without baking it now. `completion` runs on the main
|
|
637
|
+
// thread once the tile is cached, at once when it already is.
|
|
638
|
+
static void SmoothClipRequestShadowTile(
|
|
639
|
+
CGFloat tileRadius,
|
|
640
|
+
SmoothClipCornerCurve curve,
|
|
641
|
+
const smoothclip::Shadow &shadow,
|
|
642
|
+
CGFloat scale,
|
|
643
|
+
SmoothClipShadowTileCompletion completion) {
|
|
644
|
+
SmoothClipShadowTile *cached =
|
|
645
|
+
SmoothClipCachedShadowTile(tileRadius, curve, shadow, scale);
|
|
646
|
+
if (cached != nil) {
|
|
647
|
+
if (completion != nil) completion(cached);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
NSString *key = SmoothClipShadowTileKey(tileRadius, curve, shadow, scale);
|
|
651
|
+
NSMutableDictionary<NSString *, SmoothClipShadowTileRequest *> *byKey =
|
|
652
|
+
SmoothClipPendingShadowTilesByKey();
|
|
653
|
+
SmoothClipShadowTileRequest *request = byKey[key];
|
|
654
|
+
if (request == nil) {
|
|
655
|
+
request = [SmoothClipShadowTileRequest new];
|
|
656
|
+
request.tileRadius = tileRadius;
|
|
657
|
+
request.curve = curve;
|
|
658
|
+
request.shadow = shadow;
|
|
659
|
+
request.scale = scale;
|
|
660
|
+
byKey[key] = request;
|
|
661
|
+
[SmoothClipPendingShadowTiles() addObject:request];
|
|
662
|
+
}
|
|
663
|
+
if (completion != nil) [request.completions addObject:[completion copy]];
|
|
664
|
+
SmoothClipScheduleShadowTileBake();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
NSUInteger SmoothClipShadowTileBakeCountForTesting(void) {
|
|
668
|
+
return gSmoothClipShadowTileBakeCount;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
NSUInteger SmoothClipPendingShadowTileCountForTesting(void) {
|
|
672
|
+
return SmoothClipPendingShadowTiles().count;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
void SmoothClipBakePendingShadowTilesForTesting(void) {
|
|
676
|
+
while (SmoothClipPendingShadowTiles().count > 0) {
|
|
677
|
+
SmoothClipBakeNextShadowTile();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// The shadow layer's frame for a baked tile: the shape rect (aperture grown by
|
|
682
|
+
// the spread, then offset) plus the tile margin on every side.
|
|
683
|
+
static CGRect SmoothClipBakedShadowLayerRect(
|
|
684
|
+
SmoothClipCanonicalGeometry geometry,
|
|
685
|
+
const smoothclip::Shadow &shadow,
|
|
686
|
+
CGFloat margin) {
|
|
687
|
+
const CGFloat spread = shadow.spreadDistance;
|
|
688
|
+
CGRect rect = CGRectInset(geometry.rect, -spread, -spread);
|
|
689
|
+
if (CGRectGetWidth(rect) <= 0 || CGRectGetHeight(rect) <= 0) {
|
|
690
|
+
return CGRectZero;
|
|
691
|
+
}
|
|
692
|
+
rect = CGRectOffset(rect, shadow.offsetX, shadow.offsetY);
|
|
693
|
+
return CGRectInset(rect, -margin, -margin);
|
|
694
|
+
}
|
|
695
|
+
|
|
384
696
|
@implementation SmoothClipView {
|
|
385
697
|
CALayer *_shadowLayer;
|
|
386
698
|
SmoothClipPresentationContainerView *_presentationContainer;
|
|
@@ -395,6 +707,9 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
395
707
|
BOOL _hasShadowModel;
|
|
396
708
|
SmoothClipCanonicalGeometry _shadowModelGeometry;
|
|
397
709
|
smoothclip::Shadow _shadowModel;
|
|
710
|
+
// `shadowRendering="baked"`: stretch one pre-blurred tile instead of
|
|
711
|
+
// blurring a path every frame. See SmoothClipBakedShadowTile.
|
|
712
|
+
BOOL _bakedShadows;
|
|
398
713
|
|
|
399
714
|
SmoothClipContainerView *_clipContainer;
|
|
400
715
|
SmoothClipContainerView *_contentContainer;
|
|
@@ -485,6 +800,7 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
485
800
|
|
|
486
801
|
_shadowLayer = nil;
|
|
487
802
|
// Created lazily so the common clipping-only path owns no shadow layer.
|
|
803
|
+
_bakedShadows = NO;
|
|
488
804
|
|
|
489
805
|
_unequalCornerMask = [CAShapeLayer layer];
|
|
490
806
|
_unequalCornerMask.actions = @{
|
|
@@ -600,31 +916,175 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
600
916
|
@"shadowOffset": [NSNull null],
|
|
601
917
|
@"bounds": [NSNull null],
|
|
602
918
|
@"position": [NSNull null],
|
|
919
|
+
@"opacity": [NSNull null],
|
|
920
|
+
@"contents": [NSNull null],
|
|
921
|
+
@"contentsCenter": [NSNull null],
|
|
922
|
+
@"contentsScale": [NSNull null],
|
|
603
923
|
};
|
|
604
924
|
[_presentationContainer.layer insertSublayer:_shadowLayer below:_clipContainer.layer];
|
|
605
925
|
return _shadowLayer;
|
|
606
926
|
}
|
|
607
927
|
|
|
928
|
+
- (CGFloat)bakedShadowScale {
|
|
929
|
+
const CGFloat scale = self.traitCollection.displayScale;
|
|
930
|
+
return scale > 0 ? scale : UIScreen.mainScreen.scale;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Radius step of the tile a geometry's shadow draws from.
|
|
934
|
+
- (CGFloat)bakedShadowTileRadiusForGeometry:(SmoothClipCanonicalGeometry)geometry
|
|
935
|
+
shadow:(const smoothclip::Shadow &)shadow {
|
|
936
|
+
return SmoothClipBakedShadowTileRadius(SmoothClipAdjustedShadowRadius(
|
|
937
|
+
geometry.radii.topLeft, shadow.spreadDistance));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
- (BOOL)usesBakedShadowForGeometry:(SmoothClipCanonicalGeometry)geometry
|
|
941
|
+
shadow:(const smoothclip::Shadow &)shadow {
|
|
942
|
+
// Unequal radii have no single tile; they keep the blur path. So does a
|
|
943
|
+
// shape too small for two corner slices on a side, 2 x (margin + radius):
|
|
944
|
+
// the compositor would squash the slices rather than overlap the blur
|
|
945
|
+
// tails the way the true shadow does.
|
|
946
|
+
if (!_bakedShadows || !SmoothClipCornerRadiiAreUniform(geometry.radii)) {
|
|
947
|
+
return NO;
|
|
948
|
+
}
|
|
949
|
+
const CGFloat minimumSide = 2 *
|
|
950
|
+
(SmoothClipBakedShadowMargin(shadow) +
|
|
951
|
+
[self bakedShadowTileRadiusForGeometry:geometry shadow:shadow]);
|
|
952
|
+
const CGFloat spread = 2 * shadow.spreadDistance;
|
|
953
|
+
return CGRectGetWidth(geometry.rect) + spread >= minimumSide &&
|
|
954
|
+
CGRectGetHeight(geometry.rect) + spread >= minimumSide;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// A requested tile is cached: draw it, if the model still wants it and
|
|
958
|
+
// nothing else owns the layer (a run in flight swaps tiles from its own
|
|
959
|
+
// keyframes, and its completion writes the model again).
|
|
960
|
+
- (void)shadowTileDidLand:(SmoothClipShadowTile *)tile {
|
|
961
|
+
if (tile == nil || _activeAnimationId != 0 || !_hasShadowModel ||
|
|
962
|
+
_shadowLayer == nil ||
|
|
963
|
+
![self usesBakedShadowForGeometry:_shadowModelGeometry
|
|
964
|
+
shadow:_shadowModel]) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const CGFloat wanted = [self bakedShadowTileRadiusForGeometry:_shadowModelGeometry
|
|
968
|
+
shadow:_shadowModel];
|
|
969
|
+
if (SmoothClipCachedShadowTile(wanted, _shadowModelGeometry.curve,
|
|
970
|
+
_shadowModel, [self bakedShadowScale]) != tile ||
|
|
971
|
+
_shadowLayer.contents == (__bridge id)tile.image) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
[CATransaction begin];
|
|
975
|
+
[CATransaction setDisableActions:YES];
|
|
976
|
+
[self writeBakedShadowModel:_shadowModel
|
|
977
|
+
geometry:_shadowModelGeometry
|
|
978
|
+
visible:SmoothBoxShadowVisible(_shadowModelGeometry, _shadowModel)
|
|
979
|
+
nearestTile:NO
|
|
980
|
+
layer:_shadowLayer];
|
|
981
|
+
[CATransaction commit];
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// `allowNearest`: a missing tile may be stood in for by the nearest cached
|
|
985
|
+
// step until the exact one lands (a drag crossing a step, a model write); a
|
|
986
|
+
// run's resting target passes NO and bakes now, so its last frame is exact.
|
|
987
|
+
- (void)writeBakedShadowModel:(const smoothclip::Shadow &)shadow
|
|
988
|
+
geometry:(SmoothClipCanonicalGeometry)geometry
|
|
989
|
+
visible:(BOOL)visible
|
|
990
|
+
nearestTile:(BOOL)allowNearest
|
|
991
|
+
layer:(CALayer *)shadowLayer {
|
|
992
|
+
SmoothClipShadowTile *tile = nil;
|
|
993
|
+
if (visible) {
|
|
994
|
+
const CGFloat tileRadius =
|
|
995
|
+
[self bakedShadowTileRadiusForGeometry:geometry shadow:shadow];
|
|
996
|
+
const CGFloat scale = [self bakedShadowScale];
|
|
997
|
+
tile = SmoothClipCachedShadowTile(tileRadius, geometry.curve, shadow, scale);
|
|
998
|
+
if (tile == nil && allowNearest) {
|
|
999
|
+
tile = SmoothClipNearestCachedShadowTile(
|
|
1000
|
+
tileRadius, geometry.curve, shadow, scale);
|
|
1001
|
+
if (tile != nil) {
|
|
1002
|
+
// A neighbouring step for now (one step off is invisible under the
|
|
1003
|
+
// blur the tile was made for); the exact tile lands off this frame.
|
|
1004
|
+
__weak SmoothClipView *weakSelf = self;
|
|
1005
|
+
SmoothClipRequestShadowTile(
|
|
1006
|
+
tileRadius, geometry.curve, shadow, scale,
|
|
1007
|
+
^(SmoothClipShadowTile *exact) {
|
|
1008
|
+
[weakSelf shadowTileDidLand:exact];
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (tile == nil) {
|
|
1013
|
+
// Nothing near enough to show meanwhile (a cold cache, at mount), or a
|
|
1014
|
+
// resting target that must be exact.
|
|
1015
|
+
tile = SmoothClipBakeShadowTile(tileRadius, geometry.curve, shadow, scale);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Leave no blur-path state behind: a tile layer that kept a shadowPath
|
|
1019
|
+
// would draw both.
|
|
1020
|
+
if (shadowLayer.shadowPath != NULL) shadowLayer.shadowPath = NULL;
|
|
1021
|
+
shadowLayer.shadowOpacity = 0;
|
|
1022
|
+
if (tile == nil) {
|
|
1023
|
+
shadowLayer.opacity = 0;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (shadowLayer.contents != (__bridge id)tile.image) {
|
|
1027
|
+
shadowLayer.contents = (__bridge id)tile.image;
|
|
1028
|
+
shadowLayer.contentsCenter = tile.contentsCenter;
|
|
1029
|
+
shadowLayer.contentsScale = tile.scale;
|
|
1030
|
+
shadowLayer.contentsGravity = kCAGravityResize;
|
|
1031
|
+
}
|
|
1032
|
+
const CGRect rect =
|
|
1033
|
+
SmoothClipBakedShadowLayerRect(geometry, shadow, tile.margin);
|
|
1034
|
+
shadowLayer.bounds = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
|
1035
|
+
shadowLayer.position =
|
|
1036
|
+
CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
|
|
1037
|
+
shadowLayer.opacity = smoothclip::clamp01(shadow.alpha);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
608
1040
|
- (void)writeShadowModel:(const smoothclip::Shadow &)shadow
|
|
609
1041
|
geometry:(SmoothClipCanonicalGeometry)geometry {
|
|
1042
|
+
[self writeShadowModel:shadow
|
|
1043
|
+
geometry:geometry
|
|
1044
|
+
baked:[self usesBakedShadowForGeometry:geometry shadow:shadow]
|
|
1045
|
+
nearestTile:YES];
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
- (void)writeShadowModel:(const smoothclip::Shadow &)shadow
|
|
1049
|
+
geometry:(SmoothClipCanonicalGeometry)geometry
|
|
1050
|
+
baked:(BOOL)baked
|
|
1051
|
+
nearestTile:(BOOL)allowNearest {
|
|
610
1052
|
const smoothclip::Shadow effectiveShadow =
|
|
611
1053
|
SmoothBoxShadowWithEffectiveAlpha(geometry, shadow);
|
|
612
1054
|
const BOOL visible = SmoothBoxShadowVisible(geometry, effectiveShadow);
|
|
613
1055
|
if (_shadowLayer == nil && !visible) return;
|
|
614
1056
|
|
|
615
1057
|
CALayer *shadowLayer = visible ? [self ensureShadowLayer] : _shadowLayer;
|
|
1058
|
+
const BOOL pathInputChanged = !_hasShadowModel ||
|
|
1059
|
+
!SmoothBoxShadowPathInputEqual(
|
|
1060
|
+
_shadowModelGeometry, _shadowModel, geometry, effectiveShadow);
|
|
1061
|
+
_hasShadowModel = YES;
|
|
1062
|
+
_shadowModelGeometry = geometry;
|
|
1063
|
+
_shadowModel = effectiveShadow;
|
|
1064
|
+
if (baked) {
|
|
1065
|
+
[self writeBakedShadowModel:effectiveShadow
|
|
1066
|
+
geometry:geometry
|
|
1067
|
+
visible:visible
|
|
1068
|
+
nearestTile:allowNearest
|
|
1069
|
+
layer:shadowLayer];
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
BOOL leftTile = NO;
|
|
1073
|
+
if (shadowLayer.contents != nil) {
|
|
1074
|
+
// Back from a baked tile: the blur path draws at layer opacity 1 with
|
|
1075
|
+
// the alpha in shadowColor, and needs its path again.
|
|
1076
|
+
shadowLayer.contents = nil;
|
|
1077
|
+
shadowLayer.opacity = 1;
|
|
1078
|
+
leftTile = YES;
|
|
1079
|
+
}
|
|
616
1080
|
shadowLayer.bounds = self.bounds;
|
|
617
1081
|
shadowLayer.position = CGPointMake(
|
|
618
1082
|
CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
|
|
619
|
-
if (
|
|
620
|
-
_shadowModelGeometry, _shadowModel, geometry, effectiveShadow)) {
|
|
1083
|
+
if (pathInputChanged || leftTile) {
|
|
621
1084
|
CGPathRef path = SmoothClipCreateShadowPath(geometry, effectiveShadow);
|
|
622
1085
|
shadowLayer.shadowPath = path;
|
|
623
1086
|
CGPathRelease(path);
|
|
624
1087
|
}
|
|
625
|
-
_hasShadowModel = YES;
|
|
626
|
-
_shadowModelGeometry = geometry;
|
|
627
|
-
_shadowModel = effectiveShadow;
|
|
628
1088
|
shadowLayer.shadowColor = [self colorForShadow:effectiveShadow];
|
|
629
1089
|
shadowLayer.shadowOpacity = visible ? 1 : 0;
|
|
630
1090
|
shadowLayer.shadowRadius = MAX(0, effectiveShadow.blurRadius) / 2.0;
|
|
@@ -632,6 +1092,178 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
632
1092
|
effectiveShadow.offsetX, effectiveShadow.offsetY);
|
|
633
1093
|
}
|
|
634
1094
|
|
|
1095
|
+
// Animations for a baked shadow run. Bounds, position and opacity
|
|
1096
|
+
// interpolate; the tile (radius step, blur, colour) is categorical, so it steps
|
|
1097
|
+
// at the key times where the run's progress crosses each radius step, with the
|
|
1098
|
+
// target's blur and colour from the first swap on. Steps are one radius step
|
|
1099
|
+
// apart, invisible under any blur the tile was made for.
|
|
1100
|
+
- (NSArray<CAAnimation *> *)bakedShadowAnimationsFromGeometry:
|
|
1101
|
+
(SmoothClipCanonicalGeometry)fromGeometry
|
|
1102
|
+
fromShadow:(smoothclip::Shadow)fromShadow
|
|
1103
|
+
toGeometry:(SmoothClipCanonicalGeometry)toGeometry
|
|
1104
|
+
toShadow:(smoothclip::Shadow)toShadow
|
|
1105
|
+
timing:(CAMediaTimingFunction *)timing
|
|
1106
|
+
spring:(BOOL)spring
|
|
1107
|
+
velocity:(double)velocity
|
|
1108
|
+
duration:(CFTimeInterval)duration {
|
|
1109
|
+
const CGFloat scale = [self bakedShadowScale];
|
|
1110
|
+
const CGFloat fromRadius =
|
|
1111
|
+
[self bakedShadowTileRadiusForGeometry:fromGeometry shadow:fromShadow];
|
|
1112
|
+
const CGFloat toRadius =
|
|
1113
|
+
[self bakedShadowTileRadiusForGeometry:toGeometry shadow:toShadow];
|
|
1114
|
+
// The start is whatever the model shows (its step or a stand-in); the
|
|
1115
|
+
// target was baked by the model write just before this and must be exact.
|
|
1116
|
+
SmoothClipShadowTile *fromTile = SmoothClipNearestCachedShadowTile(
|
|
1117
|
+
fromRadius, fromGeometry.curve, fromShadow, scale);
|
|
1118
|
+
if (fromTile == nil) {
|
|
1119
|
+
fromTile = SmoothClipBakeShadowTile(
|
|
1120
|
+
fromRadius, fromGeometry.curve, fromShadow, scale);
|
|
1121
|
+
}
|
|
1122
|
+
SmoothClipShadowTile *toTile = SmoothClipBakeShadowTile(
|
|
1123
|
+
toRadius, toGeometry.curve, toShadow, scale);
|
|
1124
|
+
const CGFloat fromMargin = fromTile != nil
|
|
1125
|
+
? fromTile.margin : SmoothClipBakedShadowMargin(fromShadow);
|
|
1126
|
+
const CGFloat toMargin = toTile != nil
|
|
1127
|
+
? toTile.margin : SmoothClipBakedShadowMargin(toShadow);
|
|
1128
|
+
const CGRect fromRect =
|
|
1129
|
+
SmoothClipBakedShadowLayerRect(fromGeometry, fromShadow, fromMargin);
|
|
1130
|
+
const CGRect toRect =
|
|
1131
|
+
SmoothClipBakedShadowLayerRect(toGeometry, toShadow, toMargin);
|
|
1132
|
+
NSMutableArray<CAAnimation *> *animations = [NSMutableArray array];
|
|
1133
|
+
const auto add = [&](NSString *keyPath, id from, id to) {
|
|
1134
|
+
[animations addObject:spring
|
|
1135
|
+
? [self springAnimationForKeyPath:keyPath fromValue:from toValue:to
|
|
1136
|
+
velocity:velocity duration:duration]
|
|
1137
|
+
: [self basicAnimationForKeyPath:keyPath fromValue:from toValue:to
|
|
1138
|
+
timingFunction:timing]];
|
|
1139
|
+
};
|
|
1140
|
+
if (!CGSizeEqualToSize(fromRect.size, toRect.size)) {
|
|
1141
|
+
add(@"bounds",
|
|
1142
|
+
[NSValue valueWithCGRect:CGRectMake(
|
|
1143
|
+
0, 0, fromRect.size.width, fromRect.size.height)],
|
|
1144
|
+
[NSValue valueWithCGRect:CGRectMake(
|
|
1145
|
+
0, 0, toRect.size.width, toRect.size.height)]);
|
|
1146
|
+
}
|
|
1147
|
+
const CGPoint fromCenter =
|
|
1148
|
+
CGPointMake(CGRectGetMidX(fromRect), CGRectGetMidY(fromRect));
|
|
1149
|
+
const CGPoint toCenter =
|
|
1150
|
+
CGPointMake(CGRectGetMidX(toRect), CGRectGetMidY(toRect));
|
|
1151
|
+
if (!CGPointEqualToPoint(fromCenter, toCenter)) {
|
|
1152
|
+
add(@"position",
|
|
1153
|
+
[NSValue valueWithCGPoint:fromCenter],
|
|
1154
|
+
[NSValue valueWithCGPoint:toCenter]);
|
|
1155
|
+
}
|
|
1156
|
+
if (fromShadow.alpha != toShadow.alpha) {
|
|
1157
|
+
add(@"opacity", @(smoothclip::clamp01(fromShadow.alpha)),
|
|
1158
|
+
@(smoothclip::clamp01(toShadow.alpha)));
|
|
1159
|
+
}
|
|
1160
|
+
if (fromTile == nil || toTile == nil || fromTile == toTile) {
|
|
1161
|
+
return animations;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
NSMutableArray *images = [NSMutableArray arrayWithObject:(__bridge id)fromTile.image];
|
|
1165
|
+
NSMutableArray<NSValue *> *centers =
|
|
1166
|
+
[NSMutableArray arrayWithObject:[NSValue valueWithCGRect:fromTile.contentsCenter]];
|
|
1167
|
+
NSMutableArray<NSNumber *> *keyTimes = [NSMutableArray arrayWithObject:@0];
|
|
1168
|
+
const CGFloat rawFrom = SmoothClipAdjustedShadowRadius(
|
|
1169
|
+
fromGeometry.radii.topLeft, fromShadow.spreadDistance);
|
|
1170
|
+
const CGFloat rawTo = SmoothClipAdjustedShadowRadius(
|
|
1171
|
+
toGeometry.radii.topLeft, toShadow.spreadDistance);
|
|
1172
|
+
// Normalised time at which the run's progress first reaches `target`.
|
|
1173
|
+
const auto timeForProgress = [&](double target) -> double {
|
|
1174
|
+
if (target <= 0) return 0;
|
|
1175
|
+
if (target >= 1) return 1;
|
|
1176
|
+
constexpr double step = 1.0 / 240.0;
|
|
1177
|
+
if (spring) {
|
|
1178
|
+
smoothclip::ScalarSpringState state{0, velocity};
|
|
1179
|
+
double elapsed = 0;
|
|
1180
|
+
while (elapsed < duration) {
|
|
1181
|
+
state = smoothclip::advanceScalarSpring(state, _springAnimation, step);
|
|
1182
|
+
elapsed += step;
|
|
1183
|
+
if (state.position >= target) break;
|
|
1184
|
+
}
|
|
1185
|
+
return duration > 0 ? std::min(1.0, elapsed / duration) : 1.0;
|
|
1186
|
+
}
|
|
1187
|
+
double low = 0, high = 1;
|
|
1188
|
+
for (int iteration = 0; iteration < 20; iteration += 1) {
|
|
1189
|
+
const double middle = (low + high) / 2;
|
|
1190
|
+
const double progress = smoothclip::cubicBezier(
|
|
1191
|
+
_timingAnimation.controlPoint1X, _timingAnimation.controlPoint1Y,
|
|
1192
|
+
_timingAnimation.controlPoint2X, _timingAnimation.controlPoint2Y,
|
|
1193
|
+
middle);
|
|
1194
|
+
if (progress < target) low = middle; else high = middle;
|
|
1195
|
+
}
|
|
1196
|
+
return (low + high) / 2;
|
|
1197
|
+
};
|
|
1198
|
+
if (fromTile.radius == toTile.radius || rawTo == rawFrom) {
|
|
1199
|
+
// Same radius step, a different blur or colour: one swap, mid-run.
|
|
1200
|
+
[images addObject:(__bridge id)toTile.image];
|
|
1201
|
+
[centers addObject:[NSValue valueWithCGRect:toTile.contentsCenter]];
|
|
1202
|
+
[keyTimes addObject:@0.5];
|
|
1203
|
+
} else {
|
|
1204
|
+
const BOOL growing = toTile.radius > fromTile.radius;
|
|
1205
|
+
const CGFloat step = kSmoothClipShadowTileRadiusStep * (growing ? 1 : -1);
|
|
1206
|
+
// Key time a missing step hands to the next cached tile on a growing run.
|
|
1207
|
+
double carriedTime = -1;
|
|
1208
|
+
for (CGFloat tileRadius = fromTile.radius + step;
|
|
1209
|
+
growing ? tileRadius <= toTile.radius : tileRadius >= toTile.radius;
|
|
1210
|
+
tileRadius += step) {
|
|
1211
|
+
// Rounding up: a growing radius takes tile T once it passes T - step,
|
|
1212
|
+
// a shrinking one once it reaches T.
|
|
1213
|
+
const CGFloat threshold = growing
|
|
1214
|
+
? tileRadius - kSmoothClipShadowTileRadiusStep : tileRadius;
|
|
1215
|
+
const double progress = (threshold - rawFrom) / (rawTo - rawFrom);
|
|
1216
|
+
SmoothClipShadowTile *tile = SmoothClipCachedShadowTile(
|
|
1217
|
+
tileRadius, toGeometry.curve, toShadow, scale);
|
|
1218
|
+
if (tile == nil) {
|
|
1219
|
+
// Not baked inside this transaction: it lands for the next run. A
|
|
1220
|
+
// growing run swaps to the next cached (rounder) tile at this step's
|
|
1221
|
+
// time instead, so the shown corner is never tighter than the clip's;
|
|
1222
|
+
// a shrinking run simply keeps the rounder tile a step longer.
|
|
1223
|
+
SmoothClipRequestShadowTile(
|
|
1224
|
+
tileRadius, toGeometry.curve, toShadow, scale, nil);
|
|
1225
|
+
if (growing && carriedTime < 0) carriedTime = timeForProgress(progress);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const double ownTime = timeForProgress(progress);
|
|
1229
|
+
const double time = std::max(
|
|
1230
|
+
keyTimes.lastObject.doubleValue,
|
|
1231
|
+
carriedTime >= 0 ? std::min(carriedTime, ownTime) : ownTime);
|
|
1232
|
+
carriedTime = -1;
|
|
1233
|
+
if (time <= keyTimes.lastObject.doubleValue + 1e-6) {
|
|
1234
|
+
// Same instant as the previous swap (leaving a step value at t = 0):
|
|
1235
|
+
// the later tile is the one shown; key times must strictly increase.
|
|
1236
|
+
images[images.count - 1] = (__bridge id)tile.image;
|
|
1237
|
+
centers[centers.count - 1] = [NSValue valueWithCGRect:tile.contentsCenter];
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
[images addObject:(__bridge id)tile.image];
|
|
1241
|
+
[centers addObject:[NSValue valueWithCGRect:tile.contentsCenter]];
|
|
1242
|
+
[keyTimes addObject:@(time)];
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (images.count < 2) return animations;
|
|
1246
|
+
// Discrete key times bracket their values: one more entry than values, the
|
|
1247
|
+
// last at 1 (CAKeyframeAnimation.keyTimes). Without the closing entry Core
|
|
1248
|
+
// Animation ignores the array and spaces the swaps evenly over the run.
|
|
1249
|
+
[keyTimes addObject:@1];
|
|
1250
|
+
CAKeyframeAnimation *contents =
|
|
1251
|
+
[CAKeyframeAnimation animationWithKeyPath:@"contents"];
|
|
1252
|
+
contents.values = images;
|
|
1253
|
+
contents.keyTimes = keyTimes;
|
|
1254
|
+
contents.calculationMode = kCAAnimationDiscrete;
|
|
1255
|
+
contents.duration = duration;
|
|
1256
|
+
[animations addObject:contents];
|
|
1257
|
+
CAKeyframeAnimation *contentsCenter =
|
|
1258
|
+
[CAKeyframeAnimation animationWithKeyPath:@"contentsCenter"];
|
|
1259
|
+
contentsCenter.values = centers;
|
|
1260
|
+
contentsCenter.keyTimes = keyTimes;
|
|
1261
|
+
contentsCenter.calculationMode = kCAAnimationDiscrete;
|
|
1262
|
+
contentsCenter.duration = duration;
|
|
1263
|
+
[animations addObject:contentsCenter];
|
|
1264
|
+
return animations;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
635
1267
|
- (void)writeShadow:(const smoothclip::Shadow &)shadow
|
|
636
1268
|
geometry:(SmoothClipCanonicalGeometry)geometry {
|
|
637
1269
|
_canonicalShadow = shadow;
|
|
@@ -964,6 +1596,15 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
964
1596
|
if (shadowLayer == nil) return SmoothClipCanonicalSnapshot({
|
|
965
1597
|
geometry, transform.tx, transform.ty, contentScale, shadow,
|
|
966
1598
|
[self visibleRotation], [self visibleOpacity]});
|
|
1599
|
+
if (shadowLayer.contents != nil) {
|
|
1600
|
+
// A baked tile: colour, blur, offset and spread never move on the layer
|
|
1601
|
+
// (a run swaps tiles); the layer's opacity carries the alpha.
|
|
1602
|
+
shadow.enabled = _canonicalShadow.enabled || shadowLayer.opacity > 0;
|
|
1603
|
+
shadow.alpha = shadowLayer.opacity;
|
|
1604
|
+
return SmoothClipCanonicalSnapshot({
|
|
1605
|
+
geometry, transform.tx, transform.ty, contentScale, shadow,
|
|
1606
|
+
[self visibleRotation], [self visibleOpacity]});
|
|
1607
|
+
}
|
|
967
1608
|
shadow.enabled = _canonicalShadow.enabled || shadowLayer.shadowOpacity > 0;
|
|
968
1609
|
shadow.blurRadius = shadowLayer.shadowRadius * 2.0;
|
|
969
1610
|
shadow.offsetX = shadowLayer.shadowOffset.width;
|
|
@@ -1087,16 +1728,7 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1087
1728
|
// Every key path shares one normalized trajectory. Derive the terminal
|
|
1088
1729
|
// duration from the same relative-energy rule as Android/Reanimated instead
|
|
1089
1730
|
// 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;
|
|
1731
|
+
return smoothclip::springSettleDuration(_springAnimation, velocity);
|
|
1100
1732
|
}
|
|
1101
1733
|
|
|
1102
1734
|
- (CASpringAnimation *)springAnimationForKeyPath:(NSString *)keyPath
|
|
@@ -1174,11 +1806,22 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1174
1806
|
_canonicalShadow = toShadow;
|
|
1175
1807
|
SmoothBoxShadowNormalizeAnimationEndpoints(
|
|
1176
1808
|
fromGeometry, &fromShadow, toGeometry, &toShadow);
|
|
1809
|
+
// A baked run animates the tile layer's frame and opacity and swaps tiles
|
|
1810
|
+
// in steps (bakedShadowAnimationsFromGeometry:...). One non-uniform
|
|
1811
|
+
// endpoint has no tile, so such a run takes the blur path throughout.
|
|
1812
|
+
const BOOL bakedShadow =
|
|
1813
|
+
(SmoothBoxShadowVisible(fromGeometry, fromShadow) ||
|
|
1814
|
+
SmoothBoxShadowVisible(toGeometry, toShadow)) &&
|
|
1815
|
+
[self usesBakedShadowForGeometry:fromGeometry shadow:fromShadow] &&
|
|
1816
|
+
[self usesBakedShadowForGeometry:toGeometry shadow:toShadow];
|
|
1177
1817
|
// The target model layer is complete before any presentation animations are
|
|
1178
1818
|
// attached. Core Animation may remove the terminal presentation animation
|
|
1179
1819
|
// before its delegate runs, so correctness cannot depend on completion-time
|
|
1180
1820
|
// cleanup hiding a stale path or opaque color.
|
|
1181
|
-
[self writeShadowModel:toShadow
|
|
1821
|
+
[self writeShadowModel:toShadow
|
|
1822
|
+
geometry:toGeometry
|
|
1823
|
+
baked:bakedShadow
|
|
1824
|
+
nearestTile:NO];
|
|
1182
1825
|
if (usesMask) {
|
|
1183
1826
|
// Keep one mask representation for the full interval, including an
|
|
1184
1827
|
// unequal→uniform transition. Switching layer.mask mid-animation would
|
|
@@ -1217,12 +1860,13 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1217
1860
|
[self ensureShadowLayer];
|
|
1218
1861
|
// Reveal opacity is encoded in shadowColor alpha. Animating layer opacity
|
|
1219
1862
|
// too would multiply both curves and produce a quadratic-looking fade.
|
|
1220
|
-
|
|
1863
|
+
// (A baked tile has no shadowColor; its alpha is the layer opacity.)
|
|
1864
|
+
if (!bakedShadow) _shadowLayer.shadowOpacity = 1;
|
|
1221
1865
|
}
|
|
1222
|
-
CGPathRef fromShadowPath = animatesShadowPath
|
|
1866
|
+
CGPathRef fromShadowPath = animatesShadowPath && !bakedShadow
|
|
1223
1867
|
? SmoothClipCreateShadowPath(fromGeometry, fromShadow)
|
|
1224
1868
|
: nil;
|
|
1225
|
-
CGPathRef toShadowPath = animatesShadowPath
|
|
1869
|
+
CGPathRef toShadowPath = animatesShadowPath && !bakedShadow
|
|
1226
1870
|
? SmoothClipCreateShadowPath(toGeometry, toShadow)
|
|
1227
1871
|
: nil;
|
|
1228
1872
|
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
@@ -1292,7 +1936,18 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1292
1936
|
}
|
|
1293
1937
|
contentGroup.animations = contentAnimations;
|
|
1294
1938
|
contentGroup.duration = duration;
|
|
1295
|
-
if (animatesShadow) {
|
|
1939
|
+
if (animatesShadow && bakedShadow) {
|
|
1940
|
+
shadowGroup.animations = [self
|
|
1941
|
+
bakedShadowAnimationsFromGeometry:fromGeometry
|
|
1942
|
+
fromShadow:fromShadow
|
|
1943
|
+
toGeometry:toGeometry
|
|
1944
|
+
toShadow:toShadow
|
|
1945
|
+
timing:timing
|
|
1946
|
+
spring:NO
|
|
1947
|
+
velocity:0
|
|
1948
|
+
duration:duration];
|
|
1949
|
+
shadowGroup.duration = duration;
|
|
1950
|
+
} else if (animatesShadow) {
|
|
1296
1951
|
NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
|
|
1297
1952
|
if (animatesShadowPath) [shadowAnimations addObject:
|
|
1298
1953
|
[self basicAnimationForKeyPath:@"shadowPath"
|
|
@@ -1435,7 +2090,18 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1435
2090
|
}
|
|
1436
2091
|
contentGroup.animations = contentAnimations;
|
|
1437
2092
|
contentGroup.duration = springDuration;
|
|
1438
|
-
if (animatesShadow) {
|
|
2093
|
+
if (animatesShadow && bakedShadow) {
|
|
2094
|
+
shadowGroup.animations = [self
|
|
2095
|
+
bakedShadowAnimationsFromGeometry:fromGeometry
|
|
2096
|
+
fromShadow:fromShadow
|
|
2097
|
+
toGeometry:toGeometry
|
|
2098
|
+
toShadow:toShadow
|
|
2099
|
+
timing:nil
|
|
2100
|
+
spring:YES
|
|
2101
|
+
velocity:velocity
|
|
2102
|
+
duration:springDuration];
|
|
2103
|
+
shadowGroup.duration = springDuration;
|
|
2104
|
+
} else if (animatesShadow) {
|
|
1439
2105
|
NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
|
|
1440
2106
|
if (animatesShadowPath) [shadowAnimations addObject:
|
|
1441
2107
|
[self springAnimationForKeyPath:@"shadowPath"
|
|
@@ -1527,6 +2193,14 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1527
2193
|
convertTime:sharedBeginTime
|
|
1528
2194
|
fromLayer:nil];
|
|
1529
2195
|
}
|
|
2196
|
+
// A stamped epoch may sit up to one frame ahead of this commit. Until it
|
|
2197
|
+
// arrives the run presents its first frame rather than the model values
|
|
2198
|
+
// the layers already hold, which are the target.
|
|
2199
|
+
appearanceGroup.fillMode = kCAFillModeBackwards;
|
|
2200
|
+
group.fillMode = kCAFillModeBackwards;
|
|
2201
|
+
contentGroup.fillMode = kCAFillModeBackwards;
|
|
2202
|
+
if (animatesShadow) shadowGroup.fillMode = kCAFillModeBackwards;
|
|
2203
|
+
if (maskAnimation != nil) maskAnimation.fillMode = kCAFillModeBackwards;
|
|
1530
2204
|
}
|
|
1531
2205
|
|
|
1532
2206
|
_animationDelegate = [SmoothClipAnimationDelegate new];
|
|
@@ -1859,6 +2533,17 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1859
2533
|
[super updateProps:props oldProps:oldProps];
|
|
1860
2534
|
self.clipsToBounds = YES;
|
|
1861
2535
|
|
|
2536
|
+
const BOOL bakedShadows = newProps.shadowRendering == 1;
|
|
2537
|
+
if (bakedShadows != _bakedShadows) {
|
|
2538
|
+
_bakedShadows = bakedShadows;
|
|
2539
|
+
// Re-render the current shadow in the new mode. A run in flight keeps its
|
|
2540
|
+
// renderer to the end; its completion writes the model again.
|
|
2541
|
+
if (_activeAnimationId == 0 && _hasShadowModel) {
|
|
2542
|
+
[self writeShadow:_requestedShadow
|
|
2543
|
+
geometry:[self canonicalGeometryValue]];
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
1862
2547
|
const uint64_t nextDriverId =
|
|
1863
2548
|
isfinite(newProps.driverId) && newProps.driverId > 0
|
|
1864
2549
|
? static_cast<uint64_t>(newProps.driverId)
|
|
@@ -1888,6 +2573,8 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
1888
2573
|
newProps.initialClipBoxShadowSpreadDistance,
|
|
1889
2574
|
newProps.initialRotation,
|
|
1890
2575
|
newProps.initialOpacity,
|
|
2576
|
+
newProps.initialBackdropTranslateX,
|
|
2577
|
+
newProps.initialBackdropTranslateY,
|
|
1891
2578
|
&initial)) {
|
|
1892
2579
|
// The initial presentation is atomic. Never apply a valid subset when one
|
|
1893
2580
|
// field rejects.
|
|
@@ -2013,6 +2700,10 @@ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
|
|
|
2013
2700
|
shadowSpreadDistance,
|
|
2014
2701
|
rotation,
|
|
2015
2702
|
opacity,
|
|
2703
|
+
// The command protocol carries no backdrop channel; a command-driven
|
|
2704
|
+
// host keeps its backdrops where they are.
|
|
2705
|
+
0,
|
|
2706
|
+
0,
|
|
2016
2707
|
&presentation)) {
|
|
2017
2708
|
return;
|
|
2018
2709
|
}
|