BookNest's shelf starts as six quads, one per book in catalog order, each vertex carrying a position, a cover coordinate (u, v from 0 to 1) and the cover color. The fragment shader darkens a spine strip and lights the top:
<canvas id="wall" width="1200" height="340" style="width:600px;height:170px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f']; // books 1-6
const gl = document.getElementById('wall').getContext('webgl2');
const program = GLH.program(gl, `#version 300 es
in vec2 aPosition; in vec2 aUV; in vec3 aColor; out vec2 vUV; out vec3 vColor;
void main() { vUV = aUV; vColor = aColor; gl_Position = vec4(aPosition, 0.0, 1.0); }`,
`#version 300 es
precision mediump float; in vec2 vUV; in vec3 vColor; out vec4 outColor;
void main() {
float spine = mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)); // dark strip at the left
float light = mix(1.2, 0.7, vUV.y); // brighter at the top
outColor = vec4(vColor * spine * light, 1.0);
}`);
const vertices = covers.flatMap((hex, i) => { // x, y, u, v, r, g, b per vertex
const rgb = [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
return [[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, ...rgb]);
});
GLH.buffer(gl, new Float32Array(vertices));
for (const [name, size, offset] of [['aPosition', 2, 0], ['aUV', 2, 8], ['aColor', 3, 16]]) {
const location = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 28, offset); // 7 floats each
}
gl.useProgram(program);
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 7);
window.__done = true;
</script>
One draw call paints all six covers, since they share a program and a buffer. MVP Matrices and Directional and Point Lighting move them onto a 3D shelf and light them.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; }
canvas { display: block; width: 100%; max-width: 600px; height: auto; }
</style>
<canvas id="wall" width="1200" height="340"></canvas>
<script>
function program(gl, vsSource, fsSource) { // compile, link, throw the logs on failure
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src.trim()); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(p, s);
}
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
return p;
}
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f']; // books 1-6
const gl = document.getElementById('wall').getContext('webgl2');
const prog = program(gl, `#version 300 es
in vec2 aPosition; in vec2 aUV; in vec3 aColor; out vec2 vUV; out vec3 vColor;
void main() { vUV = aUV; vColor = aColor; gl_Position = vec4(aPosition, 0.0, 1.0); }`,
`#version 300 es
precision mediump float; in vec2 vUV; in vec3 vColor; out vec4 outColor;
void main() {
float spine = mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)); // dark strip at the left
float light = mix(1.2, 0.7, vUV.y); // brighter at the top
outColor = vec4(vColor * spine * light, 1.0);
}`);
const vertices = covers.flatMap((hex, i) => { // x, y, u, v, r, g, b per vertex
const rgb = [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
return [[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, ...rgb]);
});
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], ['aColor', 3, 16]]) {
const location = gl.getAttribLocation(prog, name);
gl.enableVertexAttribArray(location);
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 28, offset); // 7 floats each
}
gl.useProgram(prog);
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 7); // one draw call paints all six covers
</script>