1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-10-26 13:01:25 +03:00
Macaw/Source/render/ShapeRenderer.swift
2020-04-16 15:46:02 +07:00

277 lines
11 KiB
Swift

import Foundation
#if os(iOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
class ShapeRenderer: NodeRenderer {
var shape: Shape
init(shape: Shape, view: DrawingView?, parentRenderer: GroupRenderer? = nil) {
self.shape = shape
super.init(node: shape, view: view, parentRenderer: parentRenderer)
}
deinit {
dispose()
}
override var node: Node {
return shape
}
override func doAddObservers() {
super.doAddObservers()
observe(shape.formVar)
observe(shape.fillVar)
observe(shape.strokeVar)
}
override func doRender(in context: CGContext, force: Bool, opacity: Double, coloringMode: ColoringMode = .rgb) {
if shape.fill == nil && shape.stroke == nil {
return
}
RenderUtils.setGeometry(shape.form, ctx: context)
var fillRule = FillRule.nonzero
if let path = shape.form as? Path {
fillRule = path.fillRule
}
switch coloringMode {
case .rgb:
drawPath(fill: shape.fill, stroke: shape.stroke, ctx: context, opacity: opacity, fillRule: fillRule)
case .greyscale:
drawPath(fill: shape.fill?.fillUsingGrayscaleNoAlpha(), stroke: shape.stroke?.strokeUsingGrayscaleNoAlpha(), ctx: context, opacity: opacity, fillRule: fillRule)
case .alphaOnly:
drawPath(fill: shape.fill?.fillUsingAlphaOnly(), stroke: shape.stroke?.strokeUsingAlphaOnly(), ctx: context, opacity: opacity, fillRule: fillRule)
}
}
override func doFindNodeAt(path: NodePath, ctx: CGContext) -> NodePath? {
RenderUtils.setGeometry(shape.form, ctx: ctx)
var drawingMode: CGPathDrawingMode?
if let stroke = shape.stroke {
RenderUtils.setStrokeAttributes(stroke, ctx: ctx)
if shape.fill != nil {
drawingMode = .fillStroke
} else {
drawingMode = .stroke
}
} else {
drawingMode = .fill
}
var contains = false
if let mode = drawingMode {
contains = ctx.pathContains(path.location, mode: mode)
if contains {
return path
}
}
// Prepare for next figure hittesting - clear current context path
ctx.beginPath()
return .none
}
fileprivate func drawPath(fill: Fill?, stroke: Stroke?, ctx: CGContext?, opacity: Double, fillRule: FillRule) {
var shouldStrokePath = false
if fill is Gradient || stroke?.fill is Gradient {
shouldStrokePath = true
}
if let fill = fill, let stroke = stroke {
let path = ctx!.path
setFill(fill, ctx: ctx, opacity: opacity)
if stroke.fill is Gradient && !(fill is Gradient) {
ctx!.drawPath(using: fillRule == .nonzero ? .fill : .eoFill)
}
drawWithStroke(stroke, ctx: ctx, opacity: opacity, shouldStrokePath: shouldStrokePath, path: path, mode: fillRule == .nonzero ? .fillStroke : .eoFillStroke)
return
}
if let fill = fill {
setFill(fill, ctx: ctx, opacity: opacity)
ctx!.drawPath(using: fillRule == .nonzero ? .fill : .eoFill)
return
}
if let stroke = stroke {
drawWithStroke(stroke, ctx: ctx, opacity: opacity, shouldStrokePath: shouldStrokePath, mode: .stroke)
return
}
}
fileprivate func setFill(_ fill: Fill?, ctx: CGContext?, opacity: Double) {
guard let fill = fill else {
return
}
if let fillColor = fill as? Color {
let color = RenderUtils.applyOpacity(fillColor, opacity: opacity)
ctx!.setFillColor(color.toCG())
} else if let gradient = fill as? Gradient {
drawGradient(gradient, ctx: ctx, opacity: opacity)
} else if let pattern = fill as? Pattern {
drawPattern(pattern, ctx: ctx, opacity: opacity)
} else {
print("Unsupported fill: \(fill)")
}
}
fileprivate func drawWithStroke(_ stroke: Stroke, ctx: CGContext?, opacity: Double, shouldStrokePath: Bool = false, path: CGPath? = nil, mode: CGPathDrawingMode) {
if let path = path, shouldStrokePath {
ctx!.addPath(path)
}
RenderUtils.setStrokeAttributes(stroke, ctx: ctx)
if stroke.fill is Gradient {
gradientStroke(stroke, ctx: ctx, opacity: opacity)
return
} else if stroke.fill is Color {
colorStroke(stroke, ctx: ctx, opacity: opacity)
}
if shouldStrokePath {
ctx!.strokePath()
} else {
ctx!.drawPath(using: mode)
}
}
fileprivate func colorStroke(_ stroke: Stroke, ctx: CGContext?, opacity: Double) {
guard let strokeColor = stroke.fill as? Color else {
return
}
let color = RenderUtils.applyOpacity(strokeColor, opacity: opacity)
ctx!.setStrokeColor(color.toCG())
}
fileprivate func gradientStroke(_ stroke: Stroke, ctx: CGContext?, opacity: Double) {
guard let gradient = stroke.fill as? Gradient else {
return
}
ctx!.replacePathWithStrokedPath()
drawGradient(gradient, ctx: ctx, opacity: opacity)
}
fileprivate func drawPattern(_ pattern: Pattern, ctx: CGContext?, opacity: Double) {
var patternNode = pattern.content
if !pattern.userSpace, let node = BoundsUtils.createNodeFromRespectiveCoords(respectiveNode: pattern.content, absoluteLocus: shape.form) {
patternNode = node
}
let renderer = RenderUtils.createNodeRenderer(patternNode, view: view)
var patternBounds = pattern.bounds
if !pattern.userSpace {
let boundsTranform = BoundsUtils.transformForLocusInRespectiveCoords(respectiveLocus: pattern.bounds, absoluteLocus: shape.form)
patternBounds = pattern.bounds.applying(boundsTranform)
}
let tileImage = renderer.renderToImage(bounds: patternBounds, inset: 0)
ctx?.clip()
ctx?.draw(tileImage.cgImage!, in: patternBounds.toCG(), byTiling: true)
}
fileprivate func drawGradient(_ gradient: Gradient, ctx: CGContext?, opacity: Double) {
ctx!.saveGState()
var colors: [CGColor] = []
var stops: [CGFloat] = []
for stop in gradient.stops {
stops.append(CGFloat(stop.offset))
let color = RenderUtils.applyOpacity(stop.color, opacity: opacity)
colors.append(color.toCG())
}
if let gradient = gradient as? LinearGradient {
var start = CGPoint(x: CGFloat(gradient.x1), y: CGFloat(gradient.y1))
var end = CGPoint(x: CGFloat(gradient.x2), y: CGFloat(gradient.y2))
if !gradient.userSpace {
let bounds = ctx!.boundingBoxOfPath
start = CGPoint(x: start.x * bounds.width + bounds.minX, y: start.y * bounds.height + bounds.minY)
end = CGPoint(x: end.x * bounds.width + bounds.minX, y: end.y * bounds.height + bounds.minY)
}
ctx!.clip()
let cgGradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: stops)
ctx!.drawLinearGradient(cgGradient!, start: start, end: end, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
} else if let gradient = gradient as? RadialGradient {
var innerCenter = CGPoint(x: CGFloat(gradient.fx), y: CGFloat(gradient.fy))
var outerCenter = CGPoint(x: CGFloat(gradient.cx), y: CGFloat(gradient.cy))
var radius = CGFloat(gradient.r)
if !gradient.userSpace {
var bounds = ctx!.boundingBoxOfPath
var scaleX: CGFloat = 1
var scaleY: CGFloat = 1
if bounds.width > bounds.height {
scaleY = bounds.height / bounds.width
} else {
scaleX = bounds.width / bounds.height
}
ctx!.scaleBy(x: scaleX, y: scaleY)
bounds = ctx!.boundingBoxOfPath
innerCenter = CGPoint(x: innerCenter.x * bounds.width + bounds.minX, y: innerCenter.y * bounds.height + bounds.minY)
outerCenter = CGPoint(x: outerCenter.x * bounds.width + bounds.minX, y: outerCenter.y * bounds.height + bounds.minY)
radius = min(radius * bounds.width, radius * bounds.height)
}
ctx!.clip()
let cgGradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: stops)
ctx!.drawRadialGradient(cgGradient!, startCenter: innerCenter, startRadius: 0, endCenter: outerCenter, endRadius: radius, options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
}
ctx!.restoreGState()
}
}
extension Stroke {
func strokeUsingAlphaOnly() -> Stroke {
return Stroke(fill: fill.fillUsingAlphaOnly(), width: width, cap: cap, join: join, dashes: dashes, offset: offset)
}
func strokeUsingGrayscaleNoAlpha() -> Stroke {
return Stroke(fill: fill.fillUsingGrayscaleNoAlpha(), width: width, cap: cap, join: join, dashes: dashes, offset: offset)
}
}
extension Fill {
func fillUsingAlphaOnly() -> Fill {
if let color = self as? Color {
return color.colorUsingAlphaOnly()
}
let gradient = self as! Gradient
let newStops = gradient.stops.map { Stop(offset: $0.offset, color: $0.color.colorUsingAlphaOnly()) }
if let radial = self as? RadialGradient {
return RadialGradient(cx: radial.cx, cy: radial.cy, fx: radial.fx, fy: radial.fy, r: radial.r, userSpace: radial.userSpace, stops: newStops)
}
let linear = self as! LinearGradient
return LinearGradient(x1: linear.x1, y1: linear.y1, x2: linear.x2, y2: linear.y2, userSpace: linear.userSpace, stops: newStops)
}
func fillUsingGrayscaleNoAlpha() -> Fill {
if let color = self as? Color {
return color.toGrayscaleNoAlpha()
}
let gradient = self as! Gradient
let newStops = gradient.stops.map { Stop(offset: $0.offset, color: $0.color.toGrayscaleNoAlpha()) }
if let radial = self as? RadialGradient {
return RadialGradient(cx: radial.cx, cy: radial.cy, fx: radial.fx, fy: radial.fy, r: radial.r, userSpace: radial.userSpace, stops: newStops)
}
let linear = self as! LinearGradient
return LinearGradient(x1: linear.x1, y1: linear.y1, x2: linear.x2, y2: linear.y2, userSpace: linear.userSpace, stops: newStops)
}
}
extension Color {
func colorUsingAlphaOnly() -> Color {
return Color.black.with(a: Double(a()) / 255.0)
}
func toGrayscaleNoAlpha() -> Color {
let grey = Int(0.21 * Double(r()) + 0.72 * Double(g()) + 0.07 * Double(b()))
return Color.rgb(r: grey, g: grey, b: grey)
}
}