A pool of liquid metal rolls and swells, mirroring bands of studio light across its surface. Faster token rates set the chrome flowing quicker and churn it into sharper ripples.
Also known as: liquid metal shader, chrome flow animation, ai thinking chrome.
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-liquid-chrome" data-tps="75" data-color="#8fd3ff" 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-liquid-chrome");
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;
// Gradient noise on a lattice that repeats every PER cells. Moving offsets are
// wrapped to whole periods, so the motion loops without a seam.
const float PER = 32.0;
// Octave turn and scale: an integer matrix keeps the lattice periodic.
const mat2 OCT = mat2(2.0, -1.0, 1.0, 2.0);
vec2 grad(vec2 i) {
i = mod(i, PER);
vec3 q = fract(vec3(i.xyx) * vec3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy) * 2.0 - 1.0;
}
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(grad(i), f);
float b = dot(grad(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(grad(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(grad(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// A phase that climbs with x but wraps to the noise period.
float wrapP(float x) { return mod(x, PER); }
// Rotate a colour around the grey axis: hue shifts that follow uColor.
vec3 hue(vec3 c, float a) {
const vec3 k = vec3(0.57735);
float ca = cos(a);
return c * ca + cross(k, c) * sin(a) + k * dot(k, c) * (1.0 - ca);
}
float hash12(vec2 p) {
vec3 q = fract(vec3(p.xyx) * 0.1031);
q += dot(q, q.yzx + 33.33);
return fract((q.x + q.y) * q.z);
}
// Soft shoulder tone map plus a one step dither against banding.
vec3 finish(vec3 c, vec2 fc) {
c = 1.0 - exp(-max(c, 0.0));
return c + (hash12(fc) - 0.5) / 255.0;
}
// Height of the metal surface: a slow swell warped by a second layer.
float height(vec2 p, vec2 w1, vec2 w2, float e) {
vec2 wq = vec2(noise(p + w2), noise(p + vec2(3.7, 8.1) - w2.yx));
vec2 s = p + 0.8 * wq + w1;
float h = noise(s);
s = OCT * s + w2;
h += 0.3 * noise(s);
s = OCT * s - w1;
h += (0.03 + 0.09 * e) * noise(s);
return h;
}
// A dark studio around the pool: a coloured floor rising to a crisp horizon,
// a bright sky band above it and one strip light, all bent by the ripples.
vec3 env(vec3 r) {
float y = r.y * 0.9 + 0.25 * r.z - 0.26;
float aa = fwidth(y) + 0.002;
vec3 gnd = mix(vec3(0.006), uColor * 0.45, smoothstep(-0.7, 0.0, y));
vec3 sky = mix(mix(uColor, vec3(1.0), 0.6) * 1.3, uColor * 0.1, smoothstep(0.0, 0.55, y));
sky += vec3(1.4) * smoothstep(0.05, 0.0, abs(y - 0.4));
vec3 c = mix(gnd, sky, smoothstep(-aa, aa, y));
c += hue(uColor, 0.8) * smoothstep(0.14, 0.0, abs(r.x - 0.55)) * 0.3;
return c;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float e = 1.0 - exp(-uTps / 120.0);
vec2 w1 = vec2(wrapP(uTokens * 0.0022) + wrapP(iTime * 0.03), -wrapP(uTokens * 0.0015));
vec2 w2 = vec2(-wrapP(uTokens * 0.0031) - wrapP(iTime * 0.02), wrapP(uTokens * 0.0026));
vec2 p = uv * vec2(1.0, 1.25);
float eps = 1.0 / iResolution.y;
float h0 = height(p, w1, w2, e);
float hx = height(p + vec2(eps, 0.0), w1, w2, e);
float hy = height(p + vec2(0.0, eps), w1, w2, e);
float bump = 0.65 + 0.35 * e;
vec3 n = normalize(vec3(-(hx - h0) / eps * bump, -(hy - h0) / eps * bump, 1.0));
vec3 v = normalize(vec3(uv * 0.35, -1.0));
vec3 r = reflect(v, n);
float fres = pow(1.0 - max(dot(-v, n), 0.0), 3.0);
vec3 metal = mix(vec3(0.7), mix(uColor, vec3(1.0), 0.4), 0.5);
vec3 col = env(r) * mix(metal, vec3(1.0), fres) * (0.8 + 0.4 * e);
// a little of the light's colour pooled in the hollows
col += uColor * smoothstep(0.1, -0.5, h0) * 0.05;
float vig = 1.0 - 0.35 * dot(uv, uv);
fragColor = vec4(finish(col * vig, 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>// Inputs the site passes in, as constants for Shadertoy.
// uTps: tokens per second. uTokens: tokens so far. uColor: the accent.
#define uTps 75.0
#define uTokens (iTime * uTps)
#define uColor vec3(0.561, 0.827, 1.000)
// Gradient noise on a lattice that repeats every PER cells. Moving offsets are
// wrapped to whole periods, so the motion loops without a seam.
const float PER = 32.0;
// Octave turn and scale: an integer matrix keeps the lattice periodic.
const mat2 OCT = mat2(2.0, -1.0, 1.0, 2.0);
vec2 grad(vec2 i) {
i = mod(i, PER);
vec3 q = fract(vec3(i.xyx) * vec3(0.1031, 0.1030, 0.0973));
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy) * 2.0 - 1.0;
}
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(grad(i), f);
float b = dot(grad(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(grad(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(grad(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// A phase that climbs with x but wraps to the noise period.
float wrapP(float x) { return mod(x, PER); }
// Rotate a colour around the grey axis: hue shifts that follow uColor.
vec3 hue(vec3 c, float a) {
const vec3 k = vec3(0.57735);
float ca = cos(a);
return c * ca + cross(k, c) * sin(a) + k * dot(k, c) * (1.0 - ca);
}
float hash12(vec2 p) {
vec3 q = fract(vec3(p.xyx) * 0.1031);
q += dot(q, q.yzx + 33.33);
return fract((q.x + q.y) * q.z);
}
// Soft shoulder tone map plus a one step dither against banding.
vec3 finish(vec3 c, vec2 fc) {
c = 1.0 - exp(-max(c, 0.0));
return c + (hash12(fc) - 0.5) / 255.0;
}
// Height of the metal surface: a slow swell warped by a second layer.
float height(vec2 p, vec2 w1, vec2 w2, float e) {
vec2 wq = vec2(noise(p + w2), noise(p + vec2(3.7, 8.1) - w2.yx));
vec2 s = p + 0.8 * wq + w1;
float h = noise(s);
s = OCT * s + w2;
h += 0.3 * noise(s);
s = OCT * s - w1;
h += (0.03 + 0.09 * e) * noise(s);
return h;
}
// A dark studio around the pool: a coloured floor rising to a crisp horizon,
// a bright sky band above it and one strip light, all bent by the ripples.
vec3 env(vec3 r) {
float y = r.y * 0.9 + 0.25 * r.z - 0.26;
float aa = fwidth(y) + 0.002;
vec3 gnd = mix(vec3(0.006), uColor * 0.45, smoothstep(-0.7, 0.0, y));
vec3 sky = mix(mix(uColor, vec3(1.0), 0.6) * 1.3, uColor * 0.1, smoothstep(0.0, 0.55, y));
sky += vec3(1.4) * smoothstep(0.05, 0.0, abs(y - 0.4));
vec3 c = mix(gnd, sky, smoothstep(-aa, aa, y));
c += hue(uColor, 0.8) * smoothstep(0.14, 0.0, abs(r.x - 0.55)) * 0.3;
return c;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float e = 1.0 - exp(-uTps / 120.0);
vec2 w1 = vec2(wrapP(uTokens * 0.0022) + wrapP(iTime * 0.03), -wrapP(uTokens * 0.0015));
vec2 w2 = vec2(-wrapP(uTokens * 0.0031) - wrapP(iTime * 0.02), wrapP(uTokens * 0.0026));
vec2 p = uv * vec2(1.0, 1.25);
float eps = 1.0 / iResolution.y;
float h0 = height(p, w1, w2, e);
float hx = height(p + vec2(eps, 0.0), w1, w2, e);
float hy = height(p + vec2(0.0, eps), w1, w2, e);
float bump = 0.65 + 0.35 * e;
vec3 n = normalize(vec3(-(hx - h0) / eps * bump, -(hy - h0) / eps * bump, 1.0));
vec3 v = normalize(vec3(uv * 0.35, -1.0));
vec3 r = reflect(v, n);
float fres = pow(1.0 - max(dot(-v, n), 0.0), 3.0);
vec3 metal = mix(vec3(0.7), mix(uColor, vec3(1.0), 0.4), 0.5);
vec3 col = env(r) * mix(metal, vec3(1.0), fres) * (0.8 + 0.4 * e);
// a little of the light's colour pooled in the hollows
col += uColor * smoothstep(0.1, -0.5, h0) * 0.05;
float vig = 1.0 - 0.35 * dot(uv, uv);
fragColor = vec4(finish(col * vig, fragCoord), 1.0);
}
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.data-color on the canvas, or change uColor in the Shadertoy version. Currently previewing #8fd3ff.