draw 10,000 objects on canvas javascript
Asked Answered
H

5

19

I need draw over 10,000 images (32x32 px) on canvas but over 2000 draws the performances is very bad.

this is a little example:

object structure {position:0}

for(var nObject = 0; nObject < objects.length; nObject++){
    ctx.save();
    ctx.translate(coords.x,coords.y);
    ctx.rotate(objects[nObject].position/100);
    ctx.translate(radio,0);
    ctx.drawImage(img,0,0);
    ctx.restore();
    objects[nObject].position++;
}

with this code I traslate the images around of a coordinates.

What do you recommend to improve performance?

update:

i try layering but the performances worsens

http://jsfiddle.net/72nCX/3/

Hybris answered 5/5, 2014 at 8:37 Comment(7)
Depending on what you are drawing you could draw them over multiple canvases with the static non changing objects on one to avoid redrawing them,Merralee
all pictures change position in the cycle but does not change the pictureHybris
Could you setup a live exampleMerralee
jsfiddle.net/B72gkHybris
can you give real images example? are all of these images different? can you avoid rotation? do all of these images change position on each frame? Knowing this can help to find optimization.Bibby
Hello viliusL the example image is very similar to the original and sometimes the image change too.Hybris
WADR, You might want to change your design. 10,000 elements like the one in your example fiddle would be overwhelming to the viewers attention. More is not always better--try your design with 500 elements instead. Your results will be within the performance limits of html canvas and within your viewers limits of appreciation!Dexterdexterity
A
57

I can get you 10,000 but there are two main drawbacks.

  1. You may notice the images don't respect transparency entirely, its possible to fix.. but that's beyond the scope of this answer.

  2. You will have to use math to do any sort of transformations because the standard canvas transformation matrix can not be applied to ImageData

Live Demo

Explanation of the code and methods

So to get the fastest performance possible with canvas and a large number of objects you need to use ImageData. This is accessing the canvas element on a per pixel level basically, and allows you to do all sorts of cool stuff. I used two primary methods.

Also here is a nice tutorial that goes into it a bit to help get a better understanding.

So what I did is first I created a temporary canvas for the image

imgToDraw.onload = function () {
    // In memory canvas
    imageCanvas = document.createElement("canvas"),
    iCtx = imageCanvas.getContext("2d");

    // set the canvas to the size of the image
    imageCanvas.width = this.width;
    imageCanvas.height = this.height;

    // draw the image onto the canvas
    iCtx.drawImage(this, 0, 0);

    // get the ImageData for the image.
    imageData = iCtx.getImageData(0, 0, this.width, this.height);
    // get the pixel component data from the image Data.
    imagePixData = imageData.data;

    // store our width and height so we can reference it faster.
    imgWidth = this.width;
    imgHeight = this.height;

    draw();
};

Next Is the main piece which is in the rendering function

I'm just posting the relevant portion.

// create new Image data. Doing this everytime gets rid of our 
// need to manually clear the canvas since the data is fresh each time
var canvasData = ctx.createImageData(canvas.width, canvas.height),
    // get the pixel data
    cData = canvasData.data;

// Iterate over the image we stored 
for (var w = 0; w < imgWidth; w++) {
    for (var h = 0; h < imgHeight; h++) {
        // make sure the edges of the image are still inside the canvas
        // This also is VERY important for perf reasons
        // you never want to draw outside of the canvas bounds with this method
        if (entity.x + w < width && entity.x + w > 0 &&
            entity.y + h > 0 && entity.y + h < height) {

            // get the position pixel from the image canvas
            var iData = (h * imgWidth + w) * 4;

            // get the position of the data we will write to on our main canvas
            // the values must be whole numbers ~~ is just Math.floor basically
            var pData = (~~ (entity.x + w) + ~~ (entity.y + h) * width) * 4;

            // copy the r/g/b/ and alpha values to our main canvas from 
            // our image canvas data.

            cData[pData] = imagePixData[iData];
            cData[pData + 1] = imagePixData[iData + 1];
            cData[pData + 2] = imagePixData[iData + 2];
            // this is where alpha blending could be applied
            if(cData[pData + 3] < 100){
                cData[pData + 3] = imagePixData[iData + 3];
            }
        }
    }
}

// now put all of that image data we just wrote onto the actual canvas.
ctx.putImageData(canvasData, 0, 0);

The main Take away from this is, if you need to draw a ridiculous number of objects on the canvas you can't use drawImage, pixel manipulation is your friend.

Adiaphorous answered 6/5, 2014 at 5:48 Comment(6)
thanks @NevinMadhukarK problems like this with canvas are my favorite to try and solve. Your answer is a good one too, I should check out Kinect.js to see how they handle the rendering.Adiaphorous
@Loktar, +1 nice solution. I particularly like your effective use of getImageData+putImageData which is often misused causing reduced performance.Dexterdexterity
wow! this is amazing! I'm not particularly familiar with putImageData and createImageData but now I have something new to learn.Hybris
If you don't care about transparency, you can squeeze a bit more performance out of this by creating Uint32Array views over the underlying ArrayBuffers used for the Uint8Clamped arrays of cData and ImagePixData, and copying 32 bits at once. This is slightly faster.Crews
Two other small optimisations: switch loop order for better memory access (so go over h in the outer loop, and w in the inner). Second, don't call createImagedata every draw loop - do it once in a set-up function and re-use your cData array (to clear the screen, use cData.fill(0)). TypedArray allocation is still a bit slowCrews
Also, the code is memory bound, so instead of putting four floating point values in a JS-object, put all data in a Float64Array. Putting it all together you can (in my laptop) increase the nr of sprites to 50000, and still be faster: jsfiddle.net/vanderZwan/ddg1kpfr/1Crews
S
4

