mirror of
https://github.com/exyte/Macaw.git
synced 2024-10-26 04:49:57 +03:00
Add animation layer hierarchy
This commit is contained in:
parent
916240f118
commit
44dfb89d16
@ -16,6 +16,7 @@
|
||||
575129B61CBD14AF00BD3C2E /* AnimationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575129B51CBD14AF00BD3C2E /* AnimationsView.swift */; };
|
||||
57AF398C1E67E9DB00F0BFE2 /* EventsExampleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AF398B1E67E9DB00F0BFE2 /* EventsExampleController.swift */; };
|
||||
58E4D50C1D841C6E00EC8815 /* TransformExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4D50B1D841C6E00EC8815 /* TransformExampleView.swift */; };
|
||||
5B195EAD2276D5C40008AE8B /* AnimationsHierarchyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B195EAB2276D5C40008AE8B /* AnimationsHierarchyViewController.swift */; };
|
||||
5BAE3CB120C54E3D006BEF51 /* FiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAE3CB020C54E3D006BEF51 /* FiltersViewController.swift */; };
|
||||
66AE19DB1CC8CB3C00B78B5E /* tiger.svg in Resources */ = {isa = PBXBuildFile; fileRef = 66AE19DA1CC8CB3C00B78B5E /* tiger.svg */; };
|
||||
B02E75F11C16104900D1971D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02E75F01C16104900D1971D /* AppDelegate.swift */; };
|
||||
@ -50,6 +51,7 @@
|
||||
575129B51CBD14AF00BD3C2E /* AnimationsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationsView.swift; sourceTree = "<group>"; };
|
||||
57AF398B1E67E9DB00F0BFE2 /* EventsExampleController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsExampleController.swift; sourceTree = "<group>"; };
|
||||
58E4D50B1D841C6E00EC8815 /* TransformExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformExampleView.swift; sourceTree = "<group>"; };
|
||||
5B195EAB2276D5C40008AE8B /* AnimationsHierarchyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationsHierarchyViewController.swift; sourceTree = "<group>"; };
|
||||
5BAE3CB020C54E3D006BEF51 /* FiltersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewController.swift; sourceTree = "<group>"; };
|
||||
66AE19DA1CC8CB3C00B78B5E /* tiger.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = tiger.svg; path = Example/Assets/SVG/tiger.svg; sourceTree = "<group>"; };
|
||||
B02E75ED1C16104900D1971D /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -111,6 +113,7 @@
|
||||
5747F9BB1E38B660004E338F /* Morphing */,
|
||||
57AF398A1E67E86400F0BFE2 /* Events */,
|
||||
5BAE3CAF20C54DA5006BEF51 /* Filters */,
|
||||
5BE9F15D226EEAE500030967 /* AnimationsHierarchy */,
|
||||
574EC4401CB7E2440063F317 /* MenuViewController.swift */,
|
||||
);
|
||||
path = Examples;
|
||||
@ -158,6 +161,14 @@
|
||||
path = Filters;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5BE9F15D226EEAE500030967 /* AnimationsHierarchy */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B195EAB2276D5C40008AE8B /* AnimationsHierarchyViewController.swift */,
|
||||
);
|
||||
path = AnimationsHierarchy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
65CBD13FE29C7F0265D0E051 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -360,6 +371,7 @@
|
||||
574EC4441CBB60AF0063F317 /* AnimationsExampleController.swift in Sources */,
|
||||
5747F9BD1E38B683004E338F /* MorphingView.swift in Sources */,
|
||||
575129B61CBD14AF00BD3C2E /* AnimationsView.swift in Sources */,
|
||||
5B195EAD2276D5C40008AE8B /* AnimationsHierarchyViewController.swift in Sources */,
|
||||
58E4D50C1D841C6E00EC8815 /* TransformExampleView.swift in Sources */,
|
||||
B02E75F11C16104900D1971D /* AppDelegate.swift in Sources */,
|
||||
5BAE3CB120C54E3D006BEF51 /* FiltersViewController.swift in Sources */,
|
||||
|
@ -383,5 +383,38 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-113" y="1196"/>
|
||||
</scene>
|
||||
<!--AnimationsHierarchy-->
|
||||
<scene sceneID="8bU-7m-OgE">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="AnimationsHierarchyViewController" title="Animations Hierarchy" id="T46-H6-N3f" userLabel="AnimationsHierarchy" customClass="AnimationsHierarchyViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="F0r-c7-NMQ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="MiP-PT-hkh"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="ONf-4d-IZ5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WpG-iW-4sM" customClass="MacawView" customModule="Macaw">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="WpG-iW-4sM" firstAttribute="top" secondItem="F0r-c7-NMQ" secondAttribute="bottom" id="IyG-gg-H9z"/>
|
||||
<constraint firstAttribute="trailing" secondItem="WpG-iW-4sM" secondAttribute="trailing" id="Zx6-cu-1ui"/>
|
||||
<constraint firstItem="WpG-iW-4sM" firstAttribute="leading" secondItem="ONf-4d-IZ5" secondAttribute="leading" id="gPb-ic-ege"/>
|
||||
<constraint firstItem="MiP-PT-hkh" firstAttribute="top" secondItem="WpG-iW-4sM" secondAttribute="bottom" id="iOg-aV-zFW"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="animView" destination="WpG-iW-4sM" id="a3r-wa-eYi"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="t7f-Bd-Zcu" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="598" y="1194"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
@ -0,0 +1,64 @@
|
||||
//
|
||||
// AnimationsHierarchyViewController.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Alisa Mylnikova on 29/04/2019.
|
||||
// Copyright © 2019 Exyte. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Macaw
|
||||
|
||||
class AnimationsHierarchyViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var animView: MacawView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
animView.node = createTree(height: 3)
|
||||
animView.zoom.enable()
|
||||
}
|
||||
|
||||
func createTree(height: Int) -> Node {
|
||||
let rect = Rect(w: 10, h: 10)
|
||||
|
||||
let root = createLeaf(childForm: rect, xDelta: Double(UIScreen.main.bounds.width/2), yDelta: 20)
|
||||
createLeavesRecursive(root, maxLevel: height, childForm: rect)
|
||||
return root
|
||||
}
|
||||
|
||||
func createLeavesRecursive(_ root: Group, maxLevel: Int, _ level: Int = 0, childForm: Locus) {
|
||||
if level == maxLevel {
|
||||
return
|
||||
}
|
||||
let (left, right) = createLeaves(root, childForm: childForm, level: level)
|
||||
createLeavesRecursive(left, maxLevel: maxLevel, level + 1, childForm: childForm)
|
||||
createLeavesRecursive(right, maxLevel: maxLevel, level + 1, childForm: childForm)
|
||||
}
|
||||
|
||||
func createLeaves(_ root: Group, childForm: Locus, level: Int) -> (Group, Group) {
|
||||
let delta = Double(90/(level+1))
|
||||
let height = Double(50)
|
||||
let inset = childForm.bounds().w / 2
|
||||
let left = createLeaf(childForm: childForm, xDelta: -delta, yDelta: height)
|
||||
let right = createLeaf(childForm: childForm, xDelta: delta, yDelta: height)
|
||||
let leftBranch = Shape(form: Line(x1: 0, y1: inset, x2: -delta, y2: height - inset), stroke: Stroke(fill: Color.purple, width: 2))
|
||||
let rightBranch = Shape(form: Line(x1: 0, y1: inset, x2: delta, y2: height - inset), stroke: Stroke(fill: Color.purple, width: 2))
|
||||
root.contents.append(contentsOf: [leftBranch, rightBranch, left, right])
|
||||
return (left, right)
|
||||
}
|
||||
|
||||
func createLeaf(childForm: Locus, xDelta: Double, yDelta: Double) -> Group {
|
||||
let inset = childForm.bounds().w / 2
|
||||
let leaf = Shape(form: childForm, fill: Color.teal, place: .move(dx: -inset, dy: -inset))
|
||||
leaf.placeVar.animation(angle: 2 * .pi, during: 5).cycle().play()
|
||||
|
||||
let leafGroup = [leaf].group(place: .move(dx: xDelta, dy: yDelta))
|
||||
leaf.onTap { _ in
|
||||
leafGroup.placeVar.animation(angle: .pi / 4, x: leaf.place.dx, y: leaf.place.dy, during: 2).easing(.elasticOut).autoreversed().play()
|
||||
}
|
||||
return leafGroup
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,7 @@ import UIKit
|
||||
|
||||
open class MenuViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
@IBOutlet var tableView: UITableView?
|
||||
@IBOutlet var tableView: UITableView!
|
||||
|
||||
fileprivate var viewControllers = [
|
||||
"FirstPageViewController",
|
||||
@ -13,7 +13,8 @@ open class MenuViewController: UIViewController, UITableViewDataSource, UITableV
|
||||
"MorphingExampleController",
|
||||
"EventsExampleController",
|
||||
"FiltersViewController",
|
||||
"TextsViewController"
|
||||
"TextsViewController",
|
||||
"AnimationsHierarchyViewController"
|
||||
].map {
|
||||
UIStoryboard(name: "Main", bundle: .none).instantiateViewController(withIdentifier: $0)
|
||||
}
|
||||
@ -24,10 +25,6 @@ open class MenuViewController: UIViewController, UITableViewDataSource, UITableV
|
||||
tableView?.reloadData()
|
||||
}
|
||||
|
||||
open func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewControllers.count
|
||||
}
|
||||
|
@ -16,9 +16,7 @@ class AnimationProducer {
|
||||
|
||||
struct ContentAnimationDesc {
|
||||
let animation: ContentsAnimation
|
||||
let layer: CALayer
|
||||
weak var cache: AnimationCache?
|
||||
let topLayers: [ShapeLayer]
|
||||
let startDate: Date
|
||||
let finishDate: Date
|
||||
let completion: (() -> Void)?
|
||||
@ -92,11 +90,7 @@ class AnimationProducer {
|
||||
return
|
||||
}
|
||||
|
||||
guard let layer = macawView.mLayer else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let cache = macawView.animationCache else {
|
||||
guard let layer = macawView.mLayer, let cache = macawView.animationCache else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -256,39 +250,9 @@ class AnimationProducer {
|
||||
unionBounds = unionBounds?.union(rect: contentsAnimation.getVFunc()(t).group().bounds!)
|
||||
}
|
||||
|
||||
guard let renderer = animation.nodeRenderer, let layer = cache?.layerForNodeRenderer(renderer, context, animation: contentsAnimation, customBounds: unionBounds) else {
|
||||
return
|
||||
}
|
||||
|
||||
var rootRenderer: NodeRenderer? = renderer
|
||||
while rootRenderer?.parentRenderer != nil {
|
||||
rootRenderer = rootRenderer?.parentRenderer
|
||||
}
|
||||
let allRenderers = rootRenderer?.getAllChildrenRecursive()
|
||||
|
||||
var animationRenderers = [NodeRenderer]()
|
||||
if let groupRenderer = renderer as? GroupRenderer {
|
||||
animationRenderers.append(contentsOf: groupRenderer.getAllChildrenRecursive())
|
||||
}
|
||||
let bottomRenderer = animationRenderers.min { $0.zPosition < $1.zPosition }
|
||||
|
||||
var topLayers = [ShapeLayer]()
|
||||
if let bottomRenderer = bottomRenderer, let allRenderers = allRenderers {
|
||||
for renderer in allRenderers
|
||||
where !(renderer is GroupRenderer)
|
||||
&& renderer.zPosition > bottomRenderer.zPosition
|
||||
&& !animationRenderers.contains(renderer) {
|
||||
if let layer = cache?.layerForNodeRenderer(renderer, context, animation: contentsAnimation) {
|
||||
topLayers.append(layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let animationDesc = ContentAnimationDesc(
|
||||
animation: contentsAnimation,
|
||||
layer: layer,
|
||||
cache: cache,
|
||||
topLayers: topLayers,
|
||||
startDate: Date(),
|
||||
finishDate: Date(timeInterval: contentsAnimation.duration, since: startDate),
|
||||
completion: completion
|
||||
@ -322,11 +286,6 @@ class AnimationProducer {
|
||||
continue
|
||||
}
|
||||
|
||||
defer {
|
||||
animationDesc.layer.setNeedsDisplay()
|
||||
animationDesc.layer.displayIfNeeded()
|
||||
}
|
||||
|
||||
let progress = currentDate.timeIntervalSince(animationDesc.startDate) / animation.duration + animation.pausedProgress
|
||||
|
||||
// Completion
|
||||
@ -345,9 +304,6 @@ class AnimationProducer {
|
||||
contentsAnimations.remove(at: count - 1 - index)
|
||||
animationDesc.cache?.freeLayer(renderer)
|
||||
animationDesc.completion?()
|
||||
for layer in animationDesc.topLayers {
|
||||
animationDesc.cache?.freeLayer(layer: layer)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -369,11 +325,6 @@ class AnimationProducer {
|
||||
animation.pausedProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
for layer in animationDesc.topLayers {
|
||||
layer.setNeedsDisplay()
|
||||
layer.displayIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import Foundation
|
||||
class AnimationUtils {
|
||||
|
||||
class func absolutePosition(_ nodeRenderer: NodeRenderer?, _ context: AnimationContext) -> Transform {
|
||||
return AnimationUtils.absoluteTransform(nodeRenderer, context, pos: nodeRenderer?.node()?.place ?? .identity)
|
||||
return AnimationUtils.absoluteTransform(nodeRenderer, context, pos: nodeRenderer?.node.place ?? .identity)
|
||||
}
|
||||
|
||||
class func absoluteTransform(_ nodeRenderer: NodeRenderer?, _ context: AnimationContext, pos: Transform) -> Transform {
|
||||
var transform = pos
|
||||
var parentRenderer = nodeRenderer?.parentRenderer
|
||||
while parentRenderer != nil {
|
||||
if let node = parentRenderer?.node() {
|
||||
if let node = parentRenderer?.node {
|
||||
transform = node.place.concat(with: transform)
|
||||
}
|
||||
parentRenderer = parentRenderer?.parentRenderer
|
||||
@ -20,14 +20,14 @@ class AnimationUtils {
|
||||
|
||||
class func absoluteClip(_ nodeRenderer: NodeRenderer?) -> Locus? {
|
||||
// shouldn't this be a superposition of all parents' clips?
|
||||
let node = nodeRenderer?.node()
|
||||
let node = nodeRenderer?.node
|
||||
if let nodeClip = node?.clip {
|
||||
return nodeClip
|
||||
}
|
||||
|
||||
var parentRenderer = nodeRenderer?.parentRenderer
|
||||
while parentRenderer != nil {
|
||||
if let parentClip = parentRenderer?.node()?.clip {
|
||||
if let parentClip = parentRenderer?.node.clip {
|
||||
return parentClip
|
||||
}
|
||||
|
||||
@ -38,22 +38,4 @@ class AnimationUtils {
|
||||
}
|
||||
|
||||
private static var indexCache = [Node: Int]()
|
||||
|
||||
class func animatedNodes(root: Node, animationCache: AnimationCache) -> [Node] {
|
||||
if animationCache.isAnimating(root) {
|
||||
return [root]
|
||||
}
|
||||
|
||||
guard let rootGroup = root as? Group else {
|
||||
return []
|
||||
}
|
||||
|
||||
var result = [Node]()
|
||||
rootGroup.contents.forEach { child in
|
||||
let childAnimatedNodes = animatedNodes(root: child, animationCache: animationCache)
|
||||
result.append(contentsOf: childAnimatedNodes)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,12 @@ open class Easing {
|
||||
public static let easeIn: Easing = EaseIn()
|
||||
public static let easeOut: Easing = EaseOut()
|
||||
public static let easeInOut: Easing = EaseInOut()
|
||||
public static let elasticOut: Easing = ElasticOut()
|
||||
public static let elasticInOut: Easing = ElasticInOut()
|
||||
|
||||
public static func elasticOut(elasticity: Double = 10.0) -> ElasticOut {
|
||||
return ElasticOut(elasticity: elasticity)
|
||||
}
|
||||
public static func elasticInOut(elasticity: Double = 10.0) -> ElasticInOut {
|
||||
return ElasticInOut(elasticity: elasticity)
|
||||
}
|
||||
@ -51,6 +56,29 @@ private class EaseInOut: Easing {
|
||||
}
|
||||
}
|
||||
|
||||
public class ElasticOut: Easing {
|
||||
let elasticity: Double
|
||||
|
||||
init(elasticity: Double = 10.0) { // less elasticity means more springy effect
|
||||
self.elasticity = elasticity
|
||||
}
|
||||
|
||||
override open func progressFor(time: Double) -> Double {
|
||||
if time == 0 {
|
||||
return 0
|
||||
}
|
||||
let t = time / 0.5
|
||||
if t == 2 {
|
||||
return 1
|
||||
}
|
||||
let p = 0.3
|
||||
let s = p / 4
|
||||
|
||||
let postFix = pow(2, -elasticity * t)
|
||||
return (postFix * sin((t - s) * (2 * .pi) / p ) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
public class ElasticInOut: Easing {
|
||||
let elasticity: Double
|
||||
|
||||
@ -62,19 +90,17 @@ public class ElasticInOut: Easing {
|
||||
if time == 0 {
|
||||
return 0
|
||||
}
|
||||
var t = time / 0.5
|
||||
if t == 2 {
|
||||
let t = time / 0.5 - 1
|
||||
if t == 1 {
|
||||
return 1
|
||||
}
|
||||
let p = 0.3 * 1.5
|
||||
let s = p / 4
|
||||
|
||||
if t < 1 {
|
||||
t -= 1
|
||||
if t < 0 {
|
||||
let postFix = pow(2, elasticity * t)
|
||||
return (-0.5 * (postFix * sin((t - s) * (2 * .pi) / p)))
|
||||
}
|
||||
t -= 1
|
||||
let postFix = pow(2, -elasticity * t)
|
||||
return (postFix * sin((t - s) * (2 * .pi) / p ) * 0.5 + 1)
|
||||
}
|
||||
|
@ -7,9 +7,10 @@
|
||||
//
|
||||
|
||||
class ShapeAnimation: AnimationImpl<Shape> {
|
||||
let toParentGlobalTransfrom: Transform
|
||||
let fromLayoutGlobalTransfrom: Transform
|
||||
let toLayoutGlobalTransfrom: Transform
|
||||
|
||||
convenience init(animatedNode: Shape, finalValue: Shape, toParentGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
convenience init(animatedNode: Shape, finalValue: Shape, fromLayoutGlobalTransfrom: Transform = .identity, toLayoutGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
|
||||
let interpolationFunc = { (t: Double) -> Shape in
|
||||
if t == 0 {
|
||||
@ -19,11 +20,12 @@ class ShapeAnimation: AnimationImpl<Shape> {
|
||||
return finalValue
|
||||
}
|
||||
|
||||
self.init(animatedNode: animatedNode, valueFunc: interpolationFunc, toParentGlobalTransfrom: toParentGlobalTransfrom, animationDuration: animationDuration, delay: delay, autostart: autostart, fps: fps)
|
||||
self.init(animatedNode: animatedNode, valueFunc: interpolationFunc, fromLayoutGlobalTransfrom: fromLayoutGlobalTransfrom, toLayoutGlobalTransfrom: toLayoutGlobalTransfrom, animationDuration: animationDuration, delay: delay, autostart: autostart, fps: fps)
|
||||
}
|
||||
|
||||
init(animatedNode: Shape, valueFunc: @escaping (Double) -> Shape, toParentGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
self.toParentGlobalTransfrom = toParentGlobalTransfrom
|
||||
init(animatedNode: Shape, valueFunc: @escaping (Double) -> Shape, fromLayoutGlobalTransfrom: Transform = .identity, toLayoutGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
self.fromLayoutGlobalTransfrom = fromLayoutGlobalTransfrom
|
||||
self.toLayoutGlobalTransfrom = toLayoutGlobalTransfrom
|
||||
super.init(observableValue: AnimatableVariable<Shape>(animatedNode), valueFunc: valueFunc, animationDuration: animationDuration, delay: delay, fps: fps)
|
||||
type = .shape
|
||||
node = animatedNode
|
||||
@ -33,8 +35,9 @@ class ShapeAnimation: AnimationImpl<Shape> {
|
||||
}
|
||||
}
|
||||
|
||||
init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> Shape)), toParentGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
self.toParentGlobalTransfrom = toParentGlobalTransfrom
|
||||
init(animatedNode: Shape, factory: @escaping (() -> ((Double) -> Shape)), fromLayoutGlobalTransfrom: Transform = .identity, toLayoutGlobalTransfrom: Transform = .identity, animationDuration: Double, delay: Double = 0.0, autostart: Bool = false, fps: UInt = 30) {
|
||||
self.fromLayoutGlobalTransfrom = fromLayoutGlobalTransfrom
|
||||
self.toLayoutGlobalTransfrom = toLayoutGlobalTransfrom
|
||||
super.init(observableValue: AnimatableVariable<Shape>(animatedNode), factory: factory, animationDuration: animationDuration, delay: delay, fps: fps)
|
||||
type = .shape
|
||||
node = animatedNode
|
||||
|
@ -104,35 +104,31 @@ public extension AnimatableVariable where T: TransformInterpolation {
|
||||
|
||||
func animation(angle: Double, x: Double? = .none, y: Double? = .none, during: Double = 1.0, delay: Double = 0.0) -> Animation {
|
||||
let bounds = node!.bounds!
|
||||
let place = node!.place
|
||||
|
||||
let factory = { () -> (Double) -> Transform in { t in
|
||||
let asin = sin(angle * t); let acos = cos(angle * t)
|
||||
|
||||
let rotation = Transform(
|
||||
m11: acos, m12: -asin,
|
||||
m21: asin, m22: acos,
|
||||
dx: 0.0, dy: 0.0
|
||||
m21: asin, m22: acos
|
||||
)
|
||||
|
||||
let move = Transform.move(
|
||||
dx: x ?? bounds.w / 2.0,
|
||||
dy: y ?? bounds.h / 2.0
|
||||
dx: x ?? bounds.x + bounds.w / 2.0,
|
||||
dy: y ?? bounds.y + bounds.h / 2.0
|
||||
)
|
||||
|
||||
let t1 = move.concat(with: rotation)
|
||||
let t2 = t1.concat(with: move.invert()!)
|
||||
let result = t1.concat(with: t2)
|
||||
|
||||
return result
|
||||
return place.concat(with: move).concat(with: rotation).concat(with: move.invert()!)
|
||||
}
|
||||
}
|
||||
|
||||
return TransformAnimation(animatedNode: self.node!, factory: factory, animationDuration: during, delay: delay)
|
||||
return TransformAnimation(animatedNode: node!, factory: factory, animationDuration: during, delay: delay)
|
||||
}
|
||||
|
||||
func animation(along path: Path, during: Double = 1.0, delay: Double = 0.0) -> Animation {
|
||||
|
||||
let factory = { () -> (Double) -> Transform in { (t: Double) in Transform.identity }
|
||||
let factory = { () -> (Double) -> Transform in { (t: Double) in self.node?.place ?? .identity }
|
||||
}
|
||||
return TransformAnimation(animatedNode: self.node!, factory: factory, along: path, animationDuration: during, delay: delay)
|
||||
}
|
||||
|
@ -9,18 +9,17 @@ import AppKit
|
||||
class AnimationCache {
|
||||
|
||||
class CachedLayer {
|
||||
let layer: ShapeLayer
|
||||
let animation: Animation
|
||||
var linksCounter = 1
|
||||
let rootLayer: ShapeLayer
|
||||
let animationLayer: ShapeLayer
|
||||
|
||||
required init(layer: ShapeLayer, animation: Animation) {
|
||||
self.layer = layer
|
||||
self.animation = animation
|
||||
required init(rootLayer: ShapeLayer, animationLayer: ShapeLayer) {
|
||||
self.rootLayer = rootLayer
|
||||
self.animationLayer = animationLayer
|
||||
}
|
||||
}
|
||||
|
||||
weak var sceneLayer: CALayer?
|
||||
var layerCache = [NodeRenderer: CachedLayer]()
|
||||
var cache = [NodeRenderer: CachedLayer]()
|
||||
|
||||
required init(sceneLayer: CALayer) {
|
||||
self.sceneLayer = sceneLayer
|
||||
@ -28,44 +27,45 @@ class AnimationCache {
|
||||
|
||||
func layerForNodeRenderer(_ renderer: NodeRenderer, _ context: AnimationContext, animation: Animation, customBounds: Rect? = .none, shouldRenderContent: Bool = true) -> ShapeLayer {
|
||||
|
||||
guard let node = renderer.node() else {
|
||||
return ShapeLayer()
|
||||
let node = renderer.node
|
||||
if let cachedLayer = cache[renderer] {
|
||||
cachedLayer.rootLayer.transform = CATransform3DMakeAffineTransform(uncachedParentsPlace(renderer).toCG())
|
||||
cachedLayer.animationLayer.opacity = Float(node.opacity)
|
||||
return cachedLayer.animationLayer
|
||||
}
|
||||
|
||||
if let cachedLayer = layerCache[renderer] {
|
||||
cachedLayer.linksCounter += 1
|
||||
return cachedLayer.layer
|
||||
}
|
||||
// 'sublayer' is for actual CAAnimations, and 'layer' is for manual transforming and hierarchy changes
|
||||
let sublayer = ShapeLayer()
|
||||
sublayer.shouldRenderContent = shouldRenderContent
|
||||
sublayer.animationCache = self
|
||||
|
||||
let layer = ShapeLayer()
|
||||
layer.shouldRenderContent = shouldRenderContent
|
||||
layer.animationCache = self
|
||||
layer.addSublayer(sublayer)
|
||||
layer.masksToBounds = false
|
||||
|
||||
// Use to debug animation layers
|
||||
// layer.backgroundColor = MColor.green.cgColor
|
||||
// layer.borderWidth = 1.0
|
||||
// layer.borderColor = MColor.blue.cgColor
|
||||
// sublayer.backgroundColor = MColor.green.cgColor
|
||||
// sublayer.borderWidth = 2.0
|
||||
// sublayer.borderColor = MColor.red.cgColor
|
||||
// layer.backgroundColor = MColor.blue.cgColor
|
||||
// layer.borderWidth = 2.0
|
||||
// layer.borderColor = MColor.cyan.cgColor
|
||||
|
||||
let calculatedBounds = customBounds ?? node.bounds
|
||||
if let shapeBounds = calculatedBounds {
|
||||
let cgRect = shapeBounds.toCG()
|
||||
|
||||
let origFrame = CGRect(x: 0.0, y: 0.0,
|
||||
width: cgRect.width,
|
||||
height: cgRect.height)
|
||||
|
||||
layer.bounds = origFrame
|
||||
layer.anchorPoint = CGPoint(
|
||||
let anchorPoint = CGPoint(
|
||||
x: -1.0 * cgRect.origin.x / cgRect.width,
|
||||
y: -1.0 * cgRect.origin.y / cgRect.height
|
||||
)
|
||||
|
||||
layer.bounds = cgRect
|
||||
sublayer.bounds = cgRect
|
||||
layer.anchorPoint = anchorPoint
|
||||
sublayer.anchorPoint = anchorPoint
|
||||
layer.zPosition = CGFloat(renderer.zPosition)
|
||||
|
||||
layer.renderTransform = CGAffineTransform(translationX: -1.0 * cgRect.origin.x, y: -1.0 * cgRect.origin.y)
|
||||
|
||||
let nodeTransform = AnimationUtils.absolutePosition(renderer, context).toCG()
|
||||
layer.transform = CATransform3DMakeAffineTransform(nodeTransform)
|
||||
|
||||
// Clip
|
||||
if let clip = AnimationUtils.absoluteClip(renderer) {
|
||||
let maskLayer = CAShapeLayer()
|
||||
@ -77,18 +77,48 @@ class AnimationCache {
|
||||
}
|
||||
}
|
||||
|
||||
layer.opacity = Float(node.opacity)
|
||||
layer.renderer = renderer
|
||||
sublayer.opacity = Float(node.opacity)
|
||||
sublayer.renderer = renderer
|
||||
sublayer.contentsScale = calculateAnimationScale(animation: animation)
|
||||
sublayer.setNeedsDisplay()
|
||||
|
||||
layer.contentsScale = calculateAnimationScale(animation: animation)
|
||||
// find first parent with cached layer
|
||||
var parent: NodeRenderer? = renderer.parentRenderer
|
||||
var parentCachedLayer: CALayer? = sceneLayer
|
||||
while parent != nil {
|
||||
if let parent = parent {
|
||||
if let cached = cache[parent] {
|
||||
parentCachedLayer = cached.animationLayer
|
||||
break
|
||||
}
|
||||
}
|
||||
parent = parent?.parentRenderer
|
||||
}
|
||||
layer.transform = CATransform3DMakeAffineTransform(uncachedParentsPlace(renderer).toCG())
|
||||
sublayer.transform = CATransform3DMakeAffineTransform(node.place.toCG())
|
||||
parentCachedLayer?.addSublayer(layer)
|
||||
parentCachedLayer?.setNeedsDisplay()
|
||||
|
||||
layer.setNeedsDisplay()
|
||||
sceneLayer?.addSublayer(layer)
|
||||
cache[renderer] = CachedLayer(rootLayer: layer, animationLayer: sublayer)
|
||||
|
||||
// move children to new layer
|
||||
for child in renderer.getAllChildrenRecursive() {
|
||||
if let cachedChildLayer = cache[child], let parentCachedLayer = parentCachedLayer {
|
||||
parentCachedLayer.sublayers?.forEach { childLayer in
|
||||
if childLayer === cachedChildLayer.rootLayer {
|
||||
|
||||
childLayer.removeFromSuperlayer()
|
||||
childLayer.transform = CATransform3DMakeAffineTransform(uncachedParentsPlace(child).toCG())
|
||||
sublayer.addSublayer(childLayer)
|
||||
sublayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layerCache[renderer] = CachedLayer(layer: layer, animation: animation)
|
||||
sceneLayer?.setNeedsDisplay()
|
||||
|
||||
return layer
|
||||
return sublayer
|
||||
}
|
||||
|
||||
private func calculateAnimationScale(animation: Animation) -> CGFloat {
|
||||
@ -130,106 +160,73 @@ class AnimationCache {
|
||||
}
|
||||
|
||||
func freeLayerHard(_ renderer: NodeRenderer) {
|
||||
guard let cachedLayer = layerCache[renderer] else {
|
||||
freeLayer(renderer)
|
||||
}
|
||||
|
||||
func freeLayer(_ renderer: NodeRenderer?) {
|
||||
guard let nodeRenderer = renderer, let layer = cache[nodeRenderer] else {
|
||||
return
|
||||
}
|
||||
|
||||
let layer = cachedLayer.layer
|
||||
layerCache.removeValue(forKey: renderer)
|
||||
sceneLayer?.setNeedsDisplay()
|
||||
layer.removeFromSuperlayer()
|
||||
}
|
||||
cache.removeValue(forKey: nodeRenderer)
|
||||
|
||||
func freeLayer(layer: ShapeLayer) {
|
||||
var cached: CachedLayer?
|
||||
var renderer: NodeRenderer?
|
||||
layerCache.forEach { key, value in
|
||||
if value.layer === layer {
|
||||
cached = value
|
||||
renderer = key
|
||||
// find first parent with cached layer
|
||||
var parent: NodeRenderer? = nodeRenderer.parentRenderer
|
||||
var parentCachedLayer: CALayer? = sceneLayer
|
||||
while parent != nil {
|
||||
if let parent = parent, let cached = cache[parent] {
|
||||
parentCachedLayer = cached.animationLayer
|
||||
break
|
||||
}
|
||||
parent = parent?.parentRenderer
|
||||
}
|
||||
|
||||
// move children to closest parent layer
|
||||
for child in nodeRenderer.getAllChildrenRecursive() {
|
||||
if let cachedChildLayer = cache[child], let parentCachedLayer = parentCachedLayer {
|
||||
layer.animationLayer.sublayers?.forEach { childLayer in
|
||||
if childLayer === cachedChildLayer.rootLayer {
|
||||
CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions)
|
||||
childLayer.removeFromSuperlayer()
|
||||
childLayer.transform = CATransform3DMakeAffineTransform(uncachedParentsPlace(child).toCG())
|
||||
parentCachedLayer.addSublayer(childLayer)
|
||||
childLayer.setNeedsDisplay()
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let cachedLayer = cached, let nodeRenderer = renderer else {
|
||||
return
|
||||
}
|
||||
|
||||
cachedLayer.linksCounter -= 1
|
||||
|
||||
if cachedLayer.linksCounter != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let layer = cachedLayer.layer
|
||||
layerCache.removeValue(forKey: nodeRenderer)
|
||||
layer.animationLayer.removeFromSuperlayer()
|
||||
layer.rootLayer.removeFromSuperlayer()
|
||||
parentCachedLayer?.setNeedsDisplay()
|
||||
sceneLayer?.setNeedsDisplay()
|
||||
layer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
func freeLayer(_ renderer: NodeRenderer) {
|
||||
guard let cachedLayer = layerCache[renderer] else {
|
||||
return
|
||||
}
|
||||
|
||||
cachedLayer.linksCounter -= 1
|
||||
|
||||
if cachedLayer.linksCounter != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let layer = cachedLayer.layer
|
||||
layerCache.removeValue(forKey: renderer)
|
||||
sceneLayer?.setNeedsDisplay()
|
||||
layer.removeFromSuperlayer()
|
||||
func isAnimating(_ nodeRenderer: NodeRenderer) -> Bool {
|
||||
return cache[nodeRenderer] != nil
|
||||
}
|
||||
|
||||
func isAnimating(_ node: Node) -> Bool {
|
||||
|
||||
let renderer = layerCache.keys.first { $0.node() === node }
|
||||
if let renderer = renderer, let _ = layerCache[renderer] {
|
||||
return true
|
||||
if let renderer = cache.keys.first(where: { $0.node === node }) {
|
||||
return isAnimating(renderer)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isChildrenAnimating(_ group: Group) -> Bool {
|
||||
|
||||
for child in group.contents {
|
||||
if isAnimating(child) {
|
||||
return true
|
||||
}
|
||||
|
||||
if let childGroup = child as? Group {
|
||||
return isChildrenAnimating(childGroup)
|
||||
func uncachedParentsPlace(_ renderer: NodeRenderer) -> Transform {
|
||||
var parent: NodeRenderer? = renderer.parentRenderer
|
||||
var uncachedParentsPlace = Transform.identity
|
||||
while parent != nil {
|
||||
if let parent = parent {
|
||||
if cache[parent] != nil {
|
||||
break
|
||||
}
|
||||
uncachedParentsPlace = uncachedParentsPlace.concat(with: parent.node.place)
|
||||
}
|
||||
parent = parent?.parentRenderer
|
||||
}
|
||||
|
||||
return false
|
||||
return uncachedParentsPlace
|
||||
}
|
||||
|
||||
func containsAnimation(_ node: Node) -> Bool {
|
||||
if isAnimating(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
if let group = node as? Group {
|
||||
return isChildrenAnimating(group)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func animations() -> [Animation] {
|
||||
|
||||
return layerCache.map { $0.1.animation }
|
||||
}
|
||||
|
||||
func replace(original: NodeRenderer, replacement: NodeRenderer) {
|
||||
guard let layer = layerCache[original] else {
|
||||
return
|
||||
}
|
||||
|
||||
layerCache[replacement] = layer
|
||||
layerCache.removeValue(forKey: original)
|
||||
}
|
||||
}
|
||||
|
@ -13,19 +13,21 @@ import AppKit
|
||||
|
||||
extension AnimationProducer {
|
||||
|
||||
func createChildAnimations(_ combineAnimation: Animation, globalToPosition: Transform = .identity, animations: [Animation] = []) -> [Animation] {
|
||||
func createChildAnimations(_ combineAnimation: Animation, fromLayoutGlobalTransfrom: Transform, toLayoutGlobalTransfrom: Transform, animations: [Animation] = []) -> [Animation] {
|
||||
guard let combine = combineAnimation as? CombineAnimation else {
|
||||
return animations
|
||||
}
|
||||
|
||||
let globalToPosition = globalToPosition
|
||||
let during = combine.duration
|
||||
let delay = combine.delay
|
||||
let fromNode = combine.node as! Group
|
||||
let to = combine.toNodes
|
||||
|
||||
let fromContentsCopy = fromNode.contents.compactMap { SceneUtils.copyNode($0) }
|
||||
fromNode.contents = fromContentsCopy
|
||||
|
||||
// Shapes on same hierarhy level
|
||||
let fromShapes = fromNode.contents.compactMap { $0 as? Shape }
|
||||
let fromShapes = fromContentsCopy.compactMap { $0 as? Shape }
|
||||
let toShapes = to.compactMap { $0 as? Shape }
|
||||
let minPathsNumber = min(fromShapes.count, toShapes.count)
|
||||
|
||||
@ -34,7 +36,7 @@ extension AnimationProducer {
|
||||
let fromShape = fromShapes[i]
|
||||
let toShape = toShapes[i]
|
||||
|
||||
let animation = ShapeAnimation(animatedNode: fromShape, finalValue: toShape, toParentGlobalTransfrom: globalToPosition, animationDuration: during, delay: delay)
|
||||
let animation = ShapeAnimation(animatedNode: fromShape, finalValue: toShape, fromLayoutGlobalTransfrom: fromLayoutGlobalTransfrom, toLayoutGlobalTransfrom: toLayoutGlobalTransfrom, animationDuration: during, delay: delay)
|
||||
animations.append(animation)
|
||||
}
|
||||
|
||||
@ -66,7 +68,7 @@ extension AnimationProducer {
|
||||
let toGroup = toGroups[i]
|
||||
|
||||
let groupAnimation = fromGroup.contentsVar.animation(to: toGroup.contents, during: during, delay: delay)
|
||||
let groupAnimations = createChildAnimations(groupAnimation, globalToPosition: globalToPosition.concat(with: toGroup.place), animations: animations)
|
||||
let groupAnimations = createChildAnimations(groupAnimation, fromLayoutGlobalTransfrom: fromLayoutGlobalTransfrom, toLayoutGlobalTransfrom: toLayoutGlobalTransfrom, animations: animations)
|
||||
animations.append(contentsOf: groupAnimations)
|
||||
}
|
||||
|
||||
@ -119,9 +121,10 @@ extension AnimationProducer {
|
||||
}
|
||||
|
||||
var animations = combine.animations
|
||||
if let toBounds = combine.toNodes.group().bounds {
|
||||
let globalTransform = view.contentLayout.layout(rect: toBounds, into: view.frame.size.toMacaw())
|
||||
let childAnimations = createChildAnimations(combine, globalToPosition: globalTransform) as! [BasicAnimation]
|
||||
if let fromBounds = combine.node?.bounds, let toBounds = combine.toNodes.group().bounds {
|
||||
let fromLayoutTransform = view.contentLayout.layout(rect: fromBounds, into: view.frame.size.toMacaw())
|
||||
let toLayoutTransform = view.contentLayout.layout(rect: toBounds, into: view.frame.size.toMacaw())
|
||||
let childAnimations = createChildAnimations(combine, fromLayoutGlobalTransfrom: fromLayoutTransform, toLayoutGlobalTransfrom: toLayoutTransform) as! [BasicAnimation]
|
||||
animations.append(contentsOf: childAnimations)
|
||||
}
|
||||
|
||||
@ -180,6 +183,12 @@ extension AnimationProducer {
|
||||
}
|
||||
}
|
||||
|
||||
CATransaction.setDisableActions(true)
|
||||
defer {
|
||||
CATransaction.commit()
|
||||
//CATransaction.flush()
|
||||
}
|
||||
|
||||
// Launching
|
||||
animations.forEach { animation in
|
||||
self.play(animation, context)
|
||||
|
@ -23,9 +23,8 @@ func addMorphingAnimation(_ animation: BasicAnimation, _ context: AnimationConte
|
||||
return
|
||||
}
|
||||
|
||||
let mutatingShape = SceneUtils.shapeCopy(from: shape)
|
||||
renderer.replaceNode(with: mutatingShape)
|
||||
animation.node = mutatingShape
|
||||
let transactionsDisabled = CATransaction.disableActions()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
||||
let fromLocus = morphingAnimation.getVFunc()(0.0)
|
||||
let toLocus = morphingAnimation.getVFunc()(animation.autoreverses ? 0.5 : 1.0)
|
||||
@ -35,24 +34,29 @@ func addMorphingAnimation(_ animation: BasicAnimation, _ context: AnimationConte
|
||||
return
|
||||
}
|
||||
// Creating proper animation
|
||||
let generatedAnim = pathAnimation(
|
||||
let generatedAnimation = pathAnimation(
|
||||
from: fromLocus,
|
||||
to: toLocus,
|
||||
duration: duration,
|
||||
renderTransform: layer.renderTransform!)
|
||||
duration: duration)
|
||||
|
||||
generatedAnim.repeatCount = Float(animation.repeatCount)
|
||||
generatedAnim.timingFunction = caTimingFunction(animation.easing)
|
||||
generatedAnim.autoreverses = animation.autoreverses
|
||||
generatedAnimation.repeatCount = Float(animation.repeatCount)
|
||||
generatedAnimation.timingFunction = caTimingFunction(animation.easing)
|
||||
generatedAnimation.autoreverses = animation.autoreverses
|
||||
|
||||
generatedAnim.completion = { finished in
|
||||
generatedAnimation.progress = { progress in
|
||||
let t = Double(progress)
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
generatedAnimation.completion = { finished in
|
||||
|
||||
if animation.manualStop {
|
||||
animation.progress = 0.0
|
||||
mutatingShape.form = morphingAnimation.getVFunc()(0.0)
|
||||
shape.form = morphingAnimation.getVFunc()(0.0)
|
||||
} else if finished {
|
||||
animation.progress = 1.0
|
||||
mutatingShape.form = morphingAnimation.getVFunc()(1.0)
|
||||
shape.form = morphingAnimation.getVFunc()(1.0)
|
||||
}
|
||||
|
||||
animationCache?.freeLayer(renderer)
|
||||
@ -65,19 +69,10 @@ func addMorphingAnimation(_ animation: BasicAnimation, _ context: AnimationConte
|
||||
completion()
|
||||
}
|
||||
|
||||
generatedAnim.progress = { progress in
|
||||
|
||||
let t = Double(progress)
|
||||
mutatingShape.form = morphingAnimation.getVFunc()(t)
|
||||
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
layer.path = fromLocus.toCGPath()
|
||||
|
||||
// Stroke
|
||||
if let stroke = mutatingShape.stroke {
|
||||
if let stroke = shape.stroke {
|
||||
if let color = stroke.fill as? Color {
|
||||
layer.strokeColor = color.toCG()
|
||||
} else {
|
||||
@ -91,24 +86,27 @@ func addMorphingAnimation(_ animation: BasicAnimation, _ context: AnimationConte
|
||||
}
|
||||
|
||||
// Fill
|
||||
if let color = mutatingShape.fill as? Color {
|
||||
if let color = shape.fill as? Color {
|
||||
layer.fillColor = color.toCG()
|
||||
} else {
|
||||
layer.fillColor = MColor.clear.cgColor
|
||||
}
|
||||
|
||||
let animationId = animation.ID
|
||||
layer.add(generatedAnim, forKey: animationId)
|
||||
layer.add(generatedAnimation, forKey: animationId)
|
||||
animation.removeFunc = { [weak layer] in
|
||||
layer?.removeAnimation(forKey: animationId)
|
||||
}
|
||||
|
||||
if !transactionsDisabled {
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func pathAnimation(from: Locus, to: Locus, duration: Double, renderTransform: CGAffineTransform) -> CAAnimation {
|
||||
fileprivate func pathAnimation(from: Locus, to: Locus, duration: Double) -> CAAnimation {
|
||||
|
||||
var transform = renderTransform
|
||||
let fromPath = from.toCGPath().copy(using: &transform)
|
||||
let toPath = to.toCGPath().copy(using: &transform)
|
||||
let fromPath = from.toCGPath()
|
||||
let toPath = to.toCGPath()
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "path")
|
||||
animation.fromValue = fromPath
|
||||
|
@ -15,6 +15,9 @@ func addOpacityAnimation(_ animation: BasicAnimation, _ context: AnimationContex
|
||||
return
|
||||
}
|
||||
|
||||
let transactionsDisabled = CATransaction.disableActions()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
||||
// Creating proper animation
|
||||
let generatedAnimation = opacityAnimationByFunc(opacityAnimation.getVFunc(),
|
||||
duration: animation.getDuration(),
|
||||
@ -23,6 +26,12 @@ func addOpacityAnimation(_ animation: BasicAnimation, _ context: AnimationContex
|
||||
generatedAnimation.repeatCount = Float(animation.repeatCount)
|
||||
generatedAnimation.timingFunction = caTimingFunction(animation.easing)
|
||||
|
||||
generatedAnimation.progress = { progress in
|
||||
let t = Double(progress)
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
generatedAnimation.completion = { finished in
|
||||
|
||||
animationCache?.freeLayer(renderer)
|
||||
@ -49,22 +58,17 @@ func addOpacityAnimation(_ animation: BasicAnimation, _ context: AnimationContex
|
||||
completion()
|
||||
}
|
||||
|
||||
generatedAnimation.progress = { progress in
|
||||
|
||||
let t = Double(progress)
|
||||
node.opacityVar.value = opacityAnimation.getVFunc()(t)
|
||||
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
if let renderer = animation.nodeRenderer, let layer = animationCache?.layerForNodeRenderer(renderer, context, animation: animation) {
|
||||
if let layer = animationCache?.layerForNodeRenderer(renderer, context, animation: animation) {
|
||||
let animationId = animation.ID
|
||||
layer.add(generatedAnimation, forKey: animationId)
|
||||
animation.removeFunc = { [weak layer] in
|
||||
layer?.removeAnimation(forKey: animationId)
|
||||
}
|
||||
}
|
||||
|
||||
if !transactionsDisabled {
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func opacityAnimationByFunc(_ valueFunc: (Double) -> Double, duration: Double, offset: Double, fps: UInt) -> CAAnimation {
|
||||
|
@ -23,44 +23,49 @@ func addShapeAnimation(_ animation: BasicAnimation, _ context: AnimationContext,
|
||||
return
|
||||
}
|
||||
|
||||
let transactionsDisabled = CATransaction.disableActions()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
||||
let fromShape = shapeAnimation.getVFunc()(0.0)
|
||||
let toShape = shapeAnimation.getVFunc()(animation.autoreverses ? 0.5 : 1.0)
|
||||
let duration = animation.autoreverses ? animation.getDuration() / 2.0 : animation.getDuration()
|
||||
|
||||
let mutatingShape = SceneUtils.shapeCopy(from: fromShape)
|
||||
renderer.replaceNode(with: mutatingShape)
|
||||
|
||||
guard let layer = animationCache?.layerForNodeRenderer(renderer, context, animation: animation, shouldRenderContent: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Creating proper animation
|
||||
let generatedAnim = generateShapeAnimation(context,
|
||||
from: mutatingShape,
|
||||
let generatedAnimation = generateShapeAnimation(context,
|
||||
from: fromShape,
|
||||
to: toShape,
|
||||
animation: shapeAnimation,
|
||||
duration: duration,
|
||||
renderTransform: layer.renderTransform!)
|
||||
duration: duration)
|
||||
|
||||
generatedAnim.repeatCount = Float(animation.repeatCount)
|
||||
generatedAnim.timingFunction = caTimingFunction(animation.easing)
|
||||
generatedAnim.autoreverses = animation.autoreverses
|
||||
generatedAnimation.repeatCount = Float(animation.repeatCount)
|
||||
generatedAnimation.timingFunction = caTimingFunction(animation.easing)
|
||||
generatedAnimation.autoreverses = animation.autoreverses
|
||||
|
||||
generatedAnim.completion = { finished in
|
||||
generatedAnimation.progress = { progress in
|
||||
let t = Double(progress)
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
generatedAnimation.completion = { finished in
|
||||
|
||||
animation.progress = animation.manualStop ? 0.0 : 1.0
|
||||
|
||||
if !animation.autoreverses && finished {
|
||||
mutatingShape.form = toShape.form
|
||||
mutatingShape.stroke = toShape.stroke
|
||||
mutatingShape.fill = toShape.fill
|
||||
shape.form = toShape.form
|
||||
shape.stroke = toShape.stroke
|
||||
shape.fill = toShape.fill
|
||||
}
|
||||
|
||||
if !finished {
|
||||
animation.progress = 0.0
|
||||
mutatingShape.form = fromShape.form
|
||||
mutatingShape.stroke = fromShape.stroke
|
||||
mutatingShape.fill = fromShape.fill
|
||||
shape.form = fromShape.form
|
||||
shape.stroke = fromShape.stroke
|
||||
shape.fill = fromShape.fill
|
||||
}
|
||||
|
||||
animationCache?.freeLayer(renderer)
|
||||
@ -72,27 +77,6 @@ func addShapeAnimation(_ animation: BasicAnimation, _ context: AnimationContext,
|
||||
completion()
|
||||
}
|
||||
|
||||
generatedAnim.progress = { progress in
|
||||
|
||||
let t = Double(progress)
|
||||
|
||||
if !animation.autoreverses {
|
||||
let currentShape = shapeAnimation.getVFunc()(t)
|
||||
mutatingShape.place = currentShape.place
|
||||
mutatingShape.opaque = currentShape.opaque
|
||||
mutatingShape.opacity = currentShape.opacity
|
||||
mutatingShape.clip = currentShape.clip
|
||||
mutatingShape.mask = currentShape.mask
|
||||
mutatingShape.effect = currentShape.effect
|
||||
mutatingShape.form = currentShape.form
|
||||
mutatingShape.stroke = currentShape.stroke
|
||||
mutatingShape.fill = currentShape.fill
|
||||
}
|
||||
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
layer.path = fromShape.form.toCGPath()
|
||||
|
||||
// Stroke
|
||||
@ -121,21 +105,23 @@ func addShapeAnimation(_ animation: BasicAnimation, _ context: AnimationContext,
|
||||
}
|
||||
|
||||
let animationId = animation.ID
|
||||
layer.add(generatedAnim, forKey: animationId)
|
||||
layer.add(generatedAnimation, forKey: animationId)
|
||||
animation.removeFunc = { [weak layer] in
|
||||
layer?.removeAnimation(forKey: animationId)
|
||||
}
|
||||
|
||||
if !transactionsDisabled {
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func generateShapeAnimation(_ context: AnimationContext, from: Shape, to: Shape, animation: ShapeAnimation, duration: Double, renderTransform: CGAffineTransform) -> CAAnimation {
|
||||
fileprivate func generateShapeAnimation(_ context: AnimationContext, from: Shape, to: Shape, animation: ShapeAnimation, duration: Double) -> CAAnimation {
|
||||
|
||||
let group = CAAnimationGroup()
|
||||
|
||||
// Shape
|
||||
// Path
|
||||
var transform = renderTransform
|
||||
let fromPath = from.form.toCGPath().copy(using: &transform)
|
||||
let toPath = to.form.toCGPath().copy(using: &transform)
|
||||
let fromPath = from.form.toCGPath()
|
||||
let toPath = to.form.toCGPath()
|
||||
|
||||
let pathAnimation = CABasicAnimation(keyPath: "path")
|
||||
pathAnimation.fromValue = fromPath
|
||||
@ -145,16 +131,12 @@ fileprivate func generateShapeAnimation(_ context: AnimationContext, from: Shape
|
||||
group.animations = [pathAnimation]
|
||||
|
||||
// Transform
|
||||
let scaleAnimation = CABasicAnimation(keyPath: "transform")
|
||||
scaleAnimation.duration = duration
|
||||
let parentPos = AnimationUtils.absolutePosition(animation.nodeRenderer?.parentRenderer, context)
|
||||
let fromPos = parentPos.concat(with: from.place)
|
||||
let toParentPos = animation.toParentGlobalTransfrom
|
||||
let toPos = toParentPos.concat(with: to.place)
|
||||
scaleAnimation.fromValue = CATransform3DMakeAffineTransform(fromPos.toCG())
|
||||
scaleAnimation.toValue = CATransform3DMakeAffineTransform(toPos.toCG())
|
||||
let transformAnimation = CABasicAnimation(keyPath: "transform")
|
||||
transformAnimation.duration = duration
|
||||
transformAnimation.fromValue = CATransform3DMakeAffineTransform(animation.fromLayoutGlobalTransfrom.concat(with: from.place).toCG())
|
||||
transformAnimation.toValue = CATransform3DMakeAffineTransform(animation.toLayoutGlobalTransfrom.concat(with: to.place).toCG())
|
||||
|
||||
group.animations?.append(scaleAnimation)
|
||||
group.animations?.append(transformAnimation)
|
||||
|
||||
// Fill
|
||||
let fromFillColor = from.fill as? Color ?? Color.clear
|
||||
|
@ -11,33 +11,37 @@ func addTransformAnimation(_ animation: BasicAnimation, _ context: AnimationCont
|
||||
return
|
||||
}
|
||||
|
||||
guard let node = animation.node, let renderer = animation.nodeRenderer else {
|
||||
return
|
||||
}
|
||||
|
||||
if transformAnimation.trajectory != nil && transformAnimation.easing === Easing.elasticInOut {
|
||||
fatalError("Transform animation with trajectory can't have elastic easing, try using contentVar animation instead")
|
||||
}
|
||||
|
||||
node.placeVar.value = transformAnimation.getVFunc()(0.0)
|
||||
|
||||
// Creating proper animation
|
||||
var generatedAnimation: CAAnimation?
|
||||
|
||||
generatedAnimation = transformAnimationByFunc(transformAnimation,
|
||||
context,
|
||||
node: node,
|
||||
duration: animation.getDuration(),
|
||||
offset: animation.pausedProgress,
|
||||
fps: transformAnimation.logicalFps)
|
||||
|
||||
guard let generatedAnim = generatedAnimation else {
|
||||
guard let node = animation.node, let renderer = animation.nodeRenderer else {
|
||||
return
|
||||
}
|
||||
|
||||
generatedAnim.repeatCount = Float(animation.repeatCount)
|
||||
let transactionsDisabled = CATransaction.disableActions()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
||||
generatedAnim.completion = { finished in
|
||||
guard let layer = animationCache?.layerForNodeRenderer(renderer, context, animation: animation, shouldRenderContent: true) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Creating proper animation
|
||||
let generatedAnimation = transformAnimationByFunc(transformAnimation,
|
||||
context,
|
||||
duration: animation.getDuration(),
|
||||
offset: animation.pausedProgress,
|
||||
fps: transformAnimation.logicalFps)
|
||||
|
||||
generatedAnimation.repeatCount = Float(animation.repeatCount)
|
||||
|
||||
generatedAnimation.progress = { progress in
|
||||
let t = Double(progress)
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
}
|
||||
|
||||
generatedAnimation.completion = { finished in
|
||||
|
||||
if animation.paused {
|
||||
animation.pausedProgress += animation.progress
|
||||
@ -63,25 +67,18 @@ func addTransformAnimation(_ animation: BasicAnimation, _ context: AnimationCont
|
||||
completion()
|
||||
}
|
||||
|
||||
generatedAnim.progress = { progress in
|
||||
|
||||
let t = Double(progress)
|
||||
node.placeVar.value = transformAnimation.getVFunc()(t)
|
||||
|
||||
animation.progress = t
|
||||
animation.onProgressUpdate?(t)
|
||||
let animationId = animation.ID
|
||||
layer.add(generatedAnimation, forKey: animationId)
|
||||
animation.removeFunc = { [weak layer] in
|
||||
layer?.removeAnimation(forKey: animationId)
|
||||
}
|
||||
|
||||
if let renderer = animation.nodeRenderer, let layer = animationCache?.layerForNodeRenderer(renderer, context, animation: animation) {
|
||||
let animationId = animation.ID
|
||||
layer.add(generatedAnim, forKey: animationId)
|
||||
animation.removeFunc = { [weak layer] in
|
||||
layer?.removeAnimation(forKey: animationId)
|
||||
}
|
||||
if !transactionsDisabled {
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func transformAnimationByFunc(_ animation: TransformAnimation, _ context: AnimationContext, node: Node, duration: Double, offset: Double, fps: UInt) -> CAAnimation {
|
||||
func transformAnimationByFunc(_ animation: TransformAnimation, _ context: AnimationContext, duration: Double, offset: Double, fps: UInt) -> CAAnimation {
|
||||
|
||||
let valueFunc = animation.getVFunc()
|
||||
|
||||
@ -90,8 +87,6 @@ func transformAnimationByFunc(_ animation: TransformAnimation, _ context: Animat
|
||||
pathAnimation.timingFunction = caTimingFunction(animation.easing)
|
||||
pathAnimation.duration = duration / 2
|
||||
pathAnimation.autoreverses = animation.autoreverses
|
||||
let value = AnimationUtils.absoluteTransform(animation.nodeRenderer, context, pos: valueFunc(0))
|
||||
pathAnimation.values = [NSValue(caTransform3D: CATransform3DMakeAffineTransform(value.toCG()))]
|
||||
pathAnimation.fillMode = MCAMediaTimingFillMode.forwards
|
||||
pathAnimation.isRemovedOnCompletion = false
|
||||
pathAnimation.path = trajectory.toCGPath()
|
||||
@ -105,7 +100,7 @@ func transformAnimationByFunc(_ animation: TransformAnimation, _ context: Animat
|
||||
tValue.append(1.0)
|
||||
for t in tValue {
|
||||
let progress = animation.easing.progressFor(time: t)
|
||||
let value = AnimationUtils.absoluteTransform(animation.nodeRenderer, context, pos: valueFunc(offset + progress))
|
||||
let value = valueFunc(offset + progress)
|
||||
let cgValue = CATransform3DMakeAffineTransform(value.toCG())
|
||||
transformValues.append(cgValue)
|
||||
}
|
||||
|
@ -12,11 +12,6 @@ import UIKit
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public enum Relativity {
|
||||
case parent
|
||||
case scene
|
||||
}
|
||||
|
||||
class NodePath {
|
||||
let node: Node
|
||||
let location: CGPoint
|
||||
@ -36,11 +31,13 @@ public struct TouchPoint {
|
||||
|
||||
private let absoluteLocation: Point
|
||||
private let relativeLocation: Point // location inside the node
|
||||
private let viewLocation: Point // location relative t
|
||||
|
||||
init(id: Int, location: Point, relativeLocation: Point) {
|
||||
init(id: Int, location: Point, relativeToNodeLocation: Point, relativeToViewLocation: Point) {
|
||||
self.id = id
|
||||
self.absoluteLocation = location
|
||||
self.relativeLocation = relativeLocation
|
||||
self.relativeLocation = relativeToNodeLocation
|
||||
self.viewLocation = relativeToViewLocation
|
||||
}
|
||||
|
||||
public func location(in relativity: Relativity = .parent) -> Point {
|
||||
@ -49,6 +46,8 @@ public struct TouchPoint {
|
||||
return relativeLocation
|
||||
case .scene:
|
||||
return absoluteLocation
|
||||
case .view:
|
||||
return viewLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,10 @@ public final class Transform {
|
||||
}
|
||||
|
||||
public func invert() -> Transform? {
|
||||
if m11 == 1 && m12 == 0 && m21 == 0 && m22 == 1 {
|
||||
return .move(dx: -dx, dy: -dy)
|
||||
}
|
||||
|
||||
let det = self.m11 * self.m22 - self.m12 * self.m21
|
||||
if det == 0 {
|
||||
return nil
|
||||
|
@ -39,14 +39,16 @@ class SceneUtils {
|
||||
let clip = referenceNode.clip
|
||||
let tag = referenceNode.tag
|
||||
|
||||
var result: Node?
|
||||
|
||||
if let shape = referenceNode as? Shape {
|
||||
return Shape(form: shape.form, fill: shape.fill, stroke: shape.stroke, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
result = Shape(form: shape.form, fill: shape.fill, stroke: shape.stroke, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
}
|
||||
if let text = referenceNode as? Text {
|
||||
return Text(text: text.text, font: text.font, fill: text.fill, stroke: text.stroke, align: text.align, baseline: text.baseline, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
result = Text(text: text.text, font: text.font, fill: text.fill, stroke: text.stroke, align: text.align, baseline: text.baseline, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
}
|
||||
if let image = referenceNode as? Image {
|
||||
return Image(src: image.src, xAlign: image.xAlign, yAlign: image.yAlign, aspectRatio: image.aspectRatio, w: image.w, h: image.h, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
result = Image(src: image.src, xAlign: image.xAlign, yAlign: image.yAlign, aspectRatio: image.aspectRatio, w: image.w, h: image.h, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
}
|
||||
if let group = referenceNode as? Group {
|
||||
var contents = [Node]()
|
||||
@ -55,8 +57,19 @@ class SceneUtils {
|
||||
contents.append(copy)
|
||||
}
|
||||
}
|
||||
return Group(contents: contents, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
result = Group(contents: contents, place: pos, opaque: opaque, clip: clip, visible: visible, tag: tag)
|
||||
}
|
||||
return .none
|
||||
|
||||
result?.touchPressedHandlers = referenceNode.touchPressedHandlers
|
||||
result?.touchMovedHandlers = referenceNode.touchMovedHandlers
|
||||
result?.touchReleasedHandlers = referenceNode.touchReleasedHandlers
|
||||
|
||||
result?.tapHandlers = referenceNode.tapHandlers
|
||||
result?.longTapHandlers = referenceNode.longTapHandlers
|
||||
result?.panHandlers = referenceNode.panHandlers
|
||||
result?.rotateHandlers = referenceNode.rotateHandlers
|
||||
result?.pinchHandlers = referenceNode.pinchHandlers
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,16 @@ import UIKit
|
||||
|
||||
class GroupRenderer: NodeRenderer {
|
||||
|
||||
weak var group: Group?
|
||||
var group: Group
|
||||
var renderers: [NodeRenderer] = []
|
||||
|
||||
init(group: Group, view: MacawView?, animationCache: AnimationCache?) {
|
||||
override var node: Node {
|
||||
return group
|
||||
}
|
||||
|
||||
init(group: Group, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) {
|
||||
self.group = group
|
||||
super.init(node: group, view: view, animationCache: animationCache)
|
||||
super.init(node: group, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
updateRenderers()
|
||||
}
|
||||
|
||||
@ -22,23 +26,23 @@ class GroupRenderer: NodeRenderer {
|
||||
override func doAddObservers() {
|
||||
super.doAddObservers()
|
||||
|
||||
guard let group = group else {
|
||||
return
|
||||
}
|
||||
|
||||
group.contentsVar.onChange { [weak self] _ in
|
||||
self?.updateRenderers()
|
||||
}
|
||||
observe(group.contentsVar)
|
||||
}
|
||||
|
||||
override func node() -> Node? {
|
||||
return group
|
||||
override func freeCachedAbsPlace() {
|
||||
for renderer in renderers {
|
||||
renderer.freeCachedAbsPlace()
|
||||
}
|
||||
}
|
||||
|
||||
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
renderers.forEach { renderer in
|
||||
renderer.render(in: context, force: force, opacity: opacity, coloringMode: coloringMode)
|
||||
if !(animationCache?.isAnimating(renderer) ?? false) {
|
||||
renderer.render(in: context, force: force, opacity: opacity, coloringMode: coloringMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,12 +69,8 @@ class GroupRenderer: NodeRenderer {
|
||||
}
|
||||
renderers.removeAll()
|
||||
|
||||
if let updatedRenderers = group?.contents.compactMap ({ child -> NodeRenderer? in
|
||||
let childRenderer = RenderUtils.createNodeRenderer(child, view: view, animationCache: animationCache)
|
||||
childRenderer.parentRenderer = self
|
||||
return childRenderer
|
||||
}) {
|
||||
renderers = updatedRenderers
|
||||
renderers = group.contents.compactMap { child -> NodeRenderer? in
|
||||
return RenderUtils.createNodeRenderer(child, view: view, animationCache: animationCache, parentRenderer: self)
|
||||
}
|
||||
|
||||
var parent: NodeRenderer = self
|
||||
@ -79,12 +79,4 @@ class GroupRenderer: NodeRenderer {
|
||||
}
|
||||
parent.calculateZPositionRecursively()
|
||||
}
|
||||
|
||||
override func replaceNode(with replacementNode: Node) {
|
||||
super.replaceNode(with: replacementNode)
|
||||
|
||||
if let node = replacementNode as? Group {
|
||||
group = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,30 +9,26 @@ import UIKit
|
||||
#endif
|
||||
|
||||
class ImageRenderer: NodeRenderer {
|
||||
weak var image: Image?
|
||||
var image: Image
|
||||
|
||||
var renderedPaths: [CGPath] = [CGPath]()
|
||||
|
||||
init(image: Image, view: MacawView?, animationCache: AnimationCache?) {
|
||||
override var node: Node {
|
||||
return image
|
||||
}
|
||||
|
||||
init(image: Image, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) {
|
||||
self.image = image
|
||||
super.init(node: image, view: view, animationCache: animationCache)
|
||||
super.init(node: image, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
|
||||
override func node() -> Node? {
|
||||
return image
|
||||
}
|
||||
|
||||
override func doAddObservers() {
|
||||
super.doAddObservers()
|
||||
|
||||
guard let image = image else {
|
||||
return
|
||||
}
|
||||
|
||||
observe(image.srcVar)
|
||||
observe(image.xAlignVar)
|
||||
observe(image.yAlignVar)
|
||||
@ -42,9 +38,6 @@ class ImageRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
guard let image = image else {
|
||||
return
|
||||
}
|
||||
|
||||
var mImage: MImage?
|
||||
if image.src.contains("memory") {
|
||||
@ -64,9 +57,6 @@ class ImageRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
override func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
|
||||
guard let image = image else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if let mImage = MImage(named: image.src),
|
||||
let rect = BoundsUtils.getRect(of: image, mImage: mImage) {
|
||||
@ -77,12 +67,4 @@ class ImageRenderer: NodeRenderer {
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
override func replaceNode(with replacementNode: Node) {
|
||||
super.replaceNode(with: replacementNode)
|
||||
|
||||
if let node = replacementNode as? Image {
|
||||
image = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,12 @@ import UIKit
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public enum Relativity {
|
||||
case parent
|
||||
case scene
|
||||
case view
|
||||
}
|
||||
|
||||
enum ColoringMode {
|
||||
case rgb, greyscale, alphaOnly
|
||||
}
|
||||
@ -13,17 +19,54 @@ enum ColoringMode {
|
||||
class NodeRenderer {
|
||||
|
||||
weak var view: MacawView?
|
||||
weak var parentRenderer: NodeRenderer?
|
||||
internal var zPosition: Int = 0
|
||||
let parentRenderer: GroupRenderer?
|
||||
var zPosition: Int = 0
|
||||
|
||||
fileprivate let onNodeChange: () -> Void
|
||||
fileprivate let disposables = GroupDisposable()
|
||||
fileprivate var active = false
|
||||
weak var animationCache: AnimationCache?
|
||||
|
||||
init(node: Node, view: MacawView?, animationCache: AnimationCache?) {
|
||||
fileprivate var cachedAbsPlace: Transform?
|
||||
fileprivate var absPlace: Transform {
|
||||
if let place = cachedAbsPlace {
|
||||
return place
|
||||
}
|
||||
|
||||
if let place = parentRenderer?.absPlace.concat(with: node.place) {
|
||||
cachedAbsPlace = place
|
||||
return place
|
||||
}
|
||||
|
||||
return node.place
|
||||
}
|
||||
|
||||
func freeCachedAbsPlace() {
|
||||
cachedAbsPlace = nil
|
||||
}
|
||||
|
||||
public func place(in relativity: Relativity = .parent) -> Transform {
|
||||
switch relativity {
|
||||
case .parent:
|
||||
return node.place
|
||||
case .scene:
|
||||
return absPlace
|
||||
case .view:
|
||||
if let viewPlace = view?.place {
|
||||
return viewPlace.concat(with: absPlace)
|
||||
}
|
||||
return absPlace
|
||||
}
|
||||
}
|
||||
|
||||
open var node: Node {
|
||||
fatalError("Unsupported")
|
||||
}
|
||||
|
||||
init(node: Node, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) {
|
||||
self.view = view
|
||||
self.animationCache = animationCache
|
||||
self.parentRenderer = parentRenderer
|
||||
|
||||
onNodeChange = { [unowned node, weak view] in
|
||||
guard let isAnimating = animationCache?.isAnimating(node) else {
|
||||
@ -45,10 +88,6 @@ class NodeRenderer {
|
||||
}
|
||||
|
||||
func doAddObservers() {
|
||||
guard let node = node() else {
|
||||
return
|
||||
}
|
||||
|
||||
observe(node.placeVar)
|
||||
observe(node.opaqueVar)
|
||||
observe(node.opacityVar)
|
||||
@ -56,6 +95,10 @@ class NodeRenderer {
|
||||
observe(node.effectVar)
|
||||
|
||||
node.animationObservers.append(self)
|
||||
|
||||
node.placeVar.onChange { [weak self] _ in
|
||||
self?.freeCachedAbsPlace()
|
||||
}
|
||||
}
|
||||
|
||||
func observe<E>(_ v: Variable<E>) {
|
||||
@ -72,11 +115,7 @@ class NodeRenderer {
|
||||
|
||||
open func dispose() {
|
||||
removeObservers()
|
||||
node()?.animationObservers = node()?.animationObservers.filter { !($0 as? NodeRenderer === self) } ?? []
|
||||
}
|
||||
|
||||
open func node() -> Node? {
|
||||
fatalError("Unsupported")
|
||||
node.animationObservers = node.animationObservers.filter { !($0 as? NodeRenderer === self) }
|
||||
}
|
||||
|
||||
final public func render(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
@ -84,9 +123,6 @@ class NodeRenderer {
|
||||
defer {
|
||||
context.restoreGState()
|
||||
}
|
||||
guard let node = node() else {
|
||||
return
|
||||
}
|
||||
let newOpacity = node.opacity * opacity
|
||||
|
||||
context.concatenate(node.place.toCG())
|
||||
@ -133,11 +169,7 @@ class NodeRenderer {
|
||||
}
|
||||
|
||||
final func directRender(in context: CGContext, force: Bool = true, opacity: Double = 1.0, coloringMode: ColoringMode = .rgb) {
|
||||
guard let node = node() else {
|
||||
return
|
||||
}
|
||||
|
||||
if let isAnimating = animationCache?.isAnimating(node), isAnimating {
|
||||
if let isAnimating = animationCache?.isAnimating(self), isAnimating {
|
||||
self.removeObservers()
|
||||
if !force {
|
||||
return
|
||||
@ -166,7 +198,7 @@ class NodeRenderer {
|
||||
}
|
||||
|
||||
fileprivate func applyEffects(_ effects: [Effect], context: CGContext, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
guard let node = node(), let bounds = node.bounds else {
|
||||
guard let bounds = node.bounds else {
|
||||
return
|
||||
}
|
||||
var inset: Double = 0
|
||||
@ -232,65 +264,51 @@ class NodeRenderer {
|
||||
fatalError("Unsupported")
|
||||
}
|
||||
|
||||
final func findNodeAt(parentNodePath: NodePath, ctx: CGContext) -> NodePath? {
|
||||
guard let node = node() else {
|
||||
final func findNodeAt(location: CGPoint, ctx: CGContext) -> NodePath? {
|
||||
guard node.opaque, let inverted = node.place.invert() else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if node.opaque {
|
||||
let place = node.place
|
||||
if let inverted = place.invert() {
|
||||
ctx.saveGState()
|
||||
defer {
|
||||
ctx.restoreGState()
|
||||
}
|
||||
|
||||
ctx.concatenate(place.toCG())
|
||||
applyClip(in: ctx)
|
||||
let loc = parentNodePath.location.applying(inverted.toCG())
|
||||
let path = NodePath(node: node, location: loc, parent: parentNodePath)
|
||||
let result = doFindNodeAt(path: path, ctx: ctx)
|
||||
return result
|
||||
}
|
||||
ctx.saveGState()
|
||||
defer {
|
||||
ctx.restoreGState()
|
||||
}
|
||||
return .none
|
||||
|
||||
ctx.concatenate(node.place.toCG())
|
||||
applyClip(in: ctx)
|
||||
let loc = location.applying(inverted.toCG())
|
||||
let path = NodePath(node: node, location: loc)
|
||||
let result = doFindNodeAt(path: path, ctx: ctx)
|
||||
return result
|
||||
}
|
||||
|
||||
final func findNodeAt(parentNodePath: NodePath, ctx: CGContext) -> NodePath? {
|
||||
guard node.opaque, let inverted = node.place.invert() else {
|
||||
return .none
|
||||
}
|
||||
|
||||
ctx.saveGState()
|
||||
defer {
|
||||
ctx.restoreGState()
|
||||
}
|
||||
|
||||
ctx.concatenate(node.place.toCG())
|
||||
applyClip(in: ctx)
|
||||
let loc = parentNodePath.location.applying(inverted.toCG())
|
||||
let path = NodePath(node: node, location: loc, parent: parentNodePath)
|
||||
let result = doFindNodeAt(path: path, ctx: ctx)
|
||||
return result
|
||||
}
|
||||
|
||||
public func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceNode(with replacementNode: Node) {
|
||||
guard let node = node() else {
|
||||
return
|
||||
}
|
||||
|
||||
if let groupRenderer = parentRenderer as? GroupRenderer, let group = groupRenderer.node() as? Group {
|
||||
var contents = group.contents
|
||||
var indexToInsert = 0
|
||||
if let index = contents.firstIndex(of: node) {
|
||||
contents.remove(at: index)
|
||||
indexToInsert = index
|
||||
}
|
||||
|
||||
contents.insert(replacementNode, at: indexToInsert)
|
||||
group.contents = contents
|
||||
}
|
||||
|
||||
if let hostingView = view, hostingView.node == node {
|
||||
hostingView.node = replacementNode
|
||||
}
|
||||
}
|
||||
|
||||
func calculateZPositionRecursively() {
|
||||
calculateZPosition(self)
|
||||
}
|
||||
|
||||
private func applyClip(in context: CGContext) {
|
||||
guard let node = node() else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let clip = node.clip else {
|
||||
return
|
||||
}
|
||||
@ -309,7 +327,7 @@ class NodeRenderer {
|
||||
}
|
||||
|
||||
private func getMaskedImage(bounds: Rect) -> CGImage {
|
||||
let mask = node()!.mask!
|
||||
let mask = node.mask!
|
||||
let image = renderToImage(bounds: bounds)
|
||||
let nodeRenderer = RenderUtils.createNodeRenderer(mask, view: .none, animationCache: animationCache)
|
||||
let maskImage = nodeRenderer.renderToImage(bounds: bounds, coloringMode: .greyscale)
|
||||
@ -352,7 +370,11 @@ class NodeRenderer {
|
||||
}
|
||||
|
||||
func getAllChildrenRecursive() -> [NodeRenderer] {
|
||||
return getAllChildren(self)
|
||||
var children = getAllChildren(self)
|
||||
children.removeAll(where: { (r) -> Bool in
|
||||
r === self
|
||||
})
|
||||
return children
|
||||
}
|
||||
|
||||
private func getAllChildren(_ nodeRenderer: NodeRenderer) -> [NodeRenderer] {
|
||||
|
@ -16,19 +16,15 @@ class RenderUtils {
|
||||
return p
|
||||
}
|
||||
|
||||
class func createNodeRenderer(
|
||||
_ node: Node,
|
||||
view: MacawView?,
|
||||
animationCache: AnimationCache?
|
||||
) -> NodeRenderer {
|
||||
class func createNodeRenderer(_ node: Node, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) -> NodeRenderer {
|
||||
if let group = node as? Group {
|
||||
return GroupRenderer(group: group, view: view, animationCache: animationCache)
|
||||
return GroupRenderer(group: group, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
} else if let shape = node as? Shape {
|
||||
return ShapeRenderer(shape: shape, view: view, animationCache: animationCache)
|
||||
return ShapeRenderer(shape: shape, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
} else if let text = node as? Text {
|
||||
return TextRenderer(text: text, view: view, animationCache: animationCache)
|
||||
return TextRenderer(text: text, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
} else if let image = node as? Image {
|
||||
return ImageRenderer(image: image, view: view, animationCache: animationCache)
|
||||
return ImageRenderer(image: image, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
}
|
||||
fatalError("Unsupported node: \(node)")
|
||||
}
|
||||
|
@ -8,37 +8,30 @@ import AppKit
|
||||
|
||||
class ShapeRenderer: NodeRenderer {
|
||||
|
||||
weak var shape: Shape?
|
||||
var shape: Shape
|
||||
|
||||
init(shape: Shape, view: MacawView?, animationCache: AnimationCache?) {
|
||||
init(shape: Shape, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) {
|
||||
self.shape = shape
|
||||
super.init(node: shape, view: view, animationCache: animationCache)
|
||||
super.init(node: shape, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
|
||||
override func node() -> Node? {
|
||||
override var node: Node {
|
||||
return shape
|
||||
}
|
||||
|
||||
override func doAddObservers() {
|
||||
super.doAddObservers()
|
||||
|
||||
guard let shape = shape else {
|
||||
return
|
||||
}
|
||||
|
||||
observe(shape.formVar)
|
||||
observe(shape.fillVar)
|
||||
observe(shape.strokeVar)
|
||||
}
|
||||
|
||||
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
guard let shape = shape else {
|
||||
return
|
||||
}
|
||||
if shape.fill == nil && shape.stroke == nil {
|
||||
return
|
||||
}
|
||||
@ -61,10 +54,6 @@ class ShapeRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
override func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
|
||||
guard let shape = shape else {
|
||||
return .none
|
||||
}
|
||||
|
||||
RenderUtils.setGeometry(shape.form, ctx: ctx)
|
||||
var drawingMode: CGPathDrawingMode?
|
||||
if let stroke = shape.stroke {
|
||||
@ -172,9 +161,6 @@ class ShapeRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
fileprivate func drawPattern(_ pattern: Pattern, ctx: CGContext?, opacity: Double) {
|
||||
guard let shape = shape else {
|
||||
return
|
||||
}
|
||||
var patternNode = pattern.content
|
||||
if !pattern.userSpace, let node = BoundsUtils.createNodeFromRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: shape.form) {
|
||||
patternNode = node
|
||||
@ -239,14 +225,6 @@ class ShapeRenderer: NodeRenderer {
|
||||
ctx!.restoreGState()
|
||||
}
|
||||
|
||||
override func replaceNode(with replacementNode: Node) {
|
||||
super.replaceNode(with: replacementNode)
|
||||
|
||||
if let node = replacementNode as? Shape {
|
||||
shape = node
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Stroke {
|
||||
|
@ -7,28 +7,24 @@ import AppKit
|
||||
#endif
|
||||
|
||||
class TextRenderer: NodeRenderer {
|
||||
weak var text: Text?
|
||||
var text: Text
|
||||
|
||||
init(text: Text, view: MacawView?, animationCache: AnimationCache?) {
|
||||
override var node: Node {
|
||||
return text
|
||||
}
|
||||
|
||||
init(text: Text, view: MacawView?, animationCache: AnimationCache?, parentRenderer: GroupRenderer? = nil) {
|
||||
self.text = text
|
||||
super.init(node: text, view: view, animationCache: animationCache)
|
||||
super.init(node: text, view: view, animationCache: animationCache, parentRenderer: parentRenderer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
|
||||
override func node() -> Node? {
|
||||
return text
|
||||
}
|
||||
|
||||
override func doAddObservers() {
|
||||
super.doAddObservers()
|
||||
|
||||
guard let text = text else {
|
||||
return
|
||||
}
|
||||
|
||||
observe(text.textVar)
|
||||
observe(text.fontVar)
|
||||
observe(text.fillVar)
|
||||
@ -39,9 +35,6 @@ class TextRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
|
||||
guard let text = text else {
|
||||
return
|
||||
}
|
||||
|
||||
let message = text.text
|
||||
let font = getMFont()
|
||||
@ -78,7 +71,7 @@ class TextRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
override func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
|
||||
guard let node = node(), let contains = node.bounds?.toCG().contains(path.location) else {
|
||||
guard let contains = node.bounds?.toCG().contains(path.location) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
@ -95,7 +88,7 @@ class TextRenderer: NodeRenderer {
|
||||
// However it is needed for the Swift Package Manager to work accordingly.
|
||||
return MFont()
|
||||
}
|
||||
guard let text = text, let textFont = text.font else {
|
||||
guard let textFont = text.font else {
|
||||
return MFont.systemFont(ofSize: MFont.mSystemFontSize)
|
||||
}
|
||||
|
||||
@ -130,9 +123,6 @@ class TextRenderer: NodeRenderer {
|
||||
}
|
||||
|
||||
fileprivate func getBounds(_ font: MFont) -> CGRect {
|
||||
guard let text = text else {
|
||||
return .zero
|
||||
}
|
||||
|
||||
var textAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font]
|
||||
if text.kerning != 0.0 {
|
||||
@ -182,12 +172,4 @@ class TextRenderer: NodeRenderer {
|
||||
}
|
||||
return MColor.black
|
||||
}
|
||||
|
||||
override func replaceNode(with replacementNode: Node) {
|
||||
super.replaceNode(with: replacementNode)
|
||||
|
||||
if let node = replacementNode as? Text {
|
||||
text = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,14 +230,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
guard let renderer = renderer else {
|
||||
return .none
|
||||
}
|
||||
ctx.saveGState()
|
||||
defer {
|
||||
ctx.restoreGState()
|
||||
}
|
||||
let transform = place.toCG()
|
||||
ctx.concatenate(transform)
|
||||
let loc = location.applying(transform.inverted())
|
||||
return renderer.findNodeAt(parentNodePath: NodePath(node: Node(), location: loc), ctx: ctx)
|
||||
return renderer.findNodeAt(location: location, ctx: ctx)
|
||||
}
|
||||
|
||||
private func doFindNode(location: CGPoint) -> NodePath? {
|
||||
@ -275,12 +268,15 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
let inverted = node.place.invert()!
|
||||
let loc = location.applying(inverted.toCG())
|
||||
|
||||
let invertedViewPlace = self.place.invert()!
|
||||
let relativeToView = location.applying(invertedViewPlace.toCG())
|
||||
|
||||
let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque())
|
||||
|
||||
while let current = nodePath {
|
||||
let node = current.node
|
||||
let relativeLocation = current.location
|
||||
let point = TouchPoint(id: id, location: loc.toMacaw(), relativeLocation: relativeLocation.toMacaw())
|
||||
let point = TouchPoint(id: id, location: loc.toMacaw(), relativeToNodeLocation: relativeLocation.toMacaw(), relativeToViewLocation: relativeToView.toMacaw())
|
||||
let touchEvent = TouchEvent(node: node, points: [point])
|
||||
|
||||
if touchesOfNode[node] == nil {
|
||||
@ -312,6 +308,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let invertedViewPlace = self.place.invert()!
|
||||
var points = [TouchPoint]()
|
||||
for initialTouch in initialTouches {
|
||||
guard let currentIndex = touchPoints.firstIndex(of: initialTouch) else {
|
||||
@ -324,7 +321,8 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
let location = CGPoint(x: currentTouch.x, y: currentTouch.y)
|
||||
let inverted = currentNode.place.invert()!
|
||||
let loc = location.applying(inverted.toCG())
|
||||
let point = TouchPoint(id: currentTouch.id, location: loc.toMacaw(), relativeLocation: nodePath.location.toMacaw())
|
||||
let relativeToView = location.applying(invertedViewPlace.toCG())
|
||||
let point = TouchPoint(id: currentTouch.id, location: loc.toMacaw(), relativeToNodeLocation: nodePath.location.toMacaw(), relativeToViewLocation: relativeToView.toMacaw())
|
||||
points.append(point)
|
||||
}
|
||||
|
||||
@ -355,6 +353,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let invertedViewPlace = self.place.invert()!
|
||||
let touchPoints = convert(touches: touches)
|
||||
for touch in touchPoints {
|
||||
|
||||
@ -364,8 +363,9 @@ open class MacawView: MView, MGestureRecognizerDelegate {
|
||||
let inverted = node.place.invert()!
|
||||
let location = CGPoint(x: touch.x, y: touch.y)
|
||||
let loc = location.applying(inverted.toCG())
|
||||
let relativeToView = location.applying(invertedViewPlace.toCG())
|
||||
let id = Int(bitPattern: Unmanaged.passUnretained(touch).toOpaque())
|
||||
let point = TouchPoint(id: id, location: loc.toMacaw(), relativeLocation: nodePath.location.toMacaw())
|
||||
let point = TouchPoint(id: id, location: loc.toMacaw(), relativeToNodeLocation: nodePath.location.toMacaw(), relativeToViewLocation: relativeToView.toMacaw())
|
||||
let touchEvent = TouchEvent(node: node, points: [point])
|
||||
|
||||
node.handleTouchReleased(touchEvent)
|
||||
@ -588,8 +588,8 @@ class LayoutHelper {
|
||||
|
||||
public func getTransform(_ nodeRenderer: NodeRenderer, _ layout: ContentLayout, _ size: Size) -> Transform {
|
||||
setSize(size: size)
|
||||
let node = nodeRenderer.node()
|
||||
var rect = node?.bounds
|
||||
let node = nodeRenderer.node
|
||||
var rect = node.bounds
|
||||
if let canvas = node as? SVGCanvas {
|
||||
if let view = nodeRenderer.view {
|
||||
rect = canvas.layout(size: view.bounds.size.toMacaw()).rect()
|
||||
|
@ -8,7 +8,6 @@ import AppKit
|
||||
|
||||
class ShapeLayer: CAShapeLayer {
|
||||
weak var renderer: NodeRenderer?
|
||||
var renderTransform: CGAffineTransform?
|
||||
weak var animationCache: AnimationCache?
|
||||
var shouldRenderContent = true
|
||||
var isForceRenderingEnabled = true
|
||||
@ -22,10 +21,6 @@ class ShapeLayer: CAShapeLayer {
|
||||
let renderContext = RenderContext(view: .none)
|
||||
renderContext.cgContext = ctx
|
||||
|
||||
if let renderTransform = renderTransform {
|
||||
ctx.concatenate(renderTransform)
|
||||
}
|
||||
|
||||
renderer?.directRender(in: ctx, force: isForceRenderingEnabled)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user