Gradients in SpriteKit

In 1313 : Puzzle Game, I wanted to add a way for the players to customize the colour of the shapes. Colour blindness can make some of the default colours hard to distinguish from each other.

The easiest way to adjust the colour is to provide sliders for red, green, and blue components. I had code kicking around from ages ago, from a drawing game called Draw Breaker, but I couldn’t find it easily. So, I decided to search for a solution.

One of the top hits was a blog entry by Maxim Bilan called Sprite Kit Gradient. Very nicely written and easy to follow. I was a bit hesitant to use a CIFilter but if it works, 差不多就行了吧.

I quickly reworked Bilan’s code into my own. And it worked for both macOS and iOS. Cool.

But. It was way way too slow. That was my worry about using a CIFilter.

Gradients are not going to be fast anyhow but my first run at the code didn’t have a very large texture to work with. I was very unhappy with the performance.

Then I remembered that I had used CAGradientLayer in Draw Breaker.

Putting the parts together, here’s the code to make a simple gradient.

#if os(OSX)
    typealias CSImage           = NSImage
#else
    typealias CSImage           = UIImage
#endif

extension SKTexture {
    convenience init( color0:SKColor, color1:SKColor, size:CGSize ) {
        let layer = CAGradientLayer()
        layer.frame = CGRect(origin: CGPoint.zero, size: size)
        #if os(OSX)
            layer.colors = [color0.cgColor, color1.cgColor]
        #else
            layer.colors = [color1.cgColor, color0.cgColor]
        #endif
        let image = layer.image()
        self.init(image: image)
    }
}

extension CALayer {
    func image() -> CSImage {
        #if os(OSX)
            let width = Int(bounds.width * self.contentsScale)
            let height = Int(bounds.height * self.contentsScale)
            
            let imageRepresentation = NSBitmapImageRep(
                bitmapDataPlanes: nil,
                pixelsWide: width,
                pixelsHigh: height,
                bitsPerSample: 8,
                samplesPerPixel: 4,
                hasAlpha: true,
                isPlanar: false,
                colorSpaceName: NSDeviceRGBColorSpace,
                bytesPerRow: 0,
                bitsPerPixel: 0)!
            imageRepresentation.size = bounds.size
            
            let context = NSGraphicsContext(bitmapImageRep: imageRepresentation)!
            
            render(in: context.cgContext)
            
            return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
        #else
            UIGraphicsBeginImageContextWithOptions(self.frame.size, false, self.contentsScale)
            
            self.render(in: UIGraphicsGetCurrentContext()!)
            
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            return image!
        #endif
    }
}

In the code above, the resulting gradient will be color0 at the bottom and color1 at the top. I’ll leave it up to you to make the code more general.

One way to improve the speed of making a gradient texture is to reduce the width. In 1313, I’m making the texture a couple pixels wide and has tall as the slider. With CAGradientLayer and the 3 sliders I’m using, that does the job.