liquid-gl 1.0.6 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +135 -41
  2. package/liquidGL.js +2304 -183
  3. package/package.json +3 -13
package/liquidGL.js CHANGED
@@ -4,13 +4,14 @@
4
4
  *
5
5
  * Author: NaughtyDuk© – https://liquidgl.naughtyduk.com
6
6
  * Licence: MIT
7
+ * Version: v2.0.1
7
8
  */
8
9
 
9
- import html2canvas from "html2canvas";
10
-
11
10
  const liquidGL = (() => {
12
11
  "use strict";
13
12
 
13
+ const RECAPTURE_INTERVAL_MS = 250;
14
+
14
15
  /* --------------------------------------------------
15
16
  * Utilities
16
17
  * ------------------------------------------------*/
@@ -65,14 +66,1749 @@ const liquidGL = (() => {
65
66
  console.error("Program link error", gl.getProgramInfoLog(p));
66
67
  return null;
67
68
  }
68
- return p;
69
- }
69
+ return p;
70
+ }
71
+
72
+ /* --------------------------------------------------
73
+ * NaughtyDOM
74
+ * ------------------------------------------------*/
75
+ const NaughtyDOM = (() => {
76
+ const IDENT = [1, 0, 0, 1, 0, 0];
77
+ const CLIP_OVERFLOW = /^(hidden|clip|scroll|auto)$/;
78
+
79
+ function mul(m, n) {
80
+ return [
81
+ m[0] * n[0] + m[2] * n[1],
82
+ m[1] * n[0] + m[3] * n[1],
83
+ m[0] * n[2] + m[2] * n[3],
84
+ m[1] * n[2] + m[3] * n[3],
85
+ m[0] * n[4] + m[2] * n[5] + m[4],
86
+ m[1] * n[4] + m[3] * n[5] + m[5],
87
+ ];
88
+ }
89
+
90
+ function apply(m, x, y) {
91
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
92
+ }
93
+
94
+ function invertLinear(m) {
95
+ const det = m[0] * m[3] - m[1] * m[2];
96
+ if (!det || !isFinite(det)) return null;
97
+ return [m[3] / det, -m[1] / det, -m[2] / det, m[0] / det, 0, 0];
98
+ }
99
+
100
+ function parseMatrix(str) {
101
+ if (!str || str === "none") return null;
102
+ const open = str.indexOf("(");
103
+ if (open === -1) return null;
104
+ const kind = str.slice(0, open);
105
+ const v = str
106
+ .slice(open + 1, str.lastIndexOf(")"))
107
+ .split(",")
108
+ .map((n) => parseFloat(n));
109
+ if (kind === "matrix" && v.length >= 6) {
110
+ return [v[0], v[1], v[2], v[3], v[4], v[5]];
111
+ }
112
+ if (kind === "matrix3d" && v.length >= 16) {
113
+ return [v[0], v[1], v[4], v[5], v[12], v[13]];
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function parseOrigin(str) {
119
+ if (!str) return [0, 0];
120
+ const p = str.split(" ");
121
+ return [parseFloat(p[0]) || 0, parseFloat(p[1]) || 0];
122
+ }
123
+
124
+ function isTransparent(color) {
125
+ if (!color || color === "transparent" || color === "none") return true;
126
+ const alpha = color.match(
127
+ /^(?:rgba|hsla|hwb|lab|lch|oklab|oklch|color)\([^)]*[,/]\s*([0-9.]+)%?\s*\)$/,
128
+ );
129
+ return alpha ? parseFloat(alpha[1]) === 0 : false;
130
+ }
131
+
132
+ function splitTopLevel(value) {
133
+ const out = [];
134
+ let depth = 0;
135
+ let start = 0;
136
+ for (let i = 0; i < value.length; i++) {
137
+ const c = value[i];
138
+ if (c === "(") depth++;
139
+ else if (c === ")") depth--;
140
+ else if (c === "," && depth === 0) {
141
+ out.push(value.slice(start, i).trim());
142
+ start = i + 1;
143
+ }
144
+ }
145
+ const tail = value.slice(start).trim();
146
+ if (tail) out.push(tail);
147
+ return out;
148
+ }
149
+
150
+ function resolveLength(token, basis) {
151
+ if (!token) return 0;
152
+ if (token.indexOf("%") !== -1) {
153
+ return (parseFloat(token) / 100) * basis;
154
+ }
155
+ const n = parseFloat(token);
156
+ return isNaN(n) ? 0 : n;
157
+ }
158
+
159
+ function cornerRadius(value, w, h) {
160
+ if (!value) return [0, 0];
161
+ const parts = value.split(" ").filter(Boolean);
162
+ const rx = resolveLength(parts[0], w);
163
+ const ry = parts.length > 1 ? resolveLength(parts[1], h) : rx;
164
+ return [Math.max(0, rx), Math.max(0, ry)];
165
+ }
166
+
167
+ function parseRadii(style, w, h) {
168
+ const r = [
169
+ cornerRadius(style.borderTopLeftRadius, w, h),
170
+ cornerRadius(style.borderTopRightRadius, w, h),
171
+ cornerRadius(style.borderBottomRightRadius, w, h),
172
+ cornerRadius(style.borderBottomLeftRadius, w, h),
173
+ ];
174
+ if (!r.some((c) => c[0] > 0 || c[1] > 0)) return null;
175
+ let f = 1;
176
+ const ratio = (sum, len) => (sum > len && sum > 0 ? len / sum : 1);
177
+ f = Math.min(
178
+ f,
179
+ ratio(r[0][0] + r[1][0], w),
180
+ ratio(r[3][0] + r[2][0], w),
181
+ ratio(r[0][1] + r[3][1], h),
182
+ ratio(r[1][1] + r[2][1], h),
183
+ );
184
+ if (f < 1) {
185
+ for (let i = 0; i < 4; i++) {
186
+ r[i][0] *= f;
187
+ r[i][1] *= f;
188
+ }
189
+ }
190
+ return r;
191
+ }
192
+
193
+ function insetRadii(radii, top, right, bottom, left) {
194
+ if (!radii) return null;
195
+ return [
196
+ [Math.max(0, radii[0][0] - left), Math.max(0, radii[0][1] - top)],
197
+ [Math.max(0, radii[1][0] - right), Math.max(0, radii[1][1] - top)],
198
+ [Math.max(0, radii[2][0] - right), Math.max(0, radii[2][1] - bottom)],
199
+ [Math.max(0, radii[3][0] - left), Math.max(0, radii[3][1] - bottom)],
200
+ ];
201
+ }
202
+
203
+ function invert(m) {
204
+ const det = m[0] * m[3] - m[1] * m[2];
205
+ if (!det || !isFinite(det)) return null;
206
+ return [
207
+ m[3] / det,
208
+ -m[1] / det,
209
+ -m[2] / det,
210
+ m[0] / det,
211
+ (m[2] * m[5] - m[3] * m[4]) / det,
212
+ (m[1] * m[4] - m[0] * m[5]) / det,
213
+ ];
214
+ }
215
+
216
+ const imageCache = new Map();
217
+ const svgCache = new WeakMap();
218
+ let pending = [];
219
+
220
+ function sameOrigin(src) {
221
+ if (/^data:/.test(src) || /^blob:/.test(src)) return true;
222
+ try {
223
+ return new URL(src, location.href).origin === location.origin;
224
+ } catch (e) {
225
+ return false;
226
+ }
227
+ }
228
+
229
+ function loadImage(src) {
230
+ if (!src) return null;
231
+ if (imageCache.has(src)) return imageCache.get(src);
232
+ const img = new Image();
233
+ const entry = { img, ready: false, failed: false };
234
+ imageCache.set(src, entry);
235
+ if (!sameOrigin(src)) img.crossOrigin = "anonymous";
236
+ const done = new Promise((resolve) => {
237
+ img.onload = () => {
238
+ entry.ready = true;
239
+ resolve();
240
+ };
241
+ img.onerror = () => {
242
+ entry.failed = true;
243
+ resolve();
244
+ };
245
+ });
246
+ img.src = src;
247
+ if (img.complete && img.naturalWidth) {
248
+ entry.ready = true;
249
+ } else {
250
+ pending.push(done);
251
+ }
252
+ return entry;
253
+ }
254
+
255
+ const SVG_PAINT = [
256
+ "fill",
257
+ "fill-opacity",
258
+ "fill-rule",
259
+ "stroke",
260
+ "stroke-width",
261
+ "stroke-opacity",
262
+ "stroke-linecap",
263
+ "stroke-linejoin",
264
+ "stroke-dasharray",
265
+ "stroke-dashoffset",
266
+ "opacity",
267
+ "color",
268
+ "stop-color",
269
+ "stop-opacity",
270
+ "font-family",
271
+ "font-size",
272
+ "font-weight",
273
+ "font-style",
274
+ "text-anchor",
275
+ "letter-spacing",
276
+ "display",
277
+ "visibility",
278
+ "transform",
279
+ "transform-origin",
280
+ "mix-blend-mode",
281
+ "clip-path",
282
+ "mask",
283
+ "filter",
284
+ "marker-start",
285
+ "marker-mid",
286
+ "marker-end",
287
+ ];
288
+
289
+ function inlineSvgStyles(source, clone) {
290
+ const computed = getComputedStyle(source);
291
+ let css = "";
292
+ for (let i = 0; i < SVG_PAINT.length; i++) {
293
+ const prop = SVG_PAINT[i];
294
+ const value = computed.getPropertyValue(prop);
295
+ if (value) css += `${prop}:${value};`;
296
+ }
297
+ if (css) clone.setAttribute("style", css);
298
+ const sk = source.children;
299
+ const ck = clone.children;
300
+ for (let i = 0; i < sk.length && i < ck.length; i++) {
301
+ inlineSvgStyles(sk[i], ck[i]);
302
+ }
303
+ }
304
+
305
+ function svgToImage(el) {
306
+ const r = el.getBoundingClientRect();
307
+ const signature = `${el.outerHTML.length}|${el.childElementCount}|${Math.round(r.width)}x${Math.round(r.height)}|${el.className}`;
308
+ const cached = svgCache.get(el);
309
+ if (cached && cached.signature === signature) return cached.entry;
310
+
311
+ const clone = el.cloneNode(true);
312
+ inlineSvgStyles(el, clone);
313
+ let html = new XMLSerializer().serializeToString(clone);
314
+ if (
315
+ !/^<svg[^>]*\swidth=/.test(html) ||
316
+ !/^<svg[^>]*\sheight=/.test(html)
317
+ ) {
318
+ html = html.replace(
319
+ /^<svg/,
320
+ `<svg width="${r.width}" height="${r.height}"`,
321
+ );
322
+ }
323
+ if (!/xmlns=/.test(html)) {
324
+ html = html.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
325
+ }
326
+ const entry = loadImage(
327
+ "data:image/svg+xml;charset=utf-8," + encodeURIComponent(html),
328
+ );
329
+ svgCache.set(el, { signature, entry });
330
+ return entry;
331
+ }
332
+
333
+ function usableDomImage(el) {
334
+ return (
335
+ el.complete &&
336
+ el.naturalWidth > 0 &&
337
+ (sameOrigin(el.currentSrc || el.src) || !!el.crossOrigin)
338
+ );
339
+ }
340
+
341
+ function layerUrls(value) {
342
+ if (!value || value === "none") return [];
343
+ const out = [];
344
+ splitTopLevel(value).forEach((layer) => {
345
+ const m = layer.match(/^url\((['"]?)(.*?)\1\)$/);
346
+ if (m) out.push(m[2]);
347
+ });
348
+ return out;
349
+ }
350
+
351
+ const COLOR_TOKEN =
352
+ /^(rgba?\([^)]*\)|hsla?\([^)]*\)|hwb\([^)]*\)|(?:ok)?lab\([^)]*\)|(?:ok)?lch\([^)]*\)|color\([^)]*\)|color-mix\([^)]*\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)/;
353
+
354
+ function parseAngle(token) {
355
+ const v = parseFloat(token);
356
+ if (isNaN(v)) return 180;
357
+ if (token.indexOf("turn") !== -1) return v * 360;
358
+ if (token.indexOf("grad") !== -1) return v * 0.9;
359
+ if (token.indexOf("rad") !== -1) return (v * 180) / Math.PI;
360
+ return v;
361
+ }
362
+
363
+ function sideAngle(spec, w, h) {
364
+ const has = (k) => spec.indexOf(k) !== -1;
365
+ const diag = (Math.atan2(w, h) * 180) / Math.PI;
366
+ if (has("top") && has("right")) return diag;
367
+ if (has("bottom") && has("right")) return 180 - diag;
368
+ if (has("bottom") && has("left")) return 180 + diag;
369
+ if (has("top") && has("left")) return 360 - diag;
370
+ if (has("top")) return 0;
371
+ if (has("right")) return 90;
372
+ if (has("bottom")) return 180;
373
+ if (has("left")) return 270;
374
+ return 180;
375
+ }
376
+
377
+ function parseStops(parts, length) {
378
+ const stops = [];
379
+ parts.forEach((part) => {
380
+ const m = part.match(COLOR_TOKEN);
381
+ if (!m) return;
382
+ const color = m[0];
383
+ const rest = part.slice(color.length).trim();
384
+ const positions = rest ? rest.split(/\s+/) : [];
385
+ if (!positions.length) {
386
+ stops.push({ color, pos: null });
387
+ } else {
388
+ positions.forEach((p) => {
389
+ const pos =
390
+ p.indexOf("%") !== -1
391
+ ? parseFloat(p) / 100
392
+ : length
393
+ ? parseFloat(p) / length
394
+ : 0;
395
+ stops.push({ color, pos: isNaN(pos) ? null : pos });
396
+ });
397
+ }
398
+ });
399
+ if (!stops.length) return stops;
400
+ if (stops[0].pos === null) stops[0].pos = 0;
401
+ if (stops[stops.length - 1].pos === null) {
402
+ stops[stops.length - 1].pos = 1;
403
+ }
404
+ let last = 0;
405
+ for (let i = 0; i < stops.length; i++) {
406
+ if (stops[i].pos === null) {
407
+ let next = i;
408
+ while (next < stops.length && stops[next].pos === null) next++;
409
+ const span = stops[next].pos - last;
410
+ for (let k = i; k < next; k++) {
411
+ stops[k].pos = last + (span * (k - i + 1)) / (next - i + 1);
412
+ }
413
+ i = next - 1;
414
+ }
415
+ last = stops[i].pos;
416
+ if (i > 0 && stops[i].pos < stops[i - 1].pos) {
417
+ stops[i].pos = stops[i - 1].pos;
418
+ }
419
+ }
420
+ return stops;
421
+ }
422
+
423
+ function repeatStops(stops) {
424
+ if (stops.length < 2) return stops;
425
+ const first = stops[0].pos;
426
+ const period = stops[stops.length - 1].pos - first;
427
+ if (period <= 0.0001) return stops;
428
+ const out = [];
429
+ const cycles = Math.min(Math.ceil(1 / period) + 1, 200);
430
+ for (let c = -1; c < cycles; c++) {
431
+ for (let i = 0; i < stops.length; i++) {
432
+ const pos = stops[i].pos + period * c;
433
+ if (pos < -period || pos > 1 + period) continue;
434
+ out.push({
435
+ color: stops[i].color,
436
+ pos: Math.min(1, Math.max(0, pos)),
437
+ });
438
+ }
439
+ }
440
+ return out.length ? out : stops;
441
+ }
442
+
443
+ function resolvePosition(tokens, w, h, iw, ih) {
444
+ let x = "50%";
445
+ let y = "50%";
446
+ if (tokens.length === 1) {
447
+ x = tokens[0];
448
+ y = "50%";
449
+ if (tokens[0] === "top" || tokens[0] === "bottom") {
450
+ y = tokens[0];
451
+ x = "50%";
452
+ }
453
+ } else if (tokens.length >= 2) {
454
+ x = tokens[0];
455
+ y = tokens[1];
456
+ }
457
+ const map = {
458
+ left: "0%",
459
+ top: "0%",
460
+ center: "50%",
461
+ right: "100%",
462
+ bottom: "100%",
463
+ };
464
+ if (map[x] !== undefined) x = map[x];
465
+ if (map[y] !== undefined) y = map[y];
466
+ const px =
467
+ x.indexOf("%") !== -1
468
+ ? (parseFloat(x) / 100) * (w - iw)
469
+ : parseFloat(x) || 0;
470
+ const py =
471
+ y.indexOf("%") !== -1
472
+ ? (parseFloat(y) / 100) * (h - ih)
473
+ : parseFloat(y) || 0;
474
+ return [px, py];
475
+ }
476
+
477
+ function makeGradient(ctx, spec, x, y, w, h) {
478
+ const open = spec.indexOf("(");
479
+ const kind = spec.slice(0, open);
480
+ const body = spec.slice(open + 1, spec.lastIndexOf(")"));
481
+ const parts = splitTopLevel(body);
482
+ if (!parts.length) return null;
483
+ const repeating = kind.indexOf("repeating-") === 0;
484
+ const base = kind.replace("repeating-", "");
485
+
486
+ if (base === "linear-gradient") {
487
+ let angle = 180;
488
+ if (/^(to\s|[-0-9.]+(deg|grad|rad|turn))/.test(parts[0])) {
489
+ angle =
490
+ parts[0].indexOf("to ") === 0
491
+ ? sideAngle(parts[0], w, h)
492
+ : parseAngle(parts[0]);
493
+ parts.shift();
494
+ }
495
+ const rad = ((angle - 90) * Math.PI) / 180;
496
+ const dx = Math.cos(rad);
497
+ const dy = Math.sin(rad);
498
+ const ar = (angle * Math.PI) / 180;
499
+ const length = Math.abs(w * Math.sin(ar)) + Math.abs(h * Math.cos(ar));
500
+ const cx = x + w / 2;
501
+ const cy = y + h / 2;
502
+ let stops = parseStops(parts, length);
503
+ if (!stops.length) return null;
504
+ if (repeating) stops = repeatStops(stops);
505
+ const g = ctx.createLinearGradient(
506
+ cx - (dx * length) / 2,
507
+ cy - (dy * length) / 2,
508
+ cx + (dx * length) / 2,
509
+ cy + (dy * length) / 2,
510
+ );
511
+ stops.forEach((s) => {
512
+ try {
513
+ g.addColorStop(Math.min(1, Math.max(0, s.pos)), s.color);
514
+ } catch (e) {}
515
+ });
516
+ return { gradient: g };
517
+ }
518
+
519
+ if (base === "radial-gradient") {
520
+ let shape = "ellipse";
521
+ let sizing = "farthest-corner";
522
+ let posTokens = [];
523
+ let explicit = [];
524
+ if (!COLOR_TOKEN.test(parts[0]) || /\bat\b/.test(parts[0])) {
525
+ const head = parts[0];
526
+ const atIndex = head.indexOf(" at ");
527
+ const geom = (atIndex === -1 ? head : head.slice(0, atIndex)).trim();
528
+ if (atIndex !== -1) {
529
+ posTokens = head
530
+ .slice(atIndex + 4)
531
+ .trim()
532
+ .split(/\s+/);
533
+ }
534
+ geom.split(/\s+/).forEach((tok) => {
535
+ if (tok === "circle" || tok === "ellipse") shape = tok;
536
+ else if (/closest|farthest/.test(tok)) sizing = tok;
537
+ else if (tok) explicit.push(tok);
538
+ });
539
+ if (geom || atIndex !== -1) parts.shift();
540
+ }
541
+ const cxy = posTokens.length
542
+ ? resolvePosition(posTokens, w, h, 0, 0)
543
+ : [w / 2, h / 2];
544
+ const cx = x + cxy[0];
545
+ const cy = y + cxy[1];
546
+ const lx = cxy[0];
547
+ const ly = cxy[1];
548
+ let rx;
549
+ let ry;
550
+ if (explicit.length) {
551
+ rx = resolveLength(explicit[0], w);
552
+ ry = explicit.length > 1 ? resolveLength(explicit[1], h) : rx;
553
+ } else {
554
+ const dxs = [Math.abs(lx), Math.abs(w - lx)];
555
+ const dys = [Math.abs(ly), Math.abs(h - ly)];
556
+ const near = sizing.indexOf("closest") === 0;
557
+ const sx = near ? Math.min(dxs[0], dxs[1]) : Math.max(dxs[0], dxs[1]);
558
+ const sy = near ? Math.min(dys[0], dys[1]) : Math.max(dys[0], dys[1]);
559
+ if (sizing.indexOf("side") !== -1) {
560
+ rx = sx;
561
+ ry = sy;
562
+ } else {
563
+ rx = Math.sqrt(sx * sx + sy * sy);
564
+ ry = rx;
565
+ if (shape === "ellipse") {
566
+ rx = sx * Math.SQRT2;
567
+ ry = sy * Math.SQRT2;
568
+ }
569
+ }
570
+ if (shape === "circle") {
571
+ rx =
572
+ sizing.indexOf("closest") === 0
573
+ ? Math.min(sx, sy)
574
+ : Math.max(sx, sy);
575
+ if (sizing.indexOf("corner") !== -1)
576
+ rx = Math.sqrt(sx * sx + sy * sy);
577
+ ry = rx;
578
+ }
579
+ }
580
+ rx = Math.max(0.01, rx);
581
+ ry = Math.max(0.01, ry);
582
+ let stops = parseStops(parts, rx);
583
+ if (!stops.length) return null;
584
+ if (repeating) stops = repeatStops(stops);
585
+ const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
586
+ stops.forEach((s) => {
587
+ try {
588
+ g.addColorStop(Math.min(1, Math.max(0, s.pos)), s.color);
589
+ } catch (e) {}
590
+ });
591
+ return { gradient: g, scaleY: ry / rx, cx, cy };
592
+ }
593
+
594
+ if (base === "conic-gradient" && ctx.createConicGradient) {
595
+ let from = 0;
596
+ let posTokens = [];
597
+ if (/^(from\s|at\s)/.test(parts[0])) {
598
+ const head = parts[0];
599
+ const atIndex = head.indexOf(" at ");
600
+ const fromPart = atIndex === -1 ? head : head.slice(0, atIndex);
601
+ if (fromPart.indexOf("from") === 0) {
602
+ from = parseAngle(fromPart.replace("from", "").trim());
603
+ }
604
+ if (atIndex !== -1) {
605
+ posTokens = head
606
+ .slice(atIndex + 4)
607
+ .trim()
608
+ .split(/\s+/);
609
+ }
610
+ parts.shift();
611
+ }
612
+ const cxy = posTokens.length
613
+ ? resolvePosition(posTokens, w, h, 0, 0)
614
+ : [w / 2, h / 2];
615
+ let stops = parseStops(parts, 360);
616
+ if (!stops.length) return null;
617
+ if (repeating) stops = repeatStops(stops);
618
+ const g = ctx.createConicGradient(
619
+ ((from - 90) * Math.PI) / 180,
620
+ x + cxy[0],
621
+ y + cxy[1],
622
+ );
623
+ stops.forEach((s) => {
624
+ try {
625
+ g.addColorStop(Math.min(1, Math.max(0, s.pos)), s.color);
626
+ } catch (e) {}
627
+ });
628
+ return { gradient: g };
629
+ }
630
+
631
+ return null;
632
+ }
633
+
634
+ function boxFor(kind, node, style) {
635
+ let x = node.x;
636
+ let y = node.y;
637
+ let w = node.w;
638
+ let h = node.h;
639
+ let radii = node.radii;
640
+ const bt = parseFloat(style.borderTopWidth) || 0;
641
+ const br = parseFloat(style.borderRightWidth) || 0;
642
+ const bb = parseFloat(style.borderBottomWidth) || 0;
643
+ const bl = parseFloat(style.borderLeftWidth) || 0;
644
+ if (kind === "padding-box" || kind === "content-box") {
645
+ x += bl;
646
+ y += bt;
647
+ w -= bl + br;
648
+ h -= bt + bb;
649
+ radii = insetRadii(radii, bt, br, bb, bl);
650
+ if (kind === "content-box") {
651
+ const pt = parseFloat(style.paddingTop) || 0;
652
+ const pr = parseFloat(style.paddingRight) || 0;
653
+ const pb = parseFloat(style.paddingBottom) || 0;
654
+ const pl = parseFloat(style.paddingLeft) || 0;
655
+ x += pl;
656
+ y += pt;
657
+ w -= pl + pr;
658
+ h -= pt + pb;
659
+ radii = insetRadii(radii, pt, pr, pb, pl);
660
+ }
661
+ }
662
+ return { x, y, w: Math.max(0, w), h: Math.max(0, h), radii };
663
+ }
664
+
665
+ function tracePath(ctx, x, y, w, h, radii) {
666
+ if (w <= 0 || h <= 0) return;
667
+ if (!radii) {
668
+ ctx.rect(x, y, w, h);
669
+ return;
670
+ }
671
+ const [tl, tr, br, bl] = radii;
672
+ const HALF = Math.PI / 2;
673
+ ctx.moveTo(x + tl[0], y);
674
+ ctx.lineTo(x + w - tr[0], y);
675
+ if (tr[0] > 0 || tr[1] > 0) {
676
+ ctx.ellipse(x + w - tr[0], y + tr[1], tr[0], tr[1], 0, -HALF, 0);
677
+ }
678
+ ctx.lineTo(x + w, y + h - br[1]);
679
+ if (br[0] > 0 || br[1] > 0) {
680
+ ctx.ellipse(x + w - br[0], y + h - br[1], br[0], br[1], 0, 0, HALF);
681
+ }
682
+ ctx.lineTo(x + bl[0], y + h);
683
+ if (bl[0] > 0 || bl[1] > 0) {
684
+ ctx.ellipse(x + bl[0], y + h - bl[1], bl[0], bl[1], 0, HALF, Math.PI);
685
+ }
686
+ ctx.lineTo(x, y + tl[1]);
687
+ if (tl[0] > 0 || tl[1] > 0) {
688
+ ctx.ellipse(
689
+ x + tl[0],
690
+ y + tl[1],
691
+ tl[0],
692
+ tl[1],
693
+ 0,
694
+ Math.PI,
695
+ Math.PI + HALF,
696
+ );
697
+ }
698
+ ctx.closePath();
699
+ }
700
+
701
+ function borderBoxSize(el, style) {
702
+ let w = parseFloat(style.width);
703
+ let h = parseFloat(style.height);
704
+ if (isNaN(w) || isNaN(h)) {
705
+ if (typeof el.offsetWidth === "number" && el.offsetWidth) {
706
+ return { w: el.offsetWidth, h: el.offsetHeight };
707
+ }
708
+ const r = el.getBoundingClientRect();
709
+ return { w: r.width, h: r.height };
710
+ }
711
+ if (style.boxSizing !== "border-box") {
712
+ w +=
713
+ parseFloat(style.paddingLeft) +
714
+ parseFloat(style.paddingRight) +
715
+ parseFloat(style.borderLeftWidth) +
716
+ parseFloat(style.borderRightWidth);
717
+ h +=
718
+ parseFloat(style.paddingTop) +
719
+ parseFloat(style.paddingBottom) +
720
+ parseFloat(style.borderTopWidth) +
721
+ parseFloat(style.borderBottomWidth);
722
+ }
723
+ return { w, h };
724
+ }
725
+
726
+ function isStackingContext(el, style) {
727
+ if (style.position !== "static" && style.zIndex !== "auto") return true;
728
+ if (style.position === "fixed" || style.position === "sticky")
729
+ return true;
730
+ if (parseFloat(style.opacity) < 1) return true;
731
+ if (style.transform && style.transform !== "none") return true;
732
+ if (style.filter && style.filter !== "none") return true;
733
+ if (style.isolation === "isolate") return true;
734
+ if (style.mixBlendMode && style.mixBlendMode !== "normal") return true;
735
+ if (/paint|layout|strict|content/.test(style.contain || "")) return true;
736
+ if (style.webkitOverflowScrolling === "touch") return true;
737
+ return false;
738
+ }
739
+
740
+ function measureRuns(el, style) {
741
+ const runs = [];
742
+ if (style.visibility !== "visible") return runs;
743
+
744
+ const transform = style.textTransform;
745
+ const kids = el.childNodes;
746
+ let range = null;
747
+
748
+ for (let i = 0; i < kids.length; i++) {
749
+ const textNode = kids[i];
750
+ if (textNode.nodeType !== 3) continue;
751
+ const raw = textNode.data;
752
+ if (!raw || !raw.trim()) continue;
753
+ if (!range) range = document.createRange();
754
+
755
+ const re = /\S+/g;
756
+ let match;
757
+ while ((match = re.exec(raw)) !== null) {
758
+ try {
759
+ range.setStart(textNode, match.index);
760
+ range.setEnd(textNode, match.index + match[0].length);
761
+ } catch (e) {
762
+ continue;
763
+ }
764
+ const rects = range.getClientRects();
765
+ if (!rects.length) continue;
766
+
767
+ let text = match[0];
768
+ if (transform === "uppercase") text = text.toUpperCase();
769
+ else if (transform === "lowercase") text = text.toLowerCase();
770
+ else if (transform === "capitalize") {
771
+ text = text.replace(/\b\w/g, (c) => c.toUpperCase());
772
+ }
773
+
774
+ if (rects.length === 1) {
775
+ const r = rects[0];
776
+ runs.push({
777
+ text,
778
+ left: r.left,
779
+ top: r.top,
780
+ right: r.right,
781
+ height: r.height,
782
+ });
783
+ } else {
784
+ const per = Math.ceil(text.length / rects.length);
785
+ for (let k = 0; k < rects.length; k++) {
786
+ const slice = text.substr(k * per, per);
787
+ if (!slice) continue;
788
+ const r = rects[k];
789
+ runs.push({
790
+ text: slice,
791
+ left: r.left,
792
+ top: r.top,
793
+ right: r.right,
794
+ height: r.height,
795
+ });
796
+ }
797
+ }
798
+ }
799
+ }
800
+
801
+ const tag = el.tagName;
802
+ if (tag === "INPUT" || tag === "TEXTAREA") {
803
+ const value = el.value || el.placeholder;
804
+ if (value) runs.push({ text: value, value: true });
805
+ }
806
+
807
+ return runs;
808
+ }
809
+
810
+ function discoverAssets(el, style) {
811
+ const layers = style.backgroundImage;
812
+ if (layers && layers !== "none") layerUrls(layers).forEach(loadImage);
813
+ const tag = el.tagName;
814
+ if (tag === "IMG") {
815
+ const src = el.currentSrc || el.src;
816
+ if (src && !usableDomImage(el)) loadImage(src);
817
+ } else if (tag === "svg") {
818
+ svgToImage(el);
819
+ }
820
+ }
821
+
822
+ function buildNode(el, parent, clips, ignore) {
823
+ if (ignore && ignore(el)) return null;
824
+ const style = getComputedStyle(el);
825
+ if (style.display === "none") return null;
826
+
827
+ const size = borderBoxSize(el, style);
828
+ const rect = el.getBoundingClientRect();
829
+ const B = parent ? parent.m : IDENT;
830
+ const Blin = [B[0], B[1], B[2], B[3], 0, 0];
831
+ const own = parseMatrix(style.transform);
832
+ const N = own || IDENT;
833
+ const Nlin = [N[0], N[1], N[2], N[3], 0, 0];
834
+ const BN = mul(Blin, Nlin);
835
+
836
+ let x;
837
+ let y;
838
+ let m;
839
+ if (!own && (!parent || parent.untransformed)) {
840
+ x = rect.left;
841
+ y = rect.top;
842
+ m = IDENT;
843
+ } else {
844
+ const o = parseOrigin(style.transformOrigin);
845
+ const no = apply(Nlin, o[0], o[1]);
846
+ const tv = [o[0] - no[0] + N[4], o[1] - no[1] + N[5]];
847
+ const bt = apply(Blin, tv[0], tv[1]);
848
+ const C = [bt[0] + B[4], bt[1] + B[5]];
849
+ let minX = Infinity;
850
+ let minY = Infinity;
851
+ const corners = [
852
+ [0, 0],
853
+ [size.w, 0],
854
+ [0, size.h],
855
+ [size.w, size.h],
856
+ ];
857
+ for (let i = 0; i < corners.length; i++) {
858
+ const p = apply(BN, corners[i][0], corners[i][1]);
859
+ if (p[0] < minX) minX = p[0];
860
+ if (p[1] < minY) minY = p[1];
861
+ }
862
+ const inv = invertLinear(Blin);
863
+ if (!inv) return null;
864
+ const u = apply(inv, rect.left - minX - C[0], rect.top - minY - C[1]);
865
+ x = u[0];
866
+ y = u[1];
867
+ m = [BN[0], BN[1], BN[2], BN[3], C[0], C[1]];
868
+ }
869
+
870
+ const radii = parseRadii(style, size.w, size.h);
871
+ const node = {
872
+ el,
873
+ style,
874
+ x,
875
+ y,
876
+ w: size.w,
877
+ h: size.h,
878
+ m,
879
+ radii,
880
+ clips,
881
+ opacity: parseFloat(style.opacity),
882
+ untransformed: !own && (!parent || parent.untransformed),
883
+ children: [],
884
+ runs: measureRuns(el, style),
885
+ };
886
+
887
+ discoverAssets(el, style);
888
+
889
+ if (
890
+ CLIP_OVERFLOW.test(style.overflowX) ||
891
+ CLIP_OVERFLOW.test(style.overflowY)
892
+ ) {
893
+ const bt = parseFloat(style.borderTopWidth);
894
+ const br = parseFloat(style.borderRightWidth);
895
+ const bb = parseFloat(style.borderBottomWidth);
896
+ const bl = parseFloat(style.borderLeftWidth);
897
+ node.childClips = clips.concat([
898
+ {
899
+ m,
900
+ x: x + bl,
901
+ y: y + bt,
902
+ w: Math.max(0, size.w - bl - br),
903
+ h: Math.max(0, size.h - bt - bb),
904
+ radii: insetRadii(radii, bt, br, bb, bl),
905
+ },
906
+ ]);
907
+ } else {
908
+ node.childClips = clips;
909
+ }
910
+
911
+ return node;
912
+ }
913
+
914
+ function newStack(node) {
915
+ return {
916
+ node,
917
+ negative: [],
918
+ zeroOrAuto: [],
919
+ positive: [],
920
+ floats: [],
921
+ };
922
+ }
923
+
924
+ function collect(node, stack, ignore) {
925
+ if (node.el.tagName === "svg") return;
926
+ const kids = node.el.children;
927
+ for (let i = 0; i < kids.length; i++) {
928
+ const child = buildNode(kids[i], node, node.childClips, ignore);
929
+ if (!child) continue;
930
+ const cs = child.style;
931
+ if (isStackingContext(child.el, cs)) {
932
+ const sub = newStack(child);
933
+ collect(child, sub, ignore);
934
+ const z =
935
+ cs.position !== "static" && cs.zIndex !== "auto"
936
+ ? parseInt(cs.zIndex, 10) || 0
937
+ : 0;
938
+ if (z < 0) stack.negative.push({ z, sub });
939
+ else if (z > 0) stack.positive.push({ z, sub });
940
+ else stack.zeroOrAuto.push(sub);
941
+ } else if (cs.position !== "static") {
942
+ const sub = newStack(child);
943
+ collect(child, sub, ignore);
944
+ stack.zeroOrAuto.push(sub);
945
+ } else if (cs.float !== "none") {
946
+ const sub = newStack(child);
947
+ collect(child, sub, ignore);
948
+ stack.floats.push(sub);
949
+ } else {
950
+ node.children.push(child);
951
+ collect(child, stack, ignore);
952
+ }
953
+ }
954
+ }
955
+
956
+ function Painter(ctx, base) {
957
+ this.ctx = ctx;
958
+ this.base = base;
959
+ this.activeClips = null;
960
+ this.alphaStack = [];
961
+ }
962
+
963
+ Painter.prototype.space = function (m) {
964
+ const d = mul(this.base, m);
965
+ this.ctx.setTransform(d[0], d[1], d[2], d[3], d[4], d[5]);
966
+ };
967
+
968
+ Painter.prototype.setClips = function (clips) {
969
+ if (this.activeClips === clips) return;
970
+ const ctx = this.ctx;
971
+ if (this.activeClips !== null) ctx.restore();
972
+ ctx.save();
973
+ for (let i = 0; i < clips.length; i++) {
974
+ const c = clips[i];
975
+ this.space(c.m);
976
+ ctx.beginPath();
977
+ tracePath(ctx, c.x, c.y, c.w, c.h, c.radii);
978
+ ctx.clip();
979
+ }
980
+ this.activeClips = clips;
981
+ };
982
+
983
+ Painter.prototype.release = function () {
984
+ if (this.activeClips !== null) {
985
+ this.ctx.restore();
986
+ this.activeClips = null;
987
+ }
988
+ };
989
+
990
+ Painter.prototype.shadows = function (node) {
991
+ const value = node.style.boxShadow;
992
+ if (!value || value === "none") return;
993
+ const ctx = this.ctx;
994
+ const layers = splitTopLevel(value).reverse();
995
+ for (let i = 0; i < layers.length; i++) {
996
+ const raw = layers[i];
997
+ const inset = raw.indexOf("inset") !== -1;
998
+ const body = raw.replace("inset", "").trim();
999
+ const colorMatch = body.match(
1000
+ /^(rgba?\([^)]*\)|hsla?\([^)]*\)|[a-z]+\([^)]*\)|#[0-9a-f]+|[a-z]+)/i,
1001
+ );
1002
+ if (!colorMatch) continue;
1003
+ const color = colorMatch[0];
1004
+ if (isTransparent(color)) continue;
1005
+ const nums = body
1006
+ .slice(color.length)
1007
+ .trim()
1008
+ .split(/\s+/)
1009
+ .map((n) => parseFloat(n) || 0);
1010
+ const dx = nums[0] || 0;
1011
+ const dy = nums[1] || 0;
1012
+ const blur = nums[2] || 0;
1013
+ const spread = nums[3] || 0;
1014
+
1015
+ ctx.save();
1016
+ this.space(node.m);
1017
+ if (inset) {
1018
+ ctx.beginPath();
1019
+ tracePath(ctx, node.x, node.y, node.w, node.h, node.radii);
1020
+ ctx.clip();
1021
+ const gx = node.x - node.w - 100;
1022
+ const gy = node.y - node.h - 100;
1023
+ ctx.beginPath();
1024
+ ctx.rect(gx, gy, node.w * 3 + 200, node.h * 3 + 200);
1025
+ tracePath(
1026
+ ctx,
1027
+ node.x + spread,
1028
+ node.y + spread,
1029
+ node.w - spread * 2,
1030
+ node.h - spread * 2,
1031
+ insetRadii(node.radii, spread, spread, spread, spread),
1032
+ );
1033
+ ctx.shadowColor = color;
1034
+ ctx.shadowOffsetX = dx;
1035
+ ctx.shadowOffsetY = dy;
1036
+ ctx.shadowBlur = blur;
1037
+ ctx.fillStyle = "#000";
1038
+ ctx.fill("evenodd");
1039
+ } else {
1040
+ ctx.beginPath();
1041
+ tracePath(ctx, node.x, node.y, node.w, node.h, node.radii);
1042
+ const gx = node.x - node.w - blur * 2 - Math.abs(dx) - spread - 100;
1043
+ const gy = node.y - node.h - blur * 2 - Math.abs(dy) - spread - 100;
1044
+ ctx.rect(
1045
+ gx,
1046
+ gy,
1047
+ node.w * 3 + blur * 4 + Math.abs(dx) * 2 + spread * 2 + 200,
1048
+ node.h * 3 + blur * 4 + Math.abs(dy) * 2 + spread * 2 + 200,
1049
+ );
1050
+ ctx.clip("evenodd");
1051
+ ctx.shadowColor = color;
1052
+ ctx.shadowOffsetX = dx;
1053
+ ctx.shadowOffsetY = dy;
1054
+ ctx.shadowBlur = blur;
1055
+ ctx.fillStyle = "#000";
1056
+ ctx.beginPath();
1057
+ tracePath(
1058
+ ctx,
1059
+ node.x - spread,
1060
+ node.y - spread,
1061
+ node.w + spread * 2,
1062
+ node.h + spread * 2,
1063
+ insetRadii(node.radii, -spread, -spread, -spread, -spread),
1064
+ );
1065
+ ctx.fill();
1066
+ }
1067
+ ctx.restore();
1068
+ }
1069
+ };
1070
+
1071
+ Painter.prototype.background = function (node) {
1072
+ const style = node.style;
1073
+ const ctx = this.ctx;
1074
+ const images = style.backgroundImage;
1075
+ const hasImages = images && images !== "none";
1076
+ if (isTransparent(style.backgroundColor) && !hasImages) return;
1077
+
1078
+ const clipList = splitTopLevel(style.backgroundClip || "border-box");
1079
+ const clipBox = boxFor(clipList[clipList.length - 1], node, style);
1080
+ if (clipBox.w <= 0 || clipBox.h <= 0) return;
1081
+
1082
+ if (!isTransparent(style.backgroundColor)) {
1083
+ this.space(node.m);
1084
+ ctx.beginPath();
1085
+ tracePath(
1086
+ ctx,
1087
+ clipBox.x,
1088
+ clipBox.y,
1089
+ clipBox.w,
1090
+ clipBox.h,
1091
+ clipBox.radii,
1092
+ );
1093
+ ctx.fillStyle = style.backgroundColor;
1094
+ ctx.fill();
1095
+ }
1096
+
1097
+ if (!hasImages) return;
1098
+
1099
+ const layers = splitTopLevel(images);
1100
+ const originList = splitTopLevel(style.backgroundOrigin || "padding-box");
1101
+ const sizeList = splitTopLevel(style.backgroundSize || "auto");
1102
+ const posList = splitTopLevel(style.backgroundPosition || "0% 0%");
1103
+ const repeatList = splitTopLevel(style.backgroundRepeat || "repeat");
1104
+ const pick = (list, i) => list[i % list.length];
1105
+
1106
+ for (let i = layers.length - 1; i >= 0; i--) {
1107
+ const layer = layers[i];
1108
+ if (!layer || layer === "none") continue;
1109
+ const lClip = boxFor(pick(clipList, i), node, style);
1110
+ const origin = boxFor(pick(originList, i), node, style);
1111
+ if (lClip.w <= 0 || lClip.h <= 0) continue;
1112
+
1113
+ ctx.save();
1114
+ this.space(node.m);
1115
+ ctx.beginPath();
1116
+ tracePath(ctx, lClip.x, lClip.y, lClip.w, lClip.h, lClip.radii);
1117
+ ctx.clip();
1118
+
1119
+ const urlMatch = layer.match(/^url\((['"]?)(.*?)\1\)$/);
1120
+ if (urlMatch) {
1121
+ const entry = imageCache.get(urlMatch[2]);
1122
+ if (entry && entry.ready) {
1123
+ this.tile(
1124
+ entry.img,
1125
+ origin,
1126
+ pick(sizeList, i),
1127
+ pick(posList, i),
1128
+ pick(repeatList, i),
1129
+ );
1130
+ }
1131
+ } else if (/gradient\(/.test(layer)) {
1132
+ try {
1133
+ const made = makeGradient(
1134
+ ctx,
1135
+ layer,
1136
+ origin.x,
1137
+ origin.y,
1138
+ origin.w,
1139
+ origin.h,
1140
+ );
1141
+ if (made) {
1142
+ ctx.fillStyle = made.gradient;
1143
+ if (made.scaleY && Math.abs(made.scaleY - 1) > 0.001) {
1144
+ const inv = 1 / made.scaleY;
1145
+ ctx.translate(made.cx, made.cy);
1146
+ ctx.scale(1, made.scaleY);
1147
+ ctx.translate(-made.cx, -made.cy);
1148
+ ctx.fillRect(
1149
+ lClip.x,
1150
+ made.cy + (lClip.y - made.cy) * inv,
1151
+ lClip.w,
1152
+ lClip.h * inv,
1153
+ );
1154
+ } else {
1155
+ ctx.fillRect(lClip.x, lClip.y, lClip.w, lClip.h);
1156
+ }
1157
+ }
1158
+ } catch (e) {}
1159
+ }
1160
+ ctx.restore();
1161
+ }
1162
+ };
1163
+
1164
+ Painter.prototype.tile = function (img, area, size, position, repeat) {
1165
+ const ctx = this.ctx;
1166
+ const nw = img.naturalWidth || img.width;
1167
+ const nh = img.naturalHeight || img.height;
1168
+ if (!nw || !nh) return;
1169
+
1170
+ let dw;
1171
+ let dh;
1172
+ const ratio = nw / nh;
1173
+ if (size === "cover" || size === "contain") {
1174
+ const areaRatio = area.w / area.h;
1175
+ const wide = size === "cover" ? areaRatio < ratio : areaRatio > ratio;
1176
+ if (wide) {
1177
+ dh = area.h;
1178
+ dw = dh * ratio;
1179
+ } else {
1180
+ dw = area.w;
1181
+ dh = dw / ratio;
1182
+ }
1183
+ } else {
1184
+ const tokens = (size || "auto").split(/\s+/);
1185
+ const sx = tokens[0] || "auto";
1186
+ const sy = tokens[1] || "auto";
1187
+ if (sx === "auto" && sy === "auto") {
1188
+ dw = nw;
1189
+ dh = nh;
1190
+ } else if (sx === "auto") {
1191
+ dh = resolveLength(sy, area.h);
1192
+ dw = dh * ratio;
1193
+ } else if (sy === "auto") {
1194
+ dw = resolveLength(sx, area.w);
1195
+ dh = dw / ratio;
1196
+ } else {
1197
+ dw = resolveLength(sx, area.w);
1198
+ dh = resolveLength(sy, area.h);
1199
+ }
1200
+ }
1201
+ if (dw <= 0 || dh <= 0) return;
1202
+
1203
+ const offset = resolvePosition(
1204
+ (position || "0% 0%").split(/\s+/),
1205
+ area.w,
1206
+ area.h,
1207
+ dw,
1208
+ dh,
1209
+ );
1210
+ const ox = area.x + offset[0];
1211
+ const oy = area.y + offset[1];
1212
+
1213
+ if (repeat === "no-repeat") {
1214
+ ctx.drawImage(img, ox, oy, dw, dh);
1215
+ return;
1216
+ }
1217
+
1218
+ const repeatX = repeat !== "repeat-y";
1219
+ const repeatY = repeat !== "repeat-x";
1220
+ const mode =
1221
+ repeatX && repeatY ? "repeat" : repeatX ? "repeat-x" : "repeat-y";
1222
+ const pattern = ctx.createPattern(img, mode);
1223
+ if (!pattern) return;
1224
+
1225
+ if (pattern.setTransform && typeof DOMMatrix !== "undefined") {
1226
+ pattern.setTransform(new DOMMatrix([dw / nw, 0, 0, dh / nh, ox, oy]));
1227
+ ctx.fillStyle = pattern;
1228
+ ctx.fillRect(
1229
+ repeatX ? area.x : ox,
1230
+ repeatY ? area.y : oy,
1231
+ repeatX ? area.w : dw,
1232
+ repeatY ? area.h : dh,
1233
+ );
1234
+ return;
1235
+ }
1236
+
1237
+ ctx.save();
1238
+ ctx.translate(ox, oy);
1239
+ ctx.scale(dw / nw, dh / nh);
1240
+ ctx.fillStyle = pattern;
1241
+ ctx.fillRect(
1242
+ ((repeatX ? area.x : ox) - ox) / (dw / nw),
1243
+ ((repeatY ? area.y : oy) - oy) / (dh / nh),
1244
+ (repeatX ? area.w : dw) / (dw / nw),
1245
+ (repeatY ? area.h : dh) / (dh / nh),
1246
+ );
1247
+ ctx.restore();
1248
+ };
1249
+
1250
+ Painter.prototype.replaced = function (node) {
1251
+ const el = node.el;
1252
+ const tag = el.tagName;
1253
+ if (tag === "VIDEO" || tag === "IFRAME") return;
1254
+
1255
+ let source = null;
1256
+ if (tag === "IMG") {
1257
+ if (usableDomImage(el)) {
1258
+ source = el;
1259
+ } else {
1260
+ const entry = imageCache.get(el.currentSrc || el.src);
1261
+ if (entry && entry.ready) source = entry.img;
1262
+ }
1263
+ } else if (tag === "CANVAS") {
1264
+ source = el.width && el.height ? el : null;
1265
+ } else if (tag === "svg") {
1266
+ const cached = svgCache.get(el);
1267
+ if (cached && cached.entry.ready) source = cached.entry.img;
1268
+ }
1269
+ if (!source) return;
1270
+
1271
+ const style = node.style;
1272
+ const box = boxFor("content-box", node, style);
1273
+ if (box.w <= 0 || box.h <= 0) return;
1274
+
1275
+ const nw = source.naturalWidth || source.width || box.w;
1276
+ const nh = source.naturalHeight || source.height || box.h;
1277
+ if (!nw || !nh) return;
1278
+
1279
+ const fit = style.objectFit || "fill";
1280
+ let dw = box.w;
1281
+ let dh = box.h;
1282
+ if (fit !== "fill") {
1283
+ const ratio = nw / nh;
1284
+ const areaRatio = box.w / box.h;
1285
+ if (fit === "contain" || fit === "scale-down") {
1286
+ if (areaRatio > ratio) {
1287
+ dh = box.h;
1288
+ dw = dh * ratio;
1289
+ } else {
1290
+ dw = box.w;
1291
+ dh = dw / ratio;
1292
+ }
1293
+ if (fit === "scale-down" && (dw > nw || dh > nh)) {
1294
+ dw = nw;
1295
+ dh = nh;
1296
+ }
1297
+ } else if (fit === "cover") {
1298
+ if (areaRatio < ratio) {
1299
+ dh = box.h;
1300
+ dw = dh * ratio;
1301
+ } else {
1302
+ dw = box.w;
1303
+ dh = dw / ratio;
1304
+ }
1305
+ } else if (fit === "none") {
1306
+ dw = nw;
1307
+ dh = nh;
1308
+ }
1309
+ }
1310
+
1311
+ const offset = resolvePosition(
1312
+ (style.objectPosition || "50% 50%").split(/\s+/),
1313
+ box.w,
1314
+ box.h,
1315
+ dw,
1316
+ dh,
1317
+ );
1318
+
1319
+ const ctx = this.ctx;
1320
+ ctx.save();
1321
+ this.space(node.m);
1322
+ ctx.beginPath();
1323
+ tracePath(ctx, box.x, box.y, box.w, box.h, box.radii);
1324
+ ctx.clip();
1325
+ try {
1326
+ ctx.drawImage(source, box.x + offset[0], box.y + offset[1], dw, dh);
1327
+ } catch (e) {}
1328
+ ctx.restore();
1329
+ };
1330
+
1331
+ Painter.prototype.text = function (node) {
1332
+ const style = node.style;
1333
+ const ctx = this.ctx;
1334
+ const strokeWidth = parseFloat(style.webkitTextStrokeWidth) || 0;
1335
+ if (isTransparent(style.color) && strokeWidth <= 0) return;
1336
+
1337
+ this.setClips(node.clips);
1338
+ this.space(node.m);
1339
+
1340
+ const fontSize = parseFloat(style.fontSize) || 16;
1341
+ ctx.font = `${style.fontStyle} ${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
1342
+ if ("letterSpacing" in ctx) {
1343
+ ctx.letterSpacing =
1344
+ style.letterSpacing === "normal" ? "0px" : style.letterSpacing;
1345
+ }
1346
+ if ("wordSpacing" in ctx) {
1347
+ ctx.wordSpacing =
1348
+ style.wordSpacing === "normal" ? "0px" : style.wordSpacing;
1349
+ }
1350
+ const rtl = style.direction === "rtl";
1351
+ ctx.direction = rtl ? "rtl" : "ltr";
1352
+ ctx.textAlign = rtl ? "right" : "left";
1353
+ ctx.textBaseline = "alphabetic";
1354
+ ctx.fillStyle = style.color;
1355
+
1356
+ const strokeColor = style.webkitTextStrokeColor;
1357
+ const strokeFirst = (style.paintOrder || "").indexOf("stroke") === 0;
1358
+
1359
+ const shadows = [];
1360
+ if (style.textShadow && style.textShadow !== "none") {
1361
+ splitTopLevel(style.textShadow).forEach((raw) => {
1362
+ const cm = raw.match(COLOR_TOKEN);
1363
+ if (!cm || isTransparent(cm[0])) return;
1364
+ const nums = raw
1365
+ .slice(cm[0].length)
1366
+ .trim()
1367
+ .split(/\s+/)
1368
+ .map((n) => parseFloat(n) || 0);
1369
+ shadows.push({
1370
+ color: cm[0],
1371
+ dx: nums[0] || 0,
1372
+ dy: nums[1] || 0,
1373
+ blur: nums[2] || 0,
1374
+ });
1375
+ });
1376
+ }
1377
+
1378
+ const inv = node.untransformed ? null : invert(node.m);
1379
+ const toLocal = (px, py) => (inv ? apply(inv, px, py) : [px, py]);
1380
+ const scaleY = inv ? Math.hypot(inv[2], inv[3]) : 1;
1381
+
1382
+ const decoration = style.textDecorationLine;
1383
+ const decorate =
1384
+ decoration && decoration !== "none"
1385
+ ? {
1386
+ color: style.textDecorationColor || style.color,
1387
+ under: decoration.indexOf("underline") !== -1,
1388
+ through: decoration.indexOf("line-through") !== -1,
1389
+ over: decoration.indexOf("overline") !== -1,
1390
+ }
1391
+ : null;
1392
+
1393
+ const draw = (text, run) => {
1394
+ if (!text) return;
1395
+ const local = toLocal(run.left, run.top);
1396
+ const metrics = ctx.measureText(text);
1397
+ const ascent =
1398
+ metrics.fontBoundingBoxAscent ||
1399
+ metrics.actualBoundingBoxAscent ||
1400
+ fontSize * 0.8;
1401
+ const descent =
1402
+ metrics.fontBoundingBoxDescent ||
1403
+ metrics.actualBoundingBoxDescent ||
1404
+ fontSize * 0.2;
1405
+ const boxH = run.height * scaleY;
1406
+ const baseline = local[1] + (boxH - (ascent + descent)) / 2 + ascent;
1407
+ const anchor = rtl ? toLocal(run.right, run.top)[0] : local[0];
1408
+
1409
+ for (let s = 0; s < shadows.length; s++) {
1410
+ const sh = shadows[s];
1411
+ ctx.save();
1412
+ ctx.shadowColor = sh.color;
1413
+ ctx.shadowOffsetX = sh.dx;
1414
+ ctx.shadowOffsetY = sh.dy;
1415
+ ctx.shadowBlur = sh.blur;
1416
+ ctx.fillText(text, anchor, baseline);
1417
+ ctx.restore();
1418
+ }
1419
+
1420
+ const strokeIt = () => {
1421
+ if (strokeWidth <= 0 || isTransparent(strokeColor)) return;
1422
+ ctx.save();
1423
+ ctx.lineWidth = strokeWidth * 2;
1424
+ ctx.strokeStyle = strokeColor;
1425
+ ctx.lineJoin = "round";
1426
+ ctx.strokeText(text, anchor, baseline);
1427
+ ctx.restore();
1428
+ };
1429
+
1430
+ if (strokeFirst) strokeIt();
1431
+ ctx.fillText(text, anchor, baseline);
1432
+ if (!strokeFirst) strokeIt();
1433
+
1434
+ if (decorate) {
1435
+ const width = metrics.width;
1436
+ const x0 = rtl ? anchor - width : anchor;
1437
+ const thickness = Math.max(1, fontSize / 14);
1438
+ ctx.save();
1439
+ ctx.fillStyle = decorate.color;
1440
+ if (decorate.under) {
1441
+ ctx.fillRect(x0, baseline + thickness * 1.5, width, thickness);
1442
+ }
1443
+ if (decorate.through) {
1444
+ ctx.fillRect(x0, baseline - ascent / 3, width, thickness);
1445
+ }
1446
+ if (decorate.over) {
1447
+ ctx.fillRect(x0, baseline - ascent, width, thickness);
1448
+ }
1449
+ ctx.restore();
1450
+ }
1451
+ };
1452
+
1453
+ const runs = node.runs;
1454
+ for (let i = 0; i < runs.length; i++) {
1455
+ const run = runs[i];
1456
+ if (run.value) {
1457
+ const box = boxFor("content-box", node, style);
1458
+ const metrics = ctx.measureText(run.text);
1459
+ const ascent = metrics.fontBoundingBoxAscent || fontSize * 0.8;
1460
+ const descent = metrics.fontBoundingBoxDescent || fontSize * 0.2;
1461
+ const baseline = box.y + (box.h - (ascent + descent)) / 2 + ascent;
1462
+ ctx.fillText(run.text, rtl ? box.x + box.w : box.x, baseline);
1463
+ } else {
1464
+ draw(run.text, run);
1465
+ }
1466
+ }
1467
+ };
1468
+
1469
+ Painter.prototype.borders = function (node) {
1470
+ const style = node.style;
1471
+ const widths = [
1472
+ parseFloat(style.borderTopWidth) || 0,
1473
+ parseFloat(style.borderRightWidth) || 0,
1474
+ parseFloat(style.borderBottomWidth) || 0,
1475
+ parseFloat(style.borderLeftWidth) || 0,
1476
+ ];
1477
+ const styles = [
1478
+ style.borderTopStyle,
1479
+ style.borderRightStyle,
1480
+ style.borderBottomStyle,
1481
+ style.borderLeftStyle,
1482
+ ];
1483
+ const colors = [
1484
+ style.borderTopColor,
1485
+ style.borderRightColor,
1486
+ style.borderBottomColor,
1487
+ style.borderLeftColor,
1488
+ ];
1489
+ let any = false;
1490
+ for (let i = 0; i < 4; i++) {
1491
+ if (
1492
+ widths[i] > 0 &&
1493
+ styles[i] !== "none" &&
1494
+ styles[i] !== "hidden" &&
1495
+ !isTransparent(colors[i])
1496
+ ) {
1497
+ any = true;
1498
+ break;
1499
+ }
1500
+ }
1501
+ if (!any) return;
1502
+
1503
+ const ctx = this.ctx;
1504
+ const inner = insetRadii(
1505
+ node.radii,
1506
+ widths[0],
1507
+ widths[1],
1508
+ widths[2],
1509
+ widths[3],
1510
+ );
1511
+ const ix = node.x + widths[3];
1512
+ const iy = node.y + widths[0];
1513
+ const iw = Math.max(0, node.w - widths[1] - widths[3]);
1514
+ const ih = Math.max(0, node.h - widths[0] - widths[2]);
1515
+
1516
+ const uniform =
1517
+ colors[0] === colors[1] &&
1518
+ colors[1] === colors[2] &&
1519
+ colors[2] === colors[3] &&
1520
+ styles[0] === styles[1] &&
1521
+ styles[1] === styles[2] &&
1522
+ styles[2] === styles[3] &&
1523
+ styles[0] === "solid";
1524
+
1525
+ this.space(node.m);
1526
+
1527
+ if (uniform) {
1528
+ ctx.beginPath();
1529
+ tracePath(ctx, node.x, node.y, node.w, node.h, node.radii);
1530
+ tracePath(ctx, ix, iy, iw, ih, inner);
1531
+ ctx.fillStyle = colors[0];
1532
+ ctx.fill("evenodd");
1533
+ return;
1534
+ }
1535
+
1536
+ const wedges = [
1537
+ [
1538
+ [node.x, node.y],
1539
+ [node.x + node.w, node.y],
1540
+ [ix + iw, iy],
1541
+ [ix, iy],
1542
+ ],
1543
+ [
1544
+ [node.x + node.w, node.y],
1545
+ [node.x + node.w, node.y + node.h],
1546
+ [ix + iw, iy + ih],
1547
+ [ix + iw, iy],
1548
+ ],
1549
+ [
1550
+ [node.x + node.w, node.y + node.h],
1551
+ [node.x, node.y + node.h],
1552
+ [ix, iy + ih],
1553
+ [ix + iw, iy + ih],
1554
+ ],
1555
+ [
1556
+ [node.x, node.y + node.h],
1557
+ [node.x, node.y],
1558
+ [ix, iy],
1559
+ [ix, iy + ih],
1560
+ ],
1561
+ ];
1562
+
1563
+ for (let i = 0; i < 4; i++) {
1564
+ if (
1565
+ widths[i] <= 0 ||
1566
+ styles[i] === "none" ||
1567
+ styles[i] === "hidden" ||
1568
+ isTransparent(colors[i])
1569
+ ) {
1570
+ continue;
1571
+ }
1572
+ ctx.save();
1573
+ this.space(node.m);
1574
+ const wedge = wedges[i];
1575
+ ctx.beginPath();
1576
+ ctx.moveTo(wedge[0][0], wedge[0][1]);
1577
+ for (let p = 1; p < wedge.length; p++) {
1578
+ ctx.lineTo(wedge[p][0], wedge[p][1]);
1579
+ }
1580
+ ctx.closePath();
1581
+ ctx.clip();
1582
+ ctx.beginPath();
1583
+ tracePath(ctx, node.x, node.y, node.w, node.h, node.radii);
1584
+ tracePath(ctx, ix, iy, iw, ih, inner);
1585
+ ctx.fillStyle = colors[i];
1586
+ ctx.fill("evenodd");
1587
+ ctx.restore();
1588
+ }
1589
+ };
1590
+
1591
+ Painter.prototype.node = function (node) {
1592
+ if (node.style.visibility !== "visible") return;
1593
+ if (node.w <= 0 || node.h <= 0) return;
1594
+ this.setClips(node.clips);
1595
+ this.shadows(node);
1596
+ this.background(node);
1597
+ this.borders(node);
1598
+ this.replaced(node);
1599
+ };
1600
+
1601
+ const OP_BOX = 0;
1602
+ const OP_TEXT = 1;
1603
+ const OP_ALPHA_PUSH = 2;
1604
+ const OP_ALPHA_POP = 3;
1605
+
1606
+ function emitInFlowBoxes(node, out) {
1607
+ for (let i = 0; i < node.children.length; i++) {
1608
+ const child = node.children[i];
1609
+ out.push({ t: OP_BOX, node: child });
1610
+ emitInFlowBoxes(child, out);
1611
+ }
1612
+ }
1613
+
1614
+ function emitInFlowText(node, out) {
1615
+ for (let i = 0; i < node.children.length; i++) {
1616
+ const child = node.children[i];
1617
+ if (child.runs.length) out.push({ t: OP_TEXT, node: child });
1618
+ emitInFlowText(child, out);
1619
+ }
1620
+ }
1621
+
1622
+ function emitStack(stack, out) {
1623
+ const alpha = stack.node.opacity;
1624
+ const fade = !isNaN(alpha) && alpha < 1;
1625
+ if (fade) out.push({ t: OP_ALPHA_PUSH, alpha });
1626
+
1627
+ out.push({ t: OP_BOX, node: stack.node });
1628
+
1629
+ const byZ = (a, b) => a.z - b.z;
1630
+ stack.negative.sort(byZ);
1631
+ for (let i = 0; i < stack.negative.length; i++) {
1632
+ emitStack(stack.negative[i].sub, out);
1633
+ }
1634
+
1635
+ emitInFlowBoxes(stack.node, out);
1636
+
1637
+ if (stack.node.runs.length) out.push({ t: OP_TEXT, node: stack.node });
1638
+ emitInFlowText(stack.node, out);
1639
+
1640
+ for (let i = 0; i < stack.floats.length; i++) {
1641
+ emitStack(stack.floats[i], out);
1642
+ }
1643
+ for (let i = 0; i < stack.zeroOrAuto.length; i++) {
1644
+ emitStack(stack.zeroOrAuto[i], out);
1645
+ }
1646
+ stack.positive.sort(byZ);
1647
+ for (let i = 0; i < stack.positive.length; i++) {
1648
+ emitStack(stack.positive[i].sub, out);
1649
+ }
1650
+
1651
+ if (fade) out.push({ t: OP_ALPHA_POP });
1652
+ }
1653
+
1654
+ Painter.prototype.run = function (ops, from, budgetMs) {
1655
+ const ctx = this.ctx;
1656
+ const deadline = budgetMs ? performance.now() + budgetMs : 0;
1657
+ for (let i = from; i < ops.length; i++) {
1658
+ const op = ops[i];
1659
+ if (op.t === OP_BOX) {
1660
+ this.node(op.node);
1661
+ } else if (op.t === OP_TEXT) {
1662
+ this.text(op.node);
1663
+ } else if (op.t === OP_ALPHA_PUSH) {
1664
+ this.release();
1665
+ this.alphaStack.push(ctx.globalAlpha);
1666
+ ctx.globalAlpha = ctx.globalAlpha * op.alpha;
1667
+ } else {
1668
+ this.release();
1669
+ ctx.globalAlpha = this.alphaStack.pop();
1670
+ }
1671
+ if (deadline && (i & 63) === 0 && performance.now() > deadline) {
1672
+ return i + 1;
1673
+ }
1674
+ }
1675
+ return ops.length;
1676
+ };
1677
+
1678
+ function measure(element, options) {
1679
+ const opts = options || {};
1680
+ const scale = opts.scale || 1;
1681
+ const ignore = opts.ignoreElements || null;
1682
+
1683
+ pending = [];
1684
+ const root = buildNode(element, null, [], ignore);
1685
+ if (!root) return null;
1686
+
1687
+ const stack = newStack(root);
1688
+ collect(root, stack, ignore);
1689
+
1690
+ const ops = [];
1691
+ emitStack(stack, ops);
1692
+
1693
+ let base = [scale, 0, 0, scale, -root.x * scale, -root.y * scale];
1694
+ if (!root.untransformed) {
1695
+ const rootInv = invert(root.m);
1696
+ if (rootInv) base = mul(base, rootInv);
1697
+ }
1698
+
1699
+ const assets = pending;
1700
+ pending = [];
1701
+
1702
+ return {
1703
+ root,
1704
+ ops,
1705
+ base,
1706
+ scale,
1707
+ assets,
1708
+ width: opts.width != null ? opts.width : root.w,
1709
+ height: opts.height != null ? opts.height : root.h,
1710
+ backgroundColor: opts.backgroundColor || null,
1711
+ };
1712
+ }
1713
+
1714
+ function prepareCanvas(plan, canvas) {
1715
+ const cw = Math.max(1, Math.round(plan.width * plan.scale));
1716
+ const ch = Math.max(1, Math.round(plan.height * plan.scale));
1717
+ if (canvas.width !== cw) canvas.width = cw;
1718
+ if (canvas.height !== ch) canvas.height = ch;
1719
+
1720
+ const ctx = canvas.getContext("2d");
1721
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1722
+ ctx.globalAlpha = 1;
1723
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1724
+ if (plan.backgroundColor) {
1725
+ ctx.fillStyle = plan.backgroundColor;
1726
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1727
+ }
1728
+ return ctx;
1729
+ }
1730
+
1731
+ function paint(plan, canvas) {
1732
+ const ctx = prepareCanvas(plan, canvas);
1733
+ const painter = new Painter(ctx, plan.base);
1734
+ painter.run(plan.ops, 0, 0);
1735
+ painter.release();
1736
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1737
+ return canvas;
1738
+ }
1739
+
1740
+ function nextTask() {
1741
+ return new Promise((resolve) => {
1742
+ if (typeof requestIdleCallback === "function") {
1743
+ requestIdleCallback(() => resolve(), { timeout: 32 });
1744
+ } else {
1745
+ setTimeout(resolve, 0);
1746
+ }
1747
+ });
1748
+ }
1749
+
1750
+ async function paintChunked(plan, canvas, budgetMs) {
1751
+ const ctx = prepareCanvas(plan, canvas);
1752
+ const painter = new Painter(ctx, plan.base);
1753
+ let index = 0;
1754
+ while (index < plan.ops.length) {
1755
+ index = painter.run(plan.ops, index, budgetMs);
1756
+ if (index < plan.ops.length) await nextTask();
1757
+ }
1758
+ painter.release();
1759
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1760
+ return canvas;
1761
+ }
1762
+
1763
+ function rasterise(element, options) {
1764
+ const opts = options || {};
1765
+ const canvas = opts.canvas || document.createElement("canvas");
1766
+ const plan = measure(element, opts);
1767
+ if (!plan) {
1768
+ canvas.width = Math.max(
1769
+ 1,
1770
+ Math.round((opts.width || 1) * (opts.scale || 1)),
1771
+ );
1772
+ canvas.height = Math.max(
1773
+ 1,
1774
+ Math.round((opts.height || 1) * (opts.scale || 1)),
1775
+ );
1776
+ return canvas;
1777
+ }
1778
+ return paint(plan, canvas);
1779
+ }
1780
+
1781
+ async function rasteriseAsync(element, options) {
1782
+ const opts = options || {};
1783
+ const canvas = opts.canvas || document.createElement("canvas");
1784
+ let plan = measure(element, opts);
1785
+ if (!plan) {
1786
+ canvas.width = Math.max(
1787
+ 1,
1788
+ Math.round((opts.width || 1) * (opts.scale || 1)),
1789
+ );
1790
+ canvas.height = Math.max(
1791
+ 1,
1792
+ Math.round((opts.height || 1) * (opts.scale || 1)),
1793
+ );
1794
+ return canvas;
1795
+ }
1796
+ if (plan.assets.length) {
1797
+ await Promise.all(plan.assets);
1798
+ plan = measure(element, opts) || plan;
1799
+ }
1800
+ return paintChunked(plan, canvas, opts.budgetMs || 8);
1801
+ }
1802
+
1803
+ return { measure, paint, paintChunked, rasterise, rasteriseAsync };
1804
+ })();
70
1805
 
71
1806
  /* --------------------------------------------------
72
1807
  * Shared renderer (one per page)
73
1808
  * ------------------------------------------------*/
74
1809
  class liquidGLRenderer {
75
1810
  constructor(snapshotSelector, snapshotResolution = 1.0) {
1811
+ this._naughtyQueued = false;
76
1812
  this.canvas = document.createElement("canvas");
77
1813
  this.canvas.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;`;
78
1814
  this.canvas.setAttribute("data-liquid-ignore", "");
@@ -138,7 +1874,7 @@ const liquidGL = (() => {
138
1874
  this._resizeCanvas();
139
1875
  this.lenses.forEach((l) => l.updateMetrics());
140
1876
  this.captureSnapshot();
141
- }, 250);
1877
+ }, RECAPTURE_INTERVAL_MS);
142
1878
  window.addEventListener("resize", onResize, { passive: true });
143
1879
 
144
1880
  if ("ResizeObserver" in window) {
@@ -157,27 +1893,30 @@ const liquidGL = (() => {
157
1893
  document.head.appendChild(styleEl);
158
1894
  this._dynamicStyleSheet = styleEl.sheet;
159
1895
 
1896
+ this._snapshotResolution = Math.max(
1897
+ 0.1,
1898
+ Math.min(3.0, snapshotResolution),
1899
+ );
1900
+ this._pendingReveal = [];
1901
+
160
1902
  this._resizeCanvas();
161
1903
  this.captureSnapshot();
162
1904
 
163
- this._pendingReveal = [];
164
-
165
1905
  /* --------------------------------------------------
166
1906
  * Dynamic media (video) support
167
1907
  * ------------------------------------------------*/
168
1908
  this._videoNodes = Array.from(
169
- this.snapshotTarget.querySelectorAll("video")
1909
+ this.snapshotTarget.querySelectorAll("video"),
170
1910
  );
171
1911
  this._videoNodes = this._videoNodes.filter((v) => !this._isIgnored(v));
172
1912
  this._tmpCanvas = document.createElement("canvas");
173
1913
  this._tmpCtx = this._tmpCanvas.getContext("2d");
174
1914
 
175
- this.canvas.style.opacity = "0";
1915
+ this._videoFrameState = new WeakMap();
176
1916
 
177
- this._snapshotResolution = Math.max(
178
- 0.1,
179
- Math.min(3.0, snapshotResolution)
180
- );
1917
+ this._videoAlphaState = new WeakMap();
1918
+
1919
+ this.canvas.style.opacity = "0";
181
1920
 
182
1921
  this.useExternalTicker = false;
183
1922
 
@@ -227,7 +1966,7 @@ const liquidGL = (() => {
227
1966
  y,
228
1967
  gl.RGBA,
229
1968
  gl.UNSIGNED_BYTE,
230
- bmp
1969
+ bmp,
231
1970
  );
232
1971
  };
233
1972
  }
@@ -244,13 +1983,18 @@ const liquidGL = (() => {
244
1983
  }`;
245
1984
 
246
1985
  const fsSource = `
1986
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
1987
+ precision highp float;
1988
+ #else
247
1989
  precision mediump float;
1990
+ #endif
248
1991
  varying vec2 v_uv;
249
1992
  uniform sampler2D u_tex;
250
1993
  uniform vec2 u_resolution;
251
1994
  uniform vec2 u_textureResolution;
252
1995
  uniform vec4 u_bounds;
253
1996
  uniform float u_refraction;
1997
+ uniform float u_aberration;
254
1998
  uniform float u_bevelDepth;
255
1999
  uniform float u_bevelWidth;
256
2000
  uniform float u_frost;
@@ -262,31 +2006,46 @@ const liquidGL = (() => {
262
2006
  uniform float u_tiltX;
263
2007
  uniform float u_tiltY;
264
2008
  uniform float u_magnify;
2009
+ uniform vec2 u_subpixel;
2010
+ uniform vec2 u_boxSize;
265
2011
 
266
2012
  float udRoundBox( vec2 p, vec2 b, float r ) {
267
2013
  return length(max(abs(p)-b+r,0.0))-r;
268
2014
  }
269
2015
 
2016
+ vec2 cornerNormal( vec2 p, vec2 b, float r, vec2 fallback ) {
2017
+ vec2 q = abs(p) - b + r;
2018
+ vec2 m = max(q, 0.0);
2019
+ float l = length(m);
2020
+ if (l <= 0.0) return fallback;
2021
+ float w = smoothstep(0.0, max(r * 0.5, 1.0), min(m.x, m.y));
2022
+ if (w <= 0.0) return fallback;
2023
+ vec2 s = vec2(p.x < 0.0 ? -1.0 : 1.0, p.y < 0.0 ? -1.0 : 1.0);
2024
+ return normalize(mix(fallback, s * (m / l), w));
2025
+ }
2026
+
270
2027
  float random(vec2 st) {
271
2028
  return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
272
2029
  }
273
2030
 
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;
2031
+ float edgeFactor(vec2 p_px, vec2 b_px, float radius_px){
277
2032
  float d = -udRoundBox(p_px, b_px, radius_px);
278
- float bevel_px = u_bevelWidth * min(u_resolution.x, u_resolution.y);
2033
+ float bevel_px = u_bevelWidth * min(u_boxSize.x, u_boxSize.y);
279
2034
  return 1.0 - smoothstep(0.0, bevel_px, d);
280
2035
  }
281
2036
  void main(){
282
2037
  vec2 p = v_uv - 0.5;
283
2038
  p.x *= u_resolution.x / u_resolution.y;
284
2039
 
285
- float edge = edgeFactor(v_uv, u_radius);
2040
+ vec2 p_px = v_uv * u_resolution - u_subpixel - 0.5 * u_boxSize;
2041
+ vec2 b_px = 0.5 * u_boxSize;
2042
+
2043
+ float edge = edgeFactor(p_px, b_px, u_radius);
286
2044
  float min_dimension = min(u_resolution.x, u_resolution.y);
287
2045
  float offsetAmt = (edge * u_refraction + pow(edge, 10.0) * u_bevelDepth);
288
2046
  float centreBlend = smoothstep(0.15, 0.45, length(p));
289
- vec2 offset = normalize(p) * offsetAmt * centreBlend;
2047
+ vec2 refractDir = cornerNormal(p_px, b_px, u_radius, normalize(p));
2048
+ vec2 offset = refractDir * offsetAmt * centreBlend;
290
2049
 
291
2050
  float tiltRefractionScale = 0.05;
292
2051
  vec2 tiltOffset = vec2(tan(radians(u_tiltY)), -tan(radians(u_tiltX))) * tiltRefractionScale;
@@ -326,6 +2085,12 @@ const liquidGL = (() => {
326
2085
  refrCol /= 5.0;
327
2086
  }
328
2087
 
2088
+ if (u_aberration > 0.0) {
2089
+ vec2 chroma = offset * u_aberration;
2090
+ refrCol.r = texture2D(u_tex, sampleUV - chroma).r;
2091
+ refrCol.b = texture2D(u_tex, sampleUV + chroma).b;
2092
+ }
2093
+
329
2094
  if (refrCol.a < 0.1) {
330
2095
  refrCol = baseCol;
331
2096
  }
@@ -336,10 +2101,8 @@ const liquidGL = (() => {
336
2101
 
337
2102
  vec4 final = refrCol;
338
2103
 
339
- vec2 p_px = (v_uv - 0.5) * u_resolution;
340
- vec2 b_px = 0.5 * u_resolution;
341
2104
  float dmask = udRoundBox(p_px, b_px, u_radius);
342
- float inShape = 1.0 - step(0.0, dmask);
2105
+ float inShape = 1.0 - smoothstep(-0.5, 0.5, dmask);
343
2106
 
344
2107
  if (u_specular) {
345
2108
  vec2 lp1 = vec2(sin(u_time*0.2), cos(u_time*0.3))*0.6 + 0.5;
@@ -370,22 +2133,26 @@ const liquidGL = (() => {
370
2133
  gl.bufferData(
371
2134
  gl.ARRAY_BUFFER,
372
2135
  new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
373
- gl.STATIC_DRAW
2136
+ gl.STATIC_DRAW,
374
2137
  );
375
2138
 
376
2139
  const posLoc = gl.getAttribLocation(this.program, "a_position");
377
2140
  gl.enableVertexAttribArray(posLoc);
378
2141
  gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
379
2142
 
2143
+ this._posBuf = posBuf;
2144
+ this._posLoc = posLoc;
2145
+
380
2146
  this.u = {
381
2147
  tex: gl.getUniformLocation(this.program, "u_tex"),
382
2148
  res: gl.getUniformLocation(this.program, "u_resolution"),
383
2149
  textureResolution: gl.getUniformLocation(
384
2150
  this.program,
385
- "u_textureResolution"
2151
+ "u_textureResolution",
386
2152
  ),
387
2153
  bounds: gl.getUniformLocation(this.program, "u_bounds"),
388
2154
  refraction: gl.getUniformLocation(this.program, "u_refraction"),
2155
+ aberration: gl.getUniformLocation(this.program, "u_aberration"),
389
2156
  bevelDepth: gl.getUniformLocation(this.program, "u_bevelDepth"),
390
2157
  bevelWidth: gl.getUniformLocation(this.program, "u_bevelWidth"),
391
2158
  frost: gl.getUniformLocation(this.program, "u_frost"),
@@ -397,6 +2164,8 @@ const liquidGL = (() => {
397
2164
  tiltX: gl.getUniformLocation(this.program, "u_tiltX"),
398
2165
  tiltY: gl.getUniformLocation(this.program, "u_tiltY"),
399
2166
  magnify: gl.getUniformLocation(this.program, "u_magnify"),
2167
+ subpixel: gl.getUniformLocation(this.program, "u_subpixel"),
2168
+ boxSize: gl.getUniformLocation(this.program, "u_boxSize"),
400
2169
  };
401
2170
  }
402
2171
 
@@ -412,7 +2181,7 @@ const liquidGL = (() => {
412
2181
 
413
2182
  /* ----------------------------- */
414
2183
  async captureSnapshot() {
415
- if (this._capturing || typeof html2canvas === "undefined") return;
2184
+ if (this._capturing) return;
416
2185
  this._capturing = true;
417
2186
 
418
2187
  const undos = [];
@@ -420,7 +2189,7 @@ const liquidGL = (() => {
420
2189
  const attemptCapture = async (
421
2190
  attempt = 1,
422
2191
  maxAttempts = 3,
423
- delayMs = 500
2192
+ delayMs = 500,
424
2193
  ) => {
425
2194
  try {
426
2195
  const fullW = this.snapshotTarget.scrollWidth;
@@ -432,13 +2201,24 @@ const liquidGL = (() => {
432
2201
  let scale = Math.min(
433
2202
  this._snapshotResolution,
434
2203
  maxTex / fullW,
435
- maxTex / fullH
2204
+ maxTex / fullH,
436
2205
  );
437
2206
 
438
2207
  if (isMobileSafari) {
439
2208
  const over = (Math.max(fullW, fullH) * scale) / MAX_MOBILE_DIM;
440
2209
  if (over > 1) scale = scale / over;
441
2210
  }
2211
+
2212
+ const maxArea = isMobileSafari ? 4096 * 4096 : 16384 * 16384;
2213
+ if (fullW * fullH * scale * scale > maxArea) {
2214
+ scale = Math.sqrt(maxArea / (fullW * fullH));
2215
+ console.warn(
2216
+ `liquidGL: snapshot area capped, resolution reduced to ${scale.toFixed(
2217
+ 3,
2218
+ )} for a ${fullW}x${fullH} document.`,
2219
+ );
2220
+ }
2221
+
442
2222
  this.scaleFactor = Math.max(0.1, scale);
443
2223
 
444
2224
  this.canvas.style.visibility = "hidden";
@@ -448,9 +2228,16 @@ const liquidGL = (() => {
448
2228
  .flatMap((lens) => [lens.el, lens._shadowEl])
449
2229
  .filter(Boolean);
450
2230
 
2231
+ lensElements.forEach((el) => {
2232
+ el.setAttribute("data-liquidgl-hide", "");
2233
+ undos.push(() => {
2234
+ el.removeAttribute("data-liquidgl-hide");
2235
+ });
2236
+ });
2237
+
451
2238
  const ignoreElementsFunc = (element) => {
452
2239
  if (!element || !element.hasAttribute) return false;
453
- if (element === this.canvas || lensElements.includes(element)) {
2240
+ if (element === this.canvas) {
454
2241
  return true;
455
2242
  }
456
2243
  const style = window.getComputedStyle(element);
@@ -463,26 +2250,29 @@ const liquidGL = (() => {
463
2250
  );
464
2251
  };
465
2252
 
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
- });
2253
+ const naughtyIgnore = (el) =>
2254
+ ignoreElementsFunc(el) ||
2255
+ (el.hasAttribute && el.hasAttribute("data-liquidgl-hide"));
2256
+
2257
+ const snapCanvas = await NaughtyDOM.rasteriseAsync(
2258
+ this.snapshotTarget,
2259
+ {
2260
+ width: fullW,
2261
+ height: fullH,
2262
+ scale: scale,
2263
+ ignoreElements: naughtyIgnore,
2264
+ },
2265
+ );
478
2266
 
479
- this._uploadTexture(snapCanvas);
2267
+ if (!this._uploadTexture(snapCanvas)) {
2268
+ throw new Error("liquidGL: snapshot could not be uploaded.");
2269
+ }
480
2270
  return true;
481
2271
  } catch (e) {
482
2272
  console.error("liquidGL snapshot failed on attempt " + attempt, e);
483
2273
  if (attempt < maxAttempts) {
484
2274
  console.log(
485
- `Retrying snapshot capture (${attempt + 1}/${maxAttempts})...`
2275
+ `Retrying snapshot capture (${attempt + 1}/${maxAttempts})...`,
486
2276
  );
487
2277
  await new Promise((resolve) => setTimeout(resolve, delayMs));
488
2278
  return await attemptCapture(attempt + 1, maxAttempts, delayMs);
@@ -503,27 +2293,39 @@ const liquidGL = (() => {
503
2293
 
504
2294
  /* ----------------------------- */
505
2295
  _uploadTexture(srcCanvas) {
506
- if (!srcCanvas) return;
2296
+ if (!srcCanvas) {
2297
+ console.error("liquidGL: snapshot produced no canvas.");
2298
+ return false;
2299
+ }
507
2300
 
508
2301
  if (!(srcCanvas instanceof HTMLCanvasElement)) {
509
2302
  const tmp = document.createElement("canvas");
510
2303
  tmp.width = srcCanvas.width || 0;
511
2304
  tmp.height = srcCanvas.height || 0;
512
- if (tmp.width === 0 || tmp.height === 0) return;
2305
+ if (tmp.width === 0 || tmp.height === 0) {
2306
+ console.error("liquidGL: snapshot canvas has zero dimensions.");
2307
+ return false;
2308
+ }
513
2309
  try {
514
2310
  const ctx = tmp.getContext("2d");
515
2311
  ctx.drawImage(srcCanvas, 0, 0);
516
2312
  srcCanvas = tmp;
517
2313
  } catch (e) {
518
- console.warn(
2314
+ console.error(
519
2315
  "liquidGL: Unable to convert OffscreenCanvas for upload",
520
- e
2316
+ e,
521
2317
  );
522
- return;
2318
+ return false;
523
2319
  }
524
2320
  }
525
2321
 
526
- if (srcCanvas.width === 0 || srcCanvas.height === 0) return;
2322
+ if (srcCanvas.width === 0 || srcCanvas.height === 0) {
2323
+ console.error(
2324
+ "liquidGL: snapshot canvas has zero dimensions, requested area " +
2325
+ `${srcCanvas.width}x${srcCanvas.height} exceeds a browser limit.`,
2326
+ );
2327
+ return false;
2328
+ }
527
2329
  this.staticSnapshotCanvas = srcCanvas;
528
2330
  const gl = this.gl;
529
2331
  if (!this.texture) this.texture = gl.createTexture();
@@ -535,7 +2337,7 @@ const liquidGL = (() => {
535
2337
  gl.RGBA,
536
2338
  gl.RGBA,
537
2339
  gl.UNSIGNED_BYTE,
538
- srcCanvas
2340
+ srcCanvas,
539
2341
  );
540
2342
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
541
2343
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
@@ -545,12 +2347,18 @@ const liquidGL = (() => {
545
2347
  this.textureWidth = srcCanvas.width;
546
2348
  this.textureHeight = srcCanvas.height;
547
2349
 
2350
+ if (this._videoFrameState) this._videoFrameState = new WeakMap();
2351
+
2352
+ this._vFboTexture = null;
2353
+
548
2354
  this.render();
549
2355
 
550
2356
  if (this._pendingReveal.length) {
551
2357
  this._pendingReveal.forEach((ln) => ln._reveal());
552
2358
  this._pendingReveal.length = 0;
553
2359
  }
2360
+
2361
+ return true;
554
2362
  }
555
2363
 
556
2364
  /* ----------------------------- */
@@ -594,6 +2402,8 @@ const liquidGL = (() => {
594
2402
 
595
2403
  this._updateDynamicNodes();
596
2404
 
2405
+ this._frameSnapRect = this.snapshotTarget.getBoundingClientRect();
2406
+
597
2407
  this.lenses.forEach((lens) => {
598
2408
  lens.updateMetrics();
599
2409
  if (lens._mirrorActive && lens._mirrorClipUpdater) {
@@ -624,15 +2434,15 @@ const liquidGL = (() => {
624
2434
  const x = Math.max(0, Math.round(left * dpr) - expand);
625
2435
  const y = Math.max(
626
2436
  0,
627
- Math.round(this.canvas.height - (top + height) * dpr) - expand
2437
+ Math.round(this.canvas.height - (top + height) * dpr) - expand,
628
2438
  );
629
2439
  const w = Math.min(
630
2440
  this.canvas.width - x,
631
- Math.round(width * dpr) + expand * 2
2441
+ Math.round(width * dpr) + expand * 2,
632
2442
  );
633
2443
  const h = Math.min(
634
2444
  this.canvas.height - y,
635
- Math.round(height * dpr) + expand * 2
2445
+ Math.round(height * dpr) + expand * 2,
636
2446
  );
637
2447
  if (w > 0 && h > 0) {
638
2448
  gl.enable(gl.SCISSOR_TEST);
@@ -656,22 +2466,29 @@ const liquidGL = (() => {
656
2466
  let overscrollY = 0;
657
2467
  let overscrollX = 0;
658
2468
 
659
- if (window.visualViewport) {
660
- overscrollX = window.visualViewport.offsetLeft;
661
- overscrollY = window.visualViewport.offsetTop;
2469
+ const vv = window.visualViewport;
2470
+ if (vv && Math.abs(vv.scale - 1) < 0.01) {
2471
+ overscrollX = vv.offsetLeft;
2472
+ overscrollY = vv.offsetTop;
662
2473
  }
663
2474
 
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;
2475
+ const leftPx = (rect.left + overscrollX) * dpr;
2476
+ const topPx = (rect.top + overscrollY) * dpr;
2477
+ const x = Math.round(leftPx);
2478
+ const yTop = Math.round(topPx);
2479
+ const w = Math.round(leftPx + rect.width * dpr) - x;
2480
+ const h = Math.round(topPx + rect.height * dpr) - yTop;
2481
+ const y = this.canvas.height - (yTop + h);
669
2482
 
670
2483
  gl.viewport(x, y, w, h);
671
2484
  gl.uniform2f(this.u.res, w, h);
2485
+ gl.uniform2f(this.u.subpixel, leftPx - x, topPx - yTop);
2486
+ gl.uniform2f(this.u.boxSize, rect.width * dpr, rect.height * dpr);
672
2487
 
673
- const docX = rect.left - this.snapshotTarget.getBoundingClientRect().left;
674
- const docY = rect.top - this.snapshotTarget.getBoundingClientRect().top;
2488
+ const snapRect =
2489
+ this._frameSnapRect || this.snapshotTarget.getBoundingClientRect();
2490
+ const docX = rect.left - snapRect.left;
2491
+ const docY = rect.top - snapRect.top;
675
2492
  const leftUV = (docX * this.scaleFactor) / this.textureWidth;
676
2493
  const topUV = (docY * this.scaleFactor) / this.textureHeight;
677
2494
  const wUV = (rect.width * this.scaleFactor) / this.textureWidth;
@@ -681,9 +2498,10 @@ const liquidGL = (() => {
681
2498
  gl.uniform2f(
682
2499
  this.u.textureResolution,
683
2500
  this.textureWidth,
684
- this.textureHeight
2501
+ this.textureHeight,
685
2502
  );
686
2503
  gl.uniform1f(this.u.refraction, lens.options.refraction);
2504
+ gl.uniform1f(this.u.aberration, lens.options.aberration || 0);
687
2505
  gl.uniform1f(this.u.bevelDepth, lens.options.bevelDepth);
688
2506
  gl.uniform1f(this.u.bevelWidth, lens.options.bevelWidth);
689
2507
  gl.uniform1f(this.u.frost, lens.options.frost);
@@ -696,8 +2514,8 @@ const liquidGL = (() => {
696
2514
  0.001,
697
2515
  Math.min(
698
2516
  3.0,
699
- lens.options.magnify !== undefined ? lens.options.magnify : 1.0
700
- )
2517
+ lens.options.magnify !== undefined ? lens.options.magnify : 1.0,
2518
+ ),
701
2519
  );
702
2520
  gl.uniform1f(this.u.magnify, mag);
703
2521
 
@@ -722,6 +2540,166 @@ const liquidGL = (() => {
722
2540
  ctx.closePath();
723
2541
  }
724
2542
 
2543
+ _initVideoBlit() {
2544
+ if (this._vBlitReady !== undefined) return this._vBlitReady;
2545
+
2546
+ const gl = this.gl;
2547
+
2548
+ const vs = `
2549
+ attribute vec2 a_position;
2550
+ varying vec2 v_uv;
2551
+ void main(){
2552
+ v_uv = (a_position + 1.0) * 0.5;
2553
+ gl_Position = vec4(a_position, 0.0, 1.0);
2554
+ }`;
2555
+
2556
+ const fs = `
2557
+ precision mediump float;
2558
+ varying vec2 v_uv;
2559
+ uniform sampler2D u_src;
2560
+ uniform vec4 u_srcRect;
2561
+ void main(){
2562
+ gl_FragColor = texture2D(u_src, u_srcRect.xy + v_uv * u_srcRect.zw);
2563
+ }`;
2564
+
2565
+ const prog = createProgram(gl, vs, fs);
2566
+ if (!prog) {
2567
+ this._vBlitReady = false;
2568
+ return false;
2569
+ }
2570
+
2571
+ this._vProg = prog;
2572
+ this._vPosLoc = gl.getAttribLocation(prog, "a_position");
2573
+ this._vU = {
2574
+ src: gl.getUniformLocation(prog, "u_src"),
2575
+ srcRect: gl.getUniformLocation(prog, "u_srcRect"),
2576
+ };
2577
+
2578
+ this._vTex = gl.createTexture();
2579
+ gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2580
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2581
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2582
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2583
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2584
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2585
+
2586
+ this._vFbo = gl.createFramebuffer();
2587
+ this._vFboTexture = null;
2588
+
2589
+ this._vBlitReady = true;
2590
+ return true;
2591
+ }
2592
+
2593
+ _videoIsOpaque(vid) {
2594
+ if (!vid.videoWidth || !vid.videoHeight) return false;
2595
+
2596
+ const key = vid.videoWidth + "x" + vid.videoHeight;
2597
+ const cached = this._videoAlphaState.get(vid);
2598
+ if (cached && cached.key === key) return cached.opaque;
2599
+
2600
+ let opaque = false;
2601
+ try {
2602
+ const probe =
2603
+ this._alphaProbeCanvas ||
2604
+ (this._alphaProbeCanvas = document.createElement("canvas"));
2605
+ const size = 32;
2606
+ probe.width = size;
2607
+ probe.height = size;
2608
+ const ctx = probe.getContext("2d", { willReadFrequently: true });
2609
+ ctx.clearRect(0, 0, size, size);
2610
+ ctx.drawImage(vid, 0, 0, size, size);
2611
+ const data = ctx.getImageData(0, 0, size, size).data;
2612
+ opaque = true;
2613
+ for (let i = 3; i < data.length; i += 4) {
2614
+ if (data[i] !== 255) {
2615
+ opaque = false;
2616
+ break;
2617
+ }
2618
+ }
2619
+ } catch (e) {
2620
+ opaque = false;
2621
+ }
2622
+
2623
+ this._videoAlphaState.set(vid, { key, opaque });
2624
+ return opaque;
2625
+ }
2626
+
2627
+ _blitVideoToTexture(vid, dstX, dstY, dstW, dstH, srcRect) {
2628
+ const gl = this.gl;
2629
+
2630
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._vFbo);
2631
+
2632
+ if (this._vFboTexture !== this.texture) {
2633
+ gl.framebufferTexture2D(
2634
+ gl.FRAMEBUFFER,
2635
+ gl.COLOR_ATTACHMENT0,
2636
+ gl.TEXTURE_2D,
2637
+ this.texture,
2638
+ 0,
2639
+ );
2640
+ if (
2641
+ gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
2642
+ ) {
2643
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2644
+ this._vBlitReady = false;
2645
+ return false;
2646
+ }
2647
+ this._vFboTexture = this.texture;
2648
+ }
2649
+
2650
+ gl.activeTexture(gl.TEXTURE0);
2651
+ gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2652
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2653
+
2654
+ try {
2655
+ gl.texImage2D(
2656
+ gl.TEXTURE_2D,
2657
+ 0,
2658
+ gl.RGBA,
2659
+ gl.RGBA,
2660
+ gl.UNSIGNED_BYTE,
2661
+ vid,
2662
+ );
2663
+ } catch (e) {
2664
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2665
+ this._restoreLensProgramState();
2666
+ return false;
2667
+ }
2668
+
2669
+ gl.useProgram(this._vProg);
2670
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2671
+ gl.enableVertexAttribArray(this._vPosLoc);
2672
+ gl.vertexAttribPointer(this._vPosLoc, 2, gl.FLOAT, false, 0, 0);
2673
+
2674
+ gl.uniform1i(this._vU.src, 0);
2675
+ gl.uniform4f(
2676
+ this._vU.srcRect,
2677
+ srcRect.u,
2678
+ srcRect.v,
2679
+ srcRect.uw,
2680
+ srcRect.vh,
2681
+ );
2682
+
2683
+ gl.viewport(dstX, dstY, dstW, dstH);
2684
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2685
+
2686
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2687
+ this._restoreLensProgramState();
2688
+
2689
+ return true;
2690
+ }
2691
+
2692
+ _restoreLensProgramState() {
2693
+ const gl = this.gl;
2694
+ gl.useProgram(this.program);
2695
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2696
+ gl.enableVertexAttribArray(this._posLoc);
2697
+ gl.vertexAttribPointer(this._posLoc, 2, gl.FLOAT, false, 0, 0);
2698
+ gl.activeTexture(gl.TEXTURE0);
2699
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2700
+ gl.uniform1i(this.u.tex, 0);
2701
+ }
2702
+
725
2703
  /* ----------------------------- */
726
2704
  _updateDynamicVideos() {
727
2705
  if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
@@ -755,6 +2733,76 @@ const liquidGL = (() => {
755
2733
 
756
2734
  if (drawW <= 0 || drawH <= 0) return;
757
2735
 
2736
+ const geomKey =
2737
+ Math.round(texX) + "," + Math.round(texY) + "," + drawW + "," + drawH;
2738
+ const prevState = this._videoFrameState.get(vid);
2739
+ if (
2740
+ prevState &&
2741
+ prevState.time === vid.currentTime &&
2742
+ prevState.geom === geomKey
2743
+ ) {
2744
+ return;
2745
+ }
2746
+ this._videoFrameState.set(vid, {
2747
+ time: vid.currentTime,
2748
+ geom: geomKey,
2749
+ });
2750
+
2751
+ const drawX = Math.round(texX);
2752
+ const drawY = Math.round(texY);
2753
+
2754
+ const maxW = this.textureWidth;
2755
+ const maxH = this.textureHeight;
2756
+ let dstX = drawX;
2757
+ let dstY = drawY;
2758
+ let srcX = 0,
2759
+ srcY = 0,
2760
+ updW = drawW,
2761
+ updH = drawH;
2762
+
2763
+ if (dstX < 0) {
2764
+ srcX = -dstX;
2765
+ updW += dstX;
2766
+ dstX = 0;
2767
+ }
2768
+ if (dstY < 0) {
2769
+ srcY = -dstY;
2770
+ updH += dstY;
2771
+ dstY = 0;
2772
+ }
2773
+
2774
+ if (dstX + updW > maxW) {
2775
+ updW = maxW - dstX;
2776
+ }
2777
+ if (dstY + updH > maxH) {
2778
+ updH = maxH - dstY;
2779
+ }
2780
+
2781
+ if (updW <= 0 || updH <= 0) return;
2782
+
2783
+ const style = window.getComputedStyle(vid);
2784
+ const scaledRadii = {
2785
+ tl: parseFloat(style.borderTopLeftRadius) * this.scaleFactor,
2786
+ tr: parseFloat(style.borderTopRightRadius) * this.scaleFactor,
2787
+ br: parseFloat(style.borderBottomRightRadius) * this.scaleFactor,
2788
+ bl: parseFloat(style.borderBottomLeftRadius) * this.scaleFactor,
2789
+ };
2790
+ const isRounded = Object.values(scaledRadii).some((r) => r > 0);
2791
+
2792
+ if (
2793
+ !isRounded &&
2794
+ this._initVideoBlit() &&
2795
+ this._videoIsOpaque(vid) &&
2796
+ this._blitVideoToTexture(vid, dstX, dstY, updW, updH, {
2797
+ u: srcX / drawW,
2798
+ v: srcY / drawH,
2799
+ uw: updW / drawW,
2800
+ vh: updH / drawH,
2801
+ })
2802
+ ) {
2803
+ return;
2804
+ }
2805
+
758
2806
  if (
759
2807
  this._tmpCanvas.width !== drawW ||
760
2808
  this._tmpCanvas.height !== drawH
@@ -767,20 +2815,12 @@ const liquidGL = (() => {
767
2815
  this._tmpCtx.save();
768
2816
  this._tmpCtx.clearRect(0, 0, drawW, drawH);
769
2817
 
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)) {
2818
+ if (isRounded) {
779
2819
  this._createRoundedRectPath(
780
2820
  this._tmpCtx,
781
2821
  drawW,
782
2822
  drawH,
783
- scaledRadii
2823
+ scaledRadii,
784
2824
  );
785
2825
  this._tmpCtx.clip();
786
2826
  }
@@ -794,7 +2834,7 @@ const liquidGL = (() => {
794
2834
  0,
795
2835
  0,
796
2836
  drawW,
797
- drawH
2837
+ drawH,
798
2838
  );
799
2839
 
800
2840
  this._tmpCtx.drawImage(vid, 0, 0, drawW, drawH);
@@ -804,40 +2844,6 @@ const liquidGL = (() => {
804
2844
  return;
805
2845
  }
806
2846
 
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
2847
  gl.bindTexture(gl.TEXTURE_2D, this.texture);
842
2848
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
843
2849
  gl.texSubImage2D(
@@ -847,7 +2853,7 @@ const liquidGL = (() => {
847
2853
  dstY,
848
2854
  gl.RGBA,
849
2855
  gl.UNSIGNED_BYTE,
850
- this._tmpCanvas
2856
+ this._tmpCanvas,
851
2857
  );
852
2858
  });
853
2859
  }
@@ -894,7 +2900,7 @@ const liquidGL = (() => {
894
2900
  xInComposite,
895
2901
  yInComposite,
896
2902
  wInComposite,
897
- hInComposite
2903
+ hInComposite,
898
2904
  );
899
2905
  }
900
2906
  });
@@ -908,27 +2914,37 @@ const liquidGL = (() => {
908
2914
  if (meta.needsRecapture && !meta._capturing && !this._isScrolling) {
909
2915
  meta._capturing = true;
910
2916
 
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;
2917
+ const ignoreDynamic = (n) =>
2918
+ n.tagName === "CANVAS" || n.hasAttribute("data-liquid-ignore");
2919
+
2920
+ if (this._naughtyQueued) {
2921
+ meta._capturing = false;
2922
+ } else {
2923
+ this._naughtyQueued = true;
2924
+ const capture = () => {
2925
+ this._naughtyQueued = false;
2926
+ try {
2927
+ const cv = NaughtyDOM.rasterise(el, {
2928
+ scale: this.scaleFactor,
2929
+ ignoreElements: ignoreDynamic,
2930
+ canvas: meta.naughtyCanvas,
2931
+ });
2932
+ meta.naughtyCanvas = cv;
2933
+ if (cv.width > 0 && cv.height > 0) {
2934
+ meta.lastCapture = cv;
2935
+ meta.needsRecapture = false;
2936
+ }
2937
+ } catch (e) {
2938
+ console.error("liquidGL: Dynamic element capture failed.", e);
924
2939
  }
925
- })
926
- .catch((e) => {
927
- console.error("liquidGL: Dynamic element capture failed.", e);
928
- })
929
- .finally(() => {
930
2940
  meta._capturing = false;
931
- });
2941
+ };
2942
+ if (typeof requestIdleCallback === "function") {
2943
+ requestIdleCallback(capture, { timeout: 100 });
2944
+ } else {
2945
+ setTimeout(capture, 0);
2946
+ }
2947
+ }
932
2948
  }
933
2949
 
934
2950
  if (meta.lastCapture) {
@@ -949,7 +2965,7 @@ const liquidGL = (() => {
949
2965
  0,
950
2966
  0,
951
2967
  w,
952
- h
2968
+ h,
953
2969
  );
954
2970
  gl.bindTexture(gl.TEXTURE_2D, this.texture);
955
2971
  gl.texSubImage2D(
@@ -959,7 +2975,7 @@ const liquidGL = (() => {
959
2975
  y,
960
2976
  gl.RGBA,
961
2977
  gl.UNSIGNED_BYTE,
962
- eraseCanvas
2978
+ eraseCanvas,
963
2979
  );
964
2980
  }
965
2981
  }
@@ -1037,7 +3053,7 @@ const liquidGL = (() => {
1037
3053
  0,
1038
3054
  0,
1039
3055
  drawW,
1040
- drawH
3056
+ drawH,
1041
3057
  );
1042
3058
  compositeVideos(this._compositeCtx, rect);
1043
3059
 
@@ -1046,7 +3062,7 @@ const liquidGL = (() => {
1046
3062
  this._compositeCtx.translate(drawW / 2, drawH / 2);
1047
3063
  if (style.transform !== "none") {
1048
3064
  this._compositeCtx.transform(
1049
- ...this._parseTransform(style.transform)
3065
+ ...this._parseTransform(style.transform),
1050
3066
  );
1051
3067
  }
1052
3068
  this._compositeCtx.translate(-drawW / 2, -drawH / 2);
@@ -1062,7 +3078,7 @@ const liquidGL = (() => {
1062
3078
  dstY,
1063
3079
  gl.RGBA,
1064
3080
  gl.UNSIGNED_BYTE,
1065
- compositeCanvas
3081
+ compositeCanvas,
1066
3082
  );
1067
3083
 
1068
3084
  if (this._workerEnabled && meta._heavyAnim) {
@@ -1080,7 +3096,7 @@ const liquidGL = (() => {
1080
3096
  dstX,
1081
3097
  dstY,
1082
3098
  updW,
1083
- updH
3099
+ updH,
1084
3100
  ),
1085
3101
  createImageBitmap(meta.lastCapture),
1086
3102
  ]).then(([snapBmp, dynBmp]) => {
@@ -1092,7 +3108,7 @@ const liquidGL = (() => {
1092
3108
  snap: snapBmp,
1093
3109
  dyn: dynBmp,
1094
3110
  },
1095
- [snapBmp, dynBmp]
3111
+ [snapBmp, dynBmp],
1096
3112
  );
1097
3113
  });
1098
3114
  meta.prevDrawRect = { x: dstX, y: dstY, w: updW, h: updH };
@@ -1152,6 +3168,7 @@ const liquidGL = (() => {
1152
3168
  _capturing: false,
1153
3169
  prevDrawRect: null,
1154
3170
  lastCapture: null,
3171
+ naughtyCanvas: null,
1155
3172
  needsRecapture: true,
1156
3173
  hoverClassName: null,
1157
3174
  _animating: false,
@@ -1217,7 +3234,7 @@ const liquidGL = (() => {
1217
3234
  try {
1218
3235
  this._dynamicStyleSheet.insertRule(
1219
3236
  rule,
1220
- this._dynamicStyleSheet.cssRules.length
3237
+ this._dynamicStyleSheet.cssRules.length,
1221
3238
  );
1222
3239
  m.hoverClassName = className;
1223
3240
  el.classList.add(className);
@@ -1227,7 +3244,7 @@ const liquidGL = (() => {
1227
3244
  }
1228
3245
  setDirty();
1229
3246
  },
1230
- { passive: true }
3247
+ { passive: true },
1231
3248
  );
1232
3249
 
1233
3250
  el.addEventListener("mouseleave", handleLeave, { passive: true });
@@ -1247,7 +3264,7 @@ const liquidGL = (() => {
1247
3264
  if (
1248
3265
  meta._heavyAnim &&
1249
3266
  !meta._capturing &&
1250
- ts - meta._lastCaptureTs > 33
3267
+ ts - meta._lastCaptureTs > RECAPTURE_INTERVAL_MS
1251
3268
  ) {
1252
3269
  meta._lastCaptureTs = ts;
1253
3270
  meta.needsRecapture = true;
@@ -1293,7 +3310,7 @@ const liquidGL = (() => {
1293
3310
  if (m) m._heavyAnim = true;
1294
3311
  startRealtime();
1295
3312
  },
1296
- { passive: true }
3313
+ { passive: true },
1297
3314
  );
1298
3315
 
1299
3316
  el.addEventListener(
@@ -1305,7 +3322,7 @@ const liquidGL = (() => {
1305
3322
  if (!m._animating) startRealtime();
1306
3323
  }
1307
3324
  },
1308
- { passive: true }
3325
+ { passive: true },
1309
3326
  );
1310
3327
 
1311
3328
  const stopRealtime = () => {
@@ -1454,9 +3471,12 @@ const liquidGL = (() => {
1454
3471
  let overscrollY = 0;
1455
3472
  let overscrollX = 0;
1456
3473
 
1457
- if (window.visualViewport) {
1458
- overscrollX = -window.visualViewport.offsetLeft;
1459
- overscrollY = -window.visualViewport.offsetTop;
3474
+ const vv = window.visualViewport;
3475
+ if (vv) {
3476
+ if (Math.abs(vv.scale - 1) < 0.01) {
3477
+ overscrollX = -vv.offsetLeft;
3478
+ overscrollY = -vv.offsetTop;
3479
+ }
1460
3480
  } else {
1461
3481
  const bodyStyle = window.getComputedStyle(document.body);
1462
3482
  const htmlStyle = window.getComputedStyle(document.documentElement);
@@ -1641,23 +3661,47 @@ const liquidGL = (() => {
1641
3661
  const getMaxTilt = () =>
1642
3662
  Number.isFinite(this.options.tiltFactor) ? this.options.tiltFactor : 5;
1643
3663
 
3664
+ const getTiltEase = () =>
3665
+ Number.isFinite(this.options.tiltEase)
3666
+ ? Math.max(0, this.options.tiltEase)
3667
+ : 400;
3668
+
3669
+ const TILT_TRACK_MS = 120;
3670
+
3671
+ const setTiltTransition = (ms) => {
3672
+ const value = `transform ${ms}ms cubic-bezier(0.33,1,0.68,1)`;
3673
+ this.el.style.transition = value;
3674
+ if (this._mirror) this._mirror.style.transition = value;
3675
+ if (this._shadowEl) this._shadowEl.style.transition = value;
3676
+ };
3677
+
3678
+ this._clearTiltEnterTimer = () => {
3679
+ if (this._tiltEnterTimer) {
3680
+ clearTimeout(this._tiltEnterTimer);
3681
+ this._tiltEnterTimer = null;
3682
+ }
3683
+ };
3684
+
1644
3685
  this._applyTilt = (clientX, clientY) => {
1645
3686
  if (!this._tiltInteracting) {
1646
3687
  this._tiltInteracting = true;
1647
- this.el.style.transition =
1648
- "transform 0.12s cubic-bezier(0.33,1,0.68,1)";
1649
3688
  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)";
3689
+
3690
+ const enterMs = getTiltEase();
3691
+ setTiltTransition(enterMs);
3692
+
3693
+ this._tiltEntering = enterMs > 0;
3694
+ this._clearTiltEnterTimer();
3695
+ if (this._tiltEntering) {
3696
+ this._tiltEnterTimer = setTimeout(() => {
3697
+ this._tiltEntering = false;
3698
+ this._tiltEnterTimer = null;
3699
+ setTiltTransition(TILT_TRACK_MS);
3700
+ }, enterMs);
1657
3701
  }
1658
3702
  }
1659
3703
 
1660
- const r = this._baseRect || this.el.getBoundingClientRect();
3704
+ const r = this._baseRect || this._measureBaseRect();
1661
3705
  const cx = r.left + r.width / 2;
1662
3706
  const cy = r.top + r.height / 2;
1663
3707
 
@@ -1686,8 +3730,13 @@ const liquidGL = (() => {
1686
3730
 
1687
3731
  const transformStr = `${overscrollCompensation}${baseTransform}perspective(800px) rotateX(${rotX}deg) rotateY(${rotY}deg)`;
1688
3732
 
1689
- this.tiltX = rotX;
1690
- this.tiltY = rotY;
3733
+ if (this._tiltEntering) {
3734
+ this._easeTiltTo(rotX, rotY, getTiltEase());
3735
+ } else {
3736
+ this._cancelTiltEase();
3737
+ this.tiltX = rotX;
3738
+ this.tiltY = rotY;
3739
+ }
1691
3740
 
1692
3741
  this.el.style.transformOrigin = `50% 50%`;
1693
3742
  this.el.style.transform = transformStr;
@@ -1705,8 +3754,60 @@ const liquidGL = (() => {
1705
3754
  this.renderer.render();
1706
3755
  };
1707
3756
 
3757
+ this._cancelTiltEase = () => {
3758
+ if (this._tiltEaseRaf) {
3759
+ cancelAnimationFrame(this._tiltEaseRaf);
3760
+ this._tiltEaseRaf = null;
3761
+ }
3762
+ };
3763
+
3764
+ this._easeTiltTo = (toX, toY, duration) => {
3765
+ this._cancelTiltEase();
3766
+
3767
+ const fromX = this.tiltX;
3768
+ const fromY = this.tiltY;
3769
+ if (fromX === toX && fromY === toY) return;
3770
+
3771
+ if (!duration || duration <= 0) {
3772
+ this.tiltX = toX;
3773
+ this.tiltY = toY;
3774
+ this.renderer.render();
3775
+ return;
3776
+ }
3777
+
3778
+ const start =
3779
+ typeof performance !== "undefined" ? performance.now() : Date.now();
3780
+
3781
+ const step = (now) => {
3782
+ const elapsed =
3783
+ (typeof now === "number"
3784
+ ? now
3785
+ : typeof performance !== "undefined"
3786
+ ? performance.now()
3787
+ : Date.now()) - start;
3788
+ const t = Math.min(1, elapsed / duration);
3789
+ const eased = 1 - Math.pow(1 - t, 3);
3790
+
3791
+ this.tiltX = fromX + (toX - fromX) * eased;
3792
+ this.tiltY = fromY + (toY - fromY) * eased;
3793
+ this.renderer.render();
3794
+
3795
+ if (t < 1) {
3796
+ this._tiltEaseRaf = requestAnimationFrame(step);
3797
+ } else {
3798
+ this._tiltEaseRaf = null;
3799
+ this.tiltX = toX;
3800
+ this.tiltY = toY;
3801
+ this.renderer.render();
3802
+ }
3803
+ };
3804
+
3805
+ this._tiltEaseRaf = requestAnimationFrame(step);
3806
+ };
3807
+
1708
3808
  this._smoothReset = () => {
1709
- this.el.style.transition = "transform 0.4s cubic-bezier(0.33,1,0.68,1)";
3809
+ const restMs = getTiltEase();
3810
+ setTiltTransition(restMs);
1710
3811
  this.el.style.transformOrigin = `50% 50%`;
1711
3812
  const baseRest =
1712
3813
  this._savedTransform && this._savedTransform !== "none"
@@ -1726,13 +3827,11 @@ const liquidGL = (() => {
1726
3827
 
1727
3828
  this.el.style.transform = `${overscrollCompensation}${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1728
3829
 
1729
- this.tiltX = 0;
1730
- this.tiltY = 0;
1731
- this.renderer.render();
3830
+ this._tiltEntering = false;
3831
+ this._clearTiltEnterTimer();
3832
+ this._easeTiltTo(0, 0, getTiltEase());
1732
3833
 
1733
3834
  if (this._mirror) {
1734
- this._mirror.style.transition =
1735
- "transform 0.4s cubic-bezier(0.33, 1, 0.68, 1)";
1736
3835
  this._mirror.style.transformOrigin = this._pivotOrigin || "50% 50%";
1737
3836
  this._mirror.style.transform = `${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1738
3837
  const clean = () => {
@@ -1742,12 +3841,10 @@ const liquidGL = (() => {
1742
3841
  this._mirror.addEventListener("transitionend", clean, {
1743
3842
  once: true,
1744
3843
  });
1745
- this._resetCleanupTimer = setTimeout(clean, 350);
3844
+ this._resetCleanupTimer = setTimeout(clean, restMs + 50);
1746
3845
  }
1747
3846
 
1748
3847
  if (this._shadowEl) {
1749
- this._shadowEl.style.transition =
1750
- "transform 0.4s cubic-bezier(0.33,1,0.68,1)";
1751
3848
  this._shadowEl.style.transformOrigin = `50% 50%`;
1752
3849
  this._shadowEl.style.transform = `${baseRest}perspective(800px) rotateX(0deg) rotateY(0deg)`;
1753
3850
  }
@@ -1766,7 +3863,7 @@ const liquidGL = (() => {
1766
3863
  this._tiltInteracting = false;
1767
3864
  this._createMirrorCanvas();
1768
3865
 
1769
- const r = this._baseRect || this.el.getBoundingClientRect();
3866
+ const r = this._baseRect || this._measureBaseRect();
1770
3867
  const cx = r.left + r.width / 2;
1771
3868
  const cy = r.top + r.height / 2;
1772
3869
 
@@ -1853,6 +3950,11 @@ const liquidGL = (() => {
1853
3950
 
1854
3951
  _unbindTiltHandlers() {
1855
3952
  if (!this._tiltHandlersBound) return;
3953
+ if (this._cancelTiltEase) this._cancelTiltEase();
3954
+ if (this._clearTiltEnterTimer) this._clearTiltEnterTimer();
3955
+ this._tiltEntering = false;
3956
+ this.tiltX = 0;
3957
+ this.tiltY = 0;
1856
3958
  this.el.removeEventListener("mouseenter", this._onMouseEnter.bind(this));
1857
3959
  this.el.removeEventListener("mousemove", this._onMouseMove.bind(this));
1858
3960
  document.removeEventListener("mousemove", this._boundCheckLeave);
@@ -1872,8 +3974,26 @@ const liquidGL = (() => {
1872
3974
  this.renderer.render();
1873
3975
  }
1874
3976
 
3977
+ _measureBaseRect() {
3978
+ const tilted =
3979
+ this.tiltX !== 0 || this.tiltY !== 0 || !!this._tiltEaseRaf;
3980
+ if (!tilted) return this.el.getBoundingClientRect();
3981
+
3982
+ const prevTransition = this.el.style.transition;
3983
+ const prevTransform = this.el.style.transform;
3984
+
3985
+ this.el.style.transition = "none";
3986
+ this.el.style.transform = this._savedTransform || "";
3987
+ const rect = this.el.getBoundingClientRect();
3988
+
3989
+ this.el.style.transform = prevTransform;
3990
+ this.el.style.transition = prevTransition;
3991
+
3992
+ return rect;
3993
+ }
3994
+
1875
3995
  _createMirrorCanvas() {
1876
- this._baseRect = this.el.getBoundingClientRect();
3996
+ this._baseRect = this._measureBaseRect();
1877
3997
  if (this._mirror) return;
1878
3998
  this._mirror = document.createElement("canvas");
1879
3999
  Object.assign(this._mirror.style, {
@@ -1891,9 +4011,9 @@ const liquidGL = (() => {
1891
4011
 
1892
4012
  const updateClip = () => {
1893
4013
  if (this._mirrorActive) {
1894
- this._baseRect = this._baseRect || this.el.getBoundingClientRect();
4014
+ this._baseRect = this._baseRect || this._measureBaseRect();
1895
4015
  }
1896
- const r = this._baseRect || this.el.getBoundingClientRect();
4016
+ const r = this._baseRect || this._measureBaseRect();
1897
4017
  const radius = `${this.radiusCss}px`;
1898
4018
  this._mirror.style.clipPath = `inset(${r.top}px ${
1899
4019
  innerWidth - r.right
@@ -1928,12 +4048,13 @@ const liquidGL = (() => {
1928
4048
  /* --------------------------------------------------
1929
4049
  * Public API
1930
4050
  * ------------------------------------------------*/
1931
- const liquidGL = function (userOptions = {}) {
4051
+ window.liquidGL = function (userOptions = {}) {
1932
4052
  const defaults = {
1933
4053
  target: ".liquidGL",
1934
4054
  snapshot: "body",
1935
4055
  resolution: 2.0,
1936
4056
  refraction: 0.01,
4057
+ aberration: 0,
1937
4058
  bevelDepth: 0.08,
1938
4059
  bevelWidth: 0.15,
1939
4060
  frost: 0,
@@ -1942,6 +4063,7 @@ const liquidGL = (() => {
1942
4063
  reveal: "fade",
1943
4064
  tilt: false,
1944
4065
  tiltFactor: 5,
4066
+ tiltEase: 400,
1945
4067
  magnify: 1,
1946
4068
  on: {},
1947
4069
  };
@@ -1960,7 +4082,7 @@ const liquidGL = (() => {
1960
4082
 
1961
4083
  if (noWebGL) {
1962
4084
  console.warn(
1963
- "liquidGL: WebGL not available – falling back to CSS backdrop-filter."
4085
+ "liquidGL: WebGL not available – falling back to CSS backdrop-filter.",
1964
4086
  );
1965
4087
  const fallbackNodes = document.querySelectorAll(options.target);
1966
4088
  fallbackNodes.forEach((node) => {
@@ -1984,13 +4106,13 @@ const liquidGL = (() => {
1984
4106
  const nodeList = document.querySelectorAll(options.target);
1985
4107
  if (!nodeList || nodeList.length === 0) {
1986
4108
  console.warn(
1987
- `liquidGL: Target element(s) '${options.target}' not found.`
4109
+ `liquidGL: Target element(s) '${options.target}' not found.`,
1988
4110
  );
1989
4111
  return;
1990
4112
  }
1991
4113
 
1992
4114
  const instances = Array.from(nodeList).map((el) =>
1993
- renderer.addLens(el, options)
4115
+ renderer.addLens(el, options),
1994
4116
  );
1995
4117
 
1996
4118
  if (!renderer._rafId && !renderer.useExternalTicker) {
@@ -2007,7 +4129,7 @@ const liquidGL = (() => {
2007
4129
  /* --------------------------------------------------
2008
4130
  * Public helper: register elements that need live updates
2009
4131
  * ------------------------------------------------*/
2010
- liquidGL.registerDynamic = function (elements) {
4132
+ window.liquidGL.registerDynamic = function (elements) {
2011
4133
  const renderer = window.__liquidGLRenderer__;
2012
4134
  if (!renderer || !renderer.addDynamicElement) return;
2013
4135
  renderer.addDynamicElement(elements);
@@ -2019,19 +4141,19 @@ const liquidGL = (() => {
2019
4141
  /* --------------------------------------------------
2020
4142
  * Public helper: Universal smooth scroll / animation sync
2021
4143
  * ------------------------------------------------*/
2022
- liquidGL.syncWith = function (config = {}) {
4144
+ window.liquidGL.syncWith = function (config = {}) {
2023
4145
  const renderer = window.__liquidGLRenderer__;
2024
4146
  if (!renderer) {
2025
4147
  console.warn(
2026
- "liquidGL: Please initialize liquidGL *before* calling syncWith()."
4148
+ "liquidGL: Please initialize liquidGL *before* calling syncWith().",
2027
4149
  );
2028
4150
  return;
2029
4151
  }
2030
4152
 
2031
- const G = window.gsap;
4153
+ const G = config.gsap === false ? null : config.gsap || window.gsap;
2032
4154
  const L = window.Lenis;
2033
4155
  const LS = window.LocomotiveScroll;
2034
- const ST = G ? G.ScrollTrigger : null;
4156
+ const ST = G ? config.ScrollTrigger || G.ScrollTrigger : null;
2035
4157
 
2036
4158
  let lenis = config.lenis;
2037
4159
  let loco = config.locomotiveScroll;
@@ -2103,8 +4225,7 @@ const liquidGL = (() => {
2103
4225
 
2104
4226
  return { lenis, locomotiveScroll: loco };
2105
4227
  };
2106
-
2107
- return liquidGL;
4228
+ return window.liquidGL;
2108
4229
  })();
2109
4230
 
2110
4231
  export default liquidGL;