I think this is what you need.

Eric Rowell (creator of KineticJS) has done some stress tests here.

And he says this:

"Create 10 layers each containing 1000 shapes to create 10,000 shapes. This greatly improves performance because only 1,000 shapes will have to be drawn at a time when a circle is removed from a layer rather than all 10,000 shapes."

"Keep in mind that having too many layers can also slow down performance. I found that using 10 layers each made up of 1,000 shapes performs better than 20 layers with 500 shapes or 5 layers with 2,000 shapes."

Update: You would need to run test cases in which the most optimized procedure would be for you. Example: 10000 shapes can be achieved by either:

10000 shapes * 1 layer

5000 shapes * 2 layer

2500 shapes * 4 layer

Whichever works for you,choose that! It depends upon your code.

Stemware answered 5/5, 2014 at 8:52 Comment(6)
if I understand correctly, it would be best to create one canvas element per 1,000 objects to be printed?Hybris
Yes 10 times the (1 layer upon which a 1000shapes) rather than (10000 shapes on 1 layer).Stemware
I create a test and tell you.Hybris
@NevinMadhukarK. Notice that the KineticJS example just repositioned the x,y of the objects. The questioner is attempting to save+transfrom+restore the context 10,000 times per second. That attempt is beyond canvas's ability.Dexterdexterity
@Dexterdexterity I think he added the transform feature later on? I am not sure. Or i must have missed it out.Stemware
@Dexterdexterity its doable! But for the transformations you're going to have to do the math yourself, since the only way will be pixel manipulation directly. Check out my answer :)Adiaphorous
A
1

If the images don't overlap, then the resulting image is 3200x3200 pixels which is more than most displays can display. So you can try to get the bounding box of the transformed image and skip those which are outside the visible area (even though the canvas should already do that for you).

Another idea is to combine the small images into bigger ones and transform them together as a group.

If you want to organize the images in a ring, then you can draw them once as a ring, save that as an image and then rotate the "ring image" instead of each individual image.

Lastly, have a look at WebGL which might be more efficient than 2D canvas API.

Acadia answered 5/5, 2014 at 9:11 Comment(4)
I like your last tip. I will try to implement it. As for WebGL, I got a last alternative but not forgotten. thank you very much.Hybris
The images may look grainy if you rotate the ring. If that's the case, render the intermediate image with 2x-4x the final resolution (i.e. render 128x128px) and then scale the image down. That will give you a nice anti-aliasing with little cost.Acadia
@AaronDigulla :-) Just saying...32x32 times 10,000 images is 32,000 x 32,000 and WebGL is canvas (3D context). +1 for grouping similar transforms and caching as images.Dexterdexterity
@markE: My guess is that he'll arrange them in a square (mostly) so that would be sqrt(10000) images per axis or 100, not 1000.Acadia
S
1

Here are some steps you can do to increase the performance:

  • First get rid of the save/restore - they are very expensive calls and can be replaced with setTransform
  • Unwind the loop to do more inside per iteration
  • Cache all properties

FIDDLE

Example with loop unwound for 4 iterations:

for(var nObject = 0,
        len = objects.length,    // cache these
        x = coords.x,
        y = coords.y; nObject < len; nObject++){

    ctx.setTransform(1,0,0,1, x, y);   // sets absolute transformation
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;

    ctx.setTransform(1,0,0,1,x, y);
    ctx.rotate(objects[nObject].position*0.01);
    ctx.translate(radio,0);
    ctx.drawImage(imgToDraw,0,0);
    objects[nObject++].position++;
}
ctx.setTransform(1,0,0,1,0,0);  // reset transform for rAF loop

(don't expect real-time performance though).

Although, it is perhaps a bit pointless drawing 2000 objects in such a small area. If you are after the effect I would suggest this approach instead:

  • Create an off-screen canvas
  • Produce 5-8 frames with the method above and store them as images
  • Play back those 5-8 images as-is instead of doing all the calculations

If you need more fluid look simply produce more frames. You can store each frame in a single canvas based on cells which you use as a sprite-sheet later. When drawing you must of course take care that current positions are static versus moving when actually animated. Rotation and the resulting position is another factor.

Surface answered 5/5, 2014 at 17:22 Comment(0)
H
1

After various tests, I have come to the following conclusions:

  • canvas does not have the capacity for this task.
  • Layered canvas only helps performance when static elements need not be constantly redrawn.
  • Add coordinates print limit helps a lot in rendering.
  • alternatives to slow functions
  • Do not print elements will ultimately hidden by another element with a higher z-index (working on it).

the end result is a small mix of all contributions. but needs improvement.

Tested with 30,000 objects and the performance is maintained at 60/fps.

http://jsfiddle.net/NGn29/1/

        var banPrint = true;
        for(nOverlap = nObject; nOverlap < objects.length; nOverlap++){
            if(
                objects[nOverlap].position == objects[nObject].position
                && nOverlap != nObject
            ){
                banPrint = false;
                break;
            }
        }
Hybris answered 5/5, 2014 at 23:54 Comment(1)
Canvas is capable to an extent. Its a bit tricky to do under certain conditins but possible.Adiaphorous

© 2022 - 2024 — McMap. All rights reserved.