Convert a 0-1 value to a hex colour?

2.2k views Asked by At

I'm creating an app that visualises stars using a NASA API. The colour of a star comes back as a 0 to 1 value, with 0 being pure blue, and 1 being pure red. Essentially I need to set up a way to convert 0-1 values in javascript to a sliding HEX (or rgb) scale, progressing like this:

0: blue (9aafff)
.165: blue white (cad8ff)
.33: white (f7f7ff)
.495: yellow white (fcffd4)
.66: yellow (fff3a1)
.825: orange (ffa350)
1: red (fb6252)

Is this possible? I don't have any idea how to even begin to approach this. Cheers!

3

There are 3 answers

6
Denys Séguret On BEST ANSWER

The best would be to work in another color space than the RGB one. For example HSL.

Example:

var stones = [ // Your Data
  {v:0, hex:'#9aafff'},
  {v:.165, hex:'#cad8ff'},
  {v:.33, hex:'#f7f7ff'},
  {v:.495, hex:'#fcffd4'},
  {v:.66, hex:'#fff3a1'},
  {v:.825, hex:'#ffa350'},
  {v:1, hex:'#fb6252'},
]
stones.forEach(function(s){
  s.rgb = hexToRgb(s.hex);
  s.hsl = rgbToHsl.apply(0, s.rgb);
});

function valueToRgbColor(val){
  for (var i=1; i<stones.length; i++) {
    if (val<=stones[i].v) {
      var k = (val-stones[i-1].v)/(stones[i].v-stones[i-1].v),
          hsl = interpolArrays(stones[i-1].hsl, stones[i].hsl, k);
      return 'rgb('+hslToRgb.apply(0,hsl).map(function(v){ return v|0})+')';
    }
  }
  throw "bad value";
}

/**
 * Converts an RGB color value to HSL. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes r, g, and b are contained in the set [0, 255] and
 * returns h, s, and l in the set [0, 1].
 *
 * @param   Number  r       The red color value
 * @param   Number  g       The green color value
 * @param   Number  b       The blue color value
 * @return  Array           The HSL representation
 */
function rgbToHsl(r, g, b){
    r /= 255, g /= 255, b /= 255;
    var max = Math.max(r, g, b), min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2;

    if(max == min){
        h = s = 0; // achromatic
    }else{
        var d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        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, s, l];
}

/**
 * Converts an HSL color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes h, s, and l are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param   Number  h       The hue
 * @param   Number  s       The saturation
 * @param   Number  l       The lightness
 * @return  Array           The RGB representation
 */
