AI CSS Animations

Spiral Galaxy

A tilted spiral galaxy turns with your throughput: stars orbit through glowing arms around a warm core, and faster token rates spin it up and fill the arms with stars.

Also known as: rotating galaxy, spiral arms.

HTML + JS

A canvas and a module script, nothing to install. Set canvas.dataset.tps from your stream, or call canvas.token() once per token.

<canvas id="shader-spiral-galaxy" data-tps="400" data-color="#b79cff" style="display:block;width:100%;height:240px;border-radius:12px"></canvas>
<script type="module">
  // Set canvas.dataset.tps from your stream, or call canvas.token() per token.
  const canvas = document.getElementById("shader-spiral-galaxy");
  const gl = canvas.getContext("webgl2", { alpha: false, antialias: false });
  const fragment = `#version 300 es
precision highp float;
uniform vec3 iResolution;
uniform float iTime;
uniform float uTokens;
uniform float uTps;
uniform vec3 uColor;
out vec4 outColor;
#define TAU 6.28318530718
uint hu(uint x) { x ^= x >> 16u; x *= 0x7feb352du; x ^= x >> 15u; x *= 0x846ca68bu; x ^= x >> 16u; return x; }
float hf(uint x) { return float(hu(x) >> 8u) * (1.0 / 16777216.0); }
float h1(float a) { return hf(uint(int(floor(a)))); }
float h2(vec2 a) { ivec2 i = ivec2(floor(a)); return hf(uint(i.x) * 0x27d4eb2du ^ hu(uint(i.y) + 0x9e3779b9u)); }
float h3(vec3 a) { ivec3 i = ivec3(floor(a)); return hf(uint(i.x) * 0x27d4eb2du ^ hu(uint(i.y) * 0x165667b1u ^ hu(uint(i.z) + 0x9e3779b9u))); }
float segd(vec2 p, vec2 a, vec2 b, out float t) {
  vec2 pa = p - a, ba = b - a;
  t = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0);
  return length(pa - ba * t);
}
vec3 tone(vec3 c) { return 1.0 - exp(-c); }
float dither(vec2 f) { return (h2(f) - 0.5) / 255.0; }
// 0 at idle, about 0.23 at 15 tok/s, 0.46 at 60, 0.7 at 200, 1 at 800
float energy() { return clamp(log2(1.0 + max(uTps, 0.0) / 8.0) / log2(101.0), 0.0, 1.0); }

float vn(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(mix(h2(i), h2(i + vec2(1.0, 0.0)), f.x), mix(h2(i + vec2(0.0, 1.0)), h2(i + vec2(1.0, 1.0)), f.x), f.y);
}
float fbm(vec2 p) { return vn(p) * 0.55 + vn(p * 2.1 + 3.1) * 0.3 + vn(p * 4.3 + 7.7) * 0.15; }

const float TILT = 0.5;
const float PITCH = 2.3;
// one ring layer of stars in the disk plane, each orbiting at its ring's speed
vec3 stars(vec2 p, vec2 uv, float r, float th, float rw, float spacing, float lid, float patRot, float tk, float it, float e, float px) {
  float k = floor(r / rw);
  float rc = (k + 0.5) * rw;
  float M = max(floor(TAU * rc / spacing), 3.0);
  float n = floor(14.0 / (rc + 0.05));
  float ringRev = fract(tk * n / 65536.0 + it * n / 4096.0 + h2(vec2(k, lid)));
  float rps = max(uTps, 0.0) * n / 65536.0 + n / 4096.0;
  float trail = min(rps * TAU * 0.06, TAU / M * 0.95);
  float fth = (th / TAU - ringRev) * M;
  float c0 = floor(fth);
  float fcs = cos(th), fsn = sin(th);
  float fR = length(vec2(fcs, TILT * fsn));
  float fT = length(vec2(-fsn, TILT * fcs));
  vec3 acc = vec3(0.0);
  for (int q = -1; q <= 1; q++) {
    float cc = c0 + float(q);
    float id = mod(cc, M);
    float s1 = h3(vec3(id, k, lid)), s2 = h3(vec3(id, k, lid + 50.0)), s3 = h3(vec3(id, k, lid + 90.0));
    float ths = (cc + 0.5 + (s1 - 0.5) * 0.6) / M * TAU + ringRev * TAU;
    float rs = rc + (s2 - 0.5) * rw * 0.45;
    float arm = pow(0.5 + 0.5 * cos(2.0 * (ths - patRot + PITCH * log(rs))), 2.0);
    float dens = (0.18 + 0.82 * arm) * (0.35 + 0.65 * e) * smoothstep(1.0, 0.55, rs);
    float on = smoothstep(s3 - 0.06, s3 + 0.06, dens);
    if (on <= 0.0) continue;
    float dth = th - ths;
    dth -= TAU * floor(dth / TAU + 0.5);
    float ta = clamp(-dth, 0.0, trail);
    float d = length(vec2((r - rs) * fR, r * (dth + ta) * fT));
    float w = rw * (0.06 + 0.1 * s1 * s1) + px * 0.45;
    float b = pow(fract(s3 * 7.13 + s1), 4.0) * 1.6 + 0.25;
    float fall = trail > 0.0 ? 1.0 - ta / max(trail, 1e-4) * 0.75 : 1.0;
    float I = on * b * fall * (smoothstep(w + px, w - px, d) + w * w * 2.0 / (d * d + w * w * 2.0) * 0.3);
    acc += mix(uColor, vec3(1.0), 0.5 + 0.3 * s2) * I;
  }
  return acc;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
  float px = 1.0 / iResolution.y;
  float e = energy();
  float tk = mod(uTokens, 65536.0);
  float it = mod(iTime, 4096.0);
  float cr = cos(0.3), sr = sin(0.3);
  vec2 p = vec2(cr * uv.x + sr * uv.y, -sr * uv.x + cr * uv.y);
  p.y /= TILT;
  p *= 1.25;
  float r = length(p);
  float th = atan(p.y, p.x);
  float patRot = TAU * fract(tk * 10.0 / 65536.0 + it * 10.0 / 4096.0);

  // diffuse disk, arms and dust in the rotating frame
  float ca = cos(-patRot), sa = sin(-patRot);
  vec2 pr = vec2(ca * p.x - sa * p.y, sa * p.x + ca * p.y);
  float ph = th - patRot + PITCH * log(r + 0.002);
  float arm = pow(0.5 + 0.5 * cos(2.0 * ph), 2.5);
  float nz = fbm(pr * 3.5 + 11.0);
  float disk = exp(-r * 3.0) * smoothstep(0.0, 0.08, r);
  float dust = smoothstep(0.7, 1.0, 0.5 + 0.5 * cos(2.0 * (ph + 0.42))) * smoothstep(0.35, 0.75, fbm(pr * 6.0 + 3.0));
  float glow = disk * (0.12 + 1.1 * arm * (0.45 + 0.75 * nz)) * (1.0 - 0.65 * dust);
  vec3 col = vec3(0.003, 0.003, 0.007);
  col += uColor * glow * (0.55 + 0.75 * e);
  col += mix(uColor, vec3(0.6, 0.7, 1.0) * length(uColor), 0.3) * disk * arm * nz * nz * 0.35 * e;

  col += stars(p, uv, r, th, 0.03, 0.02, 0.0, patRot, tk, it, e, px) * (0.55 + 0.55 * e) * 0.6;
  col += stars(p, uv, r + 0.015, th, 0.045, 0.034, 3.0, patRot, tk, it, e, px) * (0.6 + 0.6 * e);

  // the bulge, round on screen
  float rb = length(uv * vec2(0.85, 1.0));
  vec3 warm = mix(uColor, vec3(1.0, 0.9, 0.78), 0.65);
  col += warm * (exp(-rb * rb / 0.0028) * 1.4 + exp(-rb / 0.055) * 0.4) * (0.7 + 0.6 * e);

  col *= 1.0 - 0.25 * dot(uv * vec2(0.5, 1.0), uv * vec2(0.5, 1.0));
  col = tone(col * 1.25);
  fragColor = vec4(col + dither(fragCoord), 1.0);
}

void main() {
  mainImage(outColor, gl_FragCoord.xy);
  outColor.a = 1.0;
}
`;
  const vertex = `#version 300 es
void main() {
  vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}
`;
  const compile = (type, src) => {
    const s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
    return s;
  };
  const program = gl.createProgram();
  gl.attachShader(program, compile(gl.VERTEX_SHADER, vertex));
  gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragment));
  gl.linkProgram(program);
  gl.useProgram(program);
  const u = (n) => gl.getUniformLocation(program, n);
  const [res, time, tokens, rate, color] = ["iResolution", "iTime", "uTokens", "uTps", "uColor"].map(u);
  const hex = parseInt((canvas.dataset.color || "#7c5cff").slice(1), 16);
  gl.uniform3f(color, ((hex >> 16) & 255) / 255, ((hex >> 8) & 255) / 255, (hex & 255) / 255);

  // canvas.token(): count real tokens and derive the rate from the last second.
  const stamps = [];
  canvas.token = () => {
    stamps.push(performance.now());
    canvas.dataset.tps = "auto";
  };
  const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
  let count = 0, last = performance.now(), visible = true, frame = 0;
  new IntersectionObserver(([e]) => {
    visible = e.isIntersecting;
    if (visible && !frame) frame = requestAnimationFrame(draw);
  }).observe(canvas);

  function draw(now) {
    frame = 0;
    const dt = Math.min((now - last) / 1000, 0.1);
    last = now;
    while (stamps.length && now - stamps[0] > 1000) stamps.shift();
    const wobble = 1 + 0.25 * Math.sin(now / 588) + 0.15 * Math.sin(now / 233 + 1) + 0.08 * Math.sin(now / 91 + 2);
    const tps = canvas.dataset.tps === "auto" ? stamps.length : Number(canvas.dataset.tps) * wobble;
    count += tps * dt;
    const dpr = Math.min(devicePixelRatio || 1, 2);
    const w = Math.round(canvas.clientWidth * dpr), h = Math.round(canvas.clientHeight * dpr);
    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
    gl.viewport(0, 0, w, h);
    gl.uniform3f(res, w, h, 1);
    gl.uniform1f(time, still ? 2 : now / 1000);
    gl.uniform1f(tokens, still ? Number(canvas.dataset.tps) * 2 : count);
    gl.uniform1f(rate, tps);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    if (visible && !still) frame = requestAnimationFrame(draw);
  }
  frame = requestAnimationFrame(draw);
