Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for zooming and panning HTML5 canvas with > 10k objects

I need to build kind of a map in canvas which displays over 10.000 elements (circles) and needs to be zoom- and panable. I described my approach here Android significantly slower in resizing and moving multiple canvas elements and changed my implementation on suggestions made in the comments.

To pan the map setTransform is now used on the canvas context and then all elements that are in the viewport are redrawn after the canvas was erased. (I get them out of an R-Tree). This happens on every mousemove event.

While this is really fast when I have a zoomed map with ~200 objects to draw, the panning is really slow when zoomed out and over 10k objects need to be drawn. I obviously need it to be fast, too.

What would be the best practice to fulfil this requirement? My approach would be the following:

  • Have a viewport div over the canvas and make the canvas bigger (like 50% to each side)
  • Move the canvas in the div with topand leftstyling and redraw less frequently (when the canvas gets close to the viewport border)
like image 899
Bernd Strehl Avatar asked Apr 21 '17 13:04

Bernd Strehl


2 Answers

My approach would probably be:

  • Create an on-screen canvas the size of the "viewport" (the size of the browser window for instance)

  • Store the objects to draw in a data structure that lets you quickly determine which objects are visible at any given time (given the current viewport position and zoom).

  • Make sure the objects to draw (circles) are available as bitmaps (either loaded from image file or pre-rendered to a canvas if you draw them with canvas).
  • The circles's bitmap should be in the correct size given the zoom level, so pre-render the images to an off-screen canvas with the correct size when the zoom level changes. I'm assuming not all 10 000 points have unique images, that they all look the same or that there's only a handful variations.

Then on each render:

  • Clear the canvas
  • Call drawImage() for each circle being visible within the viewport, but only specify position, not width/height or use any transforms. The point being is that the image should only be "copied" to the viewport canvas 1-1. This is really fast.
  • I would suggest to not redraw on every mousemove event (or window resize etc.). Instead, use window.requestAnimationFrame() to schedule a redraw. That way you're not redrawing more than the browser's "refresh" rate, typically 60fps.
  • Panning in the viewport should be really fast since you're just calling drawImage() without any transformations for each visible circle. When you render and the zoom level has changed, there will be the cost of redrawing the pre-rendered images used as source for drawImage.
like image 174
Strille Avatar answered Oct 19 '22 15:10

Strille


I second @Strilles answer.

here is a filtering example that switches between computing all sprites and computing visible-only sprites ever 5 seconds:

var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.width = 100;
canvas.height = canvas.width;
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(255,0,0,0.1)";
;
var sprites = [];
while (sprites.length < 100000) {
    sprites.push({
        x: Math.round(Math.random() * 10000 - 5000),
        y: Math.round(Math.random() * 10000 - 5000)
    });
}
var drawAll = true;
function draw() {
    var targets;
    if (drawAll == true) {
        targets = sprites.slice(0);
    }
    else {
        targets = sprites.filter(function (sprite) {
            return sprite.x > -10 && sprite.x < 110 && sprite.y > -10 && sprite.y < 110;
        });
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (var t = 0; t < targets.length; t++) {
        var target = targets[t];
        ctx.fillRect(target.x - 5, target.y - 5, 10, 10);
        ctx.strokeRect(target.x - 5, target.y - 5, 10, 10);
    }
}
function main() {
    requestAnimationFrame(main);
    for (var i = 0; i < sprites.length; i++) {
        var sprite = sprites[i];
        sprite.y++;
        if (sprite.y > 110) {
            sprite.y -= 200;
        }
    }
    draw();
}
setInterval(function () {
    drawAll = !drawAll;
    console.log(drawAll ? "Draw all" : "Draw filtered");
}, 5000);
main();
like image 25
Emil S. Jørgensen Avatar answered Oct 19 '22 16:10

Emil S. Jørgensen