html5 – How to implement game camera for HTML 5 Canvas (camera zoom in/zoom out)?

Question:

Actually the task is to implement a camera that will follow the player in the game world with the ability to zoom in / out of the camera from the player.

Making the camera follow the player is quite simple:

ctx.save();
ctx.translate(-camera.leftTopPos.x, -camera.leftTopPos.y);
// Рисуем игрока и игровой мир
ctx.restore();

Where camera is:

var camera = {
  leftTopPos: { x: 0, y: 0 }, // Левый верхний угол камеры в игровом мире.
  size: { x: 0, y: 0 }, // Размер камеры (по умолчанию, равен размеру холста)
  scale: 1,
};

Is it correct to use ctx.translate for a large game world? Would it be more efficient, in terms of performance, to keep the player at the same point (0, 0) or (w/2, h/2) all the time and move all other objects relative to him?

Problem with adding camera zoom in/out. If you do this:

ctx.translate(-camera.leftTopPos.x, -camera.leftTopPos.y);
ctx.scale(camera.scale.x, camera.scale.y);

then there are problems with changing the size of the camera and it shifts somewhere.

I made an example for working with the camera on jsfiddle .

wasd – camera movement. zx – zoom in and out of the camera.

function p(s) { document.body.innerHTML += s + '<br>'; }

var w, h;
var camera = {
  leftTopPos: { x: 0, y: 0 },
  size: { x: 0, y: 0 },
  scale: 1,
};
var canvas = document.querySelector('canvas');
var can = canvas;
var context = canvas.getContext('2d');
var ctx = context;

function updateCameraSize() {
	camera.size = {
  	x: canvas.width,
    y: canvas.height,
  };
}

function setFullscreenSize() {
  w = canvas.width = window.innerWidth;
  h = canvas.height = window.innerHeight;
  updateCameraSize();
}

function makeAlwaysCanvasFullscreen() {
  setFullscreenSize();
  window.addEventListener('resize', setFullscreenSize);
}

