mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
282 lines
9.2 KiB
Swift
282 lines
9.2 KiB
Swift
//
|
|
// TrimPathNode.swift
|
|
// lottie-swift
|
|
//
|
|
// Created by Brandon Withrow on 1/23/19.
|
|
//
|
|
|
|
import Foundation
|
|
import QuartzCore
|
|
|
|
// MARK: - TrimPathProperties
|
|
|
|
final class TrimPathProperties: NodePropertyMap, KeypathSearchable {
|
|
|
|
// MARK: Lifecycle
|
|
|
|
init(trim: Trim) {
|
|
keypathName = trim.name
|
|
start = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.start.keyframes))
|
|
end = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.end.keyframes))
|
|
offset = NodeProperty(provider: KeyframeInterpolator(keyframes: trim.offset.keyframes))
|
|
type = trim.trimType
|
|
keypathProperties = [
|
|
"Start" : start,
|
|
"End" : end,
|
|
"Offset" : offset,
|
|
]
|
|
properties = Array(keypathProperties.values)
|
|
}
|
|
|
|
// MARK: Internal
|
|
|
|
let keypathProperties: [String: AnyNodeProperty]
|
|
let properties: [AnyNodeProperty]
|
|
let keypathName: String
|
|
|
|
let start: NodeProperty<Vector1D>
|
|
let end: NodeProperty<Vector1D>
|
|
let offset: NodeProperty<Vector1D>
|
|
let type: TrimType
|
|
}
|
|
|
|
// MARK: - TrimPathNode
|
|
|
|
final class TrimPathNode: AnimatorNode {
|
|
|
|
// MARK: Lifecycle
|
|
|
|
init(parentNode: AnimatorNode?, trim: Trim, upstreamPaths: [PathOutputNode]) {
|
|
outputNode = PassThroughOutputNode(parent: parentNode?.outputNode)
|
|
self.parentNode = parentNode
|
|
properties = TrimPathProperties(trim: trim)
|
|
self.upstreamPaths = upstreamPaths
|
|
}
|
|
|
|
// MARK: Internal
|
|
|
|
let properties: TrimPathProperties
|
|
|
|
let parentNode: AnimatorNode?
|
|
let outputNode: NodeOutput
|
|
var hasLocalUpdates = false
|
|
var hasUpstreamUpdates = false
|
|
var lastUpdateFrame: CGFloat? = nil
|
|
var isEnabled = true
|
|
|
|
// MARK: Animator Node
|
|
var propertyMap: NodePropertyMap & KeypathSearchable {
|
|
properties
|
|
}
|
|
|
|
func forceUpstreamOutputUpdates() -> Bool {
|
|
hasLocalUpdates || hasUpstreamUpdates
|
|
}
|
|
|
|
func rebuildOutputs(frame: CGFloat) {
|
|
/// Make sure there is a trim.
|
|
let startValue = properties.start.value.cgFloatValue * 0.01
|
|
let endValue = properties.end.value.cgFloatValue * 0.01
|
|
let start = min(startValue, endValue)
|
|
let end = max(startValue, endValue)
|
|
|
|
let offset = properties.offset.value.cgFloatValue.truncatingRemainder(dividingBy: 360) / 360
|
|
|
|
/// No need to trim, it's a full path
|
|
if start == 0, end == 1 {
|
|
return
|
|
}
|
|
|
|
/// All paths are empty.
|
|
if start == end {
|
|
for pathContainer in upstreamPaths {
|
|
pathContainer.removePaths(updateFrame: frame)
|
|
}
|
|
return
|
|
}
|
|
|
|
if properties.type == .simultaneously {
|
|
/// Just trim each path
|
|
for pathContainer in upstreamPaths {
|
|
let pathObjects = pathContainer.removePaths(updateFrame: frame)
|
|
for path in pathObjects {
|
|
// We are treating each compount path as an individual path. Its subpaths are treated as a whole.
|
|
pathContainer.appendPath(
|
|
path.trim(fromPosition: start, toPosition: end, offset: offset, trimSimultaneously: false),
|
|
updateFrame: frame)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
/// Individual path trimming.
|
|
|
|
/// Brace yourself for the below code.
|
|
|
|
/// Normalize lengths with offset.
|
|
var startPosition = (start + offset).truncatingRemainder(dividingBy: 1)
|
|
var endPosition = (end + offset).truncatingRemainder(dividingBy: 1)
|
|
|
|
if startPosition < 0 {
|
|
startPosition = 1 + startPosition
|
|
}
|
|
|
|
if endPosition < 0 {
|
|
endPosition = 1 + endPosition
|
|
}
|
|
if startPosition == 1 {
|
|
startPosition = 0
|
|
}
|
|
if endPosition == 0 {
|
|
endPosition = 1
|
|
}
|
|
|
|
/// First get the total length of all paths.
|
|
var totalLength: CGFloat = 0
|
|
upstreamPaths.forEach({ totalLength = totalLength + $0.totalLength })
|
|
|
|
/// Now determine the start and end cut lengths
|
|
let startLength = startPosition * totalLength
|
|
let endLength = endPosition * totalLength
|
|
var pathStart: CGFloat = 0
|
|
|
|
/// Now loop through all path containers
|
|
for pathContainer in upstreamPaths {
|
|
|
|
let pathEnd = pathStart + pathContainer.totalLength
|
|
|
|
if
|
|
!startLength.isInRange(pathStart, pathEnd) &&
|
|
endLength.isInRange(pathStart, pathEnd)
|
|
{
|
|
// pathStart|=======E----------------------|pathEnd
|
|
// Cut path components, removing after end.
|
|
|
|
let pathCutLength = endLength - pathStart
|
|
let subpaths = pathContainer.removePaths(updateFrame: frame)
|
|
var subpathStart: CGFloat = 0
|
|
for path in subpaths {
|
|
let subpathEnd = subpathStart + path.length
|
|
if pathCutLength < subpathEnd {
|
|
/// This is the subpath that needs to be cut.
|
|
let cutLength = pathCutLength - subpathStart
|
|
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
|
|
pathContainer.appendPath(newPath, updateFrame: frame)
|
|
break
|
|
} else {
|
|
/// Add to container and move on
|
|
pathContainer.appendPath(path, updateFrame: frame)
|
|
}
|
|
if pathCutLength == subpathEnd {
|
|
/// Right on the end. The next subpath is not included. Break.
|
|
break
|
|
}
|
|
subpathStart = subpathEnd
|
|
}
|
|
|
|
} else if
|
|
!endLength.isInRange(pathStart, pathEnd) &&
|
|
startLength.isInRange(pathStart, pathEnd)
|
|
{
|
|
// pathStart|-------S======================|pathEnd
|
|
//
|
|
|
|
// Cut path components, removing before beginning.
|
|
let pathCutLength = startLength - pathStart
|
|
// Clear paths from container
|
|
let subpaths = pathContainer.removePaths(updateFrame: frame)
|
|
var subpathStart: CGFloat = 0
|
|
for path in subpaths {
|
|
let subpathEnd = subpathStart + path.length
|
|
|
|
if subpathStart < pathCutLength, pathCutLength < subpathEnd {
|
|
/// This is the subpath that needs to be cut.
|
|
let cutLength = pathCutLength - subpathStart
|
|
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
|
|
pathContainer.appendPath(newPath, updateFrame: frame)
|
|
} else if pathCutLength <= subpathStart {
|
|
pathContainer.appendPath(path, updateFrame: frame)
|
|
}
|
|
subpathStart = subpathEnd
|
|
}
|
|
} else if
|
|
endLength.isInRange(pathStart, pathEnd) &&
|
|
startLength.isInRange(pathStart, pathEnd)
|
|
{
|
|
// pathStart|-------S============E---------|endLength
|
|
// pathStart|=====E----------------S=======|endLength
|
|
// trim from path beginning to endLength.
|
|
|
|
// Cut path components, removing before beginnings.
|
|
let startCutLength = startLength - pathStart
|
|
let endCutLength = endLength - pathStart
|
|
// Clear paths from container
|
|
let subpaths = pathContainer.removePaths(updateFrame: frame)
|
|
var subpathStart: CGFloat = 0
|
|
for path in subpaths {
|
|
|
|
let subpathEnd = subpathStart + path.length
|
|
|
|
if
|
|
!startCutLength.isInRange(subpathStart, subpathEnd) &&
|
|
!endCutLength.isInRange(subpathStart, subpathEnd)
|
|
{
|
|
// The whole path is included. Add
|
|
// S|==============================|E
|
|
pathContainer.appendPath(path, updateFrame: frame)
|
|
|
|
} else if
|
|
startCutLength.isInRange(subpathStart, subpathEnd) &&
|
|
!endCutLength.isInRange(subpathStart, subpathEnd)
|
|
{
|
|
/// The start of the path needs to be trimmed
|
|
// |-------S======================|E
|
|
let cutLength = startCutLength - subpathStart
|
|
let newPath = path.trim(fromPosition: cutLength / path.length, toPosition: 1, offset: 0, trimSimultaneously: false)
|
|
pathContainer.appendPath(newPath, updateFrame: frame)
|
|
} else if
|
|
!startCutLength.isInRange(subpathStart, subpathEnd) &&
|
|
endCutLength.isInRange(subpathStart, subpathEnd)
|
|
{
|
|
// S|=======E----------------------|
|
|
let cutLength = endCutLength - subpathStart
|
|
let newPath = path.trim(fromPosition: 0, toPosition: cutLength / path.length, offset: 0, trimSimultaneously: false)
|
|
pathContainer.appendPath(newPath, updateFrame: frame)
|
|
break
|
|
} else if
|
|
startCutLength.isInRange(subpathStart, subpathEnd) &&
|
|
endCutLength.isInRange(subpathStart, subpathEnd)
|
|
{
|
|
// |-------S============E---------|
|
|
let cutFromLength = startCutLength - subpathStart
|
|
let cutToLength = endCutLength - subpathStart
|
|
let newPath = path.trim(
|
|
fromPosition: cutFromLength / path.length,
|
|
toPosition: cutToLength / path.length,
|
|
offset: 0,
|
|
trimSimultaneously: false)
|
|
pathContainer.appendPath(newPath, updateFrame: frame)
|
|
break
|
|
}
|
|
|
|
subpathStart = subpathEnd
|
|
}
|
|
} else if
|
|
(endLength <= pathStart && pathEnd <= startLength) ||
|
|
(startLength <= pathStart && endLength <= pathStart) ||
|
|
(pathEnd <= startLength && pathEnd <= endLength)
|
|
{
|
|
/// The Path needs to be cleared
|
|
pathContainer.removePaths(updateFrame: frame)
|
|
}
|
|
|
|
pathStart = pathEnd
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: Fileprivate
|
|
|
|
fileprivate let upstreamPaths: [PathOutputNode]
|
|
}
|