Lottie tests [skip ci]

This commit is contained in:
Isaac
2024-05-08 22:43:27 +04:00
parent 9c017f9f03
commit 9fcef12d55
426 changed files with 39240 additions and 428 deletions

View File

@@ -0,0 +1,77 @@
//
// 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
{
if let xformedPath = rootPath.copy(using: &xform) {
/// Now add root path. Note root path is transformed.
newPath.addPath(xformedPath)
}
}
outputPath = newPath
}
return upstreamUpdates || localUpdates
}
}

View File

@@ -0,0 +1,70 @@
#ifndef PassThroughOutputNode_hpp
#define PassThroughOutputNode_hpp
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasRenderUpdates.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/HasUpdate.hpp"
#include "Lottie/Private/MainThread/NodeRenderSystem/Protocols/NodeOutput.hpp"
namespace lottie {
class PassThroughOutputNode: virtual public NodeOutput, virtual public HasRenderUpdates, virtual public HasUpdate {
public:
PassThroughOutputNode(std::shared_ptr<NodeOutput> parent) :
_parent(parent) {
}
virtual std::shared_ptr<NodeOutput> parent() override {
return _parent;
}
virtual bool isEnabled() const override {
return _isEnabled;
}
virtual void setIsEnabled(bool isEnabled) override {
_isEnabled = isEnabled;
}
virtual bool hasUpdate() override {
return _hasUpdate;
}
void setHasUpdate(bool hasUpdate) {
_hasUpdate = hasUpdate;
}
virtual std::shared_ptr<CGPath> outputPath() override {
if (_parent) {
return _parent->outputPath();
}
return nullptr;
}
virtual bool hasOutputUpdates(double forFrame) override {
/// Changes to this node do not affect downstream nodes.
bool parentUpdate = false;
if (_parent) {
parentUpdate = _parent->hasOutputUpdates(forFrame);
}
/// Changes to upstream nodes do, however, affect this nodes state.
_hasUpdate = _hasUpdate || parentUpdate;
return parentUpdate;
}
virtual bool hasRenderUpdates(double forFrame) override {
/// Return true if there are upstream updates or if this node has updates
bool upstreamUpdates = false;
if (_parent) {
upstreamUpdates = _parent->hasOutputUpdates(forFrame);
}
_hasUpdate = _hasUpdate || upstreamUpdates;
return _hasUpdate;
}
private:
std::shared_ptr<NodeOutput> _parent;
bool _hasUpdate = false;
bool _isEnabled = true;
};
}
#endif /* PassThroughOutputNode_hpp */

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,311 @@
//
// GradientFillRenderer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/30/19.
//
import Foundation
import QuartzCore
public var lottieSwift_getPathNativeBoundingBox: ((CGPath) -> CGRect)?
// MARK: - GradientFillLayer
extension CGPath {
var stringRepresentation: String {
var result = ""
var indent = 1
self.applyWithBlock { element in
let indentString = Array<String>(repeating: " ", count: indent * 2).joined(separator: "")
switch element.pointee.type {
case .moveToPoint:
let point = element.pointee.points.advanced(by: 0).pointee
result += indentString + (NSString(format: "moveto (%10.15f, %10.15f)\n", point.x, point.y) as String)
indent += 1
case .addLineToPoint:
let point = element.pointee.points.advanced(by: 0).pointee
result += indentString + (NSString(format: "lineto (%10.15f, %10.15f)\n", point.x, point.y) as String)
case .addCurveToPoint:
let cp1 = element.pointee.points.advanced(by: 0).pointee
let cp2 = element.pointee.points.advanced(by: 1).pointee
let point = element.pointee.points.advanced(by: 2).pointee
result += indentString + (NSString(format: "curveto (%10.15f, %10.15f) (%10.15f, %10.15f) (%10.15f, %10.15f)\n", cp1.x, cp1.y, cp2.x, cp2.y, point.x, point.y) as String)
case .addQuadCurveToPoint:
let cp = element.pointee.points.advanced(by: 0).pointee
let point = element.pointee.points.advanced(by: 1).pointee
result += indentString + (NSString(format: "quadcurveto (%10.15f, %10.15f) (%10.15f, %10.15f)\n", cp.x, cp.y, point.x, point.y) as String)
case .closeSubpath:
result += indentString + "closepath\n"
indent -= 1
@unknown default:
break
}
}
return result
}
}
private final class GradientFillLayer: CALayer, LottieDrawingLayer {
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 alphaValues = [CGFloat]()
var alphaLocations = [CGFloat]()
var gradientColors = [Color]()
var colorLocations = [CGFloat]()
let colorSpace = ctx.colorSpace ?? CGColorSpaceCreateDeviceRGB()
for i in 0..<numberOfColors {
let ix = i * 4
if colors.count > ix {
gradientColors.append(Color(r: colors[ix + 1], g: colors[ix + 2], b: colors[ix + 3], a: 1.0))
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
}
alphaLocations.append(colors[i])
alphaValues.append(alpha)
}
if drawMask {
var locations: [CGFloat] = []
for i in 0 ..< min(gradientColors.count, colorLocations.count) {
if !locations.contains(colorLocations[i]) {
locations.append(colorLocations[i])
}
}
for i in 0 ..< min(alphaValues.count, alphaLocations.count) {
if !locations.contains(alphaLocations[i]) {
locations.append(alphaLocations[i])
}
}
locations.sort()
if locations[0] != 0.0 {
locations.insert(0.0, at: 0)
}
if locations[locations.count - 1] != 1.0 {
locations.append(1.0)
}
var colors: [Color] = []
for location in locations {
var color: Color?
for i in 0 ..< min(gradientColors.count, colorLocations.count) - 1 {
if location >= colorLocations[i] && location <= colorLocations[i + 1] {
let localLocation: Double
if colorLocations[i] != colorLocations[i + 1] {
localLocation = location.remap(fromLow: colorLocations[i], fromHigh: colorLocations[i + 1], toLow: 0.0, toHigh: 1.0)
} else {
localLocation = 0.0
}
let fromColor = gradientColors[i]
let toColor = gradientColors[i + 1]
color = fromColor.interpolate(to: toColor, amount: localLocation)
break
}
}
var alpha: CGFloat?
for i in 0 ..< min(alphaValues.count, alphaLocations.count) - 1 {
if location >= alphaLocations[i] && location <= alphaLocations[i + 1] {
let localLocation: Double
if alphaLocations[i] != alphaLocations[i + 1] {
localLocation = location.remap(fromLow: alphaLocations[i], fromHigh: alphaLocations[i + 1], toLow: 0.0, toHigh: 1.0)
} else {
localLocation = 0.0
}
let fromAlpha = alphaValues[i]
let toAlpha = alphaValues[i + 1]
alpha = fromAlpha.interpolate(to: toAlpha, amount: localLocation)
break
}
}
var resultColor = color ?? gradientColors[0]
resultColor.a = alpha ?? 1.0
/*resultColor.r = 1.0
resultColor.g = 0.0
resultColor.b = 0.0
resultColor.a = 1.0*/
colors.append(resultColor)
}
gradientColors = colors
colorLocations = locations
}
let cgGradientColors: [CGColor] = gradientColors.map { color -> CGColor in
return color.cgColorValue(colorSpace: colorSpace)
}
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgGradientColors 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 = lottieSwift_getPathNativeBoundingBox!(path)
let anchor = (frame.size.width.isZero || frame.size.height.isZero) ? CGPoint() : CGPoint(
x: -frame.origin.x / frame.size.width,
y: -frame.origin.y / frame.size.height)
maskLayer.path = path
maskLayer.bounds = frame
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 = LottieCAShapeLayer()
}

View File

@@ -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 = GradientFillRenderer(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: GradientFillRenderer
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)
}
}

View File

@@ -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])
}
}
}

View File

@@ -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)) })
}
}
}