[Intro]
HSP color model is a made-up color model created in 2006. It uses the same values as HSV for Hue and Saturation but, for calculating the P (perceived brightness), it uses Weighted Euclidean norm of the [R, G, B] vector.
More info: https://alienryderflex.com/hsp.html
As you can see, at the bottom of the website, there are formulas for calculating between RGB and HSP that I've taken and re-formatted for Python.
[Issues]
In some places, I found that for calculating the Perceived brightness, you need to first linearize the RGB channels (assuming it's sRGB) but if you do so, then the formulas no longer work. For that reason, I'm not doing that and applying the formulas directly on the input RGB color. Also, I found in a js library someone made it so the perceived brightness is in range 0-255. I don't know where they got that idea, but it should be in range 0-100 (percentage).
[Where it all goes wrong]
I don't have any issues with calculating from RGB to HSP. The problem is when calculating RGB from HSP. I won't bother you with the full code since you can take it from the link above but I'm giving you a snippet of the part that doesn't work correctly (or I have a mistake that I can't find).
P.S: After further investigation, it turns out that more than just this snippet gives false results!
elif H < 4 / 6: # B > G > R
H = 6 * (-H + 4 / 6)
B = (P ** 2 / (Pb + Pg * H ** 2)) ** 0.5
G = B * H
R = 0
This is the part where Saturation is 100%. The problem is that when you pass it these values HSP(253, 100, 50), or any similar ones, the resulting blue is beyond the acceptable range (in this case 356). I tried clamping the values to 255 but then when doing the RGB to HSV conversion, the values don't match so the problem isn't there.
Any ideas?
So, I found a mistake in my code which brought down the out-of-range values from 300+ to a maximum of 261 which is acceptable to be clamped at 255 (for 8-bit colors) without needing to do anything to the other values. No values need to be clamped on the black side.
Here's my simplified version of the calculation with comments:
def hsp_to_rgb(HSP: tuple | list, depth: int = 8, normalized: bool = False):
"""### Takes an HSP color and returns R, G, B values.
#### N/B: All examples below are given for 8-bit color depth that has range 0-255. \
If you want to use this function with a different depth the actual range is 0-(max value for bit depth).
### Args:
`color` (tuple | list): Either int in range 0-255 or float in range 0-1
`depth` (int): The bit depth of the input RGB values. Defaults to 8-bit (range 0-255)
`normalized` (bool, optional): Returns the values in range 0-1. Defaults to False.
Reference: http://alienryderflex.com/hsp.html
### Returns:
list[int, int, int] | list[float, float, float]: (H, S, P)
"""
H, S, P = HSP[0]/360, HSP[1]/100, HSP[2]/100
max_value = 2 ** depth - 1
def wrap(HSP: tuple | list, c1: float, c2: float, c3: float, S1: bool):
"""### This is an internal helper function for the hsp_to_rgb function to lift off some of the calculations.
c1, c2, c3 - Pr, Pg, Pb in different order
### Args:
`HSP` (tuple | list): Hue, Saturation, Perceived brightness in range 0-1
`c1` (float): Constant. Either 0.299, 0.587 or 0.114
`c2` (float): Constant. Either 0.299, 0.587 or 0.114
`c3` (float): Constant. Either 0.299, 0.587 or 0.114
`S1` (bool): Whether S (Saturation) is 1 (100%). Defaults to False
### Returns:
tuple[float, float, float]: R, G, B values in different order depending on the constants.
"""
if S1:
ch1 = (HSP[2] ** 2 / (c1 + c2 * HSP[0] ** 2)) ** 0.5
ch2 = ch1 * HSP[0]
ch3 = 0
return ch3, ch1, ch2
min_over_max = 1 - HSP[1]
part = 1 + HSP[0] * (1 / min_over_max - 1)
ch1 = HSP[2] / (c1 / min_over_max ** 2 + c2 * part ** 2 + c3) ** 0.5
ch2 = ch1 / min_over_max
ch3 = ch1 + HSP[0] * (ch2 - ch1)
return ch1, ch2, ch3
# Get weights constants
Pr, Pg, Pb = 0.299, 0.587, 0.114
# Calculate R, G, B based on the Hue
if H < 1 / 6: # R > G > B
H = 6 * H
B, R, G = wrap((H, S, P), Pr, Pg, Pb, S >= 1)
elif H < 2 / 6: # G > R > B
H = 6 * (-H + 2 / 6)
B, G, R = wrap((H, S, P), Pg, Pr, Pb, S >= 1)
elif H < 3 / 6: # G > B > R
H = 6 * (H - 2 / 6)
R, G, B = wrap((H, S, P), Pg, Pb, Pr, S >= 1)
elif H < 4 / 6: # B > G > R
H = 6 * (-H + 4 / 6)
R, B, G = wrap((H, S, P), Pb, Pg, Pr, S >= 1)
elif H < 5 / 6: # B > R > G
H = 6 * (H - 4 / 6)
G, B, R = wrap((H, S, P), Pb, Pr, Pg, S >= 1)
else: # R > B > G
H = 6 * (-H + 1)
G, R, B = wrap((H, S, P), Pr, Pb, Pg, S >= 1)
return [min(i, 1.0) for i in (R, G, B)] if normalized else [min(i*max_value, 255) for i in (R, G, B)]
This works pretty well and the conversions are really accurate. Note that in order to get perfect conversions, you'll need to use an exact floating-point number for the calculations. Otherwise, you'll get a number of overlapping values due to limitations of the system. Ex. RGB = 256 * 256 * 256 = 16 777 216 colors, whereas HSP = 360 * 100 * 100 = 3 600 000 unique colors.
I am storing id (which is a value comprised in 24bit-range) into an Float32Array(3) for latter retrieval in WebGL:
var r = 0,
g = 0,
b = id;
if (b >= 65536) {
r = ~~(b / 65536);
b -= r * 65536;
}
if (b >= 256) {
g = ~~(b / 256);
b -= g * 256;
}
var fa = new Float32Array([r/255, g/255, b/255]);
For the sake of completeness, here is how i am using that value:
gl.uniform3fv(uniforms['u_id'], fa);
...and this is how i get my id back from WebGLRenderingContext.readPixels():
var result = new Uint8Array(4);
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
var id = result[0] << 16 | result[1] << 8 | result[2];
Which is the correct way to split that value into my Float32Array? I strongly believe that such task could be accomplished in a more efficient and elegant way (actually, what i am doing is working but is really hurting my eyes).
id has a form like this:
0000 0000 rrrr rrrr gggg gggg bbbb bbbb
A part (r, g or b) can be extracted by putting it in the lowest byte and masking the rest away. Sometimes one of those steps is unnecessary. So:
b = id & 255 // b is already in the low byte, so no shift
g = (id >> 8) & 255
r = id >> 16 // no mask because there is nothing "above" r
This can be put together with the division and putting it in an array:
[(id >> 16) / 255, ((id >> 8) & 255) / 255, (id & 255) / 255]
This question already has answers here:
Programmatically Lighten or Darken a hex color (or rgb, and blend colors)
(21 answers)
Closed 9 years ago.
I want to have the user be able to enter a random hex color and my javascript code would print out a lighter version of that color (some sort of algorithm so to speak)
A quick example of how I want the colors to change.
What the user inputs: #2AC0A3
What it spits out: #C6EEE6
Thanks so much to anyone who can help!
An easy way to lighten up a color is linear interpolation with white. In the same way, a color can be darkened by interpolating with black.
Here's a function that takes a color string and changes the brightness indicated by light:
function hex2(c) {
c = Math.round(c);
if (c < 0) c = 0;
if (c > 255) c = 255;
var s = c.toString(16);
if (s.length < 2) s = "0" + s;
return s;
}
function color(r, g, b) {
return "#" + hex2(r) + hex2(g) + hex2(b);
}
function shade(col, light) {
// TODO: Assert that col is good and that -1 < light < 1
var r = parseInt(col.substr(1, 2), 16);
var g = parseInt(col.substr(3, 2), 16);
var b = parseInt(col.substr(5, 2), 16);
if (light < 0) {
r = (1 + light) * r;
g = (1 + light) * g;
b = (1 + light) * b;
} else {
r = (1 - light) * r + light * 255;
g = (1 - light) * g + light * 255;
b = (1 - light) * b + light * 255;
}
return color(r, g, b);
}
When light is negative, the color is darkened; -1 always yields black. When light is positive, the color is lightened, 1always yields white. Finally, 0 always yields the original color:
alert(shade("#2ac0a3", 0.731));
I can already convert 32bit integers into their rgba values like this:
pixelData[i] = {
red: pixelValue >> 24 & 0xFF,
green: pixelValue >> 16 & 0xFF,
blue: pixelValue >> 8 & 0xFF,
alpha: pixelValue & 0xFF
};
But I don't really know how to reverse it.
To reverse it, you just have to combine the bytes into an integer.
Simply use left-shift and add them, and it will work.
var rgb = (red << 24) + (green << 16) + (blue << 8) + (alpha);
Alternatively, to make it safer, you could first AND each of them with 0xFF:
var r = red & 0xFF;
var g = green & 0xFF;
var b = blue & 0xFF;
var a = alpha & 0xFF;
var rgb = (r << 24) + (g << 16) + (b << 8) + (a);
(You may use bitwise OR | instead of + here, the outcome will be the same).
I have various hexadecimal RRGGBBAA colors as stop values in a heat map gradient but I have noticed that setting different Alpha values for some of the stop doesn't change the opacity in my code, I always get the same view -although setting the last two alpha bits to 00 as 0.0 opacity works for some reason-. The RRGGBBAA values are written like this:
0xaa00007f (the last two bits, 7f should be 0.5 opacity)
0xaa0000ff (ff is the 1.0 opacity)
The setGradientStops function that takes the stop values is like this -this is from a heat map library, not my code-
setGradientStops: function(stops) {
var ctx = document.createElement('canvas').getContext('2d');
var grd = ctx.createLinearGradient(0, 0, 256, 0);
for (var i in stops) {
grd.addColorStop(i, 'rgba(' +
((stops[i] >> 24) & 0xFF) + ',' +
((stops[i] >> 16) & 0xFF) + ',' +
((stops[i] >> 8) & 0x7F) + ',' +
((stops[i] >> 0) & 0x7F) + ')');
}
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 256, 1);
this.gradient = ctx.getImageData(0, 0, 256, 1).data;
}
The problem is that opacity expects a value in the range of 0 - 1 and there you are outputting a value in the range of 0 - 127. I would try...
grd.addColorStop(i, 'rgba(' +
((stops[i] >> 24) & 0xFF) + ',' +
((stops[i] >> 16) & 0xFF) + ',' +
((stops[i] >> 8) & 0xFF) + ',' +
(((stops[i] >> 0) & 0xFF) / 255) + ')');
So it takes the bits from the part that represents the alpha (all of them rather than almost all of them) by using the & bit operator on 0xFF rather than 0x7F. So...
0xFF (11111111) & 0xFF (11111111) = 0xFF (11111111) = 255
Rather than...
0xFF (11111111) & 0x7F (01111111) = 0x7F (01111111) = 127
and then you have the value in the range of 0 - 255, divide by 255 to get this to the required range.
0xFF / 255 = 1, 0x7F / 255 = 0.498, 0x00 / 255 = 0
So then for 0xaa00007f, grd.addColorStop would be given the string 'rgba(170,0,0,0.498)'