JavaScript conversion to Float32Array and back for WebGL readPixels - javascript

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]

Related

Issues calculating HSP color model

[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.

how to convert Ikea light bulb color XY ( CIE 1931 colorspace ) to RGB

Problem
i got a similar problem like this one:
How to convert CIE color space into RGB or HEX color code in PHP
how to convert xy color to sRGB?
I can't get the formular working xyY. What should i enter for Y?
Setup of the environment
i got an ikea light bulb which gives me a XY color value in the (CIE 1931 colorspace)
I would like to convert it into RGB,(sRGB) or HEX.
The Phosconn app is sending the following xy values when setting the colors by full brighness and saturation.
RED [0.735, 0.265]
GREEN [0.115, 0.826]
BLUE [0.157, 0.018]
i figured out that the lamp shows deeper colors when i send following values:
RED [1.0, 0.0]
GREEN [0.0, 1.0]
BLUE [0.0, 0.0]
To be more precise here is an illustration what i try to achieve:
Retrieve imformation from the bulb (xy color) via zigbee, convert it with javascript to RGB or HEX for the dashboard's color picker
The other way around does already work. Retrieving information from dashboard's color picker (RGB,brightness,saturation) convert it with JS into XY color, brightness and saturation and send it with zigbee to the bulb.
Current Implementation
It's based on the suggested cie-rgb-color-converter NPM module.
function xyBriToRgb(x, y, bri){
// bri = bri/254*100
node.warn("XYBRI: "+x+ " | "+y+" | "+bri)
function getReversedGammaCorrectedValue(value) {
return value <= 0.0031308 ? 12.92 * value : (1.0 + 0.055) * Math.pow(value, (1.0 / 2.4)) - 0.055;
}
let xy = {
x: x,
y: y
};
let z = 1.0 - xy.x - xy.y;
let Y = bri / 255;
let X = (Y / xy.y) * xy.x;
let Z = (Y / xy.y) * z;
let r = X * 1.656492 - Y * 0.354851 - Z * 0.255038;
let g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152;
let b = X * 0.051713 - Y * 0.121364 + Z * 1.011530;
r = getReversedGammaCorrectedValue(r);
g = getReversedGammaCorrectedValue(g);
b = getReversedGammaCorrectedValue(b);
// Bring all negative components to zero
r = Math.max(r, 0);
g = Math.max(g, 0);
b = Math.max(b, 0);
// If one component is greater than 1, weight components by that value
let max = Math.max(r, g, b);
if (max > 1) {
r = r / max;
g = g / max;
b = b / max;
}
return {
r: Math.floor(r * 255),
g: Math.floor(g * 255),
b: Math.floor(b * 255),
};
}
msg.payload = xyBriToRgb(msg.payload.xy[0], msg.payload.xy[1], msg.payload.bri);
node.warn("RGB: "+ JSON.stringify(msg.payload))
return msg;
Results
let rgb = ColorConverter.xyBriToRgb(0.157 ,0.018, 6);
// return {r: 64, g: 0, b: 255}
Research Material
With the help of the fantastic guys here i found some explanations in this Phillips HUE docs
which was leading me to a Review of RGB color spaces
Meanwhile i discovered some bugs inside the phosconn api or its the firmware of the bulb, that the saturation can not be set via api.
I found zigbee2mqtt page which could fix all my problems with a page fitting 100% to the model of the ikea bulb Zigbee2MQTT IKEA LED1624G9
Im trying to setup zigbee2mqtt for this, because i got some problems with phosconn and the api not setting correctly brightness and stuff.
Also the brightness is just the luminosity of the bulb and has here nothing to do with the color so i assume it's phosconn specific or bulb specific?
Short Answer
Okay, so what I am seeing there are the xy coordinates of the 1931 chromaticity diagram? From that we can generate a matrix, and thru the matrixes we can generate RGB or XYZ (and xyY in a simple further transform.)
Longer Answer
Those coordinates are "xy" not to be confused with XYZ (because what would color science be if it wasn't completely confusing?)
XYZ is a color space, and xyY is a derivative, with the xy being coordinates on the 1931 chromaticity diagram:
The Y, luminance (spectrally weighted light) is missing, but let's solve for all three primaries at 255, and let's assume that's white.
One thing I don't have is the "white point"... when you set the RGB values to 255,255,255 what is the color temperature of the white light? In order to create the matrix I made the assumption of D50, reasonable for lighting like this... but it could be lower or even higher.
The math for the matrixes can be seen at Bruce Lindbloom's site: http://brucelindbloom.com
Assuming that 255,255,255 creates a white light close to D50 then the matrix to go from XYZ to RGB is:
MATRIX
XYZ TO RGB
X
1.4628522474616900
-0.1840680708796900
-0.2743691849401830
R
Y
-0.5217863795540330
1.4472188263102400
0.0677218457168470
  =  
G
Z
0.0349375228787856
-0.0969021860977637
1.2885320438253000
B
But wait...
Where are you getting these xy values? It seems your controller is sending 255 to each of the lights... so, I'm a little confused as to the application?
Assuming white at a given color temperature when all three are at 255, then the relative Y is 1.0 (0.0 to 1.0 scale, though you can also use a 0 to 100 scale instead).
The next question is ... linearity. On a computer monitor, typically sRGB has an output gamma, a power curve of ^2.2 ... an LED bulb probably does not. Are you trying to match a computer monitor?
Assuming not, and assuming that your controller is also a gamma of 1, then 128,128,128 would be a Y of 0.5
If you want to get the whole XYZ from your RGB, that matrix is here:
For reference the RGB to XYZ and the reference whitepoint
MATRIX
RGB TO XYZ
R
0.7160822783599350
0.1009309426243870
0.1471718784550240
X
G
0.2581793248508610
0.7249474661542950
0.0168732089948435
  =  
Y
B
0.0000000000000000
0.0517819618681639
0.7733554122636590
Z
CALCULATED
REF WHITE POINT
WP
0.964185099439346
1.00
0.825137374131823
Matrix Sans Neo
Once the matrix is generated, it's fairly simple to process.
You multiply each number of each row with each row of the input channels, and then sum and that gives you the corresponding output.
so for the output to RGB, you go
X * 1.46285 + Y * -0.184068 + Z * -0.274369 = R
X * 0.258179 + Y * 0.724947 + Z * 0.01687 = G
X * 0.0 + Y * 0.05178 + Z * 0.773355 = B
If I understood better what you were attempting to acheive, I might be able to be more helpful.
Recalcualted Matrixes
with the white point you gave:
FINAL RGB TO XYZ MATRIX
R 0.6491852651246980 0.1034883891428110 0.1973263457324920 X
G 0.2340599935483600 0.7433166037561910 0.0226234026954449 Y
B 0.0000000000000000 0.0530940431254422 1.0369059568745600 Z
CALCULATED REF WHITE POINT
WP 0.950000000000001 1.00 1.090000000000000
MATRIX INVERTED — XYZ TO RGB
X 1.6135957276619700 -0.2030358522440090 -0.3026422835182280 R
Y -0.5088917855729640 1.4114545750797300 0.0660482763436608 G
Z 0.0260574473801223 -0.0722725427335469 0.9610256584609440 B
You can use cie-rgb-color-converter NPM module.
let xy = ColorConverter.rgbToXy(255, 0, 0);
// It returns {x: 0.7350000508904126, y: 0.26499994910958735}
The number of xy is same as question example.
But if you want to convert these numbers back to RGB. You need to Brightness parameter.
let rgb = ColorConverter.xyBriToRgb(0.7350000508904126 ,0.26499994910958735 , Brightness);
If you set Brightness to 0, it is darkest light (no light, no color), and all number of RGB comes back as zero, because in zero light human eyes can not see anything.
Following example is nearest numbers:
let rgb = ColorConverter.xyBriToRgb(0.7350000508904126 ,0.26499994910958735, 70);
// return {r: 255, g: 0, b: 16}
let rgb = ColorConverter.xyBriToRgb(0.11500021676131911 ,0.8259995753701338, 200);
// return {r: 0, g: 255, b: 0}
let rgb = ColorConverter.xyBriToRgb(0.15700016726803506 ,0.01799963360335173, 6);
// return {r: 64, g: 0, b: 255}
Note: cie-rgb-color-converter has a simple problem, after install go to ColorConverter.js and change these lines:
let red = parseInt(r * 255) > 255 ? 255: parseInt(r * 255);
let green = parseInt(g * 255) > 255 ? 255: parseInt(g * 255);
let blue = parseInt(b * 255) > 255 ? 255: parseInt(b * 255);
red = Math.abs(red);
green = Math.abs(green);
blue = Math.abs(blue);
return {r: red, g: green, b: blue};
to
// Bring all negative components to zero
r = Math.max(r, 0);
g = Math.max(g, 0);
b = Math.max(b, 0);
// If one component is greater than 1, weight components by that value
let max = Math.max(r, g, b);
if (max > 1) {
r = r / max;
g = g / max;
b = b / max;
}
return {
r: Math.floor(r * 255),
g: Math.floor(g * 255),
b: Math.floor(b * 255),
};
#bokup PR it on GitHub.

Converting rgba values into one integer in Javascript

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).

Calculate color HEX having 2 colors and percent/position

Is it possible to calculate a color in a middle of a gradient?
var color1 = 'FF0000';
var color2 = '00FF00';
// 50% between the two colors, should return '808000'
var middle = gradient(color1, color2, 0.5);
I only have two hex strings, and I want one in return.
This should work:
It basically involves converting them to decimal, finding the halves, converting the results back to hex and then concatenating them.
var color1 = 'FF0000';
var color2 = '00FF00';
var ratio = 0.5;
var hex = function(x) {
x = x.toString(16);
return (x.length == 1) ? '0' + x : x;
};
var r = Math.ceil(parseInt(color1.substring(0,2), 16) * ratio + parseInt(color2.substring(0,2), 16) * (1-ratio));
var g = Math.ceil(parseInt(color1.substring(2,4), 16) * ratio + parseInt(color2.substring(2,4), 16) * (1-ratio));
var b = Math.ceil(parseInt(color1.substring(4,6), 16) * ratio + parseInt(color2.substring(4,6), 16) * (1-ratio));
var middle = hex(r) + hex(g) + hex(b);
An ES6 version with comprehensions:
function interpolateColor(c0, c1, f){
c0 = c0.match(/.{1,2}/g).map((oct)=>parseInt(oct, 16) * (1-f))
c1 = c1.match(/.{1,2}/g).map((oct)=>parseInt(oct, 16) * f)
let ci = [0,1,2].map(i => Math.min(Math.round(c0[i]+c1[i]), 255))
return ci.reduce((a,v) => ((a << 8) + v), 0).toString(16).padStart(6, "0")
}
As in the accepted answer, c0,c1 are color codes (without the leading #) and f is "progress" between the two values. (At f=0 this ends up returning c0, at f=1 this returns c1).
The first two lines convert the color codes into arrays of scaled integers
The third line:
"zips" the two integer arrays
sums the corresponding values
rounds the sum and clamps it to 0-255
The fourth line:
converts the integer array into a single integer (reduce and bitshifting)
converts the integer into its hexadecimal string form
ensures the resulting string is 6 characters long and returns it
I can't comment on the answer above, so I write it here:
I found out that in the Javascript substring method the to parameter index is not included in the returned string. That means:
var string = "test";
//index: 0123
alert(string.substring(1,3));
//will alert es and NOT est
Edit: So it should be:
parseInt(color1.substring(0,2), 16);
parseInt(color1.substring(2,4), 16);
and
parseInt(color1.substring(4,6), 16);
You can use this ready function (ES6):
const calculateMiddleColor = ({
color1 = 'FF0000',
color2 = '00FF00',
ratio,
}) => {
const hex = (color) => {
const colorString = color.toString(16);
return colorString.length === 1 ? `0${colorString}` : colorString;
};
const r = Math.ceil(
parseInt(color2.substring(0, 2), 16) * ratio
+ parseInt(color1.substring(0, 2), 16) * (1 - ratio),
);
const g = Math.ceil(
parseInt(color2.substring(2, 4), 16) * ratio
+ parseInt(color1.substring(2, 4), 16) * (1 - ratio),
);
const b = Math.ceil(
parseInt(color2.substring(4, 6), 16) * ratio
+ parseInt(color1.substring(4, 6), 16) * (1 - ratio),
);
return hex(r) + hex(g) + hex(b);
};
//////////////////////////////////////////////////////////////////////
console.log(calculateMiddleColor({ ratio: 0 / 5 })); // ff0000
console.log(calculateMiddleColor({ ratio: 5 / 5 })); // 00ff00
console.log(calculateMiddleColor({ ratio: 2.5 / 5 })); // 808000
console.log(calculateMiddleColor({ ratio: 4.2 / 5 })); // 29d700

Changing opacity in hexadecimal doesn't work -heat map stop gradient-

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)'

Categories

Resources