Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,47 @@
// Created by Jianjun Wu on 2022/5/12.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import CoreGraphics
import Foundation
// MARK: - CachedImageProvider
private final class CachedImageProvider: AnimationImageProvider {
// MARK: Lifecycle
/// Initializes an image provider with an image provider
///
/// - Parameter imageProvider: The provider to load image from asset
///
public init(imageProvider: AnimationImageProvider) {
self.imageProvider = imageProvider
}
// MARK: Public
public func imageForAsset(asset: ImageAsset) -> CGImage? {
if let image = imageCache.object(forKey: asset.id as NSString) {
return image
}
if let image = imageProvider.imageForAsset(asset: asset) {
imageCache.setObject(image, forKey: asset.id as NSString)
return image
}
return nil
}
// MARK: Internal
let imageCache: NSCache<NSString, CGImage> = .init()
let imageProvider: AnimationImageProvider
}
extension AnimationImageProvider {
/// Create a cache enabled image provider which will reuse the asset image with the same asset id
/// It wraps the current provider as image loader, and uses `NSCache` to cache the images for resue.
/// The cache will be reset when the `animation` is reset.
var cachedImageProvider: AnimationImageProvider {
CachedImageProvider(imageProvider: self)
}
}
@@ -0,0 +1,90 @@
//
// CompositionLayersInitializer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import CoreGraphics
import Foundation
extension Array where Element == LayerModel {
func initializeCompositionLayers(
assetLibrary: AssetLibrary?,
layerImageProvider: LayerImageProvider,
textProvider: AnimationTextProvider,
fontProvider: AnimationFontProvider,
frameRate: CGFloat) -> [CompositionLayer]
{
var compositionLayers = [CompositionLayer]()
var layerMap = [Int : CompositionLayer]()
/// Organize the assets into a dictionary of [ID : ImageAsset]
var childLayers = [LayerModel]()
for layer in self {
if layer.hidden == true {
let genericLayer = NullCompositionLayer(layer: layer)
compositionLayers.append(genericLayer)
layerMap[layer.index] = genericLayer
} else if let shapeLayer = layer as? ShapeLayerModel {
let shapeContainer = ShapeCompositionLayer(shapeLayer: shapeLayer)
compositionLayers.append(shapeContainer)
layerMap[layer.index] = shapeContainer
} else if let solidLayer = layer as? SolidLayerModel {
let solidContainer = SolidCompositionLayer(solid: solidLayer)
compositionLayers.append(solidContainer)
layerMap[layer.index] = solidContainer
} else if
let precompLayer = layer as? PreCompLayerModel,
let assetLibrary = assetLibrary,
let precompAsset = assetLibrary.precompAssets[precompLayer.referenceID]
{
let precompContainer = PreCompositionLayer(
precomp: precompLayer,
asset: precompAsset,
layerImageProvider: layerImageProvider,
textProvider: textProvider,
fontProvider: fontProvider,
assetLibrary: assetLibrary,
frameRate: frameRate)
compositionLayers.append(precompContainer)
layerMap[layer.index] = precompContainer
} else if
let imageLayer = layer as? ImageLayerModel,
let assetLibrary = assetLibrary,
let imageAsset = assetLibrary.imageAssets[imageLayer.referenceID]
{
let imageContainer = ImageCompositionLayer(
imageLayer: imageLayer,
size: CGSize(width: imageAsset.width, height: imageAsset.height))
compositionLayers.append(imageContainer)
layerMap[layer.index] = imageContainer
} else if let textLayer = layer as? TextLayerModel {
let textContainer = TextCompositionLayer(textLayer: textLayer, textProvider: textProvider, fontProvider: fontProvider)
compositionLayers.append(textContainer)
layerMap[layer.index] = textContainer
} else {
let genericLayer = NullCompositionLayer(layer: layer)
compositionLayers.append(genericLayer)
layerMap[layer.index] = genericLayer
}
if layer.parent != nil {
childLayers.append(layer)
}
}
/// Now link children with their parents
for layerModel in childLayers {
if let parentID = layerModel.parent {
let childLayer = layerMap[layerModel.index]
let parentLayer = layerMap[parentID]
childLayer?.transformNode.parentNode = parentLayer?.transformNode
}
}
return compositionLayers
}
}
@@ -0,0 +1,320 @@
//
// TextLayer.swift
// Pods
//
// Created by Brandon Withrow on 8/3/20.
//
import CoreGraphics
import CoreText
import Foundation
import QuartzCore
/// Needed for NSMutableParagraphStyle...
#if os(OSX)
import AppKit
#else
import UIKit
#endif
// MARK: - CoreTextRenderLayer
/// A CALayer subclass that renders text content using CoreText
final class CoreTextRenderLayer: CALayer {
// MARK: Public
public var text: String? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var font: CTFont? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var alignment: NSTextAlignment = .left {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var lineHeight: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var tracking: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var fillColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeColor: CGColor? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeWidth: CGFloat = 0 {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public var strokeOnTop = false {
didSet {
setNeedsLayout()
setNeedsDisplay()
}
}
public var preferredSize: CGSize? {
didSet {
needsContentUpdate = true
setNeedsLayout()
setNeedsDisplay()
}
}
public func sizeToFit() {
updateTextContent()
bounds = drawingRect
anchorPoint = drawingAnchor
setNeedsLayout()
setNeedsDisplay()
}
// MARK: Internal
override func action(forKey _: String) -> CAAction? {
nil
}
override func draw(in ctx: CGContext) {
guard let attributedString = attributedString else { return }
updateTextContent()
guard fillFrameSetter != nil || strokeFrameSetter != nil else { return }
ctx.textMatrix = .identity
ctx.setAllowsAntialiasing(true)
ctx.setAllowsFontSubpixelPositioning(true)
ctx.setAllowsFontSubpixelQuantization(true)
ctx.setShouldAntialias(true)
ctx.setShouldSubpixelPositionFonts(true)
ctx.setShouldSubpixelQuantizeFonts(true)
if contentsAreFlipped() {
ctx.translateBy(x: 0, y: drawingRect.height)
ctx.scaleBy(x: 1.0, y: -1.0)
}
let drawingPath = CGPath(rect: drawingRect, transform: nil)
let fillFrame: CTFrame?
if let setter = fillFrameSetter {
fillFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
fillFrame = nil
}
let strokeFrame: CTFrame?
if let setter = strokeFrameSetter {
strokeFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, attributedString.length), drawingPath, nil)
} else {
strokeFrame = nil
}
if !strokeOnTop, let strokeFrame = strokeFrame {
CTFrameDraw(strokeFrame, ctx)
}
if let fillFrame = fillFrame {
CTFrameDraw(fillFrame, ctx)
}
if strokeOnTop, let strokeFrame = strokeFrame {
CTFrameDraw(strokeFrame, ctx)
}
}
// MARK: Private
private var drawingRect: CGRect = .zero
private var drawingAnchor: CGPoint = .zero
private var fillFrameSetter: CTFramesetter?
private var attributedString: NSAttributedString?
private var strokeFrameSetter: CTFramesetter?
private var needsContentUpdate = false
// Draws Debug colors for the font alignment.
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
private func drawDebug(_ ctx: CGContext) {
if let font = font {
let ascent = CTFontGetAscent(font)
let descent = CTFontGetDescent(font)
let capHeight = CTFontGetCapHeight(font)
let leading = CTFontGetLeading(font)
// Ascent Red
ctx.setFillColor(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: 0, width: drawingRect.width, height: ascent))
// Descent Blue
ctx.setFillColor(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent, width: drawingRect.width, height: descent))
// Leading Yellow
ctx.setFillColor(CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent + descent, width: drawingRect.width, height: leading))
// Cap height Green
ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 0.5))
ctx.fill(CGRect(x: 0, y: ascent - capHeight, width: drawingRect.width, height: capHeight))
if drawingRect.height - ascent + descent + leading > 0 {
// Remainder
ctx.setFillColor(CGColor(srgbRed: 0, green: 1, blue: 1, alpha: 0.5))
ctx
.fill(CGRect(
x: 0,
y: ascent + descent + leading,
width: drawingRect.width,
height: drawingRect.height - ascent + descent + leading))
}
}
}
private func updateTextContent() {
guard needsContentUpdate else { return }
needsContentUpdate = false
guard let font = font, let text = text, text.count > 0, fillColor != nil || strokeColor != nil else {
drawingRect = .zero
drawingAnchor = .zero
attributedString = nil
fillFrameSetter = nil
strokeFrameSetter = nil
return
}
// Get Font properties
let ascent = CTFontGetAscent(font)
let descent = CTFontGetDescent(font)
let capHeight = CTFontGetCapHeight(font)
let leading = CTFontGetLeading(font)
let minLineHeight = -(ascent + descent + leading)
// Calculate line spacing
let lineSpacing = max(CGFloat(minLineHeight) + lineHeight, CGFloat(minLineHeight))
// Build Attributes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.lineHeightMultiple = 1
paragraphStyle.maximumLineHeight = ascent + descent + leading
paragraphStyle.alignment = alignment
paragraphStyle.lineBreakMode = NSLineBreakMode.byWordWrapping
var attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.ligature: 0,
NSAttributedString.Key.font: font,
NSAttributedString.Key.kern: tracking,
NSAttributedString.Key.paragraphStyle: paragraphStyle,
]
if let fillColor = fillColor {
attributes[NSAttributedString.Key.foregroundColor] = fillColor
}
let attrString = NSAttributedString(string: text, attributes: attributes)
attributedString = attrString
if fillColor != nil {
let setter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
fillFrameSetter = setter
} else {
fillFrameSetter = nil
}
if let strokeColor = strokeColor {
attributes[NSAttributedString.Key.foregroundColor] = nil
attributes[NSAttributedString.Key.strokeWidth] = strokeWidth
attributes[NSAttributedString.Key.strokeColor] = strokeColor
let strokeAttributedString = NSAttributedString(string: text, attributes: attributes)
strokeFrameSetter = CTFramesetterCreateWithAttributedString(strokeAttributedString as CFAttributedString)
} else {
strokeFrameSetter = nil
strokeWidth = 0
}
guard let setter = fillFrameSetter ?? strokeFrameSetter else {
return
}
// Calculate drawing size and anchor offset
let textAnchor: CGPoint
if let preferredSize = preferredSize {
drawingRect = CGRect(origin: .zero, size: preferredSize)
drawingRect.size.height += (ascent - capHeight)
drawingRect.size.height += descent
textAnchor = CGPoint(x: 0, y: ascent - capHeight)
} else {
let size = CTFramesetterSuggestFrameSizeWithConstraints(
setter,
CFRange(location: 0, length: attrString.length),
nil,
CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
nil)
switch alignment {
case .left:
textAnchor = CGPoint(x: 0, y: ascent)
case .right:
textAnchor = CGPoint(x: size.width, y: ascent)
case .center:
textAnchor = CGPoint(x: size.width * 0.5, y: ascent)
default:
textAnchor = .zero
}
drawingRect = CGRect(
x: 0,
y: 0,
width: ceil(size.width),
height: ceil(size.height))
}
// Now Calculate Anchor
drawingAnchor = CGPoint(
x: textAnchor.x.remap(fromLow: 0, fromHigh: drawingRect.size.width, toLow: 0, toHigh: 1),
y: textAnchor.y.remap(fromLow: 0, fromHigh: drawingRect.size.height, toLow: 0, toHigh: 1))
if fillFrameSetter != nil && strokeFrameSetter != nil {
drawingRect.size.width += strokeWidth
drawingRect.size.height += strokeWidth
}
}
}
@@ -0,0 +1,58 @@
//
// InvertedMatteLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/28/19.
//
import Foundation
import QuartzCore
/// A layer that inverses the alpha output of its input layer.
///
/// WARNING: This is experimental and probably not very performant.
final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
// MARK: Lifecycle
init(inputMatte: CompositionLayer) {
self.inputMatte = inputMatte
super.init()
inputMatte.layerDelegate = self
anchorPoint = .zero
bounds = inputMatte.bounds
setNeedsDisplay()
}
override init(layer: Any) {
guard let layer = layer as? InvertedMatteLayer else {
fatalError("init(layer:) wrong class.")
}
inputMatte = nil
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
let inputMatte: CompositionLayer?
let wrapperLayer = CALayer()
func frameUpdated(frame _: CGFloat) {
displayIfNeeded()
}
override func draw(in ctx: CGContext) {
guard let inputMatte = inputMatte else { return }
guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])
else { return }
ctx.setFillColor(fillColor)
ctx.fill(bounds)
ctx.setBlendMode(.destinationOut)
inputMatte.render(in: ctx)
}
}
@@ -0,0 +1,41 @@
//
// LayerFontProvider.swift
// Lottie
//
// Created by Brandon Withrow on 8/5/20.
// Copyright © 2020 YurtvilleProds. All rights reserved.
//
import Foundation
/// Connects a LottieFontProvider to a group of text layers
final class LayerFontProvider {
// MARK: Lifecycle
init(fontProvider: AnimationFontProvider) {
self.fontProvider = fontProvider
textLayers = []
reloadTexts()
}
// MARK: Internal
private(set) var textLayers: [TextCompositionLayer]
var fontProvider: AnimationFontProvider {
didSet {
reloadTexts()
}
}
func addTextLayers(_ layers: [TextCompositionLayer]) {
textLayers += layers
}
func reloadTexts() {
textLayers.forEach {
$0.fontProvider = fontProvider
}
}
}
@@ -0,0 +1,53 @@
//
// LayerImageProvider.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import Foundation
/// Connects a LottieImageProvider to a group of image layers
final class LayerImageProvider {
// MARK: Lifecycle
init(imageProvider: AnimationImageProvider, assets: [String: ImageAsset]?) {
self.imageProvider = imageProvider
imageLayers = [ImageCompositionLayer]()
if let assets = assets {
imageAssets = assets
} else {
imageAssets = [:]
}
reloadImages()
}
// MARK: Internal
private(set) var imageLayers: [ImageCompositionLayer]
let imageAssets: [String: ImageAsset]
var imageProvider: AnimationImageProvider {
didSet {
reloadImages()
}
}
func addImageLayers(_ layers: [ImageCompositionLayer]) {
for layer in layers {
if imageAssets[layer.imageReferenceID] != nil {
/// Found a linking asset in our asset library. Add layer
imageLayers.append(layer)
}
}
}
func reloadImages() {
for imageLayer in imageLayers {
if let asset = imageAssets[imageLayer.imageReferenceID] {
imageLayer.image = imageProvider.imageForAsset(asset: asset)
}
}
}
}
@@ -0,0 +1,40 @@
//
// LayerTextProvider.swift
// lottie-ios-iOS
//
// Created by Alexandr Goncharov on 07/06/2019.
//
import Foundation
/// Connects a LottieTextProvider to a group of text layers
final class LayerTextProvider {
// MARK: Lifecycle
init(textProvider: AnimationTextProvider) {
self.textProvider = textProvider
textLayers = []
reloadTexts()
}
// MARK: Internal
private(set) var textLayers: [TextCompositionLayer]
var textProvider: AnimationTextProvider {
didSet {
reloadTexts()
}
}
func addTextLayers(_ layers: [TextCompositionLayer]) {
textLayers += layers
}
func reloadTexts() {
textLayers.forEach {
$0.textProvider = textProvider
}
}
}
@@ -0,0 +1,144 @@
//
// LayerTransformPropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - LayerTransformProperties
final class LayerTransformProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(transform: Transform) {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
var propertyMap: [String: AnyNodeProperty] = [
"Anchor Point" : anchor,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity,
]
if
let positionKeyframesX = transform.positionX?.keyframes,
let positionKeyframesY = transform.positionY?.keyframes
{
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
propertyMap["X Position"] = xPosition
propertyMap["Y Position"] = yPosition
positionX = xPosition
positionY = yPosition
position = nil
} else if let positionKeyframes = transform.position?.keyframes {
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
propertyMap["Position"] = position
self.position = position
positionX = nil
positionY = nil
} else {
position = nil
positionY = nil
positionX = nil
}
keypathProperties = propertyMap
properties = Array(propertyMap.values)
}
// MARK: Internal
let keypathProperties: [String: AnyNodeProperty]
var keypathName = "Transform"
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let position: NodeProperty<Vector3D>?
let positionX: NodeProperty<Vector1D>?
let positionY: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>
var childKeypaths: [KeypathSearchable] {
[]
}
}
// MARK: - LayerTransformNode
class LayerTransformNode: AnimatorNode {
// MARK: Lifecycle
init(transform: Transform) {
transformProperties = LayerTransformProperties(transform: transform)
}
// MARK: Internal
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
let transformProperties: LayerTransformProperties
var parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
var opacity: Float = 1
var localTransform: CATransform3D = CATransform3DIdentity
var globalTransform: CATransform3D = CATransform3DIdentity
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
transformProperties
}
func shouldRebuildOutputs(frame _: CGFloat) -> Bool {
hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame _: CGFloat) {
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
let position: CGPoint
if let point = transformProperties.position?.value.pointValue {
position = point
} else if
let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue
{
position = CGPoint(x: xPos, y: yPos)
} else {
position = .zero
}
localTransform = CATransform3D.makeTransform(
anchor: transformProperties.anchor.value.pointValue,
position: position,
scale: transformProperties.scale.value.sizeValue,
rotation: transformProperties.rotation.value.cgFloatValue,
skew: nil,
skewAxis: nil)
if let parentNode = parentNode as? LayerTransformNode {
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
} else {
globalTransform = localTransform
}
}
}