Split screens, minimaps and multi-view editors draw one scene into several regions per frame: set the viewport and an identical scissor box (clear() ignores the viewport), clear, change what differs, and draw. BookNest's overview and close-up panes differ only in two uniforms, applied as (aPosition - uCenter) * uZoom:
const { vertices, indices } = GLH.coverWall();
gl.bindVertexArray(GLH.vao(gl, attributes, vertices,
[['aPosition', 2], ['aUV', 2], ['aBook', 1]], indices));
gl.enable(gl.SCISSOR_TEST);
for (const [x, y, w, h, gray, center, zoom] of [
[0, 0, 840, 300, 0.9, [0, 0], [1, 0.8]], // overview of all six books
[850, 0, 350, 300, 0.2, [-0.17, 0], [3, 1.1]]]) { // close-up of book 3
gl.viewport(x, y, w, h);
gl.scissor(x, y, w, h);
gl.clearColor(gray, gray, gray, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
uniforms.uCenter(center);
uniforms.uZoom(zoom);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}One VAO bind serves both panes; MVP Matrices gives each pane a real camera through view and projection matrices.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; }
canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="wall" width="1200" height="300"></canvas>
<script>
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('wall').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 vec3 uColors[6]; uniform vec2 uCenter; uniform vec2 uZoom;
out vec2 vUV; out vec3 vColor;
void main() { vUV = aUV; vColor = uColors[int(aBook)];
gl_Position = vec4((aPosition - uCenter) * uZoom, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; in vec3 vColor; out vec4 outColor;
void main() {
float title = step(abs(vUV.y - 0.25), 0.04) * step(abs(vUV.x - 0.55), 0.3); // a title band
vec3 c = vColor * mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)) * mix(1.2, 0.7, vUV.y);
outColor = vec4(mix(c, vec3(0.97), title * 0.8), 1.0);
}`]]) {
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.flatMap(rgb));
const uCenter = gl.getUniformLocation(program, 'uCenter');
const uZoom = gl.getUniformLocation(program, 'uZoom');
// The cover wall as indexed quads, recorded once in a VAO.
const vertices = [], indices = [];
covers.forEach((hex, i) => {
for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) vertices.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 vao = gl.createVertexArray();
gl.bindVertexArray(vao);
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.enable(gl.SCISSOR_TEST);
function frame(ms) {
const focus = Math.sin(ms * 0.0005) * 0.8; // the close-up roams along the shelf
for (const [x, y, w, h, gray, center, zoom] of [
[0, 0, 840, 300, 0.9, [0, 0], [1, 0.8]], // overview of all six books
[850, 0, 350, 300, 0.2, [focus, 0], [3, 1.1]]]) { // close-up
gl.viewport(x, y, w, h);
gl.scissor(x, y, w, h); // clear() ignores the viewport
gl.clearColor(gray, gray, gray, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform2fv(uCenter, center);
gl.uniform2fv(uZoom, zoom);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>