Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~20 min· topic 3 of 7

Topic 11.3

Transforms and the State Stack

In one line

translate, rotate and scale move the canvas's grid instead of your shapes, and every one of them stays set until you undo it. save and restore are how you undo them, and forgetting that is the source of most canvas surprises.

Think of it like this

A sheet of tracing paper on a table. You slide the sheet to the right, then turn it, then draw. Your pen strokes are the same, but they land in new places because the paper moved. Slide it first and turn it second, and the drawing ends up somewhere else than if you turned it first.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

transform
A change in how drawing numbers become pixels: slide, turn or stretch.
matrix
The six numbers the context uses to remember all transforms so far. getTransform() returns it.
origin
The point (0, 0) that drawing is measured from. translate moves it.
state
Everything a context remembers: colours, line width, font, transform, clip and more.
state stack
The pile of saved states. save() adds one, restore() takes the top one back.

Step by step

01Draw a square with no transform

The stage on the right draws a 40 × 40 square at the origin (0, 0), in the top-left corner. This is the starting point: every later step changes only the transform, not the square.

02translate: slide the origin

translate(120, 40) moves the origin before the same fillRect(0, 0, 40, 40). The square lands at (120, 40) without a single number in fillRect changing.

03rotate: turn the axes

rotate(Math.PI / 6) (30 degrees) after the translate turns the axes around the new origin. The square turns around its own top-left corner, because that corner is the origin.

04save and restore: undo it

Wrap the changes in save() and restore(). After restore() the origin is back at (0, 0), and a second square drawn at fillRect(0, 0, 40, 40) lands in the corner again, in the colour set before the save().

Try it yourself

  1. 1

    Forget the restore

    In the first example, delete if (restore) ctx.restore(); from marker and run run("b", true). The origin ends at (240, 90) there too: save() without restore() saves nothing useful.

  2. 2

    Reverse the order

    In the second example, swap ctx.translate(200, 30); ctx.rotate(turn); to ctx.rotate(turn); ctx.translate(200, 30);. The first log line changes from (234.6, 50) to (192.8, 146): the teal square lands near the bottom edge of the 150-pixel canvas, because the move of 200 and 30 now follows the turned axes.

Code & diagrams

The transform that leaks into every later draw New tab

A failure with no error: the left canvas shows only two of three markers, the second one in the wrong place.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

a: after drawing, the origin is at (240, 90)
b: after drawing, the origin is at (0, 0)
scale(2, 2) call 1: the matrix scales by 2
scale(2, 2) call 2: the matrix scales by 4
scale(2, 2) call 3: the matrix scales by 8
after setTransform(1, 0, 0, 1, 0, 0): 1
Order matters: translate and rotate New tab

The same numbers, a different order, a different place. Three squares: teal after translate then rotate, orange after rotate then translate, yellow after scale then translate.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

translate, then rotate: the square's corner (40, 0) lands at (234.6, 50)
rotate, then translate: the square's corner (40, 0) lands at (133.6, 88.7)
scale(2), then translate(30): the square's corner (40, 0) lands at (420, 20)
after restore, the matrix is the identity again: true

Try it: interactive

Watch the state stack

Step through the calls and watch what save() and restore() put on the stack and where the square lands.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Scale before the translate

In the first example, add ctx.scale(2, 2); as the first line of marker (before the translate) and add a last line that prints which of the pixels (40, 70), (160, 70) and (280, 70) of canvas b are painted.

terminal
$ Console
── what you'll see ──
No error. The origin log still says (0, 0), but the markers moved: pixels at x = 40 and 160 are painted (alpha 255, 255) and x = 280 is not (0), because the third marker is off the 220-pixel canvas.

Myth vs fact

Myth

translate moves the shapes I drew before.

Fact

A transform affects only what you paint afterwards. Pixels already painted never move.

Myth

Rotation is in degrees.

Fact

Radians: 30 degrees is Math.PI / 6. Passing 30 turns the axes about 1,719 degrees.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    getTransform() returns a DOMMatrix. .a and .d are x and y scale, .e and .f the translation, and .isIdentity tells you whether anything is set. Logging it is the fastest way to debug 'my shape is in the wrong place'.

  • ▸

    restore() brings back the clip too (Topic 11.5). A clip can only get smaller until you restore, so a leaked clip() makes later drawing disappear.

Remember this

  1. 1

    The problem this solves: to draw a bike marker at 25 different spots you would rather draw it once around (0, 0) and move the whole drawing. A transform changes how the canvas turns your numbers into pixels, so the drawing code stays the same.

  2. 2

    Three moves and one matrix. translate(dx, dy) slides the origin. rotate(angle) turns the axes around the origin (in radians, clockwise). scale(sx, sy) stretches the axes. Together they are stored as one matrix, six numbers: ctx.getTransform() shows it, and .e and .f are where the origin sits. The second example reads where a corner lands.

  3. 3

    Transforms add up. Each call changes the current matrix and nothing resets it. Call scale(2, 2) three times and you scale by 8. In the first example, three markers use translate(20 + i * 60, 30): without a reset, marker 2 is moved 20 + 60 = 80 pixels plus the previous 20, marker 3 lands off the canvas, and the origin ends at (240, 90).

  4. 4

    Order matters. translate(200, 30) then rotate(30°) turns a square around its own corner. rotate(30°) then translate(120, 10) moves along the already turned axes, so the same numbers land elsewhere (the example prints both landing points). scale also stretches every later translate: after scale(2, 2), translate(30, 0) moves 60 pixels.

  5. 5

    **save() and restore() are a stack.** save() pushes a copy of the whole state (transform, fillStyle, strokeStyle, lineWidth, font, globalAlpha, the clip…) onto a stack. restore() pops it and brings the old state back. The pattern is always save(); change things; draw; restore();. The widget below runs a list of calls step by step so you can watch the stack.

  6. 6

    **setTransform replaces, the others multiply.** ctx.setTransform(1, 0, 0, 1, 0, 0) (or resetTransform()) is the identity: no transform. It cannot add up, so it is the safe start of each redraw. A restore() without a save() does nothing, and a save() never restored leaks state forever.

Explain it without notes

01

Why does drawing three markers with translate and no restore put marker 3 off the canvas?

02

What does save() save, and what does restore() not bring back?

03

How do translate then rotate differ from rotate then translate?

Practice

01

Draw five 20 × 20 squares in a row, 30 pixels apart, using translate and save/restore so that no change leaks.

02

Draw a 40-pixel-long line that spins around its middle by 45 degrees, centred at (100, 50).

Trade-offs

  • ↔

    Transforms keep drawing code simple and reusable, but a leaked transform is hard to see: the symptom appears far from the cause.

Done when you can

  • I can predict where a shape lands after translate, rotate and scale in a given order.

  • I wrap changes in save() and restore(), and I can say what each one covers.

  • I can read getTransform() to find a leaked transform.

  • I know radians from degrees and that setTransform replaces while scale multiplies.