Whirlpool (palettes)
The four quadrant colors of the precision-stable whirlpool — black /
red / green / yellow — weren’t chosen; they fell out of vec3(e, 0), the
x-bit wired to red and the y-bit to green with blue pinned off. That puts
them on the four corners of one face of the RGB cube: a square, with edge
distance 1 and diagonal √2 — so they are not mutually equidistant.
Other four-color palettes are equally intrinsic to the same two-bit quadrant, and
each is just a different map from the antialiased quadrant q (q.x = x-bit,
q.y = y-bit) to a color:
square— the original black/red/green/yellow (one cube face).tetra— drive blue with the parityq.x XOR q.y(smooth formq.x + q.y - 2 q.x q.y). Black/magenta/cyan/yellow: a regular tetrahedron in the cube, every pair exactly √2 apart — the only mutually-equidistant set.rgbw— the complementary parity, giving the other tetrahedron: red/green/blue/white.gray— read the two bits as the integer2 q.x + q.yover 3: a four-step gray ramp, equidistant in 1D.cmy— the opposite cube face: white/cyan/magenta/yellow.
Shown via #+attr_diff as a changeset from the precision-stable whirlpool.
All five live inline in one palette(int i, vec2 q) dispatcher — GLSL ES has no
function pointers, so the runtime index is a plain branch — wired to a @select
input, a generic-uniform directive the engine turns into a uniform int plus a
dropdown in the shader’s hover-revealed controls panel, so you can switch
palettes live. It starts on tetra, the equidistant one:
--- whirlpool-stable-shader
+++ whirlpool-palettes-shader
@@ -1,23 +1,37 @@
+// 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
+}
+
+// a controls-panel dropdown picks the palette by index (default 1 = tetra)
+// @select iPalette 1 square tetra rgbw gray cmy
+uniform int iPalette;
+
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);
// rebuild swirled coordinates back in <0,1>
vec2 sw = vec2(cos(a), sin(a)) * r + 0.5;
vec3 col = vec3(0); // start with black
vec2 aa = fwidth(sw); // pixel footprint of the swirled coords
vec2 e = smoothstep(0.5 - aa, 0.5 + aa, sw); // antialiased quadrant edge
- col = vec3(e, 0); // same quadrants, now without the speckle
+ col = palette(iPalette, e); // palette chosen live from the Settings panel
// Output to screen
fragColor = vec4(col,1.0);
}
|
|
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);
// rebuild swirled coordinates back in <0,1>
vec2 sw = vec2(cos(a), sin(a)) * r + 0.5;
vec3 col = vec3(0); // start with black
vec2 aa = fwidth(sw); // pixel footprint of the swirled coords
vec2 e = smoothstep(0.5 - aa, 0.5 + aa, sw); // antialiased quadrant edge
col = vec3(e, 0); // same quadrants, now without the speckle
// Output to screen
fragColor = vec4(col,1.0);
}