Skip to content

Matrix Transformations

The columns are where the basis vectors land.

Beginner / linear-algebra / geometry

Rotation, scaling, and shear compose into a single 2x2 matrix that maps a reference grid and a house. The live readout shows the matrix itself; the determinant is the area scaling factor, and its sign tells you the shape flipped over.

Tagged: rotation, scale, shear, determinant

Live simulation

Sliders compose rotation, scale, and shear into one matrix, live.

Speed

Sliders compose rotation, scale, and shear into one matrix, live.

Mathematics

A 2x2 matrix is two column vectors: where the basis goes. Rotation, scaling, and shear compose by matrix multiplication into one M, and every point of the grid and the house is mapped by M x.

M = H * S * R (shear applied last, rotation first)

det(M) = area scale factor; det < 0 means orientation flip

column 1 of M = image of (1, 0); column 2 = image of (0, 1)

The composition order matters: shear-after-rotate is a different transform than rotate-after-shear, and the slider stack makes the order visible in one drag. The determinant readout is the fastest sanity check in linear algebra - watch it cross zero exactly when the shape collapses flat.

How it works

The composed matrix is recomputed from the five sliders per frame; the grid, basis vectors, and house polygon are transformed by it point by point. The readout writes the matrix entries in row-major form.

Set scale to (-1, 1) for a reflection: the determinant goes negative and the house flips - orientation, quantified.

Implementation

src/engine/experiments/matrix-transformations.ts · 155 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, ThemeColors } from "../core/types";
3
4/**
5 * 2-D matrix transformations: rotation, scaling, and shear sliders compose
6 * into one matrix that maps a reference grid and a shape. The matrix readout
7 * updates live - the columns ARE the images of the basis vectors.
8 */
9export class MatrixTransformationsExperiment extends BaseExperiment {
10 private rotation = 0;
11 private scaleX = 1;
12 private scaleY = 1;
13 private shearX = 0;
14 private shearY = 0;
15
16 readonly id = "matrix-transformations";
17
18 protected params(): ParameterDef[] {
19 return [
20 { key: "rotation", label: "Rotation", min: -180, max: 180, step: 1, defaultValue: 0, unit: "°" },
21 { key: "scaleX", label: "Scale x", min: -2, max: 2, step: 0.1, defaultValue: 1 },
22 { key: "scaleY", label: "Scale y", min: -2, max: 2, step: 0.1, defaultValue: 1 },
23 { key: "shearX", label: "Shear x", min: -1.5, max: 1.5, step: 0.05, defaultValue: 0 },
24 { key: "shearY", label: "Shear y", min: -1.5, max: 1.5, step: 0.05, defaultValue: 0 },
25 ];
26 }
27
28 protected onReset(): void {
29 this.rotation = this.num("rotation");
30 this.scaleX = this.num("scaleX");
31 this.scaleY = this.num("scaleY");
32 this.shearX = this.num("shearX");
33 this.shearY = this.num("shearY");
34 }
35
36 protected onParameterChange(): void {
37 this.reset();
38 }
39
40 protected onUpdate(): void {}
41
42 /** Composed matrix M = Shear * Scale * Rotation (column-major multiply). */
43 private matrix(): [number, number, number, number] {
44 const rad = (this.rotation * Math.PI) / 180;
45 const cos = Math.cos(rad);
46 const sin = Math.sin(rad);
47 // R then S then H: M = H * S * R
48 const r00 = cos;
49 const r01 = -sin;
50 const r10 = sin;
51 const r11 = cos;
52 const s00 = this.scaleX * r00;
53 const s01 = this.scaleX * r01;
54 const s10 = this.scaleY * r10;
55 const s11 = this.scaleY * r11;
56 return [
57 s00 + this.shearX * s10,
58 s01 + this.shearX * s11,
59 this.shearY * s00 + s10,
60 this.shearY * s01 + s11,
61 ];
62 }
63
64 private apply(m: [number, number, number, number], x: number, y: number): [number, number] {
65 return [m[0] * x + m[1] * y, m[2] * x + m[3] * y];
66 }
67
68 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
69 ctx.fillStyle = theme.bg;
70 ctx.fillRect(0, 0, this.width, this.height);
71 const m = this.matrix();
72 const scale = Math.min(this.width, this.height) / 5;
73 const cx = this.width / 2;
74 const cy = this.height / 2;
75 const to = (x: number, y: number): [number, number] => {
76 const [px, py] = this.apply(m, x, y);
77 return [cx + px * scale, cy - py * scale];
78 };
79 // Transformed grid lines
80 ctx.strokeStyle = theme.line;
81 ctx.lineWidth = 1;
82 for (let g = -2; g <= 2; g++) {
83 ctx.beginPath();
84 let start = to(g, -2);
85 let end = to(g, 2);
86 ctx.moveTo(start[0], start[1]);
87 ctx.lineTo(end[0], end[1]);
88 start = to(-2, g);
89 end = to(2, g);
90 ctx.moveTo(start[0], start[1]);
91 ctx.lineTo(end[0], end[1]);
92 ctx.stroke();
93 }
94 // Basis vectors
95 const e1 = to(1, 0);
96 const e2 = to(0, 1);
97 ctx.strokeStyle = theme.accent;
98 ctx.lineWidth = 2.5;
99 ctx.beginPath();
100 ctx.moveTo(cx, cy);
101 ctx.lineTo(e1[0], e1[1]);
102 ctx.stroke();
103 ctx.strokeStyle = theme.viz[1];
104 ctx.beginPath();
105 ctx.moveTo(cx, cy);
106 ctx.lineTo(e2[0], e2[1]);
107 ctx.stroke();
108 // House shape
109 const house: [number, number][] = [
110 [-0.5, -0.5], [0.5, -0.5], [0.5, 0.4], [0.15, 0.4], [0.15, 0.9], [-0.15, 0.9], [-0.15, 0.4], [-0.5, 0.4],
111 ];
112 ctx.strokeStyle = theme.fg;
113 ctx.lineWidth = 1.6;
114 ctx.beginPath();
115 house.forEach(([x, y], index) => {
116 const [px, py] = to(x, y);
117 if (index === 0) ctx.moveTo(px, py);
118 else ctx.lineTo(px, py);
119 });
120 ctx.closePath();
121 ctx.stroke();
122 // Matrix readout
123 ctx.font = "12px ui-monospace, monospace";
124 ctx.fillStyle = theme.fgSecondary;
125 ctx.fillText(
126 `M = [ ${m[0].toFixed(2)} ${m[1].toFixed(2)} ]`,
127 12,
128 this.height - 34,
129 );
130 ctx.fillText(
131 ` [ ${m[2].toFixed(2)} ${m[3].toFixed(2)} ]`,
132 12,
133 this.height - 16,
134 );
135 }
136
137 getMetrics() {
138 const m = this.matrix();
139 return {
140 det: (m[0] * m[3] - m[1] * m[2]).toFixed(3),
141 col1: `[${m[0].toFixed(2)}, ${m[2].toFixed(2)}]`,
142 col2: `[${m[1].toFixed(2)}, ${m[3].toFixed(2)}]`,
143 };
144 }
145
146 describe(): string {
147 const m = this.matrix();
148 return `The composed matrix has determinant ${(m[0] * m[3] - m[1] * m[2]).toFixed(2)}: areas scale by that factor, negative meaning orientation flips.`;
149 }
150
151 entities(): number {
152 return 1;
153 }
154}
155

Performance

Time per tick
O(1) per transform; O(grid) for drawing
Space
Four numbers
Notes
The matrix readout and the picture can never disagree: both come from the same four numbers.

Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.