Is it better to translate to whole canvas, or to change the position of elements drawn on the canvas?

Viewed 78

I have a canvas with around 20 images drawn on it, at certain positions. Let's just call them objects.

I have a player, right in the centre of the canvas. When this player moves, it stays centered, and all the objects drawn on the canvas will move, instead of the player. Thus, it is creating the illusion the player is moving. While of course, it is the only thing not moving.

Would it be more efficient to instead of changing all the x and y of all of the objects to make them move, translate the whole canvas context? This way I move one (quite big) thing, instead of 20 separate objects, and, on top of that: I wouldn't have to redraw all the images as well.

1 Answers

Set the transform.

Translate creates a matrix and then multiplies the current transform by the translation transform.

The current transform is always applied to all rendered items even if you don't set translate, scale, or rotate.

It is most efficient (in terms of performance and source code complexity) to maintain a shadow of the current transform and avoid the overhead of creating a translate matrix and then multiplying the current transform (happens under the hood).

All you need is an array of 6 values.

const view = [1, 0, 0, 1, 0, 0];  // Matrix representing the view.

This represents the view matrix. First two values are the direction and scale of the x axis (in this case across the canvas from left to right). The second two are the direction and scale of the y axis (in this case down the canvas). The last two are the x, y position of the origin (where, in pixele, if you draw at 0, 0 it will be drawn on the canvas)

You could also use a DOMMatrix which has some handy functionality for more advanced needs, though for basic tasks (translate, scale, rotate) using an array is more performant.

If you have a character...

const character = {x: 1000, y: 1000};  // position of character 

...if you want it centered to the canvas, you need the canvas size and then set the last two values (origin) of the view matrix

view[4] = -character.x + canvas.width * 0.5;
view[5] = -character.y + canvas.height * 0.5;

You can then use

ctx.setTransform(view[0], view[1], view[2], view[3], view[4], view[5]);
// or use spread operator
ctx.setTransform(...view);

And then draw all the object at their position.

Using setTransform and you don't need to save the current state or mess around with translating from and then back. To restore the default transform just set the transform to the identity matrix. eg ctx.setTransform(1,0,0,1,0,0);

Now you do not have to add the translation to each world object making all rendered objects independent of the view.

As the view is a full 2D matrix you can also scale and rotate the view with very little extra effort.

Example

A simple demo that uses a matrix called view to follow a character moving in the world.

requestAnimationFrame(renderLoop);
const ctx = canvas.getContext("2d");
const rndI = (min, max) => Math.random() * (max - min) + min;
const rndSize = (size) => [rndI(size * 0.2, size), rndI(size * 0.2, size)];
const Vec2 = (x, y) => ({x, y});
const view = [1, 0, 0, 1, 0, 0];  // Matrix representing the view.
const Obj = (pos, col, w, h) => ({pos, col, w, h});
const character = Obj(Vec2(0, 0), "#000", 20, 20);  // position of character 

function setView(pos) {
  view[4] = -pos.x + canvas.width * 0.5;
  view[5] = -pos.y + canvas.height * 0.5;
}
function applyView(view) {
  ctx.setTransform(...view);
}

function drawWorld(objs) {
  for(const o of objs) {
    ctx.fillStyle = o.col;
    ctx.fillRect(o.pos.x - o.w * 0.5, o.pos.y - o.h * 0.5, o.w, o.h);
  }
}

function renderLoop(time) {
    ctx.setTransform(1,0,0,1,0,0);  // set default transform to clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    time *= 0.2;
    const pAngA = time / 1000;
    const pAngB = time / 337;
    character.pos.x = Math.cos(pAngA) * 600 + Math.cos(pAngB) * 400;
    character.pos.y = Math.sin(pAngA) * 600 + Math.sin(pAngB) * 400;
    
    setView(character.pos);
    applyView(view);
    drawWorld(world);
    
    requestAnimationFrame(renderLoop);
}
const world = [
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0AF", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0AF", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0AF", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#CA0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#0A0", ...rndSize(630)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  Obj(Vec2(rndI(-1000, 1000), rndI(-1000, 1000)), "#580", ...rndSize(130)),
  character,
];
canvas {
   border: 1px solid black;
}
<canvas id="canvas" width="540" height="180"></canvas>

Related