Whirlpool (living drain)

The spiral drains, but mechanically: a fixed eye, a constant pull, a metronome spin. A real drain sloshes — the vortex wanders, surges, and breathes. This changeset (from the spiral & tunnel) adds that life with three time-varying wobbles, then keeps the @loop seamless with one trick: every wobble is a sine at a whole multiple of the loop frequency w = TAU / LOOP, so each returns to its start at iTime = LOOP.

  • meander — offset the center along a Lissajous path (w against its 2nd and 3rd harmonics) so the eye wanders instead of sitting dead center.
  • volume — a breathe factor swells and ebbs the pull, so the funnel tightens and loosens like the water level is changing.
  • surge — an extra sine on the angle makes the rotation speed up and stall, so the spin never feels like clockwork.

The tunnel was a detour; this page drops the @select and its branch to commit to the flat spiral drain, and gives that spiral all three wobbles. The base rotation is still five whole turns per loop, so LOOP = 25 holds:

Diff: whirlpool-spiral-tunnel-shader → whirlpool-living-drain-shader
--- whirlpool-spiral-tunnel-shader
+++ whirlpool-living-drain-shader
@@ -1,69 +1,104 @@
 // no function pointers in GLSL ES — palettes inline, dispatched by index
 // each maps the antialiased quadrant q (q.x = x-bit, q.y = y-bit) to a color
 vec3 palette(int i, vec2 q){
   if(i == 0) return vec3(q, 0.0);                             // square
   if(i == 1) return vec3(q, q.x + q.y - 2.0*q.x*q.y);         // tetra
   if(i == 2) return vec3(q, 1.0 - (q.x + q.y - 2.0*q.x*q.y)); // rgbw
   if(i == 3) return vec3((2.0*q.x + q.y) / 3.0);              // gray
   return vec3(1.0 - q.x, 1.0 - q.y, 1.0);                     // cmy
 }

-// iTime is periodic: wrap it and show a transport scrubber in the panel.
-// 12.5 = 5 palettes * HOLD (2.5) — one full palette cycle.
-// @loop 12.5
+// swirl a centered position into its antialiased 2-bit quadrant (q.x = x-bit,
+// q.y = y-bit) — the texture the palettes color. factored out so the peeling
+// layer can sample the same swirl from a pulled-away position.
+vec2 quad(vec2 pp, float twist, float turn){
+  float rr = length(pp);
+  float ang = atan(pp.y, pp.x) + twist * log(rr) + turn;
+  vec2 s = vec2(cos(ang), sin(ang)) * rr + 0.5;
+  vec2 fw = fwidth(s); // pixel footprint of the swirled coords
+  return smoothstep(0.5 - fw, 0.5 + fw, s); // antialiased quadrant edge
+}

