16
votes

I have a problem: I need to be able to take two colors and make a 'virtual gradient' out of them. I then need to be able to find the color at any point on this line. My current approach is this:

if (fahrenheit < kBottomThreshold)
{
    return [UIColor colorWithRed:kBottomR/255.0f green:kBottomG/255.0f blue:kBottomB/255.0f alpha:1];
}
if (fahrenheit > kTopThreshold)
{
    return [UIColor colorWithRed:kTopR/255.0f green:kTopG/255.0f blue:kTopB/255.0f alpha:1];
}

double rDiff = kTopR - kBottomR;
double gDiff = kTopG - kBottomG;
double bDiff = kTopB - kBottomB;

double tempDiff = kTopThreshold - kBottomThreshold;

double rValue;
double gValue;
double bValue;

rValue = kBottomR + ((rDiff/tempDiff) * fahrenheit);
gValue = kBottomG + ((gDiff/tempDiff) * fahrenheit);
bValue = kBottomB + ((bDiff/tempDiff) * fahrenheit);

return [UIColor colorWithRed:rValue/255.0f green:gValue/255.0f blue:bValue/255.0f alpha:1];

Variables:

  • fahrenheit is a variable passed into my function that is the number on this virtual line that I want to find the color for.
  • kTopR, kTopB, and kTopG are the RGB values for one end of the gradient. Same for their kBottom counterparts.
  • kBottomThreshold and kTopThreshold are the endpoints of my gradient.

Here's my problem: When fahrenheit goes over either end of the gradient, the gradient seems to 'jump' to a different value.

I've included an example project, hosted on my S3 server, here.

You really need to download the project and try it on the simulator/device to see what I mean (unless you are crazy smart and can tell just by looking at the code)

7

7 Answers

8
votes

The problem is that you're not subtracting kBottomThreshold from farenheit.

But let's simplify.

First, we want to map the input temperature to a parameter t in the range [0 ... 1]. Then, we want to map t to an output in the range [kBottomR ... kTopR], and also to an output in the range [kBottomG ... kTopG], and also to an output in the range [kBottomB ... kTopB].

UIColor *colorForDegreesFahrenheit(double fahrenheit) {
    double t = (fahrenheit - kBottomThreshold) / (kTopThreshold - kBottomThreshold);

    // Clamp t to the range [0 ... 1].
    t = MAX(0.0, MIN(t, 1.0));

    double r = kBottomR + t * (kTopR - kBottomR);
    double g = kBottomG + t * (kTopG - kBottomG);
    double b = kBottomB + t * (kTopB - kBottomB);

    return [UIColor colorWithRed:r/255 green:g/255 blue:b/255 alpha:1];
}
34
votes

Swift - 3.0 && 4.0

extension UIColor {
    func toColor(_ color: UIColor, percentage: CGFloat) -> UIColor {
        let percentage = max(min(percentage, 100), 0) / 100
        switch percentage {
        case 0: return self
        case 1: return color
        default:
            var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            guard self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return self }
            guard color.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return self }

            return UIColor(red: CGFloat(r1 + (r2 - r1) * percentage),
                           green: CGFloat(g1 + (g2 - g1) * percentage),
                           blue: CGFloat(b1 + (b2 - b1) * percentage),
                           alpha: CGFloat(a1 + (a2 - a1) * percentage))
        }
    }
}

Usage:-

let colorRed = UIColor.red
let colorBlue = UIColor.blue

let colorOutput = colorRed.toColor(colorBlue, percentage: 50)

Result

enter image description here

4
votes

In case your gradient is more complex than a 2 color gradient, you may consider drawing a CGGradientRef into a temporary CGImageRef and directly read RGBA values from the image buffer.

Here is something that I had to do with a 5 gradient stops and colors:

    CGFloat tmpImagewidth = 1000.0f; // Make this bigger or smaller if you need more or less resolution (number of different colors).
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // create a gradient
    CGFloat locations[] = { 0.0,
        0.35,
        0.55,
        0.8,
        1.0 };
    NSArray *colors = @[(__bridge id) [UIColor redColor].CGColor,
                        (__bridge id) [UIColor greenColor].CGColor,
                        (__bridge id) [UIColor blueColor].CGColor,
                        (__bridge id) [UIColor yellowColor].CGColor,
                        (__bridge id) [UIColor redColor].CGColor,
                        ];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGPoint startPoint = CGPointMake(0, 0);
    CGPoint endPoint = CGPointMake(tmpImagewidth, 0);

    // create a bitmap context to draw the gradient to, 1 pixel high.
    CGContextRef context = CGBitmapContextCreate(NULL, tmpImagewidth, 1, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);

    // draw the gradient into it
    CGContextAddRect(context, CGRectMake(0, 0, tmpImagewidth, 1));
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);

    // Get our RGB bytes into a buffer with a couple of intermediate steps...
    //      CGImageRef -> CFDataRef -> byte array
    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    CGDataProviderRef provider = CGImageGetDataProvider(cgImage);
    CFDataRef pixelData = CGDataProviderCopyData(provider);

    // cleanup:
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
    CGImageRelease(cgImage);
    CGContextRelease(context);

    const UInt8* data = CFDataGetBytePtr(pixelData);

    // we got all the data we need.
    // bytes in the data buffer are a succession of R G B A bytes

    // For instance, the color of the point 27% in our gradient is:
    CGFloat x = tmpImagewidth * .27;
    int pixelIndex = (int)x * 4; // 4 bytes per color
    UIColor *color = [UIColor colorWithRed:data[pixelIndex + 0]/255.0f
                                     green:data[pixelIndex + 1]/255.0f
                                      blue:data[pixelIndex + 2]/255.0f
                                     alpha:data[pixelIndex + 3]/255.0f];

    // done fetching color data, finally release the buffer
    CGDataProviderRelease(provider);

