GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,97 @@
//
// ItemsExtension.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import Foundation
// MARK: - NodeTree
final class NodeTree {
var rootNode: AnimatorNode? = nil
var transform: ShapeTransform? = nil
var renderContainers: [ShapeContainerLayer] = []
var paths: [PathOutputNode] = []
var childrenNodes: [AnimatorNode] = []
}
extension Array where Element == ShapeItem {
func initializeNodeTree() -> NodeTree {
let nodeTree = NodeTree()
for item in self {
guard item.hidden == false, item.type != .unknown else { continue }
if let fill = item as? Fill {
let node = FillNode(parentNode: nodeTree.rootNode, fill: fill)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let stroke = item as? Stroke {
let node = StrokeNode(parentNode: nodeTree.rootNode, stroke: stroke)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let gradientFill = item as? GradientFill {
let node = GradientFillNode(parentNode: nodeTree.rootNode, gradientFill: gradientFill)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let gradientStroke = item as? GradientStroke {
let node = GradientStrokeNode(parentNode: nodeTree.rootNode, gradientStroke: gradientStroke)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let ellipse = item as? Ellipse {
let node = EllipseNode(parentNode: nodeTree.rootNode, ellipse: ellipse)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let rect = item as? Rectangle {
let node = RectangleNode(parentNode: nodeTree.rootNode, rectangle: rect)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let star = item as? Star {
switch star.starType {
case .none:
continue
case .polygon:
let node = PolygonNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
case .star:
let node = StarNode(parentNode: nodeTree.rootNode, star: star)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
}
} else if let shape = item as? Shape {
let node = ShapeNode(parentNode: nodeTree.rootNode, shape: shape)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let trim = item as? Trim {
let node = TrimPathNode(parentNode: nodeTree.rootNode, trim: trim, upstreamPaths: nodeTree.paths)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
} else if let xform = item as? ShapeTransform {
nodeTree.transform = xform
continue
} else if let group = item as? Group {
let tree = group.items.initializeNodeTree()
let node = GroupNode(name: group.name, parentNode: nodeTree.rootNode, tree: tree)
nodeTree.rootNode = node
nodeTree.childrenNodes.append(node)
/// Now add all child paths to current tree
nodeTree.paths.append(contentsOf: tree.paths)
nodeTree.renderContainers.append(node.container)
}
if let pathNode = nodeTree.rootNode as? PathNode {
//// Add path container to the node tree
nodeTree.paths.append(pathNode.pathOutput)
}
if let renderNode = nodeTree.rootNode as? RenderNode {
nodeTree.renderContainers.append(ShapeRenderLayer(renderer: renderNode.renderer))
}
}
return nodeTree
}
}
@@ -0,0 +1,55 @@
//
// NodeProperty.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A node property that holds a reference to a T ValueProvider and a T ValueContainer.
class NodeProperty<T>: AnyNodeProperty {
// MARK: Lifecycle
init(provider: AnyValueProvider) {
valueProvider = provider
originalValueProvider = valueProvider
typedContainer = ValueContainer<T>(provider.value(frame: 0) as! T)
typedContainer.setNeedsUpdate()
}
// MARK: Internal
var valueProvider: AnyValueProvider
var originalValueProvider: AnyValueProvider
var valueType: Any.Type { T.self }
var value: T {
typedContainer.outputValue
}
var valueContainer: AnyValueContainer {
typedContainer
}
func needsUpdate(frame: CGFloat) -> Bool {
valueContainer.needsUpdate || valueProvider.hasUpdate(frame: frame)
}
func setProvider(provider: AnyValueProvider) {
guard provider.valueType == valueType else { return }
valueProvider = provider
valueContainer.setNeedsUpdate()
}
func update(frame: CGFloat) {
typedContainer.setValue(valueProvider.value(frame: frame), forFrame: frame)
}
// MARK: Fileprivate
fileprivate var typedContainer: ValueContainer<T>
}
@@ -0,0 +1,50 @@
//
// AnyNodeProperty.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
// MARK: - AnyNodeProperty
/// A property of a node. The node property holds a provider and a container
protocol AnyNodeProperty {
/// Returns true if the property needs to recompute its stored value
func needsUpdate(frame: CGFloat) -> Bool
/// Updates the property for the frame
func update(frame: CGFloat)
/// The stored value container for the property
var valueContainer: AnyValueContainer { get }
/// The value provider for the property
var valueProvider: AnyValueProvider { get }
/// The original value provider for the property
var originalValueProvider: AnyValueProvider { get }
/// The Type of the value provider
var valueType: Any.Type { get }
/// Sets the value provider for the property.
func setProvider(provider: AnyValueProvider)
}
extension AnyNodeProperty {
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
func getValueOfType<T>() -> T? {
valueContainer.value as? T
}
/// Returns the most recently computed value for the keypath, returns nil if property wasn't found
func getValue() -> Any? {
valueContainer.value
}
}
@@ -0,0 +1,26 @@
//
// AnyValueContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// The container for the value of a property.
protocol AnyValueContainer: AnyObject {
/// The stored value of the container
var value: Any { get }
/// Notifies the provider that it should update its container
func setNeedsUpdate()
/// When true the container needs to have its value updated by its provider
var needsUpdate: Bool { get }
/// The frame time of the last provided update
var lastUpdateFrame: CGFloat { get }
}
@@ -0,0 +1,24 @@
//
// KeypathSettable.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import Foundation
import QuartzCore
/// Protocol that provides keypath search functionality. Returns all node properties associated with a keypath.
protocol KeypathSearchable {
/// The name of the Keypath
var keypathName: String { get }
/// A list of properties belonging to the keypath.
var keypathProperties: [String: AnyNodeProperty] { get }
/// Children Keypaths
var childKeypaths: [KeypathSearchable] { get }
var keypathLayer: CALayer? { get }
}
@@ -0,0 +1,44 @@
//
// NodePropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - NodePropertyMap
protocol NodePropertyMap {
var properties: [AnyNodeProperty] { get }
}
extension NodePropertyMap {
var childKeypaths: [KeypathSearchable] {
[]
}
var keypathLayer: CALayer? {
nil
}
/// Checks if the node's local contents need to be rebuilt.
func needsLocalUpdate(frame: CGFloat) -> Bool {
for property in properties {
if property.needsUpdate(frame: frame) {
return true
}
}
return false
}
/// Rebuilds only the local nodes that have an update for the frame
func updateNodeProperties(frame: CGFloat) {
properties.forEach { property in
property.update(frame: frame)
}
}
}
@@ -0,0 +1,47 @@
//
// ValueContainer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import CoreGraphics
import Foundation
/// A container for a node value that is Typed to T.
class ValueContainer<T>: AnyValueContainer {
// MARK: Lifecycle
init(_ value: T) {
outputValue = value
}
// MARK: Internal
private(set) var lastUpdateFrame = CGFloat.infinity
fileprivate(set) var needsUpdate = true
var value: Any {
outputValue as Any
}
var outputValue: T {
didSet {
needsUpdate = false
}
}
func setValue(_ value: Any, forFrame: CGFloat) {
if let typedValue = value as? T {
needsUpdate = false
lastUpdateFrame = forFrame
outputValue = typedValue
}
}
func setNeedsUpdate() {
needsUpdate = true
}
}
@@ -0,0 +1,39 @@
//
// KeyframeGroupInterpolator.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import CoreGraphics
import Foundation
/// A value provider that produces an array of values from an array of Keyframe Interpolators
final class GroupInterpolator<ValueType>: ValueProvider where ValueType: Interpolatable {
// MARK: Lifecycle
/// Initialize with an array of array of keyframes.
init(keyframeGroups: ContiguousArray<ContiguousArray<Keyframe<ValueType>>>) {
keyframeInterpolators = ContiguousArray(keyframeGroups.map({ KeyframeInterpolator(keyframes: $0) }))
}
// MARK: Internal
let keyframeInterpolators: ContiguousArray<KeyframeInterpolator<ValueType>>
var valueType: Any.Type {
[ValueType].self
}
var storage: ValueProviderStorage<[ValueType]> {
.closure { frame in
self.keyframeInterpolators.map({ $0.value(frame: frame) as! ValueType })
}
}
func hasUpdate(frame: CGFloat) -> Bool {
let updated = keyframeInterpolators.first(where: { $0.hasUpdate(frame: frame) })
return updated != nil
}
}
@@ -0,0 +1,253 @@
//
// KeyframeInterpolator.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/15/19.
//
import CoreGraphics
import Foundation
// MARK: - KeyframeInterpolator
/// A value provider that produces a value at Time from a group of keyframes
final class KeyframeInterpolator<ValueType>: ValueProvider where ValueType: AnyInterpolatable {
// MARK: Lifecycle
init(keyframes: ContiguousArray<Keyframe<ValueType>>) {
self.keyframes = keyframes
}
// MARK: Internal
let keyframes: ContiguousArray<Keyframe<ValueType>>
var valueType: Any.Type {
ValueType.self
}
var storage: ValueProviderStorage<ValueType> {
.closure { [self] frame in
// First set the keyframe span for the frame.
updateSpanIndices(frame: frame)
lastUpdatedFrame = frame
// If only one keyframe return its value
let progress: CGFloat
let value: ValueType
if
let leading = leadingKeyframe,
let trailing = trailingKeyframe
{
/// We have leading and trailing keyframe.
progress = leading.interpolatedProgress(trailing, keyTime: frame)
value = leading.interpolate(to: trailing, progress: progress)
} else if let leading = leadingKeyframe {
progress = 0
value = leading.value
} else if let trailing = trailingKeyframe {
progress = 1
value = trailing.value
} else {
/// Satisfy the compiler.
progress = 0
value = keyframes[0].value
}
return value
}
}
/// Returns true to trigger a frame update for this interpolator.
///
/// An interpolator will be asked if it needs to update every frame.
/// If the interpolator needs updating it will be asked to compute its value for
/// the given frame.
///
/// Cases a keyframe should not be updated:
/// - If time is in span and leading keyframe is hold
/// - If time is after the last keyframe.
/// - If time is before the first keyframe
///
/// Cases for updating a keyframe:
/// - If time is in the span, and is not a hold
/// - If time is outside of the span, and there are more keyframes
/// - If a value delegate is set
/// - If leading and trailing are both nil.
func hasUpdate(frame: CGFloat) -> Bool {
if lastUpdatedFrame == nil {
return true
}
if
let leading = leadingKeyframe,
trailingKeyframe == nil,
leading.time < frame
{
/// Frame is after bounds of keyframes
return false
}
if
let trailing = trailingKeyframe,
leadingKeyframe == nil,
frame < trailing.time
{
/// Frame is before bounds of keyframes
return false
}
if
let leading = leadingKeyframe,
let trailing = trailingKeyframe,
leading.isHold,
leading.time < frame,
frame < trailing.time
{
return false
}
return true
}
// MARK: Fileprivate
fileprivate var lastUpdatedFrame: CGFloat?
fileprivate var leadingIndex: Int? = nil
fileprivate var trailingIndex: Int? = nil
fileprivate var leadingKeyframe: Keyframe<ValueType>? = nil
fileprivate var trailingKeyframe: Keyframe<ValueType>? = nil
/// Finds the appropriate Leading and Trailing keyframe index for the given time.
fileprivate func updateSpanIndices(frame: CGFloat) {
guard keyframes.count > 0 else {
leadingIndex = nil
trailingIndex = nil
leadingKeyframe = nil
trailingKeyframe = nil
return
}
// This function searches through the array to find the span of two keyframes
// that contain the current time.
//
// We could use Array.first(where:) but that would search through the entire array
// each frame.
// Instead we track the last used index and search either forwards or
// backwards from there. This reduces the iterations and complexity from
//
// O(n), where n is the length of the sequence to
// O(n), where n is the number of items after or before the last used index.
//
if keyframes.count == 1 {
/// Only one keyframe. Set it as first and move on.
leadingIndex = 0
trailingIndex = nil
leadingKeyframe = keyframes[0]
trailingKeyframe = nil
return
}
/// Sets the initial keyframes. This is often only needed for the first check.
if
leadingIndex == nil &&
trailingIndex == nil
{
if frame < keyframes[0].time {
/// Time is before the first keyframe. Set it as the trailing.
trailingIndex = 0
} else {
/// Time is after the first keyframe. Set the keyframe and the trailing.
leadingIndex = 0
trailingIndex = 1
}
}
if
let currentTrailing = trailingIndex,
keyframes[currentTrailing].time <= frame
{
/// Time is after the current span. Iterate forward.
var newLeading = currentTrailing
var keyframeFound = false
while !keyframeFound {
leadingIndex = newLeading
trailingIndex = keyframes.validIndex(newLeading + 1)
guard let trailing = trailingIndex else {
/// We have reached the end of our keyframes. Time is after the last keyframe.
keyframeFound = true
continue
}
if frame < keyframes[trailing].time {
/// Keyframe in current span.
keyframeFound = true
continue
}
/// Advance the array.
newLeading = trailing
}
} else if
let currentLeading = leadingIndex,
frame < keyframes[currentLeading].time
{
/// Time is before the current span. Iterate backwards
var newTrailing = currentLeading
var keyframeFound = false
while !keyframeFound {
leadingIndex = keyframes.validIndex(newTrailing - 1)
trailingIndex = newTrailing
guard let leading = leadingIndex else {
/// We have reached the end of our keyframes. Time is after the last keyframe.
keyframeFound = true
continue
}
if keyframes[leading].time <= frame {
/// Keyframe in current span.
keyframeFound = true
continue
}
/// Step back
newTrailing = leading
}
}
if let keyFrame = leadingIndex {
leadingKeyframe = keyframes[keyFrame]
} else {
leadingKeyframe = nil
}
if let keyFrame = trailingIndex {
trailingKeyframe = keyframes[keyFrame]
} else {
trailingKeyframe = nil
}
}
}
extension Array {
fileprivate func validIndex(_ index: Int) -> Int? {
if 0 <= index, index < endIndex {
return index
}
return nil
}
}
extension ContiguousArray {
fileprivate func validIndex(_ index: Int) -> Int? {
if 0 <= index, index < endIndex {
return index
}
return nil
}
}
@@ -0,0 +1,43 @@
//
// SingleValueProvider.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// Returns a value for every frame.
final class SingleValueProvider<ValueType: AnyInterpolatable>: ValueProvider {
// MARK: Lifecycle
init(_ value: ValueType) {
self.value = value
}
// MARK: Internal
var value: ValueType {
didSet {
hasUpdate = true
}
}
var storage: ValueProviderStorage<ValueType> {
.singleValue(value)
}
var valueType: Any.Type {
ValueType.self
}
func hasUpdate(frame _: CGFloat) -> Bool {
hasUpdate
}
// MARK: Private
private var hasUpdate = true
}
@@ -0,0 +1,281 @@
//
// TrimPathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/23/19.
//
import Foundation
import QuartzCore
// MARK: - TrimPathProperties
final class TrimPathProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(trim: Trim) {
keypathName = trim.name
start = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.start.keyframes))
end = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.end.keyframes))
offset = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.offset.keyframes))
type = trim.trimType
keypathProperties = [
"Start" : start,
"End" : end,
"Offset" : offset,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let keypathName: String
let start: NodeProperty<Vector1D>
let end: NodeProperty<Vector1D>
let offset: NodeProperty<Vector1D>
let type: TrimType
}
// MARK: - TrimPathNode
final class TrimPathNode: AnimatorNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, trim: Trim, upstreamPaths: [PathOutputNode]) {
outputNode = PassThroughOutputNode(parent: parentNode?.outputNode)
self.parentNode = parentNode
properties = TrimPathProperties(trim: trim)
self.upstreamPaths = upstreamPaths
}
// MARK: Internal
let properties: TrimPathProperties
let parentNode: AnimatorNode?
let outputNode: NodeOutput
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
func forceUpstreamOutputUpdates() -> Bool {
hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame: CGFloat) {
/// Make sure there is a trim.
let startValue = properties.start.value.cgFloatValue * 0.01
let endValue = properties.end.value.cgFloatValue * 0.01
let start = min(startValue, endValue)
let end = max(startValue, endValue)
let offset = properties.offset.value.cgFloatValue.truncatingRemainder(dividingBy: 360) / 360
/// No need to trim, it's a full path
if start == 0, end == 1 {
return
}
/// All paths are empty.
if start == end {
for pathContainer in upstreamPaths {
pathContainer.removePaths(updateFrame: frame)
}
return
}
if properties.type == .simultaneously {
/// Just trim each path
for pathContainer in upstreamPaths {
let pathObjects = pathContainer.removePaths(updateFrame: frame)
for path in pathObjects {
// We are treating each compount path as an individual path. Its subpaths are treated as a whole.
pathContainer.appendPath(
path.trim(fromPosition: start, toPosition: end, offset: offset, trimSimultaneously: false),
updateFrame: frame)
}
}
return
}
/// Individual path trimming.
/// Brace yourself for the below code.
/// Normalize lengths with offset.
var startPosition = (start + offset).truncatingRemainder(dividingBy: 1)
var endPosition = (end + offset).truncatingRemainder(dividingBy: 1)
if startPosition < 0 {
startPosition = 1 + startPosition
}
if endPosition < 0 {
endPosition = 1 + endPosition
}
if startPosition == 1 {
startPosition = 0
}
if endPosition == 0 {
endPosition = 1
}
/// First get the total length of all paths.
var totalLength: CGFloat = 0
upstreamPaths.forEach({ totalLength = totalLength + $0.totalLength })
/// Now determine the start and end cut lengths
let startLength = startPosition * totalLength
let endLength = endPosition * totalLength
var pathStart: CGFloat = 0
/// Now loop through all path containers
for pathContainer in upstreamPaths {
let pathEnd = pathStart + pathContainer.totalLength
if
!startLength.isInRange(pathStart, pathEnd) &&
endLength.isInRange(pathStart, pathEnd)
{
// pathStart|=======E----------------------|pathEnd
// Cut path components, removing after end.
let pathCutLength = endLength - pathStart
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if pathCutLength < subpathEnd {
/// This is the subpath that needs to be cut.
let cutLength = pathCutLength - subpathStart
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
} else {
/// Add to container and move on
pathContainer.appendPath(path, updateFrame: frame)
}
if pathCutLength == subpathEnd {
/// Right on the end. The next subpath is not included. Break.
break
}
subpathStart = subpathEnd
}
} else if
!endLength.isInRange(pathStart, pathEnd) &&
startLength.isInRange(pathStart, pathEnd)
{
// pathStart|-------S======================|pathEnd
//
// Cut path components, removing before beginning.
let pathCutLength = startLength - pathStart
// Clear paths from container
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if subpathStart < pathCutLength, pathCutLength < subpathEnd {
/// This is the subpath that needs to be cut.
let cutLength = pathCutLength - subpathStart
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
} else if pathCutLength <= subpathStart {
pathContainer.appendPath(path, updateFrame: frame)
}
subpathStart = subpathEnd
}
} else if
endLength.isInRange(pathStart, pathEnd) &&
startLength.isInRange(pathStart, pathEnd)
{
// pathStart|-------S============E---------|endLength
// pathStart|=====E----------------S=======|endLength
// trim from path beginning to endLength.
// Cut path components, removing before beginnings.
let startCutLength = startLength - pathStart
let endCutLength = endLength - pathStart
// Clear paths from container
let subpaths = pathContainer.removePaths(updateFrame: frame)
var subpathStart: CGFloat = 0
for path in subpaths {
let subpathEnd = subpathStart + path.length
if
!startCutLength.isInRange(subpathStart, subpathEnd) &&
!endCutLength.isInRange(subpathStart, subpathEnd)
{
// The whole path is included. Add
// S|==============================|E
pathContainer.appendPath(path, updateFrame: frame)
} else if
startCutLength.isInRange(subpathStart, subpathEnd) &&
!endCutLength.isInRange(subpathStart, subpathEnd)
{
/// The start of the path needs to be trimmed
// |-------S======================|E
let cutLength = startCutLength - subpathStart
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
} else if
!startCutLength.isInRange(subpathStart, subpathEnd) &&
endCutLength.isInRange(subpathStart, subpathEnd)
{
// S|=======E----------------------|
let cutLength = endCutLength - subpathStart
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
} else if
startCutLength.isInRange(subpathStart, subpathEnd) &&
endCutLength.isInRange(subpathStart, subpathEnd)
{
// |-------S============E---------|
let cutFromLength = startCutLength - subpathStart
let cutToLength = endCutLength - subpathStart
let newPath = path.trim(
fromPosition: cutFromLength / path.length,
toPosition: cutToLength / path.length,
offset: 0,
trimSimultaneously: false)
pathContainer.appendPath(newPath, updateFrame: frame)
break
}
subpathStart = subpathEnd
}
} else if
(endLength <= pathStart && pathEnd <= startLength) ||
(startLength <= pathStart && endLength <= pathStart) ||
(pathEnd <= startLength && pathEnd <= endLength)
{
/// The Path needs to be cleared
pathContainer.removePaths(updateFrame: frame)
}
pathStart = pathEnd
}
}
// MARK: Fileprivate
fileprivate let upstreamPaths: [PathOutputNode]
}
@@ -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)) })
}
}
}
@@ -0,0 +1,139 @@
//
// EllipseNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import Foundation
import QuartzCore
// MARK: - EllipseNodeProperties
final class EllipseNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(ellipse: Ellipse) {
keypathName = ellipse.name
direction = ellipse.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.position.keyframes))
size = NodeProperty(provider: KeyframeInterpolator(keyframes: ellipse.size.keyframes))
keypathProperties = [
"Position" : position,
"Size" : size,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let direction: PathDirection
let position: NodeProperty<Vector3D>
let size: NodeProperty<Vector3D>
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - EllipseNode
final class EllipseNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, ellipse: Ellipse) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = EllipseNodeProperties(ellipse: ellipse)
self.parentNode = parentNode
}
// MARK: Internal
static let ControlPointConstant: CGFloat = 0.55228
let pathOutput: PathOutputNode
let properties: EllipseNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var isEnabled = true {
didSet {
pathOutput.isEnabled = isEnabled
}
}
func rebuildOutputs(frame: CGFloat) {
pathOutput.setPath(
.ellipse(
size: properties.size.value.sizeValue,
center: properties.position.value.pointValue,
direction: properties.direction),
updateFrame: frame)
}
}
extension BezierPath {
/// Constructs a `BezierPath` in the shape of an ellipse
static func ellipse(
size: CGSize,
center: CGPoint,
direction: PathDirection)
-> BezierPath
{
// Unfortunately we HAVE to manually build out the ellipse.
// Every Apple method constructs an ellipse from the 3 o-clock position
// After effects constructs from the Noon position.
// After effects does clockwise, but also has a flag for reversed.
var half = size * 0.5
if direction == .counterClockwise {
half.width = half.width * -1
}
let q1 = CGPoint(x: center.x, y: center.y - half.height)
let q2 = CGPoint(x: center.x + half.width, y: center.y)
let q3 = CGPoint(x: center.x, y: center.y + half.height)
let q4 = CGPoint(x: center.x - half.width, y: center.y)
let cp = half * EllipseNode.ControlPointConstant
var path = BezierPath(startPoint: CurveVertex(
point: q1,
inTangentRelative: CGPoint(x: -cp.width, y: 0),
outTangentRelative: CGPoint(x: cp.width, y: 0)))
path.addVertex(CurveVertex(
point: q2,
inTangentRelative: CGPoint(x: 0, y: -cp.height),
outTangentRelative: CGPoint(x: 0, y: cp.height)))
path.addVertex(CurveVertex(
point: q3,
inTangentRelative: CGPoint(x: cp.width, y: 0),
outTangentRelative: CGPoint(x: -cp.width, y: 0)))
path.addVertex(CurveVertex(
point: q4,
inTangentRelative: CGPoint(x: 0, y: cp.height),
outTangentRelative: CGPoint(x: 0, y: -cp.height)))
path.addVertex(CurveVertex(
point: q1,
inTangentRelative: CGPoint(x: -cp.width, y: 0),
outTangentRelative: CGPoint(x: cp.width, y: 0)))
path.close()
return path
}
}
@@ -0,0 +1,170 @@
//
// PolygonNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - PolygonNodeProperties
final class PolygonNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(star: Star) {
keypathName = star.name
direction = star.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes))
outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes))
outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes))
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Rotation" : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
var childKeypaths: [KeypathSearchable] = []
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let outerRadius: NodeProperty<Vector1D>
let outerRoundedness: NodeProperty<Vector1D>
let rotation: NodeProperty<Vector1D>
let points: NodeProperty<Vector1D>
}
// MARK: - PolygonNode
final class PolygonNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, star: Star) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = PolygonNodeProperties(star: star)
self.parentNode = parentNode
}
// MARK: Internal
/// Magic number needed for constructing path.
static let PolygonConstant: CGFloat = 0.25
let properties: PolygonNodeProperties
let pathOutput: PathOutputNode
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var isEnabled = true {
didSet {
pathOutput.isEnabled = isEnabled
}
}
func rebuildOutputs(frame: CGFloat) {
let path = BezierPath.polygon(
position: properties.position.value.pointValue,
numberOfPoints: properties.points.value.cgFloatValue,
outerRadius: properties.outerRadius.value.cgFloatValue,
outerRoundedness: properties.outerRoundedness.value.cgFloatValue,
rotation: properties.rotation.value.cgFloatValue,
direction: properties.direction)
pathOutput.setPath(path, updateFrame: frame)
}
}
extension BezierPath {
/// Creates a `BezierPath` in the shape of a polygon
static func polygon(
position: CGPoint,
numberOfPoints: CGFloat,
outerRadius: CGFloat,
outerRoundedness inputOuterRoundedness: CGFloat,
rotation: CGFloat,
direction: PathDirection)
-> BezierPath
{
var currentAngle = (rotation - 90).toRadians()
let anglePerPoint = ((2 * CGFloat.pi) / numberOfPoints)
let outerRoundedness = inputOuterRoundedness * 0.01
var point = CGPoint(
x: outerRadius * cos(currentAngle),
y: outerRadius * sin(currentAngle))
var vertices = [CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero)]
var previousPoint = point
currentAngle += anglePerPoint;
for _ in 0..<Int(ceil(numberOfPoints)) {
previousPoint = point
point = CGPoint(
x: outerRadius * cos(currentAngle),
y: outerRadius * sin(currentAngle))
if outerRoundedness != 0 {
let cp1Theta = (atan2(previousPoint.y, previousPoint.x) - CGFloat.pi / 2)
let cp1Dx = cos(cp1Theta);
let cp1Dy = sin(cp1Theta);
let cp2Theta = (atan2(point.y, point.x) - CGFloat.pi / 2)
let cp2Dx = cos(cp2Theta)
let cp2Dy = sin(cp2Theta)
let cp1 = CGPoint(
x: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp1Dx,
y: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp1Dy)
let cp2 = CGPoint(
x: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp2Dx,
y: outerRadius * outerRoundedness * PolygonNode.PolygonConstant * cp2Dy)
let previousVertex = vertices[vertices.endIndex - 1]
vertices[vertices.endIndex - 1] = CurveVertex(
previousVertex.inTangent,
previousVertex.point,
previousVertex.point - cp1)
vertices.append(CurveVertex(point: point + position, inTangentRelative: cp2, outTangentRelative: .zero))
} else {
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
}
currentAngle += anglePerPoint;
}
let reverse = direction == .counterClockwise
if reverse {
vertices = vertices.reversed()
}
var path = BezierPath()
for vertex in vertices {
path.addVertex(reverse ? vertex.reversed() : vertex)
}
path.close()
return path
}
}
@@ -0,0 +1,225 @@
//
// RectNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import CoreGraphics
import Foundation
// MARK: - RectNodeProperties
final class RectNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(rectangle: Rectangle) {
keypathName = rectangle.name
direction = rectangle.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.position.keyframes))
size = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.size.keyframes))
cornerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: rectangle.cornerRadius.keyframes))
keypathProperties = [
"Position" : position,
"Size" : size,
"Roundness" : cornerRadius,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let size: NodeProperty<Vector3D>
let cornerRadius: NodeProperty<Vector1D>
}
// MARK: - RectangleNode
final class RectangleNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, rectangle: Rectangle) {
properties = RectNodeProperties(rectangle: rectangle)
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
self.parentNode = parentNode
}
// MARK: Internal
let properties: RectNodeProperties
let pathOutput: PathOutputNode
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var isEnabled = true {
didSet {
pathOutput.isEnabled = isEnabled
}
}
func rebuildOutputs(frame: CGFloat) {
pathOutput.setPath(
.rectangle(
position: properties.position.value.pointValue,
size: properties.size.value.sizeValue,
cornerRadius: properties.cornerRadius.value.cgFloatValue,
direction: properties.direction),
updateFrame: frame)
}
}
// MARK: - BezierPath + rectangle
extension BezierPath {
/// Constructs a `BezierPath` in the shape of a rectangle, optionally with rounded corners
static func rectangle(
position: CGPoint,
size inputSize: CGSize,
cornerRadius: CGFloat,
direction: PathDirection)
-> BezierPath
{
let size = inputSize * 0.5
let radius = min(min(cornerRadius, size.width) , size.height)
var bezierPath = BezierPath()
let points: [CurveVertex]
if radius <= 0 {
/// No Corners
points = [
/// Lead In
CurveVertex(
point: CGPoint(x: size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 1
CurveVertex(
point: CGPoint(x: size.width, y: size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 2
CurveVertex(
point: CGPoint(x: -size.width, y: size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 3
CurveVertex(
point: CGPoint(x: -size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
/// Corner 4
CurveVertex(
point: CGPoint(x: size.width, y: -size.height),
inTangentRelative: .zero,
outTangentRelative: .zero)
.translated(position),
]
} else {
let controlPoint = radius * EllipseNode.ControlPointConstant
points = [
/// Lead In
CurveVertex(
CGPoint(x: radius, y: 0),
CGPoint(x: radius, y: 0),
CGPoint(x: radius, y: 0))
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
/// Corner 1
CurveVertex(
CGPoint(x: radius, y: 0), // In tangent
CGPoint(x: radius, y: 0), // Point
CGPoint(x: radius, y: controlPoint))
.translated(CGPoint(x: -radius, y: -radius))
.translated(CGPoint(x: size.width, y: size.height))
.translated(position),
CurveVertex(
CGPoint(x: controlPoint, y: radius), // In tangent
CGPoint(x: 0, y: radius), // Point
CGPoint(x: 0, y: radius)) // Out Tangent
.translated(CGPoint(x: -radius, y: -radius))
.translated(CGPoint(x: size.width, y: size.height))
.translated(position),
/// Corner 2
CurveVertex(
CGPoint(x: 0, y: radius), // In tangent
CGPoint(x: 0, y: radius), // Point
CGPoint(x: -controlPoint, y: radius))// Out tangent
.translated(CGPoint(x: radius, y: -radius))
.translated(CGPoint(x: -size.width, y: size.height))
.translated(position),
CurveVertex(
CGPoint(x: -radius, y: controlPoint), // In tangent
CGPoint(x: -radius, y: 0), // Point
CGPoint(x: -radius, y: 0)) // Out tangent
.translated(CGPoint(x: radius, y: -radius))
.translated(CGPoint(x: -size.width, y: size.height))
.translated(position),
/// Corner 3
CurveVertex(
CGPoint(x: -radius, y: 0), // In tangent
CGPoint(x: -radius, y: 0), // Point
CGPoint(x: -radius, y: -controlPoint)) // Out tangent
.translated(CGPoint(x: radius, y: radius))
.translated(CGPoint(x: -size.width, y: -size.height))
.translated(position),
CurveVertex(
CGPoint(x: -controlPoint, y: -radius), // In tangent
CGPoint(x: 0, y: -radius), // Point
CGPoint(x: 0, y: -radius)) // Out tangent
.translated(CGPoint(x: radius, y: radius))
.translated(CGPoint(x: -size.width, y: -size.height))
.translated(position),
/// Corner 4
CurveVertex(
CGPoint(x: 0, y: -radius), // In tangent
CGPoint(x: 0, y: -radius), // Point
CGPoint(x: controlPoint, y: -radius)) // Out tangent
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
CurveVertex(
CGPoint(x: radius, y: -controlPoint), // In tangent
CGPoint(x: radius, y: 0), // Point
CGPoint(x: radius, y: 0)) // Out tangent
.translated(CGPoint(x: -radius, y: radius))
.translated(CGPoint(x: size.width, y: -size.height))
.translated(position),
]
}
let reversed = direction == .counterClockwise
let pathPoints = reversed ? points.reversed() : points
for point in pathPoints {
bezierPath.addVertex(reversed ? point.reversed() : point)
}
bezierPath.close()
return bezierPath
}
}
@@ -0,0 +1,74 @@
//
// PathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/16/19.
//
import CoreGraphics
import Foundation
// MARK: - ShapeNodeProperties
final class ShapeNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(shape: Shape) {
keypathName = shape.name
path = NodeProperty(provider: KeyframeInterpolator(keyframes: shape.path.keyframes))
keypathProperties = [
"Path" : path,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let path: NodeProperty<BezierPath>
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - ShapeNode
final class ShapeNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, shape: Shape) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = ShapeNodeProperties(shape: shape)
self.parentNode = parentNode
}
// MARK: Internal
let properties: ShapeNodeProperties
let pathOutput: PathOutputNode
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var isEnabled = true {
didSet {
pathOutput.isEnabled = isEnabled
}
}
func rebuildOutputs(frame: CGFloat) {
pathOutput.setPath(properties.path.value, updateFrame: frame)
}
}
@@ -0,0 +1,222 @@
//
// StarNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/21/19.
//
import Foundation
import QuartzCore
// MARK: - StarNodeProperties
final class StarNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(star: Star) {
keypathName = star.name
direction = star.direction
position = NodeProperty(provider: KeyframeInterpolator(keyframes: star.position.keyframes))
outerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRadius.keyframes))
outerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: star.outerRoundness.keyframes))
if let innerRadiusKeyframes = star.innerRadius?.keyframes {
innerRadius = NodeProperty(provider: KeyframeInterpolator(keyframes: innerRadiusKeyframes))
} else {
innerRadius = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
if let innderRoundedness = star.innerRoundness?.keyframes {
innerRoundedness = NodeProperty(provider: KeyframeInterpolator(keyframes: innderRoundedness))
} else {
innerRoundedness = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: star.rotation.keyframes))
points = NodeProperty(provider: KeyframeInterpolator(keyframes: star.points.keyframes))
keypathProperties = [
"Position" : position,
"Outer Radius" : outerRadius,
"Outer Roundedness" : outerRoundedness,
"Inner Radius" : innerRadius,
"Inner Roundedness" : innerRoundedness,
"Rotation" : rotation,
"Points" : points,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let direction: PathDirection
let position: NodeProperty<Vector3D>
let outerRadius: NodeProperty<Vector1D>
let outerRoundedness: NodeProperty<Vector1D>
let innerRadius: NodeProperty<Vector1D>
let innerRoundedness: NodeProperty<Vector1D>
let rotation: NodeProperty<Vector1D>
let points: NodeProperty<Vector1D>
}
// MARK: - StarNode
final class StarNode: AnimatorNode, PathNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, star: Star) {
pathOutput = PathOutputNode(parent: parentNode?.outputNode)
properties = StarNodeProperties(star: star)
self.parentNode = parentNode
}
// MARK: Internal
/// Magic number needed for building path data
static let PolystarConstant: CGFloat = 0.47829
let properties: StarNodeProperties
let pathOutput: PathOutputNode
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
// MARK: Animator Node
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var isEnabled = true {
didSet {
pathOutput.isEnabled = isEnabled
}
}
func rebuildOutputs(frame: CGFloat) {
let path = BezierPath.star(
position: properties.position.value.pointValue,
outerRadius: properties.outerRadius.value.cgFloatValue,
innerRadius: properties.innerRadius.value.cgFloatValue,
outerRoundedness: properties.outerRoundedness.value.cgFloatValue,
innerRoundedness: properties.innerRoundedness.value.cgFloatValue,
numberOfPoints: properties.points.value.cgFloatValue,
rotation: properties.rotation.value.cgFloatValue,
direction: properties.direction)
pathOutput.setPath(path, updateFrame: frame)
}
}
extension BezierPath {
/// Constructs a `BezierPath` in the shape of a star
static func star(
position: CGPoint,
outerRadius: CGFloat,
innerRadius: CGFloat,
outerRoundedness inoutOuterRoundedness: CGFloat,
innerRoundedness inputInnerRoundedness: CGFloat,
numberOfPoints: CGFloat,
rotation: CGFloat,
direction: PathDirection)
-> BezierPath
{
var currentAngle = (rotation - 90).toRadians()
let anglePerPoint = (2 * CGFloat.pi) / numberOfPoints
let halfAnglePerPoint = anglePerPoint / 2.0
let partialPointAmount = numberOfPoints - floor(numberOfPoints)
let outerRoundedness = inoutOuterRoundedness * 0.01
let innerRoundedness = inputInnerRoundedness * 0.01
var point: CGPoint = .zero
var partialPointRadius: CGFloat = 0
if partialPointAmount != 0 {
currentAngle += halfAnglePerPoint * (1 - partialPointAmount)
partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius)
point.x = (partialPointRadius * cos(currentAngle))
point.y = (partialPointRadius * sin(currentAngle))
currentAngle += anglePerPoint * partialPointAmount / 2
} else {
point.x = (outerRadius * cos(currentAngle))
point.y = (outerRadius * sin(currentAngle))
currentAngle += halfAnglePerPoint
}
var vertices = [CurveVertex]()
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
var previousPoint = point
var longSegment = false
let numPoints = Int(ceil(numberOfPoints) * 2)
for i in 0..<numPoints {
var radius = longSegment ? outerRadius : innerRadius
var dTheta = halfAnglePerPoint
if partialPointRadius != 0 && i == numPoints - 2 {
dTheta = anglePerPoint * partialPointAmount / 2
}
if partialPointRadius != 0 && i == numPoints - 1 {
radius = partialPointRadius
}
previousPoint = point
point.x = (radius * cos(currentAngle))
point.y = (radius * sin(currentAngle))
if innerRoundedness == 0 && outerRoundedness == 0 {
vertices.append(CurveVertex(point: point + position, inTangentRelative: .zero, outTangentRelative: .zero))
} else {
let cp1Theta = (atan2(previousPoint.y, previousPoint.x) - CGFloat.pi / 2)
let cp1Dx = cos(cp1Theta)
let cp1Dy = sin(cp1Theta)
let cp2Theta = (atan2(point.y, point.x) - CGFloat.pi / 2)
let cp2Dx = cos(cp2Theta)
let cp2Dy = sin(cp2Theta)
let cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness
let cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness
let cp1Radius = longSegment ? innerRadius : outerRadius
let cp2Radius = longSegment ? outerRadius : innerRadius
var cp1 = CGPoint(
x: cp1Radius * cp1Roundedness * StarNode.PolystarConstant * cp1Dx,
y: cp1Radius * cp1Roundedness * StarNode.PolystarConstant * cp1Dy)
var cp2 = CGPoint(
x: cp2Radius * cp2Roundedness * StarNode.PolystarConstant * cp2Dx,
y: cp2Radius * cp2Roundedness * StarNode.PolystarConstant * cp2Dy)
if partialPointAmount != 0 {
if i == 0 {
cp1 = cp1 * partialPointAmount
} else if i == numPoints - 1 {
cp2 = cp2 * partialPointAmount
}
}
let previousVertex = vertices[vertices.endIndex - 1]
vertices[vertices.endIndex - 1] = CurveVertex(
previousVertex.inTangent,
previousVertex.point,
previousVertex.point - cp1)
vertices.append(CurveVertex(point: point + position, inTangentRelative: cp2, outTangentRelative: .zero))
}
currentAngle += dTheta
longSegment = !longSegment
}
let reverse = direction == .counterClockwise
if reverse {
vertices = vertices.reversed()
}
var path = BezierPath()
for vertex in vertices {
path.addVertex(reverse ? vertex.reversed() : vertex)
}
path.close()
return path
}
}
@@ -0,0 +1,155 @@
//
// GroupNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - GroupNodeProperties
final class GroupNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(transform: ShapeTransform?) {
if let transform = transform {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchor.keyframes))
position = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.position.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))
skew = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.skew.keyframes))
skewAxis = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.skewAxis.keyframes))
} else {
/// Transform node missing. Default to empty transform.
anchor = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(0), y: CGFloat(0), z: CGFloat(0))))
position = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(0), y: CGFloat(0), z: CGFloat(0))))
scale = NodeProperty(provider: SingleValueProvider(Vector3D(x: CGFloat(100), y: CGFloat(100), z: CGFloat(100))))
rotation = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
opacity = NodeProperty(provider: SingleValueProvider(Vector1D(100)))
skew = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
skewAxis = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Anchor Point" : anchor,
"Position" : position,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity,
"Skew" : skew,
"Skew Axis" : skewAxis,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName = "Transform"
var childKeypaths: [KeypathSearchable] = []
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let position: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let opacity: NodeProperty<Vector1D>
let skew: NodeProperty<Vector1D>
let skewAxis: NodeProperty<Vector1D>
var caTransform: CATransform3D {
CATransform3D.makeTransform(
anchor: anchor.value.pointValue,
position: position.value.pointValue,
scale: scale.value.sizeValue,
rotation: rotation.value.cgFloatValue,
skew: skew.value.cgFloatValue,
skewAxis: skewAxis.value.cgFloatValue)
}
}
// MARK: - GroupNode
final class GroupNode: AnimatorNode {
// MARK: Lifecycle
// MARK: Initializer
init(name: String, parentNode: AnimatorNode?, tree: NodeTree) {
self.parentNode = parentNode
keypathName = name
rootNode = tree.rootNode
properties = GroupNodeProperties(transform: tree.transform)
groupOutput = GroupOutputNode(parent: parentNode?.outputNode, rootNode: rootNode?.outputNode)
var childKeypaths: [KeypathSearchable] = tree.childrenNodes
childKeypaths.append(properties)
self.childKeypaths = childKeypaths
for childContainer in tree.renderContainers {
container.insertRenderLayer(childContainer)
}
}
// MARK: Internal
// MARK: Properties
let groupOutput: GroupOutputNode
let properties: GroupNodeProperties
let rootNode: AnimatorNode?
var container = ShapeContainerLayer()
// MARK: Keypath Searchable
let keypathName: String
let childKeypaths: [KeypathSearchable]
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var keypathLayer: CALayer? {
container
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
properties
}
var outputNode: NodeOutput {
groupOutput
}
var isEnabled = true {
didSet {
container.isHidden = !isEnabled
}
}
func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool {
rootNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false
}
func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool) {
rootNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate)
}
func rebuildOutputs(frame: CGFloat) {
container.opacity = Float(properties.opacity.value.cgFloatValue) * 0.01
container.transform = properties.caTransform
groupOutput.setTransform(container.transform, forFrame: frame)
}
}
@@ -0,0 +1,90 @@
//
// FillNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import CoreGraphics
import Foundation
// MARK: - FillNodeProperties
final class FillNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(fill: Fill) {
keypathName = fill.name
color = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.color.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: fill.opacity.keyframes))
type = fill.fillRule
keypathProperties = [
"Opacity" : opacity,
PropertyName.color.rawValue : color,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let color: NodeProperty<Color>
let type: FillRule
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - FillNode
final class FillNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, fill: Fill) {
fillRender = FillRenderer(parent: parentNode?.outputNode)
fillProperties = FillNodeProperties(fill: fill)
self.parentNode = parentNode
}
// MARK: Internal
let fillRender: FillRenderer
let fillProperties: FillNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
fillRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
fillProperties
}
var isEnabled = true {
didSet {
fillRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
fillRender.color = fillProperties.color.value.cgColorValue
fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01
fillRender.fillRule = fillProperties.type
}
}
@@ -0,0 +1,102 @@
//
// GradientFillNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
// MARK: - GradientFillProperties
final class GradientFillProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(gradientfill: GradientFill) {
keypathName = gradientfill.name
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.opacity.keyframes))
startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.startPoint.keyframes))
endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.endPoint.keyframes))
colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientfill.colors.keyframes))
gradientType = gradientfill.gradientType
numberOfColors = gradientfill.numberOfColors
keypathProperties = [
"Opacity" : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let startPoint: NodeProperty<Vector3D>
let endPoint: NodeProperty<Vector3D>
let colors: NodeProperty<[Double]>
let gradientType: GradientType
let numberOfColors: Int
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - GradientFillNode
final class GradientFillNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, gradientFill: GradientFill) {
fillRender = GradientFillRenderer(parent: parentNode?.outputNode)
fillProperties = GradientFillProperties(gradientfill: gradientFill)
self.parentNode = parentNode
}
// MARK: Internal
let fillRender: GradientFillRenderer
let fillProperties: GradientFillProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
fillRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
fillProperties
}
var isEnabled = true {
didSet {
fillRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
fillRender.start = fillProperties.startPoint.value.pointValue
fillRender.end = fillProperties.endPoint.value.pointValue
fillRender.opacity = fillProperties.opacity.value.cgFloatValue * 0.01
fillRender.colors = fillProperties.colors.value.map { CGFloat($0) }
fillRender.type = fillProperties.gradientType
fillRender.numberOfColors = fillProperties.numberOfColors
}
}
@@ -0,0 +1,151 @@
//
// GradientStrokeNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/23/19.
//
import CoreGraphics
import Foundation
// MARK: - GradientStrokeProperties
final class GradientStrokeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(gradientStroke: GradientStroke) {
keypathName = gradientStroke.name
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.opacity.keyframes))
startPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.startPoint.keyframes))
endPoint = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.endPoint.keyframes))
colors = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.colors.keyframes))
gradientType = gradientStroke.gradientType
numberOfColors = gradientStroke.numberOfColors
width = NodeProperty(provider: KeyframeInterpolator(keyframes: gradientStroke.width.keyframes))
miterLimit = CGFloat(gradientStroke.miterLimit)
lineCap = gradientStroke.lineCap
lineJoin = gradientStroke.lineJoin
if let dashes = gradientStroke.dashPattern {
var dashPatterns = ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>()
var dashPhase = ContiguousArray<Keyframe<Vector1D>>()
for dash in dashes {
if dash.type == .offset {
dashPhase = dash.value.keyframes
} else {
dashPatterns.append(dash.value.keyframes)
}
}
dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns))
self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase))
} else {
dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]()))
dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
"Start Point" : startPoint,
"End Point" : endPoint,
"Colors" : colors,
"Stroke Width" : width,
"Dashes" : dashPattern,
"Dash Phase" : dashPhase,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
var keypathName: String
let opacity: NodeProperty<Vector1D>
let startPoint: NodeProperty<Vector3D>
let endPoint: NodeProperty<Vector3D>
let colors: NodeProperty<[Double]>
let width: NodeProperty<Vector1D>
let dashPattern: NodeProperty<[Vector1D]>
let dashPhase: NodeProperty<Vector1D>
let lineCap: LineCap
let lineJoin: LineJoin
let miterLimit: CGFloat
let gradientType: GradientType
let numberOfColors: Int
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
}
// MARK: - GradientStrokeNode
final class GradientStrokeNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, gradientStroke: GradientStroke) {
strokeRender = GradientStrokeRenderer(parent: parentNode?.outputNode)
strokeProperties = GradientStrokeProperties(gradientStroke: gradientStroke)
self.parentNode = parentNode
}
// MARK: Internal
let strokeRender: GradientStrokeRenderer
let strokeProperties: GradientStrokeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
strokeRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
strokeProperties
}
var isEnabled = true {
didSet {
strokeRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
/// Update gradient properties
strokeRender.gradientRender.start = strokeProperties.startPoint.value.pointValue
strokeRender.gradientRender.end = strokeProperties.endPoint.value.pointValue
strokeRender.gradientRender.opacity = strokeProperties.opacity.value.cgFloatValue
strokeRender.gradientRender.colors = strokeProperties.colors.value.map { CGFloat($0) }
strokeRender.gradientRender.type = strokeProperties.gradientType
strokeRender.gradientRender.numberOfColors = strokeProperties.numberOfColors
/// Now update stroke properties
strokeRender.strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue
strokeRender.strokeRender.width = strokeProperties.width.value.cgFloatValue
strokeRender.strokeRender.miterLimit = strokeProperties.miterLimit
strokeRender.strokeRender.lineCap = strokeProperties.lineCap
strokeRender.strokeRender.lineJoin = strokeProperties.lineJoin
/// Get dash lengths
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
if dashLengths.count > 0 {
strokeRender.strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
strokeRender.strokeRender.dashLengths = dashLengths
} else {
strokeRender.strokeRender.dashLengths = nil
strokeRender.strokeRender.dashPhase = nil
}
}
}
@@ -0,0 +1,153 @@
//
// StrokeNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
import QuartzCore
// MARK: - StrokeNodeProperties
final class StrokeNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(stroke: Stroke) {
keypathName = stroke.name
color = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.color.keyframes))
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.opacity.keyframes))
width = NodeProperty(provider: KeyframeInterpolator(keyframes: stroke.width.keyframes))
miterLimit = CGFloat(stroke.miterLimit)
lineCap = stroke.lineCap
lineJoin = stroke.lineJoin
if let dashes = stroke.dashPattern {
let (dashPatterns, dashPhase) = dashes.shapeLayerConfiguration
dashPattern = NodeProperty(provider: GroupInterpolator(keyframeGroups: dashPatterns))
if dashPhase.count == 0 {
self.dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
} else {
self.dashPhase = NodeProperty(provider: KeyframeInterpolator(keyframes: dashPhase))
}
} else {
dashPattern = NodeProperty(provider: SingleValueProvider([Vector1D]()))
dashPhase = NodeProperty(provider: SingleValueProvider(Vector1D(0)))
}
keypathProperties = [
"Opacity" : opacity,
PropertyName.color.rawValue : color,
"Stroke Width" : width,
"Dashes" : dashPattern,
"Dash Phase" : dashPhase,
]
properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathName: String
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
let opacity: NodeProperty<Vector1D>
let color: NodeProperty<Color>
let width: NodeProperty<Vector1D>
let dashPattern: NodeProperty<[Vector1D]>
let dashPhase: NodeProperty<Vector1D>
let lineCap: LineCap
let lineJoin: LineJoin
let miterLimit: CGFloat
}
// MARK: - StrokeNode
/// Node that manages stroking a path
final class StrokeNode: AnimatorNode, RenderNode {
// MARK: Lifecycle
init(parentNode: AnimatorNode?, stroke: Stroke) {
strokeRender = StrokeRenderer(parent: parentNode?.outputNode)
strokeProperties = StrokeNodeProperties(stroke: stroke)
self.parentNode = parentNode
}
// MARK: Internal
let strokeRender: StrokeRenderer
let strokeProperties: StrokeNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var renderer: NodeOutput & Renderable {
strokeRender
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
strokeProperties
}
var isEnabled = true {
didSet {
strokeRender.isEnabled = isEnabled
}
}
func localUpdatesPermeateDownstream() -> Bool {
false
}
func rebuildOutputs(frame _: CGFloat) {
strokeRender.color = strokeProperties.color.value.cgColorValue
strokeRender.opacity = strokeProperties.opacity.value.cgFloatValue * 0.01
strokeRender.width = strokeProperties.width.value.cgFloatValue
strokeRender.miterLimit = strokeProperties.miterLimit
strokeRender.lineCap = strokeProperties.lineCap
strokeRender.lineJoin = strokeProperties.lineJoin
/// Get dash lengths
let dashLengths = strokeProperties.dashPattern.value.map { $0.cgFloatValue }
if dashLengths.count > 0 {
strokeRender.dashPhase = strokeProperties.dashPhase.value.cgFloatValue
strokeRender.dashLengths = dashLengths
} else {
strokeRender.dashLengths = nil
strokeRender.dashPhase = nil
}
}
}
// MARK: - [DashElement] + shapeLayerConfiguration
extension Array where Element == DashElement {
typealias ShapeLayerConfiguration = (
dashPatterns: ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>,
dashPhase: ContiguousArray<Keyframe<Vector1D>>)
/// Converts the `[DashElement]` data model into `lineDashPattern` and `lineDashPhase`
/// representations usable in a `CAShapeLayer`
var shapeLayerConfiguration: ShapeLayerConfiguration {
var dashPatterns = ContiguousArray<ContiguousArray<Keyframe<Vector1D>>>()
var dashPhase = ContiguousArray<Keyframe<Vector1D>>()
for dash in self {
if dash.type == .offset {
dashPhase = dash.value.keyframes
} else {
dashPatterns.append(dash.value.keyframes)
}
}
return (dashPatterns, dashPhase)
}
}
@@ -0,0 +1,270 @@
//
// TextAnimatorNode.swift
// lottie-ios-iOS
//
// Created by Brandon Withrow on 2/19/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - TextAnimatorNodeProperties
final class TextAnimatorNodeProperties: NodePropertyMap, KeypathSearchable {
// MARK: Lifecycle
init(textAnimator: TextAnimator) {
keypathName = textAnimator.name
var properties = [String : AnyNodeProperty]()
if let keyframeGroup = textAnimator.anchor {
anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Anchor"] = anchor
} else {
anchor = nil
}
if let keyframeGroup = textAnimator.position {
position = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Position"] = position
} else {
position = nil
}
if let keyframeGroup = textAnimator.scale {
scale = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Scale"] = scale
} else {
scale = nil
}
if let keyframeGroup = textAnimator.skew {
skew = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Skew"] = skew
} else {
skew = nil
}
if let keyframeGroup = textAnimator.skewAxis {
skewAxis = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Skew Axis"] = skewAxis
} else {
skewAxis = nil
}
if let keyframeGroup = textAnimator.rotation {
rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Rotation"] = rotation
} else {
rotation = nil
}
if let keyframeGroup = textAnimator.opacity {
opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Opacity"] = opacity
} else {
opacity = nil
}
if let keyframeGroup = textAnimator.strokeColor {
strokeColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Stroke Color"] = strokeColor
} else {
strokeColor = nil
}
if let keyframeGroup = textAnimator.fillColor {
fillColor = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Fill Color"] = fillColor
} else {
fillColor = nil
}
if let keyframeGroup = textAnimator.strokeWidth {
strokeWidth = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Stroke Width"] = strokeWidth
} else {
strokeWidth = nil
}
if let keyframeGroup = textAnimator.tracking {
tracking = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframeGroup.keyframes))
properties["Tracking"] = tracking
} else {
tracking = nil
}
keypathProperties = properties
self.properties = Array(keypathProperties.values)
}
// MARK: Internal
let keypathName: String
let anchor: NodeProperty<Vector3D>?
let position: NodeProperty<Vector3D>?
let scale: NodeProperty<Vector3D>?
let skew: NodeProperty<Vector1D>?
let skewAxis: NodeProperty<Vector1D>?
let rotation: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>?
let strokeColor: NodeProperty<Color>?
let fillColor: NodeProperty<Color>?
let strokeWidth: NodeProperty<Vector1D>?
let tracking: NodeProperty<Vector1D>?
let keypathProperties: [String: AnyNodeProperty]
let properties: [AnyNodeProperty]
var caTransform: CATransform3D {
CATransform3D.makeTransform(
anchor: anchor?.value.pointValue ?? .zero,
position: position?.value.pointValue ?? .zero,
scale: scale?.value.sizeValue ?? CGSize(width: 100, height: 100),
rotation: rotation?.value.cgFloatValue ?? 0,
skew: skew?.value.cgFloatValue,
skewAxis: skewAxis?.value.cgFloatValue)
}
}
// MARK: - TextOutputNode
final class TextOutputNode: NodeOutput {
// MARK: Lifecycle
init(parent: TextOutputNode?) {
parentTextNode = parent
}
// MARK: Internal
var parentTextNode: TextOutputNode?
var isEnabled = true
var outputPath: CGPath?
var parent: NodeOutput? {
parentTextNode
}
var xform: CATransform3D {
get {
_xform ?? parentTextNode?.xform ?? CATransform3DIdentity
}
set {
_xform = newValue
}
}
var opacity: CGFloat {
get {
_opacity ?? parentTextNode?.opacity ?? 1
}
set {
_opacity = newValue
}
}
var strokeColor: CGColor? {
get {
_strokeColor ?? parentTextNode?.strokeColor
}
set {
_strokeColor = newValue
}
}
var fillColor: CGColor? {
get {
_fillColor ?? parentTextNode?.fillColor
}
set {
_fillColor = newValue
}
}
var tracking: CGFloat {
get {
_tracking ?? parentTextNode?.tracking ?? 0
}
set {
_tracking = newValue
}
}
var strokeWidth: CGFloat {
get {
_strokeWidth ?? parentTextNode?.strokeWidth ?? 0
}
set {
_strokeWidth = newValue
}
}
func hasOutputUpdates(_: CGFloat) -> Bool {
// TODO Fix This
true
}
// MARK: Fileprivate
fileprivate var _xform: CATransform3D?
fileprivate var _opacity: CGFloat?
fileprivate var _strokeColor: CGColor?
fileprivate var _fillColor: CGColor?
fileprivate var _tracking: CGFloat?
fileprivate var _strokeWidth: CGFloat?
}
// MARK: - TextAnimatorNode
class TextAnimatorNode: AnimatorNode {
// MARK: Lifecycle
init(parentNode: TextAnimatorNode?, textAnimator: TextAnimator) {
textOutputNode = TextOutputNode(parent: parentNode?.textOutputNode)
textAnimatorProperties = TextAnimatorNodeProperties(textAnimator: textAnimator)
self.parentNode = parentNode
}
// MARK: Internal
let textOutputNode: TextOutputNode
let textAnimatorProperties: TextAnimatorNodeProperties
let parentNode: AnimatorNode?
var hasLocalUpdates = false
var hasUpstreamUpdates = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled = true
var outputNode: NodeOutput {
textOutputNode
}
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap & KeypathSearchable {
textAnimatorProperties
}
func localUpdatesPermeateDownstream() -> Bool {
true
}
func rebuildOutputs(frame _: CGFloat) {
textOutputNode.xform = textAnimatorProperties.caTransform
textOutputNode.opacity = (textAnimatorProperties.opacity?.value.cgFloatValue ?? 100) * 0.01
textOutputNode.strokeColor = textAnimatorProperties.strokeColor?.value.cgColorValue
textOutputNode.fillColor = textAnimatorProperties.fillColor?.value.cgColorValue
textOutputNode.tracking = textAnimatorProperties.tracking?.value.cgFloatValue ?? 1
textOutputNode.strokeWidth = textAnimatorProperties.strokeWidth?.value.cgFloatValue ?? 0
}
}
@@ -0,0 +1,198 @@
//
// AnimatorNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/15/19.
//
import Foundation
import QuartzCore
// MARK: - NodeOutput
/// Defines the basic outputs of an animator node.
///
protocol NodeOutput {
/// The parent node.
var parent: NodeOutput? { get }
/// Returns true if there are any updates upstream. OutputPath must be built before returning.
func hasOutputUpdates(_ forFrame: CGFloat) -> Bool
var outputPath: CGPath? { get }
var isEnabled: Bool { get set }
}
// MARK: - AnimatorNode
/// The Animator Node is the base node in the render system tree.
///
/// It defines a single node that has an output path and option input node.
/// At animation time the root animation node is asked to update its contents for
/// the current frame.
/// The node reaches up its chain of nodes until the first node that does not need
/// updating is found. Then each node updates its contents down the render pipeline.
/// Each node adds its local path to its input path and passes it forward.
///
/// An animator node holds a group of interpolators. These interpolators determine
/// if the node needs an update for the current frame.
///
protocol AnimatorNode: AnyObject, KeypathSearchable {
/// The available properties of the Node.
///
/// These properties are automatically updated each frame.
/// These properties are also settable and gettable through the dynamic
/// property system.
///
var propertyMap: NodePropertyMap & KeypathSearchable { get }
/// The upstream input node
var parentNode: AnimatorNode? { get }
/// The output of the node.
var outputNode: NodeOutput { get }
/// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true.
func rebuildOutputs(frame: CGFloat)
/// Setters for marking current node state.
var isEnabled: Bool { get set }
var hasLocalUpdates: Bool { get set }
var hasUpstreamUpdates: Bool { get set }
var lastUpdateFrame: CGFloat? { get set }
// MARK: Optional
/// Marks if updates to this node affect nodes downstream.
func localUpdatesPermeateDownstream() -> Bool
func forceUpstreamOutputUpdates() -> Bool
/// Called at the end of this nodes update cycle. Always called. Optional.
func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool
func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool)
/// The default simply returns `hasLocalUpdates`
func shouldRebuildOutputs(frame: CGFloat) -> Bool
}
/// Basic Node Logic
extension AnimatorNode {
func shouldRebuildOutputs(frame _: CGFloat) -> Bool {
hasLocalUpdates
}
func localUpdatesPermeateDownstream() -> Bool {
/// Optional override
true
}
func forceUpstreamOutputUpdates() -> Bool {
/// Optional
false
}
func performAdditionalLocalUpdates(frame _: CGFloat, forceLocalUpdate: Bool) -> Bool {
/// Optional
forceLocalUpdate
}
func performAdditionalOutputUpdates(_: CGFloat, forceOutputUpdate _: Bool) {
/// Optional
}
@discardableResult
func updateOutputs(_ frame: CGFloat, forceOutputUpdate: Bool) -> Bool {
guard isEnabled else {
// Disabled node, pass through.
lastUpdateFrame = frame
return parentNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate) ?? false
}
if forceOutputUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame {
/// This node has already updated for this frame. Go ahead and return the results.
return hasUpstreamUpdates || hasLocalUpdates
}
/// Ask if this node should force output updates upstream.
let forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates()
/// Perform upstream output updates. Optionally mark upstream updates if any.
hasUpstreamUpdates = (
parentNode?
.updateOutputs(frame, forceOutputUpdate: forceUpstreamUpdates) ?? false || hasUpstreamUpdates)
/// Perform additional local output updates
performAdditionalOutputUpdates(frame, forceOutputUpdate: forceUpstreamUpdates)
/// If there are local updates, or if updates have been force, rebuild outputs
if forceUpstreamUpdates || shouldRebuildOutputs(frame: frame) {
lastUpdateFrame = frame
rebuildOutputs(frame: frame)
}
return hasUpstreamUpdates || hasLocalUpdates
}
/// Rebuilds the content of this node, and upstream nodes if necessary.
@discardableResult
func updateContents(_ frame: CGFloat, forceLocalUpdate: Bool) -> Bool {
guard isEnabled else {
// Disabled node, pass through.
return parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false
}
if forceLocalUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame {
/// This node has already updated for this frame. Go ahead and return the results.
return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates
}
/// Are there local updates? If so mark the node.
hasLocalUpdates = forceLocalUpdate ? forceLocalUpdate : propertyMap.needsLocalUpdate(frame: frame)
/// Were there upstream updates? If so mark the node
hasUpstreamUpdates = parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false
/// Perform property updates if necessary.
if hasLocalUpdates {
/// Rebuild local properties
propertyMap.updateNodeProperties(frame: frame)
}
/// Ask the node to perform any other updates it might have.
hasUpstreamUpdates = performAdditionalLocalUpdates(frame: frame, forceLocalUpdate: forceLocalUpdate) || hasUpstreamUpdates
/// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream.
return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates
}
func updateTree(_ frame: CGFloat, forceUpdates: Bool = false) {
updateContents(frame, forceLocalUpdate: forceUpdates)
updateOutputs(frame, forceOutputUpdate: forceUpdates)
}
}
extension AnimatorNode {
/// Default implementation for Keypath searchable.
/// Forward all calls to the propertyMap.
var keypathName: String {
propertyMap.keypathName
}
var keypathProperties: [String: AnyNodeProperty] {
propertyMap.keypathProperties
}
var childKeypaths: [KeypathSearchable] {
propertyMap.childKeypaths
}
var keypathLayer: CALayer? {
nil
}
}
@@ -0,0 +1,22 @@
//
// PathNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import Foundation
// MARK: - PathNode
protocol PathNode {
var pathOutput: PathOutputNode { get }
}
extension PathNode where Self: AnimatorNode {
var outputNode: NodeOutput {
pathOutput
}
}
@@ -0,0 +1,62 @@
//
// RenderNode.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/17/19.
//
import CoreGraphics
import Foundation
import QuartzCore
// MARK: - RenderNode
/// A protocol that defines a node that holds render instructions
protocol RenderNode {
var renderer: Renderable & NodeOutput { get }
}
// MARK: - Renderable
/// A protocol that defines anything with render instructions
protocol Renderable {
/// The last frame in which this node was updated.
var hasUpdate: Bool { get }
func hasRenderUpdates(_ forFrame: CGFloat) -> Bool
/// Determines if the renderer requires a custom context for drawing.
/// If yes the shape layer will perform a custom drawing pass.
/// If no the shape layer will be a standard CAShapeLayer
var shouldRenderInContext: Bool { get }
/// Passes in the CAShapeLayer to update
func updateShapeLayer(layer: CAShapeLayer)
/// Asks the renderer what the renderable bounds is for the given box.
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect
/// Opportunity for renderers to inject sublayers
func setupSublayers(layer: CAShapeLayer)
/// Renders the shape in a custom context
func render(_ inContext: CGContext)
}
extension RenderNode where Self: AnimatorNode {
var outputNode: NodeOutput {
renderer
}
}
extension Renderable {
func renderBoundsFor(_ boundingBox: CGRect) -> CGRect {
/// Optional
boundingBox
}
}
@@ -0,0 +1,75 @@
//
// ShapeContainerLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
/// The base layer that holds Shapes and Shape Renderers
class ShapeContainerLayer: CALayer {
// MARK: Lifecycle
override init() {
super.init()
actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"transform" : NSNull(),
"opacity" : NSNull(),
"hidden" : NSNull(),
]
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(layer: Any) {
guard let layer = layer as? ShapeContainerLayer else {
fatalError("init(layer:) wrong class.")
}
super.init(layer: layer)
}
// MARK: Internal
private(set) var renderLayers: [ShapeContainerLayer] = []
var renderScale: CGFloat = 1 {
didSet {
updateRenderScale()
}
}
func insertRenderLayer(_ layer: ShapeContainerLayer) {
renderLayers.append(layer)
insertSublayer(layer, at: 0)
}
func markRenderUpdates(forFrame: CGFloat) {
if hasRenderUpdate(forFrame: forFrame) {
rebuildContents(forFrame: forFrame)
}
guard isHidden == false else { return }
renderLayers.forEach { $0.markRenderUpdates(forFrame: forFrame) }
}
func hasRenderUpdate(forFrame _: CGFloat) -> Bool {
false
}
func rebuildContents(forFrame _: CGFloat) {
/// Override
}
func updateRenderScale() {
contentsScale = renderScale
renderLayers.forEach( { $0.renderScale = renderScale } )
}
}
@@ -0,0 +1,100 @@
//
// RenderLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/18/19.
//
import Foundation
import QuartzCore
/// The layer responsible for rendering shape objects
final class ShapeRenderLayer: ShapeContainerLayer {
// MARK: Lifecycle
init(renderer: Renderable & NodeOutput) {
self.renderer = renderer
super.init()
anchorPoint = .zero
actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"path" : NSNull(),
"transform" : NSNull(),
"opacity" : NSNull(),
"hidden" : NSNull(),
]
shapeLayer.actions = [
"position" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"path" : NSNull(),
"fillColor" : NSNull(),
"strokeColor" : NSNull(),
"lineWidth" : NSNull(),
"miterLimit" : NSNull(),
"lineDashPhase" : NSNull(),
"opacity": NSNull(),
"hidden" : NSNull(),
]
addSublayer(shapeLayer)
renderer.setupSublayers(layer: shapeLayer)
}
override init(layer: Any) {
guard let layer = layer as? ShapeRenderLayer else {
fatalError("init(layer:) wrong class.")
}
renderer = layer.renderer
super.init(layer: layer)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
fileprivate(set) var renderer: Renderable & NodeOutput
let shapeLayer = CAShapeLayer()
override func hasRenderUpdate(forFrame: CGFloat) -> Bool {
isHidden = !renderer.isEnabled
guard isHidden == false else { return false }
return renderer.hasRenderUpdates(forFrame)
}
override func rebuildContents(forFrame _: CGFloat) {
if renderer.shouldRenderInContext {
if let newPath = renderer.outputPath {
bounds = renderer.renderBoundsFor(newPath.boundingBox)
} else {
bounds = .zero
}
position = bounds.origin
setNeedsDisplay()
} else {
shapeLayer.path = renderer.outputPath
renderer.updateShapeLayer(layer: shapeLayer)
}
}
override func draw(in ctx: CGContext) {
if let path = renderer.outputPath {
if !path.isEmpty {
ctx.addPath(path)
}
}
renderer.render(ctx)
}
override func updateRenderScale() {
super.updateRenderScale()
shapeLayer.contentsScale = renderScale
}
}