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 (
wagainst its 2nd and 3rd harmonics) so the eye wanders instead of sitting dead center. - volume — a
breathefactor 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);
}
|
|
// 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 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;
// one turn per loop, the seamless base both geometries share
float spin = mod(iTime * (TAU / LOOP), TAU);
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);
}