t9mike
t9mike

Reputation: 1626

How to skip an area (cutout) when drawing with core graphics

I want to draw some items but leave a true alpha transparency cutout for a circular area. What I want to achieve:

Yellow is example background to show bleed through.

The cutout width is actually wider than the arc stroke, so they don't fully intersect. I need true cutout because I a saving to an image with transparency.

I thought maybe I could use setBlendMode() but I believe that would only work if I wanted my cutout to be exactly the same width as the arc. But there is the gist of how I was trying to go about it:

A Swift workbook follows. Any tips on achieving this are greatly appreciated.

import Foundation
import UIKit

var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;

class DonutView : UIView
{
    override func draw(_ rect: CGRect)
    {

        // cutout
        let cutoutColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
        cutoutColor.setFill()
        let cutoutPath = UIBezierPath(ovalIn: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
        cutoutPath.fill()

//        let context = UIGraphicsGetCurrentContext()!
//        context.setBlendMode(.sourceOut)

        let ringOffset = cutoutWidth/2;
        let circleWidth = dimen - ringOffset*2;

        // ring
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
        let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
        ringColor.setStroke()
        ringPath.lineWidth = strokeWidth
        ringPath.stroke()

        // arc
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)

        let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
        arcColor.setStroke()
        arcPath.lineWidth = strokeWidth
        arcPath.stroke()
    }
}

var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow

// View these elements
view

(Edit: I should have stated this initially: this is to ultimately create a UIImage for WatchKit)

Upvotes: 0

Views: 482

Answers (2)

DonMag
DonMag

Reputation: 77486

You can do this by using another CAShapeLayer as a mask.

The portion(s) of the mask layer that are alpha = 1.0 will be fully transparent.

So...

enter image description here

If we make the Arc Layer a sublayer of the Ring Layer, we can then apply the Cutout Layer as a mask, resulting in:

enter image description here

Here is source for a Playground page:

class MyDonutView : UIView
{

    let ringLayer = CAShapeLayer()
    let arcLayer = CAShapeLayer()
    let cutoutLayer = CAShapeLayer()

    var strokeWidth: CGFloat = 20.0;
    var cutoutWidth: CGFloat = 30.0;

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func commonInit() -> Void {

        // add arcLayer as a sublayer of ringLayer
        ringLayer.addSublayer(arcLayer)

        // add ringLayer as a sublayer of self.layer
        layer.addSublayer(ringLayer)

        // ring layer stroke is black at 0.3 alpha, fill is clear
        ringLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3).cgColor
        ringLayer.fillColor = UIColor.clear.cgColor
        ringLayer.lineWidth = strokeWidth

        // arc layer stroke is black at 0.6 alpha, fill is clear
        arcLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.6).cgColor
        arcLayer.lineWidth = strokeWidth
        arcLayer.fillColor = UIColor.clear.cgColor

        // cutout layer stroke is black (although we're using Zero line width
        //  fill is black
        cutoutLayer.strokeColor = UIColor.red.cgColor
        cutoutLayer.lineWidth = 0
        cutoutLayer.fillColor = UIColor.red.cgColor

    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // define the "padding" around the ring
        let ringOffset = cutoutWidth / 2.0

        // define the diameter of the ring
        let circleWidth = bounds.size.width - cutoutWidth;

        // ring path
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))

        // arc path
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)

        // set ring layer path
        ringLayer.path = ringPath.cgPath

        // set arc layer path
        arcLayer.path = arcPath.cgPath

        // create a rect path the full size of bounds of self
        let fullPath = UIBezierPath(rect: bounds)

        // create a cutout path (the small circle to cut-out of the ring/arc)
        let cutoutPath = UIBezierPath(ovalIn: CGRect(x: bounds.size.width-cutoutWidth, y: bounds.size.width/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))

        // append the cutout path to the full rect path
        fullPath.append(cutoutPath)

        // even-odd winding rule
        cutoutLayer.fillRule = CAShapeLayerFillRule.evenOdd

        // set cutout layer path
        cutoutLayer.path = fullPath.cgPath

        // use cutout layer to mask ring layer
        ringLayer.mask = cutoutLayer
    }

}

class TestViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        // instantiate a MyDonutView
        let myDonutView = MyDonutView()

        // we can set the stroke and cutout widths here
        myDonutView.strokeWidth = 20.0
        myDonutView.cutoutWidth = 30.0

        // we're using auto-layout
        myDonutView.translatesAutoresizingMaskIntoConstraints = false

        // background color yellow to see the frame
        //myDonutView.backgroundColor = .yellow

        // otherwise, it should be clear
        myDonutView.backgroundColor = .clear

        // add as subview
        view.addSubview(myDonutView)

        // constrain centerX and centerY
        // width = 200, height = width
        NSLayoutConstraint.activate([

            myDonutView.widthAnchor.constraint(equalToConstant: 200.0),
            myDonutView.heightAnchor.constraint(equalTo: myDonutView.widthAnchor),
            myDonutView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myDonutView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            ])

    }

}

let vc = TestViewController()
PlaygroundPage.current.liveView = vc

Upvotes: 1

t9mike
t9mike

Reputation: 1626

With help from How to clear circle in CGContext in iOS

import Foundation
import UIKit
import PlaygroundSupport

var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;

class DonutView : UIImageView
{
    override init(frame: CGRect) {
        super.init(frame: frame)

        UIGraphicsBeginImageContextWithOptions(CGSize(width: dimen, height: dimen), false, 1)
        let context = UIGraphicsGetCurrentContext()!

        let ringOffset = cutoutWidth/2;
        let circleWidth = dimen - ringOffset*2;

        // ring
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
        let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
        ringColor.setStroke()
        ringPath.lineWidth = strokeWidth
        ringPath.stroke()

        // arc
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)        
        let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
        arcColor.setStroke()
        arcPath.lineWidth = strokeWidth
        arcPath.stroke()

        // Cutout circle
        context.setFillColor(UIColor.clear.cgColor)
        context.setBlendMode(.clear)        
        context.addEllipse(in: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
        context.drawPath(using: .fill)
        context.setBlendMode(.normal)

        image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()        
        }

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow

// View these elements
view

Upvotes: 2

Related Questions