WebGL is the Khronos Group's JavaScript binding of OpenGL ES, the embedded subset of OpenGL: gl.bufferData() is glBufferData(). WebGL 1.0 (March 2011) exposes OpenGL ES 2.0 and GLSL ES 1.00; WebGL 2.0 (January 2017) exposes OpenGL ES 3.0 and GLSL ES 3.00, adding 3D textures, instancing, vertex array objects, multiple render targets, uniform buffers and transform feedback. It grew from Vladimir Vukićević's Canvas 3D prototype at Mozilla (2006); Safari 15 10 completed WebGL 2 support in September 2021, the date of its Baseline status on MDN 102 .
WebGL is a state machine: you bind a buffer, and later calls act on whatever is bound. And it is immediate: it keeps no scene, so each frame you clear and issue every draw call again.
<!doctype html>
<style>
body { margin: 0; font: 13px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.wrap { position: relative; width: 100%; max-width: 600px; }
canvas { display: block; width: 100%; height: auto; }
.label { position: absolute; transform: translateX(-50%); text-align: center; white-space: nowrap; }
.label b { display: block; font-size: 13px; }
.label span { font-size: 11px; color: #666; }
</style>
<div class="wrap">
<canvas id="view" width="600" height="300"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const gl = canvas.getContext('webgl2');
// One tiny program draws every rectangle: the rectangle and colour arrive as uniforms.
function compile(vsSource, fsSource) {
const program = gl.createProgram();
for (const [type, source] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source.trim());
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader));
gl.attachShader(program, shader);
}
gl.bindAttribLocation(program, 0, 'aCorner');
gl.linkProgram(program);
return program;
}
const program = compile(`#version 300 es
in vec2 aCorner; // 0..1 unit square
uniform vec4 uRect; // x, y, width, height in pixels
uniform vec2 uCanvas;
void main() {
vec2 px = uRect.xy + aCorner * uRect.zw;
vec2 clip = px / uCanvas * 2.0 - 1.0;
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); // y grows downward, like CSS
}`, `#version 300 es
precision mediump float;
uniform vec4 uColor;
out vec4 outColor;
void main() { outColor = uColor; }`);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 1,0, 1,1, 0,0, 1,1, 0,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program); // WebGL is a state machine: this program stays current until changed
const uRect = gl.getUniformLocation(program, 'uRect');
const uColor = gl.getUniformLocation(program, 'uColor');
gl.uniform2f(gl.getUniformLocation(program, 'uCanvas'), canvas.width, canvas.height);
const x = year => 40 + (year - 2005) * 30; // 2005..2022 across the canvas
const events = [
{ year: 2006, title: 'Canvas 3D', note: 'Mozilla prototype', color: [0.55, 0.55, 0.6, 1] },
{ year: 2011, title: 'WebGL 1.0', note: 'OpenGL ES 2.0 · GLSL ES 1.00', color: [0.12, 0.37, 0.55, 1] },
{ year: 2017, title: 'WebGL 2.0', note: 'OpenGL ES 3.0 · GLSL ES 3.00', color: [0.87, 0.47, 0.13, 1] },
{ year: 2021, title: 'Safari 15', note: 'WebGL 2 everywhere', color: [0.25, 0.55, 0.3, 1] },
];
const wrap = document.querySelector('.wrap');
events.forEach((e, i) => {
const label = document.createElement('div');
label.className = 'label';
label.style.left = (x(e.year) / 6) + '%';
label.style.top = (i % 2 ? 214 : 44) / 3 + '%';
label.innerHTML = `<b>${e.title} (${e.year})</b><span>${e.note}</span>`;
wrap.appendChild(label);
});
function rect(x0, y0, w, h, color) {
gl.uniform4f(uRect, x0, y0, w, h);
gl.uniform4fv(uColor, color);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
// Immediate mode: WebGL keeps no scene, so every frame clears and issues every draw again.
function frame(time) {
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
rect(30, 148, 540, 4, [0.3, 0.3, 0.3, 1]); // the time axis
for (let year = 2005; year <= 2022; year++) rect(x(year) - 1, 144, 2, 12, [0.5, 0.5, 0.5, 1]);
rect(x(2011), 160, x(2017) - x(2011), 8, [0.12, 0.37, 0.55, 1]); // WebGL 1 era
rect(x(2017), 160, x(2022) - x(2017), 8, [0.87, 0.47, 0.13, 1]); // WebGL 2 era
events.forEach((e, i) => {
const top = i % 2 ? 150 : 88;
rect(x(e.year) - 1.5, top, 3, 62, e.color); // stem
rect(x(e.year) - 7, 143, 14, 14, e.color); // marker
});
const sweep = x(2005) + ((time / 40) % 510); // a moving "now" line
rect(sweep, 128, 2, 44, [0.8, 0.2, 0.2, 0.8]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>