A transformation seems to be applied when painting colors in p5.js with an alpha value lower than 255:
for (const color of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
clear();
background(color);
loadPixels();
print(pixels.slice(0, 4).join(','));
}
Input/Expected Output Actual Output (Firefox)
1,2,3,255 1,2,3,255 ✅
1,2,3,4 0,0,0,4
10,11,12,13 0,0,0,13
10,20,30,40 6,19,25,40
50,100,200,40 51,102,204,40
50,100,200,0 0,0,0,0
50,100,200,1 0,0,255,1
The alpha value is preserved, but the RGB information is lost, especially on low alpha values.
This makes visualizations impossible where, for example, 2D shapes are first drawn and then the visibility in certain areas is animated by changing the alpha values.
Can these transformations be turned off or are they predictable in any way?
Update: The behavior is not specific to p5.js:
const ctx = new OffscreenCanvas(1, 1).getContext('2d');
for (const [r,g,b,a] of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
ctx.clearRect(0, 0, 1, 1);
ctx.fillStyle = `rgba(${r},${g},${b},${a/255})`;
ctx.fillRect(0, 0, 1, 1);
console.log(ctx.getImageData(0, 0, 1, 1).data.join(','));
}
I could be way off here...but it looks like internally that in the background method if _isErasing is true then blendMode is called. By default this will apply a linear interpolation of colours.
See https://github.com/processing/p5.js/blob/9cd186349cdb55c5faf28befff9c0d4a390e02ed/src/core/p5.Renderer2D.js#L45
See https://p5js.org/reference/#/p5/blendMode
BLEND - linear interpolation of colours: C = A*factor + B. This is the
default blending mode.
So, if you set the blend mode to REPLACE I think it should work.
REPLACE - the pixels entirely replace the others and don't utilize
alpha (transparency) values.
i.e.
blendMode(REPLACE);
for (const color of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
clear();
background(color);
loadPixels();
print(pixels.slice(0, 4).join(','));
}
Internally, the HTML Canvas stores colors in a different way that cannot preserve RGB values when fully transparent. When writing and reading pixel data, conversions take place that are lossy due to the representation by 8-bit numbers.
Take for example this row from the test above:
Input/Expected Output Actual Output
10,20,30,40 6,19,25,40
IN (conventional alpha)
R
G
B
A
values
10
20
30
40 (= 15.6%)
Interpretation: When painting, add 15.6% of (10,20,30) to the 15.6% darkened (r,g,b) background.
Canvas-internal (premultiplied alpha)
R
G
B
A
R
G
B
A
calculation
10 * 0.156
20 * 0.156
30 * 0.156
40 (= 15.6%)
values
1.56
3.12
4.7
40
values (8-bit)
1
3
4
40
Interpretation: When painting, add (1,3,4) to the 15.6% darkened (r,g,b) background.
Premultiplied alpha allows faster painting and supports additive colors, that is, adding color values without darkening the background.
OUT (conventional alpha)
R
G
B
A
calculation
1 / 0.156
3 / 0.156
4 / 0.156
40
values
6.41
19.23
25.64
40
values (8-bit)
6
19
25
40
So the results are predictable, but due to the different internal representation, the transformation cannot be turned off.
The HTML specification explicitly mentions this in section 4.12.5.1.15 Pixel manipulation:
Due to the lossy nature of converting between color spaces and converting to and from premultiplied alpha color values, pixels that have just been set using putImageData(), and are not completely opaque, might be returned to an equivalent getImageData() as different values.
see also 4.12.5.7 Premultiplied alpha and the 2D rendering context
The Wikipedia page about CieL*u*v* color space describes the conversion function from a color expressed in the XYZ color space.
For black, expressed as (R=0, G=0, B=0) in the RGB color space, and (X=0, Y=0, Z=0) in the XYZ color space, which values should we use to represent it in CieLuv?
The formula uses a division, 4x/(x+15y+3z), whose result is undefined for black.
Should we use (L=0, u=0, v=0), or are there more subtleties?
In fact black is not represented in uv.
So in Luv black can be represented by L = 0 and u = v = undefined.
Well there's a difference between theoretical black" (all zeros) and "practical black" which is 1/1000 the lightness of white.
See "Charles Poynton. Digital Video and HD. Morgan Kaufmann, second
edition edition, 2012.", page 248.
So in practice it's not exactly zero.
Or maybe imagine just grays: They all have u* = v* = 0, only L* is different.
So when you approach black, it should be clear what it looks like.
I am trying to implement a colorbar in Matlab that looks linear when printed in grey. The most straight forward approach I though would be to implement the gnuplot pm3d 30,31,32 RGB colour space. On this website I found a good introduction, which works OK for some of the easier gnuplot schemes. However, when trying to implement the 30,31,32 scheme I run into trouble.
The gnuplot instructions are
30: x/0.32-0.78125 31: 2*x-0.84 32: 4x;1;-2x+1.84;x/0.08-11.5
And I interpreted this such that for the blue channel I have to apply four different equations. One for each quarter of intensity values. This is what I have tried so far
x = linspace(0,1,128);
r = x/0.32-0.78125;
g = 2*x-0.84;
b(1:length(x)/4) = 4*x(1:length(x)/4);
b(length(x)/4:length(x)/2) = 1;
b(length(x)/2:length(x)*.75) = -2*x(length(x)/2:length(x)*.75)+1.84;
b(length(x)*.75:end) = x(length(x)*.75:end)/0.08-11.5;
pm3d303132=[r;g;b]';
but unfortunately it doesn't work. I end up with negative values and values that exceed 1, which Matlab cannot interpret.
I did read in the show palette rgbformulae help that
* negative numbers mean inverted=negative colour component
But I don't know how to implement this in Matlab; and I also don't understand how to deal with values exceeding 1. Can anyone help?
Ignoring the out of bound values seems to work. (http://juluribk.com/2011/05/18/843/)
pm3d303132(pm3d303132<0)=0;
pm3d303132(pm3d303132>1)=1;
I have a hunch this has been done before but I am a total layman at this and don't know how to begin to ask the right question. So I will describe what I am trying to do...
I have an unknown ARGB color. I only know its absolute RGB value as displayed over two known opaque background colors, for example black 0x000000 and white 0xFFFFFF. So, to continue the example, if I know that the ARGB color is RGB 0x000080 equivalent when displayed over 0x000000 and I know that the same ARGB color is RGB 0x7F7FFF equivalent when displayed over 0xFFFFFF, is there a way to compute what the original ARGB color is?
Or is this even possible???
So, you know that putting (a,r,g,b) over (r1,g1,b1) gives you (R1,G1,B1) and that putting it over (r2,g2,b2) gives you (R2,G2,B2). In other words -- incidentally I'm going to work here in units where a ranges from 0 to 1 -- you know (1-a)r1+ar=R1, (1-a)r2+ar=R2, etc. Take those two and subtract: you get (1-a)(r1-r2)=R1-R2 and hence a=1-(R1-R2)/(r1-r2). Once you know a, you can work everything else out.
You should actually compute the values of a you get from doing that calculation on all three of {R,G,B} and average them or something, to reduce the effects of roundoff error. In fact I'd recommend that you take a = 1 - [(R1-R2)sign(r1-r2) + (G1-G2)sign(g1-g2) + (B1-B2)sign(b1-b2)] / (|r1-r2|+|g1-g2|+|b1-b2), which amounts to weighting the more reliable colours more highly.
Now you have, e.g., r = (R1-(1-a)r1)/a = (R2-(1-a)r2)/a. These two would be equal if you had infinite-precision values for a,r,g,b, but of course in practice they may differ slightly. Average them: r = [(R1+R2)-(1-a)(r1+r2)]/2a.
If your value of a happens to be very small then you'll get only rather unreliable information about r,g,b. (In the limit where a=0 you'll get no information at all, and there's obviously nothing you can do about that.) It's possible that you may get numbers outside the range 0..255, in which case I don't think you can do better than just clipping.
Here's how it works out for your particular example. (r1,g1,b1)=(0,0,0); (r2,g2,b2)=(255,255,255); (R1,G1,B1)=(0,0,128); (R2,G2,B2)=(127,127,255). So a = 1 - [127+127+127]/[255+255+255] = 128/255, which happens to be one of the 256 actually-possible values of a. (If it weren't, we should probably round it at this stage.)
Now r = (127-255*127/255)*255/256 = 0; likewise g = 0; and b = (383-255*127/255)*255/256 = 255.
So our ARGB colour was 80,00,00,FF.
Choosing black and white as the background colors is the best choice, both for ease of calculation and accuracy of result. With lots of abuse of notation....
a(RGB) + (1-a)0xFFFFFF = 0x7F7FFF
a(RGB) + (1-a)0x000000 = 0x000080
Subtracting the second from the first...
(1-a)0xFFFFFF = 0x7F7FFF-0x000080 = 0x7F7F7F
So
(1-a) = 0x7F/0xFF
a = (0xFF-0x7F)/0xFF = 0x80/0xFF
A = 0x80
and RGB = (a(RGB))/a = 0x000080/a = 0x0000FF
You can do something very similar with other choices of background color. The smaller a is and the closer the two background colors are the less accurately you will be able to determine the RGBA value. Consider the extreme cases where A=0 or where the two background colors are the same.
I'm trying to control some RGB LEDs and fade from red to violet. I'm using an HSV to RGB conversion so that I can just sweep from hue 0 to hue 300 (beyond that it moves back towards red). The problem I noticed though is that it seems to spend far to much time in the cyan and blue section of the spectrum. So I looked up what the HSV spectrum is supposed to look like, and found thisL
I didn't realize that more than half the spectrum was spent between green and blue.
But I'd really like it to look much more like this:
With a nice even blend of that "standard" rainbow colors.
I'd imagine that this would end up being some sort of s-curve of the normal hue values, but am not really sure how to calculate that curve.
An actual HSV to RGB algorithm that handles this internally would be great (any code really, though it's for an Arduino) but even just an explanation of how I could calculate that hue curve would be greatly appreciated.
http://www.fourmilab.ch/documents/specrend/ has a fairly detailed description of how to convert a wavelength to CIE components (which roughly correspond to the outputs of the three kinds of cone sensors in your eyes) and then how to convert those to RGB values (with a warning that some wavelengths don't have RGB equivalents in a typical RGB gamut).
Or: there are various "perceptually uniform colour spaces" like CIE L*a*b* (see e.g. http://en.wikipedia.org/wiki/Lab_color_space); you could pick one of those, take equal steps along a straight line joining your starting and ending colours in that space, and convert to RGB.
Either of those is likely to be overkill for your application, though, and there's no obvious reason why they should be much -- or any -- better than something simpler and purely empirical. So why not do the following:
Choose your starting and ending colours. For simplicity, let's suppose they have S=1 and V=1 in HSV space. Note them down.
Look along the hue "spectrum" that you posted and find a colour that looks to you about halfway between your starting and ending points. Note this down.
Now bisect again: find colours halfway between start and mid, and halfway between mid and end.
Repeat once or twice more, so that you've divided the hue scale into 8 or 16 "perceptually equal" parts.
Convert to RGB, stick them in a lookup table, and interpolate linearly in between.
Tweak the RGB values a bit until you have something that looks good.
This is totally ad hoc and has nothing principled about it at all, but it'll probably work pretty well and the final code will be basically trivial:
void compute_rgb(int * rp, int * gp, int * bp, int t) {
// t in the range 0..255 (for convenience)
int segment = t>>5; // 0..7
int delta = t&31;
int a=rgb_table[segment].r, b=rgb_table[segment+1].r;
*rp = a + ((delta*(b-a))>>5);
a=rgb_table[segment].g; b=rgb_table[segment+1].g;
*gp = a + ((delta*(b-a))>>5);
a=rgb_table[segment].b; b=rgb_table[segment+1].b;
*bp = a + ((delta*(b-a))>>5);
}
(you can make the code somewhat clearer if you don't care about saving every available cycle).
For what it's worth, my eyes put division points at hue values of about (0), 40, 60, 90, 150, 180, 240, 270, (300). Your mileage may vary.
FastLED does a a version of this: https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
HSLUV is another option: http://www.hsluv.org/. They have libraries in a bunch of different languages.
Also, this is an interesting technique: https://www.shadertoy.com/view/4l2cDm
const float tau = acos(-1.)*2.;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
vec3 rainbow = sqrt( //gamma
sin( (uv.x+vec3(0,2,1)/3.)*tau ) * .5 + .5
);
fragColor.rgb = rainbow;
}
Also see:
https://en.wikipedia.org/wiki/Rainbow#Number_of_colours_in_spectrum_or_rainbow for more info.