Skip to content

Web graphicsInteractive widgets

A portrait in particles — an interactive field in WebGL

A photograph is broken into twenty thousand dots: the portrait assembles from particles and parts wherever the cursor passes. WebGL, one draw call, zero dependencies.

Period
2026
A man's portrait in profile made of glowing dots on a black background

Key project figures

particles in frame — donor pixels with the empty background dropped
~21K
libraries — a single gl.POINTS draw call
0
effect — particles from an image, not a 3D scene
2D

Scope of work on the project

Services

  • Breaking a photograph into particles
  • Donor preparation and tone correction
  • Cursor repulsion graded by depth
  • Adapted to the light and dark themes

About the project

Show a photograph so that it is not merely a photograph: the image is broken into twenty thousand dots, the portrait assembles from them and parts wherever the cursor passes.

A man's portrait in profile made of glowing dots on a black background
The project in detail

The condition is a lightweight 2D effect with no 3D scene and no build step: WebGL, one file, zero dependencies. All a page needs is the donor image and one line to load it.

The work

  • the donor's pixels are read once on the CPU and packed into a vertex buffer: index, position, angle and colour — seven floats per particle. The shader needs no texture at all; vertex texture fetch is unreliable on some GPUs;
  • the empty background never enters the buffer, as in the original. But the original cuts at 34/255 and throws away the subject's shadows along with the background — on a backlit portrait only the lit strip of the head survives. We cut an order of magnitude lower and give the shadows their own slider: a dot's "presence" is decoupled from its tone, otherwise the dark half of a face simply disappears;
  • the donor's tone is corrected inside the shader: an S-curve separates lights from darks and a floor removes background noise. A photograph as-is gives dots of almost identical size, and a face reads as a blob rather than a shape. Both are sliders on the page;
  • the widget finds its own frame by luminance mass, not by "is there a pixel above the threshold": a single grain of noise in a corner would otherwise stretch the crop across the whole donor and the subject assembles tiny;
  • the sampling step follows the display: sampling finer than the screen can show is pointless — bright areas clot into a patch and the midtones disappear;
  • on start the particles fly in from a random scatter and assemble into the original image — that is the live assembly of the picture;
  • the cursor pushes the dots within the image plane: the closer, the stronger the scatter, with smooth mouse following and a slight depth parallax. How hard a dot is pushed follows its depth — the front layer clears out of the way completely, the back one stays put and holds the subject's shape. Without that the cursor punched straight through the picture and left an empty hole;
  • the whole calculation lives in the vertex shader: a single gl.POINTS draw call, noise and positions computed on the GPU;
  • the field has no screen of its own: the canvas is transparent and lives on the site theme background; in the light theme the dots turn "inky" and the black donor background disappears;
  • performance guards: caps on devicePixelRatio and buffer area, rendering paused off-screen, and the system "reduce motion" setting respected.

The result

The portrait lives on the page by itself: the dots breathe, the cursor pushes the front layer aside, and no two moments look the same. This is not a video and not a sprite — twenty thousand particles are computed in the vertex shader in a single draw call.

The donor is set in the case configuration rather than in code: a logo, a product or another photograph can take its place. Preparing a donor is a separate script — crop, black point, separation of lights and darks. Without it a backlit portrait falls apart: the face blows out to a white patch and the shadow vanishes entirely.

The panel under the image is real, and all thirteen parameters are live: dot density and size, tone correction, depth, cursor radius and strength. The extremes give quite different pictures — from a dense cloud to a sparse dotted outline.

Have products you need visualized?

The average project budget starts from €500.

Smaller standalone tasks are quoted individually.