Whirlpool (spiral & tunnel)

Every whirlpool so far has used the same polar move: convert the pixel to (r, a), nudge the angle, rebuild cartesian coords. The nudge has always been a + 6 * exp(-3 r)= — a twist that dies off away from the center. Swap that one term and the geometry changes completely. This page adds a @select to flip between two classic polar remaps live (shown via #+attr_diff as a changeset from the looping transport):

  • spiral — replace the exp(-3 r) twist with 3 log(r). log is scale-invariant, so the quadrant edges become true logarithmic spirals — the same arm shape repeats at every radius, the way galaxy arms do.
  • tunnel — stop rebuilding cartesian coords at all. Feed (a, 1/r) straight into the quadrant lookup: the angle wraps around the wall, 1/r recedes to a vanishing point at the center, and fract tiles the quadrant down the bore.

The palette cycling, sweep wipe, and @loop transport all carry over untouched. Rotating a log spiral is the same as scaling it, so spinning the spiral makes its arms drain inward like a whirlpool — it turns five whole revolutions per loop while the tunnel marches one; either way whole turns keep the seam seamless and the period is still LOOP = 12.5. It starts on spiral:

Diff: whirlpool-loop-shader → whirlpool-spiral-tunnel-shader
--- whirlpool-loop-shader
+++ whirlpool-spiral-tunnel-shader
@@ -1,52 +1,69 @@
 // 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

+// a controls-panel dropdown picks the polar remap (default 0 = spiral arms)
+// @select GEOMETRY 0 spiral tunnel
+uniform int GEOMETRY;
+
 void mainImage( out vec4 fragColor, in vec2 fragCoord )
 {
   vec2 uv = fragCoord/iResolution.xy; // <0,1>

-  vec2 p = uv - 0.5; // recenter so the swirl pivots on the middle
+  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 LOOP = 5.0 * HOLD; // one full cycle of all five palettes
+  const float TAU  = 6.2831853;

-  // twist harder near the center; spin exactly one full turn per loop so the
-  // rotation closes seamlessly (the old iTime*0.3 never landed on a whole turn)
-  a += 6.0 * exp(-3.0 * r) + mod(iTime * (6.2831853 / LOOP), 6.2831853);
+  // one turn per loop, the seamless base both geometries share
+  float spin = mod(iTime * (TAU / LOOP), TAU);

-  // rebuild swirled coordinates back in <0,1>
-  vec2 sw = vec2(cos(a), sin(a)) * r + 0.5;
+  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 aa = fwidth(sw); // pixel footprint of the swirled coords
   vec2 e = smoothstep(0.5 - aa, 0.5 + aa, sw); // antialiased quadrant edge

   // 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;
   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);

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