Interpolate from one color to another
Asked Answered
W

7

25

I am trying to get an interpolation of one color to another shade of the same color. (for eg: sky blue to dark blue and then back).

I stumbled upon some code that could be used if the range was from 0-255 or 0-1. However, in my case, I have the RGB codes for Color1 and Color2, and want the rotation to occur.

Color 1: 151,206,255
Color 2: 114,127,157

Any ideas how to go about this?

Warlike answered 21/11, 2012 at 8:17 Comment(11)
Normally you would do the interpolation in another colour space, e.g. HSV, and convert the interpolated HSV values back to RGB.Laclair
Your color codes seems to be RGB...Bonnet
@PaulR: Why is that we won't use RGB in interpolation?Warlike
@Synxis: These are RGB in fact and I was thinking of interpolation in RGB terms. Didn't know about HSV stuff ;/Warlike
The HSV colour space corresponds much more closely to colour as it is perceived by humans - if you want a "natural" interpolation between two colours you therefore want to use a colour space that varies in an appropriate way - e.g. you don't want the perceived brightness to change as you interpolate.Laclair
@Warlike You can of course interpolate in RGB, you just may get funny colors in between. It may work in your case, actually.Decennary
@Warlike Have you solved your problem ? If so, please accept an answer. Else, you should put a comment saying what is still an obstacle.Bonnet
@PaulR the HSV space doesn't have constant perceived brightness. A color with yellow hue will appear much brighter than a color with a blue hue, if the saturation is high and V is the same.Subauricular
@MarkRansom What do you suggest instead, then?Lamprophyre
@DavidDoria lots of people use CIELab space. But my main point was to make sure nobody had the illusion that HSV was a perceptual color space.Subauricular
Possibly useful: I use colorhexa.com which allows interpolation by just typing two (or more) colors separated by a plus (e.g. rgb 128 168 153 + rgb 152 231 342) and then shows the results in various color systems. So if you've programmed something, compare results; and/or follow their method.Disclaim
D
19

I suggest you convert RGB to HSV, then adjust its components, then convert back to RGB.

Wikipedia has an article about it, and it's been discussed here before:

HSL to RGB color conversion

Algorithm to convert RGB to HSV and HSV to RGB in range 0-255 for both

Also many frameworks have conversion functions, for example Qt has QColor class.


But the question was about the actual interpolation... here's a trivial interpolation function:

// 0 <= stepNumber <= lastStepNumber
int interpolate(int startValue, int endValue, int stepNumber, int lastStepNumber)
{
    return (endValue - startValue) * stepNumber / lastStepNumber + startValue;
}

So call that for all color components you want to interpolate, in a loop. With RBG interpolation, you need to interpolate every component, in some other color space you may need to interpolate just one.

Decennary answered 21/11, 2012 at 8:24 Comment(3)
If you want an example loop, you could add some more info to the question, like how do you get the colors, what the color struct/class looks like, and where do you want to put the results (series of colors).Decennary
HSV is only an improvement over RGB if the hue values are widely separated.Subauricular
Yes, but then it’s the difference between creating ugly dark border artifacts and a smooth transition: tavmjong.free.fr/SVG/COLOR_INTERPOLATIONFlemish
C
37

I know this is little bit old, but is worthy if someone is searching for it.

First of all, you can do interpolation in any color space, including RGB, which, in my opinion, is one of the easiest.

Let's assume the variation will be controlled by a fraction value between 0 and 1 (e.g. 0.3), where 0 means full color1 and 1 means full color2.

The theory:

Result = (color2 - color1) * fraction + color1

Applying:

As the RGB has 3 channels (red, green and blue) we have to perform this math for each one of the channels.

Using your example colors:

fraction: 0.3
color1: 151,206,255
color2: 114,127,157

R =  (114-151) * fraction + 151
G =  (127-206) * fraction + 206
B =  (157-255) * fraction + 255

Code example in C/C++:

/**
 * interpolate 2 RGB colors
 * @param color1    integer containing color as 0x00RRGGBB
 * @param color2    integer containing color as 0x00RRGGBB
 * @param fraction  how much interpolation (0..1)
 * - 0: full color 1
 * - 1: full color 2
 * @return the new color after interpolation
 */
