Skip to content
JavaScript js canvas 5 min read

Transformations

Every drawing command on a 2D canvas is filtered through a transformation matrix before pixels land on screen. By default that matrix is the identity matrix, so coordinates map one-to-one. Once you call translate, rotate, or scale, the matrix changes and every subsequent path is moved, turned, or resized automatically. Mastering transforms lets you draw shapes in convenient local coordinates and then place them anywhere, which is the foundation of sprites, charts, and animation.

The transformation matrix

Internally the canvas keeps a 2D affine matrix with six values: a, b, c, d, e, f. Each point (x, y) you pass to a drawing call is mapped to a new point:

x' = a * x + c * y + e
y' = b * x + d * y + f

Here a and d control horizontal and vertical scaling, b and c control shearing/rotation, and e and f are translation. You rarely set these by hand — the helper methods translate, rotate, and scale multiply the current matrix for you. Transforms are cumulative: calling translate(10, 0) twice moves the origin by 20 pixels total.

translate, rotate, and scale

translate(x, y) shifts the origin of the coordinate system. rotate(angle) rotates around the current origin (angles are in radians, clockwise). scale(sx, sy) multiplies all coordinates, stretching or shrinking shapes — and importantly, line widths and fonts too.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "teal";

ctx.translate(100, 60);   // move origin to (100, 60)
ctx.rotate(Math.PI / 6);  // rotate 30 degrees
ctx.scale(2, 1);          // stretch horizontally

// Drawn in transformed space; (0,0) is now at (100,60)
ctx.fillRect(0, 0, 40, 20);

Because the order of operations matters, think of transforms as moving the paper, not the pen. The last transform applied is the first one the coordinates “see.”

MethodSignatureEffect
translatetranslate(x, y)Moves the origin by (x, y)
rotaterotate(angle)Rotates around the origin, radians, clockwise
scalescale(sx, sy)Scales coordinates; negative values flip/mirror
transformtransform(a, b, c, d, e, f)Multiplies the current matrix by a custom one
setTransformsetTransform(a, b, c, d, e, f)Replaces the matrix outright
resetTransformresetTransform()Restores the identity matrix

Tip: Convert degrees to radians with radians = degrees * Math.PI / 180. Passing degrees directly is the single most common transform bug.

transform vs setTransform

transform() concatenates a matrix onto whatever is already in effect, so it stacks with prior transforms. setTransform() discards the current matrix and installs a fresh one, which is handy for resetting to a known state without touching the save/restore stack.

// Equivalent ways to scale by 2 and shift right by 50:
ctx.transform(2, 0, 0, 2, 50, 0); // multiply onto current matrix

// Wipe everything and set an absolute matrix:
ctx.setTransform(1, 0, 0, 1, 0, 0); // identity (same as resetTransform)

Modern browsers also accept a DOMMatrix argument: ctx.setTransform(new DOMMatrix()). Reading the current matrix back is possible with ctx.getTransform(), which returns a DOMMatrix you can inspect or invert.

Rotating around a point

rotate() always pivots around the current origin (0, 0). To spin a shape around its own center — or any arbitrary point — use the translate-rotate-translate trick: move the origin to the pivot, rotate, then move back (or draw centered).

function rotateAround(ctx, cx, cy, angle, draw) {
  ctx.save();
  ctx.translate(cx, cy); // origin -> pivot
  ctx.rotate(angle);     // spin around pivot
  ctx.translate(-cx, -cy); // origin -> back
  draw();
  ctx.restore();
}

rotateAround(ctx, 80, 80, Math.PI / 4, () => {
  ctx.fillStyle = "crimson";
  ctx.fillRect(50, 50, 60, 60); // rotated 45deg around (80,80)
});

Wrapping the work in save()/restore() keeps each rotation isolated so transforms don’t leak into later drawing. The interactive demo below uses exactly this pattern in an animation loop.

<canvas id="c" width="320" height="200" style="background:#0d1117"></canvas>
<script>
  const ctx = document.getElementById("c").getContext("2d");
  let angle = 0;

  function frame() {
    ctx.clearRect(0, 0, 320, 200);

    ctx.save();
    ctx.translate(160, 100);   // move origin to canvas center
    ctx.rotate(angle);         // rotate around that center
    ctx.fillStyle = "#3fb950";
    ctx.fillRect(-40, -40, 80, 80); // centered square spins in place
    ctx.restore();

    angle += 0.02;
    requestAnimationFrame(frame);
  }
  frame();
</script>

resetTransform

After a series of transforms, the matrix is no longer the identity. Calling resetTransform() is the cleanest way to return to absolute pixel coordinates without remembering whether you balanced every save() with a restore().

ctx.translate(100, 100);
ctx.scale(3, 3);
ctx.fillRect(0, 0, 10, 10); // drawn large and offset

ctx.resetTransform();       // back to identity
ctx.fillRect(0, 0, 10, 10); // drawn at true (0,0), 10x10

The second example shows a flip-and-mirror layout that combines a negative scale with setTransform resets between draws.

<canvas id="mirror" width="320" height="160" style="background:#161b22"></canvas>
<script>
  const ctx = document.getElementById("mirror").getContext("2d");
  const drawArrow = () => {
    ctx.fillStyle = "#58a6ff";
    ctx.beginPath();
    ctx.moveTo(20, 80);
    ctx.lineTo(120, 60);
    ctx.lineTo(120, 100);
    ctx.closePath();
    ctx.fill();
  };

  ctx.setTransform(1, 0, 0, 1, 0, 0); // identity
  drawArrow();                         // normal arrow

  ctx.setTransform(-1, 0, 0, 1, 320, 0); // mirror across vertical center
  drawArrow();                            // flipped copy on the right
</script>

Output:

A blue arrow points right on the left half of the canvas,
and a mirrored blue arrow points left on the right half.

Best Practices

  • Always wrap transforms in save()/restore() so they don’t bleed into later draws.
  • Remember angles are in radians and rotation is clockwise; convert from degrees explicitly.
  • Apply transforms in logical order — translate to the pivot first, then rotate or scale.
  • Use setTransform or resetTransform to return to a known matrix instead of guessing save/restore balance.
  • Be aware that scale also scales line widths, shadows, and text; account for it or reset before stroking.
  • For animation, reset and re-apply transforms each frame rather than accumulating them indefinitely.
  • Prefer the DOMMatrix overloads of setTransform/getTransform when you need to compose or invert matrices programmatically.
Last updated June 1, 2026
Was this helpful?