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:
<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>
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.
<!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>