mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Lottie tests [skip ci]
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user