</script>

GLSL for Shadertoy

// Inputs the site passes in, as constants for Shadertoy.
// uTps: tokens per second. uTokens: tokens so far. uColor: the accent.
#define uTps 400.0
#define uTokens (iTime * uTps)
#define uColor vec3(0.718, 0.612, 1.000)

#define TAU 6.28318530718
uint hu(uint x) { x ^= x >> 16u; x *= 0x7feb352du; x ^= x >> 15u; x *= 0x846ca68bu; x ^= x >> 16u; return x; }
float hf(uint x) { return float(hu(x) >> 8u) * (1.0 / 16777216.0); }
float h1(float a) { return hf(uint(int(floor(a)))); }
float h2(vec2 a) { ivec2 i = ivec2(floor(a)); return hf(uint(i.x) * 0x27d4eb2du ^ hu(uint(i.y) + 0x9e3779b9u)); }
float h3(vec3 a) { ivec3 i = ivec3(floor(a)); return hf(uint(i.x) * 0x27d4eb2du ^ hu(uint(i.y) * 0x165667b1u ^ hu(uint(i.z) + 0x9e3779b9u))); }
float segd(vec2 p, vec2 a, vec2 b, out float t) {
  vec2 pa = p - a, ba = b - a;
  t = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0);
  return length(pa - ba * t);
}
vec3 tone(vec3 c) { return 1.0 - exp(-c); }
float dither(vec2 f) { return (h2(f) - 0.5) / 255.0; }
// 0 at idle, about 0.23 at 15 tok/s, 0.46 at 60, 0.7 at 200, 1 at 800
float energy() { return clamp(log2(1.0 + max(uTps, 0.0) / 8.0) / log2(101.0), 0.0, 1.0); }

