3

I've searched Google with no avail.

If I have the following color rgb(50,100,200). I can calculate the HSV saturation with: (200-50)/200, which is 0.75. If the want the color shade with a different saturation, how can I calculate that?

I'm using a color picker to get these test data:

saturation 1.00 = r:0   g:66    b:200

saturation 0.75 = r:50  g:100   b:200

saturation 0.50 = r:100 g:134   b:200

saturation 0.25 = r:150 g:167   b:200

I've noticed with saturation changing, the max value (blue channel) doesn't change.
There is no found ratio between the R-G-B numbers.

Can I calculate a new shade of color using RGB and not convert it first to HSV color?

VC.One
  • 14,790
  • 4
  • 25
  • 57
Daniel Cheung
  • 4,779
  • 1
  • 30
  • 63
  • [**Here**](http://stackoverflow.com/a/17243070/1615483) is a link to an answer I did on converting between the colour spaces, which may be helpful – Paul S. Jul 25 '15 at 14:27
  • @PaulS. Does saturation have a direct link to the hue? Or are you just simplifying the code, so `s` doesn't only = saturation? – Daniel Cheung Jul 25 '15 at 14:31
  • It's not so much that saturation has a direct link to hue, but rather that because you are changing coördinate systems, R, G and B are dependant on all 3 values. – Paul S. Jul 25 '15 at 14:33
  • In your specific example, you can easily work out _R_ (min) and _B_ (max) if you change the saturation `B = 200; R = B - B * Saturation`, but how do you know _G_ without going through the hue and value? All you know is `R < G < B` – Paul S. Jul 25 '15 at 14:41
  • @PaulS. I **do** know now ;) See my answer. – Daniel Cheung Jul 25 '15 at 16:49

2 Answers2

3

After Excel plotting, and hours of complex formula constructing, the solution wasn't really that complex!

Excel plotting

Solution

It contains a bit of JQuery to find the index of the max and min rgb values.

function saturation(i,v) { //i as in input, v as saturation value
    var min = $.inArray(Math.min.apply(this, i), i), //returns the index of min, max and mid.
        max = $.inArray(Math.max.apply(this, i), i),
        mid = parseInt([0, 1, 2].filter(function (j) {return Array(min, max).indexOf(j) < 0;})),
        r = (i[max] - i[mid]) / (i[mid] - i[min]), //ratio, because it is always constant, 
                                                   //we use this to calc mid value
        o = []; //o as in output
    if (min !== max) {
        o[max] = Math.round(i[max]);
        o[min] = Math.round(i[max] * (1 - v));
        o[mid] = Math.round(o[max] / (r + 1) * v + i[max] * (1 - v));
    }
    return o;
}

Usage

saturation([52,132,220], 0.5) //Array [ 110, 162, 220 ]
saturation([52,132,220], 0) //Array [ 220, 220, 220 ]
saturation([52,132,220], 1) //Array [ 0, 105, 220 ]

Wow! I'm proud of myself! It's really late here.

I recommend you plot a graph if you are ever having trouble dealing with formulae regarding color.

Daniel Cheung
  • 4,779
  • 1
  • 30
  • 63
2

Using knowledge of OP's answer that the middle value has

new_mid = max / (r + 1) * sat + max * (1 - sat)
r = (max - mid) / (mid - min)

Can simplify... 
new_mid = max / (((max - mid) / (mid - min)) + 1) * sat + max * (1 - sat)
        = max * (sat / (((max - mid) / (mid - min)) + 1) + (1 - sat))
        = max * ((1 - sat) + sat / (((max - mid) / (mid - min)) + ((mid - min)  / (mid - min))))
        = max * ((1 - sat) + sat / (((max - mid) + (mid - min)) / (mid - min)))
        = max * ((1 - sat) + sat * (mid - min) / ((max - mid) + (mid - min)))
        = max * ((1 - sat) + sat * (mid - min) / (max - min))
let
a = max - min
b = mid - min
=>
new_mid = max * ((1 - sat) + sat * b / a)

I've re-written the method to not use jQuery, do division less and accept more inputs

// Inputs
// Array rgb colour data, s new hsl saturation
// Outputs
// Array rgb colour data
function saturation(rgb, s) {
    var min = rgb.indexOf(Math.min.apply(null, rgb)), // index of min
        max = rgb.indexOf(Math.max.apply(null, rgb)), // index of max
        mid = [0, 1, 2].filter(function (i) {return i !== min && i !== max;})[0],
        a = rgb[max] - rgb[min],
        b = rgb[mid] - rgb[min],
        x = rgb[max],
        arr = [x, x, x];
    if (min === max) {
        min = 2; // both max = min = 0, => mid = 1, so set min = 2
        a = 1;   // also means a = b = 0, don't want division by 0 in `b / a`
    }

    arr[max] = x;
    arr[min] = Math.round(x * (1 - s));
    arr[mid] = Math.round(x * ((1 - s) + s * b / a));

    return arr;
}

Now same as before,

saturation([52, 132, 220], 0.5); // [ 110, 162, 220 ]
saturation([52, 132, 220], 0);   // [ 220, 220, 220 ]
saturation([52, 132, 220], 1);   // [ 0, 105, 220 ]

But also works for values which have equal sections

saturation([80, 80, 80], 0.5); // [80, 40, 40] vs []

Changing value is a lot easier

// Inputs
// Array rgb colour data, v new hsl value
// Outputs
// Array rgb colour data
function nvalue(rgb, v) {
    var x = Math.max.apply(null, rgb);
    if (x === 0)
        return [
            Math.round(255 * v),
            Math.round(255 * v),
            Math.round(255 * v)
        ];
    x = 255 * v / x;
    return [
        Math.round(rgb[0] * x),
        Math.round(rgb[1] * x),
        Math.round(rgb[2] * x)
    ];
}
Community
  • 1
  • 1
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • Thank you for your participation! I really appreciate it. It was late and I haven't have time to simplify it, by I really wanted to share my answer :) you really put your time into it despite someone had already got a working answer. – Daniel Cheung Jul 26 '15 at 02:16
  • Even I am that lazy, I rarely answer question that already has a working answer – Daniel Cheung Jul 26 '15 at 02:17
  • Also, `arr[mid]` can be further simplified by `Math.round(x * (1-s(1+b/a))` and you can plug `b` directly in it. – Daniel Cheung Jul 26 '15 at 06:36