Apply Gradient Color to Arc Created with Uibezierpath

Apply gradient color to arc created with UIBezierPath

You can use a CAGradientLayer to get the gradient effect, and use the CAShapeLayer as a mask.

e.g.:

- (void)viewDidLoad
{
[super viewDidLoad];

int radius = 100;

CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:60.0 endAngle:0.0 clockwise:YES].CGPath;

arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);

arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineWidth = 15;
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:@"drawCircleAnimation"];

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor blueColor].CGColor ];
gradientLayer.startPoint = CGPointMake(0,0.5);
gradientLayer.endPoint = CGPointMake(1,0.5);

[self.view.layer addSublayer:gradientLayer];
//Using arc as a mask instead of adding it as a sublayer.
//[self.view.layer addSublayer:arc];
gradientLayer.mask = arc;

}

Add gradient to UIBezierPath line

The short answer to your problem is most likely that you need to call cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0) to convert your stroking path on mask to fill path.

But to break this down step by step...

First, you are drawing a gradient, which is masked by a line. Not drawing a line that has applied gradient. So only one layer needs to be added and it needs to be a gradient layer. So a first step should draw a rectangle with a gradient:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]

self.layer.addSublayer(gradientLayer)
}

Now that gradient is visible lets apply some mask to it. A circle should be an easy task:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
return layer
}()

self.layer.addSublayer(gradientLayer)
}

if you do this and you used some non-zero starting coordinates of the path then you will see that the circle is cut off. The path that is being masked must be in gradient layer coordinates so path.bounds needs to be corrected to

    gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
return layer
}()

now that coordinates are OK let's convert this to a line. It gets a little bit more complicated because we need to subtract the origin of original path:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath
return layer
}()

self.layer.addSublayer(gradientLayer)
}

there are other ways to move the path. A transform could be used just as well. So if this approach is not nice enough for you please choose whichever you wish.

However, this now draws nothing at all. And the reason is that when using mask a fill approach is always used. And filling a line does nothing as it has no area. You could use multiple lines to draw a custom shape for instance. And in your case you could use 4 lines which would represent a rectangle which is the line you expect to draw.

However, drawing lines is a pain so thankfully there is a not-very-intuitively designed API which converts your stroked path to a fill path. So in your case it converts a line into a rectangle with a defined width (and other properties which are only needed for more complex strokes). You can use

public func copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = .identity) -> CGPath

The code is not documented but in online documentation you may find

Discussion The new path is created so that filling the new path draws
the same pixels as stroking the original path with the specified line
style.

Now the end result should look something like this:

func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
return layer
}()

self.layer.addSublayer(gradientLayer)
}

I hardcoded line width to 3.0 but you are probably best adding this as a method parameter or as view property.

To play around with different setup and see results quickly from this answer you can create a new project and replace your view controller code to the following:

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
view.addSubview({
let label = UILabel(frame: CGRect(x: 20.0, y: 70.0, width: 300.0, height: 20.0))
label.text = "Tap to toggle through drawing types"
return label
}())
}

private var instance: Int = 0
private var currentView: UIView?

@objc private func onTap() {
currentView?.removeFromSuperview()
let view = MyView(frame: CGRect(x: 30.0, y: 100.0, width: 100.0, height: 200.0))
currentView = view
view.backgroundColor = .lightGray
self.view.addSubview(view)
view.drawLine(type: instance, start: CGPoint(x: 90.0, y: 10.0), toPoint: .init(x: 10.0, y: 150.0))
instance += 1
}

}

private extension ViewController {

class MyView: UIView {

func drawLine(type: Int, start: CGPoint, toPoint end: CGPoint) {
let methods = [drawLine1, drawLine2, drawLine3, drawLine4, drawLine5]
methods[type%methods.count](start, end)
}

private func drawLine1(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]

self.layer.addSublayer(gradientLayer)
}

private func drawLine2(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
return layer
}()

self.layer.addSublayer(gradientLayer)
}

