@ash-qw/dsh-theme-prts 2.0.0 → 2.1.1

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,818 @@
1
+ 'use client';
2
+
3
+ /* eslint-disable react-hooks/exhaustive-deps */
4
+ import { useEffect, useState, useRef, useId } from 'react';
5
+ import './GlassSurface.css';
6
+
7
+ const FAR = 1e20;
8
+
9
+ const GLASS_VERTEX = `#version 300 es
10
+ in vec2 aPosition;
11
+ void main() {
12
+ gl_Position = vec4(aPosition, 0.0, 1.0);
13
+ }`;
14
+
15
+ const GLASS_FRAGMENT = `#version 300 es
16
+ precision highp float;
17
+ uniform sampler2D uBackdrop;
18
+ uniform sampler2D uMap;
19
+ uniform vec2 uCanvas;
20
+ uniform vec2 uSize;
21
+ uniform vec2 uOrigin;
22
+ uniform vec2 uStep;
23
+ uniform vec3 uScale;
24
+ uniform ivec2 uChannel;
25
+ uniform float uSpread;
26
+ uniform float uLod;
27
+ uniform float uSaturation;
28
+ out vec4 outColor;
29
+
30
+ vec4 bend(vec2 p) {
31
+ vec4 map = texture(uMap, clamp(p / uSize, 0.0, 1.0));
32
+ vec2 shift = vec2(map[uChannel.x], map[uChannel.y]) - 0.5;
33
+ vec2 base = uOrigin + p * uStep;
34
+ vec4 red = textureLod(uBackdrop, base + shift * uScale.r * uStep, uLod);
35
+ vec4 green = textureLod(uBackdrop, base + shift * uScale.g * uStep, uLod);
36
+ vec4 blue = textureLod(uBackdrop, base + shift * uScale.b * uStep, uLod);
37
+ return vec4(red.r, green.g, blue.b, (red.a + green.a + blue.a) / 3.0);
38
+ }
39
+
40
+ void main() {
41
+ vec2 p = vec2(gl_FragCoord.x, uCanvas.y - gl_FragCoord.y) / uCanvas * uSize;
42
+ vec4 color = vec4(0.0);
43
+ if (uSpread < 0.05) {
44
+ color = bend(p);
45
+ } else {
46
+ float total = 0.0;
47
+ for (int j = -2; j <= 2; j++) {
48
+ for (int i = -2; i <= 2; i++) {
49
+ vec2 offset = vec2(float(i), float(j)) * uSpread * 0.8;
50
+ float weight = exp(-dot(offset, offset) / (2.0 * uSpread * uSpread));
51
+ color += bend(p + offset) * weight;
52
+ total += weight;
53
+ }
54
+ }
55
+ color /= total;
56
+ }
57
+ vec3 rgb = color.a > 0.0 ? color.rgb / color.a : vec3(0.0);
58
+ float s = uSaturation;
59
+ mat3 tone = mat3(
60
+ 0.213 + 0.787 * s, 0.213 - 0.213 * s, 0.213 - 0.213 * s,
61
+ 0.715 - 0.715 * s, 0.715 + 0.285 * s, 0.715 - 0.715 * s,
62
+ 0.072 - 0.072 * s, 0.072 - 0.072 * s, 0.072 + 0.928 * s
63
+ );
64
+ rgb = clamp(tone * rgb, 0.0, 1.0);
65
+ outColor = vec4(rgb * color.a, color.a);
66
+ }`;
67
+
68
+ const CHANNELS = { R: 0, G: 1, B: 2, A: 3 };
69
+
70
+ const compileGlass = gl => {
71
+ const make = (type, source) => {
72
+ const shader = gl.createShader(type);
73
+ if (!shader) return null;
74
+ gl.shaderSource(shader, source);
75
+ gl.compileShader(shader);
76
+ if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;
77
+ gl.deleteShader(shader);
78
+ return null;
79
+ };
80
+ const vertex = make(gl.VERTEX_SHADER, GLASS_VERTEX);
81
+ const fragment = make(gl.FRAGMENT_SHADER, GLASS_FRAGMENT);
82
+ const program = gl.createProgram();
83
+ if (!vertex || !fragment || !program) return null;
84
+ gl.attachShader(program, vertex);
85
+ gl.attachShader(program, fragment);
86
+ gl.bindAttribLocation(program, 0, 'aPosition');
87
+ gl.linkProgram(program);
88
+ gl.deleteShader(vertex);
89
+ gl.deleteShader(fragment);
90
+ if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;
91
+ gl.deleteProgram(program);
92
+ return null;
93
+ };
94
+
95
+ const resolveBackdrop = backdrop => {
96
+ if (typeof window === 'undefined' || !backdrop) return null;
97
+ const node = typeof backdrop === 'object' && 'current' in backdrop ? backdrop.current : backdrop;
98
+ if (node instanceof HTMLImageElement || node instanceof HTMLVideoElement || node instanceof HTMLCanvasElement) {
99
+ return node;
100
+ }
101
+ return null;
102
+ };
103
+
104
+ const sourceSize = node => {
105
+ if (node instanceof HTMLImageElement) return [node.naturalWidth, node.naturalHeight];
106
+ if (node instanceof HTMLVideoElement) return [node.videoWidth, node.videoHeight];
107
+ return [node.width, node.height];
108
+ };
109
+
110
+ const placeAxis = (value, free) => {
111
+ if (!value) return free / 2;
112
+ if (value.endsWith('%')) return (parseFloat(value) / 100) * free;
113
+ const px = parseFloat(value);
114
+ return Number.isFinite(px) ? px : free / 2;
115
+ };
116
+
117
+ const contentBox = node => {
118
+ const rect = node.getBoundingClientRect();
119
+ const [naturalWidth, naturalHeight] = sourceSize(node);
120
+ const style = getComputedStyle(node);
121
+ const fit = style.objectFit || 'fill';
122
+ if (!naturalWidth || !naturalHeight || fit === 'fill') {
123
+ return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
124
+ }
125
+ const contain = Math.min(rect.width / naturalWidth, rect.height / naturalHeight);
126
+ const cover = Math.max(rect.width / naturalWidth, rect.height / naturalHeight);
127
+ const scale = fit === 'cover' ? cover : fit === 'contain' ? contain : fit === 'none' ? 1 : Math.min(1, contain);
128
+ const width = naturalWidth * scale;
129
+ const height = naturalHeight * scale;
130
+ const [px, py] = (style.objectPosition || '50% 50%').split(' ');
131
+ return {
132
+ x: rect.left + placeAxis(px, rect.width - width),
133
+ y: rect.top + placeAxis(py ?? px, rect.height - height),
134
+ width,
135
+ height
136
+ };
137
+ };
138
+
139
+ const distanceLine = (f, d, v, z, n) => {
140
+ let k = 0;
141
+ v[0] = 0;
142
+ z[0] = -FAR;
143
+ z[1] = FAR;
144
+ for (let q = 1; q < n; q++) {
145
+ let s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
146
+ while (s <= z[k]) {
147
+ k--;
148
+ s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]);
149
+ }
150
+ k++;
151
+ v[k] = q;
152
+ z[k] = s;
153
+ z[k + 1] = FAR;
154
+ }
155
+ k = 0;
156
+ for (let q = 0; q < n; q++) {
157
+ while (z[k + 1] < q) k++;
158
+ d[q] = (q - v[k]) * (q - v[k]) + f[v[k]];
159
+ }
160
+ };
161
+
162
+ const distanceGrid = (grid, w, h) => {
163
+ const n = Math.max(w, h);
164
+ const f = new Float64Array(n);
165
+ const d = new Float64Array(n);
166
+ const v = new Int32Array(n);
167
+ const z = new Float64Array(n + 1);
168
+ for (let x = 0; x < w; x++) {
169
+ for (let y = 0; y < h; y++) f[y] = grid[y * w + x];
170
+ distanceLine(f, d, v, z, h);
171
+ for (let y = 0; y < h; y++) grid[y * w + x] = d[y];
172
+ }
173
+ for (let y = 0; y < h; y++) {
174
+ for (let x = 0; x < w; x++) f[x] = grid[y * w + x];
175
+ distanceLine(f, d, v, z, w);
176
+ for (let x = 0; x < w; x++) grid[y * w + x] = d[x];
177
+ }
178
+ };
179
+
180
+ const blurLine = (src, dst, offset, stride, n, r) => {
181
+ const scale = 1 / (2 * r + 1);
182
+ let sum = src[offset] * (r + 1);
183
+ for (let i = 1; i <= r; i++) sum += src[offset + Math.min(i, n - 1) * stride];
184
+ for (let i = 0; i < n; i++) {
185
+ dst[offset + i * stride] = sum * scale;
186
+ sum += src[offset + Math.min(i + r + 1, n - 1) * stride] - src[offset + Math.max(i - r, 0) * stride];
187
+ }
188
+ };
189
+
190
+ const blurGrid = (grid, w, h, r) => {
191
+ const tmp = new Float32Array(w * h);
192
+ for (let pass = 0; pass < 3; pass++) {
193
+ for (let y = 0; y < h; y++) blurLine(grid, tmp, y * w, 1, w, r);
194
+ for (let x = 0; x < w; x++) blurLine(tmp, grid, x, w, h, r);
195
+ }
196
+ };
197
+
198
+ const settle = x => 1 / (1 + Math.exp(-1.702 * x));
199
+
200
+ const paint = (w, h, fill) => {
201
+ const canvas = document.createElement('canvas');
202
+ canvas.width = w;
203
+ canvas.height = h;
204
+ const context = canvas.getContext('2d');
205
+ const image = context.createImageData(w, h);
206
+ fill(image.data);
207
+ context.putImageData(image, 0, 0);
208
+ return canvas.toDataURL();
209
+ };
210
+
211
+ const shade = (depth, w, h, falloff) =>
212
+ paint(w, h, data => {
213
+ data.fill(255);
214
+ for (let i = 0; i < w * h; i++) data[i * 4 + 3] = 255 * (1 - falloff(depth[i]));
215
+ });
216
+
217
+ const traceShape = (image, width, height, ratio) => {
218
+ // PRTS shapes describe a resizable surface, not a picture to letterbox.
219
+ // Width and height can change between image decode and ResizeObserver delivery.
220
+ const box = { x: 0, y: 0, width, height };
221
+ const w = Math.max(2, Math.round(width * ratio));
222
+ const h = Math.max(2, Math.round(height * ratio));
223
+ const canvas = document.createElement('canvas');
224
+ canvas.width = w;
225
+ canvas.height = h;
226
+ const context = canvas.getContext('2d', { willReadFrequently: true });
227
+ context.drawImage(image, box.x * ratio, box.y * ratio, box.width * ratio, box.height * ratio);
228
+ const pixels = context.getImageData(0, 0, w, h).data;
229
+ const outside = new Float32Array(w * h);
230
+ const inside = new Float32Array(w * h);
231
+ for (let i = 0; i < w * h; i++) {
232
+ const edge = 0.5 - pixels[i * 4 + 3] / 255;
233
+ outside[i] = edge >= 0.5 ? FAR : Math.max(edge, 0) ** 2;
234
+ inside[i] = edge <= -0.5 ? FAR : Math.min(edge, 0) ** 2;
235
+ }
236
+ distanceGrid(outside, w, h);
237
+ distanceGrid(inside, w, h);
238
+ const depth = new Float32Array(w * h);
239
+ for (let i = 0; i < w * h; i++) depth[i] = (Math.sqrt(inside[i]) - Math.sqrt(outside[i])) / ratio;
240
+ return {
241
+ box,
242
+ depth,
243
+ w,
244
+ h,
245
+ ratio,
246
+ line: shade(depth, w, h, t => settle(t - 1)),
247
+ glow: shade(depth, w, h, t => settle((t - 4) / 5))
248
+ };
249
+ };
250
+
251
+ const bendShape = (trace, edge, softness, calm, level) => {
252
+ const { depth, w, h, ratio } = trace;
253
+ const reach = edge + softness * 2;
254
+ const field = new Float32Array(w * h);
255
+ for (let i = 0; i < w * h; i++) field[i] = Math.min(Math.max(depth[i], -6), reach);
256
+ blurGrid(field, w, h, Math.max(1, Math.round(Math.max(1.5, softness * 0.35) * ratio)));
257
+ return paint(w, h, data => {
258
+ for (let y = 0; y < h; y++) {
259
+ for (let x = 0; x < w; x++) {
260
+ const i = y * w + x;
261
+ const dx = field[i + (x < w - 1 ? 1 : 0)] - field[i - (x > 0 ? 1 : 0)];
262
+ const dy = field[i + (y < h - 1 ? w : 0)] - field[i - (y > 0 ? w : 0)];
263
+ const still = calm * settle((depth[i] - edge) / softness);
264
+ const bend = (1 - still) * ratio * 0.25;
265
+ const shift = still * (level - 0.5);
266
+ data[i * 4] = 255 * (0.5 + dx * bend + shift);
267
+ data[i * 4 + 1] = 255 * (0.5 + dy * bend + shift);
268
+ data[i * 4 + 2] = 255 * (0.5 + shift);
269
+ data[i * 4 + 3] = 255;
270
+ }
271
+ }
272
+ });
273
+ };
274
+
275
+ const GlassSurface = ({
276
+ children,
277
+ width = 200,
278
+ height = 80,
279
+ borderRadius = 20,
280
+ borderWidth = 0.07,
281
+ brightness = 50,
282
+ opacity = 0.93,
283
+ blur = 11,
284
+ displace = 0,
285
+ backgroundOpacity = 0,
286
+ saturation = 1,
287
+ distortionScale = -180,
288
+ redOffset = 0,
289
+ greenOffset = 10,
290
+ blueOffset = 20,
291
+ xChannel = 'R',
292
+ yChannel = 'G',
293
+ mixBlendMode = 'difference',
294
+ refraction = true,
295
+ renderer = 'auto',
296
+ backdrop,
297
+ shape,
298
+ className = '',
299
+ style = {}
300
+ }) => {
301
+ const uniqueId = useId().replace(/:/g, '-');
302
+ const filterId = `glass-filter-${uniqueId}`;
303
+ const redGradId = `red-grad-${uniqueId}`;
304
+ const blueGradId = `blue-grad-${uniqueId}`;
305
+ const softId = `soft-${uniqueId}`;
306
+
307
+ const [svgSupported, setSvgSupported] = useState(false);
308
+ const [checked, setChecked] = useState(false);
309
+ const [webglFailed, setWebglFailed] = useState(false);
310
+ const [boxSize, setBoxSize] = useState('');
311
+ const [loaded, setLoaded] = useState({ src: '', image: null });
312
+ const [art, setArt] = useState(null);
313
+
314
+ const containerRef = useRef(null);
315
+ const feImageRef = useRef(null);
316
+ const redChannelRef = useRef(null);
317
+ const greenChannelRef = useRef(null);
318
+ const blueChannelRef = useRef(null);
319
+ const gaussianBlurRef = useRef(null);
320
+ const traceRef = useRef(null);
321
+ const shapeMapRef = useRef('');
322
+ const mapUrlRef = useRef('');
323
+ const canvasRef = useRef(null);
324
+ const settingsRef = useRef(null);
325
+ settingsRef.current = {
326
+ backdrop,
327
+ displace,
328
+ saturation,
329
+ distortionScale,
330
+ redOffset,
331
+ greenOffset,
332
+ blueOffset,
333
+ xChannel,
334
+ yChannel
335
+ };
336
+
337
+ const generateDisplacementMap = () => {
338
+ const rect = containerRef.current?.getBoundingClientRect();
339
+ const actualWidth = rect?.width || 400;
340
+ const actualHeight = rect?.height || 200;
341
+ const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5);
342
+
343
+ const svgContent = `
344
+ <svg width="${actualWidth}" height="${actualHeight}" viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
345
+ <defs>
346
+ <linearGradient id="${redGradId}" x1="100%" y1="0%" x2="0%" y2="0%">
347
+ <stop offset="0%" stop-color="#0000"/>
348
+ <stop offset="100%" stop-color="red"/>
349
+ </linearGradient>
350
+ <linearGradient id="${blueGradId}" x1="0%" y1="0%" x2="0%" y2="100%">
351
+ <stop offset="0%" stop-color="#0000"/>
352
+ <stop offset="100%" stop-color="blue"/>
353
+ </linearGradient>
354
+ <filter id="${softId}" filterUnits="userSpaceOnUse" x="0" y="0" width="${actualWidth}" height="${actualHeight}" color-interpolation-filters="sRGB">
355
+ <feGaussianBlur stdDeviation="${blur}" />
356
+ </filter>
357
+ </defs>
358
+ <rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" fill="black"></rect>
359
+ <rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${borderRadius}" fill="url(#${redGradId})" />
360
+ <rect x="0" y="0" width="${actualWidth}" height="${actualHeight}" rx="${borderRadius}" fill="url(#${blueGradId})" style="mix-blend-mode: ${mixBlendMode}" />
361
+ <rect x="${edgeSize}" y="${edgeSize}" width="${actualWidth - edgeSize * 2}" height="${actualHeight - edgeSize * 2}" rx="${borderRadius}" fill="hsl(0 0% ${brightness}% / ${opacity})" filter="url(#${softId})" />
362
+ </svg>
363
+ `;
364
+
365
+ return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
366
+ };
367
+
368
+ const updateDisplacementMap = () => {
369
+ mapUrlRef.current = shapeMapRef.current || generateDisplacementMap();
370
+ feImageRef.current?.setAttribute('href', mapUrlRef.current);
371
+ };
372
+
373
+ useEffect(() => {
374
+ updateDisplacementMap();
375
+ [
376
+ { ref: redChannelRef, offset: redOffset },
377
+ { ref: greenChannelRef, offset: greenOffset },
378
+ { ref: blueChannelRef, offset: blueOffset }
379
+ ].forEach(({ ref, offset }) => {
380
+ if (ref.current) {
381
+ ref.current.setAttribute('scale', (distortionScale + offset).toString());
382
+ ref.current.setAttribute('xChannelSelector', xChannel);
383
+ ref.current.setAttribute('yChannelSelector', yChannel);
384
+ }
385
+ });
386
+
387
+ gaussianBlurRef.current?.setAttribute('stdDeviation', displace.toString());
388
+ }, [
389
+ boxSize,
390
+ width,
391
+ height,
392
+ borderRadius,
393
+ borderWidth,
394
+ brightness,
395
+ opacity,
396
+ blur,
397
+ displace,
398
+ distortionScale,
399
+ redOffset,
400
+ greenOffset,
401
+ blueOffset,
402
+ xChannel,
403
+ yChannel,
404
+ mixBlendMode
405
+ ]);
406
+
407
+ useEffect(() => {
408
+ if (!containerRef.current) return;
409
+
410
+ const resizeObserver = new ResizeObserver(() => {
411
+ const element = containerRef.current;
412
+ if (element) setBoxSize(`${element.offsetWidth}x${element.offsetHeight}`);
413
+ });
414
+
415
+ resizeObserver.observe(containerRef.current);
416
+
417
+ return () => {
418
+ resizeObserver.disconnect();
419
+ };
420
+ }, []);
421
+
422
+ useEffect(() => {
423
+ if (!shape) return undefined;
424
+ let alive = true;
425
+ const image = new Image();
426
+ image.crossOrigin = 'anonymous';
427
+ image.decoding = 'async';
428
+ image.onload = () => alive && setLoaded({ src: shape, image });
429
+ image.onerror = () => alive && setLoaded({ src: shape, image: null });
430
+ image.src = shape;
431
+ return () => {
432
+ alive = false;
433
+ };
434
+ }, [shape]);
435
+
436
+ const shapeImage = shape && loaded.src === shape ? loaded.image : null;
437
+
438
+ useEffect(() => {
439
+ const element = containerRef.current;
440
+ const w = element?.offsetWidth || 0;
441
+ const h = element?.offsetHeight || 0;
442
+ // PRTS: retain the previous material while a resized shape is decoding.
443
+ if (shape && loaded.src !== shape) return;
444
+ let map = '';
445
+ let trace = null;
446
+ if (shapeImage && w && h) {
447
+ const ratio = Math.min(2, window.devicePixelRatio || 1, Math.sqrt(400000 / (w * h)));
448
+ trace = traceRef.current;
449
+ try {
450
+ if (!trace || trace.image !== shapeImage || trace.width !== w || trace.height !== h || trace.ratio !== ratio) {
451
+ trace = { image: shapeImage, width: w, height: h, ...traceShape(shapeImage, w, h, ratio) };
452
+ traceRef.current = trace;
453
+ }
454
+ map = bendShape(trace, Math.min(w, h) * borderWidth * 0.5, Math.max(blur, 0.5), opacity, brightness / 100);
455
+ } catch {
456
+ trace = null;
457
+ traceRef.current = null;
458
+ }
459
+ }
460
+ shapeMapRef.current = map;
461
+ updateDisplacementMap();
462
+ if (shape && trace) setArt({ src: shape, box: trace.box, width: w, height: h, line: trace.line, glow: trace.glow });
463
+ else if (shape && loaded.src === shape) setArt({ src: shape, failed: true });
464
+ }, [shape, loaded, boxSize, borderWidth, blur, opacity, brightness]);
465
+
466
+ useEffect(() => {
467
+ setSvgSupported(supportsSVGFilters());
468
+ setChecked(true);
469
+ }, []);
470
+
471
+ const webgl =
472
+ checked &&
473
+ refraction &&
474
+ Boolean(backdrop) &&
475
+ !webglFailed &&
476
+ (renderer === 'webgl' || (renderer === 'auto' && !svgSupported));
477
+ const refractive = !webgl && svgSupported && refraction;
478
+ const frosted = !webgl && !refractive;
479
+
480
+ useEffect(() => {
481
+ if (!webgl) return undefined;
482
+ const canvas = canvasRef.current;
483
+ const element = containerRef.current;
484
+ const gl = canvas?.getContext('webgl2', {
485
+ alpha: true,
486
+ premultipliedAlpha: true,
487
+ antialias: false,
488
+ depth: false,
489
+ stencil: false
490
+ });
491
+ const program = gl ? compileGlass(gl) : null;
492
+ if (!canvas || !element || !gl || !program) {
493
+ setWebglFailed(true);
494
+ return undefined;
495
+ }
496
+
497
+ const uniforms = {};
498
+ [
499
+ 'uBackdrop',
500
+ 'uMap',
501
+ 'uCanvas',
502
+ 'uSize',
503
+ 'uOrigin',
504
+ 'uStep',
505
+ 'uScale',
506
+ 'uChannel',
507
+ 'uSpread',
508
+ 'uLod',
509
+ 'uSaturation'
510
+ ].forEach(name => {
511
+ uniforms[name] = gl.getUniformLocation(program, name);
512
+ });
513
+ const vao = gl.createVertexArray();
514
+ const buffer = gl.createBuffer();
515
+ gl.bindVertexArray(vao);
516
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
517
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
518
+ gl.enableVertexAttribArray(0);
519
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
520
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
521
+
522
+ const makeTexture = () => {
523
+ const texture = gl.createTexture();
524
+ gl.bindTexture(gl.TEXTURE_2D, texture);
525
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
526
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
527
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
528
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
529
+ return texture;
530
+ };
531
+ const backdropTexture = makeTexture();
532
+ const mapTexture = makeTexture();
533
+ const mapCanvas = document.createElement('canvas');
534
+ const mapContext = mapCanvas.getContext('2d');
535
+ const picture = { node: null, key: '', width: 0, height: 0, mipmapped: false, ready: false };
536
+ const map = { url: '', ready: false };
537
+ let frame = 0;
538
+ let visible = true;
539
+ let drawn = '';
540
+
541
+ const loadMap = url => {
542
+ map.url = url;
543
+ const image = new Image();
544
+ image.onload = () => {
545
+ if (map.url !== url || !mapContext) return;
546
+ const ratio = Math.min(2, window.devicePixelRatio || 1);
547
+ mapCanvas.width = Math.max(1, Math.round(element.offsetWidth * ratio));
548
+ mapCanvas.height = Math.max(1, Math.round(element.offsetHeight * ratio));
549
+ mapContext.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
550
+ mapContext.drawImage(image, 0, 0, mapCanvas.width, mapCanvas.height);
551
+ gl.bindTexture(gl.TEXTURE_2D, mapTexture);
552
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, mapCanvas);
553
+ map.ready = true;
554
+ drawn = '';
555
+ };
556
+ image.src = url;
557
+ };
558
+
559
+ const upload = node => {
560
+ const still = node instanceof HTMLImageElement;
561
+ gl.bindTexture(gl.TEXTURE_2D, backdropTexture);
562
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, node);
563
+ if (still) gl.generateMipmap(gl.TEXTURE_2D);
564
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, still ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);
565
+ [picture.width, picture.height] = sourceSize(node);
566
+ picture.mipmapped = still;
567
+ picture.ready = true;
568
+ };
569
+
570
+ const tick = () => {
571
+ frame = 0;
572
+ if (!visible || document.hidden) return;
573
+ frame = requestAnimationFrame(tick);
574
+ const s = settingsRef.current;
575
+ if (!s) return;
576
+ if (mapUrlRef.current && mapUrlRef.current !== map.url) loadMap(mapUrlRef.current);
577
+ const node = resolveBackdrop(s.backdrop);
578
+ if (!node) return;
579
+ let key = '';
580
+ if (node instanceof HTMLImageElement) key = node.complete && node.naturalWidth ? node.currentSrc || node.src : '';
581
+ else if (node instanceof HTMLVideoElement)
582
+ key = node.readyState >= 2 ? `${node.currentSrc}|${node.currentTime}` : '';
583
+ else key = `${node.width}x${node.height}|${performance.now()}`;
584
+ if (key && (node !== picture.node || key !== picture.key)) {
585
+ try {
586
+ upload(node);
587
+ } catch {
588
+ setWebglFailed(true);
589
+ return;
590
+ }
591
+ picture.node = node;
592
+ picture.key = key;
593
+ drawn = '';
594
+ }
595
+ if (!picture.ready || !map.ready) return;
596
+
597
+ const layoutWidth = element.offsetWidth;
598
+ const layoutHeight = element.offsetHeight;
599
+ if (!layoutWidth || !layoutHeight) return;
600
+ const box = element.getBoundingClientRect();
601
+ const content = contentBox(node);
602
+ if (!content.width || !content.height) return;
603
+ const ratio = Math.min(2, window.devicePixelRatio || 1);
604
+ const width = Math.max(1, Math.round(layoutWidth * ratio));
605
+ const height = Math.max(1, Math.round(layoutHeight * ratio));
606
+ const signature = [
607
+ box.left,
608
+ box.top,
609
+ box.width,
610
+ box.height,
611
+ content.x,
612
+ content.y,
613
+ content.width,
614
+ content.height,
615
+ width,
616
+ height,
617
+ picture.key,
618
+ map.url,
619
+ s.displace,
620
+ s.saturation,
621
+ s.distortionScale,
622
+ s.redOffset,
623
+ s.greenOffset,
624
+ s.blueOffset,
625
+ s.xChannel,
626
+ s.yChannel
627
+ ].join('|');
628
+ if (signature === drawn) return;
629
+ drawn = signature;
630
+
631
+ if (canvas.width !== width || canvas.height !== height) {
632
+ canvas.width = width;
633
+ canvas.height = height;
634
+ }
635
+ const scaleX = box.width / layoutWidth;
636
+ const scaleY = box.height / layoutHeight;
637
+ const texelsPerPixel = (picture.width / content.width) * (scaleX / ratio);
638
+ const spreadTexels = (picture.width / content.width) * scaleX * Math.max(0, s.displace);
639
+ const lod = picture.mipmapped
640
+ ? Math.max(0, Math.log2(Math.max(texelsPerPixel, spreadTexels * 0.5, 0.000001)))
641
+ : 0;
642
+ gl.viewport(0, 0, width, height);
643
+ gl.useProgram(program);
644
+ gl.bindVertexArray(vao);
645
+ gl.activeTexture(gl.TEXTURE0);
646
+ gl.bindTexture(gl.TEXTURE_2D, backdropTexture);
647
+ gl.uniform1i(uniforms.uBackdrop, 0);
648
+ gl.activeTexture(gl.TEXTURE1);
649
+ gl.bindTexture(gl.TEXTURE_2D, mapTexture);
650
+ gl.uniform1i(uniforms.uMap, 1);
651
+ gl.uniform2f(uniforms.uCanvas, width, height);
652
+ gl.uniform2f(uniforms.uSize, layoutWidth, layoutHeight);
653
+ gl.uniform2f(uniforms.uOrigin, (box.left - content.x) / content.width, (box.top - content.y) / content.height);
654
+ gl.uniform2f(uniforms.uStep, scaleX / content.width, scaleY / content.height);
655
+ gl.uniform3f(
656
+ uniforms.uScale,
657
+ s.distortionScale + s.redOffset,
658
+ s.distortionScale + s.greenOffset,
659
+ s.distortionScale + s.blueOffset
660
+ );
661
+ gl.uniform2i(uniforms.uChannel, CHANNELS[s.xChannel] ?? 0, CHANNELS[s.yChannel] ?? 1);
662
+ gl.uniform1f(uniforms.uSpread, Math.max(0, s.displace));
663
+ gl.uniform1f(uniforms.uLod, lod);
664
+ gl.uniform1f(uniforms.uSaturation, s.saturation);
665
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
666
+ };
667
+
668
+ const wake = () => {
669
+ if (!frame && visible && !document.hidden) frame = requestAnimationFrame(tick);
670
+ };
671
+ const onLost = event => {
672
+ event.preventDefault();
673
+ setWebglFailed(true);
674
+ };
675
+ const intersection = new IntersectionObserver(entries => {
676
+ visible = entries.some(entry => entry.isIntersecting);
677
+ wake();
678
+ });
679
+ intersection.observe(element);
680
+ document.addEventListener('visibilitychange', wake);
681
+ canvas.addEventListener('webglcontextlost', onLost);
682
+ wake();
683
+
684
+ return () => {
685
+ cancelAnimationFrame(frame);
686
+ intersection.disconnect();
687
+ document.removeEventListener('visibilitychange', wake);
688
+ canvas.removeEventListener('webglcontextlost', onLost);
689
+ gl.deleteTexture(backdropTexture);
690
+ gl.deleteTexture(mapTexture);
691
+ gl.deleteBuffer(buffer);
692
+ gl.deleteVertexArray(vao);
693
+ gl.deleteProgram(program);
694
+ };
695
+ }, [webgl]);
696
+
697
+ const supportsSVGFilters = () => {
698
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
699
+ return false;
700
+ }
701
+
702
+ const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
703
+ const isFirefox = /Firefox/.test(navigator.userAgent);
704
+
705
+ if (isWebkit || isFirefox) {
706
+ return false;
707
+ }
708
+
709
+ const div = document.createElement('div');
710
+ div.style.backdropFilter = `url(#${filterId})`;
711
+
712
+ return div.style.backdropFilter !== '';
713
+ };
714
+
715
+ const outline = shape && art && !art.failed ? art : null;
716
+ const maskUrl = outline ? `url("${outline.src.replace(/"/g, '%22').replace(/[\n\r]+/g, ' ')}")` : '';
717
+ const maskPlace = outline ? 'center / 100% 100% no-repeat' : '';
718
+ const shapeStyle = outline
719
+ ? { borderRadius: 0, boxShadow: 'none', WebkitMask: `${maskUrl} ${maskPlace}`, mask: `${maskUrl} ${maskPlace}` }
720
+ : shape && art?.src !== shape
721
+ ? { opacity: 0, transition: 'none' }
722
+ : {};
723
+
724
+ const containerStyle = {
725
+ ...style,
726
+ width: typeof width === 'number' ? `${width}px` : width,
727
+ height: typeof height === 'number' ? `${height}px` : height,
728
+ borderRadius: `${borderRadius}px`,
729
+ '--glass-frost': backgroundOpacity,
730
+ '--glass-saturation': saturation,
731
+ '--filter-id': `url(#${filterId})`,
732
+ ...shapeStyle
733
+ };
734
+
735
+ return (
736
+ <div
737
+ ref={containerRef}
738
+ className={`glass-surface ${webgl ? 'glass-surface--webgl' : refractive ? 'glass-surface--svg' : 'glass-surface--fallback'} ${className}`}
739
+ style={containerStyle}
740
+ >
741
+ <svg className="glass-surface__filter" xmlns="http://www.w3.org/2000/svg">
742
+ <defs>
743
+ <filter id={filterId} colorInterpolationFilters="sRGB" x="0%" y="0%" width="100%" height="100%">
744
+ <feImage ref={feImageRef} x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
745
+
746
+ <feDisplacementMap ref={redChannelRef} in="SourceGraphic" in2="map" id="redchannel" result="dispRed" />
747
+ <feColorMatrix
748
+ in="dispRed"
749
+ type="matrix"
750
+ values="1 0 0 0 0
751
+ 0 0 0 0 0
752
+ 0 0 0 0 0
753
+ 0 0 0 1 0"
754
+ result="red"
755
+ />
756
+
757
+ <feDisplacementMap
758
+ ref={greenChannelRef}
759
+ in="SourceGraphic"
760
+ in2="map"
761
+ id="greenchannel"
762
+ result="dispGreen"
763
+ />
764
+ <feColorMatrix
765
+ in="dispGreen"
766
+ type="matrix"
767
+ values="0 0 0 0 0
768
+ 0 1 0 0 0
769
+ 0 0 0 0 0
770
+ 0 0 0 1 0"
771
+ result="green"
772
+ />
773
+
774
+ <feDisplacementMap ref={blueChannelRef} in="SourceGraphic" in2="map" id="bluechannel" result="dispBlue" />
775
+ <feColorMatrix
776
+ in="dispBlue"
777
+ type="matrix"
778
+ values="0 0 0 0 0
779
+ 0 0 0 0 0
780
+ 0 0 1 0 0
781
+ 0 0 0 1 0"
782
+ result="blue"
783
+ />
784
+
785
+ <feBlend in="red" in2="green" mode="screen" result="rg" />
786
+ <feBlend in="rg" in2="blue" mode="screen" result="output" />
787
+ <feGaussianBlur ref={gaussianBlurRef} in="output" stdDeviation="0.7" />
788
+ </filter>
789
+ </defs>
790
+ </svg>
791
+
792
+ {webgl && <canvas ref={canvasRef} className="glass-surface__canvas" aria-hidden="true" />}
793
+ {webgl && (
794
+ <span className="glass-surface__frame" aria-hidden="true" style={outline ? { boxShadow: 'none' } : undefined} />
795
+ )}
796
+ {frosted && <span className="glass-surface__sheen" aria-hidden="true" />}
797
+ {frosted && !outline && <span className="glass-surface__rim" aria-hidden="true" />}
798
+ {outline && (
799
+ <span
800
+ className="glass-surface__glow"
801
+ aria-hidden="true"
802
+ style={{ WebkitMaskImage: `url(${outline.glow})`, maskImage: `url(${outline.glow})` }}
803
+ />
804
+ )}
805
+ {outline && (
806
+ <span
807
+ className="glass-surface__line"
808
+ aria-hidden="true"
809
+ style={{ WebkitMaskImage: `url(${outline.line})`, maskImage: `url(${outline.line})` }}
810
+ />
811
+ )}
812
+
813
+ <div className="glass-surface__content">{children}</div>
814
+ </div>
815
+ );
816
+ };
817
+
818
+ export default GlassSurface;