Skip to content
Buse Turgay Sönmez

Design Engineer, Belgrade

Align Field

A grid of short strokes where each stroke has only two variables: its angle and its length. Unaligned strokes wander on slow noise. Three things align them and the effects stack: the upper half is settled, a diagonal front crosses on its own, and the pointer leaves a trail that fades in about two and a half seconds.

  • 2026
  • Shader
  • Pointer

Controls

Speed
1.00×
Pointer
1.00×
Density
16

Origin

Written as the hero of alignscale.com, the studio I co-founded. Here the colours come from the theme and the trail warms to the accent.

Source

scene.ts74 lines
precision mediump float;

#define TRAIL 8
#define ALIGNED 0.7853982

uniform vec2 uRes;
uniform float uTime;
uniform float uEnergy;
uniform vec3 uTrail[TRAIL];
uniform vec3 uGround;
uniform vec3 uInk;
uniform vec3 uAccent;
uniform float uDensity;
uniform float uInteraction;

float hash(vec2 p) {
  p = fract(p * vec2(123.34, 456.21));
  p += dot(p, p + 45.32);
  return fract(p.x * p.y);
}

float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
             mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}

void main() {
  vec2 uv = gl_FragCoord.xy / uRes;
  float aspect = uRes.x / uRes.y;
  vec2 p = vec2(uv.x * aspect, uv.y);

  vec2 g = p * uDensity;
  vec2 id = floor(g);
  vec2 f = fract(g) - 0.5;
  vec2 centre = (id + 0.5) / uDensity;

  // 1. The upper half is settled.
  float split = smoothstep(0.38, 0.62, centre.y);

  // 2. A front that crosses on its own.
  float band = sin(centre.x * 1.3 + centre.y * 0.8 - uTime * 0.2) * 0.5 + 0.5;
  float sweep = smoothstep(0.62, 0.98, band);

  // 3. What the pointer has touched lately.
  float trace = 0.0;
  for (int i = 0; i < TRAIL; i++) {
    vec2 d = centre - vec2(uTrail[i].x * aspect, uTrail[i].y);
    trace = max(trace, exp(-dot(d, d) * 20.0) * uTrail[i].z);
  }
  trace *= uInteraction;

  float align = clamp(split * 0.8 + sweep * 0.45 + trace, 0.0, 1.0);

  // Unaligned strokes wander; aligned ones settle on one diagonal and grow.
  float wild = noise(id * 0.37 + uTime * 0.06) * 6.2831853;
  float angle = mix(wild, ALIGNED, align);
  vec2 dir = vec2(cos(angle), sin(angle));

  float len = mix(0.13, 0.46, align);
  float w = mix(0.026, 0.05, align);
  float along = clamp(dot(f, dir), -len, len);
  float stroke = 1.0 - smoothstep(w, w + 0.035, length(f - dir * along));

  float density = stroke * mix(0.12, 0.78, align) * (0.85 + uEnergy * 0.15);
  // The trail also darkens the stroke, so the visitor's own trace reads even where the field is
  // already aligned and could not turn any further.
  density += stroke * trace * 0.22;

  vec3 colour = mix(uInk, uAccent, clamp(trace * 1.4, 0.0, 1.0));
  gl_FragColor = vec4(mix(uGround, colour, clamp(density, 0.0, 1.0)), 1.0);
}
--ground
oklch(0.985 0.003 85)
--surface
oklch(0.958 0.005 85)
--raised
oklch(1 0 0)
--ink
oklch(0.2 0.006 85)
--muted
oklch(0.43 0.008 85)
--faint
oklch(0.5 0.008 85)
--line
oklch(0.9 0.004 85)
--line-strong
oklch(0.6 0.006 85)
--accent
oklch(0.66 0.22 38)
--accent-ink
oklch(0.52 0.19 38)
--stage
oklch(0.152 0.004 85)
--radius-stage
36px
--radius-stage-sm
28px
--radius-media
24px
--radius-panel
16px
--radius-field
10px
--ease-out-expo
cubic-bezier(0.16, 1, 0.3, 1)

This week · W39One WebGL context, three leaksGeometry · 2026