Whirlpool (looping transport)

The palette sweep already repeats — the five palettes cycle every 5 * HOLD units of iTime — but the engine doesn’t know that, so it just lets iTime run forever. Tell it the period with a @loop directive and two things change: iTime wraps to [0, DURATION), and the controls panel grows a transport scrubber across the top. Drag it (or focus it and arrow-key) to seek anywhere in the loop — accrual pauses while you scrub and resumes from wherever you let go, video-player style.

For the loop to be seamless the whole frame must be periodic, so this changeset (shown via #+attr_diff from the palette sweep) also pins the swirl to exactly one revolution per loop — its old iTime * 0.3 drift never closed the circle. The period is then just the palette cycle, LOOP = 5 * HOLD = 12.5:

Diff: whirlpool-palette-sweep-shader → whirlpool-loop-shader
--- whirlpool-palette-sweep-shader
+++ whirlpool-loop-shader
@@ -1,46 +1,52 @@
 // 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
+
 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
   float r = length(p); // distance from the center
   float a = atan(p.y, p.x); // current angle

-  // twist harder near the center; wrap the time term to one full turn so
-  // cos/sin never see a large argument (one revolution looks identical)
-  a += 6.0 * exp(-3.0 * r) + mod(iTime * 0.3, 6.2831853);
+  const float HOLD = 2.5;        // seconds per palette (hold + wipe)
+  const float LOOP = 5.0 * HOLD; // one full cycle of all five palettes
+
+  // 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);

   // rebuild swirled coordinates back in <0,1>
   vec2 sw = vec2(cos(a), sin(a)) * r + 0.5;

   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
-  const float HOLD = 2.5; // seconds per palette (hold + wipe)
   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: