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.
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>Exercise: Canvas Transformations
What unit does ctx.rotate() expect for its angle argument?