Canvas Transformations

Learn how translate(), rotate(), scale(), and the save()/restore() stack let you reposition and reshape drawings without recalculating coordinates by hand.

The Transformation Matrix

Every 2D context keeps an internal transformation matrix that is applied to every coordinate you draw after it changes. translate(x, y) shifts the origin, rotate(angle) spins the axes around the current origin (angle in radians, clockwise), and scale(sx, sy) stretches or shrinks distances along each axis. None of these methods draw anything by themselves — they only change where and how subsequent drawing commands land.

Example 1: Rotate a Rectangle Around Its Own Center

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="300" height="300" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

function drawRotatedSquare(cx, cy, size, angleInRadians) {
  ctx.save();
  ctx.translate(cx, cy);       // move origin to the square's center
  ctx.rotate(angleInRadians);  // rotate around that new origin
  ctx.fillStyle = '#3b82f6';
  ctx.fillRect(-size / 2, -size / 2, size, size); // draw centered on origin
  ctx.restore();
}

drawRotatedSquare(150, 150, 80, Math.PI / 6); // 30 degrees
</script>

</body>
</html>

Order Matters

translate(), rotate(), and scale() each multiply onto the existing matrix, so applying them in a different order produces a different picture. Translating then rotating spins a shape around its new, translated origin. Rotating then translating instead moves the shape along a rotated axis, which usually is not what you want. Always translate to the pivot point first, then rotate or scale around it.

MethodEffect
ctx.translate(x, y)Moves the origin (0, 0) by x, y. Later drawing coordinates are measured from the new origin.
ctx.rotate(angle)Rotates the coordinate system clockwise by angle radians around the current origin.
ctx.scale(sx, sy)Stretches the x-axis by sx and the y-axis by sy. Negative values flip/mirror that axis.
ctx.setTransform(a,b,c,d,e,f)Replaces the entire matrix at once instead of multiplying onto it — useful for resetting to a known state.

Isolating Transforms with save() and restore()

  • Call ctx.save() to push the current matrix (and styles) onto a stack
  • Apply translate(), rotate(), and/or scale() for this one shape
  • Draw the shape using coordinates relative to the new origin
  • Call ctx.restore() to pop the stack and undo the transform
  • Repeat the same four steps for the next shape, starting clean each time

Example 2: Drawing a Ring of Rotated Shapes

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="320" height="320" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

const petalCount = 8;
const radius = 100;

ctx.translate(canvas.width / 2, canvas.height / 2); // shared center

for (let i = 0; i < petalCount; i++) {
  ctx.save();
  ctx.rotate((i * 2 * Math.PI) / petalCount);
  ctx.translate(radius, 0);
  ctx.fillStyle = `hsl(${(i * 360) / petalCount}, 70%, 55%)`;
  ctx.beginPath();
  ctx.ellipse(0, 0, 30, 15, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore(); // undo rotate + translate, keeps the shared center translate
}
</script>

</body>
</html>

Example 3: Mirroring with Negative Scale

<!DOCTYPE html>
<html>
<body>

<canvas id="scene" width="300" height="120" style="border:1px solid #ccc;"></canvas>

<script>
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

const icon = new Image();
icon.src = 'assets/icon.png';

icon.onload = () => {
  ctx.save();
  ctx.translate(canvas.width, 0); // move origin to the right edge
  ctx.scale(-1, 1);                // flip the x-axis

  // Draw normally -- it appears mirrored because x grows leftward now
  ctx.drawImage(icon, 20, 20, 64, 64);
  ctx.restore();
};
</script>

</body>
</html>
Note: save()/restore() exist because transformations are cumulative and never expire on their own. Skipping restore() after a per-shape translate/rotate leaks that offset and rotation into every subsequent draw call, so shapes drawn later in the same frame silently shift and spin — a bug that gets worse the more shapes you add. Treat save()/restore() as mandatory brackets around any transform that should only affect one shape.
Note: setTransform(1, 0, 0, 1, 0, 0) resets the matrix to identity in a single call, which is often simpler and cheaper than balancing many nested save()/restore() pairs when you just need to guarantee a clean slate before drawing UI chrome on top of a transformed scene.

Exercise: Canvas Transformations

What unit does ctx.rotate() expect for its angle argument?