@fxshelf/glyph-rain
Version:
Matrix-style glyph rain with cursor stir and drop-head lighting for React and Vue. WebGL2, zero animation-lib deps. Copy-as-source friendly.
209 lines (191 loc) • 6.97 kB
text/typescript
export const VERT = `
precision highp float;
layout(location = 0) in vec2 aPos;
out vec2 vUv;
void main () {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}`
export const FRAG = `
precision highp float;
in vec2 vUv;
out vec4 outColor;
uniform sampler2D uContent;
uniform sampler2D uAtlas;
uniform sampler2D uWake;
uniform vec2 uResolution;
uniform float uTime;
uniform float uCell;
uniform float uGlyphCount;
uniform float uAtlasGrid;
uniform vec3 uColor;
uniform vec3 uHeadColor;
uniform float uSpeed;
uniform float uSpeedVar;
uniform float uDensity;
uniform float uTrail;
uniform float uGlow;
uniform float uMutate;
uniform float uFlicker;
uniform float uLayers;
uniform float uDim;
uniform float uLight;
uniform float uLightRadius;
uniform float uLightHeight;
uniform float uRelief;
uniform float uStir;
uniform float uScroll;
uniform float uPageLum;
uniform float uHasContent;
float hash11(float p) {
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash21(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);
}
float glyphMask(vec2 px, float cell, float seed) {
vec2 id = floor(px / cell);
vec2 f = fract(px / cell);
f = f * 0.74 + 0.13;
f.x = 1.0 - f.x;
float tick = floor(uTime * uMutate * 1.6 + hash21(id + seed) * 9.0);
float idx = floor(
hash21(id * 1.71 + vec2(seed + tick * 7.31, tick * 0.613)) * uGlyphCount
);
float gx = mod(idx, uAtlasGrid);
float gy = floor(idx / uAtlasGrid);
vec2 auv = (vec2(gx, gy) + f) / uAtlasGrid;
return texture(uAtlas, auv).a;
}
float colSpeed(float col, float seed) {
float variance = mix(0.35, 1.0, hash11(col * 0.37 + seed + 3.1));
return uSpeed * mix(1.0, variance, uSpeedVar) * 0.5;
}
float colOffset(float col, float seed) {
return hash11(col * 1.713 + seed) * 9.0;
}
vec2 wakeAt(float xpx) {
float u = clamp(xpx / max(uResolution.x, 1.0), 0.0, 1.0);
return texture(uWake, vec2(u, 0.5)).rg;
}
float lum(vec3 c) {
return dot(c, vec3(0.299, 0.587, 0.114));
}
void main () {
vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y + uScroll);
vec2 cuv = vec2(vUv.x, 1.0 - vUv.y);
float yn = 1.0 - frag.y / uResolution.y;
const float scales[3] = float[3](1.0, 1.5, 2.2);
const float weights[3] = float[3](1.0, 0.45, 0.22);
const float seeds[3] = float[3](0.0, 19.7, 41.3);
float g = 0.0;
float headG = 0.0;
for (int l = 0; l < 3; l++) {
if (float(l) >= uLayers) break;
float cell = uCell * scales[l];
float col = floor(frag.x / cell);
float sp = colSpeed(col, seeds[l]);
float off = colOffset(col, seeds[l]);
vec2 wk = uStir > 0.0 ? wakeAt((col + 0.5) * cell) : vec2(0.0);
float exc = uStir * wk.y;
float T = uTime * sp + off + sp * wk.x;
float phase = fract(yn + T);
float cyc = floor(yn + T);
float gate = step(hash21(vec2(col, cyc) + seeds[l]), uDensity);
float b = clamp(uTrail / (phase * 22.0), 0.0, 1.3) - 0.04;
if (b <= 0.0 || gate < 0.5) continue;
float flick = 1.0 + uFlicker * 0.6 *
sin(uTime * 14.0 + hash21(vec2(col, cyc)) * 40.0 + phase * 30.0);
float m = glyphMask(frag, cell, seeds[l] + cyc * 0.173);
float cellYn = cell / uResolution.y;
float head = 1.0 - smoothstep(0.0, cellYn * 1.2, phase);
g += m * b * flick * weights[l] * (1.0 + head * uGlow * 1.4) *
(1.0 + exc * 1.6);
headG += m * head * weights[l] * uGlow * (1.0 + exc * 1.1);
}
g = max(g, 0.0);
if (uHasContent < 0.5) {
vec3 rainCol = mix(uColor, uHeadColor, clamp(headG, 0.0, 1.0));
float a = clamp(g, 0.0, 1.0);
outColor = vec4(rainCol * a, a);
return;
}
vec2 e = vec2(3.0, 0.0) / uResolution;
vec4 content = texture(uContent, cuv);
float lC = lum(content.rgb);
float lX1 = lum(texture(uContent, clamp(cuv - e.xy, 0.0, 1.0)).rgb);
float lX2 = lum(texture(uContent, clamp(cuv + e.xy, 0.0, 1.0)).rgb);
float lY1 = lum(texture(uContent, clamp(cuv - e.yx, 0.0, 1.0)).rgb);
float lY2 = lum(texture(uContent, clamp(cuv + e.yx, 0.0, 1.0)).rgb);
vec3 N = normalize(vec3(
-(lX2 - lX1) * uRelief * 4.0,
-(lY2 - lY1) * uRelief * 4.0,
1.0
));
float reliefMix = clamp(uRelief, 0.0, 1.0);
vec2 e2 = vec2(30.0, 0.0) / uResolution;
float bgL = (lC
+ lum(texture(uContent, clamp(cuv - e2.xy, 0.0, 1.0)).rgb)
+ lum(texture(uContent, clamp(cuv + e2.xy, 0.0, 1.0)).rgb)
+ lum(texture(uContent, clamp(cuv - e2.yx, 0.0, 1.0)).rgb)
+ lum(texture(uContent, clamp(cuv + e2.yx, 0.0, 1.0)).rgb)) * 0.2;
float bright = smoothstep(0.55, 0.8, uPageLum) * smoothstep(0.2, 0.45, bgL);
float lightSum = 0.0;
float sigma2 = uLightRadius * uLightRadius * 0.5;
float reach = uLightRadius * 1.6;
float stride = max(1.0, ceil((uLightRadius * 1.7) / (uCell * 12.0)));
float baseCol = floor(floor(frag.x / uCell) / stride);
for (int o = -12; o <= 12; o++) {
float c = (baseCol + float(o)) * stride;
if (c < 0.0) continue;
float dx = (c + 0.5) * uCell - frag.x;
float wx = 1.0 - smoothstep(reach * 0.7, reach, abs(dx));
if (wx <= 0.0) continue;
float sp = colSpeed(c, 0.0);
float off = colOffset(c, 0.0);
vec2 wk = uStir > 0.0 ? wakeAt((c + 0.5) * uCell) : vec2(0.0);
float lampBoost = 1.0 + uStir * wk.y * 1.4;
float T = uTime * sp + off + sp * wk.x;
float s = 1.0 - frag.y / uResolution.y + T;
float k0 = floor(s);
for (int h = 0; h < 2; h++) {
float k = k0 + float(h);
float gate = step(hash21(vec2(c, k)), uDensity);
if (gate < 0.5) continue;
float lamp = 0.6 + 0.4 * hash11(c * 3.97 + k * 0.713);
float headDocY = (1.0 - (k - T)) * uResolution.y;
vec3 dv = vec3(dx, headDocY - frag.y, uLightHeight);
float d2 = dot(dv, dv);
float att = exp(-d2 / sigma2);
vec3 L = dv * inversesqrt(max(d2, 1.0));
float dif = mix(1.0, 0.25 + 0.75 * max(dot(N, L), 0.0), reliefMix);
lightSum += att * dif * wx * lamp * lampBoost;
}
}
float ls = lightSum * uLight * (0.6 + 0.4 * uGlow);
float lit = 2.2 * ls / (ls + 1.1);
float dimEff = uDim * (1.0 - bright);
float shade = mix(
clamp(1.0 - dimEff, 0.0, 1.0),
1.0,
smoothstep(0.0, 1.0, lit)
);
vec3 col = content.rgb * shade;
col += uColor * lit * 0.14 * (1.0 - lC * 0.75) * (1.0 - bright);
col += uColor * clamp(lit - 1.0, 0.0, 1.0) * 0.1 * (1.0 - bright);
vec3 glyphCol = mix(uColor, uColor * 0.24 + vec3(0.02), lC * (1.0 - bright));
glyphCol = mix(glyphCol, uHeadColor, clamp(headG, 0.0, 1.0));
glyphCol = mix(glyphCol, vec3(1.0), bright * clamp(headG - 0.6, 0.0, 0.4));
float gA = clamp(g, 0.0, 1.0);
float knock = gA * mix(mix(0.3, 0.88, lC), 1.0, bright);
float paint = min(g, 1.5) * mix(1.0, mix(0.92, 1.0, bright), lC);
col = col * (1.0 - knock) + glyphCol * paint;
float alpha = max(content.a, gA);
col = clamp(col, vec3(0.0), vec3(alpha));
outColor = vec4(col, alpha);
}`