lottie-ios 4.4.3 → 4.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.
Files changed (81) hide show
  1. package/.github/workflows/main.yml +6 -34
  2. package/Lottie.xcodeproj/project.pbxproj +28 -0
  3. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  4. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +19 -6
  5. package/Package.resolved +2 -2
  6. package/Package.swift +1 -1
  7. package/README.md +2 -2
  8. package/Rakefile +1 -1
  9. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +2 -2
  10. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +6 -6
  11. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +6 -6
  12. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +2 -2
  13. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +7 -7
  14. package/Sources/Private/CoreAnimation/Layers/ShapeItemLayer.swift +12 -12
  15. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +19 -19
  16. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +4 -4
  17. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +13 -6
  18. package/Sources/Private/EmbeddedLibraries/LRUCache/LRUCache.swift +3 -3
  19. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+BackingConfiguration.swift +10 -6
  20. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Helpers.swift +4 -0
  21. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Progress.swift +2 -2
  22. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Reading.swift +5 -0
  23. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Archive+Writing.swift +2 -0
  24. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Data+Compression.swift +1 -0
  25. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Entry+ZIP64.swift +2 -2
  26. package/Sources/Private/EmbeddedLibraries/ZipFoundation/Entry.swift +1 -0
  27. package/Sources/Private/EmbeddedLibraries/ZipFoundation/FileManager+ZIP.swift +10 -9
  28. package/Sources/Private/MainThread/LayerContainers/CompLayers/CompositionLayer.swift +19 -0
  29. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +7 -7
  30. package/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift +3 -0
  31. package/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift +24 -14
  32. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +11 -10
  33. package/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift +2 -0
  34. package/Sources/Private/MainThread/LayerContainers/Utility/CoreTextRenderLayer.swift +102 -17
  35. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTransformNode.swift +11 -11
  36. package/Sources/Private/MainThread/NodeRenderSystem/Extensions/ItemsExtension.swift +2 -0
  37. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/LayerEffectNodes/DropShadowNode.swift +102 -0
  38. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/LayerEffectNodes/LayerEffectNode.swift +24 -0
  39. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/FillRenderer.swift +4 -4
  40. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/StrokeRenderer.swift +16 -16
  41. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/PathNodes/PolygonNode.swift +4 -4
  42. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +5 -5
  43. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +60 -1
  44. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +2 -2
  45. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +9 -6
  46. package/Sources/Private/Model/LayerEffects/EffectValues/EffectValue.swift +4 -4
  47. package/Sources/Private/Model/LayerEffects/LayerEffect.swift +2 -2
  48. package/Sources/Private/Model/LayerStyles/LayerStyle.swift +2 -2
  49. package/Sources/Private/Model/Layers/LayerModel.swift +7 -7
  50. package/Sources/Private/Model/ShapeItems/ShapeItem.swift +15 -15
  51. package/Sources/Private/Model/Text/TextAnimator.swift +47 -1
  52. package/Sources/Private/Utility/Debugging/LayerDebugging.swift +6 -6
  53. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +16 -16
  54. package/Sources/Private/Utility/Extensions/BlendMode+Filter.swift +16 -16
  55. package/Sources/Private/Utility/Extensions/CGColor+RGB.swift +18 -0
  56. package/Sources/Private/Utility/Extensions/CGFloatExtensions.swift +23 -23
  57. package/Sources/Private/Utility/Extensions/MathKit.swift +4 -11
  58. package/Sources/Private/Utility/Interpolatable/InterpolatableExtensions.swift +9 -3
  59. package/Sources/Private/Utility/LottieAnimationSource.swift +4 -4
  60. package/Sources/Private/Utility/Primitives/BezierPath.swift +16 -2
  61. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +2 -2
  62. package/Sources/Private/Utility/Primitives/ColorExtension.swift +20 -20
  63. package/Sources/Private/Utility/Primitives/CompoundBezierPath.swift +9 -9
  64. package/Sources/Public/Animation/LottieAnimationLayer.swift +36 -31
  65. package/Sources/Public/Animation/LottieAnimationView.swift +15 -15
  66. package/Sources/Public/Animation/LottieAnimationViewInitializers.swift +4 -0
  67. package/Sources/Public/Animation/LottiePlaybackMode.swift +12 -12
  68. package/Sources/Public/Animation/LottieView.swift +34 -1
  69. package/Sources/Public/Configuration/ReducedMotionOption.swift +5 -5
  70. package/Sources/Public/Configuration/RenderingEngineOption.swift +4 -4
  71. package/Sources/Public/Controls/AnimatedSwitch.swift +2 -2
  72. package/Sources/Public/DotLottie/DotLottieFile.swift +2 -2
  73. package/Sources/Public/DynamicProperties/AnyValueProvider.swift +9 -9
  74. package/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +2 -2
  75. package/Sources/Public/Keyframes/Interpolatable.swift +2 -2
  76. package/Sources/Public/Primitives/LottieColor.swift +3 -3
  77. package/Sources/Public/TextProvider/AnimationTextProvider.swift +4 -4
  78. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +13 -12
  79. package/Version.xcconfig +1 -1
  80. package/lottie-ios.podspec +2 -2
  81. package/package.json +1 -1
