Created
July 10, 2023 12:00
-
-
Save Ziewvater/0cf56b85a1764dd0ea1bfe88425f8787 to your computer and use it in GitHub Desktop.
Mask view created with the negative space within a stroked path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: A UIKit based Playground for presenting user interface | |
import UIKit | |
import PlaygroundSupport | |
class StrokedPathFillMaskView: UIView { | |
var path: UIBezierPath? { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
backgroundColor = .clear | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
} | |
extension StrokedPathFillMaskView { | |
override func draw(_ rect: CGRect) { | |
guard let ctx = UIGraphicsGetCurrentContext(), | |
let path else { | |
return | |
} | |
guard let maskImage = ImageCreator.strokedInvertedFillMask(for: path, rect: ctx.boundingBoxOfClipPath), | |
let fillColorImage = ctx.image(with: UIColor.black.cgColor)else { | |
print("Failed to create mask image") | |
return | |
} | |
// https://stackoverflow.com/a/634146/2383003 | |
ctx.clip(to: ctx.boundingBoxOfClipPath, mask: maskImage) | |
ctx.draw(fillColorImage, in: ctx.boundingBoxOfClipPath, byTiling: false) | |
} | |
} | |
enum ImageCreator { | |
static func strokedInvertedFillMask(for path: UIBezierPath, rect: CGRect) -> CGImage? { | |
// Masking with an image requires that the image have no alpha channel | |
// https://developer.apple.com/documentation/coregraphics/cgimage/1456337-masking | |
// Even though we're using `CGContext.clip(to:mask:)` instead, this is still needed | |
let colorSpace = CGColorSpaceCreateDeviceRGB() | |
guard let ctx = CGContext( | |
data: nil, | |
width: Int(ceil(rect.width)), | |
height: Int(ceil(rect.height)), | |
bitsPerComponent: 8, | |
bytesPerRow: 0, | |
space: colorSpace, | |
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue | |
) else { | |
return nil | |
} | |
ctx.saveGState() | |
defer { | |
ctx.clear(ctx.boundingBoxOfClipPath) | |
ctx.restoreGState() | |
} | |
let maskPath = UIBezierPath(rect: ctx.boundingBoxOfClipPath) | |
maskPath.append(path.reversing()) | |
// Paint background white to allow for mask differential | |
let backgroundColor = UIColor.white | |
ctx.setFillColor(backgroundColor.cgColor) | |
ctx.fill(ctx.boundingBoxOfClipPath) | |
let maskColor = UIColor.black | |
ctx.setFillColor(maskColor.cgColor) | |
ctx.setStrokeColor(maskColor.cgColor) | |
// Fill path | |
ctx.addPath(maskPath.cgPath) | |
ctx.fillPath() | |
// Stroke path | |
ctx.setLineWidth(path.lineWidth) | |
ctx.addPath(maskPath.cgPath) | |
ctx.strokePath() | |
return ctx.makeImage() | |
} | |
} | |
extension CGContext { | |
func image(with color: CGColor) -> CGImage? { | |
saveGState() | |
defer { | |
clear(boundingBoxOfClipPath) | |
restoreGState() | |
} | |
setFillColor(color) | |
fill(boundingBoxOfClipPath) | |
return makeImage() | |
} | |
} | |
// Present the view controller in the Live View window | |
//PlaygroundPage.current.liveView = MyViewController() | |
let view = StrokedPathFillMaskView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) | |
//let path = UIBezierPath(ovalIn: view.bounds) | |
let path = UIBezierPath() | |
path.addLine(to: view.center) | |
path.addArc(withCenter: CGPoint(x: view.frame.maxX, y: view.frame.midY), | |
radius: view.frame.width / 2, | |
startAngle: .pi, | |
endAngle: .pi / 2, | |
clockwise: true) | |
path.addLine(to: CGPoint(x: view.frame.maxX, y: view.frame.maxY)) | |
path.addLine(to: CGPoint(x: view.frame.minX, y: view.frame.maxY)) | |
path.close() | |
path.lineWidth = 50 | |
view.path = path | |
//view.backgroundColor = .systemCyan | |
//print(view.layer) | |
//print((view.layer as! InvertedPathLayer).path) | |
let container = UIView(frame: view.bounds) | |
container.backgroundColor = .systemCyan | |
//container.addSubview(view) | |
container.mask = view | |
let outerContainer = UIView(frame: container.bounds) | |
outerContainer.backgroundColor = .systemIndigo | |
outerContainer.addSubview(container) | |
PlaygroundPage.current.liveView = outerContainer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment