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?

Community
  • 1
  • 1
user1240679
  • 6,829
  • 17
  • 60
  • 89
  • 3
    Normally you would do the interpolation in another colour space, e.g. HSV, and convert the interpolated HSV values back to RGB. – Paul R Nov 21 '12 at 08:19
  • 1
    Your color codes seems to be RGB... – Synxis Nov 21 '12 at 08:20
  • 1
    @PaulR: Why is that we won't use RGB in interpolation? – user1240679 Nov 21 '12 at 08:21
  • @Synxis: These are RGB in fact and I was thinking of interpolation in RGB terms. Didn't know about HSV stuff ;/ – user1240679 Nov 21 '12 at 08:23
  • 3
    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. – Paul R Nov 21 '12 at 08:25
  • @user1240679 You can of course interpolate in RGB, you just may get funny colors in between. It may work in your case, actually. – hyde Nov 21 '12 at 08:26
  • @user1240679 Have you solved your problem ? If so, please accept an answer. Else, you should put a comment saying what is still an obstacle. – Synxis Dec 02 '12 at 11:32
  • 1
    @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. – Mark Ransom Apr 24 '15 at 16:01
  • @MarkRansom What do you suggest instead, then? – David Doria Feb 21 '16 at 17:41
  • 2
    @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. – Mark Ransom Feb 21 '16 at 22:45
  • Possibly useful: I use https://www.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. – user3445853 Dec 02 '19 at 16:54

7 Answers7

36

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);
Carlos Barcellos
  • 544
  • 1
  • 4
  • 10
  • 1
    Interpolating in the RGB space is a really bad idea: http://tavmjong.free.fr/SVG/COLOR_INTERPOLATION/ – flying sheep Jun 23 '21 at 07:22
  • not a bad ideia, no, unless you are developing a photophop or something really precise. – Carlos Barcellos Jul 13 '22 at 17:02
  • 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: https://codepen.io/flying-sheep/pen/wPmKmB – flying sheep Jul 25 '22 at 11:01
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.

Community
  • 1
  • 1
hyde
  • 60,639
  • 21
  • 115
  • 176
  • 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). – hyde Nov 21 '12 at 08:54
  • HSV is only an improvement over RGB if the hue values are widely separated. – Mark Ransom Apr 24 '15 at 16:13
  • Yes, but then it’s the difference between creating ugly dark border artifacts and a smooth transition: http://tavmjong.free.fr/SVG/COLOR_INTERPOLATION/ – flying sheep Jun 23 '21 at 07:27
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);
Rafael Vega
  • 4,575
  • 4
  • 32
  • 50
Synxis
  • 9,236
  • 2
  • 42
  • 64
  • 2
    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. – Jonathan Lidbeck Jan 25 '18 at 18:52
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.

Mike M
  • 4,879
  • 5
  • 38
  • 58
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);
Eran W
  • 1,696
  • 15
  • 20
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;
}
Community
  • 1
  • 1
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
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.

Sam Soffes
  • 14,831
  • 9
  • 76
  • 80