liquid-gl 2.0.0 → 2.0.2

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 +38 -18
  2. package/liquidGL.js +1814 -59
  3. package/package.json +1 -4
package/liquidGL.js CHANGED
@@ -4,11 +4,9 @@
4
4
  *
5
5
  * Author: NaughtyDuk© – https://liquidgl.naughtyduk.com
6
6
  * Licence: MIT
7
- * Version: v2.0.0
7
+ * Version: v2.0.2
8
8
  */
9
9
 
10
- import html2canvas from "html2canvas";
11
-
12
10
  const liquidGL = (() => {
13
11
  "use strict";
14
12
 
@@ -71,11 +69,1746 @@ const liquidGL = (() => {
71
69
  return p;
72
70
  }
73
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
+ })();
1805
+
74
1806
  /* --------------------------------------------------
75
1807
  * Shared renderer (one per page)
76
1808
  * ------------------------------------------------*/
77
1809
  class liquidGLRenderer {
78
1810
  constructor(snapshotSelector, snapshotResolution = 1.0) {
1811
+ this._naughtyQueued = false;
79
1812
  this.canvas = document.createElement("canvas");
80
1813
  this.canvas.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;`;
81
1814
  this.canvas.setAttribute("data-liquid-ignore", "");
@@ -273,6 +2006,8 @@ const liquidGL = (() => {
273
2006
  uniform float u_tiltX;
274
2007
  uniform float u_tiltY;
275
2008
  uniform float u_magnify;
2009
+ uniform vec2 u_subpixel;
2010
+ uniform vec2 u_boxSize;
276
2011
 
277
2012
  float udRoundBox( vec2 p, vec2 b, float r ) {
278
2013
  return length(max(abs(p)-b+r,0.0))-r;
@@ -293,27 +2028,23 @@ const liquidGL = (() => {
293
2028
  return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
294
2029
  }
295
2030
 
296
- float edgeFactor(vec2 uv, float radius_px){
297
- vec2 p_px = (uv - 0.5) * u_resolution;
298
- vec2 b_px = 0.5 * u_resolution;
2031
+ float edgeFactor(vec2 p_px, vec2 b_px, float radius_px){
299
2032
  float d = -udRoundBox(p_px, b_px, radius_px);
300
- 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);
301
2034
  return 1.0 - smoothstep(0.0, bevel_px, d);
302
2035
  }
303
2036
  void main(){
304
2037
  vec2 p = v_uv - 0.5;
305
2038
  p.x *= u_resolution.x / u_resolution.y;
306
2039
 
307
- 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);
308
2044
  float min_dimension = min(u_resolution.x, u_resolution.y);
309
2045
  float offsetAmt = (edge * u_refraction + pow(edge, 10.0) * u_bevelDepth);
310
2046
  float centreBlend = smoothstep(0.15, 0.45, length(p));
311
- vec2 refractDir = cornerNormal(
312
- (v_uv - 0.5) * u_resolution,
313
- 0.5 * u_resolution,
314
- u_radius,
315
- normalize(p)
316
- );
2047
+ vec2 refractDir = cornerNormal(p_px, b_px, u_radius, normalize(p));
317
2048
  vec2 offset = refractDir * offsetAmt * centreBlend;
318
2049
 
319
2050
  float tiltRefractionScale = 0.05;
@@ -370,10 +2101,8 @@ const liquidGL = (() => {
370
2101
 
371
2102
  vec4 final = refrCol;
372
2103
 
373
- vec2 p_px = (v_uv - 0.5) * u_resolution;
374
- vec2 b_px = 0.5 * u_resolution;
375
2104
  float dmask = udRoundBox(p_px, b_px, u_radius);
376
- float inShape = clamp(0.5 - dmask, 0.0, 1.0);
2105
+ float inShape = 1.0 - smoothstep(-0.5, 0.5, dmask);
377
2106
 
378
2107
  if (u_specular) {
379
2108
  vec2 lp1 = vec2(sin(u_time*0.2), cos(u_time*0.3))*0.6 + 0.5;
@@ -435,6 +2164,8 @@ const liquidGL = (() => {
435
2164
  tiltX: gl.getUniformLocation(this.program, "u_tiltX"),
436
2165
  tiltY: gl.getUniformLocation(this.program, "u_tiltY"),
437
2166
  magnify: gl.getUniformLocation(this.program, "u_magnify"),
2167
+ subpixel: gl.getUniformLocation(this.program, "u_subpixel"),
2168
+ boxSize: gl.getUniformLocation(this.program, "u_boxSize"),
438
2169
  };
439
2170
  }
440
2171
 
@@ -450,7 +2181,7 @@ const liquidGL = (() => {
450
2181
 
451
2182
  /* ----------------------------- */
452
2183
  async captureSnapshot() {
453
- if (this._capturing || typeof html2canvas === "undefined") return;
2184
+ if (this._capturing) return;
454
2185
  this._capturing = true;
455
2186
 
456
2187
  const undos = [];
@@ -519,25 +2250,19 @@ const liquidGL = (() => {
519
2250
  );
520
2251
  };
521
2252
 
522
- const snapCanvas = await html2canvas(this.snapshotTarget, {
523
- allowTaint: false,
524
- useCORS: true,
525
- backgroundColor: null,
526
- removeContainer: true,
527
- width: fullW,
528
- height: fullH,
529
- scrollX: 0,
530
- scrollY: 0,
531
- scale: scale,
532
- ignoreElements: ignoreElementsFunc,
533
- onclone: (clonedDoc) => {
534
- clonedDoc
535
- .querySelectorAll("[data-liquidgl-hide]")
536
- .forEach((el) => {
537
- el.style.visibility = "hidden";
538
- });
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,
539
2264
  },
540
- });
2265
+ );
541
2266
 
542
2267
  if (!this._uploadTexture(snapCanvas)) {
543
2268
  throw new Error("liquidGL: snapshot could not be uploaded.");
@@ -659,6 +2384,10 @@ const liquidGL = (() => {
659
2384
  const gl = this.gl;
660
2385
  if (!this.texture) return;
661
2386
 
2387
+ this.lenses.forEach((ln) => {
2388
+ if (ln._isSticky && !ln._mirrorActive) ln.updateMetrics();
2389
+ });
2390
+
662
2391
  if (this._isScrolling) {
663
2392
  this._scrollUpdateCounter++;
664
2393
  }
@@ -757,6 +2486,8 @@ const liquidGL = (() => {
757
2486
 
758
2487
  gl.viewport(x, y, w, h);
759
2488
  gl.uniform2f(this.u.res, w, h);
2489
+ gl.uniform2f(this.u.subpixel, leftPx - x, topPx - yTop);
2490
+ gl.uniform2f(this.u.boxSize, rect.width * dpr, rect.height * dpr);
760
2491
 
761
2492
  const snapRect =
762
2493
  this._frameSnapRect || this.snapshotTarget.getBoundingClientRect();
@@ -1187,27 +2918,37 @@ const liquidGL = (() => {
1187
2918
  if (meta.needsRecapture && !meta._capturing && !this._isScrolling) {
1188
2919
  meta._capturing = true;
1189
2920
 
1190
- html2canvas(el, {
1191
- backgroundColor: null,
1192
- scale: this.scaleFactor,
1193
- useCORS: true,
1194
- removeContainer: true,
1195
- logging: false,
1196
- ignoreElements: (n) =>
1197
- n.tagName === "CANVAS" || n.hasAttribute("data-liquid-ignore"),
1198
- })
1199
- .then((cv) => {
1200
- if (cv.width > 0 && cv.height > 0) {
1201
- meta.lastCapture = cv;
1202
- meta.needsRecapture = false;
2921
+ const ignoreDynamic = (n) =>
2922
+ n.tagName === "CANVAS" || n.hasAttribute("data-liquid-ignore");
2923
+
2924
+ if (this._naughtyQueued) {
2925
+ meta._capturing = false;
2926
+ } else {
2927
+ this._naughtyQueued = true;
2928
+ const capture = () => {
2929
+ this._naughtyQueued = false;
2930
+ try {
2931
+ const cv = NaughtyDOM.rasterise(el, {
2932
+ scale: this.scaleFactor,
2933
+ ignoreElements: ignoreDynamic,
2934
+ canvas: meta.naughtyCanvas,
2935
+ });
2936
+ meta.naughtyCanvas = cv;
2937
+ if (cv.width > 0 && cv.height > 0) {
2938
+ meta.lastCapture = cv;
2939
+ meta.needsRecapture = false;
2940
+ }
2941
+ } catch (e) {
2942
+ console.error("liquidGL: Dynamic element capture failed.", e);
1203
2943
  }
1204
- })
1205
- .catch((e) => {
1206
- console.error("liquidGL: Dynamic element capture failed.", e);
1207
- })
1208
- .finally(() => {
1209
2944
  meta._capturing = false;
1210
- });
2945
+ };
2946
+ if (typeof requestIdleCallback === "function") {
2947
+ requestIdleCallback(capture, { timeout: 100 });
2948
+ } else {
2949
+ setTimeout(capture, 0);
2950
+ }
2951
+ }
1211
2952
  }
1212
2953
 
1213
2954
  if (meta.lastCapture) {
@@ -1431,6 +3172,7 @@ const liquidGL = (() => {
1431
3172
  _capturing: false,
1432
3173
  prevDrawRect: null,
1433
3174
  lastCapture: null,
3175
+ naughtyCanvas: null,
1434
3176
  needsRecapture: true,
1435
3177
  hoverClassName: null,
1436
3178
  _animating: false,
@@ -1663,6 +3405,8 @@ const liquidGL = (() => {
1663
3405
  ? "relative"
1664
3406
  : this.el.style.position;
1665
3407
 
3408
+ this._isSticky = /sticky/.test(window.getComputedStyle(this.el).position);
3409
+
1666
3410
  const bgCol = window.getComputedStyle(this.el).backgroundColor;
1667
3411
  const rgbaMatch = bgCol.match(/rgba?\(([^)]+)\)/);
1668
3412
  this._bgColorComponents = null;
@@ -1700,6 +3444,17 @@ const liquidGL = (() => {
1700
3444
  ? this._baseRect
1701
3445
  : this.el.getBoundingClientRect();
1702
3446
 
3447
+ const prev = this.rectPx;
3448
+ if (
3449
+ prev &&
3450
+ rect.left === prev.left &&
3451
+ rect.top === prev.top &&
3452
+ rect.width === prev.width &&
3453
+ rect.height === prev.height
3454
+ ) {
3455
+ return;
3456
+ }
3457
+
1703
3458
  this.rectPx = {
1704
3459
  left: rect.left,
1705
3460
  top: rect.top,
@@ -2310,7 +4065,7 @@ const liquidGL = (() => {
2310
4065
  /* --------------------------------------------------
2311
4066
  * Public API
2312
4067
  * ------------------------------------------------*/
2313
- const liquidGL = function (userOptions = {}) {
4068
+ window.liquidGL = function (userOptions = {}) {
2314
4069
  const defaults = {
2315
4070
  target: ".liquidGL",
2316
4071
  snapshot: "body",
@@ -2391,7 +4146,7 @@ const liquidGL = (() => {
2391
4146
  /* --------------------------------------------------
2392
4147
  * Public helper: register elements that need live updates
2393
4148
  * ------------------------------------------------*/
2394
- liquidGL.registerDynamic = function (elements) {
4149
+ window.liquidGL.registerDynamic = function (elements) {
2395
4150
  const renderer = window.__liquidGLRenderer__;
2396
4151
  if (!renderer || !renderer.addDynamicElement) return;
2397
4152
  renderer.addDynamicElement(elements);
@@ -2403,7 +4158,7 @@ const liquidGL = (() => {
2403
4158
  /* --------------------------------------------------
2404
4159
  * Public helper: Universal smooth scroll / animation sync
2405
4160
  * ------------------------------------------------*/
2406
- liquidGL.syncWith = function (config = {}) {
4161
+ window.liquidGL.syncWith = function (config = {}) {
2407
4162
  const renderer = window.__liquidGLRenderer__;
2408
4163
  if (!renderer) {
2409
4164
  console.warn(
@@ -2487,7 +4242,7 @@ const liquidGL = (() => {
2487
4242
 
2488
4243
  return { lenis, locomotiveScroll: loco };
2489
4244
  };
2490
- return liquidGL;
4245
+ return window.liquidGL;
2491
4246
  })();
2492
4247
 
2493
4248
  export default liquidGL;