BitZoom

Optional WebGL2 instanced rendering for circles, edges, heatmaps, and grid.

Overview

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.

WebGL2 is supported in all modern browsers (Chrome, Firefox, Safari, Edge). isWebGL2Available() probes support without side effects. If getContext('webgl2') fails, the viewer falls back silently to Canvas 2D.

Dual Canvas Layout

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.

wrapper div (position: relative) <canvas> WebGL2 grid, edges, heatmap, circles — pointer-events: none <canvas> Canvas 2D labels, counts, legend, reset — transparent background, all events behind on top
The wrapper div replaces the original canvas in the DOM flow, preserving CSS grid/flex layout.

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.

Shader Programs

Seven GLSL programs are compiled and linked at initialization:

ProgramDraw modePurpose
_circlePrograminstanced TRIANGLE_STRIP (4 verts)SDF circles with fill + stroke
_circleProgram._glowinstanced TRIANGLE_STRIP (4 verts)Selection/hover radial glow
_edgeLinePrograminstanced TRIANGLE_STRIP (4 verts)Straight line edges
_edgeCurvePrograminstanced TRIANGLE_STRIP (34 verts)Bezier curve edges
_heatSplatProginstanced TRIANGLE_STRIP (4 verts)Density splats to FBO
_heatResolveProgTRIANGLE_STRIP (4 verts)Fullscreen quad: FBO → screen
_gridProgramTRIANGLE_STRIP (4 verts)Fullscreen quad: procedural grid

All geometry uses TRIANGLE_STRIP. Four static vertex buffers are uploaded once at init:

BufferContentsVertices
_quadVBOUnit quad [−1, 1]²4 (circle/glow/heatmap instances)
_edgeLineQuadVBOLine quad [0..1] × [−1, 1]4 (straight edge instances)
_edgeCurveVBOCurve strip t ∈ [0, 1] × [−1, 1]34 (16 segments × 2 sides + 2)
_fsQuadVBOClip-space fullscreen quad4 (grid, heatmap resolve)

Circle Rendering (SDF)

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.

Instance layout (11 floats, 44 bytes)

[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.

Edge Rendering

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]

Straight lines

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.

Bezier curves (GPU tessellation)

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.

Lines 4 verts/instance, 1 draw call Curves 34 verts/instance, GPU tessellated

Density Heatmap (Two-Pass)

The density heatmap uses a two-pass framebuffer approach:

Pass 1: Splat Gaussian kernels → FBO RGBA16F, quarter-res additive blend (ONE, ONE) texture Pass 2: Resolve fullscreen quad reads FBO normalize by weight intensity via maxW, alpha ≤ 0.7 screen

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.

Procedural Grid

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.

Render Order

The 7-layer render order matches the Canvas 2D renderer:

1 Clear background color from CSS 2 Grid fullscreen quad, procedural lines 3 Normal edges instanced lines or curves 4 Heatmap density FBO + resolve, or additive splat 5 Highlighted edges selected/hovered, thicker 6 Glow halos radial gradient for selected/hovered 7 Circles instanced SDF circles (topmost geometry)
Labels, legend, and member counts are drawn by Canvas 2D on the transparent overlay.

Instancing Strategy

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.

Buffer rebuild triggers

TriggerEdgesCirclesHeatmap
Zoom / panrebuildrebuildrebuild
Level changerebuildrebuildrebuild
Selection changehilite pass onlyglow + alphano
Weight / blend changerebuildrebuildrebuild

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.

Fallback

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.

Canvas 2D vs WebGL Visual Comparison · How It Works · WebGPU Compute · Developer Guide