function hslToRgb(h, s, l){
    var r, g, b;

    if(s == 0){
        r = g = b = l; // achromatic
    }else{
        function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    return [r * 255, g * 255, b * 255];
}


function hexToRgb(hex) {
    return /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
    .slice(1).map(function(v){ return parseInt(v,16) });
}

function interpolArrays(a,b,k){
  var c = a.slice();
  for (var i=0;i<a.length;i++) c[i]+=(b[i]-a[i])*k;
  return c;
}

var stones = [ // Your Data
  {v:0, hex:'#9aafff'},
  {v:.165, hex:'#cad8ff'},
  {v:.33, hex:'#f7f7ff'},
  {v:.495, hex:'#fcffd4'},
  {v:.66, hex:'#fff3a1'},
  {v:.825, hex:'#ffa350'},
  {v:1, hex:'#fb6252'},
]
stones.forEach(function(s){
  s.rgb = hexToRgb(s.hex);
  s.hsl = rgbToHsl.apply(0, s.rgb);
});

function valueToRgbColor(val){
  for (var i=1; i<stones.length; i++) {
    if (val<=stones[i].v) {
      var k = (val-stones[i-1].v)/(stones[i].v-stones[i-1].v),
          hsl = interpolArrays(stones[i-1].hsl, stones[i].hsl, k);
      return 'rgb('+hslToRgb.apply(0,hsl).map(function(v){ return v|0})+')';
    }
  }
  throw "bad value";
}

for (var i=0; i<=1; i+=.03) {
  var color = valueToRgbColor(i);
  $('<div>').css({background:color}).text(i.toFixed(2)+" -> "+color).appendTo('body');
}
body {
  background: #222;
}
div {
  width:200px;
  margin:auto;
  color: #333;
  padding: 2px;
  text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

For this example, I took the color space conversion functions here but there are easy to find once you know what to look for.

Note that modern browsers understand HSL colors (exemple: background: hsl(120,100%, 50%);) so, if you're just building HTML, you don't have to embed all this code in your page, just precompute the color stops and interpolate on the HSL values directly.

0
Alexis Paques On

Here is one the solution in pure Javascript I just did. It process a linear interpolation between two colors.

/* 
NASA color to RGB function
by Alexis Paques

It process a linear interpolation between two colors, here is the scheme:
0: blue
.165: blue white
.33: white
.495: yellow white
.66: yellow
.825: orange
1: red
*/

var blue = [0,0,255];
var bluewhite = [127,127,255];
var white = [255,255,255];
var yellowwhite = [255,255,127];
var yellow = [255,255,0];
var orange = [255,127,0];
var red = [255,0,0];
function color01toRGB(color01){
  var RGB = [0,0,0];
  var fromRGB = [0,0,0];
  var toRGB = [0,0,0];
  if(!color01)
    return '#000000';
  if(color01 > 1 || color01 < 0)
    return '#000000';
  if(color01 >= 0 && color01 <= 0.165 ){
    fromRGB = blue;
    toRGB = bluewhite;
  }
  else if(color01 > 0.165 && color01 <= 0.33 ){
    fromRGB = bluewhite;
    toRGB = white;
  }
  else if(color01 > 0.33 && color01 <= 0.495 ){
    fromRGB = white;
    toRGB = yellowwhite;
  }
  else if(color01 > 0.495 && color01 <= 0.66 ){
    fromRGB = yellowwhite;
    toRGB = yellow;
  }
  else if(color01 > 0.66 && color01 <= 0.825 ){
    fromRGB = yellow;
    toRGB = orange;
  }
  else if(color01 > 0.825 && color01 <= 1 ){
    fromRGB = orange;
    toRGB = red;
  }

  // 0.165
  for (var i = RGB.length - 1; i >= 0; i--) {
    RGB[i] = Math.round(fromRGB[i]*color01/0.165 + toRGB[i]*(1-color01/0.165)).toString(16);
  };

  return '#' + RGB.join('');
}
3
ssube On

Since you have a list of values, all pretty well-saturated and bright, you can probably interpolate in the current (RGB) space for this. It won't be quite as pretty as if you convert to HSL, but will work fine for the colors you have.

Since you don't have any weighting or curves in the data, going with a simple linear interpolation should work just fine. Something like:

var stops = [
  [0, 154, 175, 255],
  [0.165, 202, 216, 255],
  [0.33, 247, 247, 255],
  [0.495, 252, 255, 212],
  [0.66, 255, 243, 161],
  [0.825, 255, 163, 80],
  [1, 251, 98, 82]
];

function convertColor(color) {
  var c = Math.min(Math.max(color, 0), 1); // Clamp between 0 and 1

  // Find the first stop below c
  var startIndex = 0;
  for (; stops[startIndex][0] < c && startIndex < stops.length; ++startIndex) {
    // nop
  }
  var start = stops[startIndex];
  console.log('using stop', startIndex, 'as start');

  // Find the next stop (above c)
  var stopIndex = startIndex + 1;
  if (stopIndex >= stops.length) {
    stopIndex = stops.length - 1;
  }
  var stop = stops[stopIndex];
  console.log('using stop', stopIndex, 'as stop');

  // Find the distance from start to c and start to stop
  var range = stop[0] - start[0];
  var diff = c - start[0];

  // Convert diff into a ratio from start to stop
  if (range > 0) {
    diff /= range;
  }

  console.log('interpolating', c, 'between', stop[0], 'and', start[0], 'by', diff);

  // Convert from RGB to HSL
  var a = rgbToHsl(start[1], start[2], start[3]);
  var b = rgbToHsl(stop[1], stop[2], stop[3]);
  console.log('hsl stops', a, b);

  // Interpolate between the two colors (start * diff + (stop * (1 - diff)))
  var out = [0, 0, 0];
  out[0] = a[0] * diff + (b[0] * (1 - diff));
  out[1] = a[1] * diff + (b[1] * (1 - diff));
  out[2] = a[2] * diff + (b[2] * (1 - diff));
  console.log('interpolated', out);

  // Convert back from HSL to RGB  
  var r = hslToRgb(out[0], out[1], out[2]);
  r = r.map(function(rv) {
    // Round each component of the output
    return Math.round(rv);
  });

  return r;
}

// Set the divs
var divs = document.querySelectorAll('.star');
Array.prototype.forEach.call(divs, function(star) {
  var color = convertColor(star.dataset.color);
  var colorStr = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
  console.log('setting', star, 'to', colorStr);
  star.style.backgroundColor = colorStr;
});

// HSL to RGB conversion from http://stackoverflow.com/a/30758827/129032
function rgbToHsl(r, g, b) {
  r /= 255, g /= 255, b /= 255;
  var max = Math.max(r, g, b),
    min = Math.min(r, g, b);
  var h, s, l = (max + min) / 2;

  if (max == min) {
    h = s = 0; // achromatic
  } else {
    var d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    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, s, l];
}

function hslToRgb(h, s, l) {
  var r, g, b;

  if (s == 0) {
    r = g = b = l; // achromatic
  } else {
    function hue2rgb(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    }

    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }

  return [r * 255, g * 255, b * 255];
}
.star {
  width: 24px;
  height: 24px;
  display: inline-block;
  box-shadow: 0px 0px 16px -2px rgba(0, 0, 0, 0.66);
}
<div class="star" data-color="0.0"></div>
<div class="star" data-color="0.05"></div>
<div class="star" data-color="0.1"></div>
<div class="star" data-color="0.15"></div>
<div class="star" data-color="0.2"></div>
<div class="star" data-color="0.25"></div>
<div class="star" data-color="0.3"></div>
<div class="star" data-color="0.35"></div>
<div class="star" data-color="0.4"></div>
<div class="star" data-color="0.45"></div>
<div class="star" data-color="0.5"></div>
<div class="star" data-color="0.55"></div>
<div class="star" data-color="0.6"></div>
<div class="star" data-color="0.65"></div>
<div class="star" data-color="0.7"></div>
<div class="star" data-color="0.75"></div>
<div class="star" data-color="0.8"></div>
<div class="star" data-color="0.85"></div>
<div class="star" data-color="0.9"></div>
<div class="star" data-color="0.95"></div>
<div class="star" data-color="1.0"></div>