I am trying to draw on canvas with the same colour as a computed style.
for colours like so it is fine:
rgb(0, 255, 255)"
but for something like
rgba(0, 0, 0, 0)
is is black on the canvas but white in the browser
I am trying to draw on canvas with the same colour as a computed style.
for colours like so it is fine:
rgb(0, 255, 255)"
but for something like
rgba(0, 0, 0, 0)
is is black on the canvas but white in the browser
Red, Green, Blue and the Alpha channel
For the most part all the graphics you see in your browser is comprised of 4 channels. The first 3 are colour channels and represent the intensity of each component colour, red, green, and blue. The fourth channel is the Alpha channel and represents the transparency of the pixel. As stored in memory each channel is 8 bits wide allowing 256 discrete values. For the colour channels 0 represents no contribution, to 255 to full intensity. The alpha channel also has 256 possible values from 0 fully transparent to 255 fully opaque. But it is traditional for alpha to be represented as a unit value from 0 to 1.
We can use the CSS colour string rgba(red,green,blue,alpha)
to represent a pixel colour.
Source-over blending
When a pixel has an alpha value < 1 (byte value < 255) it is blended with the pixel underneath it (this is done by the hardware) and the resulting pixel on the screen is a mix of the two pixels.
The standard formula for blending pixels is based on a paper from 1984 Porter-Duff compositing
In its simplest form and using the byte values for all channels when drawing one pixel on top of another the following procedure is used and is called 'source-over'. ref W3C Simple alpha compositing
// the destination is the pixel being drawn over
var destination = {r : 255, g : 0, b : 0, a : 255}; // red opaque
// source is the pixel being put on top
var source = {r : 0, g : 255, b : 0, a : 127}; // green about half transparent
// normalised means brought to a unit value ranging between 0-1 inclusive
var ad = destination.a / 255; // normalise the destination alpha
var as = source.a / 255; // and source
// get the normalised alpha value for the resulting pixel
var ar = as + ad * (1 - as);
// the resulting pixel
var result = {};
// calculate the colour channels.
result.r = (source.r * as + destination.r * ad * (1 - as)) / ar;
result.g = (source.g * as + destination.g * ad * (1 - as)) / ar;
result.b = (source.b * as + destination.b * ad * (1 - as)) / ar;
// calculate the alpha channel
result.a = ar * 255; // bring alpha back to the byte value
// Though it may seem silly to convert to 8 bit range
// it is important to do so because there is a
// considerable loss of precision in all these
// calculations
// convert to a pixel value a used in 2D context getImageData
var pixel = new Uint8ClampedArray([
result.r,
result.g,
result.b,
result.a,
]);
Put it into a function that will do the same, plus two helper functions.
function blendSourceOver(s,d){
var ad = d.a / 255; // normalise the destination alpha
var as = s.a / 255; // and source
var ar = as + ad * (1 - as);
var r = {};
r.r = Math.round((s.r * as + d.r * ad * (1 - as)) / ar);
r.g = Math.round((s.g * as + d.g * ad * (1 - as)) / ar);
r.b = Math.round((s.b * as + d.b * ad * (1 - as)) / ar);
r.a = Math.round(ar * 255);
return r;
}
function rgbaToColour(col){
col = col.replace("rgba(","").replace(")","").split(",");
var r = {};
r.r = Number(col[0]);
r.g = Number(col[1]);
r.b = Number(col[2]);
r.a = Math.round(Number(col[3]) * 255);
return r;
}
function colourTorgba(col){
return `rgba(${col.r},${col.g},${col.b},${col.a / 255})`;
}
Matching DOM results on canvas.
The problem is to have the canvas match the DOM. Let consider two elements, one over the other. The first div is red and the second is blue with alpha at 0.5.
DOM colour blending example
.exm { width : 100px; height: 30px; color: white; text-align: center;}
<div style = "background : rgba(255, 0, 0, 1);" class = "exm">
<div style = "background : rgba(0, 0, 255, 0.5); position : relative; top : 0px; left : 0px;" class = "exm">
Red + Blue
</div>
</div>
What the resulting colour is, is unknown?
Now say we wish to render to the canvas that resulting colour. As far as I know there is no direct way to sample the colour so we must create it from what is known.
There are two ways this can be done.
By render replication
The first is to replicate what is happening on the DOM. Add the red and then draw the blue over it.
Example of matching DOM colours by replicating the rendering steps
var ctx = can.getContext("2d");
ctx.fillStyle = "rgba(255, 0, 0, 1)";
ctx.fillRect(0, 0, 100, 30);
ctx.fillStyle = "rgba(0, 0, 255, 0.5)";
ctx.fillRect(0, 0, 100, 30);
ctx.font = "18px arial";
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.fillText("Canvas", 50, 22);
var dat = ctx.getImageData(1,1,1,1).data;
colResult.textContent = `Resulting colour rgba(${dat[0]},${dat[1]},${dat[2]},${dat[3]/255})`;
.exm { width : 100px; height: 30px; color: white; text-align: center; font : 18px arial;}
.text {color : black; font-size: xx-small;}
<p class="exm text"> Match DOM and Canvas colours via rendering replication</p>
<div style = "background : rgba(255, 0, 0, 1);" class = "exm">
<div style = "background : rgba(0, 0, 255, 0.5); position : relative; top : 0px; left : 0px;" class = "exm"> DOM
</div>
</div>
<canvas id = "can" width = "100" height = "30" class = "exm"></canvas>
<p class="exm text" id="colResult"></p>
By calculation
The second is to calculate the colour using the source-over blend function.
Example of calculating the colour by using Porter-Duff "source-over"
blending.
function blendSourceOver(s,d){
var ad = d.a / 255; // normalise the destination alpha
var as = s.a / 255; // and source
var ar = as + ad * (1 - as);
var r = {};
r.r = Math.round((s.r * as + d.r * ad * (1 - as)) / ar);
r.g = Math.round((s.g * as + d.g * ad * (1 - as)) / ar);
r.b = Math.round((s.b * as + d.b * ad * (1 - as)) / ar);
r.a = Math.round(ar * 255);
return r;
}
function rgbaToColour(col){
col = col.replace("rgba(","").replace(")","").split(",");
var r = {};
r.r = Number(col[0]);
r.g = Number(col[1]);
r.b = Number(col[2]);
r.a = Math.round(Number(col[3]) * 255);
return r;
}
function colourTorgba(col){
return `rgba(${col.r},${col.g},${col.b},${col.a / 255})`;
}
var colour = colourTorgba(
blendSourceOver(
rgbaToColour("rgba(0, 0, 255, 0.5)"), // source
rgbaToColour("rgba(255, 0, 0, 1)") // destination
)
);
var ctx = can.getContext("2d");
ctx.fillStyle = colour;
ctx.fillRect(0, 0, 100, 30);
ctx.font = "18px arial";
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.fillText("Canvas", 50, 22);
colResult.textContent = "Resulting colour "+colour;
.exm { width : 100px; height: 30px; color: white; text-align: center; font : 18px arial;}
.text {color : black; font-size: xx-small;}
<p class="exm text"> Match DOM and Canvas colours by calculation</p>
<div style = "background : rgba(255, 0, 0, 1);" class = "exm">
<div style = "background : rgba(0, 0, 255, 0.5); position : relative; top : 0px; left : 0px;" class = "exm"> DOM
</div>
</div>
<canvas id = "can" width = "100" height = "30" class = "exm"></canvas>
<p class="exm text" id="colResult"></p>
Check for alpha = 1
Now one must be careful as all calculations you do you must always end up with a colour that has an alpha value of 1. If not you are missing some information. The final result of all DOM colour blending is Alpha = 1 (the background), any further transparency is beyond the DOM's context and is part of the browser window.
Order of calculations
If you wish to calculate more than two colours you must do so in the same order as done in the DOM. If the order is incorrect then the resulting colour will also be incorrect.
For example say you have a red background then a blue div with 0.5 alpha and then on top of that a green div with 0.5 alpha. The order of calculations is from bottom up.
background = rgbaToColour("rgba(255,0,0,1)");
div1 = rgbaToColour("rgba(0,0,255,0.5)");
div2 = rgbaToColour("rgba(0,255,0,0.5)");
First the background and div1 then mix the result of that with div2
var temp = blendSourceOver(div1,background); // source then destination
var result = blendSourceOver(div2,temp); // source then temp destination
console.log(colourTorgb(result)); // => "rgba(63,128,64,1)"
Doing it the other way will result in a completely different colour.
var temp = blendSourceOver(div1,background); // source then destination
var result = blendSourceOver(div1,temp); // source then temp destination
console.log(colourTorgb(result)); // => "rgba(63,64,128,1)"
Further reading
For all you need to know the W3C (suspense filled) Compositing and Blending Level 2 covers this subject in a more detailed way.