0

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

SuperUberDuper
  • 9,242
  • 9
  • 39
  • 72
  • 1
    rgba(0,0,0,0) is not a colour, it is full transparent. Its not white on the browser, you are just seeing the background colour, which happens to be white. – Blindman67 Sep 16 '16 at 21:33
  • So I need some kind of algorithm to transform it) – SuperUberDuper Sep 16 '16 at 22:22
  • I think you are missing the point. The last value in the rgba colour is the alpha value which is 0. This means that it is fully transparent, you can not convert it as it can not be seen. Like a glass window, it is transparent, What colour is a window, that will depend on what is on the other side... – Blindman67 Sep 16 '16 at 23:22
  • What do you call "*computed style*" ? For this exact value, browser will have different output of an element's CSS retrieved through `getComputedStyle()` method. Some will output `"transparent"`, others `"rgba(0,0,0,0);"`. It's not clear at all what you're after and why. Is your problem that when you do `canvas.toDataURL("image/jpeg")` this color is converted to opaque black ? Then it's just because JPEG doesn't have support for alpha channel. To make your canvas display opaque (like it would be exported as JPEG) you could request the context like so: `canvas.getContext('2d', {alpha: false});` – Kaiido Sep 17 '16 at 04:52
  • @Blindman67 I want to preserve transparency, so if the alpha was 0.5 I would need to adjust appropriately for a canvas. – SuperUberDuper Sep 18 '16 at 12:49
  • @SuperUberDuper To reduce canvas transparency via standard canvas render is a two step process. First render to the canvas the colour what you want. `ctx.fillStyle = "#0FF"; ctx.fillRect(0,0,10,10);` then use comp `destination-out` with an alpha value `(1-alpha)` (amount alpha to remove) eg `ctx.globalAlpha = 1 - alpha; ctx.globalCompositeOperation = "destination-out"; ctx.fillRect(0,0,1,1);` Dont forget to restore context. If you want more precise control you need to work with image data and manipulate pixels directly lookup `ctx.getImageData` An example of code would help us answer – Blindman67 Sep 18 '16 at 13:15
  • @Blindman67 please out in answer for points) No code needed, I just want to have an accurate depiction of the css style when rendered to a canvas, including multiple times, to look the same as the DOM – SuperUberDuper Sep 18 '16 at 14:56

1 Answers1

2

Matching DOM colour blending

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.

Blindman67
  • 51,134
  • 11
  • 73
  • 136