I have single channel pixel data in plain DataView format. So the size of the DataView is width*height. For rendering a bitmap from that data, I need to get 4-channel data if I'm right. My current test/naive implementation is like this.
const data = new Uint8ClampedArray(data);
const expanded = new Uint8ClampedArray(width * height * 4);
data.forEach((v, i) => {
expanded[i * 4] = v;
expanded[i * 4 + 1] = v;
expanded[i * 4 + 2] = v;
expanded[i * 4 + 3] = 255;
});
But that's obviously not performant. On 12 megapixel data, this takes something like 300ms. Is there a more efficient way to do a merge like this?
Or even better as a side question: Can I draw single channel bitmap on img tag or canvas?
You can boost the performance by filling your target array with 32 bit instead of 8 bit unsigned integers. In this case the 32 bit value holds the r, g, b and alpha value which you can send using a single instruction.
To better understand let's look at an example. Say we have the color #4080c4
with full alpha ff
. On a little-endian processor architecture this corresponds to:
0xffc48040 == 4291067968
Alpha==0xff==255
Blue==0xc4==196
Green=0x80==128
Red==0x40==64
So the 8 bit unsigned integer values are 255, 64, 128 and 196 respectively. To make a 32 bit unsigned integer out of this 4 individual values we need to use bit-shifting
.
If we look back at the hexadecimal number - 0xffc48040
- in a naive way, we can see that the ff
alpha value is at the left. That means there are 24 bits before which in turn means that ff
has to be shifted 24 bits to the left. If we apply the same logic to the remaining three values we end up with this:
let UI32LE = (Alpha << 24) | (Blue << 16) | (Green << 8) | Red;
The <<
is JavaScript's bitshift operator.
Please note that the order of the bits is important! As I mentioned, the above applies to little-endian. If it's a big-endian
architecture the order is different:
let UI32BE = (Red << 24) | (Green << 16) | (Blue << 8) | Alpha;
Now if we take this technique and use it on something close to your use case we end up with this:
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let width = canvas.width;
let height = canvas.height;
let dummyData = [];
for (let a = 0; a < width * height; a++) {
dummyData.push(parseInt(Math.random() * 255));
}
const data = new Uint8ClampedArray(dummyData);
let buffer = new ArrayBuffer(width * height * 4);
let dataView = new Uint32Array(buffer);
let expanded = new Uint8ClampedArray(buffer);
data.forEach((v, i) => {
dataView[i] = (255 << 24) | (v << 16) | (v << 8) | v;
});
let imageData = context.getImageData(0, 0, width, height);
imageData.data.set(expanded);
context.putImageData(imageData, 0, 0);
<canvas id="canvas" width="200" height="100"></canvas>