@fxshelf/glyph-rain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,514 @@
1
+ import { buildAtlas } from './atlas'
2
+ import { FRAG, VERT } from './shaders'
3
+ import { supportsHtmlInCanvas } from './supports'
4
+ import {
5
+ DEFAULTS,
6
+ type ElementImageContext,
7
+ type GlyphRainElements,
8
+ type GlyphRainInstance,
9
+ type GlyphRainOptions,
10
+ type PaintableCanvas,
11
+ } from './types'
12
+
13
+ export function createGlyphRain(
14
+ elements: GlyphRainElements,
15
+ options: GlyphRainOptions = {},
16
+ ): GlyphRainInstance | null {
17
+ const config = { ...DEFAULTS, ...options }
18
+ const { source, content, output } = elements
19
+
20
+ const maybeGl = output.getContext('webgl2', {
21
+ alpha: true,
22
+ depth: false,
23
+ stencil: false,
24
+ antialias: false,
25
+ premultipliedAlpha: true,
26
+ })
27
+ if (!maybeGl || maybeGl.isContextLost()) return null
28
+ const gl: WebGL2RenderingContext = maybeGl
29
+
30
+ const sourceCtx = source.getContext('2d') as ElementImageContext | null
31
+ const paintable = source as PaintableCanvas
32
+ const htmlInCanvas = Boolean(
33
+ sourceCtx &&
34
+ typeof sourceCtx.drawElementImage === 'function' &&
35
+ typeof paintable.requestPaint === 'function',
36
+ )
37
+
38
+ let contentDirty = false
39
+ let pageLum = 0
40
+ let wake = () => {}
41
+
42
+ function readPageLum(): number {
43
+ try {
44
+ const probe = document.createElement('canvas')
45
+ probe.width = probe.height = 1
46
+ const pctx = probe.getContext('2d', { willReadFrequently: true })
47
+ if (!pctx) return 0
48
+ let el: Element | null = content
49
+ while (el instanceof Element) {
50
+ const bgColor = getComputedStyle(el).backgroundColor
51
+ if (bgColor && bgColor !== 'transparent') {
52
+ pctx.clearRect(0, 0, 1, 1)
53
+ pctx.fillStyle = bgColor
54
+ pctx.fillRect(0, 0, 1, 1)
55
+ const d = pctx.getImageData(0, 0, 1, 1).data
56
+ if (d[3]! > 128) {
57
+ return (0.2126 * d[0]! + 0.7152 * d[1]! + 0.0722 * d[2]!) / 255
58
+ }
59
+ }
60
+ el = el.parentElement
61
+ }
62
+ } catch {
63
+ /* ignore */
64
+ }
65
+ return 0
66
+ }
67
+
68
+ if (htmlInCanvas) {
69
+ paintable.onpaint = () => {
70
+ try {
71
+ sourceCtx!.reset()
72
+ sourceCtx!.drawElementImage!(content, 0, 0)
73
+ contentDirty = true
74
+ wake()
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ }
79
+ }
80
+
81
+ function compile(type: number, text: string): WebGLShader {
82
+ const shader = gl.createShader(type)!
83
+ gl.shaderSource(shader, text)
84
+ gl.compileShader(shader)
85
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
86
+ console.error('GlyphRain shader error:', gl.getShaderInfoLog(shader))
87
+ }
88
+ return shader
89
+ }
90
+
91
+ const vertexShader = compile(gl.VERTEX_SHADER, VERT)
92
+ const fragmentShader = compile(gl.FRAGMENT_SHADER, FRAG)
93
+ const program = gl.createProgram()!
94
+ gl.attachShader(program, vertexShader)
95
+ gl.attachShader(program, fragmentShader)
96
+ gl.linkProgram(program)
97
+
98
+ const uniforms: Record<string, WebGLUniformLocation> = {}
99
+ const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)
100
+ for (let i = 0; i < uniformCount; i++) {
101
+ const info = gl.getActiveUniform(program, i)!
102
+ uniforms[info.name] = gl.getUniformLocation(program, info.name)!
103
+ }
104
+
105
+ const quad = gl.createBuffer()
106
+ gl.bindBuffer(gl.ARRAY_BUFFER, quad)
107
+ gl.bufferData(
108
+ gl.ARRAY_BUFFER,
109
+ new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
110
+ gl.STATIC_DRAW,
111
+ )
112
+ gl.enableVertexAttribArray(0)
113
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
114
+
115
+ const contentTexture = gl.createTexture()!
116
+ gl.bindTexture(gl.TEXTURE_2D, contentTexture)
117
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
118
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
119
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
120
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
121
+ gl.texImage2D(
122
+ gl.TEXTURE_2D,
123
+ 0,
124
+ gl.RGBA,
125
+ 1,
126
+ 1,
127
+ 0,
128
+ gl.RGBA,
129
+ gl.UNSIGNED_BYTE,
130
+ new Uint8Array([0, 0, 0, 0]),
131
+ )
132
+
133
+ const atlasTexture = gl.createTexture()!
134
+ let atlasCount = 1
135
+ let atlasGrid = 1
136
+ let atlasCharset = ''
137
+
138
+ function syncAtlas() {
139
+ if (config.charset === atlasCharset) return
140
+ atlasCharset = config.charset
141
+ const atlas = buildAtlas(config.charset)
142
+ atlasCount = atlas.count
143
+ atlasGrid = atlas.grid
144
+ gl.bindTexture(gl.TEXTURE_2D, atlasTexture)
145
+ gl.texParameteri(
146
+ gl.TEXTURE_2D,
147
+ gl.TEXTURE_MIN_FILTER,
148
+ gl.LINEAR_MIPMAP_LINEAR,
149
+ )
150
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
151
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
152
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
153
+ gl.texImage2D(
154
+ gl.TEXTURE_2D,
155
+ 0,
156
+ gl.RGBA,
157
+ gl.RGBA,
158
+ gl.UNSIGNED_BYTE,
159
+ atlas.canvas,
160
+ )
161
+ gl.generateMipmap(gl.TEXTURE_2D)
162
+ }
163
+
164
+ syncAtlas()
165
+
166
+ let dpr = 1
167
+
168
+ function syncCanvasSize() {
169
+ dpr = Math.min(window.devicePixelRatio || 1, 2)
170
+ const width = Math.max(1, Math.round(output.clientWidth * dpr))
171
+ const height = Math.max(1, Math.round(output.clientHeight * dpr))
172
+ if (output.width !== width || output.height !== height) {
173
+ output.width = width
174
+ output.height = height
175
+ }
176
+ if (htmlInCanvas) {
177
+ const cssWidth = Math.max(1, Math.round(source.clientWidth))
178
+ const cssHeight = Math.max(1, Math.round(source.clientHeight))
179
+ if (
180
+ source.width !== cssWidth * dpr ||
181
+ source.height !== cssHeight * dpr
182
+ ) {
183
+ source.width = cssWidth * dpr
184
+ source.height = cssHeight * dpr
185
+ }
186
+ paintable.requestPaint!()
187
+ }
188
+ }
189
+
190
+ syncCanvasSize()
191
+
192
+ function uploadContent() {
193
+ if (!htmlInCanvas || !contentDirty) return
194
+ contentDirty = false
195
+ pageLum = readPageLum()
196
+ gl.bindTexture(gl.TEXTURE_2D, contentTexture)
197
+ gl.texImage2D(
198
+ gl.TEXTURE_2D,
199
+ 0,
200
+ gl.RGBA,
201
+ gl.RGBA,
202
+ gl.UNSIGNED_BYTE,
203
+ source,
204
+ )
205
+ sourceCtx!.clearRect(0, 0, source.width, source.height)
206
+ }
207
+
208
+ let time = 7.3
209
+
210
+ const WAKE_RES = 256
211
+ const wakeCharge = new Float32Array(WAKE_RES)
212
+ const wakeField = new Float32Array(WAKE_RES * 2)
213
+ let wakeLive = false
214
+ let wakeTouched = false
215
+ let pointerX = 0
216
+ let tracking = false
217
+
218
+ const wakeTexture = gl.createTexture()!
219
+ gl.bindTexture(gl.TEXTURE_2D, wakeTexture)
220
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
221
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
222
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
223
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
224
+ gl.texImage2D(
225
+ gl.TEXTURE_2D,
226
+ 0,
227
+ gl.RG32F,
228
+ WAKE_RES,
229
+ 1,
230
+ 0,
231
+ gl.RG,
232
+ gl.FLOAT,
233
+ wakeField,
234
+ )
235
+
236
+ function stirAmount(): number {
237
+ return Math.min(Math.max(config.stir, 0), 1)
238
+ }
239
+
240
+ function wakeSpan(): number {
241
+ const width = Math.max(output.clientWidth, 1)
242
+ const px = Math.min(Math.max(config.stirRadius, 8), 2000)
243
+ return Math.max(px / width, 1 / WAKE_RES)
244
+ }
245
+
246
+ function stepWake(delta: number) {
247
+ const stir = stirAmount()
248
+ const settleT = Math.min(Math.max(config.settle, 0.05), 8)
249
+ const decay = Math.exp(-delta / settleT)
250
+ const span = wakeSpan()
251
+ const drive = stir > 0.001 && !reducedMotion
252
+ const track = drive && tracking
253
+ let live = false
254
+ for (let i = 0; i < WAKE_RES; i++) {
255
+ let charge = wakeCharge[i]! * decay
256
+ if (track) {
257
+ const d = Math.abs((i + 0.5) / WAKE_RES - pointerX) / span
258
+ if (d < 1) {
259
+ const t = 1 - d
260
+ const target = t * t * (3 - 2 * t)
261
+ if (target > charge) charge = target
262
+ }
263
+ }
264
+ if (charge < 1e-4) charge = 0
265
+ wakeCharge[i] = charge
266
+ if (charge > 0) {
267
+ live = true
268
+ if (drive) {
269
+ wakeField[i * 2]! += delta * stir * 2.2 * charge
270
+ wakeTouched = true
271
+ }
272
+ }
273
+ wakeField[i * 2 + 1] = charge
274
+ }
275
+ if (!live && !wakeLive) return
276
+ wakeLive = live
277
+ gl.bindTexture(gl.TEXTURE_2D, wakeTexture)
278
+ gl.texSubImage2D(
279
+ gl.TEXTURE_2D,
280
+ 0,
281
+ 0,
282
+ 0,
283
+ WAKE_RES,
284
+ 1,
285
+ gl.RG,
286
+ gl.FLOAT,
287
+ wakeField,
288
+ )
289
+ }
290
+
291
+ function render() {
292
+ uploadContent()
293
+ gl.useProgram(program)
294
+ gl.activeTexture(gl.TEXTURE0)
295
+ gl.bindTexture(gl.TEXTURE_2D, contentTexture)
296
+ gl.uniform1i(uniforms.uContent!, 0)
297
+ gl.activeTexture(gl.TEXTURE1)
298
+ gl.bindTexture(gl.TEXTURE_2D, atlasTexture)
299
+ gl.uniform1i(uniforms.uAtlas!, 1)
300
+ gl.activeTexture(gl.TEXTURE2)
301
+ gl.bindTexture(gl.TEXTURE_2D, wakeTexture)
302
+ gl.uniform1i(uniforms.uWake!, 2)
303
+ gl.uniform2f(uniforms.uResolution!, output.width, output.height)
304
+ gl.uniform1f(uniforms.uTime!, time)
305
+ gl.uniform1f(uniforms.uCell!, Math.min(Math.max(config.cell, 8), 64) * dpr)
306
+ gl.uniform1f(uniforms.uGlyphCount!, atlasCount)
307
+ gl.uniform1f(uniforms.uAtlasGrid!, atlasGrid)
308
+ gl.uniform3f(
309
+ uniforms.uColor!,
310
+ config.color[0],
311
+ config.color[1],
312
+ config.color[2],
313
+ )
314
+ gl.uniform3f(
315
+ uniforms.uHeadColor!,
316
+ config.headColor[0],
317
+ config.headColor[1],
318
+ config.headColor[2],
319
+ )
320
+ gl.uniform1f(uniforms.uSpeed!, Math.min(Math.max(config.speed, 0.05), 3))
321
+ gl.uniform1f(
322
+ uniforms.uSpeedVar!,
323
+ Math.min(Math.max(config.speedVariance, 0), 1),
324
+ )
325
+ gl.uniform1f(uniforms.uDensity!, Math.min(Math.max(config.density, 0), 1))
326
+ gl.uniform1f(uniforms.uTrail!, Math.min(Math.max(config.trail, 0.2), 3))
327
+ gl.uniform1f(uniforms.uGlow!, Math.min(Math.max(config.glow, 0), 3))
328
+ gl.uniform1f(uniforms.uMutate!, Math.min(Math.max(config.mutate, 0), 4))
329
+ gl.uniform1f(uniforms.uFlicker!, Math.min(Math.max(config.flicker, 0), 1))
330
+ gl.uniform1f(
331
+ uniforms.uLayers!,
332
+ Math.round(Math.min(Math.max(config.layers, 1), 3)),
333
+ )
334
+ gl.uniform1f(uniforms.uDim!, Math.min(Math.max(config.dim, 0), 1))
335
+ gl.uniform1f(uniforms.uLight!, Math.min(Math.max(config.light, 0), 3))
336
+ gl.uniform1f(
337
+ uniforms.uLightRadius!,
338
+ Math.min(Math.max(config.lightRadius, 20), 600) * dpr,
339
+ )
340
+ gl.uniform1f(uniforms.uLightHeight!, Math.max(config.lightHeight, 4) * dpr)
341
+ gl.uniform1f(uniforms.uRelief!, Math.min(Math.max(config.relief, 0), 2))
342
+ gl.uniform1f(uniforms.uStir!, wakeTouched ? stirAmount() : 0)
343
+ gl.uniform1f(uniforms.uScroll!, content.scrollTop * dpr)
344
+ gl.uniform1f(uniforms.uPageLum!, pageLum)
345
+ gl.uniform1f(uniforms.uHasContent!, htmlInCanvas ? 1 : 0)
346
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
347
+ gl.viewport(0, 0, output.width, output.height)
348
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
349
+ }
350
+
351
+ let raf = 0
352
+ let lastTime = performance.now()
353
+ let destroyed = false
354
+ let running = false
355
+ let visible = true
356
+
357
+ const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
358
+ let reducedMotion = motionQuery.matches
359
+
360
+ function frame(now: number) {
361
+ if (destroyed) return
362
+ if (!visible) {
363
+ running = false
364
+ return
365
+ }
366
+ const delta = Math.min((now - lastTime) / 1000, 1 / 30)
367
+ lastTime = now
368
+ if (!reducedMotion) time += delta
369
+ stepWake(delta)
370
+ render()
371
+ if (reducedMotion && !contentDirty) {
372
+ running = false
373
+ return
374
+ }
375
+ raf = requestAnimationFrame(frame)
376
+ }
377
+
378
+ function start() {
379
+ if (destroyed || running || !visible) return
380
+ running = true
381
+ lastTime = performance.now()
382
+ raf = requestAnimationFrame(frame)
383
+ }
384
+
385
+ wake = start
386
+ start()
387
+
388
+ function onMotionChange() {
389
+ reducedMotion = motionQuery.matches
390
+ if (reducedMotion) {
391
+ tracking = false
392
+ wakeCharge.fill(0)
393
+ for (let i = 0; i < WAKE_RES; i++) wakeField[i * 2 + 1] = 0
394
+ wakeLive = true
395
+ }
396
+ start()
397
+ }
398
+ motionQuery.addEventListener('change', onMotionChange)
399
+ content.addEventListener('scroll', start, { passive: true })
400
+
401
+ const pointerHost = output.parentElement ?? output
402
+
403
+ function pointerNorm(event: PointerEvent): number {
404
+ const box = output.getBoundingClientRect()
405
+ if (box.width < 1) return -1
406
+ return (event.clientX - box.left) / box.width
407
+ }
408
+
409
+ function onPointerMove(event: PointerEvent) {
410
+ if (reducedMotion) return
411
+ const x = pointerNorm(event)
412
+ if (x < 0) return
413
+ pointerX = x
414
+ tracking = true
415
+ start()
416
+ }
417
+
418
+ function onPointerLeave() {
419
+ tracking = false
420
+ }
421
+
422
+ function onPointerDown(event: PointerEvent) {
423
+ if (reducedMotion || stirAmount() <= 0.001) return
424
+ const x = pointerNorm(event)
425
+ if (x < 0) return
426
+ pointerX = x
427
+ tracking = true
428
+ const span = wakeSpan() * 1.8
429
+ for (let i = 0; i < WAKE_RES; i++) {
430
+ const d = Math.abs((i + 0.5) / WAKE_RES - x) / span
431
+ if (d >= 1) continue
432
+ const t = 1 - d
433
+ const burst = t * t * (3 - 2 * t)
434
+ if (burst > wakeCharge[i]!) wakeCharge[i] = burst
435
+ }
436
+ start()
437
+ }
438
+
439
+ pointerHost.addEventListener('pointermove', onPointerMove, { passive: true })
440
+ pointerHost.addEventListener('pointerleave', onPointerLeave, { passive: true })
441
+ pointerHost.addEventListener('pointercancel', onPointerLeave, { passive: true })
442
+ pointerHost.addEventListener('pointerdown', onPointerDown, { passive: true })
443
+
444
+ const observer = new ResizeObserver(() => {
445
+ syncCanvasSize()
446
+ start()
447
+ })
448
+ observer.observe(output)
449
+ observer.observe(content)
450
+
451
+ const intersection = new IntersectionObserver((entries) => {
452
+ visible = entries[entries.length - 1]?.isIntersecting ?? true
453
+ if (visible) start()
454
+ })
455
+ intersection.observe(output)
456
+
457
+ return {
458
+ setOptions(next) {
459
+ let changed = false
460
+ for (const [key, value] of Object.entries(next)) {
461
+ const prev = config[key as keyof typeof config]
462
+ if (Array.isArray(value) && Array.isArray(prev)) {
463
+ if (
464
+ value.length !== prev.length ||
465
+ value.some((item, i) => item !== prev[i])
466
+ ) {
467
+ changed = true
468
+ break
469
+ }
470
+ } else if (prev !== value) {
471
+ changed = true
472
+ break
473
+ }
474
+ }
475
+ Object.assign(config, next)
476
+ if (!changed) return
477
+ syncAtlas()
478
+ syncCanvasSize()
479
+ start()
480
+ },
481
+ resize() {
482
+ syncCanvasSize()
483
+ start()
484
+ },
485
+ destroy() {
486
+ destroyed = true
487
+ cancelAnimationFrame(raf)
488
+ observer.disconnect()
489
+ intersection.disconnect()
490
+ motionQuery.removeEventListener('change', onMotionChange)
491
+ content.removeEventListener('scroll', start)
492
+ pointerHost.removeEventListener('pointermove', onPointerMove)
493
+ pointerHost.removeEventListener('pointerleave', onPointerLeave)
494
+ pointerHost.removeEventListener('pointercancel', onPointerLeave)
495
+ pointerHost.removeEventListener('pointerdown', onPointerDown)
496
+ gl.deleteTexture(contentTexture)
497
+ gl.deleteTexture(atlasTexture)
498
+ gl.deleteTexture(wakeTexture)
499
+ gl.deleteProgram(program)
500
+ gl.deleteShader(vertexShader)
501
+ gl.deleteShader(fragmentShader)
502
+ gl.deleteBuffer(quad)
503
+ if (htmlInCanvas) paintable.onpaint = null
504
+ },
505
+ }
506
+ }
507
+
508
+ export { supportsHtmlInCanvas }
509
+ export type {
510
+ GlyphRainElements,
511
+ GlyphRainInstance,
512
+ GlyphRainOptions,
513
+ } from './types'
514
+ export { DEFAULT_CHARSET, DEFAULTS } from './types'
@@ -0,0 +1,208 @@
1
+ export const VERT = `#version 300 es
2
+ precision highp float;
3
+ layout(location = 0) in vec2 aPos;
4
+ out vec2 vUv;
5
+ void main () {
6
+ vUv = aPos * 0.5 + 0.5;
7
+ gl_Position = vec4(aPos, 0.0, 1.0);
8
+ }`
9
+
10
+ export const FRAG = `#version 300 es
11
+ precision highp float;
12
+ in vec2 vUv;
13
+ out vec4 outColor;
14
+ uniform sampler2D uContent;
15
+ uniform sampler2D uAtlas;
16
+ uniform sampler2D uWake;
17
+ uniform vec2 uResolution;
18
+ uniform float uTime;
19
+ uniform float uCell;
20
+ uniform float uGlyphCount;
21
+ uniform float uAtlasGrid;
22
+ uniform vec3 uColor;
23
+ uniform vec3 uHeadColor;
24
+ uniform float uSpeed;
25
+ uniform float uSpeedVar;
26
+ uniform float uDensity;
27
+ uniform float uTrail;
28
+ uniform float uGlow;
29
+ uniform float uMutate;
30
+ uniform float uFlicker;
31
+ uniform float uLayers;
32
+ uniform float uDim;
33
+ uniform float uLight;
34
+ uniform float uLightRadius;
35
+ uniform float uLightHeight;
36
+ uniform float uRelief;
37
+ uniform float uStir;
38
+ uniform float uScroll;
39
+ uniform float uPageLum;
40
+ uniform float uHasContent;
41
+
42
+ float hash11(float p) {
43
+ p = fract(p * 0.1031);
44
+ p *= p + 33.33;
45
+ p *= p + p;
46
+ return fract(p);
47
+ }
48
+
49
+ float hash21(vec2 p) {
50
+ vec3 q = fract(vec3(p.xyx) * 0.1031);
51
+ q += dot(q, q.yzx + 33.33);
52
+ return fract((q.x + q.y) * q.z);
53
+ }
54
+
55
+ float glyphMask(vec2 px, float cell, float seed) {
56
+ vec2 id = floor(px / cell);
57
+ vec2 f = fract(px / cell);
58
+ f = f * 0.74 + 0.13;
59
+ f.x = 1.0 - f.x;
60
+ float tick = floor(uTime * uMutate * 1.6 + hash21(id + seed) * 9.0);
61
+ float idx = floor(
62
+ hash21(id * 1.71 + vec2(seed + tick * 7.31, tick * 0.613)) * uGlyphCount
63
+ );
64
+ float gx = mod(idx, uAtlasGrid);
65
+ float gy = floor(idx / uAtlasGrid);
66
+ vec2 auv = (vec2(gx, gy) + f) / uAtlasGrid;
67
+ return texture(uAtlas, auv).a;
68
+ }
69
+
70
+ float colSpeed(float col, float seed) {
71
+ float variance = mix(0.35, 1.0, hash11(col * 0.37 + seed + 3.1));
72
+ return uSpeed * mix(1.0, variance, uSpeedVar) * 0.5;
73
+ }
74
+
75
+ float colOffset(float col, float seed) {
76
+ return hash11(col * 1.713 + seed) * 9.0;
77
+ }
78
+
79
+ vec2 wakeAt(float xpx) {
80
+ float u = clamp(xpx / max(uResolution.x, 1.0), 0.0, 1.0);
81
+ return texture(uWake, vec2(u, 0.5)).rg;
82
+ }
83
+
84
+ float lum(vec3 c) {
85
+ return dot(c, vec3(0.299, 0.587, 0.114));
86
+ }
87
+
88
+ void main () {
89
+ vec2 frag = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y + uScroll);
90
+ vec2 cuv = vec2(vUv.x, 1.0 - vUv.y);
91
+ float yn = 1.0 - frag.y / uResolution.y;
92
+
93
+ const float scales[3] = float[3](1.0, 1.5, 2.2);
94
+ const float weights[3] = float[3](1.0, 0.45, 0.22);
95
+ const float seeds[3] = float[3](0.0, 19.7, 41.3);
96
+
97
+ float g = 0.0;
98
+ float headG = 0.0;
99
+ for (int l = 0; l < 3; l++) {
100
+ if (float(l) >= uLayers) break;
101
+ float cell = uCell * scales[l];
102
+ float col = floor(frag.x / cell);
103
+ float sp = colSpeed(col, seeds[l]);
104
+ float off = colOffset(col, seeds[l]);
105
+ vec2 wk = uStir > 0.0 ? wakeAt((col + 0.5) * cell) : vec2(0.0);
106
+ float exc = uStir * wk.y;
107
+ float T = uTime * sp + off + sp * wk.x;
108
+ float phase = fract(yn + T);
109
+ float cyc = floor(yn + T);
110
+ float gate = step(hash21(vec2(col, cyc) + seeds[l]), uDensity);
111
+ float b = clamp(uTrail / (phase * 22.0), 0.0, 1.3) - 0.04;
112
+ if (b <= 0.0 || gate < 0.5) continue;
113
+ float flick = 1.0 + uFlicker * 0.6 *
114
+ sin(uTime * 14.0 + hash21(vec2(col, cyc)) * 40.0 + phase * 30.0);
115
+ float m = glyphMask(frag, cell, seeds[l] + cyc * 0.173);
116
+ float cellYn = cell / uResolution.y;
117
+ float head = 1.0 - smoothstep(0.0, cellYn * 1.2, phase);
118
+ g += m * b * flick * weights[l] * (1.0 + head * uGlow * 1.4) *
119
+ (1.0 + exc * 1.6);
120
+ headG += m * head * weights[l] * uGlow * (1.0 + exc * 1.1);
121
+ }
122
+ g = max(g, 0.0);
123
+
124
+ if (uHasContent < 0.5) {
125
+ vec3 rainCol = mix(uColor, uHeadColor, clamp(headG, 0.0, 1.0));
126
+ float a = clamp(g, 0.0, 1.0);
127
+ outColor = vec4(rainCol * a, a);
128
+ return;
129
+ }
130
+
131
+ vec2 e = vec2(3.0, 0.0) / uResolution;
132
+ vec4 content = texture(uContent, cuv);
133
+ float lC = lum(content.rgb);
134
+ float lX1 = lum(texture(uContent, clamp(cuv - e.xy, 0.0, 1.0)).rgb);
135
+ float lX2 = lum(texture(uContent, clamp(cuv + e.xy, 0.0, 1.0)).rgb);
136
+ float lY1 = lum(texture(uContent, clamp(cuv - e.yx, 0.0, 1.0)).rgb);
137
+ float lY2 = lum(texture(uContent, clamp(cuv + e.yx, 0.0, 1.0)).rgb);
138
+ vec3 N = normalize(vec3(
139
+ -(lX2 - lX1) * uRelief * 4.0,
140
+ -(lY2 - lY1) * uRelief * 4.0,
141
+ 1.0
142
+ ));
143
+ float reliefMix = clamp(uRelief, 0.0, 1.0);
144
+ vec2 e2 = vec2(30.0, 0.0) / uResolution;
145
+ float bgL = (lC
146
+ + lum(texture(uContent, clamp(cuv - e2.xy, 0.0, 1.0)).rgb)
147
+ + lum(texture(uContent, clamp(cuv + e2.xy, 0.0, 1.0)).rgb)
148
+ + lum(texture(uContent, clamp(cuv - e2.yx, 0.0, 1.0)).rgb)
149
+ + lum(texture(uContent, clamp(cuv + e2.yx, 0.0, 1.0)).rgb)) * 0.2;
150
+ float bright = smoothstep(0.55, 0.8, uPageLum) * smoothstep(0.2, 0.45, bgL);
151
+
152
+ float lightSum = 0.0;
153
+ float sigma2 = uLightRadius * uLightRadius * 0.5;
154
+ float reach = uLightRadius * 1.6;
155
+ float stride = max(1.0, ceil((uLightRadius * 1.7) / (uCell * 12.0)));
156
+ float baseCol = floor(floor(frag.x / uCell) / stride);
157
+ for (int o = -12; o <= 12; o++) {
158
+ float c = (baseCol + float(o)) * stride;
159
+ if (c < 0.0) continue;
160
+ float dx = (c + 0.5) * uCell - frag.x;
161
+ float wx = 1.0 - smoothstep(reach * 0.7, reach, abs(dx));
162
+ if (wx <= 0.0) continue;
163
+ float sp = colSpeed(c, 0.0);
164
+ float off = colOffset(c, 0.0);
165
+ vec2 wk = uStir > 0.0 ? wakeAt((c + 0.5) * uCell) : vec2(0.0);
166
+ float lampBoost = 1.0 + uStir * wk.y * 1.4;
167
+ float T = uTime * sp + off + sp * wk.x;
168
+ float s = 1.0 - frag.y / uResolution.y + T;
169
+ float k0 = floor(s);
170
+ for (int h = 0; h < 2; h++) {
171
+ float k = k0 + float(h);
172
+ float gate = step(hash21(vec2(c, k)), uDensity);
173
+ if (gate < 0.5) continue;
174
+ float lamp = 0.6 + 0.4 * hash11(c * 3.97 + k * 0.713);
175
+ float headDocY = (1.0 - (k - T)) * uResolution.y;
176
+ vec3 dv = vec3(dx, headDocY - frag.y, uLightHeight);
177
+ float d2 = dot(dv, dv);
178
+ float att = exp(-d2 / sigma2);
179
+ vec3 L = dv * inversesqrt(max(d2, 1.0));
180
+ float dif = mix(1.0, 0.25 + 0.75 * max(dot(N, L), 0.0), reliefMix);
181
+ lightSum += att * dif * wx * lamp * lampBoost;
182
+ }
183
+ }
184
+ float ls = lightSum * uLight * (0.6 + 0.4 * uGlow);
185
+ float lit = 2.2 * ls / (ls + 1.1);
186
+
187
+ float dimEff = uDim * (1.0 - bright);
188
+ float shade = mix(
189
+ clamp(1.0 - dimEff, 0.0, 1.0),
190
+ 1.0,
191
+ smoothstep(0.0, 1.0, lit)
192
+ );
193
+ vec3 col = content.rgb * shade;
194
+ col += uColor * lit * 0.14 * (1.0 - lC * 0.75) * (1.0 - bright);
195
+ col += uColor * clamp(lit - 1.0, 0.0, 1.0) * 0.1 * (1.0 - bright);
196
+
197
+ vec3 glyphCol = mix(uColor, uColor * 0.24 + vec3(0.02), lC * (1.0 - bright));
198
+ glyphCol = mix(glyphCol, uHeadColor, clamp(headG, 0.0, 1.0));
199
+ glyphCol = mix(glyphCol, vec3(1.0), bright * clamp(headG - 0.6, 0.0, 0.4));
200
+ float gA = clamp(g, 0.0, 1.0);
201
+ float knock = gA * mix(mix(0.3, 0.88, lC), 1.0, bright);
202
+ float paint = min(g, 1.5) * mix(1.0, mix(0.92, 1.0, bright), lC);
203
+ col = col * (1.0 - knock) + glyphCol * paint;
204
+
205
+ float alpha = max(content.a, gA);
206
+ col = clamp(col, vec3(0.0), vec3(alpha));
207
+ outColor = vec4(col, alpha);
208
+ }`