Transforms

Translate, rotate, scale, and the critical save/restore state stack.

Lesson 5 of 10

What Is a Coordinate System?

Before we talk about transforms, let’s make sure we understand the coordinate system that Canvas uses. If you’ve ever used graph paper, you already know the idea: every point on the canvas has an address made of two numbers: x (how far right) and y (how far down).

The Canvas coordinate system:

(0,0)───────────────────────▶ x increases
  │
  │    (50,30)●
  │
  │              (200,100)●
  │
  │                          (400,250)●
  │
  ▼
y increases

Notice two things that are different from math class: (1) the origin (0,0) is at the top-left, not bottom-left; (2) y goes down, not up. This is standard for all computer graphics.

The Default Coordinate System paused

Try this: Add a new point to the points array, like { x: 300, y: 200, label: '(300,200)' }. Hit Run and verify it appears where you expect on the grid.

Why Transforms?

Imagine you’re drawing on a sheet of graph paper taped to a table. Normally, to draw a square at position (200, 100), you’d carefully count grid lines and draw there. But there’s another approach: slide the paper so that the (0,0) corner is at the position you want, then always draw at (0,0). The result on the paper is the same, but the second approach is much easier when you need to draw complex, repeated shapes.

The “moving the paper, not the pen” analogy:

 Method 1: Move the pen              Method 2: Move the paper
┌─────────────────────┐             ┌─────────────────────┐
│                     │             │                     │
│     ▲ pen moves     │             │     paper slides    │
│     │ to (200,100)  │             │     under the pen   │
│     └──────●        │             │     ●───────────▶   │
│       draw here     │             │  pen stays at (0,0) │
│                     │             │                     │
└─────────────────────┘             └─────────────────────┘

Transforms use Method 2. You transform the coordinate system (move/rotate/scale the “paper”), then draw at simple coordinates. This is especially powerful for rotation, since calculating rotated coordinates by hand requires trigonometry, but with transforms the canvas does the math for you.

There are three basic transforms: translate (slide), rotate (spin), and scale (resize). Let’s learn each one.

translate(x, y): Sliding the Origin

ctx.translate(x, y) moves the origin point of the coordinate system. After calling translate(200, 100), anything you draw at (0, 0) actually appears at pixel (200, 100) on the canvas.

Step by step: what translate does:

Before translate(200, 100):       After translate(200, 100):
┌────────────────────┐            ┌────────────────────┐
│(0,0)               │            │ old origin         │
│  ●                 │            │  ○                 │
│  │                 │            │       (0,0)        │
│  │                 │            │         ●          │
│  │                 │            │         │ new      │
│  │                 │            │         │ origin   │
└────────────────────┘            └────────────────────┘

Drawing at (50, 30) now appears at (250, 130) on screen.
The formula: screen_x = draw_x + 200
             screen_y = draw_y + 100
translate: Moving the Origin paused

The real power of translate is that you can define a shape once (drawn at 0,0) and reuse it at any position. Here’s the classic “stamp” pattern:

translate: Stamping a Shape at Multiple Positions paused

rotate(angle): Spinning the Coordinate System

Before we learn rotate(), we need to talk about radians. Canvas measures all angles in radians, not degrees. This trips up almost every beginner.

Degrees vs Radians, a quick conversion chart:

Degrees │ Radians          │ In code
────────┼──────────────────┼──────────────────────
   0°   │ 0                │ 0
  30°   │ PI / 6  ≈ 0.524  │ Math.PI / 6
  45°   │ PI / 4  ≈ 0.785  │ Math.PI / 4
  60°   │ PI / 3  ≈ 1.047  │ Math.PI / 3
  90°   │ PI / 2  ≈ 1.571  │ Math.PI / 2
 120°   │ 2*PI/3  ≈ 2.094  │ 2 * Math.PI / 3
 180°   │ PI      ≈ 3.142  │ Math.PI
 270°   │ 3*PI/2  ≈ 4.712  │ 3 * Math.PI / 2
 360°   │ 2 * PI  ≈ 6.283  │ Math.PI * 2

To convert: radians = degrees * (Math.PI / 180)
            degrees = radians * (180 / Math.PI)

ctx.rotate(angle) spins the coordinate system around the current origin. Positive angles go clockwise (because y points down). This is opposite to math class!

Several demos in this lesson use ctx.globalAlpha to make shapes semi-transparent. Setting globalAlpha to a value between 0 (fully transparent) and 1 (fully opaque) affects all subsequent drawing until you change it back or call restore().

The Canvas unit circle (y-axis is flipped!):

                270° (-PI/2)
                      │
                      │  ↺ negative
                      │    (counter-clockwise)
                      │
180° (PI) ────────────●──────────── 0° (0)
                      │
                      │  ↻ positive
                      │    (clockwise)
                      │
                  90° (PI/2)

NOTE: In Canvas, positive rotation = clockwise
(opposite of standard math, because Y points down)
rotate: How Rotation Works paused

Critical pattern: rotating around a specific point. Rotation always happens around the current origin. So the pattern is always: (1) translate to the center of what you want to rotate, (2) rotate, (3) draw centered at (0,0). If you rotate without translating first, everything spins around the top-left corner of the canvas!

rotate: A Fan of Lines from a Center Point paused

scale(sx, sy): Resizing the Coordinate System

ctx.scale(sx, sy) stretches or shrinks the coordinate system. scale(2, 2) makes everything drawn after it twice as large. scale(0.5, 0.5) makes it half size. You can scale x and y independently to create stretching effects.

scale: Growing and Shrinking paused

Negative Scaling: Mirroring and Flipping

A negative scale value flips the coordinate system along that axis. scale(-1, 1) mirrors horizontally (like looking in a mirror), and scale(1, -1) flips vertically. This is incredibly useful for game sprites; draw a character facing right once, then flip it when they walk left.

Negative Scale: Mirroring paused

save() and restore(): The State Stack

ctx.save() takes a snapshot of the current state and pushes it onto a stack. ctx.restore() pops the most recent snapshot and restores everything. The “state” includes: transforms, fill/stroke styles, font, globalAlpha, clipping region, line width/dash, shadow settings, and more.

Think of it like an undo stack:

 save()         save()         restore()       restore()
   │               │               │               │
   ▼               ▼               ▼               ▼
┌───────┐      ┌───────┐      ┌───────┐      ┌───────┐
│State A│      │State B│      │       │      │       │
├───────┤      ├───────┤      ├───────┤      ├───────┤
│       │      │State A│      │State A│      │       │
└───────┘      └───────┘      └───────┘      └───────┘
 Stack: [A]    Stack: [A,B]   Stack: [A]     Stack: []
                              (B popped)     (A popped)

Golden rule: Every save() MUST have a matching restore(). Unmatched pairs are the #1 source of transform bugs.

save/restore: Preventing Transform Leaks paused

Transform Order Matters!

This is the single most confusing thing about transforms: the order you apply transforms changes the result. “Translate then rotate” is NOT the same as “rotate then translate.” Think of each transform as acting on the already-transformed coordinate system.

Visualizing transform order:

Translate-then-Rotate:            Rotate-then-Translate:
1. Move origin to (200, 100)      1. Rotate axes 45°
2. Rotate axes 45°                2. Move along NEW rotated axes
3. Draw                           3. Draw

┌────────────────────┐            ┌────────────────────┐
│                    │            │     ◇              │
│        ◇           │            │    ╱               │
│     tilted at      │            │   ╱ moved along    │
│     (200,100)      │            │  ╱  rotated axis   │
│                    │            │ ╱                  │
└────────────────────┘            └────────────────────┘

The difference: "translate" moves along the CURRENT axes.
After a rotate, the axes point in a different direction!
Transform Order: Same Operations, Different Results paused

setTransform() vs transform(): Reset vs Accumulate

Canvas gives you three lower-level transform methods:

  • ctx.transform(a, b, c, d, e, f) multiplies the current transform by a new one (they accumulate)
  • ctx.setTransform(a, b, c, d, e, f) replaces the current transform entirely (resets first)
  • ctx.getTransform() returns the current transform as a DOMMatrix object (read-only)

The 2D transformation matrix:

Canvas uses a 3x3 matrix internally, but you only set 6 values:

┌          ┐     ┌                                ┐
│  a  c  e  │     │  scaleX   skewX    translateX  │
│  b  d  f  │  =  │  skewY    scaleY   translateY  │
│  0  0  1  │     │  0        0        1           │
└          ┘     └                                ┘

The default (identity) matrix: a=1, b=0, c=0, d=1, e=0, f=0
ctx.setTransform(1, 0, 0, 1, 0, 0) resets to default.

What the convenient methods do internally:
  translate(tx, ty)  →  transform(1, 0, 0, 1, tx, ty)
  rotate(angle)      →  transform(cos, sin, -sin, cos, 0, 0)
  scale(sx, sy)      →  transform(sx, 0, 0, sy, 0, 0)
setTransform vs transform: Reset vs Accumulate paused

Common Transform Mistakes

Every beginner hits these. Learn them now so you can spot them immediately.

Mistake #1, forgetting save/restore: Transforms accumulate. If you translate(100, 0) in a loop without restoring, each iteration adds another 100px. Your shapes fly off-screen.

Mistake #2, rotating without translating first: rotate() spins around the origin (0,0), which is the top-left corner by default. If you want to rotate a shape in-place, translate to its center first.

Mistake #3, wrong transform order: Transforms apply to the current coordinate system. If you rotate first, then translate, the translation follows the rotated axes.

Mistake #4, scale affects everything: scale(2, 2) doubles not just shapes, but also line widths, text sizes, and translations. If you scale(2,2) then translate(50, 0), you actually move 100 pixels.

Common Mistake: Rotate Without Translate paused

Practical: Spinning Loading Indicator

Transforms are perfect for radial patterns. This loading spinner draws 12 lines at equal angles, with one highlighted to create the spinning effect. It uses requestAnimationFrame to run a loop that redraws each frame; the next lesson covers animation in full detail.

Loading Spinner: Transforms + Animation paused

Practical: Solar System, Orbits Within Orbits

The real power of transforms shows when you nest them. Here, the Earth orbits the Sun, and the Moon orbits the Earth. Each orbit is just a rotate + translate inside the parent’s coordinate system.

Note: This demo advances by a fixed amount each frame for simplicity. On a 120Hz screen it will spin faster. Lesson 6 (Animation) teaches delta-time, the proper way to make motion frame-rate independent.

Solar System: Nested Transforms paused

Practical: Fractal Tree with Recursive Transforms

Recursion + transforms = fractals. Each branch translates to its end, rotates, scales down, and draws another branch. The tree emerges from simple rules repeated.

Fractal Tree: Recursive Transforms paused

Practical: Tiling Pattern

Transforms make it easy to create repeating patterns. Define a tile at the origin, then translate to stamp it across a grid. Add rotation for visual interest.

Tiling Pattern with Transforms paused

Transform Sandbox: Step-by-Step Visualization

This demo shows how a sequence of transforms builds up. Each colored rectangle shows the coordinate system after one more transform is applied. Watch how the origin (dot) moves and the axes rotate.

Transform Pipeline: Visualizing Each Step paused

Practical: Analog Clock

Let’s combine everything into a detailed clock face. This uses translate for centering, rotate for each hour/minute mark, and save/restore to keep each element independent.

Analog Clock: Putting It All Together paused

What you learned in this lesson:

  • Coordinate system: Origin at top-left, y goes down, x goes right
  • translate(x, y): Slides the origin, the “moving the paper” concept
  • rotate(radians): Spins the coordinate system around the current origin (clockwise = positive)
  • scale(sx, sy): Stretches/shrinks; negative values mirror/flip
  • save()/restore(): Stack-based state management; always pair them
  • Transform order matters: Each transform acts on the already-transformed system
  • setTransform() vs transform(): Reset-and-set vs accumulate
  • The 2x3 matrix: [scaleX, skewY, skewX, scaleY, translateX, translateY]
  • Nested transforms: Inner coordinate systems ride on outer ones (solar system pattern)
  • Recursive transforms: Fractals emerge from repeated translate+rotate+scale