I am not saying this is better than the "math way" in the answer above, certainly there is a memory and cpu tax that goes into producing the temporary image. The advantage of this however, is that the code complexity stays the same no matter how many gradient stops you need...

4
votes

Thanks @ramchandra-n I implemented the extension to get the intermediate color from an array of colors by percentage

extension Array where Element: UIColor {
    func intermediate(percentage: CGFloat) -> UIColor {
        let percentage = Swift.max(Swift.min(percentage, 100), 0) / 100
        switch percentage {
        case 0: return first ?? .clear
        case 1: return last ?? .clear
        default:
            let approxIndex = percentage / (1 / CGFloat(count - 1))
            let firstIndex = Int(approxIndex.rounded(.down))
            let secondIndex = Int(approxIndex.rounded(.up))
            let fallbackIndex = Int(approxIndex.rounded())

            let firstColor = self[firstIndex]
            let secondColor = self[secondIndex]
            let fallbackColor = self[fallbackIndex]

            var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            guard firstColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return fallbackColor }
            guard secondColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return fallbackColor }

            let intermediatePercentage = approxIndex - CGFloat(firstIndex)
            return UIColor(red: CGFloat(r1 + (r2 - r1) * intermediatePercentage),
                           green: CGFloat(g1 + (g2 - g1) * intermediatePercentage),
                           blue: CGFloat(b1 + (b2 - b1) * intermediatePercentage),
                           alpha: CGFloat(a1 + (a2 - a1) * intermediatePercentage))
        }
    }
}

You can use it to get an intermediate color between two or more colors:

let color = [.green, .yellow, .red].intermediate(percentage: 70)
1
votes

I would like to extend/modify @Sebastien Windal´s answer, as his answer works great for my use case, but can be further improved by getting the pixelData directly from the context (see Get pixel data as array from UIImage/CGImage in swift).

I implemented his answer in swift, therefore the changes are also written in swift.

Just pass the Int Array to the context before drawing the gradient. The array will then be filled with the pixelData when drawing the context.

let dataSize = tmpImagewidth * 1 * 4
var pixelData = [UInt8](repeating: 0, count: Int(dataSize))


let context = CGContext(data: &pixelData, width: Int(tmpImagewidth), height: 1, bitsPerComponent: 8, bytesPerRow: 4 * Int(tmpImagewidth), space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)

Then we can skip creating an image first to read the pixelData from there.

1
votes

Swift 5.3

Get n colors between two colors:

import UIKit

extension CGFloat {
    var getPercentageValues: [CGFloat] {
        let increment: CGFloat = 100/(self-1)
        var values = [CGFloat]()
        let last: CGFloat = 100
        var value: CGFloat = 0
        while value <= last {
            values.append(value)
            value += increment
        }
        return values
    }
}

extension UIColor {
    func toColor(_ color: UIColor, percentage: CGFloat) -> UIColor {
        let percentage = max(min(percentage, 100), 0) / 100
        switch percentage {
        case 0: return self
        case 1: return color
        default:
            var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (.zero, .zero, .zero, .zero)
            var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (.zero, .zero, .zero, .zero)
            guard self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return self }
            guard color.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return self }
            
            return UIColor(red: CGFloat(r1 + (r2 - r1) * percentage),
                           green: CGFloat(g1 + (g2 - g1) * percentage),
                           blue: CGFloat(b1 + (b2 - b1) * percentage),
                           alpha: CGFloat(a1 + (a2 - a1) * percentage))
        }
    }
    
    func getColors(to color: UIColor, with quantity: CGFloat) -> [UIColor] { 
        quantity.getPercentageValues.map { self.toColor(color, percentage: $0 ) }
    }
}

let redColor = UIColor.red
let blueColor = UIColor.blue

print(redColor.getColors(to: blueColor, with: 10))

Result

enter image description here

0
votes

SwiftUI Color operator + overload

 func +(lhs: Color, rhs: Color) -> Color {
    let lhsUIColor = UIColor(lhs)
    let rhsUIColor = UIColor(rhs)
    
    var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
    var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
    guard lhsUIColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return Color(lhsUIColor) }
    guard rhsUIColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return Color(rhsUIColor) }

    return Color(Color.RGBColorSpace.sRGB, red: Double(r1 + r2) / 2, green: Double(g1 + g2) / 2, blue: Double(b1 + b2) / 2, opacity: Double(a1 + a2) / 2)
}