int interpolate(int color1, int color2, float fraction)
{
        unsigned char   r1 = (color1 >> 16) & 0xff;
        unsigned char   r2 = (color2 >> 16) & 0xff;
        unsigned char   g1 = (color1 >> 8) & 0xff;
        unsigned char   g2 = (color2 >> 8) & 0xff;
        unsigned char   b1 = color1 & 0xff;
        unsigned char   b2 = color2 & 0xff;

        return (int) ((r2 - r1) * fraction + r1) << 16 |
                (int) ((g2 - g1) * fraction + g1) << 8 |
                (int) ((b2 - b1) * fraction + b1);
}

/* 
 * 0x0097ceff == RGB(151,206,255)
 * 0x00727f9d == RGB(114,127,157)
 */
int new_color = interpolate(0x0097ceff, 0x00727f9d, 0.3f);

EDIT 1.

As pointed out by flying sheep, the RGB format may introduce some undesired colors during a blend operation. To solve this problem new steps may be added to the process: convert to linear RGB, perform the interpolation and then convert it back. Below a working example in C that also generates a PPM file for visualization.

/* vim: set sw=4 ts=4 nowrap hlsearch: */

#include <stdio.h>
#include <math.h>
#include <inttypes.h>

/**
 * convert a single default RGB (a.k.a. sRGB) channel to linear RGB
 * @param channel    R, G or B channel
 * @return channel in linear RGB
 */
float rgb2linear(uint8_t channel)
{
    float s = channel / 255.0f;

    return s <= 0.04045 ? s / 12.92 : pow((s + 0.055) / 1.055, 2.4);
}

/**
 * convert a linear RGB channel to default RGB channel (a.k.a. sRGB)
 * @param linear    R, G or B channel in linear format
 * @return converted channel to default RGB value
 */
uint8_t linear2rgb(float linear)
{
    float s = linear <= 0.0031308 ? linear * 12.92 : 1.055 * pow(linear, 1.0/2.4) - 0.055;
    return (uint8_t) (s * 255);
}

/**
 * interpolate 2 RGB colors
 * @param color1    integer containing color as 0x00RRGGBB
 * @param color2    integer containing color as 0x00RRGGBB
 * @param fraction  how much interpolation (0..1)
 * - 0: full color 1
 * - 1: full color 2
 * @return the new color after interpolation
 */
int interpolate(int color1, int color2, float fraction)
{
    uint8_t in[3];

    for(int x = 0; x < 3; x++) {
        float c1 = rgb2linear((color1 >> (16 - x * 8)) & 0xff);
        float c2 = rgb2linear((color2 >> (16 - x * 8)) & 0xff);
        in[x] = linear2rgb((c2 - c1) * fraction + c1);
    }
    return in[0] << 16 | in[1] << 8 | in[2];;
}

/**
 * example interpolating sRGB colors
 * first the colors are converted to linear-RGB,
 * interpolated and then converted back to sRGB
 */
int main(int argc, char *argv[])
{
    float factor = 0.3f;
    int c1 = 0x0097ceff; /* RGB(151,206,255) */
    int c2 = 0x00727f9d; /* RGB(114,127,157) */
    int nc = interpolate(c1, c2, factor);
    printf("interpolate 0x%08x to 0x%08x ==> 0x%08x factor: %.2f\n", c1, c2, nc, factor);
    /* create a PPM file to vizualize the interpolation
     * gradients in 100 steps - PPM files may be opened
     * in Gimp or other image editors */
    FILE *f = fopen("interpolate.ppm", "w");
    if(f) {
        /* PPM file header */
        fprintf(f, "P3\n");
        fprintf(f, "707 50\n");
        fprintf(f, "255\n");
        /* iterate over the rows */
        for(int x = 0; x < 50; x++) {
            /* iterate over the steps */
            for(int k = 0; k <= 100; k++) {
                factor = k * 0.01f;
                nc = interpolate(c1, c2, factor);
                /* iterate over the cols */
                for(int y = 0; y < 7; y++) {
                    fprintf(f, "%d %d %d%s", (nc >> 16) & 0xff, (nc >> 8) & 0xff, nc & 0xff, k+1 > 100 && y+1 >= 7 ? "\n" : " ");
                }
            }
        }
        fclose(f);
    }
    return 0;
}

PPM image:

enter image description here

