iOS Find Color at Point Between Two Colors
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];
}
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
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...
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)