A double terminated quartz crystal turns slowly, splitting light into spectral fringes across its facets. Token speed sends pulses up its core and sweeps the light around it.
Also known as: crystal shader, prism refraction, dispersion shader.
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-refracting-crystal" data-tps="35" data-color="#5fe0ff" 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-refracting-crystal");
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
#define PI 3.14159265359
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
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); }
// a phase in radians that wraps at whole turns, so it stays precise for hours
float turn(float x, float k) { return mod(x * k, TAU); }
vec3 toLin(vec3 c) { return pow(max(c, vec3(0.0)), vec3(2.2)); }
vec3 hueShift(vec3 c, float a) {
vec3 k = vec3(0.57735);
float cs = cos(a);
return max(c * cs + cross(k, c) * sin(a) + k * dot(k, c) * (1.0 - cs), vec3(0.0));
}
// ACES filmic curve, gamma and a tiny dither against banding
vec3 finish(vec3 c, vec2 fc) {
c = max(c, vec3(0.0));
c = clamp((c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);
c = pow(c, vec3(1.0 / 2.2));
return c + (hash12(fc) + hash12(fc + 19.19) - 1.0) / 255.0;
}
vec4 PL[18];
void buildPlanes() {
float w = 0.34, H = 1.02, hT = 0.48, H2 = -0.86, hB = -0.5;
for (int k = 0; k < 6; k++) {
float a = float(k) * TAU / 6.0;
vec2 cs = vec2(cos(a), sin(a));
float j1 = hash12(vec2(float(k), 1.0)) - 0.5;
float j2 = hash12(vec2(float(k), 2.0)) - 0.5;
float j3 = hash12(vec2(float(k), 3.0)) - 0.5;
PL[k] = vec4(cs.x, 0.0, cs.y, w * (1.0 + 0.12 * j1));
vec3 nt = vec3(cs.x * (H - hT) / w, 1.0, cs.y * (H - hT) / w);
PL[k + 6] = vec4(nt, H * (1.0 + 0.08 * j2)) / length(nt);
vec3 nb = vec3(cs.x * (hB - H2) / w, -1.0, cs.y * (hB - H2) / w);
PL[k + 12] = vec4(nb, -H2 * (1.0 + 0.08 * j3)) / length(nb);
}
}
// the ray against the convex hull of the planes: entry and exit distances
vec2 hull(vec3 ro, vec3 rd, out vec3 nIn, out vec3 nOut) {
float tn = -1e5, tf = 1e5;
nIn = vec3(0.0, 1.0, 0.0); nOut = nIn;
for (int i = 0; i < 18; i++) {
vec3 n = PL[i].xyz;
float dn = dot(rd, n);
float t = (PL[i].w - dot(ro, n)) / (abs(dn) < 1e-5 ? 1e-5 : dn);
if (dn < 0.0) { if (t > tn) { tn = t; nIn = n; } }
else if (t < tf) { tf = t; nOut = n; }
}
return vec2(tn, tf);
}
float edgeDist(vec3 p) {
float m1 = 1e3, m2 = 1e3;
for (int i = 0; i < 18; i++) {
float s = PL[i].w - dot(p, PL[i].xyz);
if (s < m1) { m2 = m1; m1 = s; } else if (s < m2) { m2 = s; }
}
return m2;
}
float gAx, gAz, gSpin;
vec3 toObj(vec3 v) { v.yz *= rot(gAx); v.xy *= rot(gAz); v.xz *= rot(gSpin); return v; }
vec3 toWorld(vec3 v) { v.xz *= rot(-gSpin); v.xy *= rot(-gAz); v.yz *= rot(-gAx); return v; }
vec3 crystalEnv(vec3 d, vec3 acc, vec3 acc2, float sweep, float e) {
float key = smoothstep(0.95, 0.965, dot(d, normalize(vec3(-0.4, 0.75, 0.55))));
float az = atan(d.z, d.x);
// light strips sweeping round the crystal, faster as the tokens come
float bands = smoothstep(0.8, 0.93, 0.5 + 0.5 * cos(az * 4.0 - sweep)) * smoothstep(0.85, 0.15, abs(d.y));
float ring = smoothstep(0.07, 0.0, abs(d.y - 0.3 - 0.12 * sin(az * 2.0 + sweep)));
float low = smoothstep(0.0, -0.8, d.y) * 0.12;
return vec3(key * 4.0) + mix(acc, vec3(1.0), 0.3) * bands * (0.5 + 2.6 * e) + acc2 * ring * (0.35 + 0.9 * e) + acc * (0.012 + low);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec3 acc = toLin(uColor);
vec3 acc2 = hueShift(acc, 0.8);
float e = 1.0 - exp(-uTps / 90.0);
float sweep = turn(iTime, 0.2) + turn(uTokens, 0.009);
float shaft = fract(iTime * 0.06) + fract(uTokens * 0.004);
gAx = 0.3; gAz = 1.2; gSpin = turn(iTime, 0.14);
buildPlanes();
vec3 roW = vec3(0.0, 0.0, 4.0);
vec3 rdW = normalize(vec3(uv, -2.05));
vec3 col = acc * 0.018 * smoothstep(1.1, 0.0, length(uv * vec2(0.7, 1.0)));
vec3 ro = toObj(roW), rd = toObj(rdW);
vec3 nIn, nOut;
vec2 tt = hull(ro, rd, nIn, nOut);
float pxw = tt.x / (2.05 * iResolution.y);
float cover = clamp((tt.y - tt.x) / (2.5 * pxw), 0.0, 1.0) * step(0.0, tt.x);
if (cover > 0.0) {
vec3 p = ro + rd * tt.x;
float cosi = clamp(-dot(rd, nIn), 0.0, 1.0);
float F = 0.05 + 0.95 * pow(1.0 - cosi, 5.0);
vec3 refl = crystalEnv(toWorld(reflect(rd, nIn)), acc, acc2, sweep, e);
vec3 trans = vec3(0.0);
vec3 inner = vec3(0.0);
for (int c = 0; c < 3; c++) {
float ior = 1.52 + 0.06 * float(c);
vec3 d = refract(rd, nIn, 1.0 / ior);
vec3 a, nx;
vec2 t2 = hull(p, d, a, nx);
vec3 pe = p + d * t2.y;
vec3 dout = refract(d, -nx, ior);
if (dot(dout, dout) < 0.01) {
// total internal reflection: bounce once more inside
vec3 d2 = reflect(d, -nx);
vec2 t3 = hull(pe, d2, a, nx);
dout = refract(d2, -nx, ior);
if (dot(dout, dout) < 0.01) dout = reflect(d2, -nx);
}
vec3 env = crystalEnv(toWorld(dout), acc, acc2, sweep, e);
trans[c] = env[c];
if (c == 1) {
// a column of light up the crystal's axis, with pulses rising through it
float jit = hash12(fragCoord);
for (int i = 0; i < 6; i++) {
vec3 q = p + d * (t2.y * (float(i) + jit) / 6.0);
float beam = exp(-dot(q.xz, q.xz) * 28.0);
float x = fract(q.y * 0.9 - shaft);
float pulse = smoothstep(0.0, 0.05, x) * exp(-x * 5.0);
inner += mix(acc, vec3(1.0), 0.4 * pulse) * beam * (0.08 + 0.12 * e + pulse * (0.4 + 2.2 * e)) * t2.y / 6.0;
}
}
}
vec3 glassTint = mix(vec3(1.0), acc * 1.5 + 0.2, 0.35);
vec3 s = refl * F + (trans * glassTint + inner * 3.0) * (1.0 - F);
// the facet edges catch the light
float ed = edgeDist(p);
float eline = exp(-ed / max(pxw * 1.2, 0.004));
s += mix(acc, vec3(1.0), 0.5) * eline * (0.35 + 0.5 * e);
col = mix(col, s, cover);
}
fragColor = vec4(finish(col, 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 35.0
#define uTokens (iTime * uTps)
#define uColor vec3(0.373, 0.878, 1.000)
#define TAU 6.28318530718
#define PI 3.14159265359
mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
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); }
// a phase in radians that wraps at whole turns, so it stays precise for hours
float turn(float x, float k) { return mod(x * k, TAU); }
vec3 toLin(vec3 c) { return pow(max(c, vec3(0.0)), vec3(2.2)); }
vec3 hueShift(vec3 c, float a) {
vec3 k = vec3(0.57735);
float cs = cos(a);
return max(c * cs + cross(k, c) * sin(a) + k * dot(k, c) * (1.0 - cs), vec3(0.0));
}
// ACES filmic curve, gamma and a tiny dither against banding
vec3 finish(vec3 c, vec2 fc) {
c = max(c, vec3(0.0));
c = clamp((c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);
c = pow(c, vec3(1.0 / 2.2));
return c + (hash12(fc) + hash12(fc + 19.19) - 1.0) / 255.0;
}
vec4 PL[18];
void buildPlanes() {
float w = 0.34, H = 1.02, hT = 0.48, H2 = -0.86, hB = -0.5;
for (int k = 0; k < 6; k++) {
float a = float(k) * TAU / 6.0;
vec2 cs = vec2(cos(a), sin(a));
float j1 = hash12(vec2(float(k), 1.0)) - 0.5;
float j2 = hash12(vec2(float(k), 2.0)) - 0.5;
float j3 = hash12(vec2(float(k), 3.0)) - 0.5;
PL[k] = vec4(cs.x, 0.0, cs.y, w * (1.0 + 0.12 * j1));
vec3 nt = vec3(cs.x * (H - hT) / w, 1.0, cs.y * (H - hT) / w);
PL[k + 6] = vec4(nt, H * (1.0 + 0.08 * j2)) / length(nt);
vec3 nb = vec3(cs.x * (hB - H2) / w, -1.0, cs.y * (hB - H2) / w);
PL[k + 12] = vec4(nb, -H2 * (1.0 + 0.08 * j3)) / length(nb);
}
}
// the ray against the convex hull of the planes: entry and exit distances
vec2 hull(vec3 ro, vec3 rd, out vec3 nIn, out vec3 nOut) {
float tn = -1e5, tf = 1e5;
nIn = vec3(0.0, 1.0, 0.0); nOut = nIn;
for (int i = 0; i < 18; i++) {
vec3 n = PL[i].xyz;
float dn = dot(rd, n);
float t = (PL[i].w - dot(ro, n)) / (abs(dn) < 1e-5 ? 1e-5 : dn);
if (dn < 0.0) { if (t > tn) { tn = t; nIn = n; } }
else if (t < tf) { tf = t; nOut = n; }
}
return vec2(tn, tf);
}
float edgeDist(vec3 p) {
float m1 = 1e3, m2 = 1e3;
for (int i = 0; i < 18; i++) {
float s = PL[i].w - dot(p, PL[i].xyz);
if (s < m1) { m2 = m1; m1 = s; } else if (s < m2) { m2 = s; }
}
return m2;
}
float gAx, gAz, gSpin;
vec3 toObj(vec3 v) { v.yz *= rot(gAx); v.xy *= rot(gAz); v.xz *= rot(gSpin); return v; }
vec3 toWorld(vec3 v) { v.xz *= rot(-gSpin); v.xy *= rot(-gAz); v.yz *= rot(-gAx); return v; }
vec3 crystalEnv(vec3 d, vec3 acc, vec3 acc2, float sweep, float e) {
float key = smoothstep(0.95, 0.965, dot(d, normalize(vec3(-0.4, 0.75, 0.55))));
float az = atan(d.z, d.x);
// light strips sweeping round the crystal, faster as the tokens come
float bands = smoothstep(0.8, 0.93, 0.5 + 0.5 * cos(az * 4.0 - sweep)) * smoothstep(0.85, 0.15, abs(d.y));
float ring = smoothstep(0.07, 0.0, abs(d.y - 0.3 - 0.12 * sin(az * 2.0 + sweep)));
float low = smoothstep(0.0, -0.8, d.y) * 0.12;
return vec3(key * 4.0) + mix(acc, vec3(1.0), 0.3) * bands * (0.5 + 2.6 * e) + acc2 * ring * (0.35 + 0.9 * e) + acc * (0.012 + low);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec3 acc = toLin(uColor);
vec3 acc2 = hueShift(acc, 0.8);
float e = 1.0 - exp(-uTps / 90.0);
float sweep = turn(iTime, 0.2) + turn(uTokens, 0.009);
float shaft = fract(iTime * 0.06) + fract(uTokens * 0.004);
gAx = 0.3; gAz = 1.2; gSpin = turn(iTime, 0.14);
buildPlanes();
vec3 roW = vec3(0.0, 0.0, 4.0);
vec3 rdW = normalize(vec3(uv, -2.05));
vec3 col = acc * 0.018 * smoothstep(1.1, 0.0, length(uv * vec2(0.7, 1.0)));
vec3 ro = toObj(roW), rd = toObj(rdW);
vec3 nIn, nOut;
vec2 tt = hull(ro, rd, nIn, nOut);
float pxw = tt.x / (2.05 * iResolution.y);
float cover = clamp((tt.y - tt.x) / (2.5 * pxw), 0.0, 1.0) * step(0.0, tt.x);
if (cover > 0.0) {
vec3 p = ro + rd * tt.x;
float cosi = clamp(-dot(rd, nIn), 0.0, 1.0);
float F = 0.05 + 0.95 * pow(1.0 - cosi, 5.0);
vec3 refl = crystalEnv(toWorld(reflect(rd, nIn)), acc, acc2, sweep, e);
vec3 trans = vec3(0.0);
vec3 inner = vec3(0.0);
for (int c = 0; c < 3; c++) {
float ior = 1.52 + 0.06 * float(c);
vec3 d = refract(rd, nIn, 1.0 / ior);
vec3 a, nx;
vec2 t2 = hull(p, d, a, nx);
vec3 pe = p + d * t2.y;
vec3 dout = refract(d, -nx, ior);
if (dot(dout, dout) < 0.01) {
// total internal reflection: bounce once more inside
vec3 d2 = reflect(d, -nx);
vec2 t3 = hull(pe, d2, a, nx);
dout = refract(d2, -nx, ior);
if (dot(dout, dout) < 0.01) dout = reflect(d2, -nx);
}
vec3 env = crystalEnv(toWorld(dout), acc, acc2, sweep, e);
trans[c] = env[c];
if (c == 1) {
// a column of light up the crystal's axis, with pulses rising through it
float jit = hash12(fragCoord);
for (int i = 0; i < 6; i++) {
vec3 q = p + d * (t2.y * (float(i) + jit) / 6.0);
float beam = exp(-dot(q.xz, q.xz) * 28.0);
float x = fract(q.y * 0.9 - shaft);
float pulse = smoothstep(0.0, 0.05, x) * exp(-x * 5.0);
inner += mix(acc, vec3(1.0), 0.4 * pulse) * beam * (0.08 + 0.12 * e + pulse * (0.4 + 2.2 * e)) * t2.y / 6.0;
}
}
}
vec3 glassTint = mix(vec3(1.0), acc * 1.5 + 0.2, 0.35);
vec3 s = refl * F + (trans * glassTint + inner * 3.0) * (1.0 - F);
// the facet edges catch the light
float ed = edgeDist(p);
float eline = exp(-ed / max(pxw * 1.2, 0.004));
s += mix(acc, vec3(1.0), 0.5) * eline * (0.35 + 0.5 * e);
col = mix(col, s, cover);
}
fragColor = vec4(finish(col, 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 #5fe0ff.