Real-time halftone dot shader

June 2025

I've always been a fan of halftone art and how it brings an interesting visual rhythm to images while also invoking a sense of nostalgia for print media. I've been playing around with WebGL fragment shaders to create a real-time halftone dot effect that converts video into dot patterns.

Loading video...
Sparkles
6

Click and hold to switch between color and halftone dot modes

How it works

This shader recreates the classic halftone printing technique in real-time by dividing the video into a grid and converting each cell into a dot whose size corresponds to the brightness of that area.

The process

The video is divided into a regular grid of cells (adjustable 3–12 px), with each cell sampled at its center:

vec2 gridPos = floor(pix / gridSize);
vec2 cellCenter = (gridPos + 0.5) * gridSize;
vec2 uv = cellCenter / u_resolution.xy;
vec3 col = texture2D(u_texture, uv).rgb;

(you can also move the slider to 0 to see the source video)

if (u_gridSize <= 0.0) {
  vec2 uv = pix / u_resolution.xy;
  uv.y = 1.0 - uv.y;
  vec3 col = texture2D(u_texture, uv).rgb;
  gl_FragColor = vec4(col, 1.0);
  return;
}

Each cell's color is converted to grayscale using standard luminance weights, gently brightened, then mapped to a dot radius within a min/max range:

float gray = 0.3 * col.r + 0.59 * col.g + 0.11 * col.b;
gray = pow(gray, 0.7);      // subtle gamma lift
gray = clamp(gray * 1.3, 0.0, 1.0);

float maxRadius = gridSize * 0.7;
float minRadius = gridSize * 0.1;
float dotRadius = mix(minRadius, maxRadius, gray) * sparkleEffect;

Brighter areas get larger dots, darker areas get smaller (or no) dots.

Circle rendering

Each dot is rendered as a smooth circle with anti-aliased edges:

float circle(vec2 center, float radius, vec2 pos) {
  float dist = distance(center, pos);
  return 1.0 - smoothstep(radius - 0.1, radius + 0.1, dist);
}

The smoothstep function creates clean, smooth edges that avoid the jagged pixelation you'd get with hard cutoffs.

Sparkle effect

The shader includes an optional twinkling effect where about ~1% of dots randomly sparkle at different rates, adding gentle animation that brings the static pattern to life. It is disabled by default and can be toggled on.

Only a very small percentage of dots participate in the sparkle effect:

float rand = random(gridPos);
if (rand > 0.99) {  // Only ~1% of dots sparkle
  float phase = rand * 6.28318;  // Random phase offset
  float speed = 2.0 + rand2 * 3.0;  // Variable speed
  float pulse = sin(time * speed + phase) * 0.5 + 0.5;
  return 1.0 + pulse * 0.15;  // subtle brightness boost
}

Each sparkling dot gets unique timing characteristics (different phases and speeds), creating natural-looking variation where dots twinkle independently, avoiding synchronized blinking that would feel artificial.

The sparkle effect adapts to both viewing modes: adding brightness to colored dots in color mode, and creating white sparkles against black backgrounds in halftone mode.