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.
Files changed (64) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +47 -0
  3. package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
  4. package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
  5. package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
  6. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +18 -0
  7. package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
  13. package/cpp/SmoothClipAnimationCurve.h +36 -11
  14. package/cpp/SmoothClipRegistry.h +11 -4
  15. package/ios/SmoothClipBackdropView.h +17 -0
  16. package/ios/SmoothClipBackdropView.mm +267 -0
  17. package/ios/SmoothClipRegistry.mm +192 -9
  18. package/ios/SmoothClipTurboModule.cpp +33 -11
  19. package/ios/SmoothClipView.mm +723 -22
  20. package/ios/SmoothClipViewRegistry.h +14 -0
  21. package/lib/module/SmoothClipBackdropView.js +40 -0
  22. package/lib/module/SmoothClipBackdropView.js.map +1 -0
  23. package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
  24. package/lib/module/SmoothClipView.ios.js +17 -1
  25. package/lib/module/SmoothClipView.ios.js.map +1 -1
  26. package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
  27. package/lib/module/geometry.js +44 -6
  28. package/lib/module/geometry.js.map +1 -1
  29. package/lib/module/groups.native.js +10 -3
  30. package/lib/module/groups.native.js.map +1 -1
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/presentationCodec.js +6 -2
  34. package/lib/module/presentationCodec.js.map +1 -1
  35. package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
  36. package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
  37. package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
  38. package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
  39. package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
  40. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
  41. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  42. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  43. package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
  44. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  45. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  46. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  47. package/lib/typescript/src/geometry.d.ts +19 -0
  48. package/lib/typescript/src/geometry.d.ts.map +1 -1
  49. package/lib/typescript/src/groups.native.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts +4 -2
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/presentationCodec.d.ts +1 -1
  53. package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
  54. package/package.json +4 -1
  55. package/src/SmoothClipBackdropView.tsx +53 -0
  56. package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
  57. package/src/SmoothClipView.android.tsx +4 -1
  58. package/src/SmoothClipView.ios.tsx +24 -1
  59. package/src/SmoothClipView.tsx +4 -1
  60. package/src/SmoothClipViewNativeComponent.ts +4 -0
  61. package/src/geometry.ts +59 -4
  62. package/src/groups.native.ts +9 -3
  63. package/src/index.ts +7 -1
  64. package/src/presentationCodec.ts +8 -2
@@ -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 (!_hasShadowModel || !SmoothBoxShadowPathInputEqual(
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::ScalarSpringState state{0, velocity};
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 geometry:toGeometry];
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
- _shadowLayer.shadowOpacity = 1;
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
  }