Optional WebGL2 instanced rendering for circles, edges, heatmaps, and grid.
WebGL rendering is optional. BitZoom's default Canvas 2D renderer handles all dataset sizes. When WebGL2 is available, the viewer can offload geometry rendering (circles, edges, heatmaps, grid) to the GPU while keeping text (labels, legend, member counts) on a Canvas 2D overlay.
The implementation lives in bitzoom-gl-renderer.js (1,187 lines): seven shader programs, instanced draw calls for all geometry, and a two-pass density heatmap. No external dependencies. Toggled via the viewer's GL button or { webgl: true } in the constructor.
isWebGL2Available() probes support without side effects. If getContext('webgl2') fails, the viewer falls back silently to Canvas 2D.
WebGL and Canvas 2D have different strengths: WebGL draws thousands of circles and edges efficiently via instanced rendering; Canvas 2D renders text with correct sub-pixel anti-aliasing and font metrics. BitZoom uses both simultaneously.
The GL canvas sits behind the original canvas with pointer-events: none and an opaque background (alpha: false). The original canvas becomes a transparent overlay, receiving all mouse/touch/keyboard events. Hit testing uses the existing CPU spatial index — no GPU readback needed.
When WebGL is toggled off, the wrapper is unwrapped: the original canvas moves back into the DOM flow and resumes full Canvas 2D rendering.
Seven GLSL programs are compiled and linked at initialization:
| Program | Draw mode | Purpose |
|---|---|---|
_circleProgram | instanced TRIANGLE_STRIP (4 verts) | SDF circles with fill + stroke |
_circleProgram._glow | instanced TRIANGLE_STRIP (4 verts) | Selection/hover radial glow |
_edgeLineProgram | instanced TRIANGLE_STRIP (4 verts) | Straight line edges |
_edgeCurveProgram | instanced TRIANGLE_STRIP (34 verts) | Bezier curve edges |
_heatSplatProg | instanced TRIANGLE_STRIP (4 verts) | Density splats to FBO |
_heatResolveProg | TRIANGLE_STRIP (4 verts) | Fullscreen quad: FBO → screen |
_gridProgram | TRIANGLE_STRIP (4 verts) | Fullscreen quad: procedural grid |
All geometry uses TRIANGLE_STRIP. Four static vertex buffers are uploaded once at init:
| Buffer | Contents | Vertices |
|---|---|---|
_quadVBO | Unit quad [−1, 1]² | 4 (circle/glow/heatmap instances) |
_edgeLineQuadVBO | Line quad [0..1] × [−1, 1] | 4 (straight edge instances) |
_edgeCurveVBO | Curve strip t ∈ [0, 1] × [−1, 1] | 34 (16 segments × 2 sides + 2) |
_fsQuadVBO | Clip-space fullscreen quad | 4 (grid, heatmap resolve) |
Each supernode or leaf node is one instanced quad. The fragment shader computes a signed distance field (SDF) for anti-aliased circles with fill and stroke in a single pass:
float dist = length(v_uv) * (radius + 1.0);
float aa = smoothstep(radius + 1.0, radius - 0.5, dist); // outer edge
float strokeMask = smoothstep(radius - 2.0, radius - 0.5, dist); // stroke band
// blend fill and stroke colors, multiply by aa for anti-aliasing
No separate draw calls for fill and stroke. Selected nodes get a white stroke at full alpha. Importance-based opacity for dense views is baked into the instance data.
[cx, cy, radius, fillR, fillG, fillB, fillA, strokeR, strokeG, strokeB, strokeA]
Glow halos for selected/hovered nodes use the same quad VBO with a separate shader that renders a radial gradient falloff.
Two edge modes use different programs but share the same instance data layout (8 floats, 32 bytes per edge):
[startX, startY, endX, endY, r, g, b, a]
Each edge is one instanced quad (4 vertices). The vertex shader transforms the line quad to span from start to end with a perpendicular offset for line width.
Curves are evaluated entirely on the GPU. The curve strip VBO contains 34 vertices (16 segments × 2 sides) with t ∈ [0, 1] baked in. The vertex shader computes the cubic Bezier position and tangent per-vertex:
// Control points (same offsets as Canvas 2D)
vec2 c1 = start + dir * 0.3 + perp * len * 0.15;
vec2 c2 = start + dir * 0.7 + perp * len * 0.05;
// Cubic Bezier at t
vec2 p = (1-t)³·start + 3(1-t)²t·c1 + 3(1-t)t²·c2 + t³·end;
// Tangent for perpendicular offset (line width)
vec2 tang = 3(1-t)²·(c1-start) + 6(1-t)t·(c2-c1) + 3t²·(end-c2);
No per-frame CPU tessellation. No buffer allocation per edge. One instanced draw call renders all curves, regardless of count.
The density heatmap uses a two-pass framebuffer approach:
Pass 1 (splat): Each node is rendered as a Gaussian kernel quad to a quarter-resolution RGBA16F framebuffer with additive blending (gl.ONE, gl.ONE). The kernel function is k = (1 − dist²)² × weight. RGB channels carry the node's color; alpha carries the kernel weight.
Pass 2 (resolve): A fullscreen quad samples the FBO texture, normalizes the accumulated color by the weight channel, and maps intensity through maxW (the density normalization factor). Output alpha is capped at 0.7 to avoid obscuring the circles drawn on top.
The quarter-resolution FBO reduces fill rate by 16x while producing smooth density gradients. Requires EXT_color_buffer_half_float (~97% browser support).
The alternative splat mode skips the FBO entirely: additive-blended radial quads using the glow shader, each node drawn as a large quad (radius 50–400px) at low alpha (0.15). Simpler but less accurate.
The background grid is a single fullscreen quad. The fragment shader computes grid lines procedurally from the pan offset and grid spacing:
vec2 g = abs(fract(p / u_gridSize + 0.5) - 0.5) * u_gridSize;
float line = 1.0 - smoothstep(0.0, 1.0, min(g.x, g.y));
Grid lines fade out when the spacing falls below 4 pixels (matching the Canvas 2D renderer). No vertex data for the grid — pure math in the fragment shader.
The 7-layer render order matches the Canvas 2D renderer:
All geometry uses instanced rendering. A single shared dynamic buffer (_instanceVBO) is overwritten each draw call with DYNAMIC_DRAW. No persistent GPU buffers for instance data — positions are already in screen coordinates from layoutAll, so the instance arrays are rebuilt per frame from the current node/edge state.
| Trigger | Edges | Circles | Heatmap |
|---|---|---|---|
| Zoom / pan | rebuild | rebuild | rebuild |
| Level change | rebuild | rebuild | rebuild |
| Selection change | hilite pass only | glow + alpha | no |
| Weight / blend change | rebuild | rebuild | rebuild |
This "rebuild every frame" approach is simple and avoids state synchronization bugs. The instanced draw calls themselves are fast — the cost is dominated by JavaScript array construction, not GPU work.
If getContext('webgl2') fails, _initWebGL unwraps the dual-canvas layout and falls back silently to Canvas 2D. No error shown. The isWebGL2Available() probe creates and immediately destroys a temporary canvas to check support without side effects.
All events bind to the original canvas. The GL canvas has pointer-events: none. Hit testing uses the existing CPU spatial index. Toggling WebGL on or off at runtime is seamless — no state is lost.