float vn(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(mix(h2(i), h2(i + vec2(1.0, 0.0)), f.x), mix(h2(i + vec2(0.0, 1.0)), h2(i + vec2(1.0, 1.0)), f.x), f.y);
}
float fbm(vec2 p) { return vn(p) * 0.55 + vn(p * 2.1 + 3.1) * 0.3 + vn(p * 4.3 + 7.7) * 0.15; }

const float TILT = 0.5;
const float PITCH = 2.3;
// one ring layer of stars in the disk plane, each orbiting at its ring's speed
vec3 stars(vec2 p, vec2 uv, float r, float th, float rw, float spacing, float lid, float patRot, float tk, float it, float e, float px) {
  float k = floor(r / rw);
  float rc = (k + 0.5) * rw;
  float M = max(floor(TAU * rc / spacing), 3.0);
  float n = floor(14.0 / (rc + 0.05));
  float ringRev = fract(tk * n / 65536.0 + it * n / 4096.0 + h2(vec2(k, lid)));
  float rps = max(uTps, 0.0) * n / 65536.0 + n / 4096.0;
  float trail = min(rps * TAU * 0.06, TAU / M * 0.95);
  float fth = (th / TAU - ringRev) * M;
  float c0 = floor(fth);
  float fcs = cos(th), fsn = sin(th);
  float fR = length(vec2(fcs, TILT * fsn));
  float fT = length(vec2(-fsn, TILT * fcs));
  vec3 acc = vec3(0.0);
  for (int q = -1; q <= 1; q++) {
    float cc = c0 + float(q);
    float id = mod(cc, M);
    float s1 = h3(vec3(id, k, lid)), s2 = h3(vec3(id, k, lid + 50.0)), s3 = h3(vec3(id, k, lid + 90.0));
    float ths = (cc + 0.5 + (s1 - 0.5) * 0.6) / M * TAU + ringRev * TAU;
    float rs = rc + (s2 - 0.5) * rw * 0.45;
    float arm = pow(0.5 + 0.5 * cos(2.0 * (ths - patRot + PITCH * log(rs))), 2.0);
    float dens = (0.18 + 0.82 * arm) * (0.35 + 0.65 * e) * smoothstep(1.0, 0.55, rs);
    float on = smoothstep(s3 - 0.06, s3 + 0.06, dens);
    if (on <= 0.0) continue;
    float dth = th - ths;
    dth -= TAU * floor(dth / TAU + 0.5);
    float ta = clamp(-dth, 0.0, trail);
    float d = length(vec2((r - rs) * fR, r * (dth + ta) * fT));
    float w = rw * (0.06 + 0.1 * s1 * s1) + px * 0.45;
    float b = pow(fract(s3 * 7.13 + s1), 4.0) * 1.6 + 0.25;
    float fall = trail > 0.0 ? 1.0 - ta / max(trail, 1e-4) * 0.75 : 1.0;
    float I = on * b * fall * (smoothstep(w + px, w - px, d) + w * w * 2.0 / (d * d + w * w * 2.0) * 0.3);
    acc += mix(uColor, vec3(1.0), 0.5 + 0.3 * s2) * I;
  }
  return acc;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
  float px = 1.0 / iResolution.y;
  float e = energy();
  float tk = mod(uTokens, 65536.0);
  float it = mod(iTime, 4096.0);
  float cr = cos(0.3), sr = sin(0.3);
  vec2 p = vec2(cr * uv.x + sr * uv.y, -sr * uv.x + cr * uv.y);
  p.y /= TILT;
  p *= 1.25;
  float r = length(p);
  float th = atan(p.y, p.x);
  float patRot = TAU * fract(tk * 10.0 / 65536.0 + it * 10.0 / 4096.0);

  // diffuse disk, arms and dust in the rotating frame
  float ca = cos(-patRot), sa = sin(-patRot);
  vec2 pr = vec2(ca * p.x - sa * p.y, sa * p.x + ca * p.y);
  float ph = th - patRot + PITCH * log(r + 0.002);
  float arm = pow(0.5 + 0.5 * cos(2.0 * ph), 2.5);
  float nz = fbm(pr * 3.5 + 11.0);
  float disk = exp(-r * 3.0) * smoothstep(0.0, 0.08, r);
  float dust = smoothstep(0.7, 1.0, 0.5 + 0.5 * cos(2.0 * (ph + 0.42))) * smoothstep(0.35, 0.75, fbm(pr * 6.0 + 3.0));
  float glow = disk * (0.12 + 1.1 * arm * (0.45 + 0.75 * nz)) * (1.0 - 0.65 * dust);
  vec3 col = vec3(0.003, 0.003, 0.007);
  col += uColor * glow * (0.55 + 0.75 * e);
  col += mix(uColor, vec3(0.6, 0.7, 1.0) * length(uColor), 0.3) * disk * arm * nz * nz * 0.35 * e;

  col += stars(p, uv, r, th, 0.03, 0.02, 0.0, patRot, tk, it, e, px) * (0.55 + 0.55 * e) * 0.6;
  col += stars(p, uv, r + 0.015, th, 0.045, 0.034, 3.0, patRot, tk, it, e, px) * (0.6 + 0.6 * e);

  // the bulge, round on screen
  float rb = length(uv * vec2(0.85, 1.0));
  vec3 warm = mix(uColor, vec3(1.0, 0.9, 0.78), 0.65);
  col += warm * (exp(-rb * rb / 0.0028) * 1.4 + exp(-rb / 0.055) * 0.4) * (0.7 + 0.6 * e);

  col *= 1.0 - 0.25 * dot(uv * vec2(0.5, 1.0), uv * vec2(0.5, 1.0));
  col = tone(col * 1.25);
  fragColor = vec4(col + dither(fragCoord), 1.0);
}

Customizing this shader

  • Speed: uTps is the token rate and uTokens the running count. The standalone runner adds a natural wobble to a fixed rate, the way real streams speed up and slow down.
  • Color: set data-color on the canvas, or change uColor in the Shadertoy version. Currently previewing #b79cff.
  • Size: style the canvas with CSS. It renders at up to twice your screen's pixel density and stops drawing while it is off screen.
  • Reduced motion: the runner draws a single still frame for people who asked their OS for less motion.

Similar shaders