1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-11-12 18:18:35 +03:00

Fix #34: Implement DropShadow effect rendering

This commit is contained in:
Alisa Mylnikova 2018-05-23 15:39:46 +07:00
parent 411fe986ea
commit 7a608c7c0c
11 changed files with 173 additions and 173 deletions

View File

@ -28,7 +28,7 @@ public extension MacawView {
)
context.cgContext = ctx
renderer?.render(force: false, opacity: node.opacity)
renderer?.render(in: ctx, force: false, opacity: node.opacity)
ctx.endPDFPage()
}

View File

@ -5,4 +5,8 @@ open class Effect {
public init(input: Effect?) {
self.input = input
}
public static func dropShadow(dx: Double = 5, dy: Double = 5, radius: Double = 5) -> Effect? {
return AlphaEffect(input: OffsetEffect(dx: dx, dy: dy, input: GaussianBlur(radius: radius, input: nil)))
}
}

View File

@ -11,10 +11,10 @@ class GroupRenderer: NodeRenderer {
fileprivate var renderers: [NodeRenderer] = []
let renderingInterval: RenderingInterval?
init(group: Group, ctx: RenderContext, animationCache: AnimationCache?, interval: RenderingInterval? = .none) {
init(group: Group, animationCache: AnimationCache?, interval: RenderingInterval? = .none) {
self.group = group
self.renderingInterval = interval
super.init(node: group, ctx: ctx, animationCache: animationCache)
super.init(node: group, animationCache: animationCache)
updateRenderers()
}
@ -35,9 +35,9 @@ class GroupRenderer: NodeRenderer {
return group
}
override func doRender(_ force: Bool, opacity: Double) {
override func doRender(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
renderers.forEach { renderer in
renderer.render(force: force, opacity: opacity)
renderer.render(in: context, force: force, opacity: opacity, useAlphaOnly: useAlphaOnly)
}
}
@ -62,12 +62,12 @@ class GroupRenderer: NodeRenderer {
if let updatedRenderers = group?.contents.compactMap ({ child -> NodeRenderer? in
guard let interval = renderingInterval else {
return RenderUtils.createNodeRenderer(child, context: ctx, animationCache: animationCache)
return RenderUtils.createNodeRenderer(child, animationCache: animationCache)
}
let index = AnimationUtils.absoluteIndex(child, useCache: true)
if index > interval.from && index < interval.to {
return RenderUtils.createNodeRenderer(child, context: ctx, animationCache: animationCache, interval: interval)
return RenderUtils.createNodeRenderer(child, animationCache: animationCache, interval: interval)
}
return .none

View File

@ -13,9 +13,9 @@ class ImageRenderer: NodeRenderer {
var renderedPaths: [CGPath] = [CGPath]()
init(image: Image, ctx: RenderContext, animationCache: AnimationCache?) {
init(image: Image, animationCache: AnimationCache?) {
self.image = image
super.init(node: image, ctx: ctx, animationCache: animationCache)
super.init(node: image, animationCache: animationCache)
}
override func node() -> Node? {
@ -37,7 +37,7 @@ class ImageRenderer: NodeRenderer {
observe(image.hVar)
}
override func doRender(_ force: Bool, opacity: Double) {
override func doRender(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
guard let image = image else {
return
}
@ -52,10 +52,10 @@ class ImageRenderer: NodeRenderer {
if let mImage = mImage {
let rect = getRect(mImage)
ctx.cgContext!.scaleBy(x: 1.0, y: -1.0)
ctx.cgContext!.translateBy(x: 0.0, y: -1.0 * rect.height)
ctx.cgContext!.setAlpha(CGFloat(opacity))
ctx.cgContext!.draw(mImage.cgImage!, in: rect)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0.0, y: -1.0 * rect.height)
context.setAlpha(CGFloat(opacity))
context.draw(mImage.cgImage!, in: rect)
}
}

View File

@ -11,15 +11,12 @@ struct RenderingInterval {
class NodeRenderer {
let ctx: RenderContext
fileprivate let onNodeChange: () -> Void
fileprivate let disposables = GroupDisposable()
fileprivate var active = false
weak var animationCache: AnimationCache?
init(node: Node, ctx: RenderContext, animationCache: AnimationCache?) {
self.ctx = ctx
init(node: Node, animationCache: AnimationCache?) {
self.animationCache = animationCache
onNodeChange = {
@ -30,8 +27,6 @@ class NodeRenderer {
if isAnimating {
return
}
ctx.view?.setNeedsDisplay()
}
addObservers()
@ -69,22 +64,56 @@ class NodeRenderer {
fatalError("Unsupported")
}
final public func render(force: Bool, opacity: Double) {
ctx.cgContext!.saveGState()
final public func render(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
context.saveGState()
defer {
ctx.cgContext!.restoreGState()
context.restoreGState()
}
guard let node = node() else {
return
}
let newOpacity = node.opacity * opacity
ctx.cgContext!.concatenate(node.place.toCG())
applyClip()
directRender(force: force, opacity: node.opacity * opacity)
context.concatenate(node.place.toCG())
applyClip(in: context)
// no effects, just draw as usual
guard let effect = node.effect else {
directRender(in: context, force: force, opacity: newOpacity, useAlphaOnly: useAlphaOnly)
return
}
var effects = [Effect]()
var next: Effect? = effect
while next != nil {
effects.append(next!)
next = next?.input
}
let offset = effects.first { $0 is OffsetEffect } as? OffsetEffect
let otherEffects = effects.filter { !($0 is OffsetEffect) }
let useAlphaOnly = otherEffects.contains { effect -> Bool in
effect is AlphaEffect
}
// move to offset
let move = Transform(m11: 1, m12: 0, m21: 0, m22: 1, dx: offset?.dx ?? 0, dy: offset?.dy ?? 0)
context.concatenate(move.toCG())
if otherEffects.isEmpty {
// just draw offset shape
directRender(in: context, force: force, opacity: newOpacity, useAlphaOnly: useAlphaOnly)
} else {
// apply other effects to offset shape and draw it
applyEffects(otherEffects, context: context, opacity: opacity, useAlphaOnly: useAlphaOnly)
}
// move back and draw the shape itself
context.concatenate(move.invert()!.toCG())
directRender(in: context, force: force, opacity: newOpacity)
}
final func directRender(force: Bool = true, opacity: Double = 1.0) {
final func directRender(in context: CGContext, force: Bool = true, opacity: Double = 1.0, useAlphaOnly: Bool = false) {
guard let node = node() else {
return
}
@ -97,10 +126,62 @@ class NodeRenderer {
} else {
self.addObservers()
}
doRender(force, opacity: opacity)
doRender(in: context, force: force, opacity: opacity, useAlphaOnly: useAlphaOnly)
}
func doRender(_ force: Bool, opacity: Double) {
fileprivate func applyEffects(_ effects: [Effect], context: CGContext, opacity: Double, useAlphaOnly: Bool = false) {
guard let node = node() else {
return
}
for effect in effects {
if let blur = effect as? GaussianBlur {
guard let bounds = node.bounds() else {
return
}
let shadowInset = min(blur.radius * 6 + 1, 150)
guard let shapeImage = renderToImage(bounds: bounds, inset: shadowInset, useAlphaOnly: useAlphaOnly)?.cgImage else {
return
}
guard let filteredImage = applyBlur(shapeImage, blur: blur) else {
return
}
context.draw(filteredImage, in: CGRect(x: bounds.x - shadowInset / 2, y: bounds.y - shadowInset / 2, width: bounds.w + shadowInset, height: bounds.h + shadowInset))
}
}
}
fileprivate func applyBlur(_ image: CGImage, blur: GaussianBlur) -> CGImage? {
let image = CIImage(cgImage: image)
guard let filter = CIFilter(name: "CIGaussianBlur") else {
return .none
}
filter.setDefaults()
filter.setValue(Int(blur.radius), forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)
let context = CIContext(options: nil)
let imageRef = context.createCGImage(filter.outputImage!, from: image.extent)
return imageRef
}
func renderToImage(bounds: Rect, inset: Double, useAlphaOnly: Bool = false) -> UIImage? {
MGraphicsBeginImageContextWithOptions(CGSize(width: bounds.w + inset, height: bounds.h + inset), false, 1)
guard let tempContext = MGraphicsGetCurrentContext() else {
return .none
}
// 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, useAlphaOnly: useAlphaOnly)
let img = MGraphicsGetImageFromCurrentImageContext()
MGraphicsEndImageContext()
return img
}
func doRender(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
fatalError("Unsupported")
}
@ -118,7 +199,7 @@ class NodeRenderer {
}
ctx.concatenate(place.toCG())
applyClip()
applyClip(in: ctx)
let loc = location.applying(inverted.toCG())
let result = doFindNodeAt(location: CGPoint(x: loc.x, y: loc.y), ctx: ctx)
return result
@ -131,12 +212,12 @@ class NodeRenderer {
return nil
}
private func applyClip() {
private func applyClip(in context: CGContext) {
guard let node = node() else {
return
}
guard let clip = node.clip, let context = ctx.cgContext else {
guard let clip = node.clip else {
return
}

View File

@ -40,18 +40,17 @@ class RenderUtils {
class func createNodeRenderer(
_ node: Node,
context: RenderContext,
animationCache: AnimationCache?,
interval: RenderingInterval? = .none
) -> NodeRenderer {
if let group = node as? Group {
return GroupRenderer(group: group, ctx: context, animationCache: animationCache, interval: interval)
return GroupRenderer(group: group, animationCache: animationCache, interval: interval)
} else if let shape = node as? Shape {
return ShapeRenderer(shape: shape, ctx: context, animationCache: animationCache)
return ShapeRenderer(shape: shape, animationCache: animationCache)
} else if let text = node as? Text {
return TextRenderer(text: text, ctx: context, animationCache: animationCache)
return TextRenderer(text: text, animationCache: animationCache)
} else if let image = node as? Image {
return ImageRenderer(image: image, ctx: context, animationCache: animationCache)
return ImageRenderer(image: image, animationCache: animationCache)
}
fatalError("Unsupported node: \(node)")
}

View File

@ -10,9 +10,9 @@ class ShapeRenderer: NodeRenderer {
weak var shape: Shape?
init(shape: Shape, ctx: RenderContext, animationCache: AnimationCache?) {
init(shape: Shape, animationCache: AnimationCache?) {
self.shape = shape
super.init(node: shape, ctx: ctx, animationCache: animationCache)
super.init(node: shape, animationCache: animationCache)
}
override func node() -> Node? {
@ -31,122 +31,34 @@ class ShapeRenderer: NodeRenderer {
observe(shape.strokeVar)
}
fileprivate func drawShape(in context: CGContext, opacity: Double) {
override func doRender(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
guard let shape = shape else {
return
}
setGeometry(shape.form, ctx: context)
var fillRule = FillRule.nonzero
if let path = shape.form as? Path {
fillRule = path.fillRule
}
drawPath(shape.fill, stroke: shape.stroke, ctx: context, opacity: opacity, fillRule: fillRule)
}
override func doRender(_ force: Bool, opacity: Double) {
guard let shape = shape, let context = ctx.cgContext else {
return
}
if shape.fill == nil && shape.stroke == nil {
return
}
// no effects, just draw as usual
guard let effect = shape.effect else {
drawShape(in: context, opacity: opacity)
setGeometry(shape.form, ctx: context)
var fillRule = FillRule.nonzero
if let path = shape.form as? Path {
fillRule = path.fillRule
}
if !useAlphaOnly {
drawPath(shape.fill, stroke: shape.stroke, ctx: context, opacity: opacity, fillRule: fillRule)
return
}
var effects = [Effect]()
var next: Effect? = effect
while next != nil {
effects.append(next!)
next = next?.input
}
let color = shape.fill != nil ? shape.fill as! Color : .black
let fill = Color.black.with(a: Double(color.a()) / 255.0)
let offset = effects.first { $0 is OffsetEffect }
let otherEffects = effects.filter { !($0 is OffsetEffect) }
if let offset = offset as? OffsetEffect {
let move = Transform(m11: 1, m12: 0, m21: 0, m22: 1, dx: offset.dx, dy: offset.dy)
context.concatenate(move.toCG())
let strokeColor = shape.stroke != nil ? shape.stroke?.fill as! Color : .black
let newStrokeColor = Color.black.with(a: Double(strokeColor.a()) / 255.0)
let stroke = shape.stroke != nil ? Stroke(fill: newStrokeColor, width: shape.stroke!.width, cap: shape.stroke!.cap, join: shape.stroke!.join, dashes: shape.stroke!.dashes, offset: shape.stroke!.offset) : nil
if otherEffects.isEmpty {
// draw offset shape
drawShape(in: context, opacity: opacity)
} else {
// apply other effects to offset shape
applyEffects(otherEffects, opacity: opacity)
}
// move back and draw the shape itself
context.concatenate(move.invert()!.toCG())
drawShape(in: context, opacity: opacity)
} else {
// draw the shape
drawShape(in: context, opacity: opacity)
// apply other effects to shape
applyEffects(otherEffects, opacity: opacity)
}
}
fileprivate func applyEffects(_ effects: [Effect], opacity: Double) {
guard let shape = shape, let context = ctx.cgContext else {
return
}
for effect in effects {
if let blur = effect as? GaussianBlur {
let shadowInset = min(blur.radius * 6 + 1, 150)
guard let shapeImage = saveToImage(shape: shape, shadowInset: shadowInset, opacity: opacity)?.cgImage else {
return
}
guard let filteredImage = applyBlur(shapeImage, blur: blur) else {
return
}
guard let bounds = shape.bounds() else {
return
}
context.draw(filteredImage, in: CGRect(x: bounds.x - shadowInset / 2, y: bounds.y - shadowInset / 2, width: bounds.w + shadowInset, height: bounds.h + shadowInset))
}
}
}
fileprivate func applyBlur(_ image: CGImage, blur: GaussianBlur) -> CGImage? {
let image = CIImage(cgImage: image)
guard let filter = CIFilter(name: "CIGaussianBlur") else {
return .none
}
filter.setDefaults()
filter.setValue(Int(blur.radius), forKey: kCIInputRadiusKey)
filter.setValue(image, forKey: kCIInputImageKey)
let context = CIContext(options: nil)
let imageRef = context.createCGImage(filter.outputImage!, from: image.extent)
return imageRef
}
fileprivate func saveToImage(shape: Shape, shadowInset: Double, opacity: Double) -> MImage? {
guard let size = shape.bounds() else {
return .none
}
MGraphicsBeginImageContextWithOptions(CGSize(width: size.w + shadowInset, height: size.h + shadowInset), false, 1)
guard let tempContext = MGraphicsGetCurrentContext() else {
return .none
}
if shape.fill != nil || shape.stroke != nil {
// flip y-axis and leave space for the blur
tempContext.translateBy(x: CGFloat(shadowInset / 2 - size.x), y: CGFloat(size.h + shadowInset / 2 + size.y))
tempContext.scaleBy(x: 1, y: -1)
drawShape(in: tempContext, opacity: opacity)
}
let img = MGraphicsGetImageFromCurrentImageContext()
MGraphicsEndImageContext()
return img
drawPath(fill, stroke: stroke, ctx: context, opacity: opacity, fillRule: fillRule)
}
override func doFindNodeAt(location: CGPoint, ctx: CGContext) -> Node? {
@ -183,10 +95,10 @@ class ShapeRenderer: NodeRenderer {
fileprivate func setGeometry(_ locus: Locus, ctx: CGContext) {
if let rect = locus as? Rect {
ctx.addRect(newCGRect(rect))
ctx.addRect(rect.toCG())
} else if let round = locus as? RoundRect {
let corners = CGSize(width: CGFloat(round.rx), height: CGFloat(round.ry))
let path = MBezierPath(roundedRect: newCGRect(round.rect), byRoundingCorners:
let path = MBezierPath(roundedRect: round.rect.toCG(), byRoundingCorners:
MRectCorner.allCorners, cornerRadii: corners).cgPath
ctx.addPath(path)
} else if let circle = locus as? Circle {
@ -205,10 +117,6 @@ class ShapeRenderer: NodeRenderer {
}
}
fileprivate func newCGRect(_ rect: Rect) -> CGRect {
return CGRect(x: CGFloat(rect.x), y: CGFloat(rect.y), width: CGFloat(rect.w), height: CGFloat(rect.h))
}
fileprivate func drawPath(_ fill: Fill?, stroke: Stroke?, ctx: CGContext?, opacity: Double, fillRule: FillRule) {
var shouldStrokePath = false
if fill is Gradient || stroke?.fill is Gradient {

View File

@ -9,9 +9,9 @@ import AppKit
class TextRenderer: NodeRenderer {
weak var text: Text?
init(text: Text, ctx: RenderContext, animationCache: AnimationCache?) {
init(text: Text, animationCache: AnimationCache?) {
self.text = text
super.init(node: text, ctx: ctx, animationCache: animationCache)
super.init(node: text, animationCache: animationCache)
}
override func node() -> Node? {
@ -33,7 +33,7 @@ class TextRenderer: NodeRenderer {
observe(text.baselineVar)
}
override func doRender(_ force: Bool, opacity: Double) {
override func doRender(in context: CGContext, force: Bool, opacity: Double, useAlphaOnly: Bool = false) {
guard let text = text else {
return
}
@ -52,7 +52,7 @@ class TextRenderer: NodeRenderer {
}
attributes[NSAttributedStringKey.strokeWidth] = stroke.width as NSObject?
}
MGraphicsPushContext(ctx.cgContext!)
MGraphicsPushContext(context)
message.draw(in: getBounds(font), withAttributes: attributes)
MGraphicsPopContext()
}

View File

@ -34,10 +34,10 @@ open class SVGParser {
}
let availableStyleAttributes = ["stroke", "stroke-width", "stroke-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
"fill", "fill-rule", "text-anchor", "clip-path", "fill-opacity",
"stop-color", "stop-opacity",
"font-family", "font-size",
"font-weight", "opacity", "color", "visibility"]
"fill", "fill-rule", "fill-opacity", "clip-path",
"opacity", "color", "stop-color", "stop-opacity",
"font-family", "font-size", "font-weight", "text-anchor",
"visibility"]
fileprivate let xmlString: String
fileprivate let initialPosition: Transform
@ -159,10 +159,11 @@ open class SVGParser {
}
fileprivate func parseNode(_ node: XMLIndexer, groupStyle: [String: String] = [:]) -> Node? {
var result: Node? = nil
if let element = node.element {
switch element.name {
case "g":
return parseGroup(node, groupStyle: groupStyle)
result = parseGroup(node, groupStyle: groupStyle)
case "clipPath":
if let id = element.allAttributes["id"]?.text, let clip = parseClip(node) {
self.defClip[id] = clip
@ -171,10 +172,14 @@ open class SVGParser {
// do nothing - it was parsed on first iteration
return .none
default:
return parseElement(node, groupStyle: groupStyle)
result = parseElement(node, groupStyle: groupStyle)
}
if let result = result, let filterString = element.allAttributes["filter"]?.text ?? groupStyle["filter"], let filterId = parseIdFromUrl(filterString), let effect = defEffects[filterId] {
result.effect = effect
}
}
return .none
return result
}
fileprivate var styleTable: [String: [String: String]] = [:]
@ -240,10 +245,6 @@ open class SVGParser {
return .none
}
if let filterString = element.allAttributes["filter"]?.text ?? nodeStyle["filter"], let filterId = parseIdFromUrl(filterString), let effect = defEffects[filterId] {
parsedNode.effect = effect
}
return parsedNode
}
@ -1051,7 +1052,14 @@ open class SVGParser {
continue
}
}
return effects.first?.value
if let effect = effects["SourceAlpha"] {
return AlphaEffect(input: effect)
}
if let effect = effects[defaultSource] {
return effect
}
return nil
}
fileprivate func parseMask(_ mask: XMLIndexer) -> Shape? {

View File

@ -22,7 +22,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
nodesMap.add(node, view: self)
self.renderer?.dispose()
if let cache = animationCache {
self.renderer = RenderUtils.createNodeRenderer(node, context: context, animationCache: cache)
self.renderer = RenderUtils.createNodeRenderer(node, animationCache: cache)
}
if let _ = superview {
@ -93,7 +93,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
initializeView()
if let cache = self.animationCache {
self.renderer = RenderUtils.createNodeRenderer(node, context: context, animationCache: cache)
self.renderer = RenderUtils.createNodeRenderer(node, animationCache: cache)
}
}
}
@ -107,7 +107,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
self.node = node
nodesMap.add(node, view: self)
if let cache = self.animationCache {
self.renderer = RenderUtils.createNodeRenderer(node, context: context, animationCache: cache)
self.renderer = RenderUtils.createNodeRenderer(node, animationCache: cache)
}
}
@ -117,7 +117,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
self.node = node
nodesMap.add(node, view: self)
if let cache = self.animationCache {
self.renderer = RenderUtils.createNodeRenderer(node, context: context, animationCache: cache)
self.renderer = RenderUtils.createNodeRenderer(node, animationCache: cache)
}
}
@ -186,7 +186,7 @@ open class MacawView: MView, MGestureRecognizerDelegate {
return
}
ctx.concatenate(layoutHelper.getTransform(node, contentLayout, bounds.size.toMacaw()))
renderer.render(force: false, opacity: node.opacity)
renderer.render(in: ctx, force: false, opacity: node.opacity)
}
private func localContext( _ callback: (CGContext) -> Void) {

View File

@ -35,8 +35,8 @@ class ShapeLayer: CAShapeLayer {
ctx.concatenate(renderTransform)
}
let renderer = RenderUtils.createNodeRenderer(node, context: renderContext, animationCache: animationCache, interval: renderingInterval)
renderer.directRender(force: isForceRenderingEnabled)
let renderer = RenderUtils.createNodeRenderer(node, animationCache: animationCache, interval: renderingInterval)
renderer.directRender(in: ctx, force: isForceRenderingEnabled)
renderer.dispose()
}
}