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 theexp(-3 r)twist with3 log(r).logis 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/rrecedes to a vanishing point at the center, andfracttiles 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:
--- 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);
}
|
|
// 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
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
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);
}