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 parity q.x XOR q.y (smooth form q.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 integer 2 q.x + q.y over 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:

Diff: whirlpool-stable-shader → whirlpool-palettes-shader
--- 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);
 }
// settings
theme:
fx: