Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,312 @@
//
// KeypathSearchableExtension.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import Foundation
import QuartzCore
extension KeypathSearchable {
func animatorNodes(for keyPath: AnimationKeypath) -> [AnimatorNode]? {
// Make sure there is a current key path.
guard let currentKey = keyPath.currentKey else { return nil }
// Now try popping the keypath for wildcard / child search
guard let nextKeypath = keyPath.popKey(keypathName) else {
// We may be on the final keypath. Check for match.
if
let node = self as? AnimatorNode,
currentKey.equalsKeypath(keypathName)
{
// This is the final keypath and matches self. Return.s
return [node]
}
/// Nope. Stop Search
return nil
}
var results: [AnimatorNode] = []
if
let node = self as? AnimatorNode,
nextKeypath.currentKey == nil
{
// Keypath matched self and was the final keypath.
results.append(node)
}
for childNode in childKeypaths {
// Check if the child has any nodes matching the next keypath.
if let foundNodes = childNode.animatorNodes(for: nextKeypath) {
results.append(contentsOf: foundNodes)
}
// In this case the current key is fuzzy, and both child and self match the next keyname. Keep digging!
if
currentKey.keyPathType == .fuzzyWildcard,
let nextKeypath = keyPath.nextKeypath,
nextKeypath.equalsKeypath(childNode.keypathName),
let foundNodes = childNode.animatorNodes(for: keyPath)
{
results.append(contentsOf: foundNodes)
}
}
guard results.count > 0 else {
return nil
}
return results
}
func nodeProperties(for keyPath: AnimationKeypath) -> [AnyNodeProperty]? {
guard let nextKeypath = keyPath.popKey(keypathName) else {
/// Nope. Stop Search
return nil
}
/// Keypath matches in some way. Continue the search.
var results: [AnyNodeProperty] = []
/// Check if we have a property keypath yet
if
let propertyKey = nextKeypath.propertyKey,
let property = keypathProperties[propertyKey]
{
/// We found a property!
results.append(property)
}
if nextKeypath.nextKeypath != nil {
/// Now check child keypaths.
for child in childKeypaths {
if let childProperties = child.nodeProperties(for: nextKeypath) {
results.append(contentsOf: childProperties)
}
}
}
guard results.count > 0 else {
return nil
}
return results
}
func layer(for keyPath: AnimationKeypath) -> CALayer? {
if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) {
/// We found our layer!
return keypathLayer
}
guard let nextKeypath = keyPath.popKey(keypathName) else {
/// Nope. Stop Search
return nil
}
/// Now check child keypaths.
for child in childKeypaths {
if let layer = child.layer(for: nextKeypath) {
return layer
}
}
return nil
}
func allLayers(for keyPath: AnimationKeypath) -> [CALayer] {
if keyPath.nextKeypath == nil, let layerKey = keyPath.currentKey, layerKey.equalsKeypath(keypathName) {
/// We found our layer!
if let keypathLayer = self.keypathLayer {
return [keypathLayer]
} else {
return []
}
}
guard let nextKeypath = keyPath.popKey(keypathName) else {
/// Nope. Stop Search
return []
}
/// Now check child keypaths.
var foundSublayers: [CALayer] = []
for child in childKeypaths {
foundSublayers.append(contentsOf: child.allLayers(for: nextKeypath))
}
return foundSublayers
}
func logKeypaths(for keyPath: AnimationKeypath?) {
let newKeypath: AnimationKeypath
if let previousKeypath = keyPath {
newKeypath = previousKeypath.appendingKey(keypathName)
} else {
newKeypath = AnimationKeypath(keys: [keypathName])
}
print(newKeypath.fullPath)
for key in keypathProperties.keys {
print(newKeypath.appendingKey(key).fullPath)
}
for child in childKeypaths {
child.logKeypaths(for: newKeypath)
}
}
func allKeypaths(for keyPath: AnimationKeypath?, predicate: (AnimationKeypath) -> Bool) -> [String] {
var result: [String] = []
let newKeypath: AnimationKeypath
if let previousKeypath = keyPath {
newKeypath = previousKeypath.appendingKey(keypathName)
} else {
newKeypath = AnimationKeypath(keys: [keypathName])
}
if predicate(newKeypath) {
result.append(newKeypath.fullPath)
}
for key in keypathProperties.keys {
let subKey = newKeypath.appendingKey(key)
if predicate(subKey) {
result.append(subKey.fullPath)
}
}
for child in childKeypaths {
result.append(contentsOf: child.allKeypaths(for: newKeypath, predicate: predicate))
}
return result
}
}
extension AnimationKeypath {
var currentKey: String? {
keys.first
}
var nextKeypath: String? {
guard keys.count > 1 else {
return nil
}
return keys[1]
}
var propertyKey: String? {
if nextKeypath == nil {
/// There are no more keypaths. This is a property key.
return currentKey
}
if keys.count == 2, currentKey?.keyPathType == .fuzzyWildcard {
/// The next keypath is the last and the current is a fuzzy key.
return nextKeypath
}
return nil
}
var fullPath: String {
keys.joined(separator: ".")
}
// Pops the top keypath from the stack if the keyname matches.
func popKey(_ keyname: String) -> AnimationKeypath? {
guard
let currentKey = currentKey,
currentKey.equalsKeypath(keyname),
keys.count > 1 else
{
// Current key either doesnt match or we are on the last key.
return nil
}
// Pop the keypath from the stack and return the new stack.
let newKeys: [String]
if currentKey.keyPathType == .fuzzyWildcard {
/// Dont remove if current key is a fuzzy wildcard, and if the next keypath doesnt equal keypathname
if
let nextKeypath = nextKeypath,
nextKeypath.equalsKeypath(keyname)
{
/// Remove next two keypaths. This keypath breaks the wildcard.
var oldKeys = keys
oldKeys.remove(at: 0)
oldKeys.remove(at: 0)
newKeys = oldKeys
} else {
newKeys = keys
}
} else {
var oldKeys = keys
oldKeys.remove(at: 0)
newKeys = oldKeys
}
return AnimationKeypath(keys: newKeys)
}
func appendingKey(_ key: String) -> AnimationKeypath {
var newKeys = keys
newKeys.append(key)
return AnimationKeypath(keys: newKeys)
}
}
extension String {
var keyPathType: KeyType {
switch self {
case "*":
return .wildcard
case "**":
return .fuzzyWildcard
default:
return .specific
}
}
func equalsKeypath(_ keyname: String) -> Bool {
if keyPathType == .wildcard || keyPathType == .fuzzyWildcard {
return true
}
if self == keyname {
return true
}
if let index = firstIndex(of: "*") {
// Wildcard search.
let prefix = String(self.prefix(upTo: index))
let suffix = String(self.suffix(from: self.index(after: index)))
if prefix.count > 0 {
// Match prefix.
if keyname.count < prefix.count {
return false
}
let testPrefix = String(keyname.prefix(upTo: keyname.index(keyname.startIndex, offsetBy: prefix.count)))
if testPrefix != prefix {
// Prefix doesnt match
return false
}
}
if suffix.count > 0 {
// Match suffix.
if keyname.count < suffix.count {
// Suffix doesnt match
return false
}
let index = keyname.index(keyname.endIndex, offsetBy: -suffix.count)
let testSuffix = String(keyname.suffix(from: index))
if testSuffix != suffix {
return false
}
}
return true
}
return false
}
}
// MARK: - KeyType
enum KeyType {
case specific
case wildcard
case fuzzyWildcard
}
@@ -0,0 +1,31 @@
//
// File.swift
//
//
// Created by Denis Koryttsev on 10.05.2022.
//
extension BlendMode {
/// The Core Image filter name for this `BlendMode`, that can be applied to a `CALayer`'s `compositingFilter`.
/// Supported compositing filters are defined here: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71
var filterName: String? {
switch self {
case .normal: return nil
case .multiply: return "multiplyBlendMode"
case .screen: return "screenBlendMode"
case .overlay: return "overlayBlendMode"
case .darken: return "darkenBlendMode"
case .lighten: return "lightenBlendMode"
case .colorDodge: return "colorDodgeBlendMode"
case .colorBurn: return "colorBurnBlendMode"
case .hardLight: return "hardLightBlendMode"
case .softLight: return "softLightBlendMode"
case .difference: return "differenceBlendMode"
case .exclusion: return "exclusionBlendMode"
case .hue: return "hueBlendMode"
case .saturation: return "saturationBlendMode"
case .color: return "colorBlendMode"
case .luminosity: return "luminosityBlendMode"
}
}
}
@@ -0,0 +1,22 @@
// Created by Cal Stephens on 1/7/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.
import QuartzCore
extension CGColor {
/// Initializes a `CGColor` using the given `RGB` values
static func rgb(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> CGColor {
if #available(iOS 13.0, tvOS 13.0, macOS 10.5, *) {
return CGColor(red: red, green: green, blue: blue, alpha: 1)
} else {
return CGColor(
colorSpace: CGColorSpaceCreateDeviceRGB(),
components: [red, green, blue])!
}
}
/// Initializes a `CGColor` using the given `RGBA` values
static func rgba(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat) -> CGColor {
CGColor.rgb(red, green, blue).copy(alpha: alpha)!
}
}
@@ -0,0 +1,152 @@
//
// CGFloatExtensions.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/14/19.
//
import Foundation
import QuartzCore
extension CGFloat {
// MARK: Internal
var squared: CGFloat {
self * self
}
var cubed: CGFloat {
self * self * self
}
var cubicRoot: CGFloat {
CGFloat(pow(Double(self), 1.0 / 3.0))
}
func isInRangeOrEqual(_ from: CGFloat, _ to: CGFloat) -> Bool {
from <= self && self <= to
}
func isInRange(_ from: CGFloat, _ to: CGFloat) -> Bool {
from < self && self < to
}
func cubicBezierInterpolate(_ P0: CGPoint, _ P1: CGPoint, _ P2: CGPoint, _ P3: CGPoint) -> CGFloat {
var t: CGFloat
if self == P0.x {
// Handle corner cases explicitly to prevent rounding errors
t = 0
} else if self == P3.x {
t = 1
} else {
// Calculate t
let a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x;
let b = 3 * P0.x - 6 * P1.x + 3 * P2.x;
let c = -3 * P0.x + 3 * P1.x;
let d = P0.x - self;
let tTemp = CGFloat.SolveCubic(a, b, c, d);
if tTemp == -1 {
return -1;
}
t = tTemp
}
// Calculate y from t
return (1 - t).cubed * P0.y + 3 * t * (1 - t).squared * P1.y + 3 * t.squared * (1 - t) * P2.y + t.cubed * P3.y;
}
func cubicBezier(_ t: CGFloat, _ c1: CGFloat, _ c2: CGFloat, _ end: CGFloat) -> CGFloat {
let t_ = (1.0 - t)
let tt_ = t_ * t_
let ttt_ = t_ * t_ * t_
let tt = t * t
let ttt = t * t * t
return self * ttt_
+ 3.0 * c1 * tt_ * t
+ 3.0 * c2 * t_ * tt
+ end * ttt;
}
// MARK: Fileprivate
fileprivate static func SolveQuadratic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
var result = (-b + sqrt(b.squared - 4 * a * c)) / (2 * a);
guard !result.isInRangeOrEqual(0, 1) else {
return result
}
result = (-b - sqrt(b.squared - 4 * a * c)) / (2 * a);
guard !result.isInRangeOrEqual(0, 1) else {
return result
}
return -1;
}
fileprivate static func SolveCubic(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat, _ d: CGFloat) -> CGFloat {
if a == 0 {
return SolveQuadratic(b, c, d)
}
if d == 0 {
return 0
}
let a = a
var b = b
var c = c
var d = d
b /= a
c /= a
d /= a
var q = (3.0 * c - b.squared) / 9.0
let r = (-27.0 * d + b * (9.0 * c - 2.0 * b.squared)) / 54.0
let disc = q.cubed + r.squared
let term1 = b / 3.0
if disc > 0 {
var s = r + sqrt(disc)
s = (s < 0) ? -((-s).cubicRoot) : s.cubicRoot
var t = r - sqrt(disc)
t = (t < 0) ? -((-t).cubicRoot) : t.cubicRoot
let result = -term1 + s + t;
if result.isInRangeOrEqual(0, 1) {
return result
}
} else if disc == 0 {
let r13 = (r < 0) ? -((-r).cubicRoot) : r.cubicRoot;
var result = -term1 + 2.0 * r13;
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -(r13 + term1);
if result.isInRangeOrEqual(0, 1) {
return result
}
} else {
q = -q;
var dum1 = q * q * q;
dum1 = acos(r / sqrt(dum1));
let r13 = 2.0 * sqrt(q);
var result = -term1 + r13 * cos(dum1 / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -term1 + r13 * cos((dum1 + 2.0 * .pi) / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
result = -term1 + r13 * cos((dum1 + 4.0 * .pi) / 3.0);
if result.isInRangeOrEqual(0, 1) {
return result
}
}
return -1;
}
}
@@ -0,0 +1,27 @@
//
// DataExtension.swift
// Lottie
//
// Created by René Fouquet on 03.05.21.
//
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
extension Data {
static func jsonData(from assetName: String, in bundle: Bundle) -> Data? {
#if canImport(UIKit)
return NSDataAsset(name: assetName, bundle: bundle)?.data
#else
if #available(macOS 10.11, *) {
return NSDataAsset(name: assetName, bundle: bundle)?.data
}
return nil
#endif
}
}
@@ -0,0 +1,451 @@
//
// MathKit.swift
// UIToolBox
//
// Created by Brandon Withrow on 10/10/18.
//
// From https://github.com/buba447/UIToolBox
import CoreGraphics
import Foundation
extension Int {
var cgFloat: CGFloat {
CGFloat(self)
}
}
extension Double {
var cgFloat: CGFloat {
CGFloat(self)
}
}
// MARK: - CGFloat + Interpolatable
extension CGFloat {
func remap(fromLow: CGFloat, fromHigh: CGFloat, toLow: CGFloat, toHigh: CGFloat) -> CGFloat {
guard (fromHigh - fromLow) != 0 else {
// Would produce NAN
return 0
}
return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
/// Returns a value that is clamped between the two numbers
///
/// 1. The order of arguments does not matter.
func clamp(_ a: CGFloat, _ b: CGFloat) -> CGFloat {
CGFloat(Double(self).clamp(Double(a), Double(b)))
}
/// Returns the difference between the receiver and the given number.
/// - Parameter absolute: If *true* (Default) the returned value will always be positive.
func diff(_ a: CGFloat, absolute: Bool = true) -> CGFloat {
absolute ? abs(a - self) : a - self
}
func toRadians() -> CGFloat { self * .pi / 180 }
func toDegrees() -> CGFloat { self * 180 / .pi }
}
// MARK: - Double
extension Double {
func remap(fromLow: Double, fromHigh: Double, toLow: Double, toHigh: Double) -> Double {
toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow)
}
/// Returns a value that is clamped between the two numbers
///
/// 1. The order of arguments does not matter.
func clamp(_ a: Double, _ b: Double) -> Double {
let minValue = a <= b ? a : b
let maxValue = a <= b ? b : a
return max(min(self, maxValue), minValue)
}
}
extension CGRect {
// MARK: Lifecycle
/// Initializes a new CGRect with a center point and size.
init(center: CGPoint, size: CGSize) {
self.init(
x: center.x - (size.width * 0.5),
y: center.y - (size.height * 0.5),
width: size.width,
height: size.height)
}
// MARK: Internal
/// Returns the total area of the rect.
var area: CGFloat {
width * height
}
/// The center point of the rect. Settable.
var center: CGPoint {
get {
CGPoint(x: midX, y: midY)
}
set {
origin = CGPoint(
x: newValue.x - (size.width * 0.5),
y: newValue.y - (size.height * 0.5))
}
}
/// The top left point of the rect. Settable.
var topLeft: CGPoint {
get {
CGPoint(x: minX, y: minY)
}
set {
origin = CGPoint(
x: newValue.x,
y: newValue.y)
}
}
/// The bottom left point of the rect. Settable.
var bottomLeft: CGPoint {
get {
CGPoint(x: minX, y: maxY)
}
set {
origin = CGPoint(
x: newValue.x,
y: newValue.y - size.height)
}
}
/// The top right point of the rect. Settable.
var topRight: CGPoint {
get {
CGPoint(x: maxX, y: minY)
}
set {
origin = CGPoint(
x: newValue.x - size.width,
y: newValue.y)
}
}
/// The bottom right point of the rect. Settable.
var bottomRight: CGPoint {
get {
CGPoint(x: maxX, y: maxY)
}
set {
origin = CGPoint(
x: newValue.x - size.width,
y: newValue.y - size.height)
}
}
}
extension CGSize {
/// Operator convenience to add sizes with +
static func +(left: CGSize, right: CGSize) -> CGSize {
left.add(right)
}
/// Operator convenience to subtract sizes with -
static func -(left: CGSize, right: CGSize) -> CGSize {
left.subtract(right)
}
/// Operator convenience to multiply sizes with *
static func *(left: CGSize, right: CGFloat) -> CGSize {
CGSize(width: left.width * right, height: left.height * right)
}
/// Returns the scale float that will fit the receive inside of the given size.
func scaleThatFits(_ size: CGSize) -> CGFloat {
CGFloat.minimum(width / size.width, height / size.height)
}
/// Adds receiver size to give size.
func add(_ size: CGSize) -> CGSize {
CGSize(width: width + size.width, height: height + size.height)
}
/// Subtracts given size from receiver size.
func subtract(_ size: CGSize) -> CGSize {
CGSize(width: width - size.width, height: height - size.height)
}
/// Multiplies receiver size by the given size.
func multiply(_ size: CGSize) -> CGSize {
CGSize(width: width * size.width, height: height * size.height)
}
}
// MARK: - CGLine
/// A struct that defines a line segment with two CGPoints
struct CGLine {
// MARK: Lifecycle
/// Initializes a line segment with start and end points
init(start: CGPoint, end: CGPoint) {
self.start = start
self.end = end
}
// MARK: Internal
/// The Start of the line segment.
var start: CGPoint
/// The End of the line segment.
var end: CGPoint
/// The length of the line segment.
var length: CGFloat {
end.distanceTo(start)
}
/// Returns a line segment that is normalized to a length of 1
func normalize() -> CGLine {
let len = length
guard len > 0 else {
return self
}
let relativeEnd = end - start
let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len)
let absoluteVector = relativeVector + start
return CGLine(start: start, end: absoluteVector)
}
/// Trims a line segment to the given length
func trimmedToLength(_ toLength: CGFloat) -> CGLine {
let len = length
guard len > 0 else {
return self
}
let relativeEnd = end - start
let relativeVector = CGPoint(x: relativeEnd.x / len, y: relativeEnd.y / len)
let sizedVector = CGPoint(x: relativeVector.x * toLength, y: relativeVector.y * toLength)
let absoluteVector = sizedVector + start
return CGLine(start: start, end: absoluteVector)
}
/// Flips a line vertically and horizontally from the start point.
func flipped() -> CGLine {
let relativeEnd = end - start
let flippedEnd = CGPoint(x: relativeEnd.x * -1, y: relativeEnd.y * -1)
return CGLine(start: start, end: flippedEnd + start)
}
/// Move the line to the new start point.
func transpose(_ toPoint: CGPoint) -> CGLine {
let diff = toPoint - start
let newEnd = end + diff
return CGLine(start: toPoint, end: newEnd)
}
}
infix operator +|
infix operator +-
extension CGPoint {
/// Returns the length between the receiver and *CGPoint.zero*
var vectorLength: CGFloat {
distanceTo(.zero)
}
var isZero: Bool {
x == 0 && y == 0
}
/// Operator convenience to divide points with /
static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs))
}
/// Operator convenience to multiply points with *
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs))
}
/// Operator convenience to add points with +
static func +(left: CGPoint, right: CGPoint) -> CGPoint {
left.add(right)
}
/// Operator convenience to subtract points with -
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
left.subtract(right)
}
static func +|(left: CGPoint, right: CGFloat) -> CGPoint {
CGPoint(x: left.x, y: left.y + right)
}
static func +-(left: CGPoint, right: CGFloat) -> CGPoint {
CGPoint(x: left.x + right, y: left.y)
}
/// Returns the distance between the receiver and the given point.
func distanceTo(_ a: CGPoint) -> CGFloat {
let xDist = a.x - x
let yDist = a.y - y
return CGFloat(sqrt((xDist * xDist) + (yDist * yDist)))
}
func rounded(decimal: CGFloat) -> CGPoint {
CGPoint(x: round(decimal * x) / decimal, y: round(decimal * y) / decimal)
}
func interpolate(
_ to: CGPoint,
outTangent: CGPoint,
inTangent: CGPoint,
amount: CGFloat,
maxIterations: Int = 3,
samples: Int = 20,
accuracy: CGFloat = 1)
-> CGPoint
{
if amount == 0 {
return self
}
if amount == 1 {
return to
}
if
colinear(outTangent, inTangent) == true,
outTangent.colinear(inTangent, to) == true
{
return interpolate(to: to, amount: amount)
}
let step = 1 / CGFloat(samples)
var points: [(point: CGPoint, distance: CGFloat)] = [(point: self, distance: 0)]
var totalLength: CGFloat = 0
var previousPoint = self
var previousAmount = CGFloat(0)
var closestPoint = 0
while previousAmount < 1 {
previousAmount = previousAmount + step
if previousAmount < amount {
closestPoint = closestPoint + 1
}
let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: previousAmount)
let distance = previousPoint.distanceTo(newPoint)
totalLength = totalLength + distance
points.append((point: newPoint, distance: totalLength))
previousPoint = newPoint
}
let accurateDistance = amount * totalLength
var point = points[closestPoint]
var foundPoint = false
var pointAmount = CGFloat(closestPoint) * step
var nextPointAmount: CGFloat = pointAmount + step
var refineIterations = 0
while foundPoint == false {
refineIterations = refineIterations + 1
/// First see if the next point is still less than the projected length.
let nextPoint = points[min(closestPoint + 1, points.indices.last!)]
if nextPoint.distance < accurateDistance {
point = nextPoint
closestPoint = closestPoint + 1
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
if closestPoint == points.count {
foundPoint = true
}
continue
}
if accurateDistance < point.distance {
closestPoint = closestPoint - 1
if closestPoint < 0 {
foundPoint = true
continue
}
point = points[closestPoint]
pointAmount = CGFloat(closestPoint) * step
nextPointAmount = pointAmount + step
continue
}
/// Now we are certain the point is the closest point under the distance
let pointDiff = nextPoint.distance - point.distance
let proposedPointAmount = ((accurateDistance - point.distance) / pointDiff)
.remap(fromLow: 0, fromHigh: 1, toLow: pointAmount, toHigh: nextPointAmount)
let newPoint = pointOnPath(to, outTangent: outTangent, inTangent: inTangent, amount: proposedPointAmount)
let newDistance = point.distance + point.point.distanceTo(newPoint)
pointAmount = proposedPointAmount
point = (point: newPoint, distance: newDistance)
if
accurateDistance - newDistance <= accuracy ||
newDistance - accurateDistance <= accuracy
{
foundPoint = true
}
if refineIterations == maxIterations {
foundPoint = true
}
}
return point.point
}
func pointOnPath(_ to: CGPoint, outTangent: CGPoint, inTangent: CGPoint, amount: CGFloat) -> CGPoint {
let a = interpolate(to: outTangent, amount: amount)
let b = outTangent.interpolate(to: inTangent, amount: amount)
let c = inTangent.interpolate(to: to, amount: amount)
let d = a.interpolate(to: b, amount: amount)
let e = b.interpolate(to: c, amount: amount)
let f = d.interpolate(to: e, amount: amount)
return f
}
func colinear(_ a: CGPoint, _ b: CGPoint) -> Bool {
let area = x * (a.y - b.y) + a.x * (b.y - y) + b.x * (y - a.y);
let accuracy: CGFloat = 0.05
if area < accuracy && area > -accuracy {
return true
}
return false
}
/// Subtracts the given point from the receiving point.
func subtract(_ point: CGPoint) -> CGPoint {
CGPoint(
x: x - point.x,
y: y - point.y)
}
/// Adds the given point from the receiving point.
func add(_ point: CGPoint) -> CGPoint {
CGPoint(
x: x + point.x,
y: y + point.y)
}
}
@@ -0,0 +1,39 @@
//
// StringExtensions.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/25/19.
//
import CoreGraphics
import Foundation
extension String {
var cgColor: CGColor {
let (red, green, blue) = hexColorComponents()
return .rgb(red, green, blue)
}
func hexColorComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat) {
var cString: String = trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if cString.hasPrefix("#") {
cString.remove(at: cString.startIndex)
}
if (cString.count) != 6 {
return (red: 0, green: 0, blue: 0)
}
var rgbValue: UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return (
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0)
}
}