skip to Main Content

I’m a relatively new Swift developer and I am using the CILinearGradient CIFilter to generate gradients that I can then use as backgrounds and textures. I was pretty happy with the way it was working, until I realized that the gradients coming out of it seem to be heavily skewed towards away from the black end of the spectrum.

At first I thought I was nuts, but then I created pure black-to-white and white-to-black gradients and put them on screen next to each other. I took a screenshot and brought it into Photoshop. then I looked at the color values. You can see that the ends of each gradient line up (pure black over pure white on one end, and the opposite on the other), but the halfway point of each gradient is significantly skewed towards the black end.

enter image description here

Is this an issue with the CIFilter or am I doing something wrong? Thanks to anyone with any insight on this!

Here’s my code:

func gradient2colorIMG(UIcolor1: UIColor, UIcolor2: UIColor, width: CGFloat, height: CGFloat) -> CGImage? {
    if let gradientFilter = CIFilter(name: "CILinearGradient") {   
        let startVector:CIVector = CIVector(x: 0 + 10, y: 0)
        let endVector:CIVector = CIVector(x: width - 10, y: 0)
        let color1 = CIColor(color: UIcolor1)
        let color2 = CIColor(color: UIcolor2)
        let context = CIContext(options: nil)
        if let currentFilter = CIFilter(name: "CILinearGradient") {
            currentFilter.setValue(startVector, forKey: "inputPoint0")
            currentFilter.setValue(endVector, forKey: "inputPoint1")
            currentFilter.setValue(color1, forKey: "inputColor0")
            currentFilter.setValue(color2, forKey: "inputColor1")
            if let output = currentFilter.outputImage {
                if let cgimg = context.createCGImage(output, from: CGRect(x: 0, y: 0, width: width, height: height)) {
                    let gradImage = cgimg
                    return gradImage
                }
            }
        }
    }
    return nil
}

and then I call it in SpriteKit using this code (but this is just so I can see them on the screen to compare the CGImages that are output by the function) …

if let gradImage = gradient2colorIMG(UIcolor1: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), width: 250, height: 80) {        
    let sampleback = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
    sampleback.fillColor = .white
    sampleback.fillTexture = SKTexture(cgImage: gradImage)
    sampleback.zPosition = 200
    sampleback.position = CGPoint(x: 150, y: 50)
    self.addChild(sampleback)
}    
if let gradImage2 = gradient2colorIMG(UIcolor1: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), width: 250, height: 80) {    
    let sampleback2 = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
    sampleback2.fillColor = .white
    sampleback2.fillTexture = SKTexture(cgImage: gradImage2)
    sampleback2.zPosition = 200
    sampleback2.position = CGPoint(x: 150, y: 150)
    self.addChild(sampleback2)
}

As another follow-up, I tried doing a red-blue gradient (so purely a change in hue) and it is perfectly linear (see below). The issue seems to be around the brightness.

A red-blue gradient DOES ramp its hue in a perfectly linear fashion

2

Answers


  1. Imagine that black is 0 and white is 1. Then the problem here is that we intuitively think that 50% of black "is" a grayscale value of 0.5 — and that is not true.

    To see this, consider the following core image experiment:

    let con = CIContext(options: nil)
    let white = CIFilter(name:"CIConstantColorGenerator")!
    white.setValue(CIColor(color:.white), forKey:"inputColor")
    let black = CIFilter(name:"CIConstantColorGenerator")!
    black.setValue(CIColor(color:UIColor.black.withAlphaComponent(0.5)),
        forKey:"inputColor")
    let atop = CIFilter(name:"CISourceAtopCompositing")!
    atop.setValue(white.outputImage!, forKey:"inputBackgroundImage")
    atop.setValue(black.outputImage!, forKey:"inputImage")
    let cgim = con.createCGImage(atop.outputImage!, 
        from: CGRect(x: 0, y: 0, width: 201, height: 50))!
    let image = UIImage(cgImage: cgim)
    let iv = UIImageView(image:image)
    self.view.addSubview(iv)
    iv.frame.origin = CGPoint(x: 100, y: 150)
    

    What I’ve done here is to lay a 50% transparency black swatch on top of a white swatch. We intuitively imagine that the result will be a swatch that will read as 0.5. But it isn’t; it’s 0.737, the very same shade that is appearing at the midpoint of your gradients:

    enter image description here

    The reason is that everything here is happening, not in some mathematical vacuum, but in a color space adjusted for a specific gamma.

    Now, you may justly ask: "But where did I specify this color space? This is not what I want!" Aha. You specified it in the first line, when you created a CIContext without overriding the default working color space.

    Let’s fix that. Change the first line to this:

    let con = CIContext(options: [.workingColorSpace : NSNull()])
    

    Now the output is this:

    enter image description here

    Presto, that’s your 0.5 gray!

    So what I’m saying is, if you create your CIContext like that, you will get the gradient you are after, with 0.5 gray at the midpoint. I’m not saying that that is any more "right" than the result you are getting, but at least it shows how to get that particular result with the code you already have.

    (In fact, I think what you were getting originally is more "right", as it is adjusted for human perception.)

    Login or Signup to reply.
  2. The midpoint of the CILinearGradient appears to correspond to 188, 188, 188, which looks like the “absolute whiteness” rendition of middle gray, which is not entirely unreasonable. (The CISmoothLinearGradient offers a smoother transition, but it doesn’t have the midpoint at 0.5, 0.5, 0.5, either.) As an aside, the “linear” in CILinearGradient and CISmoothLinearGradient refer to the shape of the gradient (to differentiate it from a “radial” gradient), not the nature of the color transitions within the gradient.

    However if you want a gradient whose midpoint is 0.5, 0.5, 0.5, you can use CGGradient:

    func simpleGradient(in rect: CGRect) -> UIImage {
        return UIGraphicsImageRenderer(bounds: rect).image { context in
            let colors = [UIColor.white.cgColor, UIColor.black.cgColor]
            let colorSpace = CGColorSpaceCreateDeviceGray() // or RGB works, too
            guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil) else { return }
            context.cgContext.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: rect.maxX, y: 0), options: [])
        }
    }
    

    gradient image


    Alternatively, if you want a gradient background, you might define a UIView subclass that uses a CAGradientLayer as its backing layer:

    class GradientView: UIView {
        override class var layerClass: AnyClass { return CAGradientLayer.self }
        var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
    
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            configure()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            configure()
        }
    
        func configure() {
            gradientLayer.colors = [UIColor.white.cgColor, UIColor.black.cgColor]
            gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
            gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search