Chaplin answered 9/1, 2014 at 2:5 Comment(4)
Interpolating in the RGB space is a really bad idea: tavmjong.free.fr/SVG/COLOR_INTERPOLATIONFlemish
not a bad ideia, no, unless you are developing a photophop or something really precise.Chaplin
It’s not about academic accuracy, it’s about gradients that don’t introduce random colors in the middle. Going from bright red to bright green should go over yellow, not muddy brown: codepen.io/flying-sheep/pen/wPmKmBFlemish
@flyingsheep changed to use linear RGB. Thanks for sharing.Chaplin
D
19

I suggest you convert RGB to HSV, then adjust its components, then convert back to RGB.

Wikipedia has an article about it, and it's been discussed here before:

HSL to RGB color conversion

Algorithm to convert RGB to HSV and HSV to RGB in range 0-255 for both

Also many frameworks have conversion functions, for example Qt has QColor class.


But the question was about the actual interpolation... here's a trivial interpolation function:

// 0 <= stepNumber <= lastStepNumber
int interpolate(int startValue, int endValue, int stepNumber, int lastStepNumber)
{
    return (endValue - startValue) * stepNumber / lastStepNumber + startValue;
}

So call that for all color components you want to interpolate, in a loop. With RBG interpolation, you need to interpolate every component, in some other color space you may need to interpolate just one.

Decennary answered 21/11, 2012 at 8:24 Comment(3)
If you want an example loop, you could add some more info to the question, like how do you get the colors, what the color struct/class looks like, and where do you want to put the results (series of colors).Decennary
HSV is only an improvement over RGB if the hue values are widely separated.Subauricular
Yes, but then it’s the difference between creating ugly dark border artifacts and a smooth transition: tavmjong.free.fr/SVG/COLOR_INTERPOLATIONFlemish
B
13

Convert your RGB colors to HSV then interpolate each component (not only the color, see end of answer), afterwards you can convert back to RGB.

You can do RGB interpolation, but the results are better with HSV, because in this space color is separated from luminance and saturation (Wikipedia article on HSV). HSV interpolation is more "logical" than the RGB one, because with the latter you can get extra colors while interpolating.

Some code for interpolation:

template<typename F>
ColorRGB interpolate(ColorRGB a, ColorRGB b, float t, F interpolator)
{
    // 0.0 <= t <= 1.0
    ColorHSV ca = convertRGB2HSV(a);
    ColorHSV cb = convertRGB2HSV(b);
    ColorHSV final;

    final.h = interpolator(ca.h, cb.h, t);
    final.s = interpolator(ca.s, cb.s, t);
    final.v = interpolator(ca.v, cb.v, t);

    return convertHSV2RGB(final);
}

int linear(int a, int b, float t)
{
    return a * (1 - t) + b * t;
}

// use: result = interpolate(color1,color2,ratio,&linear);
Bonnet answered 21/11, 2012 at 8:22 Comment(1)
To expand on "you can get extra colors"... in RGB, for example, linear interpolation from blue to yellow goes unintuitively through neutral gray; HSV goes either through green, or through magenta, red, orange. So you'll get extra colors either way. In fact, the linear() HSV interpolation shown will sometimes add a range of hues, as it doesn't take into account that hue is cyclical. Going from red (at 0 degrees) to violet (at 300) will produce orange, yellow, green, blue--almost the whole spectrum--when you'd probably want to normalize and take the shorter route through violet.Gigue
D
6

The best color space to use for visual effects is HCL. This is a space created specifically to look good while traversing its dimension, where "look good" does not relate to any physical properties of light or ink, like RGB and CMYK respectively.

Using HCL is expensive so the best thing to do is to create a number of intermediary values in this space and then interpolate in RGB which is native. This is what I have done in my animation engine.

Here is a snippet from it, in Swift 4.0

