AI CSS Animations

Firefly Gathering

Soft fireflies blink over dark grass, scattered and slow. As tokens speed up they gather into one bright, buzzing cluster, and more of them stay lit together.

Also known as: fireflies cluster, glowing bokeh swarm.

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-firefly-gathering" data-tps="5" data-color="#c6f25a" 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-firefly-gathering");
  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); }

vec2 ffPos(float fi, float ph, float g, out float gi) {
  float ha = h1(fi * 11.0 + 1.0), hb = h1(fi * 11.0 + 2.0), hc = h1(fi * 11.0 + 3.0);
  float hd = h1(fi * 11.0 + 4.0), he = h1(fi * 11.0 + 5.0);
  vec2 home = vec2((ha * 2.0 - 1.0) * 0.98, (hb * 2.0 - 1.0) * 0.32 + 0.04);
  float a = TAU * ph;
  vec2 wander = vec2(0.13 * sin(a + hc * TAU) + 0.05 * sin(3.0 * a + hd * TAU),
                     0.07 * sin(2.0 * a + he * TAU) + 0.03 * cos(5.0 * a + ha * TAU));
  float rc = 0.05 + 0.15 * sqrt(hd);
  float ca = 2.0 * a + hc * TAU;
  float inc = (he - 0.5) * 2.8;
  vec2 clus = vec2(0.0, 0.05) + vec2(cos(ca) * rc * 1.3, sin(ca) * rc * cos(inc) + 0.025 * sin(3.0 * a + hb * TAU));
  gi = smoothstep(hb * 0.45, hb * 0.45 + 0.55, g);
  return mix(home + wander, clus, gi);
}

float grass(vec2 uv, float t, float px, float k) {
  float base = -0.36 + 0.035 * sin(uv.x * 2.3 + 1.0 + k) + 0.018 * sin(uv.x * 5.1 + k * 3.0);
  float bx = uv.x * (48.0 + k * 9.0) + k * 0.37;
  float id = floor(bx);
  float hh = h1(id + 300.0 + k * 1000.0);
  float bh = 0.04 + 0.12 * hh * hh;
  float yy = (uv.y - base) / bh;
  float sway = 0.13 * sin(t * 0.7 + id * 0.37);
  float xx = fract(bx) - 0.5 - sway * yy * yy;
  float wdt = 0.34 * (1.0 - clamp(yy, 0.0, 1.0));
  float aa = px * (48.0 + k * 9.0);
  float blade = smoothstep(wdt + aa, wdt - aa, abs(xx)) * step(0.0, yy) * step(yy, 1.0);
  return max(blade, smoothstep(base + px, base - px, uv.y));
}

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 g = smoothstep(0.04, 0.82, e);
  float tk = mod(uTokens, 65536.0);
  float it = mod(iTime, 512.0);
  float t = mod(iTime, 6000.0);

  vec3 col = vec3(0.004, 0.006, 0.01) + uColor * 0.02 * smoothstep(-0.5, 0.5, uv.y);
  col += uColor * 0.065 * exp(-pow((uv.y + 0.28) / 0.13, 2.0));
  vec2 cq = (uv - vec2(0.0, 0.06)) * vec2(0.75, 1.3);
  col += uColor * 0.14 * g * exp(-length(cq) * 5.0);

  vec3 farC = vec3(0.0), nearC = vec3(0.0);
  vec3 glowC = mix(uColor, vec3(1.0, 0.98, 0.88), 0.3);
  for (int i = 0; i < 40; i++) {
    float fi = float(i);
    float vis = smoothstep(fi - 1.5, fi + 1.5, 12.0 + 28.0 * e);
    if (vis <= 0.0) break;
    float n = 6.0 + floor(h1(fi * 11.0 + 6.0) * 7.0);
    float ph = fract(tk * n / 2048.0 + it * n / 512.0 + h1(fi * 11.0 + 7.0));
    float cps = max(uTps, 0.0) * n / 2048.0 + n / 512.0;
    float gi;
    vec2 P = ffPos(fi, ph, g, gi);
    float z = h1(fi * 11.0 + 8.0);
    z = z * z;
    vec2 Q = ffPos(fi, ph - clamp(cps * 0.03, 0.0, 0.035) * (1.0 - 0.7 * z), g, gi);
    float rb = mix(0.0035, 0.024, z * z) + px * 0.5;
    float tt;
    float d = segd(uv, P, Q, tt);
    float hz = h1(fi * 11.0 + 9.0);
    float blink = 0.15 + 0.85 * smoothstep(0.15, 0.9, 0.5 + 0.5 * sin(t * (0.6 + 0.9 * hz) + hz * TAU));
    blink = mix(blink, 1.0, gi * 0.65);
    float spot = exp(-d * d / (rb * rb) * 1.4);
    float disk = smoothstep(rb, rb * 0.82, d) * 0.3 + smoothstep(rb * 0.3, 0.0, abs(d - rb * 0.9)) * 0.12;
    float halo = 0.12 * rb * rb * 9.0 / (d * d + rb * rb * 9.0);
    float bok = smoothstep(0.35, 0.8, z);
    float I = mix(spot, disk, bok) + halo;
    I *= blink * vis * mix(1.4, 0.45, bok) * (0.9 + 0.5 * e) * mix(1.0, 0.25, tt * smoothstep(0.0, rb * 2.0, length(P - Q)));
    vec3 fc = mix(glowC * I, uColor * I * 0.8, 0.3);
    if (z > 0.55) nearC += fc; else farC += fc;
  }
  col += farC;
  float gm = max(grass(uv, t, px, 0.0), grass(uv + vec2(0.0, 0.012), t + 1.3, px, 1.0));
  col = mix(col, vec3(0.002, 0.003, 0.004) + uColor * 0.012, gm);
  col += nearC;

  col *= 1.0 - 0.28 * dot(uv * vec2(0.5, 1.0), uv * vec2(0.5, 1.0));
  col = tone(col * 1.3);
  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 5.0
#define uTokens (iTime * uTps)
#define uColor vec3(0.776, 0.949, 0.353)

#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); }

vec2 ffPos(float fi, float ph, float g, out float gi) {
  float ha = h1(fi * 11.0 + 1.0), hb = h1(fi * 11.0 + 2.0), hc = h1(fi * 11.0 + 3.0);
  float hd = h1(fi * 11.0 + 4.0), he = h1(fi * 11.0 + 5.0);
  vec2 home = vec2((ha * 2.0 - 1.0) * 0.98, (hb * 2.0 - 1.0) * 0.32 + 0.04);
  float a = TAU * ph;
  vec2 wander = vec2(0.13 * sin(a + hc * TAU) + 0.05 * sin(3.0 * a + hd * TAU),
                     0.07 * sin(2.0 * a + he * TAU) + 0.03 * cos(5.0 * a + ha * TAU));
  float rc = 0.05 + 0.15 * sqrt(hd);
  float ca = 2.0 * a + hc * TAU;
  float inc = (he - 0.5) * 2.8;
  vec2 clus = vec2(0.0, 0.05) + vec2(cos(ca) * rc * 1.3, sin(ca) * rc * cos(inc) + 0.025 * sin(3.0 * a + hb * TAU));
  gi = smoothstep(hb * 0.45, hb * 0.45 + 0.55, g);
  return mix(home + wander, clus, gi);
}

float grass(vec2 uv, float t, float px, float k) {
  float base = -0.36 + 0.035 * sin(uv.x * 2.3 + 1.0 + k) + 0.018 * sin(uv.x * 5.1 + k * 3.0);
  float bx = uv.x * (48.0 + k * 9.0) + k * 0.37;
  float id = floor(bx);
  float hh = h1(id + 300.0 + k * 1000.0);
  float bh = 0.04 + 0.12 * hh * hh;
  float yy = (uv.y - base) / bh;
  float sway = 0.13 * sin(t * 0.7 + id * 0.37);
  float xx = fract(bx) - 0.5 - sway * yy * yy;
  float wdt = 0.34 * (1.0 - clamp(yy, 0.0, 1.0));
  float aa = px * (48.0 + k * 9.0);
  float blade = smoothstep(wdt + aa, wdt - aa, abs(xx)) * step(0.0, yy) * step(yy, 1.0);
  return max(blade, smoothstep(base + px, base - px, uv.y));
}

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 g = smoothstep(0.04, 0.82, e);
  float tk = mod(uTokens, 65536.0);
  float it = mod(iTime, 512.0);
  float t = mod(iTime, 6000.0);

  vec3 col = vec3(0.004, 0.006, 0.01) + uColor * 0.02 * smoothstep(-0.5, 0.5, uv.y);
  col += uColor * 0.065 * exp(-pow((uv.y + 0.28) / 0.13, 2.0));
  vec2 cq = (uv - vec2(0.0, 0.06)) * vec2(0.75, 1.3);
  col += uColor * 0.14 * g * exp(-length(cq) * 5.0);

  vec3 farC = vec3(0.0), nearC = vec3(0.0);
  vec3 glowC = mix(uColor, vec3(1.0, 0.98, 0.88), 0.3);
  for (int i = 0; i < 40; i++) {
    float fi = float(i);
    float vis = smoothstep(fi - 1.5, fi + 1.5, 12.0 + 28.0 * e);
    if (vis <= 0.0) break;
    float n = 6.0 + floor(h1(fi * 11.0 + 6.0) * 7.0);
    float ph = fract(tk * n / 2048.0 + it * n / 512.0 + h1(fi * 11.0 + 7.0));
    float cps = max(uTps, 0.0) * n / 2048.0 + n / 512.0;
    float gi;
    vec2 P = ffPos(fi, ph, g, gi);
    float z = h1(fi * 11.0 + 8.0);
    z = z * z;
    vec2 Q = ffPos(fi, ph - clamp(cps * 0.03, 0.0, 0.035) * (1.0 - 0.7 * z), g, gi);
    float rb = mix(0.0035, 0.024, z * z) + px * 0.5;
    float tt;
    float d = segd(uv, P, Q, tt);
    float hz = h1(fi * 11.0 + 9.0);
    float blink = 0.15 + 0.85 * smoothstep(0.15, 0.9, 0.5 + 0.5 * sin(t * (0.6 + 0.9 * hz) + hz * TAU));
    blink = mix(blink, 1.0, gi * 0.65);
    float spot = exp(-d * d / (rb * rb) * 1.4);
    float disk = smoothstep(rb, rb * 0.82, d) * 0.3 + smoothstep(rb * 0.3, 0.0, abs(d - rb * 0.9)) * 0.12;
    float halo = 0.12 * rb * rb * 9.0 / (d * d + rb * rb * 9.0);
    float bok = smoothstep(0.35, 0.8, z);
    float I = mix(spot, disk, bok) + halo;
    I *= blink * vis * mix(1.4, 0.45, bok) * (0.9 + 0.5 * e) * mix(1.0, 0.25, tt * smoothstep(0.0, rb * 2.0, length(P - Q)));
    vec3 fc = mix(glowC * I, uColor * I * 0.8, 0.3);
    if (z > 0.55) nearC += fc; else farC += fc;
  }
  col += farC;
  float gm = max(grass(uv, t, px, 0.0), grass(uv + vec2(0.0, 0.012), t + 1.3, px, 1.0));
  col = mix(col, vec3(0.002, 0.003, 0.004) + uColor * 0.012, gm);
  col += nearC;

  col *= 1.0 - 0.28 * dot(uv * vec2(0.5, 1.0), uv * vec2(0.5, 1.0));
  col = tone(col * 1.3);
  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 #c6f25a.
  • 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