The Future’s So Bright
We Gotta Wear Shade(r)s
One PNG, one fragment shader, eighteen uniforms.
Drag any slider and watch the vanilla JavaScript rewrite itself.
Demo — WebGL2 + @paper-design/shaders
Heatmap: a bitmap turned into heat
Paper’s heatmap shader takes a still image and reads it as a heat source. Waves of colour rise through the silhouette, glow past its edges and pool in its interior. The source bitmap here is a plain product mockup — nothing about it is animated. Every frame of movement is arithmetic in the fragment shader, driven by the uniforms below.
Heatmap is only one of twenty-nine shaders in the library, and it is the one that happens to take an image. The rest generate everything from nothing: mesh and radial gradients, metaballs, voronoi and simplex and perlin noise, smoke rings, god rays, swirls, spirals, waves, warps, dithering and halftone, liquid metal, fluted glass, water and paper texture. Browse them all running live at shaders.paper.design , where each one has this same panel of controls and hands you the code to take away. The source, the GLSL and the React bindings live at github.com/paper-design/shaders under the Apache-2.0 licence. Every one of them mounts exactly the way this page does it — swap the fragment shader, swap the uniforms.
WebGL2 is not available in this browser, so the shader cannot run. The controls and the generated code below are still live.
Compiling…
Under the hood
Three blurs packed into one texture
A GPU cannot afford a wide blur per pixel per frame, so the work happens once, on the CPU, before anything is drawn. toProcessedHeatmap() loads the PNG onto a 2D canvas, pads it, converts it to greyscale, then runs three separate box blurs at three radii and packs each result into a different colour channel:
- R — a tight 5px blur: the contour, the crisp outline of the shape.
- G — a 150px blur: the wide halo the outer glow rides on.
- B — an 18px blur: the soft interior fill.
The shader then samples that one texture and reads the channels as separate distance fields. u_contour, u_outerGlow and u_innerGlow are simply how much of each channel is mixed into the heat value — which is finally used as the position along your colour gradient. Darker pixels in the original bitmap read as hotter, which is why the phone body lights up and the white screen stays cold.
// one texture, three fields
float shape = img[0];
float outerBlur = 1. - mix(1., img[1], shape);
float innerBlur = mix(img[1], 0., shape);
float contour = mix(img[2], 0., shape);
// your sliders weight each one
inner *= mix(0., 2., u_innerGlow);
inner += (u_contour * 2.) * contour;
outer *= mix(0., 5., pow(u_outerGlow, 2.));
// heat becomes a gradient position
float heat = clamp(inner + outer, 0., 1.);
float mixer = heat * u_colorsCount;