@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.
865 lines (840 loc) • 28.7 kB
JavaScript
"use strict";
var GlyphRain = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
DEFAULTS: () => DEFAULTS,
DEFAULT_CHARSET: () => DEFAULT_CHARSET,
GlyphRain: () => GlyphRain,
createGlyphRain: () => createGlyphRain,
supportsHtmlInCanvas: () => supportsHtmlInCanvas
});
// src/browser/react.ts
var globalReact = globalThis.React;
if (!globalReact) {
throw new Error(
'[glyph-rain] browser build: global "React" not found. Load a UMD build of React (react@18.3.x) before glyph-rain.'
);
}
var Fragment = globalReact.Fragment;
var createElement = globalReact.createElement;
var useEffect = globalReact.useEffect;
var useRef = globalReact.useRef;
var useState = globalReact.useState;
var useSyncExternalStore = globalReact.useSyncExternalStore;
// src/core/atlas.ts
function buildAtlas(charset) {
const glyphs = Array.from(new Set(Array.from(charset))).filter(
(g) => g.trim().length > 0
);
if (glyphs.length === 0) glyphs.push("0", "1");
const count = glyphs.length;
const grid = Math.max(Math.ceil(Math.sqrt(count)), 1);
const cellPx = 64;
const canvas = document.createElement("canvas");
canvas.width = grid * cellPx;
canvas.height = grid * cellPx;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `600 ${Math.round(cellPx * 0.72)}px ui-monospace, SFMono-Regular, Menlo, monospace`;
for (let i = 0; i < count; i++) {
const x = (i % grid + 0.5) * cellPx;
const y = (Math.floor(i / grid) + 0.5) * cellPx;
ctx.fillText(glyphs[i], x, y);
}
return { canvas, count, grid };
}
// src/core/shaders.ts
var VERT = `#version 300 es
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);
}`;
var FRAG = `#version 300 es
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);
}`;
// src/core/supports.ts
function supportsHtmlInCanvas() {
if (typeof document === "undefined") return false;
const probe = document.createElement("canvas");
const ctx = probe.getContext("2d");
return Boolean(
ctx && typeof ctx.drawElementImage === "function" && typeof probe.requestPaint === "function"
);
}
// src/core/types.ts
var DEFAULT_CHARSET = "\uFF71\uFF72\uFF73\uFF74\uFF75\uFF76\uFF77\uFF78\uFF79\uFF7A\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F\uFF80\uFF81\uFF82\uFF83\uFF84\uFF85\uFF86\uFF87\uFF88\uFF89\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E\uFF8F\uFF90\uFF91\uFF92\uFF93\uFF94\uFF95\uFF96\uFF97\uFF98\uFF99\uFF9A\uFF9B\uFF9C\uFF9D0123456789Z*+-<>\xA6=:.";
var DEFAULTS = {
charset: DEFAULT_CHARSET,
cell: 15,
color: [0.267, 0.455, 1],
headColor: [0.169, 0.416, 1],
speed: 0.2,
speedVariance: 0.5,
density: 0.15,
trail: 0.65,
glow: 1.75,
mutate: 0,
flicker: 0,
layers: 2,
dim: 0.5,
light: 2.8,
lightRadius: 240,
lightHeight: 172,
relief: 0.05,
stir: 0.7,
stirRadius: 260,
settle: 0.9
};
// src/core/createGlyphRain.ts
function createGlyphRain(elements, options = {}) {
const config = { ...DEFAULTS, ...options };
const { source, content, output } = elements;
const maybeGl = output.getContext("webgl2", {
alpha: true,
depth: false,
stencil: false,
antialias: false,
premultipliedAlpha: true
});
if (!maybeGl || maybeGl.isContextLost()) return null;
const gl = maybeGl;
const sourceCtx = source.getContext("2d");
const paintable = source;
const htmlInCanvas = Boolean(
sourceCtx && typeof sourceCtx.drawElementImage === "function" && typeof paintable.requestPaint === "function"
);
let contentDirty = false;
let pageLum = 0;
let wake = () => {
};
function readPageLum() {
try {
const probe = document.createElement("canvas");
probe.width = probe.height = 1;
const pctx = probe.getContext("2d", { willReadFrequently: true });
if (!pctx) return 0;
let el = content;
while (el instanceof Element) {
const bgColor = getComputedStyle(el).backgroundColor;
if (bgColor && bgColor !== "transparent") {
pctx.clearRect(0, 0, 1, 1);
pctx.fillStyle = bgColor;
pctx.fillRect(0, 0, 1, 1);
const d = pctx.getImageData(0, 0, 1, 1).data;
if (d[3] > 128) {
return (0.2126 * d[0] + 0.7152 * d[1] + 0.0722 * d[2]) / 255;
}
}
el = el.parentElement;
}
} catch {
}
return 0;
}
if (htmlInCanvas) {
paintable.onpaint = () => {
try {
sourceCtx.reset();
sourceCtx.drawElementImage(content, 0, 0);
contentDirty = true;
wake();
} catch {
}
};
}
function compile(type, text) {
const shader = gl.createShader(type);
gl.shaderSource(shader, text);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("GlyphRain shader error:", gl.getShaderInfoLog(shader));
}
return shader;
}
const vertexShader = compile(gl.VERTEX_SHADER, VERT);
const fragmentShader = compile(gl.FRAGMENT_SHADER, FRAG);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const uniforms = {};
const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
const info = gl.getActiveUniform(program, i);
uniforms[info.name] = gl.getUniformLocation(program, info.name);
}
const quad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW
);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
const contentTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, contentTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
1,
1,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 0, 0])
);
const atlasTexture = gl.createTexture();
let atlasCount = 1;
let atlasGrid = 1;
let atlasCharset = "";
function syncAtlas() {
if (config.charset === atlasCharset) return;
atlasCharset = config.charset;
const atlas = buildAtlas(config.charset);
atlasCount = atlas.count;
atlasGrid = atlas.grid;
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
gl.LINEAR_MIPMAP_LINEAR
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
atlas.canvas
);
gl.generateMipmap(gl.TEXTURE_2D);
}
syncAtlas();
let dpr = 1;
function syncCanvasSize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.max(1, Math.round(output.clientWidth * dpr));
const height = Math.max(1, Math.round(output.clientHeight * dpr));
if (output.width !== width || output.height !== height) {
output.width = width;
output.height = height;
}
if (htmlInCanvas) {
const cssWidth = Math.max(1, Math.round(source.clientWidth));
const cssHeight = Math.max(1, Math.round(source.clientHeight));
if (source.width !== cssWidth * dpr || source.height !== cssHeight * dpr) {
source.width = cssWidth * dpr;
source.height = cssHeight * dpr;
}
paintable.requestPaint();
}
}
syncCanvasSize();
function uploadContent() {
if (!htmlInCanvas || !contentDirty) return;
contentDirty = false;
pageLum = readPageLum();
gl.bindTexture(gl.TEXTURE_2D, contentTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
source
);
sourceCtx.clearRect(0, 0, source.width, source.height);
}
let time = 7.3;
const WAKE_RES = 256;
const wakeCharge = new Float32Array(WAKE_RES);
const wakeField = new Float32Array(WAKE_RES * 2);
let wakeLive = false;
let wakeTouched = false;
let pointerX = 0;
let tracking = false;
const wakeTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, wakeTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RG32F,
WAKE_RES,
1,
0,
gl.RG,
gl.FLOAT,
wakeField
);
function stirAmount() {
return Math.min(Math.max(config.stir, 0), 1);
}
function wakeSpan() {
const width = Math.max(output.clientWidth, 1);
const px = Math.min(Math.max(config.stirRadius, 8), 2e3);
return Math.max(px / width, 1 / WAKE_RES);
}
function stepWake(delta) {
const stir = stirAmount();
const settleT = Math.min(Math.max(config.settle, 0.05), 8);
const decay = Math.exp(-delta / settleT);
const span = wakeSpan();
const drive = stir > 1e-3 && !reducedMotion;
const track = drive && tracking;
let live = false;
for (let i = 0; i < WAKE_RES; i++) {
let charge = wakeCharge[i] * decay;
if (track) {
const d = Math.abs((i + 0.5) / WAKE_RES - pointerX) / span;
if (d < 1) {
const t = 1 - d;
const target = t * t * (3 - 2 * t);
if (target > charge) charge = target;
}
}
if (charge < 1e-4) charge = 0;
wakeCharge[i] = charge;
if (charge > 0) {
live = true;
if (drive) {
wakeField[i * 2] += delta * stir * 2.2 * charge;
wakeTouched = true;
}
}
wakeField[i * 2 + 1] = charge;
}
if (!live && !wakeLive) return;
wakeLive = live;
gl.bindTexture(gl.TEXTURE_2D, wakeTexture);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
0,
WAKE_RES,
1,
gl.RG,
gl.FLOAT,
wakeField
);
}
function render() {
uploadContent();
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, contentTexture);
gl.uniform1i(uniforms.uContent, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
gl.uniform1i(uniforms.uAtlas, 1);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, wakeTexture);
gl.uniform1i(uniforms.uWake, 2);
gl.uniform2f(uniforms.uResolution, output.width, output.height);
gl.uniform1f(uniforms.uTime, time);
gl.uniform1f(uniforms.uCell, Math.min(Math.max(config.cell, 8), 64) * dpr);
gl.uniform1f(uniforms.uGlyphCount, atlasCount);
gl.uniform1f(uniforms.uAtlasGrid, atlasGrid);
gl.uniform3f(
uniforms.uColor,
config.color[0],
config.color[1],
config.color[2]
);
gl.uniform3f(
uniforms.uHeadColor,
config.headColor[0],
config.headColor[1],
config.headColor[2]
);
gl.uniform1f(uniforms.uSpeed, Math.min(Math.max(config.speed, 0.05), 3));
gl.uniform1f(
uniforms.uSpeedVar,
Math.min(Math.max(config.speedVariance, 0), 1)
);
gl.uniform1f(uniforms.uDensity, Math.min(Math.max(config.density, 0), 1));
gl.uniform1f(uniforms.uTrail, Math.min(Math.max(config.trail, 0.2), 3));
gl.uniform1f(uniforms.uGlow, Math.min(Math.max(config.glow, 0), 3));
gl.uniform1f(uniforms.uMutate, Math.min(Math.max(config.mutate, 0), 4));
gl.uniform1f(uniforms.uFlicker, Math.min(Math.max(config.flicker, 0), 1));
gl.uniform1f(
uniforms.uLayers,
Math.round(Math.min(Math.max(config.layers, 1), 3))
);
gl.uniform1f(uniforms.uDim, Math.min(Math.max(config.dim, 0), 1));
gl.uniform1f(uniforms.uLight, Math.min(Math.max(config.light, 0), 3));
gl.uniform1f(
uniforms.uLightRadius,
Math.min(Math.max(config.lightRadius, 20), 600) * dpr
);
gl.uniform1f(uniforms.uLightHeight, Math.max(config.lightHeight, 4) * dpr);
gl.uniform1f(uniforms.uRelief, Math.min(Math.max(config.relief, 0), 2));
gl.uniform1f(uniforms.uStir, wakeTouched ? stirAmount() : 0);
gl.uniform1f(uniforms.uScroll, content.scrollTop * dpr);
gl.uniform1f(uniforms.uPageLum, pageLum);
gl.uniform1f(uniforms.uHasContent, htmlInCanvas ? 1 : 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, output.width, output.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
let raf = 0;
let lastTime = performance.now();
let destroyed = false;
let running = false;
let visible = true;
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = motionQuery.matches;
function frame(now) {
if (destroyed) return;
if (!visible) {
running = false;
return;
}
const delta = Math.min((now - lastTime) / 1e3, 1 / 30);
lastTime = now;
if (!reducedMotion) time += delta;
stepWake(delta);
render();
if (reducedMotion && !contentDirty) {
running = false;
return;
}
raf = requestAnimationFrame(frame);
}
function start() {
if (destroyed || running || !visible) return;
running = true;
lastTime = performance.now();
raf = requestAnimationFrame(frame);
}
wake = start;
start();
function onMotionChange() {
reducedMotion = motionQuery.matches;
if (reducedMotion) {
tracking = false;
wakeCharge.fill(0);
for (let i = 0; i < WAKE_RES; i++) wakeField[i * 2 + 1] = 0;
wakeLive = true;
}
start();
}
motionQuery.addEventListener("change", onMotionChange);
content.addEventListener("scroll", start, { passive: true });
const pointerHost = output.parentElement ?? output;
function pointerNorm(event) {
const box = output.getBoundingClientRect();
if (box.width < 1) return -1;
return (event.clientX - box.left) / box.width;
}
function onPointerMove(event) {
if (reducedMotion) return;
const x = pointerNorm(event);
if (x < 0) return;
pointerX = x;
tracking = true;
start();
}
function onPointerLeave() {
tracking = false;
}
function onPointerDown(event) {
if (reducedMotion || stirAmount() <= 1e-3) return;
const x = pointerNorm(event);
if (x < 0) return;
pointerX = x;
tracking = true;
const span = wakeSpan() * 1.8;
for (let i = 0; i < WAKE_RES; i++) {
const d = Math.abs((i + 0.5) / WAKE_RES - x) / span;
if (d >= 1) continue;
const t = 1 - d;
const burst = t * t * (3 - 2 * t);
if (burst > wakeCharge[i]) wakeCharge[i] = burst;
}
start();
}
pointerHost.addEventListener("pointermove", onPointerMove, { passive: true });
pointerHost.addEventListener("pointerleave", onPointerLeave, { passive: true });
pointerHost.addEventListener("pointercancel", onPointerLeave, { passive: true });
pointerHost.addEventListener("pointerdown", onPointerDown, { passive: true });
const observer = new ResizeObserver(() => {
syncCanvasSize();
start();
});
observer.observe(output);
observer.observe(content);
const intersection = new IntersectionObserver((entries) => {
visible = entries[entries.length - 1]?.isIntersecting ?? true;
if (visible) start();
});
intersection.observe(output);
return {
setOptions(next) {
let changed = false;
for (const [key, value] of Object.entries(next)) {
const prev = config[key];
if (Array.isArray(value) && Array.isArray(prev)) {
if (value.length !== prev.length || value.some((item, i) => item !== prev[i])) {
changed = true;
break;
}
} else if (prev !== value) {
changed = true;
break;
}
}
Object.assign(config, next);
if (!changed) return;
syncAtlas();
syncCanvasSize();
start();
},
resize() {
syncCanvasSize();
start();
},
destroy() {
destroyed = true;
cancelAnimationFrame(raf);
observer.disconnect();
intersection.disconnect();
motionQuery.removeEventListener("change", onMotionChange);
content.removeEventListener("scroll", start);
pointerHost.removeEventListener("pointermove", onPointerMove);
pointerHost.removeEventListener("pointerleave", onPointerLeave);
pointerHost.removeEventListener("pointercancel", onPointerLeave);
pointerHost.removeEventListener("pointerdown", onPointerDown);
gl.deleteTexture(contentTexture);
gl.deleteTexture(atlasTexture);
gl.deleteTexture(wakeTexture);
gl.deleteProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
gl.deleteBuffer(quad);
if (htmlInCanvas) paintable.onpaint = null;
}
};
}
// src/browser/jsx-runtime.ts
function jsx(type, props, key) {
const create = createElement;
return key === void 0 ? create(type, props) : create(type, { ...props, key });
}
var jsxs = jsx;
// src/react/GlyphRain.tsx
var emptySubscribe = () => () => {
};
function GlyphRain({
children,
className,
style,
...options
}) {
const sourceRef = useRef(null);
const contentRef = useRef(null);
const outputRef = useRef(null);
const instanceRef = useRef(null);
const [initialOptions] = useState(options);
const [failed, setFailed] = useState(false);
const supported = useSyncExternalStore(
emptySubscribe,
supportsHtmlInCanvas,
() => false
);
const native = supported && !failed;
useEffect(() => {
const source = sourceRef.current;
const content = contentRef.current;
const output = outputRef.current;
if (!source || !content || !output) return;
instanceRef.current = createGlyphRain(
{ source, content, output },
initialOptions
);
if (native && !instanceRef.current) setFailed(true);
return () => {
instanceRef.current?.destroy();
instanceRef.current = null;
};
}, [initialOptions, native]);
useEffect(() => {
instanceRef.current?.setOptions(options);
});
return /* @__PURE__ */ jsxs("div", { className, style: { position: "relative", ...style }, children: [
/* @__PURE__ */ jsx(
"canvas",
{
ref: sourceRef,
layoutsubtree: "true",
suppressHydrationWarning: true,
style: native ? {
position: "absolute",
inset: 0,
width: "100%",
height: "100%"
} : { display: "none" },
children: native ? /* @__PURE__ */ jsx(
"div",
{
ref: contentRef,
style: {
position: "relative",
width: "100%",
height: "100%",
overflow: "auto"
},
children
}
) : null
}
),
!native ? /* @__PURE__ */ jsx(
"div",
{
ref: contentRef,
style: {
position: "relative",
width: "100%",
height: "100%",
overflow: "auto"
},
children
}
) : null,
/* @__PURE__ */ jsx(
"canvas",
{
ref: outputRef,
"aria-hidden": true,
style: {
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none"
}
}
)
] });
}
return __toCommonJS(src_exports);
})();
//# sourceMappingURL=glyph-rain.iife.js.map