Files
Macaw/Source/render/NodeRenderer.swift

421 lines
14 KiB
Swift

import Foundation
#if os(iOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
enum ColoringMode {
case rgb, greyscale, alphaOnly
}
class CachedLayer {
let rootLayer: ShapeLayer
let animationLayer: ShapeLayer
required init(rootLayer: ShapeLayer, animationLayer: ShapeLayer) {
self.rootLayer = rootLayer
self.animationLayer = animationLayer
}
}
class NodeRenderer {
weak var view: DrawingView?
var sceneLayer: CALayer? {
return view?.mLayer
}
var layer: CachedLayer?
var zPosition: Int = 0
private(set) weak var parentRenderer: GroupRenderer?
fileprivate let onNodeChange: () -> Void
fileprivate let disposables = GroupDisposable()
fileprivate var active = false
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: DrawingView?, parentRenderer: GroupRenderer? = nil) {
self.view = view
self.parentRenderer = parentRenderer
onNodeChange = { [weak view] in
if node.isAnimating() {
return
}
view?.setNeedsDisplay()
}
addObservers()
}
deinit {
disposables.dispose()
}
func doAddObservers() {
observe(node.placeVar)
observe(node.opaqueVar)
observe(node.opacityVar)
observe(node.clipVar)
observe(node.effectVar)
node.animationObservers.append(self)
node.placeVar.onChange { [weak self] _ in
self?.freeCachedAbsPlace()
}
}
func observe<E>(_ v: Variable<E>) {
let disposable = v.onChange { [weak self] _ in
self?.onNodeChange()
}
addDisposable(disposable)
}
func addDisposable(_ disposable: Disposable) {
disposable.addTo(disposables)
}
open func dispose() {
removeObservers()
node.animationObservers = node.animationObservers.filter { !($0 as? NodeRenderer === self) }
freeLayer()
}
final public func render(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
context.saveGState()
defer {
context.restoreGState()
}
let newOpacity = node.opacity * opacity
context.concatenate(node.place.toCG())
applyClip(in: context)
// draw masked image
if let mask = node.mask, let bounds = mask.bounds {
context.draw(getMaskedImage(bounds: bounds), in: bounds.toCG())
return
}
// no effects, just draw as usual
guard let effect = node.effect else {
directRender(in: context, force: force, opacity: newOpacity, coloringMode: coloringMode)
return
}
let (offset, otherEffects) = separateEffects(effect)
let hasAlpha = otherEffects.contains { $0 is AlphaEffect }
let effectColoringMode = hasAlpha ? ColoringMode.alphaOnly : coloringMode
// move to offset
if let offset = offset {
context.concatenate(CGAffineTransform(translationX: CGFloat(offset.dx), y: CGFloat(offset.dy)))
}
if otherEffects.isEmpty {
// just draw offset shape
directRender(in: context, force: force, opacity: newOpacity, coloringMode: effectColoringMode)
} else {
// apply other effects to offset shape and draw it
applyEffects(otherEffects, context: context, opacity: opacity, coloringMode: effectColoringMode)
}
if otherEffects.contains(where: { effect -> Bool in
effect is BlendEffect
}) {
// move back and draw the shape itself
if let offset = offset {
context.concatenate(CGAffineTransform(translationX: CGFloat(-offset.dx), y: CGFloat(-offset.dy)))
}
directRender(in: context, force: force, opacity: newOpacity)
}
}
final func directRender(in context: CGContext,
force: Bool = true,
opacity: Double = 1.0,
coloringMode: ColoringMode = .rgb) {
if isAnimating() {
self.removeObservers()
if !force {
return
}
} else {
self.addObservers()
}
doRender(in: context, force: force, opacity: opacity, coloringMode: coloringMode)
}
fileprivate func separateEffects(_ effect: Effect) -> (OffsetEffect?, [Effect]) {
var next: Effect? = effect
var otherEffects = [Effect]()
var dx: Double = 0, dy: Double = 0
while next != nil {
if let offset = next as? OffsetEffect {
dx += offset.dx
dy += offset.dy
} else {
otherEffects.append(next!)
}
next = next?.input
}
let offset = dx != 0 || dy != 0 ? OffsetEffect(dx: dx, dy: dy, input: nil) : nil
return (offset, otherEffects.reversed())
}
fileprivate func applyEffects(_ effects: [Effect], context: CGContext, opacity: Double, coloringMode: ColoringMode = .rgb) {
guard let bounds = node.bounds else {
return
}
var inset: Double = 0
for effect in effects {
if let blur = effect as? GaussianBlur {
inset = min(blur.r * 6 + 1, 150)
}
}
let shapeImage = CIImage(cgImage: renderToImage(bounds: bounds, inset: inset, coloringMode: coloringMode).cgImage!)
var filteredImage = shapeImage
for effect in effects {
if let blur = effect as? GaussianBlur {
filteredImage = applyBlur(filteredImage, blur: blur)
}
if let matrix = effect as? ColorMatrixEffect {
filteredImage = applyColorMatrix(filteredImage, colorMatrixEffect: matrix)
}
}
let ciContext = CIContext(options: nil)
let finalImage = ciContext.createCGImage(filteredImage, from: shapeImage.extent)!
context.draw(finalImage, in: CGRect(x: bounds.x - inset / 2, y: bounds.y - inset / 2, width: bounds.w + inset, height: bounds.h + inset))
}
fileprivate func applyBlur(_ image: CIImage, blur: GaussianBlur) -> CIImage {
let filter = CIFilter(name: "CIGaussianBlur")!
filter.setDefaults()
filter.setValue(Int(blur.r), forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)
return filter.outputImage!
}
fileprivate func applyColorMatrix(_ image: CIImage, colorMatrixEffect: ColorMatrixEffect) -> CIImage {
let matrix = colorMatrixEffect.matrix.values.map { CGFloat($0) }
let filter = CIFilter(name: "CIColorMatrix")!
filter.setDefaults()
filter.setValue(CIVector(x: matrix[0], y: matrix[1], z: matrix[2], w: matrix[3]), forKey: "inputRVector")
filter.setValue(CIVector(x: matrix[5], y: matrix[6], z: matrix[7], w: matrix[8]), forKey: "inputGVector")
filter.setValue(CIVector(x: matrix[10], y: matrix[11], z: matrix[12], w: matrix[13]), forKey: "inputBVector")
filter.setValue(CIVector(x: matrix[15], y: matrix[16], z: matrix[17], w: matrix[18]), forKey: "inputAVector")
filter.setValue(CIVector(x: matrix[4], y: matrix[9], z: matrix[14], w: matrix[19]), forKey: "inputBiasVector")
filter.setValue(image, forKey: kCIInputImageKey)
return filter.outputImage!
}
func renderToImage(bounds: Rect, inset: Double = 0, coloringMode: ColoringMode = .rgb) -> MImage {
let screenScale: CGFloat = MMainScreen()?.mScale ?? 1.0
MGraphicsBeginImageContextWithOptions(CGSize(width: bounds.w + inset, height: bounds.h + inset), false, screenScale)
let tempContext = MGraphicsGetCurrentContext()!
// flip y-axis and leave space for the blur
tempContext.translateBy(x: CGFloat(inset / 2 - bounds.x), y: CGFloat(bounds.h + inset / 2 + bounds.y))
tempContext.scaleBy(x: 1, y: -1)
directRender(in: tempContext, force: false, opacity: 1.0, coloringMode: coloringMode)
let img = MGraphicsGetImageFromCurrentImageContext()
MGraphicsEndImageContext()
return img!
}
func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
fatalError("Unsupported")
}
final func findNodeAt(location: CGPoint, 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 = 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 calculateZPositionRecursively() {
calculateZPosition(self)
}
private func applyClip(in context: CGContext) {
guard let clip = node.clip else {
return
}
MGraphicsPushContext(context)
defer {
MGraphicsPopContext()
}
if let rect = clip as? Rect {
context.clip(to: CGRect(x: rect.x, y: rect.y, width: rect.w, height: rect.h))
return
}
RenderUtils.toBezierPath(clip).addClip()
}
private func getMaskedImage(bounds: Rect) -> CGImage {
let mask = node.mask!
let image = renderToImage(bounds: bounds)
let nodeRenderer = RenderUtils.createNodeRenderer(mask, view: .none)
let maskImage = nodeRenderer.renderToImage(bounds: bounds, coloringMode: .greyscale)
return apply(maskImage: maskImage, to: image)
}
func apply(maskImage: MImage, to image: MImage) -> CGImage {
let imageReference = image.cgImage!
let maskReference = maskImage.cgImage!
let decode = [CGFloat(1), CGFloat(0),
CGFloat(0), CGFloat(1),
CGFloat(0), CGFloat(1),
CGFloat(0), CGFloat(1)]
let invertedMask = CGImage(maskWidth: maskReference.width,
height: maskReference.height,
bitsPerComponent: maskReference.bitsPerComponent,
bitsPerPixel: maskReference.bitsPerPixel,
bytesPerRow: maskReference.bytesPerRow,
provider: maskReference.dataProvider!,
decode: decode,
shouldInterpolate: maskReference.shouldInterpolate)!
return imageReference.masking(invertedMask)!
}
private func addObservers() {
if !active {
active = true
doAddObservers()
}
}
fileprivate func removeObservers() {
if active {
active = false
disposables.dispose()
}
}
func getAllChildrenRecursive() -> [NodeRenderer] {
var children = getAllChildren(self)
children.removeAll { r -> Bool in
r === self
}
return children
}
private func getAllChildren(_ nodeRenderer: NodeRenderer) -> [NodeRenderer] {
var result = [nodeRenderer]
if let groupRenderer = nodeRenderer as? GroupRenderer {
for child in groupRenderer.renderers {
result.append(contentsOf: getAllChildren(child))
}
}
return result
}
}
protocol AnimationObserver {
func processAnimation(_ animation: BasicAnimation)
}
extension NodeRenderer: AnimationObserver {
func processAnimation(_ animation: BasicAnimation) {
animation.nodeRenderer = self
}
}
@discardableResult fileprivate func calculateZPosition(_ nodeRenderer: NodeRenderer, currentIndex: Int = 0) -> Int {
nodeRenderer.zPosition = currentIndex
if let groupRenderer = nodeRenderer as? GroupRenderer {
var i = currentIndex + 1
for child in groupRenderer.renderers {
i = calculateZPosition(child, currentIndex: i)
}
return i
}
return currentIndex + 1
}