Javascript convert HSB/HSV color to RGB accurately
Asked Answered
S

6

54

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

St answered 21/6, 2013 at 18:36 Comment(4)
Check this out axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-cLuggage
Only do your integer conversion right at the end.Evanston
I haven't tested it, but what you said makes a lot of sense, @PaulS.Woodborer
This is a related highly up-voted answer that you might want to look at also. It discusses color transformations and making gradients that appear accurate to eyes.Leanora
E
122

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.

Evanston answered 21/6, 2013 at 19:38 Comment(9)
Thats what i'm looking for! Had to divide h/360 and s,v/100. Works great.St
You sure it should be floored and not rounded?Osgood
@Mark depends where you're happiest to accumulate your errors. If v is 1 you should get 255s when flooring, so it's not like you're unable to get whitesEvanston
@PaulS. Yes, but you get 0s from 0-.9999, whereas you can only get 255s with a perfect 1. Rounding would split that evenly, and provide a closer match for all the values in between, no?Osgood
@Mark now I've written the reverse method too I see you're right - rounding makes more sense so if you go in and back you should get out what you put in (assuming int rgb)Evanston
{v: _v} not {v:_v;} that hurt eyesCentigrade
For my Infinity Ergodox, I need to do the conversion but all values, including HSI, should be in the range 0-255, may I ask you what I should change to make it work this way?Spreader
@OlivierPons to convert a range between 0..1 and 0..n, use simple multiplication or division by n. Because HSI also uses the middle, I don't think it is as simple to convert without going via RGB; here is a nice image describing it their relations mathematicallyEvanston
Totally unrelated to JavaScript, but I ported this to PHP. I, for some reason, was unable to find a version in PHP that worked correctly. Here it is if anyone needs it: gist.github.com/kkirby/10c1bd47ac10f90b521eb38f47b94f12Orgasm
T
33

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

Thither answered 3/1, 2019 at 14:51 Comment(8)
I got negative values!Uraeus
@Uraeus for exact what h,s,v values you get negative values?Nanettenani
I now realized that I should scale it between 0 and 1. I was using 80 for 80%Uraeus
This is probably the simplest and easiest to understand solution. That image you provided is really helpful!Oath
@Oath thanks - image is not my - It cames from wikiNanettenani
yeah, that's what i meant, sorryOath
seems wrong, try rgb: [255, 51, 17]Calk
@mantou this function, as OP wants, convert hsv to rgb (NOT rgb to hsv - but you can do it here)Nanettenani
G
13

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
Gumboil answered 12/2, 2016 at 12:46 Comment(0)
D
7

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.

Dimarco answered 18/7, 2015 at 11:34 Comment(0)
I
3

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.

Incrassate answered 15/9, 2016 at 12:46 Comment(0)
O
-1

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); 
}
Overweary answered 18/6, 2014 at 3:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.