@@ -83,6 +83,7 @@ extension Archive {
83
83
  bufferSize: bufferSize,
84
84
  progress: progress,
85
85
  provider: provider)
86
+
86
87
  case .deflate:
87
88
  (sizeWritten, checksum) = try writeCompressed(
88
89
  size: uncompressedSize,
@@ -90,9 +91,11 @@ extension Archive {
90
91
  progress: progress,
91
92
  provider: provider)
92
93
  }
94
+
93
95
  case .directory:
94
96
  _ = try provider(0, 0)
95
97
  if let progress { progress.completedUnitCount = progress.totalUnitCount }
98
+
96
99
  case .symlink:
97
100
  let (linkSizeWritten, linkChecksum) = try writeSymbolicLink(
98
101
  size: Int(uncompressedSize),
@@ -226,6 +229,7 @@ extension Archive {
226
229
  throw ArchiveError.invalidCentralDirectoryEntryCount
227
230
  }
228
231
  return (sizeOfCD + UInt64(cdDataLengthChange), numberOfTotalEntries + UInt64(countChange))
232
+
229
233
  case .remove:
230
234
  return (sizeOfCD - UInt64(-cdDataLengthChange), numberOfTotalEntries - UInt64(-countChange))
231
235
  }
@@ -32,9 +32,9 @@ extension Archive {
32
32
  func totalUnitCountForReading(_ entry: Entry) -> Int64 {
33
33
  switch entry.type {
34
34
  case .file, .symlink:
35
- return Int64(entry.uncompressedSize)
35
+ Int64(entry.uncompressedSize)
36
36
  case .directory:
37
- return defaultDirectoryUnitCount
37
+ defaultDirectoryUnitCount
38
38
  }
39
39
  }
40
40
 
@@ -52,6 +52,7 @@ extension Archive {
52
52
  skipCRC32: skipCRC32,
53
53
  progress: progress,
54
54
  consumer: consumer)
55
+
55
56
  case .directory:
56
57
  let consumer = { (_: Data) in
57
58
  try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
@@ -62,6 +63,7 @@ extension Archive {
62
63
  skipCRC32: skipCRC32,
63
64
  progress: progress,
64
65
  consumer: consumer)
66
+
65
67
  case .symlink:
66
68
  guard !fileManager.itemExists(at: url) else {
67
69
  throw CocoaError(.fileWriteFileExists, userInfo: [NSFilePathErrorKey: url.path])
@@ -121,6 +123,7 @@ extension Archive {
121
123
  skipCRC32: skipCRC32,
122
124
  progress: progress,
123
125
  with: consumer)
126
+
124
127
  case .deflate: checksum = try readCompressed(
125
128
  entry: entry,
126
129
  bufferSize: bufferSize,
@@ -128,9 +131,11 @@ extension Archive {
128
131
  progress: progress,
129
132
  with: consumer)
130
133
  }
134
+
131
135
  case .directory:
132
136
  try consumer(Data())
133
137
  progress?.completedUnitCount = totalUnitCountForReading(entry)
138
+
134
139
  case .symlink:
135
140
  let localFileHeader = entry.localFileHeader
136
141
  let size = Int(localFileHeader.compressedSize)
@@ -96,6 +96,7 @@ extension Archive {
96
96
  bufferSize: bufferSize,
97
97
  progress: progress,
98
98
  provider: provider)
99
+
99
100
  case .directory:
100
101
  provider = { _, _ in Data() }
101
102
  try addEntry(
@@ -108,6 +109,7 @@ extension Archive {
108
109
  bufferSize: bufferSize,
109
110
  progress: progress,
110
111
  provider: provider)
112
+
111
113
  case .symlink:
112
114
  provider = { _, _ -> Data in
113
115
  let linkDestination = try fileManager.destinationOfSymbolicLink(atPath: fileURL.path)
@@ -169,6 +169,7 @@ extension Data {
169
169
  if operation == COMPRESSION_STREAM_DECODE, !skipCRC32 { crc32 = outputData.crc32(checksum: crc32) }
170
170
  stream.dst_ptr = destPointer
171
171
  stream.dst_size = bufferSize
172
+
172
173
  default: throw CompressionError.corruptedData
173
174
  }
174
175
  } while status == COMPRESSION_STATUS_OK
@@ -121,9 +121,9 @@ extension Entry.ZIP64ExtendedInformation {
121
121
  var size: Int {
122
122
  switch self {
123
123
  case .uncompressedSize, .compressedSize, .relativeOffsetOfLocalHeader:
124
- return 8
124
+ 8
125
125
  case .diskNumberStart:
126
- return 4
126
+ 4
127
127
  }
128
128
  }
129
129
  }
@@ -185,6 +185,7 @@ struct Entry: Equatable {
185
185
  default:
186
186
  return isDirectory ? .directory : .file
187
187
  }
188
+
188
189
  case .msdos:
189
190
  isDirectory = isDirectory || ((centralDirectoryStructure.externalFileAttributes >> 4) == 0x01)
190
191
  fallthrough // For all other OSes we can only guess based on the directory suffix char
@@ -44,21 +44,22 @@ extension FileManager {
44
44
  let permissions = mode_t(externalFileAttributes >> 16) & ~S_IFMT
45
45
  let defaultPermissions = entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
46
46
  return permissions == 0 ? defaultPermissions : UInt16(permissions)
47
+
47
48
  default:
48
49
  return entryType == .directory ? defaultDirectoryPermissions : defaultFilePermissions
49
50
  }
50
51
  }
51
52
 
52
53
  class func externalFileAttributesForEntry(of type: Entry.EntryType, permissions: UInt16) -> UInt32 {
53
- var typeInt: UInt16
54
- switch type {
55
- case .file:
56
- typeInt = UInt16(S_IFREG)
57
- case .directory:
58
- typeInt = UInt16(S_IFDIR)
59
- case .symlink:
60
- typeInt = UInt16(S_IFLNK)
61
- }
54
+ let typeInt =
55
+ switch type {
56
+ case .file:
57
+ UInt16(S_IFREG)
58
+ case .directory:
59
+ UInt16(S_IFDIR)
60
+ case .symlink:
61
+ UInt16(S_IFLNK)
62
+ }
62
63
  var externalFileAttributes = UInt32(typeInt | UInt16(permissions))
63
64
  externalFileAttributes = (externalFileAttributes << 16)
64
65
  return externalFileAttributes
@@ -36,6 +36,10 @@ class CompositionLayer: CALayer, KeypathSearchable {
36
36
  "bounds" : NSNull(),
37
37
  "anchorPoint" : NSNull(),
38
38
  "sublayerTransform" : NSNull(),
39
+ "shadowOpacity" : NSNull(),
40
+ "shadowOffset" : NSNull(),
41
+ "shadowColor" : NSNull(),
42
+ "shadowRadius" : NSNull(),
39
43
  ]
40
44
 
41
45
  contentsLayer.anchorPoint = .zero
@@ -55,6 +59,14 @@ class CompositionLayer: CALayer, KeypathSearchable {
55
59
  contentsLayer.mask = maskLayer
56
60
  }
57
61
 
62
+ // There are two different drop shadow schemas, either using `DropShadowEffect` or `DropShadowStyle`.
63
+ // If both happen to be present, prefer the `DropShadowEffect` (which is the drop shadow schema
64
+ // supported on other platforms).
65
+ let dropShadowEffect = layer.effects.first(where: { $0 is DropShadowEffect }) as? DropShadowModel
66
+ let dropShadowStyle = layer.styles.first(where: { $0 is DropShadowStyle }) as? DropShadowModel
67
+ if let dropShadowModel = dropShadowEffect ?? dropShadowStyle {
68
+ layerEffectNodes.append(DropShadowNode(model: dropShadowModel))
69
+ }
58
70
  name = layer.name
59
71
  }
60
72
 
@@ -72,6 +84,7 @@ class CompositionLayer: CALayer, KeypathSearchable {
72
84
  keypathName = layer.keypathName
73
85
  childKeypaths = [transformNode.transformProperties]
74
86
  maskLayer = nil
87
+ layerEffectNodes = layer.layerEffectNodes
75
88
  super.init(layer: layer)
76
89
  }
77
90
 
@@ -96,6 +109,8 @@ class CompositionLayer: CALayer, KeypathSearchable {
96
109
  let startFrame: CGFloat
97
110
  let timeStretch: CGFloat
98
111
 
112
+ var layerEffectNodes: [LayerEffectNode] = []
113
+
99
114
  // MARK: Keypath Searchable
100
115
 
101
116
  let keypathName: String
@@ -142,6 +157,10 @@ class CompositionLayer: CALayer, KeypathSearchable {
142
157
  contentsLayer.opacity = transformNode.opacity
143
158
  contentsLayer.isHidden = !layerVisible
144
159
  layerDelegate?.frameUpdated(frame: frame)
160
+
161
+ for layerEffectNode in layerEffectNodes {
162
+ layerEffectNode.updateWithFrame(layer: self, frame: frame)
163
+ }
145
164
  }
146
165
 
147
166
  func displayContentsWithFrame(frame _: CGFloat, forceUpdates _: Bool) {
@@ -11,19 +11,19 @@ extension MaskMode {
11
11
  var usableMode: MaskMode {
12
12
  switch self {
13
13
  case .add:
14
- return .add
14
+ .add
15
15
  case .subtract:
16
- return .subtract
16
+ .subtract
17
17
  case .intersect:
18
- return .intersect
18
+ .intersect
19
19
  case .lighten:
20
- return .add
20
+ .add
21
21
  case .darken:
22
- return .darken
22
+ .darken
23
23
  case .difference:
24
- return .intersect
24
+ .intersect
25
25
  case .none:
26
- return .none
26
+ .none
27
27
  }
28
28
  }
29
29
  }
@@ -16,6 +16,7 @@ final class PreCompositionLayer: CompositionLayer {
16
16
  asset: PrecompAsset,
17
17
  layerImageProvider: LayerImageProvider,
18
18
  layerTextProvider: LayerTextProvider,
19
+ layerFontProvider: LayerFontProvider,
19
20
  textProvider: AnimationKeypathTextProvider,
20
21
  fontProvider: AnimationFontProvider,
21
22
  assetLibrary: AssetLibrary?,
@@ -38,6 +39,7 @@ final class PreCompositionLayer: CompositionLayer {
38
39
  assetLibrary: assetLibrary,
39
40
  layerImageProvider: layerImageProvider,
40
41
  layerTextProvider: layerTextProvider,
42
+ layerFontProvider: layerFontProvider,
41
43
  textProvider: textProvider,
42
44
  fontProvider: fontProvider,
43
45
  frameRate: frameRate,
@@ -77,6 +79,7 @@ final class PreCompositionLayer: CompositionLayer {
77
79
 
78
80
  layerImageProvider.addImageLayers(imageLayers)
79
81
  layerTextProvider.addTextLayers(textLayers)
82
+ layerFontProvider.addTextLayers(textLayers)
80
83
  }
81
84
 
82
85
  override init(layer: Any) {
@@ -16,22 +16,22 @@ extension TextJustification {
16
16
  var textAlignment: NSTextAlignment {
17
17
  switch self {
18
18
  case .left:
19
- return .left
19
+ .left
20
20
  case .right:
21
- return .right
21
+ .right
22
22
  case .center:
23
- return .center
23
+ .center
24
24
  }
25
25
  }
26
26
 
27
27
  var caTextAlignement: CATextLayerAlignmentMode {
28
28
  switch self {
29
29
  case .left:
30
- return .left
30
+ .left
31
31
  case .right:
32
- return .right
32
+ .right
33
33
  case .center:
34
- return .center
34
+ .center
35
35
  }
36
36
  }
37
37
  }
@@ -121,20 +121,24 @@ final class TextCompositionLayer: CompositionLayer {
121
121
  // Prior to Lottie 4.3.0 the Main Thread rendering engine always just used `LegacyAnimationTextProvider`
122
122
  // and called it with the `keypathName` (only the last path component of the full keypath).
123
123
  // Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider` instead if implemented.
124
- let textString: String
125
- if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
126
- textString = keypathTextValue
127
- } else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
128
- textString = legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
129
- } else {
130
- textString = text.text
131
- }
124
+ let textString: String =
125
+ if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
126
+ keypathTextValue
127
+ } else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
128
+ legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
129
+ } else {
130
+ text.text
131
+ }
132
132
 
133
133
  let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
134
134
  let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
135
135
  let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
136
136
  let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
137
137
  let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
138
+ let start = rootNode?.textOutputNode.start.flatMap { Int($0) }
139
+ let end = rootNode?.textOutputNode.end.flatMap { Int($0) }
140
+ let selectedRangeOpacity = rootNode?.textOutputNode.selectedRangeOpacity
141
+ let textRangeUnit = rootNode?.textAnimatorProperties.textRangeUnit
138
142
 
139
143
  // Set all of the text layer options
140
144
  textLayer.text = textString
@@ -143,6 +147,12 @@ final class TextCompositionLayer: CompositionLayer {
143
147
  textLayer.lineHeight = CGFloat(text.lineHeight)
144
148
  textLayer.tracking = tracking
145
149
 
150
+ // Configure the text animators
151
+ textLayer.start = start
152
+ textLayer.end = end
153
+ textLayer.textRangeUnit = textRangeUnit
154
+ textLayer.selectedRangeOpacity = selectedRangeOpacity
155
+
146
156
  if let fillColor = rootNode?.textOutputNode.fillColor {
147
157
  textLayer.fillColor = fillColor
148
158
  } else if let fillColor = text.fillColorData?.cgColorValue {
@@ -37,6 +37,7 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
37
37
  assetLibrary: animation.assetLibrary,
38
38
  layerImageProvider: layerImageProvider,
39
39
  layerTextProvider: layerTextProvider,
40
+ layerFontProvider: layerFontProvider,
40
41
  textProvider: textProvider,
41
42
  fontProvider: fontProvider,
42
43
  frameRate: CGFloat(animation.framerate),
@@ -127,16 +128,16 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
127
128
 
128
129
  public override func display() {
129
130
  guard Thread.isMainThread else { return }
130
- var newFrame: CGFloat
131
- if
132
- let animationKeys = animationKeys(),
133
- !animationKeys.isEmpty
134
- {
135
- newFrame = presentation()?.currentFrame ?? currentFrame
136
- } else {
137
- // We ignore the presentation's frame if there's no animation in the layer.
138
- newFrame = currentFrame
139
- }
131
+ var newFrame: CGFloat =
132
+ if
133
+ let animationKeys = animationKeys(),
134
+ !animationKeys.isEmpty
135
+ {
136
+ presentation()?.currentFrame ?? currentFrame
137
+ } else {
138
+ // We ignore the presentation's frame if there's no animation in the layer.
139
+ currentFrame
140
+ }
140
141
  if respectAnimationFrameRate {
141
142
  newFrame = floor(newFrame)
142
143
  }
@@ -14,6 +14,7 @@ extension [LayerModel] {
14
14
  assetLibrary: AssetLibrary?,
15
15
  layerImageProvider: LayerImageProvider,
16
16
  layerTextProvider: LayerTextProvider,
17
+ layerFontProvider: LayerFontProvider,
17
18
  textProvider: AnimationKeypathTextProvider,
18
19
  fontProvider: AnimationFontProvider,
19
20
  frameRate: CGFloat,
@@ -49,6 +50,7 @@ extension [LayerModel] {
49
50
  asset: precompAsset,
50
51
  layerImageProvider: layerImageProvider,
51
52
  layerTextProvider: layerTextProvider,
53
+ layerFontProvider: layerFontProvider,
52
54
  textProvider: textProvider,
53
55
  fontProvider: fontProvider,
54
56
  assetLibrary: assetLibrary,
@@ -39,7 +39,7 @@ final class CoreTextRenderLayer: CALayer {
39
39
  }
40
40
  }
41
41
 
42
- public var alignment: NSTextAlignment = .left {
42
+ public var alignment = NSTextAlignment.left {
43
43
  didSet {
44
44
  needsContentUpdate = true
45
45
  setNeedsLayout()
@@ -102,6 +102,40 @@ final class CoreTextRenderLayer: CALayer {
102
102
  }
103
103
  }
104
104
 
105
+ public var start: Int? {
106
+ didSet {
107
+ needsContentUpdate = true
108
+ setNeedsLayout()
109
+ setNeedsDisplay()
110
+ }
111
+ }
112
+
113
+ public var end: Int? {
114
+ didSet {
115
+ needsContentUpdate = true
116
+ setNeedsLayout()
117
+ setNeedsDisplay()
118
+ }
119
+ }
120
+
121
+ /// The type of unit to use when computing the `start` / `end` range within the text string
122
+ public var textRangeUnit: TextRangeUnit? {
123
+ didSet {
124
+ needsContentUpdate = true
125
+ setNeedsLayout()
126
+ setNeedsDisplay()
127
+ }
128
+ }
129
+
130
+ /// The opacity to apply to the range between `start` and `end`
131
+ public var selectedRangeOpacity: CGFloat? {
132
+ didSet {
133
+ needsContentUpdate = true
134
+ setNeedsLayout()
135
+ setNeedsDisplay()
136
+ }
137
+ }
138
+
105
139
  public func sizeToFit() {
106
140
  updateTextContent()
107
141
  bounds = drawingRect
@@ -137,19 +171,19 @@ final class CoreTextRenderLayer: CALayer {
137
171
 
138
172
  let drawingPath = CGPath(rect: drawingRect, transform: nil)
139
173
 
140
- let fillFrame: CTFrame?
141
- if let setter = fillFrameSetter {
142
- fillFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
143
- } else {
144
- fillFrame = nil
145
- }
174
+ let fillFrame: CTFrame? =
175
+ if let setter = fillFrameSetter {
176
+ CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
177
+ } else {
178
+ nil
179
+ }
146
180
 
147
- let strokeFrame: CTFrame?
148
- if let setter = strokeFrameSetter {
149
- strokeFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
150
- } else {
151
- strokeFrame = nil
152
- }
181
+ let strokeFrame: CTFrame? =
182
+ if let setter = strokeFrameSetter {
183
+ CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
184
+ } else {
185
+ nil
186
+ }
153
187
 
154
188
  // This fixes a vertical padding issue that arises when drawing some fonts.
155
189
  // For some reason some fonts, such as Helvetica draw with and ascender that is greater than the one reported by CTFontGetAscender.
@@ -176,14 +210,14 @@ final class CoreTextRenderLayer: CALayer {
176
210
 
177
211
  // MARK: Private
178
212
 
179
- private var drawingRect: CGRect = .zero
180
- private var drawingAnchor: CGPoint = .zero
213
+ private var drawingRect = CGRect.zero
214
+ private var drawingAnchor = CGPoint.zero
181
215
  private var fillFrameSetter: CTFramesetter?
182
216
  private var attributedString: NSAttributedString?
183
217
  private var strokeFrameSetter: CTFramesetter?
184
218
  private var needsContentUpdate = false
185
219
 
186
- // Draws Debug colors for the font alignment.
220
+ /// Draws Debug colors for the font alignment.
187
221
  private func drawDebug(_ ctx: CGContext) {
188
222
  if let font {
189
223
  let ascent = CTFontGetAscent(font)
@@ -259,7 +293,58 @@ final class CoreTextRenderLayer: CALayer {
259
293
  attributes[NSAttributedString.Key.foregroundColor] = fillColor
260
294
  }
261
295
 
262
- let attrString = NSAttributedString(string: text, attributes: attributes)
296
+ let attrString = NSMutableAttributedString(string: text, attributes: attributes)
297
+
298
+ // Apply the text animator within between the `start` and `end` indices
299
+ if let selectedRangeOpacity {
300
+ // The start and end of a text animator refer to the portions of the text
301
+ // where that animator is applies. In the schema these can be represented
302
+ // in absolute index value, or as percentages relative to the dynamic string length.
303
+ var startIndex: Int
304
+ var endIndex: Int
305
+
306
+ switch textRangeUnit ?? .percentage {
307
+ case .index:
308
+ startIndex = start ?? 0
309
+ endIndex = end ?? text.count
310
+
311
+ case .percentage:
312
+ let startPercentage = Double(start ?? 0) / 100
313
+ let endPercentage = Double(end ?? 100) / 100
314
+
315
+ startIndex = Int(round(Double(attrString.length) * startPercentage))
316
+ endIndex = Int(round(Double(attrString.length) * endPercentage))
317
+ }
318
+
319
+ // Carefully cap the indices, since passing invalid indices
320
+ // to `NSAttributedString` will crash the app.
321
+ startIndex = startIndex.clamp(0, attrString.length)
322
+ endIndex = endIndex.clamp(0, attrString.length)
323
+
324
+ // Make sure the end index actually comes after the start index
325
+ if endIndex < startIndex {
326
+ swap(&startIndex, &endIndex)
327
+ }
328
+
329
+ // Apply the `selectedRangeOpacity` to the current `fillColor` if provided
330
+ let textRangeColor: CGColor
331
+ if let fillColor {
332
+ if let (r, g, b) = fillColor.rgb {
333
+ textRangeColor = .rgba(r, g, b, selectedRangeOpacity)
334
+ } else {
335
+ LottieLogger.shared.warn("Could not convert color \(fillColor) to RGB values.")
336
+ textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
337
+ }
338
+ } else {
339
+ textRangeColor = .rgba(0, 0, 0, selectedRangeOpacity)
340
+ }
341
+
342
+ attrString.addAttribute(
343
+ NSAttributedString.Key.foregroundColor,
344
+ value: textRangeColor,
345
+ range: NSRange(location: startIndex, length: endIndex - startIndex))
346
+ }
347
+
263
348
  attributedString = attrString
264
349
 
265
350
  if fillColor != nil {
@@ -119,17 +119,17 @@ class LayerTransformNode: AnimatorNode {
119
119
  func rebuildOutputs(frame _: CGFloat) {
120
120
  opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
121
121
 
122
- let position: CGPoint
123
- if let point = transformProperties.position?.value.pointValue {
124
- position = point
125
- } else if
126
- let xPos = transformProperties.positionX?.value.cgFloatValue,
127
- let yPos = transformProperties.positionY?.value.cgFloatValue
128
- {
129
- position = CGPoint(x: xPos, y: yPos)
130
- } else {
131
- position = .zero
132
- }
122
+ let position: CGPoint =
123
+ if let point = transformProperties.position?.value.pointValue {
124
+ point
125
+ } else if
126
+ let xPos = transformProperties.positionX?.value.cgFloatValue,
127
+ let yPos = transformProperties.positionY?.value.cgFloatValue
128
+ {
129
+ CGPoint(x: xPos, y: yPos)
130
+ } else {
131
+ .zero
132
+ }
133
133
 
134
134
  localTransform = CATransform3D.makeTransform(
135
135
  anchor: transformProperties.anchor.value.pointValue,
@@ -49,10 +49,12 @@ extension [ShapeItem] {
49
49
  switch star.starType {
50
50
  case .none:
51
51
  continue
52
+
52
53
  case .polygon:
53
54
  let node = PolygonNode(parentNode: nodeTree.rootNode, star: star)
54
55
  nodeTree.rootNode = node
55
56
  nodeTree.childrenNodes.append(node)
57
+
56
58
  case .star:
57
59
  let node = StarNode(parentNode: nodeTree.rootNode, star: star)
58
60
  nodeTree.rootNode = node