So, I was getting bored of looking at a grey background and, as a break from cleaning up, refactoring, and adding the fiddly (nut necessary) UI code, I decided to run up a quick background.
The original version of Nonoku had a vertical gradient fill, light to dark, with subtle diagonal striping. I liked this so decided to reproduce it. However, it made sense to me to implement this in code. Since I’m using SpriteKit
already, rendering to an SKEffectNode
which rasterises the nodes made sense (the background is not animated so I can basically update once and be done).
There’s no native way, as far as I’m aware, to draw a gradient on a node, so the first thing would be to create an SKTexture
. Again, since this is a one time deal, I’m not too worried. I’ve done something similar before so I already knew I could use a CIFilter to generate this. From that, I can get a CIImage
. That can be passed to a CIContext
to create a CGImage
which can finally be passed to the SKTexture
which I’ll pass into an SKSprite
.
From reviewing the CIFilter
documentation, I can get a list of available filters as follows:
class func filterNames(inCategory category: String?) -> [String]
Even better, under the list of category constants, there is kCICategoryGradient
. For this kind of quick exploratory code, I usually start a Swift Playground to spike out the things I don’t know.
So, skipping a step, I have this:
print("\(CIFilter.filterNames(inCategory: kCICategoryGradient))")
let filter = CIFilter(name: "CILinearGradient")
print("\(filter?.attributes)")
The list of filterNames has a couple of likely contenders, CILinearGradient
and CISmoothLinearGradient
. They take the same attributes (two colours, and two vectors) so I ended up trying both. In my use case I couldn’t see any difference between the two so decided to stick with CILinearGradient
. If it ever comes up as an issue, it’s a very simple change to make.
CIFilter.attributes()
gives me a list of the supported attributes, and a brief description of them. That's enough to define how I'm going to use it so I can leave the playground and come back to my code.
Since I want to create a new SKTexture
with this gradient it makes sense to do it as an extension on SKTexture
. I need the size, the start colour, and the end colour. I could add additional logic here but, following YAGNI (You Ain’t Gonna Need It), I am only interested in a vertical gradient so that’s all I’ll support.
extension SKTexture {
convenience init?(size: CGSize, color1: SKColor, color2: SKColor) {
guard let filter = CIFilter(name: "CILinearGradient") else {
return nil
}
let startVector = CIVector(x: 0, y: size.height)
let endVector = CIVector(x: 0, y: 0)
filter.setValue(startVector, forKey: "inputPoint0")
filter.setValue(endVector, forKey: "inputPoint1")
filter.setValue(CIColor(color: color1), forKey: "inputColor0")
filter.setValue(CIColor(color: color2), forKey: "inputColor1")
let context = CIContext(options: nil)
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
guard let filterImage = filter.outputImage,
let image = context.createCGImage(filterImage, from: rect) else {
return nil
}
self.init(cgImage: image)
}
}
Interestingly, whilst I can just get a ciColor
from a UIColor
(SKColor
aliases UIColor
), I can’t use it in the CIFilter
due to the following issue.
*** -CIColor not defined for the UIColor UIExtendedSRGBColorSpace 0.666667 0.223529 0.223529 1; need to first convert colorspace.
Instead, I had to create a new instance of CIColor
with the UIColor
. I’m not sure if there are any issues associated with this, but it’s working fine for me so far.
From there, I just created an SKSpriteNode
with the generated texture. The lines were just stroked SKShapeNodes
with a CGPath
which just have a defined start and end point.
So here’s how it looks: