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