Question

I need to accurately convert HSB to RGB but I am not sure how to get around the problem of turning decimals into whole numbers without rounding. This is the current function I have out of a colorpicker library:

HSBToRGB = function (hsb) {

    var rgb = { };
    var h = Math.round(hsb.h);
    var s = Math.round(hsb.s * 255 / 100);
    var v = Math.round(hsb.b * 255 / 100);

        if (s == 0) {

        rgb.r = rgb.g = rgb.b = v;
        } else {
        var t1 = v;
        var t2 = (255 - s) * v / 255;
        var t3 = (t1 - t2) * (h % 60) / 60;

            if (h == 360) h = 0;

                if (h < 60) { rgb.r = t1; rgb.b = t2; rgb.g = t2 + t3 }
                else if (h < 120) { rgb.g = t1; rgb.b = t2; rgb.r = t1 - t3 }
                else if (h < 180) { rgb.g = t1; rgb.r = t2; rgb.b = t2 + t3 }
                else if (h < 240) { rgb.b = t1; rgb.r = t2; rgb.g = t1 - t3 }
                else if (h < 300) { rgb.b = t1; rgb.g = t2; rgb.r = t2 + t3 }
                else if (h < 360) { rgb.r = t1; rgb.g = t2; rgb.b = t1 - t3 }
                else { rgb.r = 0; rgb.g = 0; rgb.b = 0 }
        }

    return { r: Math.round(rgb.r), g: Math.round(rgb.g), b: Math.round(rgb.b) };

As you can see the inaccuracy in this function comes from the Math.round

Was it helpful?

Solution

From Parthik Gosar's link in this comment with slight modification to let you enter each value independently or all at once as an object

/* accepts parameters
 * h  Object = {h:x, s:y, v:z}
 * OR 
 * h, s, v
*/
function HSVtoRGB(h, s, v) {
    var r, g, b, i, f, p, q, t;
    if (arguments.length === 1) {
        s = h.s, v = h.v, h = h.h;
    }
    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: Math.round(r * 255),
        g: Math.round(g * 255),
        b: Math.round(b * 255)
    };
}

This code expects 0 <= h, s, v <= 1, if you're using degrees or radians, remember to divide them out.

The returned 0 <= r, g, b <= 255 are rounded to the nearest Integer. If you don't want this behaviour remove the Math.rounds from the returned object.


And the reverse (with less division)

/* accepts parameters
 * r  Object = {r:x, g:y, b:z}
 * OR 
 * r, g, b
*/
function RGBtoHSV(r, g, b) {
    if (arguments.length === 1) {
        g = r.g, b = r.b, r = r.r;
    }
    var max = Math.max(r, g, b), min = Math.min(r, g, b),
        d = max - min,
        h,
        s = (max === 0 ? 0 : d / max),
        v = max / 255;

    switch (max) {
        case min: h = 0; break;
        case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break;
        case g: h = (b - r) + d * 2; h /= 6 * d; break;
        case b: h = (r - g) + d * 4; h /= 6 * d; break;
    }

    return {
        h: h,
        s: s,
        v: v
    };
}

This code will output 0 <= h, s, v <= 1, but this time takes any 0 <= r, g, b <= 255 (does not need to be an integer)


For completeness,

function HSVtoHSL(h, s, v) {
    if (arguments.length === 1) {
        s = h.s, v = h.v, h = h.h;
    }
    var _h = h,
        _s = s * v,
        _l = (2 - s) * v;
    _s /= (_l <= 1) ? _l : 2 - _l;
    _l /= 2;

    return {
        h: _h,
        s: _s,
        l: _l
    };
}

function HSLtoHSV(h, s, l) {
    if (arguments.length === 1) {
        s = h.s, l = h.l, h = h.h;
    }
    var _h = h,
        _s,
        _v;

    l *= 2;
    s *= (l <= 1) ? l : 2 - l;
    _v = (l + s) / 2;
    _s = (2 * s) / (l + s);

    return {
        h: _h,
        s: _s,
        v: _v
    };
}

All of these values should be in the range 0 to 1. For HSL<->RGB go via HSV.

OTHER TIPS

Short but precise

Try this (more: rgb2hsv, hsl2rgb, rgb2hsl and sl22sv)

// input: h in [0,360] and s,v in [0,1] - output: r,g,b in [0,1]
function hsv2rgb(h,s,v) 
{                              
  let f= (n,k=(n+h/60)%6) => v - v*s*Math.max( Math.min(k,4-k,1), 0);     
  return [f(5),f(3),f(1)];       
}   

// oneliner version
let hsv2rgb=(h,s,v,f=(n,k=(n+h/60)%6)=>v-v*s*Math.max(Math.min(k,4-k,1),0))=>[f(5),f(3),f(1)];

console.log(`hsv: (340,0.3,0.9) -> rgb: (${hsv2rgb(340,0.3,0.9)})`)


// ---------------
// UX
// ---------------

rgb= [0,0,0];
hs= [0,0,0];

