Live-coding editors recompile as you type: build a new program, and only if it links, delete the old one and switch; otherwise keep drawing with the old program and show the log:
const vs = `#version 300 es
out vec2 vUV; void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0;
gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`;
const head = '#version 300 es\nprecision mediump float;\nin vec2 vUV; out vec4 c;\n';
let current = GLH.load(gl, vs, head + 'void main() { c = vec4(vUV, 0.5, 1.0); }');
function rebuild(source) {
try {
const next = GLH.load(gl, vs, source); // throws on a compile or link error
gl.deleteProgram(current.program);
current = next;
return 'swapped';
} catch (e) {
return e.message.trim(); // the old program stays in use
}
}
console.log(rebuild(head + 'void main() { c = vec4(0.12, 0.37, vUV.y, 1.0); }'));
console.log(rebuild(head + 'void main() { c = vec4(0.12, 0.37, vUV.z, 1.0); }'));Output
swapped Fragment shader failed to compile: ERROR: 0:4: 'z' : vector field selection out of range
Call rebuild() from a debounced input handler. GLH.load() calls useProgram() only after a successful link, so a failed edit leaves the last good program drawing. Re-send uniforms after a swap, since a new program starts with every uniform at zero.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 10px; max-width: 600px; }
textarea { flex: 1; height: 190px; font: 11px/1.4 monospace; border: 1px solid #ccc; padding: 6px; }
canvas { width: 220px; height: 190px; flex: none; }
#status { max-width: 600px; margin-top: 6px; font-family: monospace; font-size: 11px; white-space: pre-wrap; }
.bad { color: #b5452f; } .good { color: #3f7d3a; }
</style>
<div class="row">
<textarea id="src" spellcheck="false"></textarea>
<canvas id="view" width="220" height="190"></canvas>
</div>
<div id="status"></div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const vs = `#version 300 es
out vec2 vUV; uniform float uTime;
void main() {
vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; // a big triangle, no buffer
gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0);
}`;
const head = '#version 300 es\nprecision mediump float;\nin vec2 vUV; out vec4 c;\nuniform float uTime;\n';
function load(fsSource) { // throws on a compile or link error
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fsSource]]) {
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);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
return { program, uTime: gl.getUniformLocation(program, 'uTime') };
}
let current = load(head + 'void main() { c = vec4(vUV, 0.5, 1.0); }');
function rebuild(source) {
try {
const next = load(source);
gl.deleteProgram(current.program); // only now is the old program thrown away
current = next;
return { ok: true, text: 'swapped' };
} catch (e) {
return { ok: false, text: e.message.trim() }; // the old program stays in use
}
}
const status = document.getElementById('status');
function report(r) { status.className = r.ok ? 'good' : 'bad'; status.textContent = r.text; }
// The book's two edits: the first builds, the second (vUV.z on a vec2) does not.
const log = [rebuild(head + 'void main() { c = vec4(0.12, 0.37, vUV.y, 1.0); }'),
rebuild(head + 'void main() { c = vec4(0.12, 0.37, vUV.z, 1.0); }')];
const editor = document.getElementById('src');
editor.value = head + `void main() {
float rings = sin(length(vUV - 0.5) * 40.0 - uTime * 3.0);
vec3 blue = vec3(0.12, 0.37, 0.55), gold = vec3(0.88, 0.6, 0.06);
c = vec4(mix(blue, gold, 0.5 + 0.5 * rings), 1.0);
}`;
report(rebuild(editor.value));
status.className = '';
status.textContent = 'Startup edits: ' + log.map(r => r.text.split('\n')[0]).join(' | ') +
'\nThen loaded the editor text: swapped. Type to edit (debounced 300 ms).';
let timer;
editor.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => report(rebuild(editor.value)), 300);
});
gl.bindVertexArray(gl.createVertexArray());
function frame(ms) {
gl.useProgram(current.program);
gl.uniform1f(current.uTime, ms / 1000); // re-sent every frame, so a new program gets it too
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>