HTML5 Canvas viewport optimisation with Konva

When trying to squeeze every last morsel of optimisation out of a canvas app, we need to be concerned about NOT drawing anything we don’t need to. This means anything that the user cannot possibly see should not be drawn. I’ll be showing how to know a node is offscreen using the Konva HTML5 canvas library.

So – how to know when a node is offscreen ? The answer is quick and straightforward – we need to compare two rectangles for overlap. One is the what I’ll call the ‘viewport’, and the other is the rect bounding the node we are checking.

Looking at the image above, if da Vinci’s Mona Lisa is the stage and the red box is the container, that makes the red box our viewport and we don’t need to display anything outside of that.

The viewport is the current in-view area of the stage. Thinking about scrolling stages and other potentials to mess with the stage position, we can’t always rely on stage.pos X & Y to be at topleft of the canvas container. But we can think about this another way – the only parts of the stage we can see are bounded by the stage container. For Konva this is a plain DIV that we pass to Konva during creation and which Konva uses to house the canvas element. So, we can formulate a rectangle as

viewPort = {x: 0, y: 0, width: stageContainer.width(), height: stageContainer.height()};
if (! hasOverlap(viewPort, nodeRect){
   offscreen = true;
}

Now turning to the node rect – if we use plain stage co-ordinates we will have to reverse any movements in the stage, layer and group (x, y) positions, which will be complex and non-robust. However, Konva provides the node.getClientRect() method which does all that for us and returns the bounding rectangle of the node in units that make sense when compared to the container div. If the container div has a CSS width of 600px and the node.getClientRect() says the node’s X position is 300 then the node is half way across the container. Following that rule then, if the node’s X position is > 600 then it is off the container and cannot be seen!

This makes our code into:

viewPort = {x: 0, y: 0, width: stageContainer.width(), height: stageContainer.height()};
if (! hasOverlap(viewPort, node.getClientRect()){
   offscreen = true;
}

The hasOverlap() function here looks like below – this is a standard way to test for collision of any two non-rotated rectangles.

// Use center-center distance check for non-rotated rects.
function hasOverlap(r1, r2){

  let w1 = r1.width, h1 = r1.height;
  let w2 = r2.width, h2 = r2.height;

  let diff = {x: Math.abs((r1.x + w1/2) - (r2.x + w2/2)), 
              y: Math.abs((r1.y + h1/2) - (r2.y + h2/2))};

  let compWidth = (r1.width + r2.width)/2,
      compHeight = (r1.height + r2.height)/2;

  let hasOverlap = ((diff.x <= compWidth) && (diff.y <= compHeight)) 

  return hasOverlap;
}

Here’s the code for a demo including the above.

What do I do with this ?

Well, every time you redraw a layer there is a cost, and the less you draw the more you save. So use this approach to know if a node is offscreen, and set the node.visible() attribute as dictated.

Ok – but I always make the stage match the size of the container?

A good approach, but if you allow stage or layer dragging / scrolling or scaling then you could benefit from this technique.

Summary

Not a lot to add really – knowing when a node is completely out-of-sight will save some GPU cycles, helping improve performance. Maybe a bit, but maybe a lot.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: