Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Computer Graphics (rasterisation, ray tracing, WebGPU)  Module 4 – 2D transformations, viewing and clipping

2D transformations and composite transforms

Translate, scale, rotate, reflect and shear with 3 × 3 matrices, turn about any pivot, split a matrix into its parts and build an eight-fold pattern.

  • Beginner
  • 25 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Apply translation, scaling, rotation, reflection and shear as 3 × 3 homogeneous matrices
  • Compose transformations about an arbitrary pivot and explain why their order matters
  • Decompose an affine matrix into translation, rotation, shear and scale
  • Build rotationally symmetric patterns from composed rotations and reflections

Before you start

What you’ll learn · Before you start

What you will learn

  • Apply translation, scaling, rotation, reflection and shear as 3 × 3 homogeneous matrices
  • Compose transformations about an arbitrary pivot and explain why their order matters
  • Decompose an affine matrix into translation, rotation, shear and scale
  • Build rotationally symmetric patterns from composed rotations and reflections

Before you start

On this page

Turn the lesson’s arrow a quarter turn with the plain rotation matrix, and the middle of its tail, which sat at (1, 2), lands at (−2, 1): the arrow swings round a corner of the drawing instead of turning where it is. Every move a drawing program offers, from dragging and stretching to mirroring and slanting, is a 3 × 3 matrix, and many transform bugs come down to two questions: which point stays fixed, and in which order the matrices act.

You will apply the five basic matrices to that arrow and measure what each does to its area, turn and mirror it about any point and any line, find which pairs of matrices can swap places, read the six numbers of a canvas transform, split any matrix back into its parts, and grow an eight-fold pattern from one petal. Every number comes from a program that runs on this page.

Five matrices move every point of a shape

The arrow of this lesson has seven corners, from (1, 1) round to its tip at (6, 2), and covers an area of 10. A transformation moves every point of it, and in homogeneous coordinates: Coordinates with an extra component w. Points have w = 1, so a 4 × 4 matrix can translate them; directions have w = 0, so translation leaves them alone. More in “Matrices and homogeneous coordinates” each of the five basic ones is a 3 × 3 matrix: a point (x, y) is the column (x, y, 1), and its image is the matrix times that column. The bottom row of every matrix here is (0, 0, 1), so the 1 stays a 1.

T(tx,ty)=(10tx01ty001)T(t_x, t_y) = \begin{pmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{pmatrix} S(sx,sy)=(sx000sy0001)S(s_x, s_y) = \begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{pmatrix}

The translation adds its offsets to x and y, which the 1 in the column makes possible, and the scale multiplies x and y by its two factors. T(−3, 1) moves the tip (6, 2) to (3, 3); S(2, 1.5) sends it to (12, 3).

R(θ)=(cos⁡θ−sin⁡θ0sin⁡θcos⁡θ0001)R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{pmatrix}

The rotation turns every point by θ about the origin, counter-clockwise when y points up: its first column, (cos θ, sin θ), is where the x axis goes. R(90°) sends the tip (6, 2) to (−2, 6).

F=(1000−10001)F = \begin{pmatrix} 1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{pmatrix} Hx(k)=(1k0010001)H_x(k) = \begin{pmatrix} 1 & k & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix}

The reflection: A transformation that mirrors every point in a line and keeps the points of the line itself fixed. Its matrix has determinant −1, and applying it twice gives the identity. F mirrors in the x axis by negating y, so the tip goes to (6, −2). The shear: A transformation that slides each point parallel to one axis by an amount proportional to its other coordinate, as in x′ = x + k·y. It keeps areas and turns rectangles into parallelograms. slides each point along x by k times its height: with k = 0.5 the tip, at height 2, moves 1 to the right, to (7, 2). The figure shows the whole arrow under each of the five:

Six panels: an arrow, then its images under a translation, a scale, a 90° rotation, a mirror in the x axis and a shear.The arrow under the five basic matricesThe four linear matrices keep the origin fixedThe arrowtranslate by (−3, 1)scale by (2, 1.5)rotate by 90°reflect in the x axisshear x by 0.5 ythe arrowits image, tip markedthe origin (0, 0)

The lesson's arrow under the five basic matrices

Text description of the diagram

Six small panels in two columns, each with the x and y axes drawn faintly and the origin (0, 0) marked with a dot.

The first panel shows the arrow itself: seven corners from (1, 1) to the tip (6, 2), pointing right, to the right of the y axis, with one corner, (4, 0), on the x axis.

The other five panels each show the arrow dashed and its image solid, with the image's tip marked. Translate by (−3, 1): the same arrow, moved left by 3 and up by 1, so that it now crosses the y axis. Scale by (2, 1.5): the arrow twice as long and one and a half times as tall, reaching x = 12, moved away from the origin because every coordinate was multiplied. Rotate by 90°: the arrow turned a quarter turn about the origin, now pointing up and lying to the left of the y axis. Reflect in the x axis: the arrow flipped below the x axis, still pointing right. Shear x by 0.5 y: the arrow slanted to the right, each point moved along x by half its height: the top corner (4, 4) moved 2 to the right, and the corner on the x axis, (4, 0), stayed put.

The legend under the panels names the dashed outline as the arrow, the solid one as its image with the tip marked, and the dot as the origin.

Four of them keep the origin where it is, which makes them linear transformation: A map that keeps the origin in place and preserves sums and scaling. Rotations, scalings and shears are linear; translations are not. More in “Matrices and homogeneous coordinates”; the translation moves it, so any product that includes one is an affine transformation: A linear transformation followed by a translation. It keeps straight lines straight and parallel lines parallel. More in “Matrices and homogeneous coordinates”. The program applies all five and measures the arrow’s signed area with the shoelace formula: The area of a polygon from its corners: half the sum of x_i y_(i+1) − x_(i+1) y_i round the outline. It is positive when the corners run counter-clockwise. More in “Polygon clipping: Sutherland-Hodgman and beyond”, half the sum of xiyi+1−xi+1yix_i y_{i+1} - x_{i+1} y_i taken round the outline:

The five basic matrices applied to the arrow JavaScript · basic/transform2d.mjs
// The five basic 2D transformations as 3 x 3 matrices in homogeneous coordinates.
// A point (x, y) is the column (x, y, 1), and p' = M p. Each matrix is applied to
// the lesson's arrow, and the shoelace formula measures the arrow's signed area.

const ARROW = [[1, 1], [4, 1], [4, 0], [6, 2], [4, 4], [4, 3], [1, 3]];

const translate = (tx, ty) => [[1, 0, tx], [0, 1, ty], [0, 0, 1]];
const scale = (sx, sy) => [[sx, 0, 0], [0, sy, 0], [0, 0, 1]];
const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  const c = Math.cos(r);
  const s = Math.sin(r);
  return [[c, -s, 0], [s, c, 0], [0, 0, 1]];
};
const reflectInXAxis = () => [[1, 0, 0], [0, -1, 0], [0, 0, 1]];
const shearX = (k) => [[1, k, 0], [0, 1, 0], [0, 0, 1]]; // x moves by k times y

const apply = (m, [x, y]) => [m[0][0] * x + m[0][1] * y + m[0][2], m[1][0] * x + m[1][1] * y + m[1][2]];
const det = (m) => m[0][0] * m[1][1] - m[0][1] * m[1][0]; // of the 2 x 2 part

// Twice the signed area is the sum of x_i * y_(i+1) - x_(i+1) * y_i round the outline.
function signedArea(poly) {
  let twice = 0;
  for (let i = 0; i < poly.length; i++) {
    const [x0, y0] = poly[i];
    const [x1, y1] = poly[(i + 1) % poly.length];
    twice += x0 * y1 - x1 * y0;
  }
  return twice / 2;
}

const num = (v) => {
  const r = Number(v.toFixed(3));
  return Object.is(r, -0) ? '0' : String(r);
};
const point = ([x, y]) => `(${num(x)}, ${num(y)})`;
const row = (r) => `[${r.map(num).join(', ')}]`;

const cases = [
  ['translate by (-3, 1)', translate(-3, 1)],
  ['scale by (2, 1.5)', scale(2, 1.5)],
  ['rotate by 90 degrees', rotate(90)],
  ['reflect in the x axis', reflectInXAxis()],
  ['shear x by 0.5 y', shearX(0.5)],
];

console.log(`The arrow: ${ARROW.map(point).join(' ')}`);
console.log(`Signed area ${num(signedArea(ARROW))} (positive: its corners run counter-clockwise)`);
for (const [name, m] of cases) {
  const image = ARROW.map((p) => apply(m, p));
  console.log('');
  console.log(`${name}: rows ${m.map(row).join(' ')}`);
  console.log(`  image: ${image.map(point).join(' ')}`);
  console.log(`  determinant ${num(det(m))}, signed area ${num(signedArea(image))}`);
}

Output

The arrow: (1, 1) (4, 1) (4, 0) (6, 2) (4, 4) (4, 3) (1, 3)
Signed area 10 (positive: its corners run counter-clockwise)

translate by (-3, 1): rows [1, 0, -3] [0, 1, 1] [0, 0, 1]
  image: (-2, 2) (1, 2) (1, 1) (3, 3) (1, 5) (1, 4) (-2, 4)
  determinant 1, signed area 10

scale by (2, 1.5): rows [2, 0, 0] [0, 1.5, 0] [0, 0, 1]
  image: (2, 1.5) (8, 1.5) (8, 0) (12, 3) (8, 6) (8, 4.5) (2, 4.5)
  determinant 3, signed area 30

rotate by 90 degrees: rows [0, -1, 0] [1, 0, 0] [0, 0, 1]
  image: (-1, 1) (-1, 4) (0, 4) (-2, 6) (-4, 4) (-3, 4) (-3, 1)
  determinant 1, signed area 10

reflect in the x axis: rows [1, 0, 0] [0, -1, 0] [0, 0, 1]
  image: (1, -1) (4, -1) (4, 0) (6, -2) (4, -4) (4, -3) (1, -3)
  determinant -1, signed area -10

shear x by 0.5 y: rows [1, 0.5, 0] [0, 1, 0] [0, 0, 1]
  image: (1.5, 1) (4.5, 1) (4, 0) (7, 2) (6, 4) (5.5, 3) (2.5, 3)
  determinant 1, signed area 10

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node transform2d.mjs

The determinant: A number computed from a square matrix: the factor by which it scales areas or volumes. It is negative when the matrix mirrors and 0 when the matrix has no inverse. More in “Matrices and homogeneous coordinates” of each matrix’s 2 × 2 part predicts the new area exactly: 1 for the translation, the rotation and the shear, which all keep the area at 10, and 3 for the scale by (2, 1.5), which makes it 30. The mirror has determinant −1, and the signed area comes out as −10: the corners now run clockwise, the mark of a mirrored shape. The shear is the one to watch. It keeps the area and still changes every angle, turning the arrow’s square corners into slanted ones.

Degrees passed to Math.cos

The ECMAScript standard defines Math.cos and Math.sin with the argument in radians. A rotate(90) that skips the conversion turns by 90 radians, about 5,157 degrees, which leaves the shape turned by about 117 degrees. Convert once, with degrees * Math.PI / 180, where the angle enters the program.

Checkpoint

Check: The five basic matrices

  1. Question 1 of 2 The arrow covers an area of 10. What area does it cover after the shear H_x(0.5)?

    Type a number.

    A hint

    Look at the determinant of the shear's 2 × 2 part.

    Show the answer: Question 1 of 2

    Answer: 10 square units

    The shear's 2 × 2 part has rows [1, 0.5] and [0, 1], whose determinant is 1 × 1 − 0.5 × 0 = 1, so the area stays 10. The program measured exactly that, although every angle of the arrow changed.

  2. Question 2 of 2 After the mirror in the x axis, the arrow's signed area is −10 instead of 10. What does the minus sign tell you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The corners now run clockwise, the mark of a mirrored shape

    The determinant of F is −1: the area keeps its size and the orientation flips, so the corners that ran counter-clockwise now run clockwise.

Turn, scale or mirror about any point with the same sandwich

R(90°) on its own turned the arrow about the origin, so the middle of its tail, the pivot: The point that a rotation or a scale keeps fixed. Transforming about a pivot p means moving p to the origin, transforming there, and moving it back: T(p) M T(−p). (1, 2), ended at (−2, 1) and the arrow left its place. To turn about a pivot p instead, move p to the origin, turn, and move it back:

Mp=T(p)MT(−p)M_p = T(p)\, M\, T(-p)

Read it from right to left, in the order a point meets the matrices: T(−p) subtracts the pivot, M turns about the origin, which is now where the pivot is, and T(p) adds the pivot back. For the tip (6, 2) and p = (1, 2) the three steps give (5, 0), (0, 5) and (1, 7). Step through both ways of turning the arrow:

Interactive · step by step A quarter turn about the origin, and about the pivot Step through both columns. Each row applies one more matrix to the arrow's tip; watch where the pivot (1, 2) ends up in each.
A quarter turn about the origin, and about the pivot, step 8 of 8. Showing tip (6, 2), tip (6, 2), tip at (−2, 6), (5, 0), pivot at (−2, 1), (0, 5), the arrow orbits (0, 0), tip at (1, 7). In focus: the arrow orbits (0, 0), tip at (1, 7).tip (6, 2)R(90°) alonetip (6, 2)T(1, 2) R(90°) T(−1, −2)tip at (−2, 6)turned about (0, 0)(5, 0)T(−1, −2): pivot to originpivot at (−2, 1)the tail moved as well(0, 5)R(90°) turns itthe arrow orbits (0, 0)it has left its placetip at (1, 7)T(1, 2): moved back

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Two ways to turn the arrow. Both columns follow the arrow's tip, (6, 2), through a quarter turn. On the left the rotation R(90°) acts alone; on the right it sits between two translations that take the pivot (1, 2) to the origin and back.
  2. R alone turns about (0, 0). R(90°) sends (x, y) to (−y, x), so the tip lands at (−2, 6). A rotation matrix always turns about the origin, wherever the shape happens to be.
  3. The pivot moves too. The same matrix sends the pivot (1, 2) to (−2, 1). The tail has left its place, so the arrow did not turn where it was.
  4. Swung round the origin. Every point of the arrow travelled a quarter circle about (0, 0). This is the bug of a rotate tool that forgets the pivot.
  5. Pivot to the origin first. On the right, T(−1, −2) subtracts the pivot. The tip becomes (5, 0), its offset from the pivot, and the pivot itself is now at the origin.
  6. Turn about the moved pivot. R(90°) turns (5, 0) into (0, 5). Turning about the origin is now turning about the pivot, because the pivot is at the origin.
  7. Move back. T(1, 2) adds the pivot back. The tip ends at (1, 7), five units straight above the pivot, and the pivot (1, 2) is exactly where it started.
  8. One matrix for the sandwich. Multiplied out, T(1, 2) R(90°) T(−1, −2) is the single matrix with rows [0, −1, 3] and [1, 0, 1]. The left column's arrow orbits the origin; the right column's turns in place.

The same sandwich scales about a point and mirrors in any line, because whatever must stay fixed goes to the origin first and comes back last. A mirror in the line y = 2x + 1 needs one more pair of matrices: move the line’s point (0, 1) to the origin, turn the line onto the x axis with R(−θ), mirror with F, and undo both, where θ = 63.435° is the line’s angle. The program builds all three composite transformation: One matrix made by multiplying several transformation matrices, so that a whole chain of moves is applied at once. With column vectors the rightmost factor acts on a point first. and checks the mirror against its exact matrix:

Rotate, scale and mirror about points and lines JavaScript · pivot/pivot.mjs
// Transforming about a point other than the origin: move the point to the origin,
// transform, and move it back. With column vectors the product is written
// T(p) M T(-p), and the rightmost matrix acts first.

const ARROW = [[1, 1], [4, 1], [4, 0], [6, 2], [4, 4], [4, 3], [1, 3]];

const translate = (tx, ty) => [[1, 0, tx], [0, 1, ty], [0, 0, 1]];
const scale = (sx, sy) => [[sx, 0, 0], [0, sy, 0], [0, 0, 1]];
const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const rotateRadians = (r) => [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
const reflectInXAxis = () => [[1, 0, 0], [0, -1, 0], [0, 0, 1]];
const IDENTITY = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];

function multiply(a, b) {
  const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) out[i][j] += a[i][k] * b[k][j];
  return out;
}
const compose = (...ms) => ms.reduce(multiply); // compose(A, B, C) = A B C: C acts first
const apply = (m, [x, y]) => [m[0][0] * x + m[0][1] * y + m[0][2], m[1][0] * x + m[1][1] * y + m[1][2]];
const maxDiff = (a, b) => Math.max(...a.flatMap((r, i) => r.map((v, j) => Math.abs(v - b[i][j]))));

const num = (v) => {
  const r = Number(v.toFixed(3));
  return Object.is(r, -0) ? '0' : String(r);
};
const point = ([x, y]) => `(${num(x)}, ${num(y)})`;
const rows = (m) => m.map((r) => `[${r.map(num).join(', ')}]`).join(' ');

const pivot = [1, 2]; // the middle of the arrow's tail
const aboutOrigin = rotate(90);
const aboutPivot = compose(translate(...pivot), rotate(90), translate(-pivot[0], -pivot[1]));
console.log('Rotate the arrow by 90 degrees');
console.log(`  about the origin:  ${ARROW.map((p) => point(apply(aboutOrigin, p))).join(' ')}`);
console.log(`  about (1, 2):      ${ARROW.map((p) => point(apply(aboutPivot, p))).join(' ')}`);
console.log(`  T(1, 2) R(90) T(-1, -2) = ${rows(aboutPivot)}`);
console.log(`  the pivot (1, 2) goes to ${point(apply(aboutPivot, pivot))}; the tip (6, 2) goes to ${point(apply(aboutPivot, [6, 2]))}`);

const tip = [6, 2];
const grow = compose(translate(...tip), scale(2, 2), translate(-tip[0], -tip[1]));
console.log('');
console.log('Scale the arrow by 2 about its tip (6, 2)');
console.log(`  image: ${ARROW.map((p) => point(apply(grow, p))).join(' ')}`);
console.log(`  T(6, 2) S(2, 2) T(-6, -2) = ${rows(grow)}`);

// Mirror in the line y = 2x + 1: move a point of the line, (0, 1), to the origin,
// turn the line onto the x axis, mirror, and undo both.
const theta = Math.atan2(2, 1); // the line's direction (1, 2)
const mirror = compose(translate(0, 1), rotateRadians(theta), reflectInXAxis(), rotateRadians(-theta), translate(0, -1));
const exact = [[-0.6, 0.8, -0.8], [0.8, 0.6, 0.4], [0, 0, 1]];
console.log('');
console.log(`Reflect in the line y = 2x + 1 (the line turns ${num((theta * 180) / Math.PI)} degrees from the x axis)`);
console.log(`  T(0, 1) R(t) F R(-t) T(0, -1) = ${rows(mirror)}`);
console.log(`  equal to the exact matrix ${rows(exact)} within 1e-12: ${maxDiff(mirror, exact) < 1e-12 ? 'yes' : 'no'}`);
console.log(`  points of the line stay put: (1, 3) -> ${point(apply(mirror, [1, 3]))}, (-1, -1) -> ${point(apply(mirror, [-1, -1]))}`);
console.log(`  the origin goes to ${point(apply(mirror, [0, 0]))}; mirroring twice gives the identity: ${maxDiff(multiply(mirror, mirror), IDENTITY) < 1e-12 ? 'yes' : 'no'}`);
console.log(`  image of the arrow: ${ARROW.map((p) => point(apply(mirror, p))).join(' ')}`);

Output

Rotate the arrow by 90 degrees
  about the origin:  (-1, 1) (-1, 4) (0, 4) (-2, 6) (-4, 4) (-3, 4) (-3, 1)
  about (1, 2):      (2, 2) (2, 5) (3, 5) (1, 7) (-1, 5) (0, 5) (0, 2)
  T(1, 2) R(90) T(-1, -2) = [0, -1, 3] [1, 0, 1] [0, 0, 1]
  the pivot (1, 2) goes to (1, 2); the tip (6, 2) goes to (1, 7)

Scale the arrow by 2 about its tip (6, 2)
  image: (-4, 0) (2, 0) (2, -2) (6, 2) (2, 6) (2, 4) (-4, 4)
  T(6, 2) S(2, 2) T(-6, -2) = [2, 0, -6] [0, 2, -2] [0, 0, 1]

Reflect in the line y = 2x + 1 (the line turns 63.435 degrees from the x axis)
  T(0, 1) R(t) F R(-t) T(0, -1) = [-0.6, 0.8, -0.8] [0.8, 0.6, 0.4] [0, 0, 1]
  equal to the exact matrix [-0.6, 0.8, -0.8] [0.8, 0.6, 0.4] [0, 0, 1] within 1e-12: yes
  points of the line stay put: (1, 3) -> (1, 3), (-1, -1) -> (-1, -1)
  the origin goes to (-0.8, 0.4); mirroring twice gives the identity: yes
  image of the arrow: (-0.6, 1.8) (-2.4, 4.2) (-3.2, 3.6) (-2.8, 6.4) (0, 6) (-0.8, 5.4) (1, 3)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pivot.mjs

The pivot (1, 2) stays put and the tip lands at (1, 7), from one matrix with rows [0, −1, 3] and [1, 0, 1]. Its last column, (3, 1), is the translation the sandwich adds so that the pivot does not move. The scale by 2 about the tip leaves (6, 2) fixed and doubles every distance from it. The mirror’s five matrices multiply out to rows [−0.6, 0.8, −0.8] and [0.8, 0.6, 0.4], which is exact: a mirror in a line through the origin at angle θ has the 2 × 2 part [cos 2θ, sin 2θ; sin 2θ, −cos 2θ], and with tan θ = 2 those are −0.6 and 0.8. The program’s two checks are the ones to keep for any mirror: points of the line, such as (1, 3), stay where they are, and mirroring twice gives the identity.

Key idea

To transform about a point, put it at the origin first and back last: T(p) M T(−p). A mirror in any line is the same sandwich with a turn on each side, T R(θ) F R(−θ) T⁻¹.

Checkpoint

Check: Transforming about a pivot or a line

  1. Question 1 of 2 With column vectors, which product turns a shape by θ about the point p?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: T(p) R(θ) T(−p)

    Read the product right to left: T(−p) puts the pivot at the origin, R(θ) turns about it, T(p) puts it back. For a quarter turn about (1, 2) the product has rows [0, −1, 3] and [1, 0, 1].

  2. Question 2 of 2 Rotate by 90° about the pivot (2, 0). Where does the origin (0, 0) go?

    Choose one answer.

    A hint

    Subtract the pivot, turn, add the pivot back.

    Show the answer: Question 2 of 2

    Answer: (2, −2)

    The sandwich T(2, 0) R(90°) T(−2, 0) moves (0, 0) to (−2, 0), turns it to (0, −2) and moves it back to (2, −2), a quarter circle round the pivot.

Order matters: most pairs do not commute

Rotate the unit square by 30° and stretch it by (2, 1), and the result depends on which comes first. Scale first, R(30°) S(2, 1), and the square becomes a 2 × 1 rectangle turned by 30°. Rotate first, S(2, 1) R(30°), and the stretch pulls along the world’s x axis, which runs diagonally across the turned square, so its corners stop being right angles. The program measures that, and multiplies nine pairs of matrices both ways:

Nine pairs multiplied both ways, and a square in two orders JavaScript · order/order.mjs
// Which pairs of transformations commute (A B = B A) and which do not? Each pair is
// multiplied both ways and the largest difference between the two products is printed.
// Then a unit square shows what the wrong order does to a shape.

const translate = (tx, ty) => [[1, 0, tx], [0, 1, ty], [0, 0, 1]];
const scale = (sx, sy) => [[sx, 0, 0], [0, sy, 0], [0, 0, 1]];
const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const shearX = (k) => [[1, k, 0], [0, 1, 0], [0, 0, 1]];
const shearY = (k) => [[1, 0, 0], [k, 1, 0], [0, 0, 1]];
const reflectInXAxis = () => [[1, 0, 0], [0, -1, 0], [0, 0, 1]];

function multiply(a, b) {
  const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) out[i][j] += a[i][k] * b[k][j];
  return out;
}
const maxDiff = (a, b) => Math.max(...a.flatMap((r, i) => r.map((v, j) => Math.abs(v - b[i][j]))));
const num = (v, places = 3) => {
  const r = Number(v.toFixed(places));
  return Object.is(r, -0) ? '0' : String(r);
};

const pairs = [
  ['R(30) and R(50)', rotate(30), rotate(50)],
  ['T(3, 0) and T(0, 5)', translate(3, 0), translate(0, 5)],
  ['S(2, 1) and S(1, 3)', scale(2, 1), scale(1, 3)],
  ['R(30) and S(2, 2), a uniform scale', rotate(30), scale(2, 2)],
  ['R(30) and S(2, 1)', rotate(30), scale(2, 1)],
  ['R(30) and shear x by 0.5', rotate(30), shearX(0.5)],
  ['shear x by 0.5 and shear y by 0.5', shearX(0.5), shearY(0.5)],
  ['T(3, 0) and R(90)', translate(3, 0), rotate(90)],
  ['F (mirror in x) and R(30)', reflectInXAxis(), rotate(30)],
];
console.log('pair                                  largest |AB - BA|  commute?');
for (const [name, a, b] of pairs) {
  const d = maxDiff(multiply(a, b), multiply(b, a));
  console.log(`${name.padEnd(38)}${num(d).padEnd(19)}${d < 1e-12 ? 'yes' : 'no'}`);
}

// The unit square's sides are the x and y axes. After a matrix they are its first two columns.
function describe(name, m) {
  const ex = [m[0][0], m[1][0]];
  const ey = [m[0][1], m[1][1]];
  const len = ([x, y]) => Math.hypot(x, y);
  const angle = (Math.acos((ex[0] * ey[0] + ex[1] * ey[1]) / (len(ex) * len(ey))) * 180) / Math.PI;
  console.log(`${name}: sides ${num(len(ex))} and ${num(len(ey))}, corner angle ${num(angle, 1)} degrees`);
}
console.log('');
console.log('The unit square, rotated by 30 degrees and scaled by (2, 1):');
describe('  R(30) S(2, 1), scale first ', multiply(rotate(30), scale(2, 1)));
describe('  S(2, 1) R(30), rotate first', multiply(scale(2, 1), rotate(30)));

Output

pair                                  largest |AB - BA|  commute?
R(30) and R(50)                       0                  yes
T(3, 0) and T(0, 5)                   0                  yes
S(2, 1) and S(1, 3)                   0                  yes
R(30) and S(2, 2), a uniform scale    0                  yes
R(30) and S(2, 1)                     0.5                no
R(30) and shear x by 0.5              0.25               no
shear x by 0.5 and shear y by 0.5     0.25               no
T(3, 0) and R(90)                     3                  no
F (mirror in x) and R(30)             1                  no

The unit square, rotated by 30 degrees and scaled by (2, 1):
  R(30) S(2, 1), scale first : sides 2 and 1, corner angle 90 degrees
  S(2, 1) R(30), rotate first: sides 1.803 and 1.323, corner angle 123 degrees

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node order.mjs

Four pairs commute: two rotations, two translations, two scales, and a rotation with a uniform scale. Five do not, and the size of the difference shows how much: a rotation and a non-uniform scale differ by 0.5, a rotation and a shear by 0.25, the two shears by 0.25, a translation and a rotation by 3, the full length of the move, and the mirror and a rotation by 1. The square makes the cost visible. In the order S(2, 1) R(30°) its sides are 1.803 and 1.323 and its corner is 123 degrees: a parallelogram. A shear has appeared that neither matrix contained on its own, and the decomposition section below takes it apart.

Pair Commute? Why
Two rotations, two translations, two scales Yes Angles add, offsets add, factors multiply, in any order
Rotation and uniform scale Yes A uniform scale stretches every direction alike
Rotation and non-uniform scale No The stretch acts along fixed axes, before or after the turn
Translation and rotation No A rotation turns the offset as well, unless the move comes after it
Mirror and rotation No F R(θ) = R(−θ) F: the mirror reverses the turn

Test a composite with numbers

Build both orders and compare their products, or apply both to two or three points, before trusting an argument about which order is right. A test that multiplies a shear, a rotation and a non-uniform scale in every order catches what intuition misses.

Checkpoint

Check: Which transformations commute

  1. Question 1 of 2 Which of these pairs commute, giving the same product in either order?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • R(30°) and R(50°)
    • R(30°) and the uniform scale S(2, 2)

    Rotations commute with each other and with uniform scales. Non-uniform scales, shears, translations and mirrors do not commute with a rotation.

  2. Question 2 of 2 A unit square is turned by 30° and stretched by (2, 1). Which product leaves it a rectangle?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: R(30°) S(2, 1), which stretches first and turns after

    In R(30°) S(2, 1) the scale acts first, along the square's sides. In S(2, 1) R(30°) it acts after the turn, and the program measured sides of 1.803 and 1.323 with a 123 degree corner.

On a canvas, y points down, so positive angles turn clockwise

The maths above, glTF files and most plots put y upwards; glTF’s specification defines +Y as up. A canvas does the opposite: the HTML standard puts the origin of its coordinate space in the top left corner, x increasing rightwards and y increasing down the screen, and it calls the argument of rotate() “a clockwise rotation angle expressed in radians”. Nothing in the matrix changes. The same numbers that turn a point counter-clockwise on paper turn it clockwise on the screen, because the screen’s y axis is upside down:

The same matrix turns the arrow counter-clockwise when y points up and clockwise when y points down, as on a canvas.One matrix, two directions of yT(1, 2) R(90°) T(−1, −2) on bothy up: maths, glTFxycounter-clockwisey down: canvas, SVGxyclockwisedashed: the arrow · solid: turned · dot: pivotThe tip (6, 2) goes to (1, 7) on both.

One matrix, two directions of y

Text description of the diagram

Two panels side by side apply the same matrix, T(1, 2) R(90°) T(−1, −2), to the lesson's arrow, with the pivot (1, 2) marked by a dot.

In the left panel, titled "y up: maths, glTF", the y axis points up. The dashed arrow points right; the solid arrow, its image, points straight up from the pivot. A curved arrow from the old tip to the new one sweeps counter-clockwise.

In the right panel, titled "y down: canvas, SVG", the y axis points down, as in a canvas or an SVG drawing. The same numbers put the image's tip at (1, 7), which is now below the pivot, so the solid arrow points down the page and the curved arrow sweeps clockwise.

The text under the panels says that the tip (6, 2) goes to (1, 7) in both panels: only the direction of the y axis differs.

A canvas keeps its current matrix as six numbers. setTransform(a, b, c, d, e, f) sets it to the matrix with rows [a, c, e] and [b, d, f], so the arguments run down the columns, and transform() multiplies the current matrix by a new one on the right. The standard therefore says the transformations are performed in reverse order: the call made last acts on the drawing first. The program follows those rules with plain arithmetic, not a browser canvas:

A canvas's matrix, by the HTML standard's rules JavaScript · canvas/canvas_matrix.mjs
// A model of the current transformation matrix of a canvas 2D context, following the
// HTML standard's rules: setTransform(a, b, c, d, e, f) sets the matrix
//   [a c e]
//   [b d f]
//   [0 0 1]
// and transform(a, b, c, d, e, f) multiplies the current matrix by that one on the
// right, so the call made last acts on the drawing coordinates first. This is plain
// arithmetic, not a browser canvas.

class MatrixState {
  constructor() {
    this.m = [1, 0, 0, 1, 0, 0]; // a, b, c, d, e, f: the identity
  }
  setTransform(a, b, c, d, e, f) {
    this.m = [a, b, c, d, e, f];
  }
  transform(a2, b2, c2, d2, e2, f2) {
    const [a, b, c, d, e, f] = this.m; // current x new, with the 3 x 3 layout above
    this.m = [a * a2 + c * b2, b * a2 + d * b2, a * c2 + c * d2, b * c2 + d * d2, a * e2 + c * f2 + e, b * e2 + d * f2 + f];
  }
  translate(x, y) {
    this.transform(1, 0, 0, 1, x, y);
  }
  rotate(angle) {
    this.transform(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0);
  }
  scale(x, y) {
    this.transform(x, 0, 0, y, 0, 0);
  }
  apply([x, y]) {
    const [a, b, c, d, e, f] = this.m;
    return [a * x + c * y + e, b * x + d * y + f];
  }
}

const num = (v) => {
  const r = Number(v.toFixed(3));
  return Object.is(r, -0) ? '0' : String(r);
};
const point = ([x, y]) => `(${num(x)}, ${num(y)})`;

// Turn about the pivot (1, 2): the calls read like T(1, 2) R T(-1, -2), left to right.
const ctx = new MatrixState();
ctx.translate(1, 2);
ctx.rotate(Math.PI / 2);
ctx.translate(-1, -2);
console.log(`translate(1, 2); rotate(pi / 2); translate(-1, -2) leaves a, b, c, d, e, f = ${ctx.m.map(num).join(', ')}`);
console.log(`the same six numbers as the matrix T(1, 2) R(90) T(-1, -2): rows [0, -1, 3] and [1, 0, 1]`);

const pivot = [1, 2];
const tip = [6, 2];
const turned = ctx.apply(tip);
console.log(`the tip ${point(tip)} goes to ${point(turned)}; the pivot stays at ${point(ctx.apply(pivot))}`);
console.log(`on a canvas y grows downwards, so ${point(turned)} is ${num(turned[1] - pivot[1])} pixels below the pivot:`);
console.log('the arrow that pointed right now points down the screen, a clockwise quarter turn');

// The six arguments go column by column. Passing the rows as they are printed puts
// numbers in the wrong places.
const byRows = new MatrixState();
byRows.setTransform(0, -1, 3, 1, 0, 1);
console.log(`setTransform(0, -1, 3, 1, 0, 1), the rows in reading order, sends the tip to ${point(byRows.apply(tip))}`);
const byColumns = new MatrixState();
byColumns.setTransform(0, 1, -1, 0, 3, 1);
console.log(`setTransform(0, 1, -1, 0, 3, 1), column by column, sends it to ${point(byColumns.apply(tip))}`);

// setTransform replaces the matrix; transform multiplies into it.
const a = new MatrixState();
a.setTransform(2, 0, 0, 2, 0, 0);
a.setTransform(1, 0, 0, 1, 10, 0);
const b = new MatrixState();
b.transform(2, 0, 0, 2, 0, 0);
b.transform(1, 0, 0, 1, 10, 0);
console.log('');
console.log(`setTransform twice: (1, 1) goes to ${point(a.apply([1, 1]))}, the second call replaced the scale`);
console.log(`transform twice:    (1, 1) goes to ${point(b.apply([1, 1]))}, moved by 10, then scaled by 2`);

Output

translate(1, 2); rotate(pi / 2); translate(-1, -2) leaves a, b, c, d, e, f = 0, 1, -1, 0, 3, 1
the same six numbers as the matrix T(1, 2) R(90) T(-1, -2): rows [0, -1, 3] and [1, 0, 1]
the tip (6, 2) goes to (1, 7); the pivot stays at (1, 2)
on a canvas y grows downwards, so (1, 7) is 5 pixels below the pivot:
the arrow that pointed right now points down the screen, a clockwise quarter turn
setTransform(0, -1, 3, 1, 0, 1), the rows in reading order, sends the tip to (6, -3)
setTransform(0, 1, -1, 0, 3, 1), column by column, sends it to (1, 7)

setTransform twice: (1, 1) goes to (11, 1), the second call replaced the scale
transform twice:    (1, 1) goes to (22, 2), moved by 10, then scaled by 2

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node canvas_matrix.mjs

translate(1, 2), rotate(Math.PI / 2) and translate(-1, -2) read left to right exactly like T(1, 2) R(90°) T(−1, −2), and they leave the six numbers 0, 1, −1, 0, 3, 1: the pivot rotation of the last section. On the screen the tip lands 5 pixels below the pivot, a clockwise quarter turn. The last two lines show the reverse order at work: two transform() calls, a scale by 2 and then a move by 10, send (1, 1) to (22, 2), because the move acts first.

Passing the rows to setTransform

The six arguments run down the columns: a and b are the first column, c and d the second, e and f the translation. Typing the rows in reading order, setTransform(0, -1, 3, 1, 0, 1), sends the tip to (6, −3), above the top of the canvas; setTransform(0, 1, -1, 0, 3, 1) sends it to (1, 7). The standard itself warns that the order of the second and third arguments varies from one API to another.

Checkpoint

Check: Canvas matrices and y pointing down

  1. Question 1 of 2 On a canvas, ctx.rotate(Math.PI / 2) turns what is drawn next which way on the screen?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Clockwise

    With y growing downwards, the matrix that turns counter-clockwise on paper sends a point that was to the right of the pivot to below it: in the lesson the tip ended 5 pixels below the pivot.

  2. Question 2 of 2 Which call sets a canvas to the matrix with rows [0, −1, 3] and [1, 0, 1]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: setTransform(0, 1, -1, 0, 3, 1)

    setTransform(a, b, c, d, e, f) sets the rows [a, c, e] and [b, d, f], so the six numbers are read column by column: 0, 1, −1, 0, 3, 1.

Any affine matrix splits into translation, rotation, shear and scale

An editor that shows rotation and scale fields for a selected shape, or a program that reads a matrix back from a file or a canvas, has to run the multiplication backwards. With the convention below, the x shear placed between the rotation and the scale and the first scale kept positive, every matrix whose determinant is not 0 splits in exactly one way:

M=T(tx,ty)R(θ)Hx(k)S(sx,sy),sx>0M = T(t_x, t_y)\, R(\theta)\, H_x(k)\, S(s_x, s_y), \qquad s_x > 0

Each column gives up one part. The last column is the translation. The first column is R H S applied to (1, 0), which is sx(cos⁡θ,sin⁡θ)s_x(\cos\theta, \sin\theta), so its length is the first scale and its direction is the angle. Turning the second column back by θ removes R and leaves H S applied to (0, 1), which is (ksy, sy)(k\,s_y,\ s_y): the second scale, and the shear once you divide. Step through it on S(2, 1) R(30°), the matrix that made the parallelogram:

Interactive · stage by stage Splitting S(2, 1) R(30°) into rotation, shear and scale Step through the decomposition. Each stage reads one column of the matrix and fixes one number; the last stage multiplies them back.
Splitting S(2, 1) R(30°) into rotation, shear and scale, step 5 of 5. Showing M = S(2, 1) R(30°), First column (sx = 1.803, angle 16.102°), Column 2, turned back, The four numbers (shear −0.65, sy 1.109), Multiply them back. In focus: Multiply them back.M = S(2, 1) R(30°)rotated first, then stretchedFirst columnsx = 1.803, angle 16.102°Column 2, turned backleaves (shear × sy, sy)The four numbersshear −0.65, sy 1.109Multiply them backT R H S against Mrecompose

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The matrix. M = S(2, 1) R(30°) rotates the unit square and then stretches it along x. The order section measured its corner at 123 degrees, so it is not a rotated rectangle.
  2. Length and direction. R H S sends (1, 0) to sx (cos a, sin a), so the first column alone gives sx and the angle. Here it has length 1.803 and points 16.102 degrees above the x axis.
  3. Undo the rotation. Turning the second column back by 16.102 degrees removes R. What is left is H S applied to (0, 1), which is (shear × sy, sy).
  4. Shear and second scale. The second number of that column is sy = 1.109, and the first divided by it is the shear, −0.65. The determinant sx × sy is 2, the area factor of S(2, 1).
  5. The check. Multiplied back as T R H S, the four numbers, unrounded, give M within 1e-12. The shear was there all along, hidden in the order of R and S.

The program splits four matrices and multiplies each one back:

Four matrices split into T R H S JavaScript · decompose/decompose.mjs
// Splitting an affine matrix into M = T(tx, ty) R(angle) H(shear) S(sx, sy), where H is
// the x shear [1 k; 0 1]. The convention keeps sx > 0, so a mirror shows as sy < 0.
//   1. The last column is the translation.
//   2. The first column is R H S applied to (1, 0), which is sx (cos a, sin a):
//      its length is sx and its direction the angle.
//   3. Rotating the second column back by the angle leaves (shear * sy, sy).

const translate = (tx, ty) => [[1, 0, tx], [0, 1, ty], [0, 0, 1]];
const scale = (sx, sy) => [[sx, 0, 0], [0, sy, 0], [0, 0, 1]];
const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const shearX = (k) => [[1, k, 0], [0, 1, 0], [0, 0, 1]];
function multiply(a, b) {
  const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) out[i][j] += a[i][k] * b[k][j];
  return out;
}
const compose = (...ms) => ms.reduce(multiply);
const maxDiff = (a, b) => Math.max(...a.flatMap((r, i) => r.map((v, j) => Math.abs(v - b[i][j]))));

function decompose(m) {
  const det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
  if (Math.abs(det) < 1e-12) throw new Error('the matrix flattens the plane (determinant 0): no decomposition');
  const sx = Math.hypot(m[0][0], m[1][0]);
  const angle = Math.atan2(m[1][0], m[0][0]);
  const c = Math.cos(angle);
  const s = Math.sin(angle);
  const back = [c * m[0][1] + s * m[1][1], -s * m[0][1] + c * m[1][1]]; // R(-angle) times column 2
  const sy = back[1];
  return { tx: m[0][2], ty: m[1][2], angle: (angle * 180) / Math.PI, shear: back[0] / sy, sx, sy };
}
const recompose = (d) => compose(translate(d.tx, d.ty), rotate(d.angle), shearX(d.shear), scale(d.sx, d.sy));

const num = (v) => {
  const r = Number(v.toFixed(3));
  return Object.is(r, -0) ? '0' : String(r);
};

const cases = [
  ['T(4, -1) R(30) H(0.5) S(2, 1.5)', compose(translate(4, -1), rotate(30), shearX(0.5), scale(2, 1.5))],
  ['S(2, 1) R(30), rotated first', multiply(scale(2, 1), rotate(30))],
  ['the mirror in y = 2x + 1', [[-0.6, 0.8, -0.8], [0.8, 0.6, 0.4], [0, 0, 1]]],
  ['S(1, 0), which flattens y', scale(1, 0)],
];
for (const [name, m] of cases) {
  console.log(name);
  try {
    const d = decompose(m);
    console.log(`  translation (${num(d.tx)}, ${num(d.ty)}), angle ${num(d.angle)} degrees, shear ${num(d.shear)}, scale (${num(d.sx)}, ${num(d.sy)})`);
    console.log(`  determinant sx * sy = ${num(d.sx * d.sy)}; recomposed within 1e-12: ${maxDiff(recompose(d), m) < 1e-12 ? 'yes' : 'no'}`);
  } catch (e) {
    console.log(`  refused: ${e.message}`);
  }
}

Output

T(4, -1) R(30) H(0.5) S(2, 1.5)
  translation (4, -1), angle 30 degrees, shear 0.5, scale (2, 1.5)
  determinant sx * sy = 3; recomposed within 1e-12: yes
S(2, 1) R(30), rotated first
  translation (0, 0), angle 16.102 degrees, shear -0.65, scale (1.803, 1.109)
  determinant sx * sy = 2; recomposed within 1e-12: yes
the mirror in y = 2x + 1
  translation (-0.8, 0.4), angle 126.87 degrees, shear 0, scale (1, -1)
  determinant sx * sy = -1; recomposed within 1e-12: yes
S(1, 0), which flattens y
  refused: the matrix flattens the plane (determinant 0): no decomposition

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node decompose.mjs

The first comes apart into exactly the numbers it was built from. The second shows the hidden shear: S(2, 1) R(30°) is a turn by 16.102°, a shear of −0.65 and a scale of (1.803, 1.109), and its determinant, 2, is still the area factor of S(2, 1). The mirror in y = 2x + 1 has no shear and a second scale of −1: it is the mirror in the x axis, then a turn by 126.87°, twice the line’s 63.435°, then a move by (−0.8, 0.4). The last matrix flattens the plane, and nothing undoes that, so the program refuses it.

The split is a convention, not a fact about the matrix. CSS Transforms Level 1 decomposes a 2D matrix for animation into two scales, an angle and a leftover 2 × 2 matrix instead of a shear (section 13.2.1), so two libraries can report different numbers for the same matrix and both be right. Compare decompositions by multiplying them back, never number by number.

Checkpoint

Check: Splitting a matrix into its parts

  1. Question 1 of 2 In M = T R H S with the first scale kept positive, which part of M gives the first scale and the angle directly?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The first column, its length and its direction

    The first column alone is free of the shear, so its length is sx and its direction is θ. The second column, turned back by θ, gives the shear and sy.

  2. Question 2 of 2 The mirror in y = 2x + 1 decomposes into a turn by 126.87°, no shear and the scale (1, −1). Why must one scale be negative?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Its determinant is −1, and turns and shears have determinant 1, so the sign must come from a scale; the convention puts it in sy

    The determinant is sx × sy, and the mirror's is −1. With sx > 0 the split has to put the minus sign in sy, which is why a mirrored shape always decomposes with a negative second scale.

Patterns from composed rotations and mirrors

An eight-fold pattern in the style of a rangoli needs one petal and nothing else: every other petal is that petal under a composed matrix. Turning it by R(45k), for k from 0 to 7, gives eight copies that circle the centre like a pinwheel. The petal leans to one side, so the pinwheel has no mirror lines; adding the mirror F gives eight more copies, R(45k) F, and the pattern becomes symmetric about eight lines:

Sixteen copies of one leaning petal round a centre: the petal, its mirror in the x axis, and 14 more, with 8 mirror lines.Sixteen copies of one petalR(45k) turns it; R(45k) F mirrors itthe petalits mirror, Fthe 8 mirror lines; grey: 14 more copies

An eight-fold pattern from one petal and the 16 symmetries

Text description of the diagram

A round pattern of 16 narrow petals radiating from a central dot, drawn over eight faint dashed lines through the centre, one every 22.5 degrees.

One petal, just above the line pointing right, is drawn solid and shaded blue: it leans to one side, so it is not its own mirror image. Just below that line is its mirror image in the x axis, drawn with a dashed blue outline. The other 14 petals are grey. Together they are the images of the one petal under the 16 matrices R(45k) and R(45k) F, for k from 0 to 7: the 8 rotations by multiples of 45 degrees, and the same 8 rotations applied after the mirror F.

Each pair of neighbouring petals is mirrored across one of the dashed lines, which are the 8 mirror lines of the pattern, and turning the whole pattern by 45 degrees puts every petal on another one.

The legend names the solid petal, its dashed mirror, the dashed mirror lines and the 14 grey copies.

The 16 matrices are every symmetry: A transformation that maps a shape or a pattern onto itself, such as a turn by 45 degrees of an eight-fold pattern or a mirror in one of its lines. of the pattern, and the program checks the facts that make that true:

Sixteen symmetries and the pattern they make JavaScript · pattern/rangoli.mjs
// An eight-fold pattern in the style of a rangoli, built from one petal by composed
// transformations. Rotations by 45 degrees give 8 copies that turn like a pinwheel;
// adding the mirror F in the x axis gives 8 more, and the 16 matrices R(45k) and
// R(45k) F are every symmetry of the pattern.

const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const F = [[1, 0, 0], [0, -1, 0], [0, 0, 1]];
const IDENTITY = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
function multiply(a, b) {
  const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) out[i][j] += a[i][k] * b[k][j];
  return out;
}
const apply = (m, [x, y]) => [m[0][0] * x + m[0][1] * y + m[0][2], m[1][0] * x + m[1][1] * y + m[1][2]];
const det = (m) => m[0][0] * m[1][1] - m[0][1] * m[1][0];
const maxDiff = (a, b) => Math.max(...a.flatMap((r, i) => r.map((v, j) => Math.abs(v - b[i][j]))));
const near = (a, b) => maxDiff(a, b) < 1e-12;
function signedArea(poly) {
  let twice = 0;
  for (let i = 0; i < poly.length; i++) {
    const [x0, y0] = poly[i];
    const [x1, y1] = poly[(i + 1) % poly.length];
    twice += x0 * y1 - x1 * y0;
  }
  return twice / 2;
}
const num = (v) => {
  const r = Number(v.toFixed(3));
  return Object.is(r, -0) ? '0' : String(r);
};

// One petal, leaning to one side, inside the wedge from 3 to 20 degrees.
// Each corner is (radius, angle in degrees).
const POLAR = [[1.2, 8], [2.0, 4], [3.0, 3], [4.0, 6], [4.6, 12], [4.4, 19], [3.6, 20], [2.8, 17], [2.0, 13]];
const PETAL = POLAR.map(([r, a]) => [r * Math.cos((a * Math.PI) / 180), r * Math.sin((a * Math.PI) / 180)]);

const symmetries = [];
for (let k = 0; k < 8; k++) symmetries.push({ name: `R(${45 * k})`, m: rotate(45 * k) });
for (let k = 0; k < 8; k++) symmetries.push({ name: `R(${45 * k}) F`, m: multiply(rotate(45 * k), F) });

console.log('symmetry     determinant  what it is');
for (const { name, m } of symmetries) {
  const d = det(m);
  let what;
  if (d > 0) what = near(m, IDENTITY) ? 'the identity' : `a turn by ${name.slice(2, -1)} degrees`;
  else {
    // A mirror leaves its line in place. Both vectors below lie along that line;
    // take the longer one, because one of them is (0, 0) for the mirrors in the axes.
    const v1 = [1 + m[0][0], m[1][0]];
    const v2 = [m[0][1], 1 - m[0][0]];
    const [vx, vy] = Math.hypot(...v1) >= Math.hypot(...v2) ? v1 : v2;
    const angle = (Math.atan2(vy, vx) * 180) / Math.PI;
    what = `a mirror in the line at ${num((angle + 180) % 180)} degrees`;
  }
  console.log(`${name.padEnd(13)}${num(d).padEnd(13)}${what}`);
}

const copies = symmetries.map(({ m }) => PETAL.map((p) => apply(m, p)));
let r45 = IDENTITY;
for (let i = 0; i < 8; i++) r45 = multiply(rotate(45), r45);
console.log('');
console.log(`R(45) applied 8 times is the identity: ${near(r45, IDENTITY) ? 'yes' : 'no'}`);
console.log(`every mirror applied twice is the identity: ${symmetries.slice(8).every(({ m }) => near(multiply(m, m), IDENTITY)) ? 'yes' : 'no'}`);
console.log(`R(45) F equals F R(-45): ${near(multiply(rotate(45), F), multiply(F, rotate(-45))) ? 'yes' : 'no'}`);
let closed = 0;
for (const a of symmetries) for (const b of symmetries) if (symmetries.some(({ m }) => near(m, multiply(a.m, b.m)))) closed++;
console.log(`products of two symmetries that are again one of the 16: ${closed} of ${symmetries.length ** 2}`);
console.log('');
console.log(`petal: ${PETAL.length} corners, signed area ${num(signedArea(PETAL))}`);
console.log(`pattern: ${copies.length} copies, ${copies.length * PETAL.length} corners`);
// Every copy's signed area, the turned ones first: "each" is printed only when all of a group agree.
const areas = copies.map(signedArea);
const group = (list) => (list.every((a) => Math.abs(a - list[0]) < 1e-12) ? `${num(list[0])} each` : list.map(num).join(', '));
console.log(`signed areas: the 8 turned copies ${group(areas.slice(0, 8))}, the 8 mirrored copies ${group(areas.slice(8))}`);
const svg = (poly) => `M${poly.map(([x, y]) => `${num(x)} ${num(-y)}`).join(' L')} Z`; // SVG's y points down
console.log(`SVG path of the petal: ${svg(PETAL)}`);
console.log(`SVG path of its mirror: ${svg(copies[8])}`);

Output

symmetry     determinant  what it is
R(0)         1            the identity
R(45)        1            a turn by 45 degrees
R(90)        1            a turn by 90 degrees
R(135)       1            a turn by 135 degrees
R(180)       1            a turn by 180 degrees
R(225)       1            a turn by 225 degrees
R(270)       1            a turn by 270 degrees
R(315)       1            a turn by 315 degrees
R(0) F       -1           a mirror in the line at 0 degrees
R(45) F      -1           a mirror in the line at 22.5 degrees
R(90) F      -1           a mirror in the line at 45 degrees
R(135) F     -1           a mirror in the line at 67.5 degrees
R(180) F     -1           a mirror in the line at 90 degrees
R(225) F     -1           a mirror in the line at 112.5 degrees
R(270) F     -1           a mirror in the line at 135 degrees
R(315) F     -1           a mirror in the line at 157.5 degrees

R(45) applied 8 times is the identity: yes
every mirror applied twice is the identity: yes
R(45) F equals F R(-45): yes
products of two symmetries that are again one of the 16: 256 of 256

petal: 9 corners, signed area 1.947
pattern: 16 copies, 144 corners
signed areas: the 8 turned copies 1.947 each, the 8 mirrored copies -1.947 each
SVG path of the petal: M1.188 -0.167 L1.995 -0.14 L2.996 -0.157 L3.978 -0.418 L4.499 -0.956 L4.16 -1.432 L3.383 -1.231 L2.678 -0.819 L1.949 -0.45 Z
SVG path of its mirror: M1.188 0.167 L1.995 0.14 L2.996 0.157 L3.978 0.418 L4.499 0.956 L4.16 1.432 L3.383 1.231 L2.678 0.819 L1.949 0.45 Z

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node rangoli.mjs

The eight with determinant 1 are turns. The eight with determinant −1 are mirrors, and each R(45k) F is a mirror in the line at 22.5k degrees, half the angle of its turn, like the 126.87° of the mirror above. R(45°) applied eight times is the identity, every mirror applied twice is the identity, and all 256 products of two symmetries are again one of the 16, which is why turning or mirroring the finished pattern lands every petal on another. The signed areas tell the turned copies, 1.947 each, from the mirrored ones, −1.947, the same change of sign as the mirrored arrow. To write the pattern as SVG, whose y axis also points down, the program negates y as it writes each path.

UGC NET and university syllabi

For UGC NET, the Computer Science and Applications paper’s Unit 3 covers “Translation, Scaling, Rotation, Reflection and Shear Transformations; Matrix Representations and Homogeneous Coordinates; Composite Transforms”. GTU’s BE05000161 course, Computer Graphics and Visualization, covers the same transformations with composite transformations and homogeneous coordinates, and its practicals include basic 2D transformation operations. An exam-style question: find the matrix of a quarter turn about (1, 2). The sandwich gives rows [0, −1, 3] and [1, 0, 1].

Checkpoint

Check: Patterns from rotations and mirrors

  1. Question 1 of 2 The matrix R(90°) F is a mirror. At what angle from the x axis is its mirror line?

    Type a number.

    A hint

    Each R(45k) F is a mirror in the line at half the angle of its turn.

    Show the answer: Question 1 of 2

    Answer: 45 degrees (anything from 44.99 to 45.01 counts)

    R(θ) F mirrors in the line at θ ÷ 2, so R(90°) F mirrors in the line at 45 degrees; the program listed it as "a mirror in the line at 45 degrees".

  2. Question 2 of 2 A petal that leans to one side is copied by the 8 rotations R(45k) only. What does the pattern lack?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Mirror lines; it circles the centre like a pinwheel

    The 8 rotations make a pinwheel. Adding the mirror F gives the 8 mirrors R(45k) F, 16 symmetries in all, and the pattern gains its 8 mirror lines.

Practise: compose and decompose

Two exercises build the matrix tools of this lesson, with sample tests that run in the page. The first composes a list of operations in order, about pivots and mirror lines, and is held to the lesson’s matrices and to two copies of the petal pattern; the second splits any matrix into T R H S and puts 500 seeded matrices back together:

Code lab · 2 steps

Build a 2D transform toolkit

Compose operations in the right order about any pivot or mirror line, then split any affine matrix back into translation, rotation, shear and scale.

  1. Compose transformations in order, about any point To do
  2. Split a matrix into translation, rotation, shear and scale To do

Exercise · Medium · JavaScript

Compose transformations in order, about any point

Finish composite(ops) in compose.mjs. It returns the 3 × 3 matrix, as three rows, that applies a list of operations to a point in the order they are listed: the first operation acts first.

  • { op: 'rotate', degrees, about } and { op: 'scale', x, y, about } turn or scale about the point about when it is given, and about the origin when it is not.
  • { op: 'reflect', through: [[x1, y1], [x2, y2]] } mirrors in the line through the two points.
  • translate and shear (x moves by k times y) already work on their own.

The starter multiplies the matrices in the wrong order, ignores about and cannot reflect. The sample tests check single operations, the order of two, the lesson's pivot cases, the mirror in y = 2x + 1, and two copies of the petal pattern, each entry within 1e-12.

Starter code · compose.mjs

/**
 * composite(ops) returns the 3 x 3 matrix that applies the operations in the order listed.
 * An operation is one of
 *   { op: 'translate', x, y }
 *   { op: 'rotate', degrees, about }      about: an optional pivot [px, py]
 *   { op: 'scale', x, y, about }          about: an optional fixed point [px, py]
 *   { op: 'shear', k }                    x moves by k times y
 *   { op: 'reflect', through }            through: two points [[x1, y1], [x2, y2]] of the mirror line
 * This first version multiplies in the wrong order, ignores `about` and cannot reflect.
 */
const translate = (x, y) => [[1, 0, x], [0, 1, y], [0, 0, 1]];
const rotate = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const scale = (x, y) => [[x, 0, 0], [0, y, 0], [0, 0, 1]];
const shear = (k) => [[1, k, 0], [0, 1, 0], [0, 0, 1]];

export function multiply(a, b) {
  const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) out[i][j] += a[i][k] * b[k][j];
  return out;
}

function matrixOf(op) {
  switch (op.op) {
    case 'translate':
      return translate(op.x, op.y);
    case 'rotate':
      return rotate(op.degrees);
    case 'scale':
      return scale(op.x, op.y);
    case 'shear':
      return shear(op.k);
    default:
      throw new Error(`cannot do ${op.op} yet`);
  }
}

export function composite(ops) {
  let m = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
  for (const op of ops) m = multiply(m, matrixOf(op));
  return m;
}
The sample tests · compose.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { composite } from './compose.mjs';

const near = (m, expected, message) => {
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      assert.ok(Math.abs(m[i][j] - expected[i][j]) < 1e-12, `${message}: row ${i + 1}, column ${j + 1} is ${m[i][j]}, expected ${expected[i][j]}`);
    }
  }
};
const apply = (m, [x, y]) => [m[0][0] * x + m[0][1] * y + m[0][2], m[1][0] * x + m[1][1] * y + m[1][2]];
const det = (m) => m[0][0] * m[1][1] - m[0][1] * m[1][0];
const round = (v) => {
  const r = Math.round(v * 1000) / 1000;
  return Object.is(r, -0) ? 0 : r;
};
const c30 = Math.cos(Math.PI / 6);

test('one operation each, as in the lesson', () => {
  near(composite([{ op: 'translate', x: -3, y: 1 }]), [[1, 0, -3], [0, 1, 1], [0, 0, 1]], 'translate by (-3, 1)');
  near(composite([{ op: 'rotate', degrees: 90 }]), [[0, -1, 0], [1, 0, 0], [0, 0, 1]], 'rotate by 90 degrees');
  near(composite([{ op: 'scale', x: 2, y: 1.5 }]), [[2, 0, 0], [0, 1.5, 0], [0, 0, 1]], 'scale by (2, 1.5)');
  near(composite([{ op: 'shear', k: 0.5 }]), [[1, 0.5, 0], [0, 1, 0], [0, 0, 1]], 'shear x by 0.5 y');
  near(composite([]), [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'no operations');
});

test('the first operation acts first', () => {
  // scale, then rotate: R(30) S(2, 1)
  near(composite([{ op: 'scale', x: 2, y: 1 }, { op: 'rotate', degrees: 30 }]), [[2 * c30, -0.5, 0], [1, c30, 0], [0, 0, 1]], 'scale then rotate');
  // rotate, then scale: S(2, 1) R(30)
  near(composite([{ op: 'rotate', degrees: 30 }, { op: 'scale', x: 2, y: 1 }]), [[2 * c30, -1, 0], [0.5, c30, 0], [0, 0, 1]], 'rotate then scale');
  const moved = apply(composite([{ op: 'translate', x: 3, y: 0 }, { op: 'rotate', degrees: 90 }]), [0, 0]);
  assert.deepEqual(moved.map(round), [0, 3], 'move by (3, 0), then turn: the origin ends at (0, 3)');
});

test('rotate and scale about a pivot', () => {
  near(composite([{ op: 'rotate', degrees: 90, about: [1, 2] }]), [[0, -1, 3], [1, 0, 1], [0, 0, 1]], 'rotate 90 about (1, 2)');
  near(composite([{ op: 'scale', x: 2, y: 2, about: [6, 2] }]), [[2, 0, -6], [0, 2, -2], [0, 0, 1]], 'scale 2 about the tip (6, 2)');
  const m = composite([{ op: 'rotate', degrees: 37, about: [-4, 9] }]);
  assert.deepEqual(apply(m, [-4, 9]).map(round), [-4, 9], 'the pivot stays put');
});

test('reflect in any line', () => {
  const m = composite([{ op: 'reflect', through: [[0, 1], [1, 3]] }]);
  near(m, [[-0.6, 0.8, -0.8], [0.8, 0.6, 0.4], [0, 0, 1]], 'the mirror in y = 2x + 1');
  near(composite([{ op: 'reflect', through: [[1, 3], [2, 5]] }]), m, 'the same line, given by two other points');
  near(composite([{ op: 'reflect', through: [[1, 3], [-1, -1]] }]), m, 'the same line, its points in the other order');
  assert.ok(Math.abs(det(m) + 1) < 1e-12, 'a mirror has determinant -1');
  near(composite([{ op: 'reflect', through: [[2, -1], [5, 3]] }, { op: 'reflect', through: [[2, -1], [5, 3]] }]), [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'mirroring twice');
  assert.deepEqual(apply(composite([{ op: 'reflect', through: [[0, 0], [0, 1]] }]), [3, 4]).map(round), [-3, 4], 'the mirror in the y axis');
});

test('the petal pattern of the lesson', () => {
  const POLAR = [[1.2, 8], [2.0, 4], [3.0, 3], [4.0, 6], [4.6, 12], [4.4, 19], [3.6, 20], [2.8, 17], [2.0, 13]];
  const PETAL = POLAR.map(([r, a]) => [r * Math.cos((a * Math.PI) / 180), r * Math.sin((a * Math.PI) / 180)]);
  const copy = (ops) => PETAL.map((p) => apply(composite(ops), p).map(round));
  assert.deepEqual(copy([{ op: 'rotate', degrees: 90 }]), [[-0.167, 1.188], [-0.14, 1.995], [-0.157, 2.996], [-0.418, 3.978], [-0.956, 4.499], [-1.432, 4.16], [-1.231, 3.383], [-0.819, 2.678], [-0.45, 1.949]], 'R(90)');
  const mirrored = copy([{ op: 'reflect', through: [[0, 0], [1, 0]] }, { op: 'rotate', degrees: 45 }]);
  assert.deepEqual(mirrored, [[0.958, 0.722], [1.509, 1.312], [2.229, 2.007], [3.109, 2.517], [3.858, 2.505], [3.955, 1.929], [3.263, 1.521], [2.472, 1.315], [1.696, 1.06]], 'mirror in the x axis, then R(45): R(45) F');
  near(composite([{ op: 'reflect', through: [[0, 0], [1, 0]] }, { op: 'rotate', degrees: 45 }]), composite([{ op: 'rotate', degrees: -45 }, { op: 'reflect', through: [[0, 0], [1, 0]] }]), 'R(45) F equals F R(-45)');
});
A hint

With column vectors the matrix that acts first stands on the right, so each new operation goes on the left of what you have: m = multiply(next, m). For a pivot p, the sandwich is multiply(multiply(translate(p), M), translate(-p)). A mirror in a line through the origin with direction (dx, dy) is the matrix with rows [c, s] and [s, −c], where c = (dx² − dy²) ÷ (dx² + dy²) and s = 2·dx·dy ÷ (dx² + dy²); sandwich it about the line's first point.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · JavaScript

Split a matrix into translation, rotation, shear and scale

Finish decompose(m) in decompose.mjs. It splits an affine matrix, given as three rows, into M = T(tx, ty) R(angle) H(shear) S(sx, sy), where H is the x shear with rows [1, shear] and [0, 1], and returns { tx, ty, angle, shear, sx, sy } with the angle in degrees.

  • Keep sx > 0, so that a mirror comes out as a negative sy.
  • Throw an Error when the matrix has determinant 0: it flattens the plane, and no T R H S can give it.

The starter reads both scales as the lengths of the two columns and never finds a shear, so it is right only when the matrix has no shear and no mirror. The sample tests check the lesson's matrices, 500 seeded matrices that must come apart and multiply back within 1e-9, and two matrices with determinant 0.

Starter code · decompose.mjs

/**
 * decompose(m) splits an affine 3 x 3 matrix (three rows) into
 *   M = T(tx, ty) R(angle) H(shear) S(sx, sy)
 * where H is the x shear [1 shear; 0 1] and S scales by sx and sy. Keep sx > 0, so that a mirror
 * shows as a negative sy. Return { tx, ty, angle, shear, sx, sy }, with the angle in degrees.
 * This first version reads the scales as the lengths of the two columns and never finds a shear.
 */
export function decompose(m) {
  const sx = Math.hypot(m[0][0], m[1][0]);
  const sy = Math.hypot(m[0][1], m[1][1]);
  const angle = (Math.atan2(m[1][0], m[0][0]) * 180) / Math.PI;
  return { tx: m[0][2], ty: m[1][2], angle, shear: 0, sx, sy };
}
The sample tests · decompose.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { decompose } from './decompose.mjs';

const multiply = (a, b) => a.map((row) => [0, 1, 2].map((j) => row[0] * b[0][j] + row[1] * b[1][j] + row[2] * b[2][j]));
const T = (x, y) => [[1, 0, x], [0, 1, y], [0, 0, 1]];
const R = (degrees) => {
  const r = (degrees * Math.PI) / 180;
  return [[Math.cos(r), -Math.sin(r), 0], [Math.sin(r), Math.cos(r), 0], [0, 0, 1]];
};
const H = (k) => [[1, k, 0], [0, 1, 0], [0, 0, 1]];
const S = (x, y) => [[x, 0, 0], [0, y, 0], [0, 0, 1]];
const compose = (...ms) => ms.reduce(multiply);
const recompose = (d) => compose(T(d.tx, d.ty), R(d.angle), H(d.shear), S(d.sx, d.sy));
const close = (a, b, eps = 1e-9) => Math.abs(a - b) < eps;

function expectParts(d, want, label) {
  for (const k of ['tx', 'ty', 'angle', 'shear', 'sx', 'sy']) {
    assert.ok(typeof d[k] === 'number' && close(d[k], want[k]), `${label}: ${k} is ${d[k]}, expected ${want[k]}`);
  }
}
function expectSame(a, b, label) {
  for (let i = 0; i < 2; i++) for (let j = 0; j < 3; j++) assert.ok(close(a[i][j], b[i][j]), `${label}: recomposed row ${i + 1}, column ${j + 1} is ${a[i][j]}, expected ${b[i][j]}`);
}

test('a rotation and a scale, no shear', () => {
  expectParts(decompose(compose(T(4, -1), R(30), S(2, 1.5))), { tx: 4, ty: -1, angle: 30, shear: 0, sx: 2, sy: 1.5 }, 'T(4, -1) R(30) S(2, 1.5)');
});

test('the lesson cases: a shear, a hidden shear and a mirror', () => {
  expectParts(decompose(compose(T(4, -1), R(30), H(0.5), S(2, 1.5))), { tx: 4, ty: -1, angle: 30, shear: 0.5, sx: 2, sy: 1.5 }, 'T(4, -1) R(30) H(0.5) S(2, 1.5)');
  const hidden = decompose(multiply(S(2, 1), R(30)));
  expectParts(hidden, { tx: 0, ty: 0, angle: (Math.atan2(0.5, Math.sqrt(3)) * 180) / Math.PI, shear: (-3 * Math.sqrt(3)) / 8, sx: Math.sqrt(3.25), sy: 2 / Math.sqrt(3.25) }, 'S(2, 1) R(30)');
  const mirror = decompose([[-0.6, 0.8, -0.8], [0.8, 0.6, 0.4], [0, 0, 1]]);
  expectParts(mirror, { tx: -0.8, ty: 0.4, angle: (Math.atan2(0.8, -0.6) * 180) / Math.PI, shear: 0, sx: 1, sy: -1 }, 'the mirror in y = 2x + 1');
});

test('500 seeded matrices come apart and go back together', () => {
  let seed = 11; // mulberry32, so every run tests the same matrices
  const random = () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
  const between = (lo, hi) => lo + (hi - lo) * random();
  for (let n = 0; n < 500; n++) {
    const want = { tx: between(-50, 50), ty: between(-50, 50), angle: between(-179, 179), shear: between(-2, 2), sx: between(0.2, 5), sy: between(0.2, 5) * (random() < 0.5 ? -1 : 1) };
    const m = recompose(want);
    const d = decompose(m);
    expectParts(d, want, `matrix ${n + 1}`);
    expectSame(recompose(d), m, `matrix ${n + 1}`);
  }
});

test('a matrix that flattens the plane is refused', () => {
  assert.throws(() => decompose(S(1, 0)), 'S(1, 0) has determinant 0');
  assert.throws(() => decompose([[1, 2, 5], [2, 4, 6], [0, 0, 1]]), 'parallel columns: determinant 0');
});
A hint

Only the first column is free of the shear: R H S sends (1, 0) to sx·(cos a, sin a), so its length is sx and its direction is the angle. Turn the second column back by that angle, by multiplying it by R(−a), and you have H S applied to (0, 1), which is (shear × sy, sy): read sy from the second number, then divide to get the shear.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Matrix Calculator Enter T(1, 2), R(90°) and T(−1, −2), multiply them two at a time, and compare the product with the program's rows. Unit Circle Chart & Calculator Read the cosine and sine of 30°, 45° and 63.435° before you check a rotation or a mirror matrix.

Then test yourself on all six ideas, from the five matrices to the petal pattern. The session opens with the idea your answers show you are weakest on, and it repeats a question you miss:

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 A shape of area 10 is scaled by S(3, 0.5). What area does it cover afterwards?

    Type a number.

    Show the answer: Question 1 of 18

    Answer: 15 square units

    The determinant of S(3, 0.5) is 3 × 0.5 = 1.5, so the area becomes 10 × 1.5 = 15.

  2. Question 2 of 18 Which of the five basic matrices moves the origin?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: The translation

    Rotation, scale, reflection and shear are linear and keep the origin fixed. The translation is not linear, which is why it needs the third coordinate of homogeneous coordinates.

  3. Question 3 of 18 A rotate(degrees) function passes its argument straight to Math.cos and Math.sin. What does rotate(90) do?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: It turns by 90 radians, about 5,157 degrees, which is a turn of about 117 degrees

    Convert once, with degrees × π ÷ 180, where an angle enters the program, and keep radians inside.

  4. Question 4 of 18 Which matrix scales by 2 about the arrow's tip (6, 2)?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: Rows [2, 0, −6] and [0, 2, −2]

    The last column of T(p) S T(−p) is p − S p. For S = S(2, 2) and p = (6, 2) that is (6, 2) − (12, 4) = (−6, −2).

  5. Question 5 of 18 You have built the matrix of a mirror in some line. Which checks would catch a wrong matrix?

    Choose every answer that is right.

    Show the answer: Question 5 of 18

    Answer:

    • The matrix times itself is the identity
    • The determinant of the 2 × 2 part is −1
    • Two points of the line stay where they are

    The lesson's program checked (1, 3) and (−1, −1) on the line and the square of the matrix; the determinant check catches a matrix that turns instead of mirroring.

  6. Question 6 of 18 The mirror in y = 2x + 1 has rows [−0.6, 0.8, −0.8] and [0.8, 0.6, 0.4]. What is the y coordinate of the image of (0, 0)?

    Type a number.

    Show the answer: Question 6 of 18

    Answer: 0.4 (anything from 0.399 to 0.401 counts)

    The image of the origin is the last column, (−0.8, 0.4). The midpoint of (0, 0) and (−0.8, 0.4) is (−0.4, 0.2), which lies on the line, as it must.

  7. Question 7 of 18 Where does the origin go under R(90°) T(3, 0), and where under T(3, 0) R(90°)?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: (0, 3) and (3, 0)

    A translation and a rotation do not commute: a rotation applied after the move turns the offset as well.

  8. Question 8 of 18 Why does a rotation commute with a uniform scale, but not with S(2, 1)?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: A uniform scale stretches every direction alike, so it does not matter whether the shape has turned

    A uniform scale is a number times the identity, and the identity commutes with everything; a non-uniform scale stretches along fixed axes.

  9. Question 9 of 18 F is the mirror in the x axis. Which matrix equals F R(30°)?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: R(−30°) F

    F R(θ) = R(−θ) F, the identity the pattern program checked as R(45°) F = F R(−45°).

  10. Question 10 of 18 A canvas starts from the identity, then runs transform(2, 0, 0, 2, 0, 0) and transform(1, 0, 0, 1, 10, 0). Where is (1, 1) drawn?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: (22, 2)

    The HTML standard multiplies the current matrix by each new one on the right, so the transformations are performed in reverse order. The program printed (22, 2).

  11. Question 11 of 18 In the coordinate space of a canvas 2D context, where is the origin and which way does y grow?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: At the top left corner, with y growing downwards

    The direction of y is what makes positive angles turn clockwise on screen.

  12. Question 12 of 18 You want to draw with y pointing up, with (0, 0) at the bottom left of a canvas 300 pixels tall. Which call sets that up?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: setTransform(1, 0, 0, -1, 0, 300)

    The flip is a mirror in the x axis followed by a move of 300 down the screen: rows [1, 0, 0] and [0, −1, 300], which setTransform takes column by column. Turns now look counter-clockwise again.

  13. Question 13 of 18 An affine matrix has the columns (2, 0), (1, 3) and (5, 5). Decomposed as T R H S with sx > 0, what is its shear?

    Type a number.

    A hint

    The first column lies along x, so the angle is 0 and nothing needs turning back.

    Show the answer: Question 13 of 18

    Answer: 0.333 (anything from 0.332 to 0.334 counts)

    The first column (2, 0) gives sx = 2 and an angle of 0. The second column, unchanged, is (k × sy, sy) = (1, 3), so sy = 3 and the shear is 1 ÷ 3 ≈ 0.333; the translation is (5, 5).

  14. Question 14 of 18 In the split M = T R H S, what does the product sx × sy equal?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: The determinant of M's 2 × 2 part, its area factor

    For S(2, 1) R(30°) the split gave 1.803 × 1.109 = 2, the determinant of S(2, 1).

  15. Question 15 of 18 Two libraries report different scales and angles for the same matrix. How do you check whether both are right?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: Multiply each split back and compare the results with the matrix

    The decomposition depends on the order and the factors chosen; the matrix does not. Recomposing is the test the lesson's program and exercise use.

  16. Question 16 of 18 How many symmetries does the eight-fold petal pattern have, turns and mirrors together?

    Type a number.

    Show the answer: Question 16 of 18

    Answer: 16

    The 8 rotations R(45k) and the 8 mirrors R(45k) F, for k from 0 to 7.

  17. Question 17 of 18 What is the product of the two mirrors R(90°) F and R(45°) F, in that order?

    Choose one answer.

    A hint

    Use F R(θ) = R(−θ) F to move one F past the rotation, and F F is the identity.

    Show the answer: Question 17 of 18

    Answer: R(45°), a turn

    The two mirror lines are at 45 and 22.5 degrees, 22.5 degrees apart, and two mirrors compose to a turn by twice that angle: 45 degrees. All 256 such products land back among the 16 symmetries.

  18. Question 18 of 18 Why does each mirrored petal have the signed area −1.947 while the turned ones have 1.947?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: Each R(45k) F has determinant −1, which reverses the order of the corners

    Rotations have determinant 1 and keep the orientation; every mirror has −1 and reverses it, the same change the mirrored arrow showed.

Interview questions

Warm-up (fresher to 2-5 years): How do you rotate a shape about a point that is not the origin?

A rotation matrix turns every point about the origin, so rotating about a pivot p takes three steps: translate by −p so that the pivot sits at the origin, rotate, and translate back by p. With column vectors the single matrix is T(p) R(θ) T(−p), read from right to left. For a quarter turn about (1, 2) it has rows [0, −1, 3] and [1, 0, 1]: the 2 × 2 part is the plain rotation, and the last column, p − R p = (3, 1), is the correction that keeps the pivot fixed. The same sandwich scales about a point, and with a turn on each side it mirrors in any line. On a canvas, where y points down, the same matrix turns the shape clockwise on screen.

Two questions of this module go further, into animating between transforms and building a mirror tool. Their first steps and follow-ups are on the module’s question page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An animation tool moves a sprite from a start matrix to an end matrix by interpolating each matrix entry. A sprite that turns by 180° shrinks to a point halfway, and one that turns by 90° looks about 29 % smaller in the middle frame. Explain both and fix the animation.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A vector drawing tool needs "mirror the selection across a guide line", where the guide can be drawn at any angle through any point. Build the transformation for a guide through (2, −1) and (5, 3), say what goes wrong if you build it from the line's slope, and describe the tests you would ship with it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • In homogeneous coordinates translation, scale, rotation, reflection and shear are 3 × 3 matrices, and the determinant of the 2 × 2 part is the area factor: 3 for the scale by (2, 1.5), and −1 for a mirror, which also reverses the order of the corners.
  • To transform about a pivot p, use T(p) M T(−p): a quarter turn about (1, 2) has rows [0, −1, 3] and [1, 0, 1], and a mirror in any line is the same sandwich with a turn on each side.
  • Rotations commute with each other and with uniform scales, but not with non-uniform scales, shears, translations or mirrors: S(2, 1) R(30°) turns a square into a parallelogram with a 123° corner.
  • A canvas stores its matrix as setTransform(a, b, c, d, e, f), column by column, applies the last call first, and turns clockwise for positive angles because its y axis points down.
  • Every invertible affine matrix splits into T R H S in one way once the convention is fixed; the split is a convention, so decompositions are compared by multiplying them back.
  • One leaning petal and the 16 matrices R(45k) and R(45k) F give an eight-fold pattern with eight mirror lines.

Terms in this lesson

composite transformation
One matrix made by multiplying several transformation matrices, so that a whole chain of moves is applied at once. With column vectors the rightmost factor acts on a point first.
pivot
The point that a rotation or a scale keeps fixed. Transforming about a pivot p means moving p to the origin, transforming there, and moving it back: T(p) M T(−p).
reflection
A transformation that mirrors every point in a line and keeps the points of the line itself fixed. Its matrix has determinant −1, and applying it twice gives the identity.
shear
A transformation that slides each point parallel to one axis by an amount proportional to its other coordinate, as in x′ = x + k·y. It keeps areas and turns rectangles into parallelograms.
symmetry
A transformation that maps a shape or a pattern onto itself, such as a turn by 45 degrees of an eight-fold pattern or a mirror in one of its lines.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.