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,76 @@
//
// TransformNodeOutput.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
import QuartzCore
class GroupOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?, rootNode: NodeOutput?) {
self.parent = parent
self.rootNode = rootNode
}
// MARK: Internal
let parent: NodeOutput?
let rootNode: NodeOutput?
var isEnabled = true
private(set) var outputPath: CGPath? = nil
private(set) var transform: CATransform3D = CATransform3DIdentity
func setTransform(_ xform: CATransform3D, forFrame _: CGFloat) {
transform = xform
outputPath = nil
}
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
guard isEnabled else {
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
outputPath = parent?.outputPath
return upstreamUpdates
}
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
if upstreamUpdates {
outputPath = nil
}
let rootUpdates = rootNode?.hasOutputUpdates(forFrame) ?? false
if rootUpdates {
outputPath = nil
}
var localUpdates = false
if outputPath == nil {
localUpdates = true
let newPath = CGMutablePath()
if let parentNode = parent, let parentPath = parentNode.outputPath {
/// First add parent path.
newPath.addPath(parentPath)
}
var xform = CATransform3DGetAffineTransform(transform)
if
let rootNode = rootNode,
let rootPath = rootNode.outputPath,
let xformedPath = rootPath.copy(using: &xform)
{
/// Now add root path. Note root path is transformed.
newPath.addPath(xformedPath)
}
outputPath = newPath
}
return upstreamUpdates || localUpdates
}
}
@@ -0,0 +1,47 @@
//
// PassThroughOutputNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
class PassThroughOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?) {
self.parent = parent
}
// MARK: Internal
let parent: NodeOutput?
var hasUpdate = false
var isEnabled = true
var outputPath: CGPath? {
if let parent = parent {
return parent.outputPath
}
return nil
}
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
/// Changes to this node do not affect downstream nodes.
let parentUpdate = parent?.hasOutputUpdates(forFrame) ?? false
/// Changes to upstream nodes do, however, affect this nodes state.
hasUpdate = hasUpdate || parentUpdate
return parentUpdate
}
func hasRenderUpdates(_ forFrame: CGFloat) -> Bool {
/// Return true if there are upstream updates or if this node has updates
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
hasUpdate = hasUpdate || upstreamUpdates
return hasUpdate
}
}
@@ -0,0 +1,90 @@
//
// PathNodeOutput.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A node that has an output of a BezierPath
class PathOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: NodeOutput?) {
self.parent = parent
}
// MARK: Internal
let parent: NodeOutput?
fileprivate(set) var outputPath: CGPath? = nil
var lastUpdateFrame: CGFloat? = nil
var lastPathBuildFrame: CGFloat? = nil
var isEnabled = true
fileprivate(set) var totalLength: CGFloat = 0
fileprivate(set) var pathObjects: [CompoundBezierPath] = []
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
guard isEnabled else {
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
outputPath = parent?.outputPath
return upstreamUpdates
}
/// Ask if parent was updated
let upstreamUpdates = parent?.hasOutputUpdates(forFrame) ?? false
/// If parent was updated and the path hasn't been built for this frame, clear the path.
if upstreamUpdates && lastPathBuildFrame != forFrame {
outputPath = nil
}
if outputPath == nil {
/// If the path is clear, build the new path.
lastPathBuildFrame = forFrame
let newPath = CGMutablePath()
if let parentNode = parent, let parentPath = parentNode.outputPath {
newPath.addPath(parentPath)
}
for path in pathObjects {
for subPath in path.paths {
newPath.addPath(subPath.cgPath())
}
}
outputPath = newPath
}
/// Return true if there were upstream updates or if this node was updated.
return upstreamUpdates || (lastUpdateFrame == forFrame)
}
@discardableResult
func removePaths(updateFrame: CGFloat?) -> [CompoundBezierPath] {
lastUpdateFrame = updateFrame
let returnPaths = pathObjects
outputPath = nil
totalLength = 0
pathObjects = []
return returnPaths
}
func setPath(_ path: BezierPath, updateFrame: CGFloat) {
lastUpdateFrame = updateFrame
outputPath = nil
totalLength = path.length
pathObjects = [CompoundBezierPath(path: path)]
}
func appendPath(_ path: CompoundBezierPath, updateFrame: CGFloat) {
lastUpdateFrame = updateFrame
outputPath = nil
totalLength = totalLength + path.length
pathObjects.append(path)
}
}
@@ -0,0 +1,71 @@
//
// FillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
import QuartzCore
extension FillRule {
var cgFillRule: CGPathFillRule {
switch self {
case .evenOdd:
return .evenOdd
default:
return .winding
}
}
var caFillRule: CAShapeLayerFillRule {
switch self {
case .evenOdd:
return CAShapeLayerFillRule.evenOdd
default:
return CAShapeLayerFillRule.nonZero
}
}
}
// MARK: - FillRenderer
/// A rendered for a Path Fill
final class FillRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = false
var color: CGColor? {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var fillRule: FillRule = .none {
didSet {
hasUpdate = true
}
}
func render(_: CGContext) {
// do nothing
}
func setupSublayers(layer _: CAShapeLayer) {
// do nothing
}
func updateShapeLayer(layer: CAShapeLayer) {
layer.fillColor = color
layer.opacity = Float(opacity)
layer.fillRule = fillRule.caFillRule
hasUpdate = false
}
}
@@ -0,0 +1,242 @@
//
// GradientFillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
// MARK: - GradientFillLayer
private final class GradientFillLayer: CALayer {
var start: CGPoint = .zero {
didSet {
setNeedsDisplay()
}
}
var numberOfColors = 0 {
didSet {
setNeedsDisplay()
}
}
var colors: [CGFloat] = [] {
didSet {
setNeedsDisplay()
}
}
var end: CGPoint = .zero {
didSet {
setNeedsDisplay()
}
}
var type: GradientType = .none {
didSet {
setNeedsDisplay()
}
}
override func draw(in ctx: CGContext) {
var alphaColors = [CGColor]()
var alphaLocations = [CGFloat]()
var gradientColors = [CGColor]()
var colorLocations = [CGFloat]()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let maskColorSpace = CGColorSpaceCreateDeviceGray()
for i in 0..<numberOfColors {
let ix = i * 4
if
colors.count > ix, let color = CGColor(
colorSpace: colorSpace,
components: [colors[ix + 1], colors[ix + 2], colors[ix + 3], 1])
{
gradientColors.append(color)
colorLocations.append(colors[ix])
}
}
var drawMask = false
for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) {
let alpha = colors[i + 1]
if alpha < 1 {
drawMask = true
}
if let color = CGColor(colorSpace: maskColorSpace, components: [alpha, 1]) {
alphaLocations.append(colors[i])
alphaColors.append(color)
}
}
/// First draw a mask is necessary.
if drawMask {
guard
let maskGradient = CGGradient(
colorsSpace: maskColorSpace,
colors: alphaColors as CFArray,
locations: alphaLocations),
let maskContext = CGContext(
data: nil,
width: ctx.width,
height: ctx.height,
bitsPerComponent: 8,
bytesPerRow: ctx.width,
space: maskColorSpace,
bitmapInfo: 0) else { return }
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(maskContext.height))
maskContext.concatenate(flipVertical)
maskContext.concatenate(ctx.ctm)
if type == .linear {
maskContext.drawLinearGradient(
maskGradient,
start: start,
end: end,
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
maskContext.drawRadialGradient(
maskGradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
/// Clips the gradient
if let alphaMask = maskContext.makeImage() {
ctx.clip(to: ctx.boundingBoxOfClipPath, mask: alphaMask)
}
}
/// Now draw the gradient
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: colorLocations)
else { return }
if type == .linear {
ctx.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
ctx.drawRadialGradient(
gradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
}
}
// MARK: - GradientFillRenderer
/// A rendered for a Path Fill
final class GradientFillRenderer: PassThroughOutputNode, Renderable {
// MARK: Lifecycle
override init(parent: NodeOutput?) {
super.init(parent: parent)
maskLayer.fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1])
gradientLayer.mask = maskLayer
maskLayer.actions = [
"startPoint" : NSNull(),
"endPoint" : NSNull(),
"opacity" : NSNull(),
"locations" : NSNull(),
"colors" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"isRadial" : NSNull(),
"path" : NSNull(),
]
gradientLayer.actions = maskLayer.actions
}
// MARK: Internal
var shouldRenderInContext = false
var start: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var numberOfColors = 0 {
didSet {
hasUpdate = true
}
}
var colors: [CGFloat] = [] {
didSet {
hasUpdate = true
}
}
var end: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var type: GradientType = .none {
didSet {
hasUpdate = true
}
}
func render(_: CGContext) {
// do nothing
}
func setupSublayers(layer: CAShapeLayer) {
layer.addSublayer(gradientLayer)
layer.fillColor = nil
}
func updateShapeLayer(layer: CAShapeLayer) {
hasUpdate = false
guard let path = layer.path else {
return
}
let frame = path.boundingBox
let anchor = CGPoint(
x: -frame.origin.x / frame.size.width,
y: -frame.origin.y / frame.size.height)
maskLayer.path = path
maskLayer.bounds = path.boundingBox
maskLayer.anchorPoint = anchor
gradientLayer.bounds = maskLayer.bounds
gradientLayer.anchorPoint = anchor
// setup gradient properties
gradientLayer.start = start
gradientLayer.end = end
gradientLayer.numberOfColors = numberOfColors
gradientLayer.colors = colors
gradientLayer.opacity = Float(opacity)
gradientLayer.type = type
}
// MARK: Private
private let gradientLayer = GradientFillLayer()
private let maskLayer = CAShapeLayer()
}
@@ -0,0 +1,66 @@
//
// GradientStrokeRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
// MARK: - Renderer
final class GradientStrokeRenderer: PassThroughOutputNode, Renderable {
// MARK: Lifecycle
override init(parent: NodeOutput?) {
strokeRender = StrokeRenderer(parent: nil)
gradientRender = LegacyGradientFillRenderer(parent: nil)
strokeRender.color = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1])
super.init(parent: parent)
}
// MARK: Internal
var shouldRenderInContext = true
let strokeRender: StrokeRenderer
let gradientRender: LegacyGradientFillRenderer
override func hasOutputUpdates(_ forFrame: CGFloat) -> Bool {
let updates = super.hasOutputUpdates(forFrame)
return updates || strokeRender.hasUpdate || gradientRender.hasUpdate
}
func updateShapeLayer(layer _: CAShapeLayer) {
/// Not Applicable
}
func setupSublayers(layer _: CAShapeLayer) {
/// Not Applicable
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
strokeRender.hasUpdate = false
hasUpdate = false
gradientRender.hasUpdate = false
strokeRender.setupForStroke(inContext)
inContext.replacePathWithStrokedPath()
/// Now draw the gradient.
gradientRender.render(inContext)
}
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
strokeRender.renderBoundsFor(boundingBox)
}
}
@@ -0,0 +1,153 @@
//
// LegacyGradientFillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// A rendered for a Path Fill
final class LegacyGradientFillRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = true
var start: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var numberOfColors = 0 {
didSet {
hasUpdate = true
}
}
var colors: [CGFloat] = [] {
didSet {
hasUpdate = true
}
}
var end: CGPoint = .zero {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var type: GradientType = .none {
didSet {
hasUpdate = true
}
}
func updateShapeLayer(layer _: CAShapeLayer) {
// Not applicable
}
func setupSublayers(layer _: CAShapeLayer) {
// Not applicable
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
hasUpdate = false
var alphaColors = [CGColor]()
var alphaLocations = [CGFloat]()
var gradientColors = [CGColor]()
var colorLocations = [CGFloat]()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let maskColorSpace = CGColorSpaceCreateDeviceGray()
for i in 0..<numberOfColors {
let ix = i * 4
if
colors.count > ix, let color = CGColor(
colorSpace: colorSpace,
components: [colors[ix + 1], colors[ix + 2], colors[ix + 3], 1])
{
gradientColors.append(color)
colorLocations.append(colors[ix])
}
}
var drawMask = false
for i in stride(from: numberOfColors * 4, to: colors.endIndex, by: 2) {
let alpha = colors[i + 1]
if alpha < 1 {
drawMask = true
}
if let color = CGColor(colorSpace: maskColorSpace, components: [alpha, 1]) {
alphaLocations.append(colors[i])
alphaColors.append(color)
}
}
inContext.setAlpha(opacity)
inContext.clip()
/// First draw a mask is necessary.
if drawMask {
guard
let maskGradient = CGGradient(
colorsSpace: maskColorSpace,
colors: alphaColors as CFArray,
locations: alphaLocations),
let maskContext = CGContext(
data: nil,
width: inContext.width,
height: inContext.height,
bitsPerComponent: 8,
bytesPerRow: inContext.width,
space: maskColorSpace,
bitmapInfo: 0) else { return }
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(maskContext.height))
maskContext.concatenate(flipVertical)
maskContext.concatenate(inContext.ctm)
if type == .linear {
maskContext.drawLinearGradient(
maskGradient,
start: start,
end: end,
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
maskContext.drawRadialGradient(
maskGradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
/// Clips the gradient
if let alphaMask = maskContext.makeImage() {
inContext.clip(to: inContext.boundingBoxOfClipPath, mask: alphaMask)
}
}
/// Now draw the gradient
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors as CFArray, locations: colorLocations)
else { return }
if type == .linear {
inContext.drawLinearGradient(gradient, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else {
inContext.drawRadialGradient(
gradient,
startCenter: start,
startRadius: 0,
endCenter: start,
endRadius: start.distanceTo(end),
options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
}
}
@@ -0,0 +1,166 @@
//
// StrokeRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
extension LineJoin {
var cgLineJoin: CGLineJoin {
switch self {
case .bevel:
return .bevel
case .none:
return .miter
case .miter:
return .miter
case .round:
return .round
}
}
var caLineJoin: CAShapeLayerLineJoin {
switch self {
case .none:
return CAShapeLayerLineJoin.miter
case .miter:
return CAShapeLayerLineJoin.miter
case .round:
return CAShapeLayerLineJoin.round
case .bevel:
return CAShapeLayerLineJoin.bevel
}
}
}
extension LineCap {
var cgLineCap: CGLineCap {
switch self {
case .none:
return .butt
case .butt:
return .butt
case .round:
return .round
case .square:
return .square
}
}
var caLineCap: CAShapeLayerLineCap {
switch self {
case .none:
return CAShapeLayerLineCap.butt
case .butt:
return CAShapeLayerLineCap.butt
case .round:
return CAShapeLayerLineCap.round
case .square:
return CAShapeLayerLineCap.square
}
}
}
// MARK: - StrokeRenderer
/// A rendered that renders a stroke on a path.
final class StrokeRenderer: PassThroughOutputNode, Renderable {
var shouldRenderInContext = false
var color: CGColor? {
didSet {
hasUpdate = true
}
}
var opacity: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var width: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var miterLimit: CGFloat = 0 {
didSet {
hasUpdate = true
}
}
var lineCap: LineCap = .none {
didSet {
hasUpdate = true
}
}
var lineJoin: LineJoin = .none {
didSet {
hasUpdate = true
}
}
var dashPhase: CGFloat? {
didSet {
hasUpdate = true
}
}
var dashLengths: [CGFloat]? {
didSet {
hasUpdate = true
}
}
func setupSublayers(layer _: CAShapeLayer) {
// empty
}
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
boundingBox.insetBy(dx: -width, dy: -width)
}
func setupForStroke(_ inContext: CGContext) {
inContext.setLineWidth(width)
inContext.setMiterLimit(miterLimit)
inContext.setLineCap(lineCap.cgLineCap)
inContext.setLineJoin(lineJoin.cgLineJoin)
if let dashPhase = dashPhase, let lengths = dashLengths {
inContext.setLineDash(phase: dashPhase, lengths: lengths)
} else {
inContext.setLineDash(phase: 0, lengths: [])
}
}
func render(_ inContext: CGContext) {
guard inContext.path != nil && inContext.path!.isEmpty == false else {
return
}
guard let color = color else { return }
hasUpdate = false
setupForStroke(inContext)
inContext.setAlpha(opacity)
inContext.setStrokeColor(color)
inContext.strokePath()
}
func updateShapeLayer(layer: CAShapeLayer) {
layer.strokeColor = color
layer.opacity = Float(opacity)
layer.lineWidth = width
layer.lineJoin = lineJoin.caLineJoin
layer.lineCap = lineCap.caLineCap
layer.lineDashPhase = dashPhase ?? 0
layer.fillColor = nil
if let dashPattern = dashLengths {
layer.lineDashPattern = dashPattern.map({ NSNumber(value: Double($0)) })
}
}
}