Search code examples
javascriptarraysimageperformancehtml5-canvas

Efficient way for in between merge of array


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?


Solution

  • 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>