fragsplainer Fibonacci Windmill
A Shader, Explained

Fibonacci Windmill

A golden-ratio tiling that spirals outward forever — and loops seamlessly.

The Fibonacci tiling — the one that traces a golden spiral through nested squares of side 1, 1, 2, 3, 5, 8, 13… — drawn live and forever zooming outward. Because each turn of the spiral is exactly φ² bigger than the last, the animation can zoom out by that factor and land back where it started: a seamless, endless windmill. It’s all white lines on black, and every line is math, not geometry.


There is no list of rectangles stored anywhere. Each square is a signed distance field evaluated at the current pixel, and the picture is just the nearest of all those fields. New squares are added with min — the union operator for distance fields.

Drawing a square's outline → technical explainer

drawSquare returns the distance to a rectangle, then folds it with abs() so only the edge is near zero — an outline instead of a filled box. The line width 2.10 control sets how fat that outline is.

float dist = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
return abs(dist + w*2.0) - w;   // distance to the outline, width w
Growing squares the Fibonacci way

The loop carries four numbers in an ivec4 and grows them by the Fibonacci rule — each new side is the sum of the previous two — stamping a square on alternating axes each step. No array, no recursion; the sequence 2, 3, 5, 8, 13, 21, 34, 55 simply unrolls itself.

for (ivec4 n = ivec4(1,1,2,3); n.z < 56; ) {
    bw = min(bw, drawSquare(p, vec2(0,n.y), vec2(n.zw), lineWidth));
    n.xz += n.yw;
    bw = min(bw, drawSquare(p, vec2(n.x,0), vec2(n.zw), lineWidth));
    n.yw += n.xz;
}
Four windmills for the price of one → technical explainer

The spiral is drawn once, in a single quadrant — then the plane is folded into four-fold rotational symmetry, so the one spiral appears spun into all four corners. Convert the point to polar, wrap its angle into a single 90° wedge, and convert back:

p = length(p) * cos( mod(atan(p.y,p.x), PI/2.) - vec2(PI/2., 0) );

That’s the move that earns the name windmill. (Trick courtesy of Fabrice Neyret.)

Why it loops without a seam → technical explainer

Time t sweeps from 0 to 2 and back. Across one sweep the zoom shrinks by exactly 0.618² ≈ φ⁻² — one whole step of the sequence — so the frame at the end is geometrically identical to the start, just shifted one square along. New 89×89 and 144×144 squares slide in from the edges right as the old ones scale past, hiding the loop point. A gentle rotation zoom 1.10 and a touch of fisheye 1.50 fisheye finish the camera.

shaders /image.glsl
↗ Shadertoy
1 const float PI = 3.14159;
2
3 // Signed distance to the *outline* of a rectangle: distance to the box, then
4 // pushed out by the line width and folded with abs() so only the edge is near zero.
5 float drawSquare(vec2 p, vec2 bL, vec2 tR, float w) {
6 vec2 d = max(bL - p, p - tR);
7 float dist = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
8 dist += w*2.0;
9 return abs(dist) - w;
10 }
11
12 mat2 rot(float k) {
13 float s = sin(k);
14 float c = cos(k);
15 return mat2(
16 c, -s,
17 s, c
18 );
19 }
20
21 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
22 vec2 uv = (fragCoord - 0.5 * R) / min(R.x, R.y);
23
24 float bw = 1.0;
25
26
27 float cycleTime = 3.0; // seconds per half-cycle
28 float t = mod(iTime*1.5, cycleTime*2.0) / cycleTime; // sweeps 0 → 2
29 uv *= rot(PI/4.0 + t*PI/4.0); // slow turn
30 uv *= 1.0 + length(uv)/8.0*1.50; // mild barrel/fisheye
31
32 float zoom = pow(0.618*0.618, t/2.0); // zoom out by φ² each cycle
33 zoom *= 1.10/50.;
34 float lineWidth = 0.005 * 2.10 / zoom;
35 vec2 p = uv / zoom;
36
37
38
39 // Fold the plane into 4-fold rotational symmetry (trick from Fabrice):
40 // re-express p in polar, wrap the angle into one quadrant, return to xy.
41 p = length(p) * cos( mod( atan(p.y,p.x), PI/2. ) - vec2(PI/2.,0) );
42
43
44 // Two 1×1 seed squares that fade out as the cycle restarts
45 float outline = drawSquare(p, vec2(0,0), vec2(1,1), lineWidth);
46 bw = min(bw, outline + (1.0 - smoothstep(0.0, 1.0, t)));
47 outline = drawSquare(p, vec2(1, 0), vec2(2,1), lineWidth);
48 bw = min(bw, outline + (1.0 - smoothstep(0.5, 1.5, t)));
49
50
51 // Grow squares the Fibonacci way: each new side is the sum of the last two,
52 // alternating which axis it extends along. 2,3,5,8,13,21,34,55…
53 for (ivec4 n = ivec4(1,1,2,3); n.z < 56; ) {
54 bw = min(bw, drawSquare(p, vec2(0,n.y), vec2(n.zw), lineWidth));
55 n.xz += n.yw;
56 bw = min(bw, drawSquare(p, vec2(n.x,0), vec2(n.zw), lineWidth));
57 n.yw += n.xz;
58 }
59
60
61 // An 89×89 square sliding in from above, then a 144×144 from the right
62 float newSquareY = mix(55.0 + 89.0, 55.0, smoothstep(0.0, 1.0, t));
63 bw = min(bw, drawSquare(p, vec2(0, newSquareY), vec2(89.0, newSquareY + 89.0), lineWidth));
64
65 float newSquareX = mix(89.0 + 144.0, 89.0, smoothstep(1.0, 2.0, t));
66 bw = min(bw, drawSquare(p, vec2(newSquareX, 0), vec2(newSquareX + 144.0, 144.0), lineWidth));
67
68 bw = smoothstep(1.0/R.x, 0.0, bw); // outline → crisp white line
69 fragColor = vec4(vec3(bw), 1.0);
70 }
71