Escher
Escher

Reputation: 5776

HTML5 Canvas transparency weirdness with pngs

I'm trying to apply transparency to some ImageData retrieved from a canvas. Simple, right? Just set an appropriate value to every fourth datapoint... only, it only seems to work with pixels that don't have pre-existing alpa values.

//create 2 canvases
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
canvas.id = "tempCanvas";
var canvas2 = document.createElement("canvas");
document.body.appendChild(canvas2);
var ctx2 = canvas2.getContext("2d");
canvas2.id = "tempCanvas2";

//draw the png in the first canvas
var testimg = document.getElementById("testimg");
ctx.drawImage(testimg, 0, 0, 200, 200);

//helper function to opacity to the pixels
opacity = function(pixels, value){
    var d = pixels.data;
    for(var i=0;i<d.length;i+=4){
        d[i+3] = value*255; //scale by 255
        if(d[i+3] > 255) d[i+3] = 255; //clamp
    }
    return pixels;
}

//make the first canvas' image data opaque
var data = ctx.getImageData(0,0,200,200);
data = opacity(data, 0.5);
//draw to second canvas
ctx2.fillStyle="#900";
ctx2.fillRect(0,0,200,200);

//should get a jsfiddle logo overlayed on red background
//instead, get yucky grey background
ctx2.putImageData(data, 0, 0);

(It's a little hard to reproduce because of same-origin policy, but here's a fiddle that uses the png jsfiddle logo: http://jsfiddle.net/97c0eetr/ )

Why does this opacity algorithm not work for the jsfiddle logo (and my other transparent pngs that I'm testing on http://localhost)?

Edit: FF42 Ubuntu, if it makes a difference.

Upvotes: 0

Views: 241

Answers (1)

GameAlchemist
GameAlchemist

Reputation: 19294

You've hit the big endian / little endian issue : On most of today's computers/devices, the endianness is little endian, meaning that for a 32 bit value, byte order is 3-2-1-0.

So the opacity, which is the 4th byte (index 3) will be found at pixelIndex * 4 + 0 ... == pixelIndex * 4.

Just change, in your code, your opacity function to :

opacity = function(pixels, value){
    var d = pixels.data;
    for(var i=0;i<d.length;i+=4){
        d[i] = value*255; //scale by 255
        if(d[i] > 255) d[i] = 255; //clamp
    }
    return pixels;
}

updated fiddle here : http://jsfiddle.net/97c0eetr/5/

The best 100% solution of course is to have two opacity function that you choose depending on endinannes of the browser. But by betting on little endian, you cover like 99% of the devices.

If you want to know endianness, i once made a small gist to get it, find it here : https://gist.github.com/TooTallNate/4750953

Here's the code :

function endianness () {
  var b = new ArrayBuffer(4);
  var a = new Uint32Array(b);
  var c = new Uint8Array(b);
  a[0] = 0xdeadbeef;
  if (c[0] == 0xef) return 'LE';
  if (c[0] == 0xde) return 'BE';
  throw new Error('unknown endianness');
}

And by the way, your function could simplify to

opacity = function(pixels, value){
    value = Math.floor(value * 255);
    if (value>255) value = 255;
    var d = pixels.data;
    for(var i=0;i<d.length;i+=4){
        d[i] = value;
    }
    return pixels;
}

Edit : So why the background isn't red ? -->> When using putImageData, you are using a raw 1 pixel for 1 pixel copy method. So everything gets replaced, whatever, for instance, the globalCompositeOperation or any setting. So the red background is just overwritten by the new data.

Solution is to putImageData on a temporary canvas -it will be written with the right opacity-, then to drawImage it on your target canvas, so that the opacity is taken into account for the draw.

Since you already had two canvases, change was easy, see updated fiddle here : http://jsfiddle.net/97c0eetr/4/

Upvotes: 2

Related Questions