function drawLine(x1, y1, x2, y2) {
	ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

function drawGrid() {
  var cellSize = 100;
  var minX = -1000, maxX = 1000;
  var minY = -1000, maxY = 1000;
  for (var x = minX; x <= maxX; x += cellSize) {
    drawLine(x, minY, x, maxY);
  }
  for (var y = minY; y <= maxY; y += cellSize) {
    drawLine(minX, y, maxX, y);
  }
}

function drawAxises() {
  var minX = -1000, maxX = 1000;
  var minY = -1000, maxY = 1000;
  ctx.save();
  ctx.lineWidth = 5;
  ctx.strokeStyle = 'red';
  drawLine(0, 2 * minY, 0, 2 * maxY);
  ctx.strokeStyle = 'green';
  drawLine(2 * minX, 0, 2 * maxX, 0);
  ctx.restore();
}

function update() {
  var dir = { x: 0, y: 0 };
  var speed = 10;
  if (key.isPressed('a')) dir.x -= 1;
  if (key.isPressed('d')) dir.x += 1;
  if (key.isPressed('w')) dir.y -= 1;
  if (key.isPressed('s')) dir.y += 1;
  camera.leftTopPos.x += speed * dir.x;
  camera.leftTopPos.y += speed * dir.y;
}

function draw() {
	ctx.clearRect(0, 0, w, h);
  ctx.save();
  ctx.translate(-camera.leftTopPos.x, -camera.leftTopPos.y);
  ctx.scale(camera.scale, camera.scale);
  drawGrid();
  drawAxises();
  drawViewportRect();
  ctx.restore();
}

function drawViewportRect() {
  ctx.save();
  ctx.lineWidth = 10;
  ctx.strokeStyle = 'yellow';
  var pos = camera.leftTopPos, sz = camera.size;
  ctx.beginPath();
  ctx.rect(pos.x, pos.y, sz.x, sz.y);
  ctx.stroke();
  ctx.restore();
}

function main() {
  function go() {
  	update();
    draw();
    requestAnimationFrame(go);
  }
  requestAnimationFrame(go);
}

makeAlwaysCanvasFullscreen();
main();

key('z', () => {
	camera.scale += 0.1;
});
key('x', () => {
	camera.scale -= 0.1;
});
canvas {
  position: fixed;
  left: 0;
  top: 0;
  /* border: 1px solid black; */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/keymaster/1.6.1/keymaster.min.js"></script>
<canvas></canvas>

Answer:

Using the method of poking and measuring distances during debugging, I nevertheless found how to implement zooming in and out of the camera:

ctx.save();
ctx.translate(-camera.leftTopPos.x * camera.scale, -camera.leftTopPos.y * camera.scale);
// Рисуем игрока и игровой мир
ctx.restore();

The dimensions of the chamber will be as follows:

camera.size = {
  x: canvas.width / camera.scale,
  y: canvas.height / camera.scale,
};

jsfiddle

function p(s) { document.body.innerHTML += s + '<br>'; }

var w, h;
var camera = {
  leftTopPos: { x: 0, y: 0 },
  size: { x: 0, y: 0 },
  scale: 1,
};
var canvas = document.querySelector('canvas');
var can = canvas;
var context = canvas.getContext('2d');
var ctx = context;

function updateCameraSize() {
  camera.size = {
    x: canvas.width / camera.scale,
    y: canvas.height / camera.scale,
  };
}

function setFullscreenSize() {
  w = canvas.width = window.innerWidth;
  h = canvas.height = window.innerHeight;
  updateCameraSize();
}

function makeAlwaysCanvasFullscreen() {
  setFullscreenSize();
  window.addEventListener('resize', setFullscreenSize);
}

function drawLine(x1, y1, x2, y2) {
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

function drawGrid() {
  var cellSize = 100;
  var minX = -1000, maxX = 1000;
  var minY = -1000, maxY = 1000;
  for (var x = minX; x <= maxX; x += cellSize) {
    drawLine(x, minY, x, maxY);
  }
  for (var y = minY; y <= maxY; y += cellSize) {
    drawLine(minX, y, maxX, y);
  }
}

function drawAxises() {
  var minX = -1000, maxX = 1000;
  var minY = -1000, maxY = 1000;
  ctx.save();
  ctx.lineWidth = 5;
  ctx.strokeStyle = 'red';
  drawLine(0, 2 * minY, 0, 2 * maxY);
  ctx.strokeStyle = 'green';
  drawLine(2 * minX, 0, 2 * maxX, 0);
  ctx.restore();
}

function update() {
  var dir = { x: 0, y: 0 };
  var speed = 10;
  if (key.isPressed('a')) dir.x -= 1;
  if (key.isPressed('d')) dir.x += 1;
  if (key.isPressed('w')) dir.y -= 1;
  if (key.isPressed('s')) dir.y += 1;
  camera.leftTopPos.x += speed * dir.x;
  camera.leftTopPos.y += speed * dir.y;
}

function draw() {
  ctx.clearRect(0, 0, w, h);
  ctx.save();
  ctx.translate(-camera.leftTopPos.x * camera.scale, -camera.leftTopPos.y * camera.scale);
  ctx.scale(camera.scale, camera.scale);
  drawGrid();
  drawAxises();
  drawViewportRect();
  ctx.restore();
}

function drawViewportRect() {
  ctx.save();
  ctx.lineWidth = 10;
  ctx.strokeStyle = 'yellow';
  var pos = camera.leftTopPos, sz = camera.size;
  ctx.beginPath();
  ctx.rect(pos.x, pos.y, sz.x, sz.y);
  ctx.stroke();
  ctx.restore();
}

function main() {
  function go() {
    update();
    draw();
    requestAnimationFrame(go);
  }
  requestAnimationFrame(go);
}

makeAlwaysCanvasFullscreen();
main();

key('z', () => {
  camera.scale += 0.1;
  updateCameraSize();
});
key('x', () => {
  camera.scale -= 0.1;
  updateCameraSize();
});
canvas {
  position: fixed;
  left: 0;
  top: 0;
  /* border: 1px solid black; */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/keymaster/1.6.1/keymaster.min.js"></script>
<canvas></canvas>

But there is still a question about the performance of translate .

Scroll to Top