private func drawLine3(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
return layer
}()

self.layer.addSublayer(gradientLayer)
}

private func drawLine4(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath
return layer
}()

self.layer.addSublayer(gradientLayer)
}

private func drawLine5(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)

let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
return layer
}()

self.layer.addSublayer(gradientLayer)
}

}

}

How to fill a bezier path with gradient color

To answer this question of yours,

I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect)
function. I would like to fill the path with a gradient color.

Lets say you have an oval path,

let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))

To create a gradient,

let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]

We need a mask layer for gradient,

let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath

Now set this shapeLayer as mask of gradient layer and add it to view's layer as subLayer

gradient.mask = shapeMask
yourCustomView.layer.addSublayer(gradient)

Update
Create a base layer with stroke and add before creating gradient layer.

let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.view.layer.addSublayer(shape)

let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]

let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask

self.view.layer.addSublayer(gradient)

UIBezierPath with color gradient

you can give it a try :)

- (void)drawRect:(CGRect)rect
{
CGFloat arcStep = (M_PI *2) / 360; // M_PI*2 is equivalent of full cirle
BOOL clocklwise = NO;
CGFloat x = CGRectGetWidth(rect) / 2; // circle's center
CGFloat y = CGRectGetHeight(rect) / 2; // circle's center
CGFloat radius = MIN(x, y) / 2;
CGContextRef ctx = UIGraphicsGetCurrentContext();
// draw colorful circle
CGContextSetLineWidth(ctx, radius*2);
for (CGFloat i = 0; i < 360; i+=1)
{
UIColor* c = [UIColor colorWithHue:i/360 saturation:1. brightness:1. alpha:1];

CGContextSetStrokeColorWithColor(ctx, c.CGColor);

CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep + 0.02;

CGContextAddArc(ctx, x, y, radius, startAngle, endAngle, clocklwise);
CGContextStrokePath(ctx);
}
// drawing circles then, you might want few of them - smaller radius and less alpha with each step
UIColor* c = [[UIColor whiteColor] colorWithAlphaComponent: 0.03];
for (CGFloat fillRadius = radius/2; fillRadius > 0; fillRadius -= 1.f)
{
CGContextSetLineWidth(ctx, fillRadius*2);
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGContextAddArc(ctx, x, y, fillRadius, 0, M_PI * 2, clocklwise);
CGContextStrokePath(ctx);
}
}

Inside gradient color UIBezierPath

One option is to use CGGradient along with using the bezier path as a clipping region. No need for layers.

Replace everything after closing the path with:

let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor,UIColor.blue.cgColor,UIColor.yellow.cgColor ,UIColor.black.cgColor] as CFArray, locations: nil)!

let ctx = UIGraphicsGetCurrentContext()!
ctx.saveGState()

// Clip to the path
bezier.addClip()
// Draw the gradient in the clipped region
ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: frame.height), options: [])

ctx.restoreGState()

Fill circle border with gradient color using UIBezierPath

Core graphics doesn't support an axial gradient so you need to draw this out in a more manual way.

Here's a custom view class that draws a circle using the range of HSV colors around the circumference.

class RadialCircleView: UIView {
override func draw(_ rect: CGRect) {
let thickness: CGFloat = 20
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - thickness / 2
var last: CGFloat = 0
for a in 1...360 {
let ang = CGFloat(a) / 180 * .pi
let arc = UIBezierPath(arcCenter: center, radius: radius, startAngle: last, endAngle: ang, clockwise: true)
arc.lineWidth = thickness
last = ang
UIColor(hue: CGFloat(a) / 360, saturation: 1, brightness: 1, alpha: 1).set()
arc.stroke()
}
}
}

let radial = RadialCircleView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
radial.backgroundColor = UIColor(red: 0.98, green: 0.92, blue: 0.84, alpha: 1) // "antique white"

Copy this into a playground to experiment with the results. The colors don't exactly match your image but it may meet your needs.



Related Topics



Leave a reply



Submit