I need to draw an animated linear gradient that is updated on every frame, using CADisplayLink. Drawing directly into a full-screen context with CoreGraphics is slow, I get around 40fps at full CPU load on an iPad Air.
What's the fastest way to do this?
Update
As #Kurt Revis pointed out in a comment on the question, the proper - and fastest - way to do this is to use CAGradientLayer. Add a CAGradientLayer to your view that fills its bounds, then:
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Disable implicit animations if this is called via CADisplayLink
CATransaction.begin()
CATransaction.setDisableActions(true)
// Draw to existing CAGradientLayer
gradientBackgroundLayer.colors = gradientColors
gradientBackgroundLayer.locations = gradientLocs
CATransaction.commit()
}
Old answer
After some experimentation, I ended up using CoreGraphics to draw the gradient into a 1px wide CGImage and then rely on UIImageView to do the scaling. I get solid 60fps at about 7-11% CPU load, depending on how many colors the gradient contains:
Subclass UIImageView with contentMode set to ScaleToFill, then call the following via CADisplayLink for a continuous animation.
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Create context at 1px width and display height
let gradientWidth:Double = 1
let gradientHeight:Double = Double(UIScreen.mainScreen().bounds.height)
UIGraphicsBeginImageContext(CGSize(width: gradientWidth, height: gradientHeight))
let buffer:CGContextRef = UIGraphicsGetCurrentContext()!
// Draw gradient into buffer
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorspace, gradientColors, gradientLocs)
CGContextDrawLinearGradient(
buffer,
gradient,
CGPoint(x: 0.0, y: 0.0),
CGPoint(x: 0.0, y: gradientHeight),
CGGradientDrawingOptions.DrawsAfterEndLocation
)
// Let UIImageView superclass handle scaling
image = UIImage.init(CGImage: CGBitmapContextCreateImage(buffer)!)
// Avoid memory leaks...
UIGraphicsEndImageContext()
}
Caveat: this approach will only work for horizontal or vertical linear gradients.
Are there better ways of doing this?
Related
I want to create a gradient view with three colours in swift.
linear-gradient(to right, #232c63 10%,#1d6cc1 59%,#33bdd6 100%);
How to define colours with %.
First Colour with 10 %
Second Colour with 59 %
Third Colour with 100 %
Example:
If you want to use hex colors, use extension like these: https://stackoverflow.com/a/24263296/4711785
After that you can use colorWithAlphaComponent instance method like above which is mentioned in official documentation
UIColor.black.withAlphaComponent(0.7)
Try using the locations array of CAGradientLayer to give the startPoint for each color in the gradient, i.e.
let gradient = CAGradientLayer()
gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor, UIColor.green.cgColor]
gradient.startPoint = .zero
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.locations = [0, 0.1, 0.59]
gradient.frame = view.bounds
view.layer.addSublayer(gradient)
In the above code, locations are calculated using the required percentage, i.e. 10%, 59%, 100%.
Also add the colors as per your requirement.
var locations: [NSNumber]? { get set }
An optional array of NSNumber objects defining the location of each
gradient stop. Animatable.
The gradient stops are specified as values between 0 and 1. The values
must be monotonically increasing. If nil, the stops are spread
uniformly across the range. Defaults to nil.
With this method you can choose the three colours you want:
func gradient(initialColor: UIColor, middleColor: UIColor,
finalColor: UIColor) -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.frame = self.frame
gradient.colors = [initialColor.withAlphaComponent(1.0), middleColor.withAlphaComponent(0.59), finalColor.withAlphaComponent(1.0)].map{$0.cgColor}
gradient.locations = [0.29, 0.60, 1]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint (x: 1.0, y: 0.5)
return gradient
}
It returns a CAGradientLayer that you can add directly into your view.
When using the "locations" parameter, you can choose where your colours are going to be displayed in terms of percentage. In my method they are: 0% (0 value), 50% (0.5 value) and 100% (1 value).
I recommend using that method inside a UIView extension as a best practice and reusable code in your app.
I want to draw a linear gradient in different colorspace in iOS device with CoreAnimation or CoreGraphics. However, I found that CAGradientLayer(Linear gradient) only draws linear in device colorspace sense, and also alpha addition is considered in device colorspace. Specifically, I want to simulate another gamma correction. Is there anyway to give custom gamma curve to the gradient?
Moreover, the ultimate objective is to make it animatable. (E.g. define gamma animatable property on CALayer.) Any related idea is okay. I could not find discussions about this.
I don't exact syntax for objective c but I can help u in swift.
Here I created an extension for a view to show gradient effect.
extension UIView {
func layerGradient() {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor.UIColorFromRGB(fromHex: 0x26344b).cgColor, UIColor.UIColorFromRGB(fromHex: 0x30bdb7).cgColor]
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 1.0)
gradient.endPoint = CGPoint(x: 1.0, y: 1.0)
gradient.frame = CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: self.frame.size.height)
self.layer.insertSublayer(gradient, at: 0)
}
}
In my app I need to have a UIView whose form is initially a circle but then it can be altered into an ellipse by changing its height or width. So, I have overridden the drawRect() method like the following:
override func drawRect(rect: CGRect) {
if (self.isRound) {
let ctx:CGContext = UIGraphicsGetCurrentContext()!
CGContextSetRGBStrokeColor(ctx, 0.0, 0.0, 0.0, 1.0)
CGContextSetLineWidth(ctx, 0.5)
CGContextAddEllipseInRect(ctx, rect)
CGContextStrokePath(ctx)
self.layer.cornerRadius = frame.size.width / 2
}
}
which works quite well, returning this result:
I would like to know how to hide, if possibile, those 4 corners so that only the ellipse itself is strictly visibile. Is there a way?
UPDATE and SOLUTION
This is the solution I have found, looking here in all Apple Core Graphics functions concerning ellipses. First of all, if my UIView is supposed to be an ellipse, in its constructor I take the following trick concerning alpha parameter:
if (self.isRound) {
self.backgroundColor = UIColor(red: 0.0, green: 0.6, blue: 1.0, alpha: 0.0)
}
else {
self.backgroundColor = UIColor(red: 0.0, green: 0.6, blue: 1.0, alpha: 0.4)
}
In this way the background rectangle is not showed at all. Then, this is my drawRect() method:
override func drawRect(rect: CGRect) {
if (self.isRound) {
let ctx:CGContext = UIGraphicsGetCurrentContext()!
CGContextSetRGBFillColor(ctx, 0.0, 0.6, 1.0, 0.4) /*!!!*/
CGContextFillEllipseInRect(ctx, rect) /*use Fill and not Stroke */
CGContextStrokePath(ctx)
}
}
which gives me this result:
You need a clipping path, have a look at CGContextClip
I'm trying to add opacity to my view. When I add a CAGradientlayer, I roughly get, what I planned, but have a few issues.
I can't change the color. No matter what values I use, I'll always get a white layer
Even though I wanted it to be distributed over the whole view, it seems like it has an offset to the left.
My code is as follows:
let maskLayer = CAGradientLayer()
maskLayer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
maskLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
maskLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
maskLayer.colors = [UIColor(white: 0.0, alpha: 0.0).CGColor, UIColor(white: 1.0, alpha: 1.0).CGColor, UIColor(white: 0.0, alpha: 0.0).CGColor]
maskLayer.locations = [0.0, 0.5, 1.0]
maskLayer.bounds = CGRectMake(0, 0, self.frame.width, self.frame.height)
self.layer.mask = maskLayer
What am I doing wrong?
Thanks
About the white, this is because a layer mask does not contribute to color, only transparency. Essentially, only the alpha component has an effect, and the R,G,B components of the layer mask are ignored.
About the offset, could it be because your view is getting resized, and your layer isn't kept up to date? If so you could see the answer to this other question: CALayers didn't get resized on its UIView's bounds change. Why?
can anyone point me to a tutorial or sample code (or both!) that explains the correct way to add views and layers for iOS.
ideally in Swift not Obj C.
I have code that works when adding SKNodes by themselves, no problem, but if i add a gradient layer - i want that to be sky in the background, the Gradient covers everything else.
I've tried various forms of adding sublayers at index above, etc, but nothing seems to work. I think this is my basic understanding of CALayer and UIView. I can't find a good tutorial :(
any help appreciated.
cheers
Adam.
here is a sample.
the view fills with the gradient, which is great, but the button seems to be hidden underneath, despite trying to force it above the background layer.
import QuartzCore
class NightScene: SKScene, SKPhysicsContactDelegate {
let playButton = SKSpriteNode(imageNamed: "play")
let gradient = CAGradientLayer()
gradient.frame = view.bounds
let colorTop = UIColor(red: 0.0, green: 0.0, blue: 0.100, alpha: 1.0).CGColor
let colorMid = UIColor(red: 0.0, green: 0.0, blue: 0.450, alpha: 1.0).CGColor
let colorBottom = UIColor(red: 0.0, green: 0.0, blue: 0.260, alpha: 1.0).CGColor
let arrayColors: Array <AnyObject> = [colorTop, colorMid, colorBottom]
gradient.colors = arrayColors
gradient.locations = [ 0.0, 0.4, 1.0]
self.view.layer.insertSublayer(gradient, atIndex:0)
let sceneLayer = CALayer()
sceneLayer.frame = view.bounds
self.view.layer.insertSublayer(sceneLayer, above: gradient)
self.playButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidX(self.frame)*2.5)
self.addChild(playButton)
}
Even though this question is over a year old I am going to provide an answer after countless hours of searching myself. I found numerous similar questions with no / vague responses.
What I come to find out is that nodes are not layer backed like gradients. This results in the gradient taking over the whole UI view. Nodes are not visible even with zLocation set. This is for SpriteKit based projects.
I stumbled across a library that you can use to create gradient background. This library basically converts a gradient into a SKSpriteNode. Gradient -> Texture -> Node
https://www.github.com/braindrizzlestudio/BDGradientNode
From the developers
We wrote BDGradientNode because gradients are a bit of a pain to set up on the fly in SpriteKit. UIViews are CALayer-backed and have access to CAGradientLayer, making them simple--fairly simple--to make. (Though sweep gradients can't be done.) SKNodes are NOT layer-backed. While you can jump through some hoops to make images, and so textures, from layers that can be assigned to an SKSpriteNode (which is what we first did when we just needed a simple gradient in SpriteKit) it's both cumbersome and inflexible.
Simple Linear Background
Once the library is added your Xcode project, you can use the following to create a linear gradient background:
let color1 = UIColor(red: 0/255, green: 118/255, blue: 162/255, alpha: 1.0)
let color2 = UIColor(red: 0/255, green: 85/255, blue: 116/255, alpha: 1.0)
let colors = [color1, color2]
let blending : Float = 1.0
let startPoint = CGPoint(x: 0.5, y: 1.0)
let endPoint = CGPoint(x: 0.5, y: 0.0)
let nodeSize = CGSize(width: self.frame.width, height: self.size.height)
let texture = SKTexture(imageNamed: "dummypixel")
let myGradientNode = BDGradientNode(linearGradientWithTexture: texture, colors: colors, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: false, size: nodeSize)
myGradientNode.zPosition = 0
myGradientNode.position = CGPointMake(self.size.width/2, self.size.height/2)
I found the tutorial here very helpful:
http://www.raywenderlich.com/75270/make-game-like-candy-crush-with-swift-tutorial-part-1
It contains layers and is in Swift. I also followed a Game of Life tutorial which used the zPosition of a node to help with layering, though they were not called layers and were not set up like your gradient:
https://www.makegameswith.us/gamernews/399/create-the-game-of-life-using-swift-and-spritekit
I found that even when I removed the gradient I could not view your button in its location, but we might just be working with differently sized devices. Is your sceneLayer empty? I didn't understand the relationship between your sceneLayer and your playButton.