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,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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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,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()
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) })
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user