Skip to content

Instantly share code, notes, and snippets.

@Ziewvater
Created July 10, 2023 12:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ziewvater/0cf56b85a1764dd0ea1bfe88425f8787 to your computer and use it in GitHub Desktop.
Save Ziewvater/0cf56b85a1764dd0ea1bfe88425f8787 to your computer and use it in GitHub Desktop.
Mask view created with the negative space within a stroked path
//: 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