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.
translatemoves 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
Forget the restore
In the first example, delete
if (restore) ctx.restore();frommarkerand runrun("b", true). The origin ends at (240, 90) there too:save()withoutrestore()saves nothing useful. - 2
Reverse the order
In the second example, swap
ctx.translate(200, 30); ctx.rotate(turn);toctx.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
A failure with no error: the left canvas shows only two of three markers, the second one in the wrong place.
Preview
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): 1The 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
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: trueTry 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.
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 aDOMMatrix..aand.dare x and y scale,.eand.fthe translation, and.isIdentitytells 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 leakedclip()makes later drawing disappear.
Remember this
- 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
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.eand.fare where the origin sits. The second example reads where a corner lands. - 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 usetranslate(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
Order matters.
translate(200, 30)thenrotate(30°)turns a square around its own corner.rotate(30°)thentranslate(120, 10)moves along the already turned axes, so the same numbers land elsewhere (the example prints both landing points).scalealso stretches every latertranslate: afterscale(2, 2),translate(30, 0)moves 60 pixels. - 5
**
save()andrestore()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 alwayssave(); change things; draw; restore();. The widget below runs a list of calls step by step so you can watch the stack. - 6
**
setTransformreplaces, the others multiply.**ctx.setTransform(1, 0, 0, 1, 0, 0)(orresetTransform()) is the identity: no transform. It cannot add up, so it is the safe start of each redraw. Arestore()without asave()does nothing, and asave()never restored leaks state forever.
Explain it without notes
Why does drawing three markers with translate and no restore put marker 3 off the canvas?
What does save() save, and what does restore() not bring back?
How do translate then rotate differ from rotate then translate?
Practice
Draw five 20 × 20 squares in a row, 30 pixels apart, using translate and save/restore so that no change leaks.
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,rotateandscalein a given order.I wrap changes in
save()andrestore(), and I can say what each one covers.I can read
getTransform()to find a leaked transform.I know radians from degrees and that
setTransformreplaces whilescalemultiplies.