extension UIColor {
    typealias Components = (CGFloat, CGFloat, CGFloat, CGFloat)
    enum Space: String {
        case RGB = "RGB"
        case HSB = "HSB"
        case HCL = "HCL"
    }
    func components(in space: UIColor.Space) -> Components {
        switch space {
        case .RGB: return self.rgba // var defined in HandyUIKit's extension
        case .HSB: return self.hsba // var defined in HandyUIKit's extension
        case .HCL: return self.hlca // var defined in HandyUIKit's extension
        }
    }
    func spectrum(to tcol: UIColor, for space: UIColor.Space) -> [UIColor] {
        var spectrum = [UIColor]()
        spectrum.append(self)
        let fcomps  = self.components(in: space)
        let tcomps  = tcol.components(in: space)
        for i in 0 ... 5 {
            let factor  = CGFloat(i) / 5.0
            let comps   = (1.0 - factor) * fcomps + factor * tcomps
            let color   = UIColor(with: comps, in: space)
            spectrum.append(color)
        }
        spectrum.append(tcol)
        return spectrum
    }
    convenience init(with components: Components, in space: Space) {
        switch space {
        case .RGB: self.init(red: components.0, green: components.1, blue: components.2, alpha: components.3)
        case .HSB: self.init(hue: components.0, saturation: components.1, brightness: components.2, alpha: components.3)
        case .HCL: self.init(hue: components.0, luminance: components.1, chroma: components.2, alpha: components.3)
        }
    }
}
func *(lhs:CGFloat, rhs:UIColor.Components) -> UIColor.Components {
    return (lhs * rhs.0, lhs * rhs.1, lhs * rhs.2, lhs * rhs.3)
}
func +(lhs:UIColor.Components, rhs:UIColor.Components) -> UIColor.Components {
    return (lhs.0 + rhs.0, lhs.1 + rhs.1, lhs.2 + rhs.2, lhs.3 + rhs.3)
}

Both the engine and the example above are using HandyUIKit for the conversions between spaces so please add this project to whatever you are building for the code above to work.

I have written an article about it.

Duero answered 6/11, 2017 at 11:40 Comment(2)
Article link is 404, but this appears to be it: medium.com/@michael.m/true-color-interpolation-a1a17352ebf0Namtar
@Namtar Thanks!Duero
W
0

I see that you have tagged this question under "openframeworks" tag. so all you need to do is use the method ofColor::getLerped or ofColor::lerp

getLerped returns new value, while lerp modifies the color.

for example:

ofColor c1(151,206,255);
ofColor c2(114,127,157);


float p = 0.2f;
ofColor c3 = c1.getLerped(c2, p);

or

c1.lerp(c2, 0.3f);
Wasson answered 19/12, 2012 at 23:29 Comment(0)
R
0

I have adapted Synxis's C example (above) into an executable JavaScript program.

The program interpolates the color yellow, from red and green. The input and output is in RGB-space, but the interpolation is handled in the HSV-space. I also added an RGB interpolation example. As you can see below, a dark-yellow is produced if you interpolate red and green in RGB-space.

/** Main */
var red        = { r : 255, g : 0,   b : 0 };
var green      = { r : 0,   g : 255, b : 0 };
var yellow     = interpolateHsv(red, green, 0.5, linear);
var darkYellow = interpolateRgb(red, green, 0.5, linear);

document.body.innerHTML =
  'Yellow: '      + JSON.stringify(yellow,     null, '  ') + '<br />' +
  'Dark Yellow: ' + JSON.stringify(darkYellow, null, '  ');

/**
 * Returns an HSV interpolated value between two rgb values. 
 *
 * @param {Object} rgbA - rgb() tuple
 * @param {Object} rgbB - rgb() tuple
 * @param {Number} threshold - float between [0.0, 1.0]
 * @param {function} interpolatorFn - interpolator function
 * @return {Object} rbg
 */
function interpolateHsv(rgbA, rgbB, threshold, interpolatorFn) {
  var hsvA = rgbToHsv(rgbA);
  var hsvB = rgbToHsv(rgbB);
  threshold = toArray(threshold, 3);
  return hsvToRgb({
    h : interpolatorFn(hsvA.h, hsvB.h, threshold[0]),
    s : interpolatorFn(hsvA.s, hsvB.s, threshold[1]),
    v : interpolatorFn(hsvA.v, hsvB.v, threshold[2])
  });
}

/**
 * Returns an RGB interpolated value between two rgb values. 
 *
 * @param {Object} rgbA - rgb() tuple
 * @param {Object} rgbB - rgb() tuple
 * @param {Number} threshold - float between [0.0, 1.0]
 * @param {function} interpolatorFn - interpolator function
 * @return {Object} rbg
 */
