liquid-gl 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/liquidGL.js +2110 -0
  4. package/package.json +43 -0
package/liquidGL.js ADDED
@@ -0,0 +1,2110 @@
1
+ /*
2
+ * liquidGL – Ultra-light glassmorphism for the web
3
+ * -----------------------------------------------------------------------------
4
+ *
5
+ * Author: NaughtyDuk© – https://liquidgl.naughtyduk.com
6
+ * Licence: MIT
7
+ */
8
+
9
+ import html2canvas from "html2canvas";
10
+
11
+ const liquidGL = (() => {
12
+ "use strict";
13
+
14
+ /* --------------------------------------------------
15
+ * Utilities
16
+ * ------------------------------------------------*/
17
+ function debounce(fn, wait) {
18
+ let t;
19
+ return (...a) => {
20
+ clearTimeout(t);
21
+ t = setTimeout(() => fn.apply(null, a), wait);
22
+ };
23
+ }
24
+
25
+ /* --------------------------------------------------
26
+ * Helper : Effective z-index (highest stacking context)
27
+ * ------------------------------------------------*/
28
+ function effectiveZ(el) {
29
+ let node = el;
30
+ while (node && node !== document.body) {
31
+ const style = window.getComputedStyle(node);
32
+ if (style.position !== "static" && style.zIndex !== "auto") {
33
+ const z = parseInt(style.zIndex, 10);
34
+ if (!isNaN(z)) return z;
35
+ }
36
+ node = node.parentElement;
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ /* --------------------------------------------------
42
+ * WebGL helpers
43
+ * ------------------------------------------------*/
44
+ function compileShader(gl, type, src) {
45
+ const s = gl.createShader(type);
46
+ gl.shaderSource(s, src.trim());
47
+ gl.compileShader(s);
48
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
49
+ console.error("Shader error", gl.getShaderInfoLog(s));
50
+ gl.deleteShader(s);
51
+ return null;
52
+ }
53
+ return s;
54
+ }
55
+
56
+ function createProgram(gl, vsSource, fsSource) {
57
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
58
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
59
+ if (!vs || !fs) return null;
60
+ const p = gl.createProgram();
61
+ gl.attachShader(p, vs);
62
+ gl.attachShader(p, fs);
63
+ gl.linkProgram(p);
64
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
65
+ console.error("Program link error", gl.getProgramInfoLog(p));
66
+ return null;
67
+ }
68
+ return p;
69
+ }
70
+
71
+ /* --------------------------------------------------
72
+ * Shared renderer (one per page)
73
+ * ------------------------------------------------*/
74
+ class liquidGLRenderer {
75
+ constructor(snapshotSelector, snapshotResolution = 1.0) {
76
+ this.canvas = document.createElement("canvas");
77
+ this.canvas.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;`;
78
+ this.canvas.setAttribute("data-liquid-ignore", "");
79
+ document.body.appendChild(this.canvas);
80
+
81
+ const ctxAttribs = {
82
+ alpha: true,
83
+ premultipliedAlpha: true,
84
+ preserveDrawingBuffer: true,
85
+ };
86
+ this.gl =
87
+ this.canvas.getContext("webgl2", ctxAttribs) ||
88
+ this.canvas.getContext("webgl", ctxAttribs) ||
89
+ this.canvas.getContext("experimental-webgl", ctxAttribs);
90
+ if (!this.gl) throw new Error("liquidGL: WebGL unavailable");
91
+
92
+ this.lenses = [];
93
+ this.texture = null;
94
+ this.textureWidth = 0;
95
+ this.textureHeight = 0;
96
+ this.scaleFactor = 1;
97
+ this.startTime = Date.now();
98
+ this._scrollUpdateCounter = 0;
99
+
100
+ this._initGL();
101
+
102
+ this.snapshotTarget =
103
+ document.querySelector(snapshotSelector) || document.body;
104
+ if (!this.snapshotTarget) this.snapshotTarget = document.body;
105
+
106
+ this._isScrolling = false;
107
+ let lastScrollY = window.scrollY;
108
+ let scrollTimeout;
109
+ const scrollCheck = () => {
110
+ if (window.scrollY !== lastScrollY) {
111
+ this._isScrolling = true;
112
+ lastScrollY = window.scrollY;
113
+ clearTimeout(scrollTimeout);
114
+ scrollTimeout = setTimeout(() => {
115
+ this._isScrolling = false;
116
+ }, 200);
117
+ }
118
+ requestAnimationFrame(scrollCheck);
119
+ };
120
+ requestAnimationFrame(scrollCheck);
121
+
122
+ const onResize = debounce(() => {
123
+ if (this._capturing || this._isScrolling) return;
124
+
125
+ if (window.visualViewport && window.visualViewport.scale !== 1) {
126
+ return;
127
+ }
128
+
129
+ this._dynamicNodes.forEach((node) => {
130
+ const meta = this._dynMeta.get(node.el);
131
+ if (meta) {
132
+ meta.needsRecapture = true;
133
+ meta.prevDrawRect = null;
134
+ meta.lastCapture = null;
135
+ }
136
+ });
137
+
138
+ this._resizeCanvas();
139
+ this.lenses.forEach((l) => l.updateMetrics());
140
+ this.captureSnapshot();
141
+ }, 250);
142
+ window.addEventListener("resize", onResize, { passive: true });
143
+
144
+ if ("ResizeObserver" in window) {
145
+ new ResizeObserver(onResize).observe(this.snapshotTarget);
146
+ }
147
+
148
+ /* --------------------------------------------------
149
+ * Dynamic DOM elements (non-video, e.g. animating text)
150
+ * ------------------------------------------------*/
151
+ this._dynamicNodes = [];
152
+ this._dynMeta = new WeakMap();
153
+ this._lastDynamicUpdate = 0;
154
+
155
+ const styleEl = document.createElement("style");
156
+ styleEl.id = "liquid-gl-dynamic-styles";
157
+ document.head.appendChild(styleEl);
158
+ this._dynamicStyleSheet = styleEl.sheet;
159
+
160
+ this._resizeCanvas();
161
+ this.captureSnapshot();
162
+
163
+ this._pendingReveal = [];
164
+
165
+ /* --------------------------------------------------
166
+ * Dynamic media (video) support
167
+ * ------------------------------------------------*/
168
+ this._videoNodes = Array.from(
169
+ this.snapshotTarget.querySelectorAll("video")
170
+ );
171
+ this._videoNodes = this._videoNodes.filter((v) => !this._isIgnored(v));
172
+ this._tmpCanvas = document.createElement("canvas");
173
+ this._tmpCtx = this._tmpCanvas.getContext("2d");
174
+
175
+ this.canvas.style.opacity = "0";
176
+
177
+ this._snapshotResolution = Math.max(
178
+ 0.1,
179
+ Math.min(3.0, snapshotResolution)
180
+ );
181
+
182
+ this.useExternalTicker = false;
183
+
184
+ /* --------------------------------------------------
185
+ * Inline worker for heavy dynamic nodes
186
+ * ------------------------------------------------*/
187
+ this._workerEnabled =
188
+ typeof OffscreenCanvas !== "undefined" &&
189
+ typeof Worker !== "undefined" &&
190
+ typeof ImageBitmap !== "undefined";
191
+
192
+ if (this._workerEnabled) {
193
+ const workerSrc = `
194
+ /* dynamic-element worker (runs in its own thread) */
195
+ self.onmessage = async (e) => {
196
+ const { id, width, height, snap, dyn } = e.data;
197
+ const off = new OffscreenCanvas(width, height);
198
+ const ctx = off.getContext('2d');
199
+
200
+ ctx.drawImage(snap, 0, 0, width, height);
201
+ ctx.drawImage(dyn, 0, 0, width, height);
202
+
203
+ const bmp = await off.transferToImageBitmap();
204
+ self.postMessage({ id, bmp }, [bmp]);
205
+ };
206
+ `;
207
+ const blob = new Blob([workerSrc], { type: "application/javascript" });
208
+ this._dynWorker = new Worker(URL.createObjectURL(blob), {
209
+ type: "module",
210
+ });
211
+
212
+ this._dynJobs = new Map();
213
+
214
+ this._dynWorker.onmessage = (e) => {
215
+ const { id, bmp } = e.data;
216
+ const meta = this._dynJobs.get(id);
217
+ if (!meta) return;
218
+ this._dynJobs.delete(id);
219
+
220
+ const { x, y, w, h } = meta;
221
+ const gl = this.gl;
222
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
223
+ gl.texSubImage2D(
224
+ gl.TEXTURE_2D,
225
+ 0,
226
+ x,
227
+ y,
228
+ gl.RGBA,
229
+ gl.UNSIGNED_BYTE,
230
+ bmp
231
+ );
232
+ };
233
+ }
234
+ }
235
+
236
+ /* ----------------------------- */
237
+ _initGL() {
238
+ const vsSource = `
239
+ attribute vec2 a_position;
240
+ varying vec2 v_uv;
241
+ void main(){
242
+ v_uv = (a_position + 1.0) * 0.5;
243
+ gl_Position = vec4(a_position, 0.0, 1.0);
244
+ }`;
245
+
246
+ const fsSource = `
247
+ precision mediump float;
248
+ varying vec2 v_uv;
249
+ uniform sampler2D u_tex;
250
+ uniform vec2 u_resolution;
251
+ uniform vec2 u_textureResolution;
252
+ uniform vec4 u_bounds;
253
+ uniform float u_refraction;
254
+ uniform float u_bevelDepth;
255
+ uniform float u_bevelWidth;
256
+ uniform float u_frost;
257
+ uniform float u_radius;
258
+ uniform float u_time;
259
+ uniform bool u_specular;
260
+ uniform float u_revealProgress;
261
+ uniform int u_revealType;
262
+ uniform float u_tiltX;
263
+ uniform float u_tiltY;
264
+ uniform float u_magnify;
265
+
266
+ float udRoundBox( vec2 p, vec2 b, float r ) {
267
+ return length(max(abs(p)-b+r,0.0))-r;
268
+ }
269
+
270
+ float random(vec2 st) {
271
+ return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
272
+ }
273
+
274
+ float edgeFactor(vec2 uv, float radius_px){
275
+ vec2 p_px = (uv - 0.5) * u_resolution;
276
+ vec2 b_px = 0.5 * u_resolution;
277
+ float d = -udRoundBox(p_px, b_px, radius_px);
278
+ float bevel_px = u_bevelWidth * min(u_resolution.x, u_resolution.y);
279
+ return 1.0 - smoothstep(0.0, bevel_px, d);
280
+ }
281
+ void main(){
282
+ vec2 p = v_uv - 0.5;
283
+ p.x *= u_resolution.x / u_resolution.y;
284
+
285
+ float edge = edgeFactor(v_uv, u_radius);
286
+ float min_dimension = min(u_resolution.x, u_resolution.y);
287
+ float offsetAmt = (edge * u_refraction + pow(edge, 10.0) * u_bevelDepth);
288
+ float centreBlend = smoothstep(0.15, 0.45, length(p));
289
+ vec2 offset = normalize(p) * offsetAmt * centreBlend;
290
+
291
+ float tiltRefractionScale = 0.05;
292
+ vec2 tiltOffset = vec2(tan(radians(u_tiltY)), -tan(radians(u_tiltX))) * tiltRefractionScale;
293
+
294
+ vec2 localUV = (v_uv - 0.5) / u_magnify + 0.5;
295
+ vec2 flippedUV = vec2(localUV.x, 1.0 - localUV.y);
296
+ vec2 mapped = u_bounds.xy + flippedUV * u_bounds.zw;
297
+ vec2 refracted = mapped + offset - tiltOffset;
298
+
299
+ float oob = max(max(-refracted.x, refracted.x - 1.0), max(-refracted.y, refracted.y - 1.0));
300
+ float blend = 1.0 - smoothstep(0.0, 0.01, oob);
301
+ vec2 sampleUV = mix(mapped, refracted, blend);
302
+
303
+ vec4 baseCol = texture2D(u_tex, mapped);
304
+
305
+ vec2 texel = 1.0 / u_textureResolution;
306
+ vec4 refrCol;
307
+
308
+ if (u_frost > 0.0) {
309
+ float radius = u_frost * 4.0;
310
+ vec4 sum = vec4(0.0);
311
+ const int SAMPLES = 16;
312
+
313
+ for (int i = 0; i < SAMPLES; i++) {
314
+ float angle = random(v_uv + float(i)) * 6.283185;
315
+ float dist = sqrt(random(v_uv - float(i))) * radius;
316
+ vec2 offset = vec2(cos(angle), sin(angle)) * texel * dist;
317
+ sum += texture2D(u_tex, sampleUV + offset);
318
+ }
319
+ refrCol = sum / float(SAMPLES);
320
+ } else {
321
+ refrCol = texture2D(u_tex, sampleUV);
322
+ refrCol += texture2D(u_tex, sampleUV + vec2( texel.x, 0.0));
323
+ refrCol += texture2D(u_tex, sampleUV + vec2(-texel.x, 0.0));
324
+ refrCol += texture2D(u_tex, sampleUV + vec2(0.0, texel.y));
325
+ refrCol += texture2D(u_tex, sampleUV + vec2(0.0, -texel.y));
326
+ refrCol /= 5.0;
327
+ }
328
+
329
+ if (refrCol.a < 0.1) {
330
+ refrCol = baseCol;
331
+ }
332
+
333
+ float diff = clamp(length(refrCol.rgb - baseCol.rgb) * 4.0, 0.0, 1.0);
334
+
335
+ float antiHalo = (1.0 - centreBlend) * diff;
336
+
337
+ vec4 final = refrCol;
338
+
339
+ vec2 p_px = (v_uv - 0.5) * u_resolution;
340
+ vec2 b_px = 0.5 * u_resolution;
341
+ float dmask = udRoundBox(p_px, b_px, u_radius);
342
+ float inShape = 1.0 - step(0.0, dmask);
343
+
344
+ if (u_specular) {
345
+ vec2 lp1 = vec2(sin(u_time*0.2), cos(u_time*0.3))*0.6 + 0.5;
346
+ vec2 lp2 = vec2(sin(u_time*-0.4+1.5), cos(u_time*0.25-0.5))*0.6 + 0.5;
347
+ float h = 0.0;
348
+ h += smoothstep(0.4,0.0,distance(v_uv, lp1))*0.1;
349
+ h += smoothstep(0.5,0.0,distance(v_uv, lp2))*0.08;
350
+ final.rgb += h;
351
+ }
352
+
353
+ if (u_revealType == 1) {
354
+ final.rgb *= u_revealProgress;
355
+ final.a *= u_revealProgress;
356
+ }
357
+
358
+ final.rgb *= inShape;
359
+ final.a *= inShape;
360
+
361
+ gl_FragColor = final;
362
+ }`;
363
+
364
+ this.program = createProgram(this.gl, vsSource, fsSource);
365
+ const gl = this.gl;
366
+ if (!this.program) throw new Error("liquidGL: Shader failed");
367
+
368
+ const posBuf = gl.createBuffer();
369
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
370
+ gl.bufferData(
371
+ gl.ARRAY_BUFFER,
372
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
373
+ gl.STATIC_DRAW
374
+ );
375
+
376
+ const posLoc = gl.getAttribLocation(this.program, "a_position");
377
+ gl.enableVertexAttribArray(posLoc);
378
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
379
+
380
+ this.u = {
381
+ tex: gl.getUniformLocation(this.program, "u_tex"),
382
+ res: gl.getUniformLocation(this.program, "u_resolution"),
383
+ textureResolution: gl.getUniformLocation(
384
+ this.program,
385
+ "u_textureResolution"
386
+ ),
387
+ bounds: gl.getUniformLocation(this.program, "u_bounds"),
388
+ refraction: gl.getUniformLocation(this.program, "u_refraction"),
389
+ bevelDepth: gl.getUniformLocation(this.program, "u_bevelDepth"),
390
+ bevelWidth: gl.getUniformLocation(this.program, "u_bevelWidth"),
391
+ frost: gl.getUniformLocation(this.program, "u_frost"),
392
+ radius: gl.getUniformLocation(this.program, "u_radius"),
393
+ time: gl.getUniformLocation(this.program, "u_time"),
394
+ specular: gl.getUniformLocation(this.program, "u_specular"),
395
+ revealProgress: gl.getUniformLocation(this.program, "u_revealProgress"),
396
+ revealType: gl.getUniformLocation(this.program, "u_revealType"),
397
+ tiltX: gl.getUniformLocation(this.program, "u_tiltX"),
398
+ tiltY: gl.getUniformLocation(this.program, "u_tiltY"),
399
+ magnify: gl.getUniformLocation(this.program, "u_magnify"),
400
+ };
401
+ }
402
+
403
+ /* ----------------------------- */
404
+ _resizeCanvas() {
405
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
406
+ this.canvas.width = innerWidth * dpr;
407
+ this.canvas.height = innerHeight * dpr;
408
+ this.canvas.style.width = `${innerWidth}px`;
409
+ this.canvas.style.height = `${innerHeight}px`;
410
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
411
+ }
412
+
413
+ /* ----------------------------- */
414
+ async captureSnapshot() {
415
+ if (this._capturing || typeof html2canvas === "undefined") return;
416
+ this._capturing = true;
417
+
418
+ const undos = [];
419
+
420
+ const attemptCapture = async (
421
+ attempt = 1,
422
+ maxAttempts = 3,
423
+ delayMs = 500
424
+ ) => {
425
+ try {
426
+ const fullW = this.snapshotTarget.scrollWidth;
427
+ const fullH = this.snapshotTarget.scrollHeight;
428
+ const maxTex = this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE) || 8192;
429
+ const MAX_MOBILE_DIM = 4096;
430
+ const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.userAgent);
431
+
432
+ let scale = Math.min(
433
+ this._snapshotResolution,
434
+ maxTex / fullW,
435
+ maxTex / fullH
436
+ );
437
+
438
+ if (isMobileSafari) {
439
+ const over = (Math.max(fullW, fullH) * scale) / MAX_MOBILE_DIM;
440
+ if (over > 1) scale = scale / over;
441
+ }
442
+ this.scaleFactor = Math.max(0.1, scale);
443
+
444
+ this.canvas.style.visibility = "hidden";
445
+ undos.push(() => (this.canvas.style.visibility = "visible"));
446
+
447
+ const lensElements = this.lenses
448
+ .flatMap((lens) => [lens.el, lens._shadowEl])
449
+ .filter(Boolean);
450
+
451
+ const ignoreElementsFunc = (element) => {
452
+ if (!element || !element.hasAttribute) return false;
453
+ if (element === this.canvas || lensElements.includes(element)) {
454
+ return true;
455
+ }
456
+ const style = window.getComputedStyle(element);
457
+ if (style.position === "fixed") {
458
+ return true;
459
+ }
460
+ return (
461
+ element.hasAttribute("data-liquid-ignore") ||
462
+ element.closest("[data-liquid-ignore]")
463
+ );
464
+ };
465
+
466
+ const snapCanvas = await html2canvas(this.snapshotTarget, {
467
+ allowTaint: false,
468
+ useCORS: true,
469
+ backgroundColor: null,
470
+ removeContainer: true,
471
+ width: fullW,
472
+ height: fullH,
473
+ scrollX: 0,
474
+ scrollY: 0,
475
+ scale: scale,
476
+ ignoreElements: ignoreElementsFunc,
477
+ });
478
+
479
+ this._uploadTexture(snapCanvas);
480
+ return true;
481
+ } catch (e) {
482
+ console.error("liquidGL snapshot failed on attempt " + attempt, e);
483
+ if (attempt < maxAttempts) {
484
+ console.log(
485
+ `Retrying snapshot capture (${attempt + 1}/${maxAttempts})...`
486
+ );
487
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
488
+ return await attemptCapture(attempt + 1, maxAttempts, delayMs);
489
+ } else {
490
+ console.error("liquidGL: All snapshot attempts failed.", e);
491
+ return false;
492
+ }
493
+ } finally {
494
+ for (let i = undos.length - 1; i >= 0; i--) {
495
+ undos[i]();
496
+ }
497
+ this._capturing = false;
498
+ }
499
+ };
500
+
501
+ return await attemptCapture();
502
+ }
503
+
504
+ /* ----------------------------- */
505
+ _uploadTexture(srcCanvas) {
506
+ if (!srcCanvas) return;
507
+
508
+ if (!(srcCanvas instanceof HTMLCanvasElement)) {
509
+ const tmp = document.createElement("canvas");
510
+ tmp.width = srcCanvas.width || 0;
511
+ tmp.height = srcCanvas.height || 0;
512
+ if (tmp.width === 0 || tmp.height === 0) return;
513
+ try {
514
+ const ctx = tmp.getContext("2d");
515
+ ctx.drawImage(srcCanvas, 0, 0);
516
+ srcCanvas = tmp;
517
+ } catch (e) {
518
+ console.warn(
519
+ "liquidGL: Unable to convert OffscreenCanvas for upload",
520
+ e
521
+ );
522
+ return;
523
+ }
524
+ }
525
+
526
+ if (srcCanvas.width === 0 || srcCanvas.height === 0) return;
527
+ this.staticSnapshotCanvas = srcCanvas;
528
+ const gl = this.gl;
529
+ if (!this.texture) this.texture = gl.createTexture();
530
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
531
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
532
+ gl.texImage2D(
533
+ gl.TEXTURE_2D,
534
+ 0,
535
+ gl.RGBA,
536
+ gl.RGBA,
537
+ gl.UNSIGNED_BYTE,
538
+ srcCanvas
539
+ );
540
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
541
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
542
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
543
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
544
+
545
+ this.textureWidth = srcCanvas.width;
546
+ this.textureHeight = srcCanvas.height;
547
+
548
+ this.render();
549
+
550
+ if (this._pendingReveal.length) {
551
+ this._pendingReveal.forEach((ln) => ln._reveal());
552
+ this._pendingReveal.length = 0;
553
+ }
554
+ }
555
+
556
+ /* ----------------------------- */
557
+ addLens(element, options) {
558
+ const lens = new liquidGLLens(this, element, options);
559
+ this.lenses.push(lens);
560
+
561
+ const maxZ = this._getMaxLensZ();
562
+ if (maxZ > 0) {
563
+ this.canvas.style.zIndex = maxZ - 1;
564
+ }
565
+
566
+ if (!this.texture) {
567
+ this._pendingReveal.push(lens);
568
+ } else {
569
+ lens._reveal();
570
+ }
571
+ return lens;
572
+ }
573
+
574
+ /* ----------------------------- */
575
+ render() {
576
+ const gl = this.gl;
577
+ if (!this.texture) return;
578
+
579
+ if (this._isScrolling) {
580
+ this._scrollUpdateCounter++;
581
+ }
582
+
583
+ gl.clearColor(0, 0, 0, 0);
584
+ gl.clear(gl.COLOR_BUFFER_BIT);
585
+ gl.useProgram(this.program);
586
+ gl.activeTexture(gl.TEXTURE0);
587
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
588
+ gl.uniform1i(this.u.tex, 0);
589
+
590
+ const time = (Date.now() - this.startTime) / 1000;
591
+ gl.uniform1f(this.u.time, time);
592
+
593
+ this._updateDynamicVideos();
594
+
595
+ this._updateDynamicNodes();
596
+
597
+ this.lenses.forEach((lens) => {
598
+ lens.updateMetrics();
599
+ if (lens._mirrorActive && lens._mirrorClipUpdater) {
600
+ lens._mirrorClipUpdater();
601
+ }
602
+ this._renderLens(lens);
603
+ });
604
+
605
+ this.lenses.forEach((ln) => {
606
+ if (ln._mirrorActive && ln._mirrorCtx) {
607
+ const mirror = ln._mirror;
608
+ if (
609
+ mirror.width !== this.canvas.width ||
610
+ mirror.height !== this.canvas.height
611
+ ) {
612
+ mirror.width = this.canvas.width;
613
+ mirror.height = this.canvas.height;
614
+ }
615
+ ln._mirrorCtx.drawImage(this.canvas, 0, 0);
616
+ }
617
+ });
618
+
619
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
620
+ this.lenses.forEach((ln) => {
621
+ if (ln._mirrorActive && ln.rectPx) {
622
+ const { left, top, width, height } = ln.rectPx;
623
+ const expand = 2;
624
+ const x = Math.max(0, Math.round(left * dpr) - expand);
625
+ const y = Math.max(
626
+ 0,
627
+ Math.round(this.canvas.height - (top + height) * dpr) - expand
628
+ );
629
+ const w = Math.min(
630
+ this.canvas.width - x,
631
+ Math.round(width * dpr) + expand * 2
632
+ );
633
+ const h = Math.min(
634
+ this.canvas.height - y,
635
+ Math.round(height * dpr) + expand * 2
636
+ );
637
+ if (w > 0 && h > 0) {
638
+ gl.enable(gl.SCISSOR_TEST);
639
+ gl.scissor(x, y, w, h);
640
+ gl.clearColor(0, 0, 0, 0);
641
+ gl.clear(gl.COLOR_BUFFER_BIT);
642
+ gl.disable(gl.SCISSOR_TEST);
643
+ }
644
+ }
645
+ });
646
+ }
647
+
648
+ /* ----------------------------- */
649
+ _renderLens(lens) {
650
+ const gl = this.gl;
651
+ const rect = lens.rectPx;
652
+ if (!rect) return;
653
+
654
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
655
+
656
+ let overscrollY = 0;
657
+ let overscrollX = 0;
658
+
659
+ if (window.visualViewport) {
660
+ overscrollX = window.visualViewport.offsetLeft;
661
+ overscrollY = window.visualViewport.offsetTop;
662
+ }
663
+
664
+ const x = (rect.left + overscrollX) * dpr;
665
+ const y =
666
+ this.canvas.height - (rect.top + overscrollY + rect.height) * dpr;
667
+ const w = rect.width * dpr;
668
+ const h = rect.height * dpr;
669
+
670
+ gl.viewport(x, y, w, h);
671
+ gl.uniform2f(this.u.res, w, h);
672
+
673
+ const docX = rect.left - this.snapshotTarget.getBoundingClientRect().left;
674
+ const docY = rect.top - this.snapshotTarget.getBoundingClientRect().top;
675
+ const leftUV = (docX * this.scaleFactor) / this.textureWidth;
676
+ const topUV = (docY * this.scaleFactor) / this.textureHeight;
677
+ const wUV = (rect.width * this.scaleFactor) / this.textureWidth;
678
+ const hUV = (rect.height * this.scaleFactor) / this.textureHeight;
679
+ gl.uniform4f(this.u.bounds, leftUV, topUV, wUV, hUV);
680
+
681
+ gl.uniform2f(
682
+ this.u.textureResolution,
683
+ this.textureWidth,
684
+ this.textureHeight
685
+ );
686
+ gl.uniform1f(this.u.refraction, lens.options.refraction);
687
+ gl.uniform1f(this.u.bevelDepth, lens.options.bevelDepth);
688
+ gl.uniform1f(this.u.bevelWidth, lens.options.bevelWidth);
689
+ gl.uniform1f(this.u.frost, lens.options.frost);
690
+ gl.uniform1f(this.u.radius, lens.radiusGl);
691
+ gl.uniform1i(this.u.specular, lens.options.specular ? 1 : 0);
692
+ gl.uniform1f(this.u.revealProgress, lens._revealProgress || 1.0);
693
+ gl.uniform1i(this.u.revealType, lens.revealTypeIndex || 0);
694
+
695
+ const mag = Math.max(
696
+ 0.001,
697
+ Math.min(
698
+ 3.0,
699
+ lens.options.magnify !== undefined ? lens.options.magnify : 1.0
700
+ )
701
+ );
702
+ gl.uniform1f(this.u.magnify, mag);
703
+
704
+ gl.uniform1f(this.u.tiltX, lens.tiltX || 0);
705
+ gl.uniform1f(this.u.tiltY, lens.tiltY || 0);
706
+
707
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
708
+ }
709
+
710
+ /* ----------------------------- */
711
+ _createRoundedRectPath(ctx, w, h, radii) {
712
+ ctx.beginPath();
713
+ ctx.moveTo(radii.tl, 0);
714
+ ctx.lineTo(w - radii.tr, 0);
715
+ ctx.arcTo(w, 0, w, radii.tr, radii.tr);
716
+ ctx.lineTo(w, h - radii.br);
717
+ ctx.arcTo(w, h, w - radii.br, h, radii.br);
718
+ ctx.lineTo(radii.bl, h);
719
+ ctx.arcTo(0, h, 0, h - radii.bl, radii.bl);
720
+ ctx.lineTo(0, radii.tl);
721
+ ctx.arcTo(0, 0, radii.tl, 0, radii.tl);
722
+ ctx.closePath();
723
+ }
724
+
725
+ /* ----------------------------- */
726
+ _updateDynamicVideos() {
727
+ if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
728
+ if (
729
+ !this.texture ||
730
+ !this.staticSnapshotCanvas ||
731
+ !this._videoNodes.length
732
+ )
733
+ return;
734
+ const gl = this.gl;
735
+
736
+ const snapRect = this.snapshotTarget.getBoundingClientRect();
737
+
738
+ const maxLensZ = this._getMaxLensZ();
739
+
740
+ this._videoNodes.forEach((vid) => {
741
+ if (effectiveZ(vid) >= maxLensZ) {
742
+ return;
743
+ }
744
+
745
+ if (this._isIgnored(vid) || vid.readyState < 2) return;
746
+
747
+ const rect = vid.getBoundingClientRect();
748
+ const texX = (rect.left - snapRect.left) * this.scaleFactor;
749
+ const texY = (rect.top - snapRect.top) * this.scaleFactor;
750
+ const texW = rect.width * this.scaleFactor;
751
+ const texH = rect.height * this.scaleFactor;
752
+
753
+ const drawW = Math.round(texW);
754
+ const drawH = Math.round(texH);
755
+
756
+ if (drawW <= 0 || drawH <= 0) return;
757
+
758
+ if (
759
+ this._tmpCanvas.width !== drawW ||
760
+ this._tmpCanvas.height !== drawH
761
+ ) {
762
+ this._tmpCanvas.width = drawW;
763
+ this._tmpCanvas.height = drawH;
764
+ }
765
+
766
+ try {
767
+ this._tmpCtx.save();
768
+ this._tmpCtx.clearRect(0, 0, drawW, drawH);
769
+
770
+ const style = window.getComputedStyle(vid);
771
+ const scaledRadii = {
772
+ tl: parseFloat(style.borderTopLeftRadius) * this.scaleFactor,
773
+ tr: parseFloat(style.borderTopRightRadius) * this.scaleFactor,
774
+ br: parseFloat(style.borderBottomRightRadius) * this.scaleFactor,
775
+ bl: parseFloat(style.borderBottomLeftRadius) * this.scaleFactor,
776
+ };
777
+
778
+ if (Object.values(scaledRadii).some((r) => r > 0)) {
779
+ this._createRoundedRectPath(
780
+ this._tmpCtx,
781
+ drawW,
782
+ drawH,
783
+ scaledRadii
784
+ );
785
+ this._tmpCtx.clip();
786
+ }
787
+
788
+ this._tmpCtx.drawImage(
789
+ this.staticSnapshotCanvas,
790
+ texX,
791
+ texY,
792
+ texW,
793
+ texH,
794
+ 0,
795
+ 0,
796
+ drawW,
797
+ drawH
798
+ );
799
+
800
+ this._tmpCtx.drawImage(vid, 0, 0, drawW, drawH);
801
+ this._tmpCtx.restore();
802
+ } catch (e) {
803
+ console.warn("liquidGL: Error drawing video frame", e);
804
+ return;
805
+ }
806
+
807
+ const drawX = Math.round(texX);
808
+ const drawY = Math.round(texY);
809
+
810
+ if (drawW <= 0 || drawH <= 0) return;
811
+
812
+ const maxW = this.textureWidth;
813
+ const maxH = this.textureHeight;
814
+ let dstX = drawX;
815
+ let dstY = drawY;
816
+ let srcX = 0,
817
+ srcY = 0,
818
+ updW = drawW,
819
+ updH = drawH;
820
+
821
+ if (dstX < 0) {
822
+ srcX = -dstX;
823
+ updW += dstX;
824
+ dstX = 0;
825
+ }
826
+ if (dstY < 0) {
827
+ srcY = -dstY;
828
+ updH += dstY;
829
+ dstY = 0;
830
+ }
831
+
832
+ if (dstX + updW > maxW) {
833
+ updW = maxW - dstX;
834
+ }
835
+ if (dstY + updH > maxH) {
836
+ updH = maxH - dstY;
837
+ }
838
+
839
+ if (updW <= 0 || updH <= 0) return;
840
+
841
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
842
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
843
+ gl.texSubImage2D(
844
+ gl.TEXTURE_2D,
845
+ 0,
846
+ dstX,
847
+ dstY,
848
+ gl.RGBA,
849
+ gl.UNSIGNED_BYTE,
850
+ this._tmpCanvas
851
+ );
852
+ });
853
+ }
854
+
855
+ /* ----------------------------- */
856
+ _updateDynamicNodes() {
857
+ if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
858
+ const gl = this.gl;
859
+ if (!this.texture || !this._dynMeta) return;
860
+ const snapRect = this.snapshotTarget.getBoundingClientRect();
861
+ const maxLensZ = this._getMaxLensZ();
862
+
863
+ const lensRects = this.lenses.map((ln) => ln.rectPx).filter(Boolean);
864
+
865
+ const rectsIntersect = (a, b) =>
866
+ a.left < b.left + b.width &&
867
+ a.left + a.width > b.left &&
868
+ a.top < b.top + b.height &&
869
+ a.top + a.height > b.top;
870
+
871
+ if (!this._compositeCtx) {
872
+ this._compositeCtx = document.createElement("canvas").getContext("2d");
873
+ }
874
+
875
+ const compositeVideos = (compositeCtx, dynamicElRect) => {
876
+ this._videoNodes.forEach((vid) => {
877
+ if (effectiveZ(vid) >= maxLensZ) return;
878
+ const vidRect = vid.getBoundingClientRect();
879
+
880
+ if (
881
+ dynamicElRect.left < vidRect.right &&
882
+ dynamicElRect.right > vidRect.left &&
883
+ dynamicElRect.top < vidRect.bottom &&
884
+ dynamicElRect.bottom > vidRect.top
885
+ ) {
886
+ const xInComposite =
887
+ (vidRect.left - dynamicElRect.left) * this.scaleFactor;
888
+ const yInComposite =
889
+ (vidRect.top - dynamicElRect.top) * this.scaleFactor;
890
+ const wInComposite = vidRect.width * this.scaleFactor;
891
+ const hInComposite = vidRect.height * this.scaleFactor;
892
+ compositeCtx.drawImage(
893
+ vid,
894
+ xInComposite,
895
+ yInComposite,
896
+ wInComposite,
897
+ hInComposite
898
+ );
899
+ }
900
+ });
901
+ };
902
+
903
+ this._dynamicNodes.forEach((node) => {
904
+ const el = node.el;
905
+ const meta = this._dynMeta.get(el);
906
+ if (!meta) return;
907
+
908
+ if (meta.needsRecapture && !meta._capturing && !this._isScrolling) {
909
+ meta._capturing = true;
910
+
911
+ html2canvas(el, {
912
+ backgroundColor: null,
913
+ scale: this.scaleFactor,
914
+ useCORS: true,
915
+ removeContainer: true,
916
+ logging: false,
917
+ ignoreElements: (n) =>
918
+ n.tagName === "CANVAS" || n.hasAttribute("data-liquid-ignore"),
919
+ })
920
+ .then((cv) => {
921
+ if (cv.width > 0 && cv.height > 0) {
922
+ meta.lastCapture = cv;
923
+ meta.needsRecapture = false;
924
+ }
925
+ })
926
+ .catch((e) => {
927
+ console.error("liquidGL: Dynamic element capture failed.", e);
928
+ })
929
+ .finally(() => {
930
+ meta._capturing = false;
931
+ });
932
+ }
933
+
934
+ if (meta.lastCapture) {
935
+ if (meta.prevDrawRect && !(this._workerEnabled && meta._heavyAnim)) {
936
+ const { x, y, w, h } = meta.prevDrawRect;
937
+ if (w > 0 && h > 0) {
938
+ const eraseCanvas = this._compositeCtx.canvas;
939
+ if (eraseCanvas.width !== w || eraseCanvas.height !== h) {
940
+ eraseCanvas.width = w;
941
+ eraseCanvas.height = h;
942
+ }
943
+ this._compositeCtx.drawImage(
944
+ this.staticSnapshotCanvas,
945
+ x,
946
+ y,
947
+ w,
948
+ h,
949
+ 0,
950
+ 0,
951
+ w,
952
+ h
953
+ );
954
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
955
+ gl.texSubImage2D(
956
+ gl.TEXTURE_2D,
957
+ 0,
958
+ x,
959
+ y,
960
+ gl.RGBA,
961
+ gl.UNSIGNED_BYTE,
962
+ eraseCanvas
963
+ );
964
+ }
965
+ }
966
+
967
+ const rect = el.getBoundingClientRect();
968
+ if (
969
+ effectiveZ(el) >= maxLensZ ||
970
+ !document.contains(el) ||
971
+ rect.width === 0 ||
972
+ rect.height === 0
973
+ ) {
974
+ meta.prevDrawRect = null;
975
+ return;
976
+ }
977
+
978
+ if (!lensRects.some((lr) => rectsIntersect(rect, lr))) {
979
+ meta.prevDrawRect = null;
980
+ return;
981
+ }
982
+
983
+ const texX = (rect.left - snapRect.left) * this.scaleFactor;
984
+ const texY = (rect.top - snapRect.top) * this.scaleFactor;
985
+ const drawW = Math.round(rect.width * this.scaleFactor);
986
+ const drawH = Math.round(rect.height * this.scaleFactor);
987
+ const drawX = Math.round(texX);
988
+ const drawY = Math.round(texY);
989
+
990
+ if (drawW <= 0 || drawH <= 0) return;
991
+
992
+ const maxW = this.textureWidth;
993
+ const maxH = this.textureHeight;
994
+ let dstX = drawX;
995
+ let dstY = drawY;
996
+ let srcX = 0,
997
+ srcY = 0,
998
+ updW = drawW,
999
+ updH = drawH;
1000
+
1001
+ if (dstX < 0) {
1002
+ srcX = -dstX;
1003
+ updW += dstX;
1004
+ dstX = 0;
1005
+ }
1006
+ if (dstY < 0) {
1007
+ srcY = -dstY;
1008
+ updH += dstY;
1009
+ dstY = 0;
1010
+ }
1011
+
1012
+ if (dstX + updW > maxW) {
1013
+ updW = maxW - dstX;
1014
+ }
1015
+ if (dstY + updH > maxH) {
1016
+ updH = maxH - dstY;
1017
+ }
1018
+
1019
+ if (updW <= 0 || updH <= 0) return;
1020
+
1021
+ const compositeCanvas = this._compositeCtx.canvas;
1022
+ if (
1023
+ compositeCanvas.width !== drawW ||
1024
+ compositeCanvas.height !== drawH
1025
+ ) {
1026
+ compositeCanvas.width = drawW;
1027
+ compositeCanvas.height = drawH;
1028
+ }
1029
+ this._compositeCtx.clearRect(0, 0, drawW, drawH);
1030
+
1031
+ this._compositeCtx.drawImage(
1032
+ this.staticSnapshotCanvas,
1033
+ texX,
1034
+ texY,
1035
+ rect.width * this.scaleFactor,
1036
+ rect.height * this.scaleFactor,
1037
+ 0,
1038
+ 0,
1039
+ drawW,
1040
+ drawH
1041
+ );
1042
+ compositeVideos(this._compositeCtx, rect);
1043
+
1044
+ const style = window.getComputedStyle(el);
1045
+ this._compositeCtx.save();
1046
+ this._compositeCtx.translate(drawW / 2, drawH / 2);
1047
+ if (style.transform !== "none") {
1048
+ this._compositeCtx.transform(
1049
+ ...this._parseTransform(style.transform)
1050
+ );
1051
+ }
1052
+ this._compositeCtx.translate(-drawW / 2, -drawH / 2);
1053
+ this._compositeCtx.globalAlpha = parseFloat(style.opacity) || 1.0;
1054
+ this._compositeCtx.drawImage(meta.lastCapture, 0, 0, drawW, drawH);
1055
+ this._compositeCtx.restore();
1056
+
1057
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1058
+ gl.texSubImage2D(
1059
+ gl.TEXTURE_2D,
1060
+ 0,
1061
+ dstX,
1062
+ dstY,
1063
+ gl.RGBA,
1064
+ gl.UNSIGNED_BYTE,
1065
+ compositeCanvas
1066
+ );
1067
+
1068
+ if (this._workerEnabled && meta._heavyAnim) {
1069
+ const jobId = `${Date.now()}_${Math.random()}`;
1070
+ this._dynJobs.set(jobId, {
1071
+ x: dstX,
1072
+ y: dstY,
1073
+ w: updW,
1074
+ h: updH,
1075
+ });
1076
+
1077
+ Promise.all([
1078
+ createImageBitmap(
1079
+ this.staticSnapshotCanvas,
1080
+ dstX,
1081
+ dstY,
1082
+ updW,
1083
+ updH
1084
+ ),
1085
+ createImageBitmap(meta.lastCapture),
1086
+ ]).then(([snapBmp, dynBmp]) => {
1087
+ this._dynWorker.postMessage(
1088
+ {
1089
+ id: jobId,
1090
+ width: updW,
1091
+ height: updH,
1092
+ snap: snapBmp,
1093
+ dyn: dynBmp,
1094
+ },
1095
+ [snapBmp, dynBmp]
1096
+ );
1097
+ });
1098
+ meta.prevDrawRect = { x: dstX, y: dstY, w: updW, h: updH };
1099
+ return;
1100
+ }
1101
+
1102
+ meta.prevDrawRect = { x: dstX, y: dstY, w: updW, h: updH };
1103
+ }
1104
+ });
1105
+ }
1106
+
1107
+ _parseTransform(transform) {
1108
+ if (transform === "none") return [1, 0, 0, 1, 0, 0];
1109
+ const matrixMatch = transform.match(/matrix\((.+)\)/);
1110
+ if (matrixMatch) {
1111
+ const values = matrixMatch[1].split(",").map(parseFloat);
1112
+ return values;
1113
+ }
1114
+ const matrix3dMatch = transform.match(/matrix3d\((.+)\)/);
1115
+ if (matrix3dMatch) {
1116
+ const v = matrix3dMatch[1].split(",").map(parseFloat);
1117
+ return [v[0], v[1], v[4], v[5], v[12], v[13]];
1118
+ }
1119
+ return [1, 0, 0, 1, 0, 0];
1120
+ }
1121
+
1122
+ /* ----------------------------- */
1123
+ _getMaxLensZ() {
1124
+ let maxZ = 0;
1125
+ this.lenses.forEach((ln) => {
1126
+ const z = effectiveZ(ln.el);
1127
+ if (z > maxZ) maxZ = z;
1128
+ });
1129
+ return maxZ;
1130
+ }
1131
+
1132
+ /* ----------------------------- */
1133
+ addDynamicElement(el) {
1134
+ if (!el) return;
1135
+ if (typeof el === "string") {
1136
+ this.snapshotTarget
1137
+ .querySelectorAll(el)
1138
+ .forEach((n) => this.addDynamicElement(n));
1139
+ return;
1140
+ }
1141
+ if (NodeList.prototype.isPrototypeOf(el) || Array.isArray(el)) {
1142
+ Array.from(el).forEach((n) => this.addDynamicElement(n));
1143
+ return;
1144
+ }
1145
+ if (!el.getBoundingClientRect) return;
1146
+ if (el.closest && el.closest("[data-liquid-ignore]")) return;
1147
+ if (this._dynamicNodes.some((n) => n.el === el)) return;
1148
+
1149
+ this._dynamicNodes = this._dynamicNodes.filter((n) => !el.contains(n.el));
1150
+
1151
+ const meta = {
1152
+ _capturing: false,
1153
+ prevDrawRect: null,
1154
+ lastCapture: null,
1155
+ needsRecapture: true,
1156
+ hoverClassName: null,
1157
+ _animating: false,
1158
+ _rafId: null,
1159
+ _lastCaptureTs: 0,
1160
+ _heavyAnim: false,
1161
+ };
1162
+ this._dynMeta.set(el, meta);
1163
+
1164
+ const setDirty = () => {
1165
+ const m = this._dynMeta.get(el);
1166
+ if (m && !m.needsRecapture) {
1167
+ m.needsRecapture = true;
1168
+ requestAnimationFrame(() => this.render());
1169
+ }
1170
+ };
1171
+
1172
+ const findAppliedHoverStyles = (element) => {
1173
+ let cssText = "";
1174
+ for (const sheet of document.styleSheets) {
1175
+ try {
1176
+ for (const rule of sheet.cssRules) {
1177
+ if (!rule.selectorText || !rule.selectorText.includes(":hover")) {
1178
+ continue;
1179
+ }
1180
+ const baseSelector = rule.selectorText.split(":hover")[0];
1181
+ if (element.matches(baseSelector)) {
1182
+ cssText += rule.style.cssText;
1183
+ }
1184
+ }
1185
+ } catch (e) {}
1186
+ }
1187
+ return cssText;
1188
+ };
1189
+
1190
+ const handleLeave = () => {
1191
+ const m = this._dynMeta.get(el);
1192
+ if (!m || !m.hoverClassName) return;
1193
+
1194
+ el.classList.remove(m.hoverClassName);
1195
+ for (let i = this._dynamicStyleSheet.cssRules.length - 1; i >= 0; i--) {
1196
+ const rule = this._dynamicStyleSheet.cssRules[i];
1197
+ if (rule.selectorText === `.${m.hoverClassName}`) {
1198
+ this._dynamicStyleSheet.deleteRule(i);
1199
+ break;
1200
+ }
1201
+ }
1202
+ m.hoverClassName = null;
1203
+ setDirty();
1204
+ };
1205
+
1206
+ el.addEventListener(
1207
+ "mouseenter",
1208
+ () => {
1209
+ const m = this._dynMeta.get(el);
1210
+ if (!m) return;
1211
+ const hoverCss = findAppliedHoverStyles(el);
1212
+ if (hoverCss) {
1213
+ const className = `lqgl-h-${Math.random()
1214
+ .toString(36)
1215
+ .substr(2, 9)}`;
1216
+ const rule = `.${className} { ${hoverCss} }`;
1217
+ try {
1218
+ this._dynamicStyleSheet.insertRule(
1219
+ rule,
1220
+ this._dynamicStyleSheet.cssRules.length
1221
+ );
1222
+ m.hoverClassName = className;
1223
+ el.classList.add(className);
1224
+ } catch (e) {
1225
+ console.error("liquidGL: Failed to insert hover style rule.", e);
1226
+ }
1227
+ }
1228
+ setDirty();
1229
+ },
1230
+ { passive: true }
1231
+ );
1232
+
1233
+ el.addEventListener("mouseleave", handleLeave, { passive: true });
1234
+ el.addEventListener("transitionend", setDirty, { passive: true });
1235
+
1236
+ const startRealtime = () => {
1237
+ const m = this._dynMeta.get(el);
1238
+ if (!m || m._animating) return;
1239
+ m._animating = true;
1240
+
1241
+ m._heavyAnim = false;
1242
+
1243
+ const step = (ts) => {
1244
+ const meta = this._dynMeta.get(el);
1245
+ if (!meta || !meta._animating) return;
1246
+
1247
+ if (
1248
+ meta._heavyAnim &&
1249
+ !meta._capturing &&
1250
+ ts - meta._lastCaptureTs > 33
1251
+ ) {
1252
+ meta._lastCaptureTs = ts;
1253
+ meta.needsRecapture = true;
1254
+ }
1255
+ if (meta._heavyAnim) {
1256
+ meta._rafId = requestAnimationFrame(step);
1257
+ } else {
1258
+ meta._rafId = null;
1259
+ }
1260
+ };
1261
+ m._rafId = requestAnimationFrame(step);
1262
+ };
1263
+
1264
+ const trackProperty = (prop) => {
1265
+ const m = this._dynMeta.get(el);
1266
+ if (!m) return;
1267
+ const low = (prop || "").toLowerCase();
1268
+ if (!(low.includes("transform") || low.includes("opacity"))) {
1269
+ const wasHeavy = m._heavyAnim;
1270
+ m._heavyAnim = true;
1271
+ if (m._animating && !wasHeavy && !m._rafId) {
1272
+ m._animating = false;
1273
+ startRealtime();
1274
+ }
1275
+ }
1276
+ };
1277
+
1278
+ const transitionRunHandler = (e) => {
1279
+ trackProperty(e.propertyName);
1280
+ startRealtime();
1281
+ };
1282
+
1283
+ el.addEventListener("transitionrun", transitionRunHandler, {
1284
+ passive: true,
1285
+ });
1286
+ el.addEventListener("transitionstart", transitionRunHandler, {
1287
+ passive: true,
1288
+ });
1289
+ el.addEventListener(
1290
+ "animationstart",
1291
+ () => {
1292
+ const m = this._dynMeta.get(el);
1293
+ if (m) m._heavyAnim = true;
1294
+ startRealtime();
1295
+ },
1296
+ { passive: true }
1297
+ );
1298
+
1299
+ el.addEventListener(
1300
+ "animationiteration",
1301
+ () => {
1302
+ const m = this._dynMeta.get(el);
1303
+ if (m) {
1304
+ m._heavyAnim = true;
1305
+ if (!m._animating) startRealtime();
1306
+ }
1307
+ },
1308
+ { passive: true }
1309
+ );
1310
+
1311
+ const stopRealtime = () => {
1312
+ const m = this._dynMeta.get(el);
1313
+ if (!m || !m._animating) return;
1314
+ m._animating = false;
1315
+ if (m._rafId) {
1316
+ cancelAnimationFrame(m._rafId);
1317
+ m._rafId = null;
1318
+ }
1319
+ m._heavyAnim = false;
1320
+ setDirty();
1321
+ };
1322
+
1323
+ el.addEventListener("transitionend", stopRealtime, { passive: true });
1324
+ el.addEventListener("transitioncancel", stopRealtime, { passive: true });
1325
+ el.addEventListener("animationend", stopRealtime, { passive: true });
1326
+ el.addEventListener("animationcancel", stopRealtime, { passive: true });
1327
+
1328
+ /* --------------------------------------------------
1329
+ * Removal clean-up
1330
+ * --------------------------------------------------*/
1331
+ if (typeof MutationObserver !== "undefined") {
1332
+ const removalObserver = new MutationObserver(() => {
1333
+ if (!document.contains(el)) {
1334
+ handleLeave();
1335
+ removalObserver.disconnect();
1336
+ this._dynamicNodes = this._dynamicNodes.filter((n) => n.el !== el);
1337
+ this._dynMeta.delete(el);
1338
+ }
1339
+ });
1340
+ removalObserver.observe(document.body, {
1341
+ childList: true,
1342
+ subtree: true,
1343
+ });
1344
+ }
1345
+
1346
+ this._dynamicNodes.push({ el });
1347
+ }
1348
+
1349
+ /* ----------------------------- */
1350
+ _isIgnored(el) {
1351
+ return !!(
1352
+ el &&
1353
+ typeof el.closest === "function" &&
1354
+ el.closest("[data-liquid-ignore]")
1355
+ );
1356
+ }
1357
+ }
1358
+
1359
+ /* --------------------------------------------------
1360
+ * Per-element lens wrapper
1361
+ * ------------------------------------------------*/
1362
+ class liquidGLLens {
1363
+ constructor(renderer, element, options) {
1364
+ this.renderer = renderer;
1365
+ this.el = element;
1366
+ this.options = options;
1367
+ this._initCalled = false;
1368
+ this.rectPx = null;
1369
+ this.radiusGl = 0;
1370
+ this.radiusCss = 0;
1371
+ this.revealTypeIndex = this.options.reveal === "fade" ? 1 : 0;
1372
+ this._revealProgress = this.revealTypeIndex === 0 ? 1 : 0;
1373
+ this.tiltX = 0;
1374
+ this.tiltY = 0;
1375
+
1376
+ this.originalShadow = this.el.style.boxShadow;
1377
+ this.originalOpacity = this.el.style.opacity;
1378
+ this.originalTransition = this.el.style.transition;
1379
+ this.el.style.transition = "none";
1380
+ this.el.style.opacity = 0;
1381
+
1382
+ this.el.style.position =
1383
+ this.el.style.position === "static"
1384
+ ? "relative"
1385
+ : this.el.style.position;
1386
+
1387
+ const bgCol = window.getComputedStyle(this.el).backgroundColor;
1388
+ const rgbaMatch = bgCol.match(/rgba?\(([^)]+)\)/);
1389
+ this._bgColorComponents = null;
1390
+ if (rgbaMatch) {
1391
+ const comps = rgbaMatch[1].split(/[ ,]+/).map(parseFloat);
1392
+ const [r, g, b, a = 1] = comps;
1393
+ this._bgColorComponents = { r, g, b, a };
1394
+ this.el.style.backgroundColor = `rgba(${r}, ${g}, ${b}, 0)`;
1395
+ }
1396
+
1397
+ this.el.style.backdropFilter = "none";
1398
+ this.el.style.webkitBackdropFilter = "none";
1399
+ this.el.style.backgroundImage = "none";
1400
+ this.el.style.background = "transparent";
1401
+
1402
+ this.el.style.pointerEvents = "none";
1403
+
1404
+ this.updateMetrics();
1405
+ this.setShadow(this.options.shadow);
1406
+ if (this.options.tilt) this._bindTiltHandlers();
1407
+
1408
+ if (typeof ResizeObserver !== "undefined" && !this._sizeObs) {
1409
+ this._sizeObs = new ResizeObserver(() => {
1410
+ this.updateMetrics();
1411
+ this.renderer.render();
1412
+ });
1413
+ this._sizeObs.observe(this.el);
1414
+ }
1415
+ }
1416
+
1417
+ /* ----------------------------- */
1418
+ updateMetrics() {
1419
+ const rect =
1420
+ this._mirrorActive && this._baseRect
1421
+ ? this._baseRect
1422
+ : this.el.getBoundingClientRect();
1423
+
1424
+ this.rectPx = {
1425
+ left: rect.left,
1426
+ top: rect.top,
1427
+ width: rect.width,
1428
+ height: rect.height,
1429
+ };
1430
+
1431
+ const style = window.getComputedStyle(this.el);
1432
+ const brRaw = style.borderTopLeftRadius.split(" ")[0];
1433
+ const isPct = brRaw.trim().endsWith("%");
1434
+ let brPx;
1435
+ if (isPct) {
1436
+ const pct = parseFloat(brRaw);
1437
+ brPx = (Math.min(rect.width, rect.height) * pct) / 100;
1438
+ } else {
1439
+ brPx = parseFloat(brRaw);
1440
+ }
1441
+ const maxAllowedCss = Math.min(rect.width, rect.height) * 0.5;
1442
+ this.radiusCss = Math.min(brPx, maxAllowedCss);
1443
+
1444
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
1445
+ this.radiusGl = this.radiusCss * dpr;
1446
+
1447
+ if (this._shadowSyncFn) {
1448
+ this._shadowSyncFn();
1449
+ }
1450
+ }
1451
+
1452
+ /* ----------------------------- */
1453
+ _handleOverscrollCompensation() {
1454
+ let overscrollY = 0;
1455
+ let overscrollX = 0;
1456
+
1457
+ if (window.visualViewport) {
1458
+ overscrollX = -window.visualViewport.offsetLeft;
1459
+ overscrollY = -window.visualViewport.offsetTop;
1460
+ } else {
1461
+ const bodyStyle = window.getComputedStyle(document.body);
1462
+ const htmlStyle = window.getComputedStyle(document.documentElement);
1463
+
1464
+ if (bodyStyle.transform && bodyStyle.transform !== "none") {
1465
+ const matrix = new DOMMatrix(bodyStyle.transform);
1466
+ overscrollX = matrix.m41;
1467
+ overscrollY = matrix.m42;
1468
+ }
1469
+
1470
+ if (
1471
+ overscrollY === 0 &&
1472
+ overscrollX === 0 &&
1473
+ htmlStyle.transform &&
1474
+ htmlStyle.transform !== "none"
1475
+ ) {
1476
+ const matrix = new DOMMatrix(htmlStyle.transform);
1477
+ overscrollX = matrix.m41;
1478
+ overscrollY = matrix.m42;
1479
+ }
1480
+ }
1481
+
1482
+ this._currentOverscrollX = overscrollX;
1483
+ this._currentOverscrollY = overscrollY;
1484
+
1485
+ if (overscrollY !== 0 || overscrollX !== 0) {
1486
+ const compensationTransform = `translate(${-overscrollX}px, ${-overscrollY}px)`;
1487
+
1488
+ let currentTransform = this.el.style.transform;
1489
+ currentTransform = currentTransform
1490
+ .replace(/translate\([^)]*\)\s*/g, "")
1491
+ .trim();
1492
+
1493
+ this.el.style.transform =
1494
+ compensationTransform +
1495
+ (currentTransform ? " " + currentTransform : "");
1496
+
1497
+ if (this._shadowEl) {
1498
+ let shadowTransform = this._shadowEl.style.transform || "";
1499
+ shadowTransform = shadowTransform
1500
+ .replace(/translate\([^)]*\)\s*/g, "")
1501
+ .trim();
1502
+ this._shadowEl.style.transform =
1503
+ compensationTransform +
1504
+ (shadowTransform ? " " + shadowTransform : "");
1505
+ }
1506
+ } else if (!this._tiltInteracting) {
1507
+ this.el.style.transform = this._savedTransform || "";
1508
+ if (this._shadowEl) {
1509
+ this._shadowEl.style.transform = "";
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ /* ----------------------------- */
1515
+ setTilt(enabled) {
1516
+ this.options.tilt = !!enabled;
1517
+ if (this.options.tilt) {
1518
+ this._bindTiltHandlers();
1519
+ } else {
1520
+ this._unbindTiltHandlers();
1521
+ }
1522
+ }
1523
+
1524
+ /* ----------------------------- */
1525
+ setShadow(enabled) {
1526
+ this.options.shadow = !!enabled;
1527
+
1528
+ const SHADOW_VAL =
1529
+ "0 10px 30px rgba(0,0,0,0.1), 0 0 0 0.5px rgba(0,0,0,0.05)";
1530
+
1531
+ const syncShadow = () => {
1532
+ if (!this._shadowEl) return;
1533
+ const r =
1534
+ this._mirrorActive && this._baseRect
1535
+ ? this._baseRect
1536
+ : this.el.getBoundingClientRect();
1537
+ this._shadowEl.style.left = `${r.left}px`;
1538
+ this._shadowEl.style.top = `${r.top}px`;
1539
+ this._shadowEl.style.width = `${r.width}px`;
1540
+ this._shadowEl.style.height = `${r.height}px`;
1541
+ this._shadowEl.style.borderRadius = `${this.radiusCss}px`;
1542
+ };
1543
+
1544
+ if (enabled) {
1545
+ this.el.style.boxShadow = SHADOW_VAL;
1546
+
1547
+ if (!this._shadowEl) {
1548
+ this._shadowEl = document.createElement("div");
1549
+ Object.assign(this._shadowEl.style, {
1550
+ position: "fixed",
1551
+ pointerEvents: "none",
1552
+ zIndex: effectiveZ(this.el) - 2,
1553
+ boxShadow: SHADOW_VAL,
1554
+ willChange: "transform, width, height",
1555
+ opacity: this.revealTypeIndex === 1 ? 0 : 1,
1556
+ });
1557
+ document.body.appendChild(this._shadowEl);
1558
+
1559
+ this._shadowSyncFn = syncShadow;
1560
+ window.addEventListener("resize", this._shadowSyncFn, {
1561
+ passive: true,
1562
+ });
1563
+ }
1564
+ syncShadow();
1565
+ } else {
1566
+ if (this._shadowEl) {
1567
+ window.removeEventListener("resize", this._shadowSyncFn);
1568
+ this._shadowEl.remove();
1569
+ this._shadowEl = null;
1570
+ }
1571
+ this.el.style.boxShadow = this.originalShadow;
1572
+ }
1573
+ }
1574
+
1575
+ /* ----------------------------- */
1576
+ _reveal() {
1577
+ if (this.revealTypeIndex === 0) {
1578
+ this.el.style.opacity = this.originalOpacity || 1;
1579
+ this.renderer.canvas.style.opacity = "1";
1580
+ this._revealProgress = 1;
1581
+ this._TriggerInit();
1582
+ return;
1583
+ }
1584
+
1585
+ if (this.renderer._revealAnimating) return;
1586
+
1587
+ this.renderer._revealAnimating = true;
1588
+
1589
+ const dur = 1000;
1590
+ const start = performance.now();
1591
+
1592
+ const animate = () => {
1593
+ const progress = Math.min(1, (performance.now() - start) / dur);
1594
+
1595
+ this.renderer.lenses.forEach((ln) => {
1596
+ ln._revealProgress = progress;
1597
+ ln.el.style.opacity = (ln.originalOpacity || 1) * progress;
1598
+ if (ln._shadowEl) {
1599
+ ln._shadowEl.style.opacity = progress;
1600
+ }
1601
+ });
1602
+
1603
+ this.renderer.canvas.style.opacity = String(progress);
1604
+
1605
+ this.renderer.render();
1606
+
1607
+ if (progress < 1) {
1608
+ requestAnimationFrame(animate);
1609
+ } else {
1610
+ this.renderer._revealAnimating = false;
1611
+ this.renderer.lenses.forEach((ln) => {
1612
+ ln.el.style.transition = ln.originalTransition || "";
1613
+ ln._TriggerInit();
1614
+ });
1615
+ }
1616
+ };
1617
+
1618
+ requestAnimationFrame(animate);
1619
+ }
1620
+
1621
+ /* ----------------------------- */
1622
+ _bindTiltHandlers() {
1623
+ if (this._tiltHandlersBound) return;
1624
+
1625
+ if (this._savedTransform === undefined) {
1626
+ const currentTransform = this.el.style.transform;
1627
+ if (currentTransform && currentTransform.includes("translate")) {
1628
+ this._savedTransform = currentTransform
1629
+ .replace(/translate\([^)]*\)\s*/g, "")
1630
+ .trim();
1631
+ if (this._savedTransform === "") this._savedTransform = "none";
1632
+ } else {
1633
+ this._savedTransform = currentTransform;
1634
+ }
1635
+ }
1636
+ if (this._savedTransformStyle === undefined) {
1637
+ this._savedTransformStyle = this.el.style.transformStyle;
1638
+ }
1639
+ this.el.style.transformStyle = "preserve-3d";
1640
+
1641
+ const getMaxTilt = () =>
1642
+ Number.isFinite(this.options.tiltFactor) ? this.options.tiltFactor : 5;
1643
+
1644
+ this._applyTilt = (clientX, clientY) => {
1645
+ if (!this._tiltInteracting) {
1646
+ this._tiltInteracting = true;
1647
+ this.el.style.transition =
1648
+ "transform 0.12s cubic-bezier(0.33,1,0.68,1)";
1649
+ this._createMirrorCanvas();
1650
+ if (this._mirror) {
1651
+ this._mirror.style.transition =
1652
+ "transform 0.12s cubic-bezier(0.33,1,0.68,1)";
1653
+ }
1654
+ if (this._shadowEl) {
1655
+ this._shadowEl.style.transition =
1656
+ "transform 0.12s cubic-bezier(0.33,1,0.68,1)";
1657
+ }
1658
+ }
1659
+
1660
+ const r = this._baseRect || this.el.getBoundingClientRect();
1661
+ const cx = r.left + r.width / 2;
1662
+ const cy = r.top + r.height / 2;
1663
+
1664
+ this._pivotOrigin = `${cx}px ${cy}px`;
1665
+
1666
+ const pctX = (clientX - cx) / (r.width / 2);
1667
+ const pctY = (clientY - cy) / (r.height / 2);
1668
+ const maxTilt = getMaxTilt();
1669
+ const rotY = pctX * maxTilt;
1670
+ const rotX = -pctY * maxTilt;
1671
+ const baseTransform =
1672
+ this._savedTransform && this._savedTransform !== "none"
1673
+ ? this._savedTransform + " "
1674
+ : "";
1675
+
1676
+ let overscrollCompensation = "";
1677
+ const bodyStyle = window.getComputedStyle(document.body);
1678
+ if (bodyStyle.transform && bodyStyle.transform !== "none") {
1679
+ const matrix = new DOMMatrix(bodyStyle.transform);
1680
+ const overscrollX = matrix.m41;
1681
+ const overscrollY = matrix.m42;
1682
+ if (overscrollX !== 0 || overscrollY !== 0) {
1683
+ overscrollCompensation = `translate(${-overscrollX}px, ${-overscrollY}px) `;
1684
+ }
1685
+ }
1686
+
1687
+ const transformStr = `${overscrollCompensation}${baseTransform}perspective(800px) rotateX(${rotX}deg) rotateY(${rotY}deg)`;
1688
+
1689
+ this.tiltX = rotX;
1690
+ this.tiltY = rotY;
1691
+
1692
+ this.el.style.transformOrigin = `50% 50%`;
1693
+ this.el.style.transform = transformStr;
1694
+
1695
+ if (this._mirror) {
1696
+ this._mirror.style.transformOrigin = this._pivotOrigin;
1697
+ this._mirror.style.transform = transformStr;
1698
+ }
1699
+
1700
+ if (this._shadowEl) {
1701
+ this._shadowEl.style.transformOrigin = `50% 50%`;
1702
+ this._shadowEl.style.transform = transformStr;
1703
+ }
1704
+
1705
+ this.renderer.render();
1706
+ };
1707
+
1708
+ this._smoothReset = () => {
1709
+ this.el.style.transition = "transform 0.4s cubic-bezier(0.33,1,0.68,1)";
1710
+ this.el.style.transformOrigin = `50% 50%`;
1711
+ const baseRest =
1712
+ this._savedTransform && this._savedTransform !== "none"
1713
+ ? this._savedTransform + " "
1714
+ : "";
1715
+
1716
+ let overscrollCompensation = "";
1717
+ const bodyStyle = window.getComputedStyle(document.body);
1718
+ if (bodyStyle.transform && bodyStyle.transform !== "none") {
1719
+ const matrix = new DOMMatrix(bodyStyle.transform);
1720
+ const overscrollX = matrix.m41;
1721
+ const overscrollY = matrix.m42;
1722
+ if (overscrollX !== 0 || overscrollY !== 0) {
1723
+ overscrollCompensation = `translate(${-overscrollX}px, ${-overscrollY}px) `;
1724
+ }
1725
+ }
1726
+
1727
+ this.el.style.transform = `${overscrollCompensation}${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1728
+
1729
+ this.tiltX = 0;
1730
+ this.tiltY = 0;
1731
+ this.renderer.render();
1732
+
1733
+ if (this._mirror) {
1734
+ this._mirror.style.transition =
1735
+ "transform 0.4s cubic-bezier(0.33, 1, 0.68, 1)";
1736
+ this._mirror.style.transformOrigin = this._pivotOrigin || "50% 50%";
1737
+ this._mirror.style.transform = `${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1738
+ const clean = () => {
1739
+ this._destroyMirrorCanvas();
1740
+ this._resetCleanupTimer = null;
1741
+ };
1742
+ this._mirror.addEventListener("transitionend", clean, {
1743
+ once: true,
1744
+ });
1745
+ this._resetCleanupTimer = setTimeout(clean, 350);
1746
+ }
1747
+
1748
+ if (this._shadowEl) {
1749
+ this._shadowEl.style.transition =
1750
+ "transform 0.4s cubic-bezier(0.33,1,0.68,1)";
1751
+ this._shadowEl.style.transformOrigin = `50% 50%`;
1752
+ this._shadowEl.style.transform = `${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1753
+ }
1754
+ };
1755
+
1756
+ this._onMouseEnter = (e) => {
1757
+ if (this._resetCleanupTimer) {
1758
+ clearTimeout(this._resetCleanupTimer);
1759
+ this._resetCleanupTimer = null;
1760
+ this._destroyMirrorCanvas();
1761
+ this.el.style.transition = "none";
1762
+ this.el.style.transform = this._savedTransform || "";
1763
+ void this.el.offsetHeight;
1764
+ }
1765
+
1766
+ this._tiltInteracting = false;
1767
+ this._createMirrorCanvas();
1768
+
1769
+ const r = this._baseRect || this.el.getBoundingClientRect();
1770
+ const cx = r.left + r.width / 2;
1771
+ const cy = r.top + r.height / 2;
1772
+
1773
+ this._applyTilt(cx, cy);
1774
+
1775
+ if (e && typeof e.clientX === "number") {
1776
+ requestAnimationFrame(() => {
1777
+ this._applyTilt(e.clientX, e.clientY);
1778
+ });
1779
+ }
1780
+
1781
+ document.addEventListener("mousemove", this._boundCheckLeave, {
1782
+ passive: true,
1783
+ });
1784
+ };
1785
+
1786
+ this._onMouseMove = (e) => this._applyTilt(e.clientX, e.clientY);
1787
+
1788
+ this._onTouchStart = (e) => {
1789
+ this._tiltInteracting = false;
1790
+ this._createMirrorCanvas();
1791
+ if (e.touches && e.touches.length === 1) {
1792
+ const t = e.touches[0];
1793
+ this._applyTilt(t.clientX, t.clientY);
1794
+ }
1795
+ };
1796
+ this._onTouchMove = (e) => {
1797
+ if (e.touches && e.touches.length === 1) {
1798
+ const t = e.touches[0];
1799
+ this._applyTilt(t.clientX, t.clientY);
1800
+ }
1801
+ };
1802
+ this._onTouchEnd = () => {
1803
+ this._smoothReset();
1804
+ };
1805
+
1806
+ this.el.addEventListener("mouseenter", this._onMouseEnter.bind(this), {
1807
+ passive: true,
1808
+ });
1809
+ this.el.addEventListener("mousemove", this._onMouseMove.bind(this), {
1810
+ passive: true,
1811
+ });
1812
+ this.el.addEventListener("touchstart", this._onTouchStart.bind(this), {
1813
+ passive: true,
1814
+ });
1815
+ this.el.addEventListener("touchmove", this._onTouchMove.bind(this), {
1816
+ passive: true,
1817
+ });
1818
+ this.el.addEventListener("touchend", this._onTouchEnd.bind(this), {
1819
+ passive: true,
1820
+ });
1821
+
1822
+ /* ----------------------------- */
1823
+ this._tiltActive = false;
1824
+
1825
+ this._docPointerMove = (e) => {
1826
+ const x = e.clientX ?? (e.touches && e.touches[0].clientX);
1827
+ const y = e.clientY ?? (e.touches && e.touches[0].clientY);
1828
+ if (x === undefined || y === undefined) return;
1829
+
1830
+ const r = this.el.getBoundingClientRect();
1831
+ const inside =
1832
+ x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
1833
+
1834
+ if (inside) {
1835
+ if (!this._tiltActive) {
1836
+ this._tiltActive = true;
1837
+ this._onMouseEnter({ clientX: x, clientY: y });
1838
+ } else {
1839
+ this._applyTilt(x, y);
1840
+ }
1841
+ } else if (this._tiltActive) {
1842
+ this._tiltActive = false;
1843
+ this._smoothReset();
1844
+ }
1845
+ };
1846
+
1847
+ document.addEventListener("pointermove", this._docPointerMove, {
1848
+ passive: true,
1849
+ });
1850
+
1851
+ this._tiltHandlersBound = true;
1852
+ }
1853
+
1854
+ _unbindTiltHandlers() {
1855
+ if (!this._tiltHandlersBound) return;
1856
+ this.el.removeEventListener("mouseenter", this._onMouseEnter.bind(this));
1857
+ this.el.removeEventListener("mousemove", this._onMouseMove.bind(this));
1858
+ document.removeEventListener("mousemove", this._boundCheckLeave);
1859
+ this.el.removeEventListener("touchstart", this._onTouchStart.bind(this));
1860
+ this.el.removeEventListener("touchmove", this._onTouchMove.bind(this));
1861
+ this.el.removeEventListener("touchend", this._onTouchEnd.bind(this));
1862
+
1863
+ if (this._docPointerMove) {
1864
+ document.removeEventListener("pointermove", this._docPointerMove);
1865
+ this._docPointerMove = null;
1866
+ }
1867
+ this._tiltHandlersBound = false;
1868
+
1869
+ this.el.style.transform = this._savedTransform || "";
1870
+ this.el.style.transformStyle = this._savedTransformStyle || "";
1871
+
1872
+ this.renderer.render();
1873
+ }
1874
+
1875
+ _createMirrorCanvas() {
1876
+ this._baseRect = this.el.getBoundingClientRect();
1877
+ if (this._mirror) return;
1878
+ this._mirror = document.createElement("canvas");
1879
+ Object.assign(this._mirror.style, {
1880
+ position: "fixed",
1881
+ top: 0,
1882
+ left: 0,
1883
+ width: "100%",
1884
+ height: "100%",
1885
+ pointerEvents: "none",
1886
+ zIndex: effectiveZ(this.el) - 1,
1887
+ willChange: "transform",
1888
+ });
1889
+ this._mirrorCtx = this._mirror.getContext("2d");
1890
+ document.body.appendChild(this._mirror);
1891
+
1892
+ const updateClip = () => {
1893
+ if (this._mirrorActive) {
1894
+ this._baseRect = this._baseRect || this.el.getBoundingClientRect();
1895
+ }
1896
+ const r = this._baseRect || this.el.getBoundingClientRect();
1897
+ const radius = `${this.radiusCss}px`;
1898
+ this._mirror.style.clipPath = `inset(${r.top}px ${
1899
+ innerWidth - r.right
1900
+ }px ${innerHeight - r.bottom}px ${r.left}px round ${radius})`;
1901
+ this._mirror.style.webkitClipPath = this._mirror.style.clipPath;
1902
+ };
1903
+ updateClip();
1904
+ this._mirrorClipUpdater = updateClip;
1905
+ window.addEventListener("resize", updateClip, { passive: true });
1906
+
1907
+ this._mirrorActive = true;
1908
+ }
1909
+
1910
+ _destroyMirrorCanvas() {
1911
+ if (!this._mirror) return;
1912
+ window.removeEventListener("resize", this._mirrorClipUpdater);
1913
+ this._mirror.remove();
1914
+ this._mirror = this._mirrorCtx = null;
1915
+ this._baseRect = null;
1916
+ this._mirrorActive = false;
1917
+ }
1918
+
1919
+ _TriggerInit() {
1920
+ if (this._initCalled) return;
1921
+ this._initCalled = true;
1922
+ if (this.options.on && this.options.on.init) {
1923
+ this.options.on.init(this);
1924
+ }
1925
+ }
1926
+ }
1927
+
1928
+ /* --------------------------------------------------
1929
+ * Public API
1930
+ * ------------------------------------------------*/
1931
+ const liquidGL = function (userOptions = {}) {
1932
+ const defaults = {
1933
+ target: ".liquidGL",
1934
+ snapshot: "body",
1935
+ resolution: 2.0,
1936
+ refraction: 0.01,
1937
+ bevelDepth: 0.08,
1938
+ bevelWidth: 0.15,
1939
+ frost: 0,
1940
+ shadow: true,
1941
+ specular: true,
1942
+ reveal: "fade",
1943
+ tilt: false,
1944
+ tiltFactor: 5,
1945
+ magnify: 1,
1946
+ on: {},
1947
+ };
1948
+ const options = { ...defaults, ...userOptions };
1949
+
1950
+ if (typeof window.__liquidGLNoWebGL__ === "undefined") {
1951
+ const testCanvas = document.createElement("canvas");
1952
+ const testCtx =
1953
+ testCanvas.getContext("webgl2") ||
1954
+ testCanvas.getContext("webgl") ||
1955
+ testCanvas.getContext("experimental-webgl");
1956
+ window.__liquidGLNoWebGL__ = !testCtx;
1957
+ }
1958
+
1959
+ const noWebGL = window.__liquidGLNoWebGL__;
1960
+
1961
+ if (noWebGL) {
1962
+ console.warn(
1963
+ "liquidGL: WebGL not available – falling back to CSS backdrop-filter."
1964
+ );
1965
+ const fallbackNodes = document.querySelectorAll(options.target);
1966
+ fallbackNodes.forEach((node) => {
1967
+ Object.assign(node.style, {
1968
+ background: "rgba(255, 255, 255, 0.07)",
1969
+ backdropFilter: "blur(12px)",
1970
+ webkitBackdropFilter: "blur(12px)",
1971
+ });
1972
+ });
1973
+ return fallbackNodes.length === 1
1974
+ ? fallbackNodes[0]
1975
+ : Array.from(fallbackNodes);
1976
+ }
1977
+
1978
+ let renderer = window.__liquidGLRenderer__;
1979
+ if (!renderer) {
1980
+ renderer = new liquidGLRenderer(options.snapshot, options.resolution);
1981
+ window.__liquidGLRenderer__ = renderer;
1982
+ }
1983
+
1984
+ const nodeList = document.querySelectorAll(options.target);
1985
+ if (!nodeList || nodeList.length === 0) {
1986
+ console.warn(
1987
+ `liquidGL: Target element(s) '${options.target}' not found.`
1988
+ );
1989
+ return;
1990
+ }
1991
+
1992
+ const instances = Array.from(nodeList).map((el) =>
1993
+ renderer.addLens(el, options)
1994
+ );
1995
+
1996
+ if (!renderer._rafId && !renderer.useExternalTicker) {
1997
+ const loop = () => {
1998
+ renderer.render();
1999
+ renderer._rafId = requestAnimationFrame(loop);
2000
+ };
2001
+ renderer._rafId = requestAnimationFrame(loop);
2002
+ }
2003
+
2004
+ return instances.length === 1 ? instances[0] : instances;
2005
+ };
2006
+
2007
+ /* --------------------------------------------------
2008
+ * Public helper: register elements that need live updates
2009
+ * ------------------------------------------------*/
2010
+ liquidGL.registerDynamic = function (elements) {
2011
+ const renderer = window.__liquidGLRenderer__;
2012
+ if (!renderer || !renderer.addDynamicElement) return;
2013
+ renderer.addDynamicElement(elements);
2014
+ if (renderer.captureSnapshot) {
2015
+ renderer.captureSnapshot();
2016
+ }
2017
+ };
2018
+
2019
+ /* --------------------------------------------------
2020
+ * Public helper: Universal smooth scroll / animation sync
2021
+ * ------------------------------------------------*/
2022
+ liquidGL.syncWith = function (config = {}) {
2023
+ const renderer = window.__liquidGLRenderer__;
2024
+ if (!renderer) {
2025
+ console.warn(
2026
+ "liquidGL: Please initialize liquidGL *before* calling syncWith()."
2027
+ );
2028
+ return;
2029
+ }
2030
+
2031
+ const G = window.gsap;
2032
+ const L = window.Lenis;
2033
+ const LS = window.LocomotiveScroll;
2034
+ const ST = G ? G.ScrollTrigger : null;
2035
+
2036
+ let lenis = config.lenis;
2037
+ let loco = config.locomotiveScroll;
2038
+ const useGSAP = config.gsap !== false && G && ST;
2039
+
2040
+ if (config.lenis !== false && L && !lenis) {
2041
+ lenis = new L();
2042
+ }
2043
+
2044
+ if (
2045
+ config.locomotiveScroll !== false &&
2046
+ LS &&
2047
+ !loco &&
2048
+ document.querySelector("[data-scroll-container]")
2049
+ ) {
2050
+ loco = new LS({
2051
+ el: document.querySelector("[data-scroll-container]"),
2052
+ smooth: true,
2053
+ });
2054
+ }
2055
+
2056
+ if (useGSAP && ST) {
2057
+ if (loco) {
2058
+ loco.on("scroll", ST.update);
2059
+ ST.scrollerProxy(loco.el, {
2060
+ scrollTop(value) {
2061
+ return arguments.length
2062
+ ? loco.scrollTo(value, { duration: 0, disableLerp: true })
2063
+ : loco.scroll.instance.scroll.y;
2064
+ },
2065
+ getBoundingClientRect() {
2066
+ return {
2067
+ top: 0,
2068
+ left: 0,
2069
+ width: window.innerWidth,
2070
+ height: window.innerHeight,
2071
+ };
2072
+ },
2073
+ pinType: loco.el.style.transform ? "transform" : "fixed",
2074
+ });
2075
+ ST.addEventListener("refresh", () => loco.update());
2076
+ ST.refresh();
2077
+ } else if (lenis) {
2078
+ lenis.on("scroll", ST.update);
2079
+ }
2080
+ }
2081
+
2082
+ if (renderer._rafId) {
2083
+ cancelAnimationFrame(renderer._rafId);
2084
+ renderer._rafId = null;
2085
+ }
2086
+ renderer.useExternalTicker = true;
2087
+
2088
+ if (useGSAP) {
2089
+ G.ticker.add((time) => {
2090
+ if (lenis) lenis.raf(time * 1000);
2091
+ renderer.render();
2092
+ });
2093
+ G.ticker.lagSmoothing(0);
2094
+ } else {
2095
+ const loop = (time) => {
2096
+ if (lenis) lenis.raf(time);
2097
+ if (loco) loco.update();
2098
+ renderer.render();
2099
+ renderer._rafId = requestAnimationFrame(loop);
2100
+ };
2101
+ renderer._rafId = requestAnimationFrame(loop);
2102
+ }
2103
+
2104
+ return { lenis, locomotiveScroll: loco };
2105
+ };
2106
+
2107
+ return liquidGL;
2108
+ })();
2109
+
2110
+ export default liquidGL;