A neon cinquefoil knot of glass tubing floats in the dark while bright pulses chase each other along it. Higher token rates send the pulses faster and brighten the halo.
Also known as: torus knot shader, neon knot animation, raymarched knot.
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-glowing-torus-knot" data-tps="130" data-color="#ff5bd2" 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-glowing-torus-knot");
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;
}
float gPulse;
float knot(vec3 p, out float s) {
float a = atan(p.z, p.x);
vec2 m = vec2(length(p.xz) - 1.0, p.y);
float best = 1e9;
s = 0.0;
for (int k = 0; k < 2; k++) {
float t = (a + TAU * float(k)) * 0.5;
float b = 5.0 * t;
float d = length(m - 0.4 * vec2(cos(b), sin(b)));
if (d < best) { best = d; s = t; }
}
return best - 0.105;
}
// pulses chasing along the knot, s is the curve parameter
float pulses(float s) {
float x = fract(s / TAU * 3.0 - gPulse);
float y = fract(s / TAU * 3.0 - gPulse + 0.5);
return exp(-x * 4.0) * smoothstep(1.0, 0.92, x) + 0.35 * exp(-y * 8.0) * smoothstep(1.0, 0.92, y);
}
vec3 toKnot(vec3 p, float spin) {
p.yz *= rot(1.02);
p.xz *= rot(spin);
return p;
}
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, 1.1);
float e = 1.0 - exp(-uTps / 90.0);
gPulse = fract(iTime * 0.07) + fract(uTokens * 0.0045);
float spin = turn(iTime, 0.06);
vec3 ro = vec3(0.0, 0.0, 5.7);
vec3 rd = normalize(vec3(uv, -1.75));
vec3 col = acc * 0.012 * smoothstep(1.1, 0.0, length(uv));
float b = dot(ro, rd);
float h = b * b - dot(ro, ro) + 1.85 * 1.85;
if (h > 0.0) {
float t = -b - sqrt(h);
float tEnd = -b + sqrt(h);
float dmin = 1e3, tmin = t, s = 0.0, smin = 0.0;
float glow = 0.0;
bool hit = false;
for (int i = 0; i < 64; i++) {
vec3 p = toKnot(ro + rd * t, spin);
float d = knot(p, s);
if (d < dmin) { dmin = d; tmin = t; smin = s; }
float stp = max(d * 0.55, 0.003);
// the halo is integrated along the ray, so it does not pile up near the tube
glow += exp(-max(d, 0.0) * 16.0) * (0.25 + pulses(s) * (0.5 + 1.5 * e)) * stp * 0.9;
if (d < 0.001) { hit = true; break; }
if (t > tEnd) break;
t += stp;
}
vec3 tint = mix(acc, acc2, 0.5 + 0.5 * sin(smin * 2.0));
col += tint * glow * (0.35 + 0.8 * e);
float pxw = 1.5 * tmin / (1.75 * iResolution.y);
float cover = 1.0 - smoothstep(0.0, pxw, dmin);
if (cover > 0.0) {
vec3 p = ro + rd * tmin;
vec2 k = vec2(1.0, -1.0) * 0.002;
float sd;
vec3 n = normalize(k.xyy * knot(toKnot(p + k.xyy, spin), sd) + k.yyx * knot(toKnot(p + k.yyx, spin), sd)
+ k.yxy * knot(toKnot(p + k.yxy, spin), sd) + k.xxx * knot(toKnot(p + k.xxx, spin), sd));
float cosv = clamp(-dot(rd, n), 0.0, 1.0);
float fres = pow(1.0 - cosv, 3.0);
vec3 L = normalize(vec3(-0.4, 0.8, 0.6));
float spec = pow(clamp(dot(reflect(rd, n), L), 0.0, 1.0), 70.0);
float pl = pulses(smin);
// a neon core seen through the glass wall: brightest down the middle of the tube
float core = pow(cosv, 7.0);
vec3 neon = tint * core * (0.45 + 0.8 * e) + tint * pow(cosv, 2.0) * 0.08;
neon += mix(tint, vec3(1.0), 0.45) * pl * pow(cosv, 3.0) * (0.7 + 2.6 * e);
vec3 glass = tint * fres * 0.45 + vec3(1.0) * spec * 1.0;
col = mix(col, neon + glass + tint * 0.02, 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 130.0
#define uTokens (iTime * uTps)
#define uColor vec3(1.000, 0.357, 0.824)
#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;
}
float gPulse;
float knot(vec3 p, out float s) {
float a = atan(p.z, p.x);
vec2 m = vec2(length(p.xz) - 1.0, p.y);
float best = 1e9;
s = 0.0;
for (int k = 0; k < 2; k++) {
float t = (a + TAU * float(k)) * 0.5;
float b = 5.0 * t;
float d = length(m - 0.4 * vec2(cos(b), sin(b)));
if (d < best) { best = d; s = t; }
}
return best - 0.105;
}
// pulses chasing along the knot, s is the curve parameter
float pulses(float s) {
float x = fract(s / TAU * 3.0 - gPulse);
float y = fract(s / TAU * 3.0 - gPulse + 0.5);
return exp(-x * 4.0) * smoothstep(1.0, 0.92, x) + 0.35 * exp(-y * 8.0) * smoothstep(1.0, 0.92, y);
}
vec3 toKnot(vec3 p, float spin) {
p.yz *= rot(1.02);
p.xz *= rot(spin);
return p;
}
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, 1.1);
float e = 1.0 - exp(-uTps / 90.0);
gPulse = fract(iTime * 0.07) + fract(uTokens * 0.0045);
float spin = turn(iTime, 0.06);
vec3 ro = vec3(0.0, 0.0, 5.7);
vec3 rd = normalize(vec3(uv, -1.75));
vec3 col = acc * 0.012 * smoothstep(1.1, 0.0, length(uv));
float b = dot(ro, rd);
float h = b * b - dot(ro, ro) + 1.85 * 1.85;
if (h > 0.0) {
float t = -b - sqrt(h);
float tEnd = -b + sqrt(h);
float dmin = 1e3, tmin = t, s = 0.0, smin = 0.0;
float glow = 0.0;
bool hit = false;
for (int i = 0; i < 64; i++) {
vec3 p = toKnot(ro + rd * t, spin);
float d = knot(p, s);
if (d < dmin) { dmin = d; tmin = t; smin = s; }
float stp = max(d * 0.55, 0.003);
// the halo is integrated along the ray, so it does not pile up near the tube
glow += exp(-max(d, 0.0) * 16.0) * (0.25 + pulses(s) * (0.5 + 1.5 * e)) * stp * 0.9;
if (d < 0.001) { hit = true; break; }
if (t > tEnd) break;
t += stp;
}
vec3 tint = mix(acc, acc2, 0.5 + 0.5 * sin(smin * 2.0));
col += tint * glow * (0.35 + 0.8 * e);
float pxw = 1.5 * tmin / (1.75 * iResolution.y);
float cover = 1.0 - smoothstep(0.0, pxw, dmin);
if (cover > 0.0) {
vec3 p = ro + rd * tmin;
vec2 k = vec2(1.0, -1.0) * 0.002;
float sd;
vec3 n = normalize(k.xyy * knot(toKnot(p + k.xyy, spin), sd) + k.yyx * knot(toKnot(p + k.yyx, spin), sd)
+ k.yxy * knot(toKnot(p + k.yxy, spin), sd) + k.xxx * knot(toKnot(p + k.xxx, spin), sd));
float cosv = clamp(-dot(rd, n), 0.0, 1.0);
float fres = pow(1.0 - cosv, 3.0);
vec3 L = normalize(vec3(-0.4, 0.8, 0.6));
float spec = pow(clamp(dot(reflect(rd, n), L), 0.0, 1.0), 70.0);
float pl = pulses(smin);
// a neon core seen through the glass wall: brightest down the middle of the tube
float core = pow(cosv, 7.0);
vec3 neon = tint * core * (0.45 + 0.8 * e) + tint * pow(cosv, 2.0) * 0.08;
neon += mix(tint, vec3(1.0), 0.45) * pl * pow(cosv, 3.0) * (0.7 + 2.6 * e);
vec3 glass = tint * fres * 0.45 + vec3(1.0) * spec * 1.0;
col = mix(col, neon + glass + tint * 0.02, 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 #ff5bd2.