Matrix products apply right to left: in T * R * S the vertex is scaled, then rotated, then translated. Since rotation and scaling pivot about the origin, order matters, and the usual model matrix is T * R * S, which spins and sizes the object in place before moving it. The listing draws a cover both ways:
<canvas id="c" width="960" height="260" style="width:480px;height:130px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c'), m4 = GLH.m4;
const { uniforms } = GLH.load(gl, `#version 300 es
uniform mat4 uMatrix; out vec2 vUV;
void main() {
vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
gl_Position = uMatrix * vec4(vUV - 0.5, 0.0, 1.0); // a unit quad around the origin
}`, `#version 300 es
precision mediump float; uniform vec3 uColor; in vec2 vUV; out vec4 c;
void main() { c = vec4(uColor * (vUV.x < 0.1 ? 0.6 : 1.0), 1.0); }`);
const aspect = m4.scaling(130 / 480, 1, 1); // stands in for Section 3.6.5's projection
const size = m4.scaling(0.55, 0.8, 1), turn = m4.rotation(25, 0, 0, 1);
const draw = (color, ...matrices) => {
uniforms.uMatrix(m4.chain(aspect, ...matrices, size));
uniforms.uColor(color);
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
draw(GLH.rgb(GLH.covers[0]), m4.translation(-1.1, 0, 0), turn); // T * R: turns in place
draw(GLH.rgb(GLH.covers[4]), turn, m4.translation(1.1, 0, 0)); // R * T: orbits the origin
window.__done = true;
</script>
On the left the cover turns about its own center; on the right the rotation, applied after the translation, swings it 25 degrees around the canvas center. To rotate about a pivot p, use T(p) * R * T(-p).
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 480px; }
.names { display: flex; max-width: 480px; text-align: center; font-family: monospace; }
.names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="960" height="260"></canvas>
<div class="names"><div>T * R: turns in place</div><div>R * T: orbits the origin</div></div>
<script>
const m4 = {
translation: (x, y, z) => new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]),
scaling: (x, y, z) => new Float32Array([x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]),
rotation(degrees, x, y, z) {
const r = degrees * Math.PI / 180, c = Math.cos(r), s = Math.sin(r), t = 1 - c;
return new Float32Array([t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0,
t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0,
t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0, 0, 0, 0, 1]);
},
multiply(a, b) {
const out = new Float32Array(16);
for (let col = 0; col < 4; col++)
for (let row = 0; row < 4; row++)
for (let k = 0; k < 4; k++) out[col * 4 + row] += a[k * 4 + row] * b[col * 4 + k];
return out;
},
chain: (...matrices) => matrices.reduce((a, b) => m4.multiply(a, b)), // A * B * C...
};
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform mat4 uMatrix; out vec2 vUV;
void main() {
vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
gl_Position = uMatrix * vec4(vUV - 0.5, 0.0, 1.0); // a unit quad around the origin
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; in vec2 vUV; out vec4 c;
void main() { c = vec4(uColor * (vUV.x < 0.1 ? 0.6 : 1.0), 1.0); }`]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const uMatrix = gl.getUniformLocation(program, 'uMatrix');
const uColor = gl.getUniformLocation(program, 'uColor');
const aspect = m4.scaling(130 / 480, 1, 1); // stands in for a projection matrix
const size = m4.scaling(0.55, 0.8, 1);
function draw(color, ...matrices) {
gl.uniformMatrix4fv(uMatrix, false, m4.chain(aspect, ...matrices, size));
gl.uniform3fv(uColor, color);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function frame(ms) {
const turn = m4.rotation(25 + 20 * Math.sin(ms * 0.0012), 0, 0, 1);
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
draw([0.8, 0.78, 0.72], m4.scaling(0.15, 0.15, 1)); // the origin (canvas centre)
draw([0.12, 0.37, 0.55], m4.translation(-1.1, 0, 0), turn); // T * R: turns in place
draw([0.71, 0.27, 0.18], turn, m4.translation(1.1, 0, 0)); // R * T: swings around the origin
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>