How can I use a CAGradientLayer to most efficiently draw a gradient around a circle / with angles?
I made the one underneath with the help of this project. It uses a bitmap context for drawing but a CAGradientLayer would be far more efficient.
Unfortunately I could only figure out how to make linear gradients with it.
I realize many years have passed since this question was asked, but for anybody who stumbles across it now, iOS 12 added a conic gradient type that makes this possible.
gradientLayer.type = CAGradientLayerType.conic
gradientLayer.frame = bounds
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
Gradient layers currently only support linear gradients. However, if you look at the interface for gradient layers, it includes a type property. Right now the only type defined is kCAGradientLayerAxial (linear).
The fact that there is a type property suggests that Apple will be adding more types at some future date, and radial gradients seem like a very like addition.
You might look at creating your own custom subclass of CAGradientLayer that draws radial gradients as well as linear. I've seen demo projects on the net that create custom CALayer subclasses.
Related
My question requires quite a bit of explanation. If you want to skip to the question, look for the bold "Question:" towards the end.
It's fairly easy to create a "circle wipe" animation in iOS or Mac OS using a Core Animation and masks.
One way is to install a CAShapeLayer as a mask on a view (typically an image view), and installing a circle in the shape layer, and animating the circle from 0 size to large enough to cover the whole image (r = sqrt(height^2 + width^2)
Another way is to use a radial CAGradientLayer as a mask, set up the center of the gradient as an opaque color, and have a sudden transition to clear just beyond the corners of the image. You then animate the "positions" values of the gradient layer to move the clear color in to the center of the image.
Both of these approaches create a sharp-edged "circle wipe" that causes the image to shrink to a point and disappear.
That effect looks like this:
The classic cinematic circle wipe animation has a feathered edge, however, rather than a sharp border. The outer edge of the circle is completely transparent. As you move in towards the center the image becomes more and more solid over a fairly short distance, and then the image is completely opaque from there into the center. As the circle shrinks, the thickness of the transition from transparent to opaque remains constant, and the circle stinks down to a point and disappears. (The mask is a circle is opaque in the middle and has a slightly blurred edge, and the radius of the circle shrinks to 0 while the thickness of the blurred edge remains constant.)
Here is an example of an old school circle wipe. This is technically an "Iris Out" where the transition is to black, but the idea is the same:
https://youtu.be/IqDhAW3TDR8?t=90
I have no idea how to achieve a circle wipe with a feathered edge using a shape layer as a mask. Shape layers are always sharp-edged. I could get fairly close by using a shape layer where the circle is stroked with 50% opacity, and the middle of the circle is filled with a color at 100% opacity, but that wouldn't get the smooth transition from transparent to opaque that I'm after.
Another approach I've tried is using a radial gradient with 3 colors in it. I set the inner colors as opaque, and the outer color as clear. I set the location of the outer color as > 1 (say 1.2), the middle location to 1, and the inner location to 0. The locations array contains [1.2, 1.0, 0] That makes the whole part of the mask that covers the image opaque, with a feathered transition to clear that happens past the edges of the image.
I then do an animation of the locations array in 2 steps.
In the first step, I animate the locations array to [0, 0, 0.2]. That causes a band of feathering to move in from the corners and stop at 0.2 from the center of the image.
In the 2nd step, I animate the locations array from [0, 0, 0.2] to [0,0,0]. That causes an inner, transparent-to-opaque center to shrink to a point and disappear.
The effect is ok, and if I run it with a narrow blur band and a fast duration, it looks decent, but it's not perfect.
Here is what it looks like with a blur range of 0.2, and a duration of (I think) 0.25 seconds:
If I introduce a pause between the animation steps, however, you can see the imperfections in the effect:
Question: Is there a way to animate a circle with a small blur radius (5 points or so) from the full size of a view down to 0 using Core Animation?
The same question applies to other types of wipe animations as well: Keyhole, star, side wipe, box, and many others. For those, the radial gradient trick I'm using for a feathered circle wipe wouldn't work at all. (You create those specialized wipe animations using a CAShapeLayer that's the shape you want to animate, and then adjust it's size from large to small.
It would be easy to create a static blurred circle (or other shape) by first drawing a circle and then applying a Core Image blur filter to it, but there's no way to animate that in iOS, and the CI blur filter is too slow on iOS even if you could.
Here is a link to the project I used to create the animations in this post using the 3-color radial gradient I describe: https://github.com/DuncanMC/GradientView.git.
Edit:
Based On James' comment, I tried using a shape layer with a shadow on it as a mask. The effect is on the right track, but there is still a stark transition from the opaque part of the image to the mostly transparent part which is masked by the shadow layer. Even with a shadow opacity of 1.0 the shadow is still mostly transparent. Here's what it looks like:
Making the line color of the circle shape partly transparent helps some, and gives a transition between the opaque part of the image and the mostly transparent shadow. Here's the image above but stroking the shape layer at a line width of 2 points and an opacity of 0.6:
Edit #2: SOLVED!
James' idea of using a shadowPath is the solution. You don't want anything in your mask layer other than a shadowPath. If you do that you can use the shadowRadius property to control the amount of blurring, and smoothly animate a shadow path from a circle that fully encloses the view down to a vanishing point all in one step.
I've updated my github project at https://github.com/DuncanMC/GradientView.git to include both the radial gradient animation and the shadowPath animation for comparison.
Here is what the finished effect looks like, first with a 5 pixel blur radius:
And then with a 30 pixel blur radius:
You could use the shadowPath on a CAShapeLayer to create a circle with a blurred outline to use as the mask.
Create a shape layer with no fill or stroke (we only want to see the shadow) and add it as the layer mask to your view.
let shapeLayer = CAShapeLayer()
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowOpacity = 1
shapeLayer.shadowRadius = 5
self.maskLayer = shapeLayer
self.layer.mask = shapeLayer
And animate it with a CABasicAnimation.
func show(_ show: Bool) {
let center = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2)
var newPath: CGPath
if show {
let radius = sqrt(self.bounds.height*self.bounds.height + self.bounds.width*self.bounds.width)/2 //assumes view is square
newPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true).cgPath
} else {
newPath = UIBezierPath(arcCenter: center, radius: 0.01, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true).cgPath
}
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = self.maskLayer.shadowPath
shadowAnimation.toValue = newPath
shadowAnimation.duration = totalDuration
self.maskLayer.shadowPath = newPath
self.maskLayer.add(shadowAnimation, forKey: "shadowAnimation")
}
As you can set any path for the shadow it should also work for other shapes (star, box etc.).
Im trying to make an app which draws a fractal tree. I managed to make the code that generates all the start and end points from all the lines. I also managed to draw the lines but right now they are really boxy and want them to have rounded corners.
I using a UIView and using UIBezierPaths to draw the lines inside the view draw function. To retrieve the points I have an array of Branch objects inside a sigleton class. A branch object has among other things a startingPoint and a ending point which are both tuples( (x: Double, y: Double) ).
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setStroke()
for branch in Tree.shared.branches{
let path = UIBezierPath()
print(branch.startingPoint)
print(branch.endingPoint)
path.move(to: CGPoint(x: branch.startingPoint.x, y: branch.startingPoint.y))
path.addLine(to: CGPoint(x: branch.endingPoint.x, y: branch.endingPoint.y))
path.lineWidth = 3
path.stroke()
}
}
How could i make the corners rounded?
Also if someone knows a free library that could facilitate this Im also interested.
edit: Im not interested in how to generate the tree, I have done already done this part of the code I need help with drawing the lines.
You don't need a library, you just need to spend a little time learning how to draw curves with UIBezierPath, and curves are one of the things that that class is best at. A key to drawing curves is understanding how control points work. Here's an answer I wrote some time ago about how to smoothly connect curved lines, which I think will help. Play around with -addCurveToPoint:controlPoint1:controlPoint2:.
If you don't actually want curves, but really just want the corners to be rounded rather than pointy, then all you need to do is to set the lineJoinStyle to kCGLineJoinRound.
About rounded corners of some view, is just set the parameters below:
func adjustView(_ view: UIView){
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.red
view.layer.cornerRadius = 10
view.clipsToBounds = true
view.backgroundColor = UIColor.white
}
If you wish more information check the current documentation about layer property:
https://developer.apple.com/documentation/uikit/uiview/1622436-layer
I have a path that is filled and also used for a clip to provide a gradient. The rendering of the gradient is just a tiny bit smaller or differently anti-aliased compared with the rendering of the fill. This creates an outline effect:
Is there any way to remove this outline?
Note that:
Using two separately drawn identical paths does not solve the problem
Using no fill, but two gradients instead does not solve the problem. Even with just a gradient over another gradient, you get the outline.
It is possible to get the gradient I want without the outline by using just a solid gradient with no fill, but that makes animating the effects of the gradient more difficult later.
Here is the code:
let path4Path = UIBezierPath()
//ordinary drawing stuff here
path4Path.close()
fillColor.setFill() //set to black fill
path4Path.fill()
context.saveGState()
context.setAlpha(0.9)
path4Path.addClip()
context.drawLinearGradient(allToldGradient, start: CGPoint(x: 52.72, y: 114.48), end: CGPoint(x: 30.22, y: 91.99), options: [])
context.restoreGState()
I think you have to turn off antialiasing like this:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetShouldAntialias(context, NO);
CGContextSetAllowsAntialiasing(context, NO);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
Apple has made it very simple to to make linear and radial gradients, but is it possible to have the color of the gradient be set by a definable function? In my situation I want to make the fill color of an object to vary with a sinus function along the x-axis. It is not hard to make pngs and use them as patterns instead, but I just wonder if it is possible to make gradients where the red, green and blue components vary along certain axis with a sinus function instead.
Any answer is appreciated. Thanks in advance.
When you create the gradient using the CAGradientLayer class, you can use the colors property with a large number of colors and vary the color components according to the sine function. There will be linear interpolation between each pair of consecutive colors which will, however, not be noticeable when the number of colors (and locations) is large enough.
Here is an example that draws a sine gradient that variates back and forth between red and blue.
// Compute the colors using a sine step-size
let samples = 100
var colors = [CGColor]()
for i in 0..<samples {
let component = CGFloat(0.5 + sin(Double(i) / Double(samples - 1) * 4.0 * M_PI) / 2.0)
colors.append(UIColor(red: component, green: 0, blue: 1 - component, alpha: 1).CGColor)
}
// Create the gradient layer
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = colors
// Install the gradient layer
gradientLayer.frame = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, atIndex: 0)
The end result looks like this.
How can I draw such a conical gradient in iOS using Core Graphics / Quartz 2D API?
(source: ods.com.ua)
If anyone is still looking for a solution, Apple finally introduced .conic gradient type in iOS 12. Perfect for masking to create circular progress bar with gradient.
Example:
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.type = .conic
gradientLayer.colors = [UIColor.red.cgColor, UIColor.orange.cgColor, UIColor.green.cgColor]
gradientLayer.frame = bounds
Recently I've made a custom CALayer class for this: AngleGradientLayer
It's not tested for performance, so beware.
I wanted pure Swift solution for this, and it was also kind of a challenging task for me.
At the end, I wrote AEConicalGradient which does that with interesting approach (by using a circle and drawing lines of different colors to the center).
There is no Quartz function for this style of gradient. Unless you're ready to dig into mathematics behind it, I'd suggest you use pre-made images for this. It's not a problem if you need it only for opacity mask.