1

I have a following task that I'm trying to accomplish the most efficient way possible: I have varying number of pictures of varying size as pixel arrays that I need to add to canvas pixel by pixel. Each pixel's value has to be added to canvas's ImageData so that the result is a blend of two or more images.

My current solution is to retrieve ImageData from the location where the picture needs to be blended with the size of the picture. Then I add the picture's ImageData to the retrieved ImageData and copy it back to the same location.

In a sense this is a manual implementation of canvas globalCompositeOperation "lighter".

"use strict";

let canvas = document.getElementById("canvas");
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
let ctx = canvas.getContext("2d");
ctx.fillStyle="black";
ctx.fillRect(0, 0, width, height);
let imageData = ctx.getImageData(0,0,width,height);
let data = imageData.data;

function random(min, max) {
    let num = Math.floor(Math.random() * (max - min + 1)) + min;
    return num;
}

function createColorArray(size, color) {
    
    let arrayLength = (size*size)*4;
    let array = new Uint8ClampedArray(arrayLength);
    
    for (let i = 0; i < arrayLength; i+=4) {
        
        switch (color) {
            case 1:

                array[i+0] = 255; // r
                array[i+1] = 0;   // g
                array[i+2] = 0;   // b
                array[i+3] = 255; // a
            
                break;
            
            case 2:

                array[i+0] = 0;    // r
                array[i+1] = 255;  // g
                array[i+2] = 0;    // b
                array[i+3] = 255;  // a
            
                break;

            case 3:

                array[i+0] = 0;    // r
                array[i+1] = 0;    // g
                array[i+2] = 255;  // b
                array[i+3] = 255;  // a
        }  

    }

    return array;
}

function picture() {
    this.size = random(10, 500);
    this.x = random(0, width);
    this.y = random(0, height);
    this.color = random(1,3);
    this.colorArray = createColorArray(this.size, this.color);
}

picture.prototype.updatePixels = function() {
    let imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
    let data = imageData.data;
        
    for (let i = 0; i < data.length; ++i) {
        data[i]+=this.colorArray[i];
    }
       
    ctx.putImageData(imageData, this.x, this.y);
}

let pictures = [];
let numPictures = 50;

for (let i = 0; i < numPictures; ++i) {
    let pic = new picture();
    pictures.push(pic);
}

function drawPictures() {
    for (let i = 0; i < pictures.length; ++i) {
        pictures[i].updatePixels();
    } 
}

drawPictures();
<!DOCTYPE html>
<html>

    <head>
        <title>...</title>

        <style type="text/css">
            body {margin: 0px}
            #canvas {position: absolute}
     </style>
        
    </head>

    <body>
        <div>
            <canvas id="canvas"></canvas>
        </div>
        
        <script type="text/javascript" src="js\script.js"></script> 
    </body>

</html>

This solution works fine but it's very slow. I don't know if pixel by pixel blending can even be made very efficient, but one reason for slow performance might be that I need to get the ImageData and put it back each time a new image is blended into canvas.

Therefore the main question is how could I get whole canvas ImageData once in the beginning and then look correct pixels to update based on location and size of each picture that needs to blended into canvas and finally put updated ImageData back to canvas? Also, any other ideas on how to make blending more efficient are greatly appreciated.

Kilipukki
  • 311
  • 3
  • 16

1 Answers1

2

Use the array methods.

The fastest way to fill an array is with the Array.fill function

const colors = new Uint32Array([0xFF0000FF,0xFF00FF00,0xFFFF00]); // red, green, blue
function createColorArray(size, color) {
    const array32 = new Uint32Array(size*size);
    array32.fill(colors[color]);
    return array32;
}

Quick clamped add with |

If you are adding 0xFF to any channel and 0 to the others you can use | and a 32 bit array. For the updatePixels function

var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
var i = 0;
var pic = this.colorArray;   // use a local scope for faster access
do{ 
    data[i] |= pic[i] ;  // only works for 0 and FF chanel values
}while(++i < data.length);
ctx.putImageData(imageData, this.x, this.y);

Bitwise or | is similar to arithmetic add and can be used to increase values using 32bit words. The values will be clamped as part of the bitwise operation.

// dark
var red = 0xFF000088;
var green = 0xFF008800;
var yellow = red | green; // 0xFF008888

There are many other ways to use 32bit operations to increase performance as long as you use only 1 or 2 operators. More and you are better off using bytes.

You can also add if you know that each channel will not overflow a bit

a = 0xFF101010;  // very dark gray
b = 0xFF000080;  // dark red

// non overflowing add
c = a + b; // result is 0xFF000090 correct 

// as 0x90 + 0x80 will overflow = 0x110 the add will not work
c += b; // result overflows bit to green  0xFF000110 // incorrect

Uint8Array V Uint8ClampedArray

Uint8Array is slightly faster than Uint8ClampedArray as the clamping is skipped for the Uint8Array so use it if you don't need to clamp the result. Also the int typedArrays do not need you to round values when assigning to them.

var data = Uint8Array(1);
data[0] = Math.random() * 255; // will floor for you

var data = Uint8Array(1);
data[0] = 256; // result is 0
data[0] = -1; // result is 255

var data = Uint8ClampedArray(1); 
data[0] = 256; // result is 255
data[0] = -1; // result is 0

You can copy data from array to array

var imageDataSource =  // some other source
var dataS = new Uint32Array(imageData.data.buffer);
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);

data.set(dataS); // copies all data

// or to copy a row of pixels   
// from coords
var x = 10;
var y = 10;
var width = 20;  // number of pixels to copy

// to coords
var xx = 30
var yy = 30

var start = y * this.size + x;
data.set(dataS.subArray(start, start + width), xx + yy * this.size);

Dont dump buffers

Don't keep fetching pixel data if not needed. If it does not change between putImageData and getImageData then there is no need to get the data again. It is better to keep the one buffer than continuously creating a new one. This will also relieve the memory stress and reduce the workload on GC.

Are you sure you can not use the GPU

And you can perform a wide range of operations on pixel data using global composite operations. Add, subtract, multiply, divide, invert These are much faster and so far in your code I can see no reason why you need to access the pixel data.

Blindman67
  • 51,134
  • 11
  • 73
  • 136