function rgb2hsv(r,g,b) {
  let v=Math.max(r,g,b), n=v-Math.min(r,g,b);
  let h= n && ((v==r) ? (g-b)/n : ((v==g) ? 2+(b-r)/n : 4+(r-g)/n)); 
  return [60*(h<0?h+6:h), v&&n/v, v];
}

function changeRGB(i,e) {
  rgb[i]=e.target.value/255;
  hs = rgb2hsv(...rgb);
  refresh();
}

function changeHS(i,e) {
  hs[i]=e.target.value/(i?255:1);
  rgb= hsv2rgb(...hs);
  refresh();
}

function refresh() {
  rr = rgb.map(x=>x*255|0).join(', ')
  tr = `RGB: ${rr}`
  th = `HSV: ${hs.map((x,i)=>i? (x*100).toFixed(2)+'%':x|0).join(', ')}`
  box.style.backgroundColor=`rgb(${rr})`;  
  infoRGB.innerHTML=`${tr}`;  
  infoHS.innerHTML =`${th}`;  
  
  r.value=rgb[0]*255;
  g.value=rgb[1]*255;
  b.value=rgb[2]*255;
  
  h.value=hs[0];
  s.value=hs[1]*255;
  v.value=hs[2]*255;  
}

refresh();
body { display: flex; }
.box { width: 50px; height: 50px; margin: 20px; }
<div>
  <input id="r" type="range" min="0" max="255" oninput="changeRGB(0,event)">R<br>
  <input id="g" type="range" min="0" max="255" oninput="changeRGB(1,event)">G<br>
  <input id="b" type="range" min="0" max="255" oninput="changeRGB(2,event)">B<br>
  <pre id="infoRGB"></pre>
</div> 

<div id="box" class="box hsl"></div>

<div>
  <input id="h" type="range" min="0" max="360" oninput="changeHS(0,event)">H<br>
  <input id="s" type="range" min="0" max="255" oninput="changeHS(1,event)">S<br>
  <input id="v" type="range" min="0" max="255" oninput="changeHS(2,event)">V<br>
  <pre id="infoHS"></pre><br>
</div>

Here are formulas which I discover and precisely describe in wiki + error analysis

enter image description here

Given the rising popularity of npm I think it is worth to mention a package containing all this functions through a simple API:

npm install colorsys

var colorsys = require('colorsys')
colorsys.rgb_to_hsv({ r: 255, g: 255, b: 255 })
// { h: 0 , s: 0 , v: 100 }

For the browser: <script src="http://netbeast.github.io/colorsys/browser.js"></script>

colorsys.rgb_to_hex(h, s, v)
// #hexcolor

I once wrote this function:

function mix(a, b, v)
{
    return (1-v)*a + v*b;
}

function HSVtoRGB(H, S, V)
{
    var V2 = V * (1 - S);
    var r  = ((H>=0 && H<=60) || (H>=300 && H<=360)) ? V : ((H>=120 && H<=240) ? V2 : ((H>=60 && H<=120) ? mix(V,V2,(H-60)/60) : ((H>=240 && H<=300) ? mix(V2,V,(H-240)/60) : 0)));
    var g  = (H>=60 && H<=180) ? V : ((H>=240 && H<=360) ? V2 : ((H>=0 && H<=60) ? mix(V2,V,H/60) : ((H>=180 && H<=240) ? mix(V,V2,(H-180)/60) : 0)));
    var b  = (H>=0 && H<=120) ? V2 : ((H>=180 && H<=300) ? V : ((H>=120 && H<=180) ? mix(V2,V,(H-120)/60) : ((H>=300 && H<=360) ? mix(V,V2,(H-300)/60) : 0)));

    return {
        r : Math.round(r * 255),
        g : Math.round(g * 255),
        b : Math.round(b * 255)
    };
}

It expects 0<=H<=360, 0<=S<=1 and 0<=V<=1 and returns an object that contains R, G and B (integer values between 0 and 255). I used this image to create the code.

There's a bug in Paul S's HSVtoHSL function: when the v input is 0 we get a divide-by-zero problem and the s output becomes NaN. Here's a fix:

function HSVtoHSL(h, s, v) {
    if (arguments.length === 1) {
        s = h.s, v = h.v, h = h.h;
    }
    var _h = h,
        _s = s * v,
        _l = (2 - s) * v;
    _s /= (_l <= 1) ? (_l === 0 ? 1 : _l) : 2 - _l;
    _l /= 2;

    return {
        h: _h,
        s: _s,
        l: _l;
    };
}

P.S. I would have added this as a comment on Paul S.'s post, but I'm new and the system told me I don't have enough rep.

Here is the algorithm in unityscript, you'll have to rewrite the float, mathf.floor functions and the output is a vector3 of 3 floats.

EDIT This was the first answer on this page and it seemed worthwile to provide this much help when there weren't any other answers, with the small reservation that you have to convert it to a nearly identical version in JS.

function HSVToRGB(h : float, s : float, v : float) {
    h=h%1;
    s=s%1;
    v=v%1;

    var r : float;
    var g : float;
    var b : float;
    var i : float;
    var f : float;
    var p : float;
    var q : float;
    var t : float; 

    i = Mathf.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 Color(r,g,b); 
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top