Every library in this section is free and open source; the differences are maturity, fallback and fit:
| Library (version) | License | Maturity (September 2026) | WebGL fallback | Use it for |
|---|---|---|---|---|
| Three.js 30,815 (0.186.1) | MIT | Mature; about 115,900 stars | Yes, built in | 3D scenes (Three.js) |
| Babylon.js 89,603 (9.28.0) | Apache-2.0 | Mature; about 26,100 stars | Yes, separate engine | 3D apps, games (Babylon.js) |
| PixiJS 313,967 (8.21.0) | MIT | Mature; about 48,200 stars | Yes, automatic | 2D games and sprites |
| Orillusion (0.9.2) | MIT | Beta, not for commercial use | No | Experiments |
| use.gpu (0.20.0) | MIT | Alpha, GitLab 377 | No | Reactive GPU plots |
| TypeGPU 320,420 (0.12.6) | MIT | Pre-1.0; about 3,200 stars | No | Typed raw WebGPU |
| wgpu-matrix 481 (3.4.2) | MIT | Stable, small | Not needed | Matrix math |
| ONNX Runtime Web 231,552 (1.30.0) | MIT | Mature (Microsoft) | CPU WebAssembly | Running ONNX models |
| Transformers.js 1,113 (4.3.0) | Apache-2.0 | Mature; about 16,300 stars | CPU WebAssembly | Hub models by task |
Commercial tools sit on the same stack rather than replacing it: Unity 6 2,152 's Web builds offer a WebGPU backend (marked experimental through the 6.5 manual, no longer in 6.6), and hosted inference APIs take over when a model is too large. For BookNest, the choice would be Three.js or Babylon.js for its 3D store, PixiJS for a 2D game, and Transformers.js for search by meaning, with raw WebGPU (this chapter's code) kept for custom compute.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="390"></canvas>
<canvas id="labels" width="600" height="390"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
// name (version), licence, stars in thousands (null: not given), maturity (0 alpha .. 3 mature), fallback, use
const libs = [
['Three.js 0.186.1', 'MIT', 115.9, 3, 'yes, built in', '3D scenes'],
['PixiJS 8.21.0', 'MIT', 48.2, 3, 'yes, automatic', '2D games, sprites'],
['Babylon.js 9.28.0', 'Apache-2.0', 26.1, 3, 'yes, separate engine', '3D apps, games'],
['Transformers.js 4.3.0', 'Apache-2.0', 16.3, 3, 'CPU WebAssembly', 'Hub models by task'],
['ONNX Runtime Web 1.30.0', 'MIT', null, 3, 'CPU WebAssembly', 'ONNX models'],
['TypeGPU 0.12.6', 'MIT', 3.2, 1, 'no', 'typed raw WebGPU'],
['wgpu-matrix 3.4.2', 'MIT', null, 2, 'not needed', 'matrix math'],
['Orillusion 0.9.2', 'MIT', null, 1, 'no', 'experiments'],
['use.gpu 0.20.0', 'MIT', null, 0, 'no', 'reactive GPU plots']];
const maturityColor = [[0.80, 0.35, 0.25], [0.88, 0.60, 0.15], [0.55, 0.70, 0.30], [0.16, 0.56, 0.30]];
const maturityName = ['alpha', 'beta / pre-1.0', 'stable, small', 'mature'];
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const boxes = [];
libs.forEach(([, , stars, maturity], i) => {
const y = 40 + i * 36;
boxes.push([8, y - 4, 584, 32, ...(i % 2 ? [0.95, 0.94, 0.91] : [1, 1, 1]), 6]);
boxes.push([170, y + 6, 10, 10, ...maturityColor[maturity], 5]); // maturity dot
if (stars) boxes.push([188, y + 3, stars / 120 * 115, 16, 0.20, 0.45, 0.75, 3]); // stars bar
});
ink.font = 'bold 12px system-ui, sans-serif'; ink.fillStyle = '#222';
ink.fillText('library', 12, 26); ink.fillText('maturity, GitHub stars', 170, 26); ink.fillText('licence', 360, 26); ink.fillText('WebGL fallback / use it for', 440, 26);
libs.forEach(([name, licence, stars, maturity, fallback, use], i) => {
const y = 40 + i * 36;
ink.font = 'bold 11.5px system-ui, sans-serif'; ink.fillStyle = '#222'; ink.fillText(name, 14, y + 15);
ink.font = '11px system-ui, sans-serif'; ink.fillStyle = '#444';
ink.fillText(stars ? `${stars}k` : maturityName[maturity], stars ? 194 + stars / 120 * 115 : 190, y + 15);
ink.fillText(licence, 360, y + 15);
ink.fillText(fallback, 440, y + 9); ink.fillStyle = '#1f4f8a'; ink.fillText(use, 440, y + 23);
});
ink.font = '11px system-ui, sans-serif'; ink.fillStyle = '#555';
ink.fillText('GitHub, npm and READMEs, 26 September 2026. All free and open source; they differ in maturity, fallback and fit.', 12, 372);
ink.fillText('BookNest: Three.js or Babylon.js for the 3D store, PixiJS for a 2D game, Transformers.js for search by meaning.', 12, 386);
const module = device.createShaderModule({ code: `
struct Box { rect: vec4f, style: vec4f } // style: r, g, b, corner radius
@group(0) @binding(0) var<storage> boxes: array<Box>;
struct Out { @builtin(position) pos: vec4f, @location(0) local: vec2f,
@location(1) @interpolate(flat) i: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let corner = vec2f(f32(v & 1), f32(v >> 1)); // triangle-strip corners
let r = boxes[i].rect;
let px = r.xy + corner * r.zw;
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 195, 0, 1), corner * r.zw, i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let b = boxes[in.i];
let half = b.rect.zw / 2;
let q = abs(in.local - half) - half + b.style.w; // signed distance to a rounded box
let d = length(max(q, vec2f(0))) + min(max(q.x, q.y), 0) - b.style.w;
let a = clamp(0.5 - d, 0, 1);
return vec4f(b.style.rgb * a, a); // premultiplied alpha
}` });
const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };
const pipeline = device.createRenderPipeline({ layout: 'auto',
vertex: { module }, primitive: { topology: 'triangle-strip' },
fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] } });
const data = new Float32Array(boxes.flat());
const buffer = device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(buffer, 0, data);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline);
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer } }] }));
pass.draw(4, boxes.length);
pass.end();
device.queue.submit([encoder.finish()]);
}
main();
</script>