function interpolateRgb(rgbA, rgbB, threshold, interpolatorFn) {        
  threshold = toArray(threshold, 3);
  return {
    r : ~~interpolatorFn(rgbA.r, rgbB.r, threshold[0]),
    g : ~~interpolatorFn(rgbA.g, rgbB.g, threshold[1]),
    b : ~~interpolatorFn(rgbA.b, rgbB.b, threshold[2])
  };
}

/**
 * Returns an interpolated value between two values. 
 *
 * @param {Number} valueA - color channel int value
 * @param {Number} valueB - color channel int value
 * @param {Number} threshold - float between [0.0, 1.0]
 * @param {function} interpolatorFn - interpolator function
 * @return {int}
 */
function linear(valueA, valueB, threshold) {
  return valueA * (1 - threshold) + valueB * threshold;
}

/**
 * Converts an RGB color value to HSV. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
 * Assumes r, g, and b are contained in the set [0, 255] and
 * returns h, s, and v in the set [0, 1].
 *
 * @param {Object} rgb - Color in rgb mode
 * @return {Object} - Color in hsv mode
 */
function rgbToHsv(rgb) {
  var r = rgb.r / 255,
      g = rgb.g / 255,
      b = rgb.b / 255;
  var max = Math.max(r, g, b), min = Math.min(r, g, b);
  var h, s, v = max;
  var d = max - min;
  s = max === 0 ? 0 : d / max;
  if (max == min) {
    h = 0; // achromatic
  } else {
    switch(max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }
    h /= 6;
  }
  return {
    h : h,
    s : s,
    v : v
  };
}

/**
 * Converts an HSV color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
 * Assumes h, s, and v are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param {Object} hsv - Color in hsv mode
 * @return {Object} - Color in rgb mode
 */
function hsvToRgb(hsv){
  var r, g, b, i, f, p, q, t,
      h = hsv.h,
      s = hsv.s,
      v = hsv.v;
  i = Math.floor(h * 6);
  f = h * 6 - i;
  p = v * (1 - s);
  q = v * (1 - f * s);
  t = v * (1 - (1 - f) * s);
  switch(i % 6){
    case 0: r = v, g = t, b = p; break;
    case 1: r = q, g = v, b = p; break;
    case 2: r = p, g = v, b = t; break;
    case 3: r = p, g = q, b = v; break;
    case 4: r = t, g = p, b = v; break;
    case 5: r = v, g = p, b = q; break;
  }
  return {
    r : r * 255,
    g : g * 255,
    b : b * 255
  };
}

function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

function toArray(arr, size) {
  var isNum = isNumeric(arr);
  arr = !Array.isArray(arr) ? [arr] : arr;
  for (var i = 1; i < size; i++) {
    if (arr.length < size) {
      arr.push(isNum ? arr[0] : 0);
    }
  }
  return arr;
}
River answered 24/4, 2015 at 15:42 Comment(0)
C
0

Here's a Swift 2 version based on @hyde's answer:

import UIKit

func interpolate(start start: CGFloat, end: CGFloat, progress: CGFloat) -> CGFloat {
  return (end - start) * progress + start
}


extension UIColor {
  func interpolateTo(color end: UIColor, progress: CGFloat) -> UIColor {
    var r1: CGFloat = 0
    var g1: CGFloat = 0
    var b1: CGFloat = 0
    var a1: CGFloat = 0
    getRed(&r1, green: &g1, blue: &b1, alpha: &a1)

    var r2: CGFloat = 0
    var g2: CGFloat = 0
    var b2: CGFloat = 0
    var a2: CGFloat = 0
    end.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)

    return UIColor(
      red: interpolate(start: r1, end: r2, progress: progress),
      green: interpolate(start: g1, end: g2, progress: progress),
      blue: interpolate(start: b1, end: b2, progress: progress),
      alpha: interpolate(start: a1, end: a2, progress: progress)
    )
  }
}

You can use it like this:

color1.interpolateTo(color: color2, progress: t)

Where t is the percentage (0-1) of your interpolation.

Caravansary answered 26/7, 2016 at 0:56 Comment(1)
Don’t interpolate in RGB space: tavmjong.free.fr/SVG/COLOR_INTERPOLATIONFlemish

© 2022 - 2024 — McMap. All rights reserved.