Cover Shaders

Compiling BookNest's Cover Shaders

The cover wall of Cover Wall now keeps its shaders in <script> elements and takes the palette as a uniform array, so a vertex carries a position, a cover coordinate and a book number (5 floats, not 7). A flat integer varying tells the fragment shader which book it shades, and an int uniform picks one to outline in gold:

BookNest's cover shaders loaded from script elementsHTML
<canvas id="wall" width="1200" height="280" style="width:600px;height:140px"></canvas>
<script type="x-shader/x-vertex" id="cover-vs">
#version 300 es
in vec2 aPosition; in vec2 aUV; in float aBook; uniform vec3 uColors[6];
out vec2 vUV; out vec3 vColor; flat out int vBook;
void main() { vUV = aUV; vBook = int(aBook); vColor = uColors[vBook];
              gl_Position = vec4(aPosition, 0.0, 1.0); }
</script>
<script type="x-shader/x-fragment" id="cover-fs">
#version 300 es
precision mediump float;
in vec2 vUV; in vec3 vColor; flat in int vBook; uniform int uSelected; out vec4 outColor;
void main() {
  bool edge = any(greaterThan(abs(vUV - 0.5), vec2(0.45)));   // outer 5% of the cover
  outColor = vec4(vBook == uSelected && edge ? vec3(1.0, 0.84, 0.3)
    : vColor * mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)) * mix(1.2, 0.7, vUV.y), 1.0);
}
</script>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('wall');
const { attributes, uniforms } = GLH.load(gl, 'cover-vs', 'cover-fs');
uniforms.uColors(GLH.covers.flatMap(GLH.rgb));
uniforms.uSelected(2);                          // outline Salt and Saffron
const vertices = GLH.covers.flatMap((hex, i) =>  // x, y, u, v, book for 6 vertices per cover
  [[0, 0], [1, 0], [1, 1], [0, 0], [1, 1], [0, 1]].flatMap(([u, v]) =>
    [-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, u, 1 - v, i]));
GLH.buffer(gl, new Float32Array(vertices));
for (const [name, size, offset] of [['aPosition', 2, 0], ['aUV', 2, 8], ['aBook', 1, 16]]) {
  gl.vertexAttribPointer(attributes[name], size, gl.FLOAT, false, 20, offset);
  gl.enableVertexAttribArray(attributes[name]);
}
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 5);
window.__done = true;
</script>
Browser output of Listing 3.35
Browser output of 35

Recoloring the shelf or moving the outline is now one uniform call, with no buffer rewrite. Buffers and VAOs stores this vertex layout in a vertex array object, and MVP Matrices replaces the clip-space positions with matrices.

BookNest's cover shaders loaded from script elements; click a cover to outline it in goldHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; cursor: pointer; }
</style>
<canvas id="wall" width="1200" height="280"></canvas>
<script type="x-shader/x-vertex" id="cover-vs">
#version 300 es
in vec2 aPosition; in vec2 aUV; in float aBook; uniform vec3 uColors[6];
out vec2 vUV; out vec3 vColor; flat out int vBook;
void main() { vUV = aUV; vBook = int(aBook); vColor = uColors[vBook];
              gl_Position = vec4(aPosition, 0.0, 1.0); }
</script>
<script type="x-shader/x-fragment" id="cover-fs">
#version 300 es
precision mediump float;
in vec2 vUV; in vec3 vColor; flat in int vBook; uniform int uSelected; out vec4 outColor;
void main() {
  bool edge = any(greaterThan(abs(vUV - 0.5), vec2(0.45)));   // outer 5% of the cover
  outColor = vec4(vBook == uSelected && edge ? vec3(1.0, 0.84, 0.3)
    : vColor * mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)) * mix(1.2, 0.7, vUV.y), 1.0);
}
</script>
<script>
const canvas = document.getElementById('wall');
const gl = canvas.getContext('webgl2');
function load(vsId, fsId) {       // read <script> text, trim so #version is on line 1
  const program = gl.createProgram();
  for (const [type, id] of [[gl.VERTEX_SHADER, vsId], [gl.FRAGMENT_SHADER, fsId]]) {
    const s = gl.createShader(type);
    gl.shaderSource(s, document.getElementById(id).textContent.trim());
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
    gl.attachShader(program, s);
  }
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
  gl.useProgram(program);
  return program;
}
const program = load('cover-vs', 'cover-fs');
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), covers.flatMap(rgb));
const uSelected = gl.getUniformLocation(program, 'uSelected');

const vertices = covers.flatMap((hex, i) =>  // x, y, u, v, book for 6 vertices per cover
  [[0, 0], [1, 0], [1, 1], [0, 0], [1, 1], [0, 1]].flatMap(([u, v]) =>
    [-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, u, 1 - v, i]));
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
for (const [name, size, offset] of [['aPosition', 2, 0], ['aUV', 2, 8], ['aBook', 1, 16]]) {
  const location = gl.getAttribLocation(program, name);
  gl.vertexAttribPointer(location, size, gl.FLOAT, false, 20, offset);   // 5 floats per vertex
  gl.enableVertexAttribArray(location);
}
function draw(selected) {
  gl.uniform1i(uSelected, selected);        // moving the outline is one uniform call
  gl.clearColor(0.93, 0.91, 0.87, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 5);
}
draw(2);                                    // outline Salt and Saffron
canvas.addEventListener('click', (e) => {
  const x = (e.offsetX / canvas.clientWidth) * 2 - 1;
  const book = Math.floor((x + 0.94) / 0.32);
  if (book >= 0 && book < 6) draw(book);
});
</script>