liquid-gl 2.0.0 → 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 +31 -14
  2. package/liquidGL.js +1797 -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.1
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.");
@@ -757,6 +2482,8 @@ const liquidGL = (() => {
757
2482
 
758
2483
  gl.viewport(x, y, w, h);
759
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);
760
2487
 
761
2488
  const snapRect =
762
2489
  this._frameSnapRect || this.snapshotTarget.getBoundingClientRect();
@@ -1187,27 +2914,37 @@ const liquidGL = (() => {
1187
2914
  if (meta.needsRecapture && !meta._capturing && !this._isScrolling) {
1188
2915
  meta._capturing = true;
1189
2916
 
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;
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);
1203
2939
  }
1204
- })
1205
- .catch((e) => {
1206
- console.error("liquidGL: Dynamic element capture failed.", e);
1207
- })
1208
- .finally(() => {
1209
2940
  meta._capturing = false;
1210
- });
2941
+ };
2942
+ if (typeof requestIdleCallback === "function") {
2943
+ requestIdleCallback(capture, { timeout: 100 });
2944
+ } else {
2945
+ setTimeout(capture, 0);
2946
+ }
2947
+ }
1211
2948
  }
1212
2949
 
1213
2950
  if (meta.lastCapture) {
@@ -1431,6 +3168,7 @@ const liquidGL = (() => {
1431
3168
  _capturing: false,
1432
3169
  prevDrawRect: null,
1433
3170
  lastCapture: null,
3171
+ naughtyCanvas: null,
1434
3172
  needsRecapture: true,
1435
3173
  hoverClassName: null,
1436
3174
  _animating: false,
@@ -2310,7 +4048,7 @@ const liquidGL = (() => {
2310
4048
  /* --------------------------------------------------
2311
4049
  * Public API
2312
4050
  * ------------------------------------------------*/
2313
- const liquidGL = function (userOptions = {}) {
4051
+ window.liquidGL = function (userOptions = {}) {
2314
4052
  const defaults = {
2315
4053
  target: ".liquidGL",
2316
4054
  snapshot: "body",
@@ -2391,7 +4129,7 @@ const liquidGL = (() => {
2391
4129
  /* --------------------------------------------------
2392
4130
  * Public helper: register elements that need live updates
2393
4131
  * ------------------------------------------------*/
2394
- liquidGL.registerDynamic = function (elements) {
4132
+ window.liquidGL.registerDynamic = function (elements) {
2395
4133
  const renderer = window.__liquidGLRenderer__;
2396
4134
  if (!renderer || !renderer.addDynamicElement) return;
2397
4135
  renderer.addDynamicElement(elements);
@@ -2403,7 +4141,7 @@ const liquidGL = (() => {
2403
4141
  /* --------------------------------------------------
2404
4142
  * Public helper: Universal smooth scroll / animation sync
2405
4143
  * ------------------------------------------------*/
2406
- liquidGL.syncWith = function (config = {}) {
4144
+ window.liquidGL.syncWith = function (config = {}) {
2407
4145
  const renderer = window.__liquidGLRenderer__;
2408
4146
  if (!renderer) {
2409
4147
  console.warn(
@@ -2487,7 +4225,7 @@ const liquidGL = (() => {
2487
4225
 
2488
4226
  return { lenis, locomotiveScroll: loco };
2489
4227
  };
2490
- return liquidGL;
4228
+ return window.liquidGL;
2491
4229
  })();
2492
4230
 
2493
4231
  export default liquidGL;