Adopting gl-matrix

Replacing the Chapter's Matrix Helpers with gl-matrix

The swap is one-for-one: each GLH.m4 builder becomes a mat4.from...() call or an appended translate, rotateY or scale, degrees become radians, and every matrix is written into an array allocated once. In Virtual Shelf's shelf only the matrix code changes, after a <script> tag for gl-matrix 5,695 :

The shelf's matrices with gl-matrix instead of GLH.m4JavaScript
const { mat4, glMatrix: { toRadian } } = glMatrix;
const camera = mat4.perspective(mat4.create(), toRadian(40), 620 / 220, 0.1, 20);
mat4.multiply(camera, camera, mat4.lookAt(mat4.create(), [1.1, 0.9, 2.9], [0.1, -0.2, 0],
  [0, 1, 0]));
const mvp = mat4.create();                  // reused for every draw
gl.bindVertexArray(plank);
mat4.translate(mvp, camera, [0, -0.495, 0]);
uniforms.uMVP(mat4.rotateX(mvp, mvp, toRadian(-90)));
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(books);
GLH.covers.forEach((hex, i) => {
  const x = -0.81 + i * 0.32;               // model = T * Ry * S * T(-x), appended in order
  mat4.translate(mvp, camera, [1.6 * x, 0, 0]);
  mat4.rotateY(mvp, mvp, toRadian(30 - i * 12));
  mat4.scale(mvp, mvp, [1.6, 0.6, 1]);
  uniforms.uMVP(mat4.translate(mvp, mvp, [-x, 0, 0]));
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);
});

Rendered, it matches Virtual Shelf's capture: comparing the screenshots found one pixel of 570,400 off by one level of blue, from rounding in rotateY(), and it allocates 3 matrices instead of 55. Later listings load gl-matrix this way; GLH.m4 stays in webgl-helper.js for the earlier ones.

BookNest's shelf with gl-matrix: one reused mvp array, transforms appended in orderHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<canvas id="c" width="1240" height="440"></canvas>
<script>
const { mat4, glMatrix: { toRadian } } = glMatrix;
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);

const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
  layout(location = 0) in vec2 aPosition; layout(location = 1) in vec2 aUV; layout(location = 2) in float aBook;
  uniform mat4 uMVP; uniform vec3 uColors[7]; out vec3 vColor;
  void main() { vColor = uColors[int(aBook)] * mix(1.15, 0.7, aUV.y);
                gl_Position = uMVP * vec4(aPosition, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
  precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), [...covers, '#8a6240'].flatMap(rgb));
const uMVP = gl.getUniformLocation(program, 'uMVP');

function vao(vertices, indices) {
  const v = gl.createVertexArray();
  gl.bindVertexArray(v);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
  [[0, 2, 0], [1, 2, 8], [2, 1, 16]].forEach(([loc, size, offset]) => {
    gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 20, offset); gl.enableVertexAttribArray(loc);
  });
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
  gl.bindVertexArray(null);
  return v;
}
const wall = [], indices = [];
covers.forEach((hex, i) => {
  for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) wall.push(-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, u, 1 - v, i);
  indices.push(i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3);
});
const books = vao(wall, indices);
const plank = vao([-1.75, -0.5, 0, 0, 6, 1.75, -0.5, 1, 0, 6, 1.75, 0.5, 1, 1, 6, -1.75, 0.5, 0, 1, 6], indices.slice(0, 6));

// Allocated once: the camera, its two parts and the mvp reused for every draw.
const projection = mat4.perspective(mat4.create(), toRadian(40), 620 / 220, 0.1, 20);
const view = mat4.create(), camera = mat4.create(), mvp = mat4.create();
function frame(ms) {
  const sway = Math.sin(ms * 0.0006) * 0.8;          // the camera drifts left and right
  mat4.lookAt(view, [1.1 + sway, 0.9, 2.9], [0.1, -0.2, 0], [0, 1, 0]);
  mat4.multiply(camera, projection, view);
  gl.clearColor(0.93, 0.91, 0.87, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.bindVertexArray(plank);
  mat4.translate(mvp, camera, [0, -0.495, 0]);
  gl.uniformMatrix4fv(uMVP, false, mat4.rotateX(mvp, mvp, toRadian(-90)));
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
  gl.bindVertexArray(books);
  covers.forEach((hex, i) => {
    const x = -0.81 + i * 0.32;                       // model = T * Ry * S * T(-x), appended in order
    mat4.translate(mvp, camera, [1.6 * x, 0, 0]);
    mat4.rotateY(mvp, mvp, toRadian(30 - i * 12));
    mat4.scale(mvp, mvp, [1.6, 0.6, 1]);
    gl.uniformMatrix4fv(uMVP, false, mat4.translate(mvp, mvp, [-x, 0, 0]));
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, i * 12);
  });
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>