-// a controls-panel dropdown picks the polar remap (default 0 = spiral arms)
-// @select GEOMETRY 0 spiral tunnel
-uniform int GEOMETRY;
+// iTime is periodic: wrap it and show a transport scrubber in the panel.
+// 25 = 5 palettes * HOLD (5.0) — one full palette cycle.
+// @loop 25

 void mainImage( out vec4 fragColor, in vec2 fragCoord )
 {
   vec2 uv = fragCoord/iResolution.xy; // <0,1>

-  vec2 p = uv - 0.5; // recenter so the figure pivots on the middle
-  float r = length(p); // distance from the center
-  float a = atan(p.y, p.x); // current angle
-
-  const float HOLD = 2.5;        // seconds per palette (hold + wipe)
+  const float HOLD = 5.0;        // seconds per palette (slow, viscous transfer)
   const float LOOP = 5.0 * HOLD; // one full cycle of all five palettes
   const float TAU  = 6.2831853;
+  float w = TAU / LOOP;          // loop fundamental: one cycle over the loop
+
+  // every wobble below is a sine at a WHOLE multiple of w, so it lands back on
+  // its start at iTime = LOOP and the loop stays seamless.

-  // one turn per loop, the seamless base both geometries share
-  float spin = mod(iTime * (TAU / LOOP), TAU);
+  // the eye wanders on a Lissajous path instead of sitting dead center, so
+  // the drain meanders as if the water were sloshing
+  vec2 wander = 0.06 * vec2(sin(w*iTime) + 0.5*sin(2.0*w*iTime + 1.3),
+                            cos(w*iTime) + 0.5*cos(3.0*w*iTime + 0.7));

-  vec2 sw; // coords (in <0,1>) handed to the quadrant/palette lookup
-  if (GEOMETRY == 0) {
-    // SPIRAL ARMS: twist the angle by log(r) instead of the whirlpool's
-    // exp(-r). log is scale-invariant, so the quadrant edges become true
-    // logarithmic spirals — the same arm shape repeats at every radius.
-    // Rotating a log spiral == scaling it, so spinning it makes the arms
-    // drain inward like a whirlpool; 5 whole turns/loop reads fast yet seamless.
-    a += 3.0 * log(r) + spin * 5.0;
-    sw = vec2(cos(a), sin(a)) * r + 0.5; // rebuild swirled coords in <0,1>
-  } else {
-    // TUNNEL: drop the cartesian rebuild. Read (angle, 1/r) as the texture
-    // coords — angle wraps the wall, 1/r recedes to a vanishing point at the
-    // center, +spin marches the wall toward you; fract tiles the quadrant.
-    sw = fract(vec2(a / TAU + spin / TAU, 0.35 / r + spin / TAU));
-  }
+  vec2 p = uv - 0.5 - wander; // recenter on the wandering eye

-  vec2 aa = fwidth(sw); // pixel footprint of the swirled coords
-  vec2 e = smoothstep(0.5 - aa, 0.5 + aa, sw); // antialiased quadrant edge
+  // the drain breathes: its pull swells and ebbs over the loop (volume)
+  float breathe = 1.0 + 0.6 * sin(2.0*w*iTime);
+
+  // one turn per loop, the seamless base of the spin
+  float spin = mod(iTime * w, TAU);
+
+  // SPIRAL ARMS: twist by log(r); rotating a log spiral == scaling it, so
+  // spinning it drains the arms inward like a whirlpool. breathe varies how
+  // hard it twists (volume); the extra sin makes the spin surge and stall.
+  float twist = 3.0 * breathe; // how hard it winds (volume)
+  float turn  = spin * 5.0 + 0.6 * sin(3.0 * w * iTime); // 5 turns/loop + surge
+
+  vec2 e = quad(p, twist, turn); // the new layer, lying flat on the surface

   // cycle the 5 palettes, one sweep wipe per slot
   float T = iTime / HOLD;
   int i0 = int(mod(floor(T), 5.0)); // current palette
   int i1 = int(mod(floor(T) + 1.0, 5.0)); // next palette
-  float sweep = smoothstep(0.6, 1.0, fract(T)); // hold, then wipe the last 40%
-
-  // diagonal wipe coord — try uv.x, or atan(p.y,p.x)/6.2831853+0.5
-  float swp = (uv.x + uv.y) * 0.5;
+  // viscous descent: the wipe eases across most of the slot, slow at the rim
+  // and creeping as it nears the eye, so the palette oozes down the drain
+  float sweep = smoothstep(0.1, 1.0, fract(T));
+
+  // drain-in wipe: the new palette floods in from the rim and collapses
+  // toward the eye last, so the transition itself drains. p tracks the
+  // wandering center, so the drain point meanders with the vortex.
+  // liquid wobble: ripple the wipe's radius with two drifting lobe sets so the
+  // front is a wavy edge, not a clean circle (whole lobes + harmonics keep it
+  // seamless). ea is the raw angle, taken before the spiral twisted a.
+  float ea = atan(p.y, p.x);
+  // the wave amplitude swells and ebbs as the front descends (sweep) and
+  // flows over time, so the wobble breathes on the way down
+  float amp = 0.09 + 0.08 * sin(sweep * 12.566 - 4.0 * w * iTime);
+  float wob = 1.0 + amp * sin(ea * 5.0 + 3.0 * w * iTime)
+                  + amp * 0.6 * sin(ea * 8.0 - 2.0 * w * iTime);
+  float swp = 1.0 - length(p) / (0.8 * wob); // wavy front: ~1 eye, ~0 rim
   float band = fwidth(swp) * 1.5; // antialias the wipe edge
   // 1 once the sweep has passed this pixel
   float toNew = smoothstep(swp - band, swp + band, sweep);

-  vec3 col = mix(palette(i0, e), palette(i1, e), toNew);
+  // PEEL: the old layer lifts off the surface and is pulled down the drain,
+  // revealing the new one underneath. lift leads the fade, so the old sheet
+  // visibly slides toward the eye (and swirls in) before it dissolves.
+  float lift = smoothstep(swp - 0.45, swp + band, sweep); // 0 ahead, 1 peeled
+  vec2 pe = p * (1.0 - 0.6 * lift); // drag old layer toward the eye
+  vec3 oldCol = palette(i0, quad(pe, twist, turn + lift * 3.0)); // sliding away
+  vec3 col = mix(oldCol, palette(i1, e), toNew);
+
+  // bright crystallized edge: a sharp ridge of light rides the wipe front
+  // (where swp ~ sweep), faceted by high-frequency angular glints so it
+  // sparkles like ice (whole-number glint frequencies/drift -> seamless)
+  float ridge = 1.0 - smoothstep(0.0, band * 3.0, abs(sweep - swp));
+  ridge *= smoothstep(0.0, 0.03, sweep); // only while a wipe is travelling
+  float glint = sin(ea * 40.0 + 6.0 * w * iTime)
+              * sin(ea * 23.0 - 4.0 * w * iTime);
+  glint = pow(0.5 + 0.5 * glint, 6.0); // sharpen the bands into sparkles
+  col += ridge * (0.25 + 0.75 * glint); // additive white-hot crystal rim

   // Output to screen
   fragColor = vec4(col,1.0);
 }
// settings
theme:
fx: