fragsplainer / The Triquetra
A Shader, Explained

The Triquetra

A three-petalled knot drawn from nothing but circles.

There is no shape stored anywhere in this program. Instead it stamps a single small dot 1000 times, walking each one a little further along a path traced by two spinning arms. Where the dots crowd together, a smooth triquetra appears.


It begins by framing the canvas — centering the coordinates and 0.80 zooming in so the figure fills the view. Then the loop runs, and the drawing begins.

what “framing the canvas” means

The raw input is fragCoord — the pixel’s position, counted in pixels. Dividing it by iResolution (the canvas size) rescales everything to a tidy 0 → 1 across the screen. These are the uv coordinates almost every shader starts from.

The next two lines correct for aspect ratio so circles stay round on a non-square canvas. Then we shift the origin to the centre, apply zoom, and nudge by shift — leaving (0,0) in the middle and equal distances in x and y. A comfortable sheet of paper to draw on.

Each step places its dot at the sum of two turning motions: an arm sweeping around at 1.0, and a hand riding on its tip, turning at its own rate of -2.0. The ratio between the two is what decides the symmetry — a clean 1 : −2 folds the path into three petals. Try nudging the hand to a positive number.

How one dot finds its place

Every step starts from the same seed point vec2(0.0, 0.2) and an angle a that creeps forward with time, plus a fraction of a full turn for each step: a = iTime + (i / 1000)·2π.

  • The arm — rotate the seed by a, then scale it by 1.0. A vector sweeping around the origin.
  • The hand — a second seed rotated by a · handSpin and added on top. A smaller wheel, turning at its own pace.

Add the two together and you have the exact spot where this step lays its dot.

center  = rotate(center, a) * armSpin;
center += rotate(vec2(0.0,0.2), a * handSpin);
and what does rotate() do?

rotate(v, a) simply multiplies v by a 2×2 rotation matrix built from the sine and cosine of the angle — the textbook way to spin a 2-D vector about the origin.

mat2 m = mat2(c, -s, s, c);
return m * v;
Why faint dots become solid lines

The dot itself is one smoothstep:

bw *= smoothstep(0.03, 0.031, distance(uv, center));

For any pixel, distance(uv, center) measures how far it sits from this step’s centre. Inside radius 0.03 the result is 0; past 0.031 it is 1. Because we multiply the running brightness every step, a pixel only stays bright if it dodged every single dot.

Since the angle steps by i / 1000 of a turn, the full loop traces exactly one revolution. Lower the count with 1000 and you watch the knot draw itself partway, like a pen lifted early; raise it and the dots fuse into one continuous line.

shaders /image.glsl
read-only · live values
1 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2
3 vec2 uv = fragCoord / iResolution.xy;
4 uv.x -= (iResolution.x - iResolution.y) / iResolution.x * 0.5;
5 uv.x *= iResolution.x / iResolution.y;
6
7 uv -= 0.5;
8 uv *= 0.80;
9 uv += shift;
10
11
12 float bw = 1.0;
13
14 for (int i = 0; i < 1000; i++) {
15 vec2 center = vec2(0.0, 0.2);
16 float a = iTime + float(i) / 1000.0 * PI * 2.0;
17 center = rotate(center, a) * 1.0;
18 center += rotate(vec2(0.0,0.2), a * -2.0);
19 bw *= smoothstep(0.03, 0.031, distance(uv, center));
20 }
21
22 fragColor = vec4(vec3(bw), 1.0);
23 }
24