@boyernick/standard-ui-react 0.4.2-canary.60 → 0.4.2-canary.62
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.
- package/package.json +1 -1
- package/src/carousel.tsx +1 -13
- package/src/glass.tsx +22 -41
- package/src/lib/lens-glass/config.ts +225 -0
- package/src/lib/lens-glass/renderer.ts +605 -0
- package/src/lib/lens-glass/shader.ts +138 -0
- package/src/lib/media-glass.ts +11 -13
- package/src/modal.tsx +2 -2
- package/src/lib/liquidglass/NOTICE.md +0 -24
- package/src/lib/liquidglass/index.d.ts +0 -45
- package/src/lib/liquidglass/index.js +0 -2862
|
@@ -1,2862 +0,0 @@
|
|
|
1
|
-
// src/defaults.ts
|
|
2
|
-
var DEFAULTS = {
|
|
3
|
-
blurAmount: 0,
|
|
4
|
-
refraction: 0.69,
|
|
5
|
-
chromAberration: 0.05,
|
|
6
|
-
edgeHighlight: 0.05,
|
|
7
|
-
specular: 0,
|
|
8
|
-
fresnel: 1,
|
|
9
|
-
distortion: 0,
|
|
10
|
-
cornerRadius: 65,
|
|
11
|
-
zRadius: 40,
|
|
12
|
-
opacity: 1,
|
|
13
|
-
saturation: 0,
|
|
14
|
-
tintStrength: 0,
|
|
15
|
-
brightness: 0,
|
|
16
|
-
shadowOpacity: 0.3,
|
|
17
|
-
shadowSpread: 10,
|
|
18
|
-
shadowOffsetY: 1,
|
|
19
|
-
floating: false,
|
|
20
|
-
button: false,
|
|
21
|
-
bevelMode: 0
|
|
22
|
-
};
|
|
23
|
-
var BLUR_ITERATIONS = 6;
|
|
24
|
-
var SHADOW_PAD = 20;
|
|
25
|
-
|
|
26
|
-
// node_modules/html-to-image/es/util.js
|
|
27
|
-
function resolveUrl(url, baseUrl) {
|
|
28
|
-
if (url.match(/^[a-z]+:\/\//i)) {
|
|
29
|
-
return url;
|
|
30
|
-
}
|
|
31
|
-
if (url.match(/^\/\//)) {
|
|
32
|
-
return window.location.protocol + url;
|
|
33
|
-
}
|
|
34
|
-
if (url.match(/^[a-z]+:/i)) {
|
|
35
|
-
return url;
|
|
36
|
-
}
|
|
37
|
-
const doc = document.implementation.createHTMLDocument();
|
|
38
|
-
const base = doc.createElement("base");
|
|
39
|
-
const a = doc.createElement("a");
|
|
40
|
-
doc.head.appendChild(base);
|
|
41
|
-
doc.body.appendChild(a);
|
|
42
|
-
if (baseUrl) {
|
|
43
|
-
base.href = baseUrl;
|
|
44
|
-
}
|
|
45
|
-
a.href = url;
|
|
46
|
-
return a.href;
|
|
47
|
-
}
|
|
48
|
-
var uuid = /* @__PURE__ */ (() => {
|
|
49
|
-
let counter = 0;
|
|
50
|
-
const random = () => (
|
|
51
|
-
// eslint-disable-next-line no-bitwise
|
|
52
|
-
`0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4)
|
|
53
|
-
);
|
|
54
|
-
return () => {
|
|
55
|
-
counter += 1;
|
|
56
|
-
return `u${random()}${counter}`;
|
|
57
|
-
};
|
|
58
|
-
})();
|
|
59
|
-
function toArray(arrayLike) {
|
|
60
|
-
const arr = [];
|
|
61
|
-
for (let i = 0, l = arrayLike.length; i < l; i++) {
|
|
62
|
-
arr.push(arrayLike[i]);
|
|
63
|
-
}
|
|
64
|
-
return arr;
|
|
65
|
-
}
|
|
66
|
-
var styleProps = null;
|
|
67
|
-
function getStyleProperties(options = {}) {
|
|
68
|
-
if (styleProps) {
|
|
69
|
-
return styleProps;
|
|
70
|
-
}
|
|
71
|
-
if (options.includeStyleProperties) {
|
|
72
|
-
styleProps = options.includeStyleProperties;
|
|
73
|
-
return styleProps;
|
|
74
|
-
}
|
|
75
|
-
styleProps = toArray(window.getComputedStyle(document.documentElement));
|
|
76
|
-
return styleProps;
|
|
77
|
-
}
|
|
78
|
-
function px(node, styleProperty) {
|
|
79
|
-
const win = node.ownerDocument.defaultView || window;
|
|
80
|
-
const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
|
|
81
|
-
return val ? parseFloat(val.replace("px", "")) : 0;
|
|
82
|
-
}
|
|
83
|
-
function getNodeWidth(node) {
|
|
84
|
-
const leftBorder = px(node, "border-left-width");
|
|
85
|
-
const rightBorder = px(node, "border-right-width");
|
|
86
|
-
return node.clientWidth + leftBorder + rightBorder;
|
|
87
|
-
}
|
|
88
|
-
function getNodeHeight(node) {
|
|
89
|
-
const topBorder = px(node, "border-top-width");
|
|
90
|
-
const bottomBorder = px(node, "border-bottom-width");
|
|
91
|
-
return node.clientHeight + topBorder + bottomBorder;
|
|
92
|
-
}
|
|
93
|
-
function getImageSize(targetNode, options = {}) {
|
|
94
|
-
const width = options.width || getNodeWidth(targetNode);
|
|
95
|
-
const height = options.height || getNodeHeight(targetNode);
|
|
96
|
-
return { width, height };
|
|
97
|
-
}
|
|
98
|
-
function getPixelRatio() {
|
|
99
|
-
let ratio;
|
|
100
|
-
let FINAL_PROCESS;
|
|
101
|
-
try {
|
|
102
|
-
FINAL_PROCESS = process;
|
|
103
|
-
} catch (e) {
|
|
104
|
-
}
|
|
105
|
-
const val = FINAL_PROCESS && FINAL_PROCESS.env ? FINAL_PROCESS.env.devicePixelRatio : null;
|
|
106
|
-
if (val) {
|
|
107
|
-
ratio = parseInt(val, 10);
|
|
108
|
-
if (Number.isNaN(ratio)) {
|
|
109
|
-
ratio = 1;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return ratio || window.devicePixelRatio || 1;
|
|
113
|
-
}
|
|
114
|
-
var canvasDimensionLimit = 16384;
|
|
115
|
-
function checkCanvasDimensions(canvas) {
|
|
116
|
-
if (canvas.width > canvasDimensionLimit || canvas.height > canvasDimensionLimit) {
|
|
117
|
-
if (canvas.width > canvasDimensionLimit && canvas.height > canvasDimensionLimit) {
|
|
118
|
-
if (canvas.width > canvas.height) {
|
|
119
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
120
|
-
canvas.width = canvasDimensionLimit;
|
|
121
|
-
} else {
|
|
122
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
123
|
-
canvas.height = canvasDimensionLimit;
|
|
124
|
-
}
|
|
125
|
-
} else if (canvas.width > canvasDimensionLimit) {
|
|
126
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
127
|
-
canvas.width = canvasDimensionLimit;
|
|
128
|
-
} else {
|
|
129
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
130
|
-
canvas.height = canvasDimensionLimit;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function createImage(url) {
|
|
135
|
-
return new Promise((resolve, reject) => {
|
|
136
|
-
const img = new Image();
|
|
137
|
-
img.onload = () => {
|
|
138
|
-
img.decode().then(() => {
|
|
139
|
-
requestAnimationFrame(() => resolve(img));
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
img.onerror = reject;
|
|
143
|
-
img.crossOrigin = "anonymous";
|
|
144
|
-
img.decoding = "async";
|
|
145
|
-
img.src = url;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async function svgToDataURL(svg) {
|
|
149
|
-
return Promise.resolve().then(() => new XMLSerializer().serializeToString(svg)).then(encodeURIComponent).then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
150
|
-
}
|
|
151
|
-
async function nodeToDataURL(node, width, height) {
|
|
152
|
-
const xmlns = "http://www.w3.org/2000/svg";
|
|
153
|
-
const svg = document.createElementNS(xmlns, "svg");
|
|
154
|
-
const foreignObject = document.createElementNS(xmlns, "foreignObject");
|
|
155
|
-
svg.setAttribute("width", `${width}`);
|
|
156
|
-
svg.setAttribute("height", `${height}`);
|
|
157
|
-
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
158
|
-
foreignObject.setAttribute("width", "100%");
|
|
159
|
-
foreignObject.setAttribute("height", "100%");
|
|
160
|
-
foreignObject.setAttribute("x", "0");
|
|
161
|
-
foreignObject.setAttribute("y", "0");
|
|
162
|
-
foreignObject.setAttribute("externalResourcesRequired", "true");
|
|
163
|
-
svg.appendChild(foreignObject);
|
|
164
|
-
foreignObject.appendChild(node);
|
|
165
|
-
return svgToDataURL(svg);
|
|
166
|
-
}
|
|
167
|
-
var isInstanceOfElement = (node, instance) => {
|
|
168
|
-
if (node instanceof instance)
|
|
169
|
-
return true;
|
|
170
|
-
const nodePrototype = Object.getPrototypeOf(node);
|
|
171
|
-
if (nodePrototype === null)
|
|
172
|
-
return false;
|
|
173
|
-
return nodePrototype.constructor.name === instance.name || isInstanceOfElement(nodePrototype, instance);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// node_modules/html-to-image/es/clone-pseudos.js
|
|
177
|
-
function formatCSSText(style) {
|
|
178
|
-
const content = style.getPropertyValue("content");
|
|
179
|
-
return `${style.cssText} content: '${content.replace(/'|"/g, "")}';`;
|
|
180
|
-
}
|
|
181
|
-
function formatCSSProperties(style, options) {
|
|
182
|
-
return getStyleProperties(options).map((name) => {
|
|
183
|
-
const value = style.getPropertyValue(name);
|
|
184
|
-
const priority = style.getPropertyPriority(name);
|
|
185
|
-
return `${name}: ${value}${priority ? " !important" : ""};`;
|
|
186
|
-
}).join(" ");
|
|
187
|
-
}
|
|
188
|
-
function getPseudoElementStyle(className, pseudo, style, options) {
|
|
189
|
-
const selector = `.${className}:${pseudo}`;
|
|
190
|
-
const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style, options);
|
|
191
|
-
return document.createTextNode(`${selector}{${cssText}}`);
|
|
192
|
-
}
|
|
193
|
-
function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
|
|
194
|
-
const style = window.getComputedStyle(nativeNode, pseudo);
|
|
195
|
-
const content = style.getPropertyValue("content");
|
|
196
|
-
if (content === "" || content === "none") {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const className = uuid();
|
|
200
|
-
try {
|
|
201
|
-
clonedNode.className = `${clonedNode.className} ${className}`;
|
|
202
|
-
} catch (err) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const styleElement = document.createElement("style");
|
|
206
|
-
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
|
|
207
|
-
clonedNode.appendChild(styleElement);
|
|
208
|
-
}
|
|
209
|
-
function clonePseudoElements(nativeNode, clonedNode, options) {
|
|
210
|
-
clonePseudoElement(nativeNode, clonedNode, ":before", options);
|
|
211
|
-
clonePseudoElement(nativeNode, clonedNode, ":after", options);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// node_modules/html-to-image/es/mimes.js
|
|
215
|
-
var WOFF = "application/font-woff";
|
|
216
|
-
var JPEG = "image/jpeg";
|
|
217
|
-
var mimes = {
|
|
218
|
-
woff: WOFF,
|
|
219
|
-
woff2: WOFF,
|
|
220
|
-
ttf: "application/font-truetype",
|
|
221
|
-
eot: "application/vnd.ms-fontobject",
|
|
222
|
-
png: "image/png",
|
|
223
|
-
jpg: JPEG,
|
|
224
|
-
jpeg: JPEG,
|
|
225
|
-
gif: "image/gif",
|
|
226
|
-
tiff: "image/tiff",
|
|
227
|
-
svg: "image/svg+xml",
|
|
228
|
-
webp: "image/webp"
|
|
229
|
-
};
|
|
230
|
-
function getExtension(url) {
|
|
231
|
-
const match = /\.([^./]*?)$/g.exec(url);
|
|
232
|
-
return match ? match[1] : "";
|
|
233
|
-
}
|
|
234
|
-
function getMimeType(url) {
|
|
235
|
-
const extension = getExtension(url).toLowerCase();
|
|
236
|
-
return mimes[extension] || "";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// node_modules/html-to-image/es/dataurl.js
|
|
240
|
-
function getContentFromDataUrl(dataURL) {
|
|
241
|
-
return dataURL.split(/,/)[1];
|
|
242
|
-
}
|
|
243
|
-
function isDataUrl(url) {
|
|
244
|
-
return url.search(/^(data:)/) !== -1;
|
|
245
|
-
}
|
|
246
|
-
function makeDataUrl(content, mimeType) {
|
|
247
|
-
return `data:${mimeType};base64,${content}`;
|
|
248
|
-
}
|
|
249
|
-
async function fetchAsDataURL(url, init, process2) {
|
|
250
|
-
const res = await fetch(url, init);
|
|
251
|
-
if (res.status === 404) {
|
|
252
|
-
throw new Error(`Resource "${res.url}" not found`);
|
|
253
|
-
}
|
|
254
|
-
const blob = await res.blob();
|
|
255
|
-
return new Promise((resolve, reject) => {
|
|
256
|
-
const reader = new FileReader();
|
|
257
|
-
reader.onerror = reject;
|
|
258
|
-
reader.onloadend = () => {
|
|
259
|
-
try {
|
|
260
|
-
resolve(process2({ res, result: reader.result }));
|
|
261
|
-
} catch (error) {
|
|
262
|
-
reject(error);
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
reader.readAsDataURL(blob);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
var cache = {};
|
|
269
|
-
function getCacheKey(url, contentType, includeQueryParams) {
|
|
270
|
-
let key = url.replace(/\?.*/, "");
|
|
271
|
-
if (includeQueryParams) {
|
|
272
|
-
key = url;
|
|
273
|
-
}
|
|
274
|
-
if (/ttf|otf|eot|woff2?/i.test(key)) {
|
|
275
|
-
key = key.replace(/.*\//, "");
|
|
276
|
-
}
|
|
277
|
-
return contentType ? `[${contentType}]${key}` : key;
|
|
278
|
-
}
|
|
279
|
-
async function resourceToDataURL(resourceUrl, contentType, options) {
|
|
280
|
-
const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
|
|
281
|
-
if (cache[cacheKey] != null) {
|
|
282
|
-
return cache[cacheKey];
|
|
283
|
-
}
|
|
284
|
-
if (options.cacheBust) {
|
|
285
|
-
resourceUrl += (/\?/.test(resourceUrl) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime();
|
|
286
|
-
}
|
|
287
|
-
let dataURL;
|
|
288
|
-
try {
|
|
289
|
-
const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
|
|
290
|
-
if (!contentType) {
|
|
291
|
-
contentType = res.headers.get("Content-Type") || "";
|
|
292
|
-
}
|
|
293
|
-
return getContentFromDataUrl(result);
|
|
294
|
-
});
|
|
295
|
-
dataURL = makeDataUrl(content, contentType);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
dataURL = options.imagePlaceholder || "";
|
|
298
|
-
let msg = `Failed to fetch resource: ${resourceUrl}`;
|
|
299
|
-
if (error) {
|
|
300
|
-
msg = typeof error === "string" ? error : error.message;
|
|
301
|
-
}
|
|
302
|
-
if (msg) {
|
|
303
|
-
console.warn(msg);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
cache[cacheKey] = dataURL;
|
|
307
|
-
return dataURL;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// node_modules/html-to-image/es/clone-node.js
|
|
311
|
-
async function cloneCanvasElement(canvas) {
|
|
312
|
-
const dataURL = canvas.toDataURL();
|
|
313
|
-
if (dataURL === "data:,") {
|
|
314
|
-
return canvas.cloneNode(false);
|
|
315
|
-
}
|
|
316
|
-
return createImage(dataURL);
|
|
317
|
-
}
|
|
318
|
-
async function cloneVideoElement(video, options) {
|
|
319
|
-
if (video.currentSrc) {
|
|
320
|
-
const canvas = document.createElement("canvas");
|
|
321
|
-
const ctx = canvas.getContext("2d");
|
|
322
|
-
canvas.width = video.clientWidth;
|
|
323
|
-
canvas.height = video.clientHeight;
|
|
324
|
-
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
325
|
-
const dataURL2 = canvas.toDataURL();
|
|
326
|
-
return createImage(dataURL2);
|
|
327
|
-
}
|
|
328
|
-
const poster = video.poster;
|
|
329
|
-
const contentType = getMimeType(poster);
|
|
330
|
-
const dataURL = await resourceToDataURL(poster, contentType, options);
|
|
331
|
-
return createImage(dataURL);
|
|
332
|
-
}
|
|
333
|
-
async function cloneIFrameElement(iframe, options) {
|
|
334
|
-
var _a;
|
|
335
|
-
try {
|
|
336
|
-
if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
|
|
337
|
-
return await cloneNode(iframe.contentDocument.body, options, true);
|
|
338
|
-
}
|
|
339
|
-
} catch (_b) {
|
|
340
|
-
}
|
|
341
|
-
return iframe.cloneNode(false);
|
|
342
|
-
}
|
|
343
|
-
async function cloneSingleNode(node, options) {
|
|
344
|
-
if (isInstanceOfElement(node, HTMLCanvasElement)) {
|
|
345
|
-
return cloneCanvasElement(node);
|
|
346
|
-
}
|
|
347
|
-
if (isInstanceOfElement(node, HTMLVideoElement)) {
|
|
348
|
-
return cloneVideoElement(node, options);
|
|
349
|
-
}
|
|
350
|
-
if (isInstanceOfElement(node, HTMLIFrameElement)) {
|
|
351
|
-
return cloneIFrameElement(node, options);
|
|
352
|
-
}
|
|
353
|
-
return node.cloneNode(isSVGElement(node));
|
|
354
|
-
}
|
|
355
|
-
var isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SLOT";
|
|
356
|
-
var isSVGElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SVG";
|
|
357
|
-
async function cloneChildren(nativeNode, clonedNode, options) {
|
|
358
|
-
var _a, _b;
|
|
359
|
-
if (isSVGElement(clonedNode)) {
|
|
360
|
-
return clonedNode;
|
|
361
|
-
}
|
|
362
|
-
let children = [];
|
|
363
|
-
if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
|
|
364
|
-
children = toArray(nativeNode.assignedNodes());
|
|
365
|
-
} else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) {
|
|
366
|
-
children = toArray(nativeNode.contentDocument.body.childNodes);
|
|
367
|
-
} else {
|
|
368
|
-
children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes);
|
|
369
|
-
}
|
|
370
|
-
if (children.length === 0 || isInstanceOfElement(nativeNode, HTMLVideoElement)) {
|
|
371
|
-
return clonedNode;
|
|
372
|
-
}
|
|
373
|
-
await children.reduce((deferred, child) => deferred.then(() => cloneNode(child, options)).then((clonedChild) => {
|
|
374
|
-
if (clonedChild) {
|
|
375
|
-
clonedNode.appendChild(clonedChild);
|
|
376
|
-
}
|
|
377
|
-
}), Promise.resolve());
|
|
378
|
-
return clonedNode;
|
|
379
|
-
}
|
|
380
|
-
function cloneCSSStyle(nativeNode, clonedNode, options) {
|
|
381
|
-
const targetStyle = clonedNode.style;
|
|
382
|
-
if (!targetStyle) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const sourceStyle = window.getComputedStyle(nativeNode);
|
|
386
|
-
if (sourceStyle.cssText) {
|
|
387
|
-
targetStyle.cssText = sourceStyle.cssText;
|
|
388
|
-
targetStyle.transformOrigin = sourceStyle.transformOrigin;
|
|
389
|
-
} else {
|
|
390
|
-
getStyleProperties(options).forEach((name) => {
|
|
391
|
-
let value = sourceStyle.getPropertyValue(name);
|
|
392
|
-
if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && name === "display" && value === "inline") {
|
|
393
|
-
value = "block";
|
|
394
|
-
}
|
|
395
|
-
if (name === "d" && clonedNode.getAttribute("d")) {
|
|
396
|
-
value = `path(${clonedNode.getAttribute("d")})`;
|
|
397
|
-
}
|
|
398
|
-
targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name));
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
function cloneInputValue(nativeNode, clonedNode) {
|
|
403
|
-
if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) {
|
|
404
|
-
clonedNode.innerHTML = nativeNode.value;
|
|
405
|
-
}
|
|
406
|
-
if (isInstanceOfElement(nativeNode, HTMLInputElement)) {
|
|
407
|
-
clonedNode.setAttribute("value", nativeNode.value);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function cloneSelectValue(nativeNode, clonedNode) {
|
|
411
|
-
if (isInstanceOfElement(nativeNode, HTMLSelectElement)) {
|
|
412
|
-
const clonedSelect = clonedNode;
|
|
413
|
-
const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute("value"));
|
|
414
|
-
if (selectedOption) {
|
|
415
|
-
selectedOption.setAttribute("selected", "");
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function decorate(nativeNode, clonedNode, options) {
|
|
420
|
-
if (isInstanceOfElement(clonedNode, Element)) {
|
|
421
|
-
cloneCSSStyle(nativeNode, clonedNode, options);
|
|
422
|
-
clonePseudoElements(nativeNode, clonedNode, options);
|
|
423
|
-
cloneInputValue(nativeNode, clonedNode);
|
|
424
|
-
cloneSelectValue(nativeNode, clonedNode);
|
|
425
|
-
}
|
|
426
|
-
return clonedNode;
|
|
427
|
-
}
|
|
428
|
-
async function ensureSVGSymbols(clone, options) {
|
|
429
|
-
const uses = clone.querySelectorAll ? clone.querySelectorAll("use") : [];
|
|
430
|
-
if (uses.length === 0) {
|
|
431
|
-
return clone;
|
|
432
|
-
}
|
|
433
|
-
const processedDefs = {};
|
|
434
|
-
for (let i = 0; i < uses.length; i++) {
|
|
435
|
-
const use = uses[i];
|
|
436
|
-
const id = use.getAttribute("xlink:href");
|
|
437
|
-
if (id) {
|
|
438
|
-
const exist = clone.querySelector(id);
|
|
439
|
-
const definition = document.querySelector(id);
|
|
440
|
-
if (!exist && definition && !processedDefs[id]) {
|
|
441
|
-
processedDefs[id] = await cloneNode(definition, options, true);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const nodes = Object.values(processedDefs);
|
|
446
|
-
if (nodes.length) {
|
|
447
|
-
const ns = "http://www.w3.org/1999/xhtml";
|
|
448
|
-
const svg = document.createElementNS(ns, "svg");
|
|
449
|
-
svg.setAttribute("xmlns", ns);
|
|
450
|
-
svg.style.position = "absolute";
|
|
451
|
-
svg.style.width = "0";
|
|
452
|
-
svg.style.height = "0";
|
|
453
|
-
svg.style.overflow = "hidden";
|
|
454
|
-
svg.style.display = "none";
|
|
455
|
-
const defs = document.createElementNS(ns, "defs");
|
|
456
|
-
svg.appendChild(defs);
|
|
457
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
458
|
-
defs.appendChild(nodes[i]);
|
|
459
|
-
}
|
|
460
|
-
clone.appendChild(svg);
|
|
461
|
-
}
|
|
462
|
-
return clone;
|
|
463
|
-
}
|
|
464
|
-
async function cloneNode(node, options, isRoot) {
|
|
465
|
-
if (!isRoot && options.filter && !options.filter(node)) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode, options)).then((clonedNode) => ensureSVGSymbols(clonedNode, options));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// node_modules/html-to-image/es/embed-resources.js
|
|
472
|
-
var URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g;
|
|
473
|
-
var URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
|
|
474
|
-
var FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
|
|
475
|
-
function toRegex(url) {
|
|
476
|
-
const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
|
|
477
|
-
return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g");
|
|
478
|
-
}
|
|
479
|
-
function parseURLs(cssText) {
|
|
480
|
-
const urls = [];
|
|
481
|
-
cssText.replace(URL_REGEX, (raw, quotation, url) => {
|
|
482
|
-
urls.push(url);
|
|
483
|
-
return raw;
|
|
484
|
-
});
|
|
485
|
-
return urls.filter((url) => !isDataUrl(url));
|
|
486
|
-
}
|
|
487
|
-
async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) {
|
|
488
|
-
try {
|
|
489
|
-
const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL;
|
|
490
|
-
const contentType = getMimeType(resourceURL);
|
|
491
|
-
let dataURL;
|
|
492
|
-
if (getContentFromUrl) {
|
|
493
|
-
const content = await getContentFromUrl(resolvedURL);
|
|
494
|
-
dataURL = makeDataUrl(content, contentType);
|
|
495
|
-
} else {
|
|
496
|
-
dataURL = await resourceToDataURL(resolvedURL, contentType, options);
|
|
497
|
-
}
|
|
498
|
-
return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`);
|
|
499
|
-
} catch (error) {
|
|
500
|
-
}
|
|
501
|
-
return cssText;
|
|
502
|
-
}
|
|
503
|
-
function filterPreferredFontFormat(str, { preferredFontFormat }) {
|
|
504
|
-
return !preferredFontFormat ? str : str.replace(FONT_SRC_REGEX, (match) => {
|
|
505
|
-
while (true) {
|
|
506
|
-
const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || [];
|
|
507
|
-
if (!format) {
|
|
508
|
-
return "";
|
|
509
|
-
}
|
|
510
|
-
if (format === preferredFontFormat) {
|
|
511
|
-
return `src: ${src};`;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
function shouldEmbed(url) {
|
|
517
|
-
return url.search(URL_REGEX) !== -1;
|
|
518
|
-
}
|
|
519
|
-
async function embedResources(cssText, baseUrl, options) {
|
|
520
|
-
if (!shouldEmbed(cssText)) {
|
|
521
|
-
return cssText;
|
|
522
|
-
}
|
|
523
|
-
const filteredCSSText = filterPreferredFontFormat(cssText, options);
|
|
524
|
-
const urls = parseURLs(filteredCSSText);
|
|
525
|
-
return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText));
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// node_modules/html-to-image/es/embed-images.js
|
|
529
|
-
async function embedProp(propName, node, options) {
|
|
530
|
-
var _a;
|
|
531
|
-
const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName);
|
|
532
|
-
if (propValue) {
|
|
533
|
-
const cssString = await embedResources(propValue, null, options);
|
|
534
|
-
node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName));
|
|
535
|
-
return true;
|
|
536
|
-
}
|
|
537
|
-
return false;
|
|
538
|
-
}
|
|
539
|
-
async function embedBackground(clonedNode, options) {
|
|
540
|
-
;
|
|
541
|
-
await embedProp("background", clonedNode, options) || await embedProp("background-image", clonedNode, options);
|
|
542
|
-
await embedProp("mask", clonedNode, options) || await embedProp("-webkit-mask", clonedNode, options) || await embedProp("mask-image", clonedNode, options) || await embedProp("-webkit-mask-image", clonedNode, options);
|
|
543
|
-
}
|
|
544
|
-
async function embedImageNode(clonedNode, options) {
|
|
545
|
-
const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);
|
|
546
|
-
if (!(isImageElement && !isDataUrl(clonedNode.src)) && !(isInstanceOfElement(clonedNode, SVGImageElement) && !isDataUrl(clonedNode.href.baseVal))) {
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal;
|
|
550
|
-
const dataURL = await resourceToDataURL(url, getMimeType(url), options);
|
|
551
|
-
await new Promise((resolve, reject) => {
|
|
552
|
-
clonedNode.onload = resolve;
|
|
553
|
-
clonedNode.onerror = options.onImageErrorHandler ? (...attributes) => {
|
|
554
|
-
try {
|
|
555
|
-
resolve(options.onImageErrorHandler(...attributes));
|
|
556
|
-
} catch (error) {
|
|
557
|
-
reject(error);
|
|
558
|
-
}
|
|
559
|
-
} : reject;
|
|
560
|
-
const image = clonedNode;
|
|
561
|
-
if (image.decode) {
|
|
562
|
-
image.decode = resolve;
|
|
563
|
-
}
|
|
564
|
-
if (image.loading === "lazy") {
|
|
565
|
-
image.loading = "eager";
|
|
566
|
-
}
|
|
567
|
-
if (isImageElement) {
|
|
568
|
-
clonedNode.srcset = "";
|
|
569
|
-
clonedNode.src = dataURL;
|
|
570
|
-
} else {
|
|
571
|
-
clonedNode.href.baseVal = dataURL;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
async function embedChildren(clonedNode, options) {
|
|
576
|
-
const children = toArray(clonedNode.childNodes);
|
|
577
|
-
const deferreds = children.map((child) => embedImages(child, options));
|
|
578
|
-
await Promise.all(deferreds).then(() => clonedNode);
|
|
579
|
-
}
|
|
580
|
-
async function embedImages(clonedNode, options) {
|
|
581
|
-
if (isInstanceOfElement(clonedNode, Element)) {
|
|
582
|
-
await embedBackground(clonedNode, options);
|
|
583
|
-
await embedImageNode(clonedNode, options);
|
|
584
|
-
await embedChildren(clonedNode, options);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// node_modules/html-to-image/es/apply-style.js
|
|
589
|
-
function applyStyle(node, options) {
|
|
590
|
-
const { style } = node;
|
|
591
|
-
if (options.backgroundColor) {
|
|
592
|
-
style.backgroundColor = options.backgroundColor;
|
|
593
|
-
}
|
|
594
|
-
if (options.width) {
|
|
595
|
-
style.width = `${options.width}px`;
|
|
596
|
-
}
|
|
597
|
-
if (options.height) {
|
|
598
|
-
style.height = `${options.height}px`;
|
|
599
|
-
}
|
|
600
|
-
const manual = options.style;
|
|
601
|
-
if (manual != null) {
|
|
602
|
-
Object.keys(manual).forEach((key) => {
|
|
603
|
-
style[key] = manual[key];
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
return node;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// node_modules/html-to-image/es/embed-webfonts.js
|
|
610
|
-
var cssFetchCache = {};
|
|
611
|
-
async function fetchCSS(url) {
|
|
612
|
-
let cache2 = cssFetchCache[url];
|
|
613
|
-
if (cache2 != null) {
|
|
614
|
-
return cache2;
|
|
615
|
-
}
|
|
616
|
-
const res = await fetch(url);
|
|
617
|
-
const cssText = await res.text();
|
|
618
|
-
cache2 = { url, cssText };
|
|
619
|
-
cssFetchCache[url] = cache2;
|
|
620
|
-
return cache2;
|
|
621
|
-
}
|
|
622
|
-
async function embedFonts(data, options) {
|
|
623
|
-
let cssText = data.cssText;
|
|
624
|
-
const regexUrl = /url\(["']?([^"')]+)["']?\)/g;
|
|
625
|
-
const fontLocs = cssText.match(/url\([^)]+\)/g) || [];
|
|
626
|
-
const loadFonts = fontLocs.map(async (loc) => {
|
|
627
|
-
let url = loc.replace(regexUrl, "$1");
|
|
628
|
-
if (!url.startsWith("https://")) {
|
|
629
|
-
url = new URL(url, data.url).href;
|
|
630
|
-
}
|
|
631
|
-
return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {
|
|
632
|
-
cssText = cssText.replace(loc, `url(${result})`);
|
|
633
|
-
return [loc, result];
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
return Promise.all(loadFonts).then(() => cssText);
|
|
637
|
-
}
|
|
638
|
-
function parseCSS(source) {
|
|
639
|
-
if (source == null) {
|
|
640
|
-
return [];
|
|
641
|
-
}
|
|
642
|
-
const result = [];
|
|
643
|
-
const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi;
|
|
644
|
-
let cssText = source.replace(commentsRegex, "");
|
|
645
|
-
const keyframesRegex = new RegExp("((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})", "gi");
|
|
646
|
-
while (true) {
|
|
647
|
-
const matches = keyframesRegex.exec(cssText);
|
|
648
|
-
if (matches === null) {
|
|
649
|
-
break;
|
|
650
|
-
}
|
|
651
|
-
result.push(matches[0]);
|
|
652
|
-
}
|
|
653
|
-
cssText = cssText.replace(keyframesRegex, "");
|
|
654
|
-
const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
|
|
655
|
-
const combinedCSSRegex = "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})";
|
|
656
|
-
const unifiedRegex = new RegExp(combinedCSSRegex, "gi");
|
|
657
|
-
while (true) {
|
|
658
|
-
let matches = importRegex.exec(cssText);
|
|
659
|
-
if (matches === null) {
|
|
660
|
-
matches = unifiedRegex.exec(cssText);
|
|
661
|
-
if (matches === null) {
|
|
662
|
-
break;
|
|
663
|
-
} else {
|
|
664
|
-
importRegex.lastIndex = unifiedRegex.lastIndex;
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
unifiedRegex.lastIndex = importRegex.lastIndex;
|
|
668
|
-
}
|
|
669
|
-
result.push(matches[0]);
|
|
670
|
-
}
|
|
671
|
-
return result;
|
|
672
|
-
}
|
|
673
|
-
async function getCSSRules(styleSheets, options) {
|
|
674
|
-
const ret = [];
|
|
675
|
-
const deferreds = [];
|
|
676
|
-
styleSheets.forEach((sheet) => {
|
|
677
|
-
if ("cssRules" in sheet) {
|
|
678
|
-
try {
|
|
679
|
-
toArray(sheet.cssRules || []).forEach((item, index) => {
|
|
680
|
-
if (item.type === CSSRule.IMPORT_RULE) {
|
|
681
|
-
let importIndex = index + 1;
|
|
682
|
-
const url = item.href;
|
|
683
|
-
const deferred = fetchCSS(url).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
|
|
684
|
-
try {
|
|
685
|
-
sheet.insertRule(rule, rule.startsWith("@import") ? importIndex += 1 : sheet.cssRules.length);
|
|
686
|
-
} catch (error) {
|
|
687
|
-
console.error("Error inserting rule from remote css", {
|
|
688
|
-
rule,
|
|
689
|
-
error
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
})).catch((e) => {
|
|
693
|
-
console.error("Error loading remote css", e.toString());
|
|
694
|
-
});
|
|
695
|
-
deferreds.push(deferred);
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
} catch (e) {
|
|
699
|
-
const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
|
|
700
|
-
if (sheet.href != null) {
|
|
701
|
-
deferreds.push(fetchCSS(sheet.href).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
|
|
702
|
-
inline.insertRule(rule, inline.cssRules.length);
|
|
703
|
-
})).catch((err) => {
|
|
704
|
-
console.error("Error loading remote stylesheet", err);
|
|
705
|
-
}));
|
|
706
|
-
}
|
|
707
|
-
console.error("Error inlining remote css file", e);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
return Promise.all(deferreds).then(() => {
|
|
712
|
-
styleSheets.forEach((sheet) => {
|
|
713
|
-
if ("cssRules" in sheet) {
|
|
714
|
-
try {
|
|
715
|
-
toArray(sheet.cssRules || []).forEach((item) => {
|
|
716
|
-
ret.push(item);
|
|
717
|
-
});
|
|
718
|
-
} catch (e) {
|
|
719
|
-
console.error(`Error while reading CSS rules from ${sheet.href}`, e);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
return ret;
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
function getWebFontRules(cssRules) {
|
|
727
|
-
return cssRules.filter((rule) => rule.type === CSSRule.FONT_FACE_RULE).filter((rule) => shouldEmbed(rule.style.getPropertyValue("src")));
|
|
728
|
-
}
|
|
729
|
-
async function parseWebFontRules(node, options) {
|
|
730
|
-
if (node.ownerDocument == null) {
|
|
731
|
-
throw new Error("Provided element is not within a Document");
|
|
732
|
-
}
|
|
733
|
-
const styleSheets = toArray(node.ownerDocument.styleSheets);
|
|
734
|
-
const cssRules = await getCSSRules(styleSheets, options);
|
|
735
|
-
return getWebFontRules(cssRules);
|
|
736
|
-
}
|
|
737
|
-
function normalizeFontFamily(font) {
|
|
738
|
-
return font.trim().replace(/["']/g, "");
|
|
739
|
-
}
|
|
740
|
-
function getUsedFonts(node) {
|
|
741
|
-
const fonts = /* @__PURE__ */ new Set();
|
|
742
|
-
function traverse(node2) {
|
|
743
|
-
const fontFamily = node2.style.fontFamily || getComputedStyle(node2).fontFamily;
|
|
744
|
-
fontFamily.split(",").forEach((font) => {
|
|
745
|
-
fonts.add(normalizeFontFamily(font));
|
|
746
|
-
});
|
|
747
|
-
Array.from(node2.children).forEach((child) => {
|
|
748
|
-
if (child instanceof HTMLElement) {
|
|
749
|
-
traverse(child);
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
traverse(node);
|
|
754
|
-
return fonts;
|
|
755
|
-
}
|
|
756
|
-
async function getWebFontCSS(node, options) {
|
|
757
|
-
const rules = await parseWebFontRules(node, options);
|
|
758
|
-
const usedFonts = getUsedFonts(node);
|
|
759
|
-
const cssTexts = await Promise.all(rules.filter((rule) => usedFonts.has(normalizeFontFamily(rule.style.fontFamily))).map((rule) => {
|
|
760
|
-
const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;
|
|
761
|
-
return embedResources(rule.cssText, baseUrl, options);
|
|
762
|
-
}));
|
|
763
|
-
return cssTexts.join("\n");
|
|
764
|
-
}
|
|
765
|
-
async function embedWebFonts(clonedNode, options) {
|
|
766
|
-
const cssText = options.fontEmbedCSS != null ? options.fontEmbedCSS : options.skipFonts ? null : await getWebFontCSS(clonedNode, options);
|
|
767
|
-
if (cssText) {
|
|
768
|
-
const styleNode = document.createElement("style");
|
|
769
|
-
const sytleContent = document.createTextNode(cssText);
|
|
770
|
-
styleNode.appendChild(sytleContent);
|
|
771
|
-
if (clonedNode.firstChild) {
|
|
772
|
-
clonedNode.insertBefore(styleNode, clonedNode.firstChild);
|
|
773
|
-
} else {
|
|
774
|
-
clonedNode.appendChild(styleNode);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// node_modules/html-to-image/es/index.js
|
|
780
|
-
async function toSvg(node, options = {}) {
|
|
781
|
-
const { width, height } = getImageSize(node, options);
|
|
782
|
-
const clonedNode = await cloneNode(node, options, true);
|
|
783
|
-
await embedWebFonts(clonedNode, options);
|
|
784
|
-
await embedImages(clonedNode, options);
|
|
785
|
-
applyStyle(clonedNode, options);
|
|
786
|
-
const datauri = await nodeToDataURL(clonedNode, width, height);
|
|
787
|
-
return datauri;
|
|
788
|
-
}
|
|
789
|
-
async function toCanvas(node, options = {}) {
|
|
790
|
-
const { width, height } = getImageSize(node, options);
|
|
791
|
-
const svg = await toSvg(node, options);
|
|
792
|
-
const img = await createImage(svg);
|
|
793
|
-
const canvas = document.createElement("canvas");
|
|
794
|
-
const context = canvas.getContext("2d");
|
|
795
|
-
const ratio = options.pixelRatio || getPixelRatio();
|
|
796
|
-
const canvasWidth = options.canvasWidth || width;
|
|
797
|
-
const canvasHeight = options.canvasHeight || height;
|
|
798
|
-
canvas.width = canvasWidth * ratio;
|
|
799
|
-
canvas.height = canvasHeight * ratio;
|
|
800
|
-
if (!options.skipAutoScale) {
|
|
801
|
-
checkCanvasDimensions(canvas);
|
|
802
|
-
}
|
|
803
|
-
canvas.style.width = `${canvasWidth}`;
|
|
804
|
-
canvas.style.height = `${canvasHeight}`;
|
|
805
|
-
if (options.backgroundColor) {
|
|
806
|
-
context.fillStyle = options.backgroundColor;
|
|
807
|
-
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
808
|
-
}
|
|
809
|
-
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
810
|
-
return canvas;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// src/HtmlCapture.ts
|
|
814
|
-
var _sharedFontBlocks = null;
|
|
815
|
-
function invalidateFontEmbedCache() {
|
|
816
|
-
_sharedFontBlocks = null;
|
|
817
|
-
}
|
|
818
|
-
function parseFontFamily(block) {
|
|
819
|
-
const m = block.match(/font-family\s*:\s*(['"]?)([^;'"]+)\1/i);
|
|
820
|
-
return m ? m[2].trim() : "";
|
|
821
|
-
}
|
|
822
|
-
function parseFontWeight(block) {
|
|
823
|
-
const m = block.match(/font-weight\s*:\s*([^;]+)/i);
|
|
824
|
-
return m ? m[1].trim() : "400";
|
|
825
|
-
}
|
|
826
|
-
function parseFontStyle(block) {
|
|
827
|
-
const m = block.match(/font-style\s*:\s*([^;]+)/i);
|
|
828
|
-
return m ? m[1].trim() : "normal";
|
|
829
|
-
}
|
|
830
|
-
function parseUnicodeRange(block) {
|
|
831
|
-
const m = block.match(/unicode-range\s*:\s*([^;]+)/i);
|
|
832
|
-
if (!m) return null;
|
|
833
|
-
const ranges = [];
|
|
834
|
-
for (const part of m[1].split(",")) {
|
|
835
|
-
const trimmed = part.trim();
|
|
836
|
-
const rangeMatch = trimmed.match(/U\+([0-9A-Fa-f]+)(?:-([0-9A-Fa-f]+))?/);
|
|
837
|
-
if (!rangeMatch) continue;
|
|
838
|
-
const start = parseInt(rangeMatch[1], 16);
|
|
839
|
-
const end = rangeMatch[2] ? parseInt(rangeMatch[2], 16) : start;
|
|
840
|
-
ranges.push([start, end]);
|
|
841
|
-
}
|
|
842
|
-
return ranges.length > 0 ? ranges : null;
|
|
843
|
-
}
|
|
844
|
-
function weightMatches(descriptor, target) {
|
|
845
|
-
const parts = descriptor.split(/\s+/).map(Number);
|
|
846
|
-
const t = Number(target) || 400;
|
|
847
|
-
if (parts.length >= 2) {
|
|
848
|
-
return t >= parts[0] && t <= parts[1];
|
|
849
|
-
}
|
|
850
|
-
return parts[0] === t;
|
|
851
|
-
}
|
|
852
|
-
function textMatchesUnicodeRange(text, ranges) {
|
|
853
|
-
for (let i = 0; i < text.length; i++) {
|
|
854
|
-
const cp = text.codePointAt(i);
|
|
855
|
-
for (const [lo, hi] of ranges) {
|
|
856
|
-
if (cp >= lo && cp <= hi) return true;
|
|
857
|
-
}
|
|
858
|
-
if (cp > 65535) i++;
|
|
859
|
-
}
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
862
|
-
function collectFontUsage(element) {
|
|
863
|
-
const indexMap = /* @__PURE__ */ new Map();
|
|
864
|
-
const fonts = [];
|
|
865
|
-
function walk(node) {
|
|
866
|
-
if (node.nodeType === 3) {
|
|
867
|
-
const content = node.textContent || "";
|
|
868
|
-
if (content.trim() === "") return;
|
|
869
|
-
const parent = node.parentElement;
|
|
870
|
-
if (!parent) return;
|
|
871
|
-
const style = getComputedStyle(parent);
|
|
872
|
-
const weight = style.fontWeight;
|
|
873
|
-
const fontStyle = style.fontStyle;
|
|
874
|
-
for (const raw of style.fontFamily.split(",")) {
|
|
875
|
-
const family = raw.replace(/['"]/g, "").trim().toLowerCase();
|
|
876
|
-
const key = `${family}|${weight}|${fontStyle}`;
|
|
877
|
-
const idx = indexMap.get(key);
|
|
878
|
-
if (idx !== void 0) {
|
|
879
|
-
fonts[idx].text += content;
|
|
880
|
-
} else {
|
|
881
|
-
indexMap.set(key, fonts.length);
|
|
882
|
-
fonts.push({ family, weight, style: fontStyle, text: content });
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
} else if (node.nodeType === 1) {
|
|
886
|
-
const el = node;
|
|
887
|
-
for (let i = 0; i < el.childNodes.length; i++) {
|
|
888
|
-
walk(el.childNodes[i]);
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
walk(element);
|
|
893
|
-
return fonts;
|
|
894
|
-
}
|
|
895
|
-
function filterFontBlocksForElement(blocks, element) {
|
|
896
|
-
const usages = collectFontUsage(element);
|
|
897
|
-
if (usages.length === 0) return [];
|
|
898
|
-
return blocks.filter((block) => {
|
|
899
|
-
const matchingUsages = usages.filter((u) => {
|
|
900
|
-
if (u.family !== block.family) return false;
|
|
901
|
-
const styleOk = block.style === u.style || block.style === "normal" && u.style === "normal";
|
|
902
|
-
return styleOk && weightMatches(block.weight, u.weight);
|
|
903
|
-
});
|
|
904
|
-
if (matchingUsages.length === 0) return false;
|
|
905
|
-
if (block.unicodeRanges) {
|
|
906
|
-
const hasMatch = matchingUsages.some(
|
|
907
|
-
(u) => u.text.length > 0 && textMatchesUnicodeRange(u.text, block.unicodeRanges)
|
|
908
|
-
);
|
|
909
|
-
if (!hasMatch) return false;
|
|
910
|
-
}
|
|
911
|
-
return true;
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
async function fetchAsDataUrl(url) {
|
|
915
|
-
try {
|
|
916
|
-
const res = await fetch(url);
|
|
917
|
-
if (!res.ok) return null;
|
|
918
|
-
const blob = await res.blob();
|
|
919
|
-
return new Promise((resolve, reject) => {
|
|
920
|
-
const reader = new FileReader();
|
|
921
|
-
reader.onload = () => resolve(reader.result);
|
|
922
|
-
reader.onerror = reject;
|
|
923
|
-
reader.readAsDataURL(blob);
|
|
924
|
-
});
|
|
925
|
-
} catch {
|
|
926
|
-
return null;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async function buildFontBlocks() {
|
|
930
|
-
const fontFaceRules = [];
|
|
931
|
-
const links = Array.from(
|
|
932
|
-
document.querySelectorAll('link[rel="stylesheet"]')
|
|
933
|
-
);
|
|
934
|
-
for (const link of links) {
|
|
935
|
-
if (!link.href) continue;
|
|
936
|
-
try {
|
|
937
|
-
const res = await fetch(link.href, { cache: "force-cache" });
|
|
938
|
-
if (!res.ok) continue;
|
|
939
|
-
const cssText = await res.text();
|
|
940
|
-
const sheet = new CSSStyleSheet();
|
|
941
|
-
await sheet.replace(cssText);
|
|
942
|
-
for (const rule of sheet.cssRules) {
|
|
943
|
-
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
944
|
-
fontFaceRules.push(rule.cssText);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
} catch {
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
for (const sheet of Array.from(document.styleSheets)) {
|
|
951
|
-
if (sheet.href) continue;
|
|
952
|
-
try {
|
|
953
|
-
for (const rule of Array.from(sheet.cssRules || [])) {
|
|
954
|
-
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
955
|
-
fontFaceRules.push(rule.cssText);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
} catch {
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
const loadedFamilies = /* @__PURE__ */ new Set();
|
|
962
|
-
if (document.fonts) {
|
|
963
|
-
for (const ff of document.fonts) {
|
|
964
|
-
if (ff.status === "loaded") {
|
|
965
|
-
loadedFamilies.add(
|
|
966
|
-
ff.family.replace(/['"]/g, "").trim().toLowerCase()
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
const candidates = loadedFamilies.size > 0 ? fontFaceRules.filter((r) => loadedFamilies.has(parseFontFamily(r).toLowerCase())) : fontFaceRules;
|
|
972
|
-
const embedded = await Promise.all(
|
|
973
|
-
candidates.map(async (ruleText) => {
|
|
974
|
-
const urlRegex = /url\(\s*['"]?([^'")\s]+)['"]?\s*\)/g;
|
|
975
|
-
const urlMatches = Array.from(ruleText.matchAll(urlRegex));
|
|
976
|
-
let css = ruleText;
|
|
977
|
-
for (const m of urlMatches) {
|
|
978
|
-
const url = m[1];
|
|
979
|
-
if (url.startsWith("data:")) continue;
|
|
980
|
-
const dataUrl = await fetchAsDataUrl(url);
|
|
981
|
-
if (dataUrl) {
|
|
982
|
-
css = css.replace(m[0], `url(${dataUrl})`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
return {
|
|
986
|
-
css,
|
|
987
|
-
family: parseFontFamily(ruleText).toLowerCase(),
|
|
988
|
-
weight: parseFontWeight(ruleText),
|
|
989
|
-
style: parseFontStyle(ruleText),
|
|
990
|
-
unicodeRanges: parseUnicodeRange(ruleText)
|
|
991
|
-
};
|
|
992
|
-
})
|
|
993
|
-
);
|
|
994
|
-
return embedded;
|
|
995
|
-
}
|
|
996
|
-
var HtmlCapture = class {
|
|
997
|
-
constructor(root) {
|
|
998
|
-
/** Elements with an in-flight html-to-image re-capture (dedupe). */
|
|
999
|
-
this._capturing = /* @__PURE__ */ new Set();
|
|
1000
|
-
/**
|
|
1001
|
-
* Optional callback fired when an async re-capture finishes and
|
|
1002
|
-
* the cache changes. Receives the element whose cache entry was
|
|
1003
|
-
* just (re)written so the consumer can scope its dirty marking
|
|
1004
|
-
* to glasses that actually intersect that element.
|
|
1005
|
-
*/
|
|
1006
|
-
this.onCacheUpdate = null;
|
|
1007
|
-
/**
|
|
1008
|
-
* Prefetched + embedded @font-face blocks. Computed once at init
|
|
1009
|
-
* via prefetchFontEmbedCSS. At capture time, filtered per-element
|
|
1010
|
-
* to include only the blocks whose family/weight/style/unicode-range
|
|
1011
|
-
* match the element's actual text content and computed styles.
|
|
1012
|
-
*/
|
|
1013
|
-
this._fontBlocks = [];
|
|
1014
|
-
this.root = root;
|
|
1015
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
1016
|
-
this.dpr = 1;
|
|
1017
|
-
}
|
|
1018
|
-
// ────────────────────────────────────────────
|
|
1019
|
-
// Public API
|
|
1020
|
-
// ────────────────────────────────────────────
|
|
1021
|
-
/**
|
|
1022
|
-
* Resolve the page's @font-face rules into a single CSS string with
|
|
1023
|
-
* every `url(...)` source already inlined as a base64 data URL. The
|
|
1024
|
-
* result is reused on every subsequent toCanvas call so the captured
|
|
1025
|
-
* raster renders text with the page's actual webfonts (e.g. Inter)
|
|
1026
|
-
* instead of system fallbacks. Matching glyph metrics is what makes
|
|
1027
|
-
* the refracted text line up with the live DOM under the glass.
|
|
1028
|
-
*
|
|
1029
|
-
* The build is shared at module scope across every LiquidGlass
|
|
1030
|
-
* instance — the first init() pays the fetch + base64 cost, every
|
|
1031
|
-
* subsequent init() awaits the same Promise.
|
|
1032
|
-
*
|
|
1033
|
-
* Implemented manually rather than via html-to-image's getFontEmbedCSS
|
|
1034
|
-
* because that path walks document.styleSheets via CSSOM, which throws
|
|
1035
|
-
* SecurityError on every cross-origin stylesheet and has a brittle
|
|
1036
|
-
* recovery flow. We just fetch each <link rel="stylesheet"> directly
|
|
1037
|
-
* (CORS-friendly for the typical Google Fonts / CDN cases), regex out
|
|
1038
|
-
* the @font-face blocks, and inline each url(...) ourselves.
|
|
1039
|
-
*/
|
|
1040
|
-
async prefetchFontEmbedCSS() {
|
|
1041
|
-
if (!_sharedFontBlocks) {
|
|
1042
|
-
_sharedFontBlocks = buildFontBlocks();
|
|
1043
|
-
}
|
|
1044
|
-
this._fontBlocks = await _sharedFontBlocks;
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Return the @font-face CSS string for a specific element,
|
|
1048
|
-
* filtered to only the blocks whose family + weight + style
|
|
1049
|
-
* match computed styles on the element's text nodes, AND whose
|
|
1050
|
-
* unicode-range covers at least one codepoint in the element's
|
|
1051
|
-
* text content.
|
|
1052
|
-
*/
|
|
1053
|
-
fontEmbedCSSForElement(element) {
|
|
1054
|
-
if (this._fontBlocks.length === 0) return "";
|
|
1055
|
-
const relevant = filterFontBlocksForElement(this._fontBlocks, element);
|
|
1056
|
-
return relevant.map((b) => b.css).join("\n");
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Update the device pixel ratio used for future captures.
|
|
1060
|
-
*/
|
|
1061
|
-
resize(dpr = 1) {
|
|
1062
|
-
this.dpr = dpr;
|
|
1063
|
-
this.cache.clear();
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Ensure an element's cached canvas is fresh enough for the current DPR.
|
|
1067
|
-
*
|
|
1068
|
-
* Cache semantics:
|
|
1069
|
-
* - Fresh hit (size matches within 0.5 px) → return immediately.
|
|
1070
|
-
* - Stale hit (size differs) → keep the stale entry so callers can
|
|
1071
|
-
* stretch-blit it, and kick off an async re-capture.
|
|
1072
|
-
* - Cache miss → kick off an async capture.
|
|
1073
|
-
*
|
|
1074
|
-
* Concurrent re-captures for the same element are deduplicated
|
|
1075
|
-
* via the `_capturing` set, so calling this every frame is cheap.
|
|
1076
|
-
*/
|
|
1077
|
-
async captureElement(element, force = false) {
|
|
1078
|
-
const rect = element.getBoundingClientRect();
|
|
1079
|
-
const cssW = rect.width;
|
|
1080
|
-
const cssH = rect.height;
|
|
1081
|
-
const w = Math.round(cssW * this.dpr);
|
|
1082
|
-
const h = Math.round(cssH * this.dpr);
|
|
1083
|
-
if (w <= 0 || h <= 0) {
|
|
1084
|
-
this.cache.delete(element);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
const cached = this.cache.get(element);
|
|
1088
|
-
const cacheIsFresh = !!cached && cached.canvas.width > 0 && cached.canvas.height > 0 && Math.abs(cached.w - w) < 0.5 && Math.abs(cached.h - h) < 0.5;
|
|
1089
|
-
if (!force && cacheIsFresh) return;
|
|
1090
|
-
if (this._capturing.has(element)) return;
|
|
1091
|
-
if (element.tagName === "CANVAS") {
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
this._capturing.add(element);
|
|
1095
|
-
try {
|
|
1096
|
-
await this._captureWithHtmlToImage(element, w, h, cssW, cssH);
|
|
1097
|
-
} finally {
|
|
1098
|
-
this._capturing.delete(element);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Draw the current cached capture for an element into an arbitrary
|
|
1103
|
-
* 2D canvas. Returns true when a cached snapshot was available.
|
|
1104
|
-
*/
|
|
1105
|
-
drawCachedElement(element, targetCtx, x, y, w, h) {
|
|
1106
|
-
const cached = this.cache.get(element);
|
|
1107
|
-
if (!cached) return false;
|
|
1108
|
-
if (cached.canvas.width <= 0 || cached.canvas.height <= 0) {
|
|
1109
|
-
this.cache.delete(element);
|
|
1110
|
-
return false;
|
|
1111
|
-
}
|
|
1112
|
-
targetCtx.drawImage(cached.canvas, x, y, w, h);
|
|
1113
|
-
return true;
|
|
1114
|
-
}
|
|
1115
|
-
/**
|
|
1116
|
-
* Capture an element's DOM content as a standalone canvas, optionally
|
|
1117
|
-
* excluding specified child nodes from the capture.
|
|
1118
|
-
*
|
|
1119
|
-
* The hideNodes are pruned from the cloned tree via html-to-image's
|
|
1120
|
-
* filter callback, so the live DOM is never mutated and there is no
|
|
1121
|
-
* visible flicker on the page even when this runs inside the render
|
|
1122
|
-
* loop (e.g. on a re-capture triggered by a content change).
|
|
1123
|
-
*/
|
|
1124
|
-
async captureToCanvas(element, cssW, cssH, hideNodes = null) {
|
|
1125
|
-
if (cssW <= 0 || cssH <= 0) return null;
|
|
1126
|
-
const hideSet = hideNodes && hideNodes.length ? new Set(hideNodes) : null;
|
|
1127
|
-
try {
|
|
1128
|
-
const rendered = await toCanvas(element, {
|
|
1129
|
-
width: cssW,
|
|
1130
|
-
height: cssH,
|
|
1131
|
-
pixelRatio: this.dpr,
|
|
1132
|
-
backgroundColor: void 0,
|
|
1133
|
-
// Reuse the prefetched font embed CSS so the per-glass
|
|
1134
|
-
// content image (used for compositing labels on top of
|
|
1135
|
-
// the shader output) uses the same Inter face the live
|
|
1136
|
-
// page does. Skips html-to-image's noisy CSSOM walk.
|
|
1137
|
-
fontEmbedCSS: this.fontEmbedCSSForElement(element),
|
|
1138
|
-
filter: hideSet ? (node) => !hideSet.has(node) : void 0,
|
|
1139
|
-
style: {
|
|
1140
|
-
position: "static",
|
|
1141
|
-
top: "auto",
|
|
1142
|
-
left: "auto",
|
|
1143
|
-
right: "auto",
|
|
1144
|
-
bottom: "auto",
|
|
1145
|
-
transform: "none",
|
|
1146
|
-
margin: "0"
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
return rendered;
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
console.warn("LiquidGlass: captureToCanvas failed for element:", element, err);
|
|
1152
|
-
return null;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Remove an element's entry from the capture cache.
|
|
1157
|
-
*/
|
|
1158
|
-
invalidateCache(element) {
|
|
1159
|
-
this.cache.delete(element);
|
|
1160
|
-
}
|
|
1161
|
-
/** Destroy the capture system and free resources. */
|
|
1162
|
-
destroy() {
|
|
1163
|
-
this.cache.clear();
|
|
1164
|
-
}
|
|
1165
|
-
// ────────────────────────────────────────────
|
|
1166
|
-
// html-to-image back-end
|
|
1167
|
-
// ────────────────────────────────────────────
|
|
1168
|
-
async _captureWithHtmlToImage(element, w, h, cssW, cssH) {
|
|
1169
|
-
if (cssW <= 0 || cssH <= 0 || w <= 0 || h <= 0) return;
|
|
1170
|
-
try {
|
|
1171
|
-
const rendered = await toCanvas(element, {
|
|
1172
|
-
width: cssW,
|
|
1173
|
-
height: cssH,
|
|
1174
|
-
pixelRatio: this.dpr,
|
|
1175
|
-
// Per-element font embed CSS so the captured raster
|
|
1176
|
-
// uses the page's actual webfont at the correct weight
|
|
1177
|
-
// and unicode subset for this element's text content.
|
|
1178
|
-
fontEmbedCSS: this.fontEmbedCSSForElement(element)
|
|
1179
|
-
});
|
|
1180
|
-
this.cache.set(element, { canvas: rendered, w, h });
|
|
1181
|
-
this.onCacheUpdate?.(element);
|
|
1182
|
-
} catch (err) {
|
|
1183
|
-
console.warn("LiquidGlass: html-to-image capture failed for element:", element, err);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
};
|
|
1187
|
-
|
|
1188
|
-
// src/shaders.ts
|
|
1189
|
-
var VS_QUAD = `
|
|
1190
|
-
attribute vec2 a_pos;
|
|
1191
|
-
varying vec2 v_uv;
|
|
1192
|
-
void main() {
|
|
1193
|
-
v_uv = a_pos * 0.5 + 0.5;
|
|
1194
|
-
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
1195
|
-
}`;
|
|
1196
|
-
var FS_BLIT = `
|
|
1197
|
-
precision mediump float;
|
|
1198
|
-
uniform sampler2D u_tex;
|
|
1199
|
-
uniform vec2 u_scale;
|
|
1200
|
-
uniform vec2 u_offset;
|
|
1201
|
-
varying vec2 v_uv;
|
|
1202
|
-
void main() {
|
|
1203
|
-
gl_FragColor = texture2D(u_tex, v_uv * u_scale + u_offset);
|
|
1204
|
-
}`;
|
|
1205
|
-
var FS_BLUR = `
|
|
1206
|
-
precision mediump float;
|
|
1207
|
-
uniform sampler2D u_tex;
|
|
1208
|
-
uniform vec2 u_dir;
|
|
1209
|
-
varying vec2 v_uv;
|
|
1210
|
-
void main() {
|
|
1211
|
-
vec4 s = texture2D(u_tex, v_uv) * 0.227027;
|
|
1212
|
-
s += texture2D(u_tex, v_uv + u_dir * 1.0) * 0.194594;
|
|
1213
|
-
s += texture2D(u_tex, v_uv - u_dir * 1.0) * 0.194594;
|
|
1214
|
-
s += texture2D(u_tex, v_uv + u_dir * 2.0) * 0.121622;
|
|
1215
|
-
s += texture2D(u_tex, v_uv - u_dir * 2.0) * 0.121622;
|
|
1216
|
-
s += texture2D(u_tex, v_uv + u_dir * 3.0) * 0.054054;
|
|
1217
|
-
s += texture2D(u_tex, v_uv - u_dir * 3.0) * 0.054054;
|
|
1218
|
-
s += texture2D(u_tex, v_uv + u_dir * 4.0) * 0.016216;
|
|
1219
|
-
s += texture2D(u_tex, v_uv - u_dir * 4.0) * 0.016216;
|
|
1220
|
-
gl_FragColor = s;
|
|
1221
|
-
}`;
|
|
1222
|
-
var VS_GLASS = `
|
|
1223
|
-
attribute vec2 a_pos;
|
|
1224
|
-
uniform vec2 u_center; // panel centre in root-pixel coords (top-left origin)
|
|
1225
|
-
uniform vec2 u_size; // panel size in px
|
|
1226
|
-
uniform vec2 u_res; // root element size in px
|
|
1227
|
-
uniform float u_pad; // shadow padding in px
|
|
1228
|
-
varying vec2 v_localPx;
|
|
1229
|
-
varying vec2 v_screenUV;
|
|
1230
|
-
|
|
1231
|
-
void main() {
|
|
1232
|
-
vec2 total = u_size + vec2(u_pad * 2.0);
|
|
1233
|
-
v_localPx = a_pos * total; // px from panel centre
|
|
1234
|
-
vec2 px = u_center + a_pos * total; // screen px (DOM)
|
|
1235
|
-
v_screenUV = vec2(px.x / u_res.x, 1.0 - px.y / u_res.y);
|
|
1236
|
-
vec2 ndc = (px / u_res) * 2.0 - 1.0;
|
|
1237
|
-
ndc.y = -ndc.y;
|
|
1238
|
-
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
1239
|
-
}`;
|
|
1240
|
-
var FS_GLASS = `
|
|
1241
|
-
precision highp float;
|
|
1242
|
-
|
|
1243
|
-
uniform sampler2D u_bgTex;
|
|
1244
|
-
uniform sampler2D u_blurTex;
|
|
1245
|
-
uniform vec2 u_size; // panel px
|
|
1246
|
-
uniform float u_radius; // corner radius px
|
|
1247
|
-
uniform vec2 u_res;
|
|
1248
|
-
|
|
1249
|
-
uniform float u_refract;
|
|
1250
|
-
uniform float u_chroma;
|
|
1251
|
-
uniform float u_edgeHL;
|
|
1252
|
-
uniform float u_spec;
|
|
1253
|
-
uniform float u_fresnel;
|
|
1254
|
-
uniform float u_distort;
|
|
1255
|
-
uniform float u_alpha;
|
|
1256
|
-
uniform float u_sat;
|
|
1257
|
-
uniform float u_tint;
|
|
1258
|
-
uniform float u_zRadius;
|
|
1259
|
-
uniform float u_brightness;
|
|
1260
|
-
uniform float u_shadowAlpha;
|
|
1261
|
-
uniform float u_shadowSpread;
|
|
1262
|
-
uniform float u_shadowOffY;
|
|
1263
|
-
uniform float u_bevelMode;
|
|
1264
|
-
|
|
1265
|
-
varying vec2 v_localPx;
|
|
1266
|
-
varying vec2 v_screenUV;
|
|
1267
|
-
|
|
1268
|
-
// Rounded-rect signed distance
|
|
1269
|
-
float rrSDF(vec2 p, vec2 b, float r) {
|
|
1270
|
-
vec2 q = abs(p) - b + vec2(r);
|
|
1271
|
-
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// Bevel height field.
|
|
1275
|
-
// Both modes use the same half-circle profile (smooth peak at centre,
|
|
1276
|
-
// steep at edges). The difference is in the refraction model:
|
|
1277
|
-
// mode 0 = biconvex pill \u2014 light refracts at both surfaces (entry + exit).
|
|
1278
|
-
// mode 1 = dome (plano-convex) \u2014 flat bottom, so only exit refraction.
|
|
1279
|
-
// d = distance inside from edge (-sdf), zR = z-radius of the bevel.
|
|
1280
|
-
float bevelHeight(float d, float zR) {
|
|
1281
|
-
if (d <= 0.0) return 0.0;
|
|
1282
|
-
if (d >= zR) return zR;
|
|
1283
|
-
return sqrt(d * (2.0 * zR - d));
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
float hash(vec2 p) {
|
|
1287
|
-
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
void main() {
|
|
1291
|
-
vec2 half_ = u_size * 0.5;
|
|
1292
|
-
float r = min(u_radius, min(half_.x, half_.y));
|
|
1293
|
-
float sdf = rrSDF(v_localPx, half_, r);
|
|
1294
|
-
|
|
1295
|
-
// \u2500\u2500 Shadow (outside panel, offset by shadowOffY) \u2500\u2500
|
|
1296
|
-
if (sdf > 0.0) {
|
|
1297
|
-
float sdfShadow = rrSDF(v_localPx - vec2(0.0, u_shadowOffY), half_, r);
|
|
1298
|
-
float d = max(sdfShadow - 1.0, 0.0);
|
|
1299
|
-
float spread = max(u_shadowSpread, 1.0);
|
|
1300
|
-
float falloff = 1.0 / (spread * spread);
|
|
1301
|
-
float outerShadow = exp(-d * d * falloff) * 0.65;
|
|
1302
|
-
float contactShadow = exp(-d * 0.08 / max(spread * 0.04, 0.01)) * 0.35;
|
|
1303
|
-
float shadow = (outerShadow + contactShadow) * u_shadowAlpha;
|
|
1304
|
-
gl_FragColor = vec4(0.0, 0.0, 0.0, shadow);
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// \u2500\u2500 Anti-aliased mask \u2500\u2500
|
|
1309
|
-
float mask = 1.0 - smoothstep(-1.5, 0.5, sdf);
|
|
1310
|
-
|
|
1311
|
-
float maxD = min(half_.x, half_.y);
|
|
1312
|
-
float inside = -sdf;
|
|
1313
|
-
float edge = smoothstep(maxD * 0.35, 0.0, inside);
|
|
1314
|
-
|
|
1315
|
-
// \u2500\u2500 Surface normal (top surface) via bevel height field \u2500\u2500
|
|
1316
|
-
float zR = u_zRadius;
|
|
1317
|
-
float e = 2.0;
|
|
1318
|
-
float dC = inside;
|
|
1319
|
-
float dR = -rrSDF(v_localPx + vec2(e, 0.0), half_, r);
|
|
1320
|
-
float dL = -rrSDF(v_localPx - vec2(e, 0.0), half_, r);
|
|
1321
|
-
float dU = -rrSDF(v_localPx + vec2(0.0, e), half_, r);
|
|
1322
|
-
float dD = -rrSDF(v_localPx - vec2(0.0, e), half_, r);
|
|
1323
|
-
float hC = bevelHeight(dC, zR);
|
|
1324
|
-
float hR = bevelHeight(dR, zR);
|
|
1325
|
-
float hL = bevelHeight(dL, zR);
|
|
1326
|
-
float hU = bevelHeight(dU, zR);
|
|
1327
|
-
float hD = bevelHeight(dD, zR);
|
|
1328
|
-
vec2 hGrad = vec2(hR - hL, hU - hD) / (2.0 * e);
|
|
1329
|
-
vec3 N = normalize(vec3(-hGrad, 1.0));
|
|
1330
|
-
|
|
1331
|
-
float depth = smoothstep(0.0, zR, inside);
|
|
1332
|
-
|
|
1333
|
-
// \u2500\u2500 Refraction \u2500\u2500
|
|
1334
|
-
vec2 pxToUV = vec2(1.0, -1.0) / u_res;
|
|
1335
|
-
float ior = 1.5;
|
|
1336
|
-
float refrPow = 1.0 - 1.0 / ior;
|
|
1337
|
-
float thickness = hC * 2.0;
|
|
1338
|
-
float thickNorm = thickness / max(zR * 2.0, 1.0);
|
|
1339
|
-
vec2 refrPx;
|
|
1340
|
-
if (u_bevelMode < 0.5) {
|
|
1341
|
-
// Biconvex: physically-based dual-surface refraction
|
|
1342
|
-
vec2 exitRefr = hGrad * refrPow;
|
|
1343
|
-
vec2 entryRefr = hGrad * refrPow;
|
|
1344
|
-
vec2 throughRefr = entryRefr * thickNorm * 0.5;
|
|
1345
|
-
refrPx = (exitRefr + entryRefr + throughRefr) * u_refract * 30.0;
|
|
1346
|
-
vec2 centerDir = -v_localPx / max(half_, vec2(1.0));
|
|
1347
|
-
refrPx += centerDir * u_refract * 4.0 * depth;
|
|
1348
|
-
} else {
|
|
1349
|
-
// Dome (plano-convex): uniform magnification by contracting UV toward center.
|
|
1350
|
-
// Each pixel samples from closer to center \u2192 content appears larger.
|
|
1351
|
-
refrPx = -v_localPx * u_refract * depth * 0.35;
|
|
1352
|
-
}
|
|
1353
|
-
vec2 refr = refrPx * pxToUV;
|
|
1354
|
-
|
|
1355
|
-
// \u2500\u2500 Micro-distortion noise \u2500\u2500
|
|
1356
|
-
vec2 ns = v_localPx * 0.08;
|
|
1357
|
-
vec2 absPxToUV = vec2(1.0) / u_res;
|
|
1358
|
-
vec2 micro = (vec2(hash(ns), hash(ns + vec2(37.0))) - 0.5) * u_distort * 4.0 * absPxToUV;
|
|
1359
|
-
|
|
1360
|
-
// \u2500\u2500 Chromatic aberration \u2500\u2500
|
|
1361
|
-
float caS = u_chroma * 18.0 * (edge * 0.7 + 0.3) * 2.0;
|
|
1362
|
-
vec2 caD = N.xy * caS * pxToUV;
|
|
1363
|
-
vec2 base = v_screenUV + refr + micro;
|
|
1364
|
-
|
|
1365
|
-
vec3 sharp = vec3(
|
|
1366
|
-
texture2D(u_bgTex, base + caD).r,
|
|
1367
|
-
texture2D(u_bgTex, base).g,
|
|
1368
|
-
texture2D(u_bgTex, base - caD).b
|
|
1369
|
-
);
|
|
1370
|
-
vec3 blur = vec3(
|
|
1371
|
-
texture2D(u_blurTex, base + caD).r,
|
|
1372
|
-
texture2D(u_blurTex, base).g,
|
|
1373
|
-
texture2D(u_blurTex, base - caD).b
|
|
1374
|
-
);
|
|
1375
|
-
// \u2500\u2500 Edge-weighted blur mix \u2500\u2500
|
|
1376
|
-
// Centre of the panel uses the blurred sample; the rim blends
|
|
1377
|
-
// toward the sharp sample so refraction edges stay crisp.
|
|
1378
|
-
float edgeMix = (1.0 - edge * 0.15);
|
|
1379
|
-
vec3 col = mix(sharp, blur, edgeMix);
|
|
1380
|
-
|
|
1381
|
-
// \u2500\u2500 Brightness \u2500\u2500
|
|
1382
|
-
col *= 1.0 + u_brightness;
|
|
1383
|
-
|
|
1384
|
-
// \u2500\u2500 Saturation \u2500\u2500
|
|
1385
|
-
float lum = dot(col, vec3(0.299, 0.587, 0.114));
|
|
1386
|
-
col = mix(vec3(lum), col, 1.0 + u_sat);
|
|
1387
|
-
|
|
1388
|
-
// \u2500\u2500 Cool glass tint \u2500\u2500
|
|
1389
|
-
col = mix(col, col * vec3(0.92, 0.95, 1.05), u_tint);
|
|
1390
|
-
col *= 1.0 + 0.06 * depth;
|
|
1391
|
-
|
|
1392
|
-
// \u2500\u2500 Fresnel \u2500\u2500
|
|
1393
|
-
float fres = pow(1.0 - abs(N.z), 4.0) * u_fresnel;
|
|
1394
|
-
|
|
1395
|
-
// \u2500\u2500 Specular highlights (multi-light Blinn-Phong) \u2500\u2500
|
|
1396
|
-
vec3 V = vec3(0.0, 0.0, 1.0);
|
|
1397
|
-
vec3 L1 = normalize(vec3(0.4, 0.7, 1.0));
|
|
1398
|
-
vec3 H1 = normalize(L1 + V);
|
|
1399
|
-
float sp1 = pow(max(dot(N, H1), 0.0), 90.0);
|
|
1400
|
-
vec3 L2 = normalize(vec3(-0.3, -0.5, 1.0));
|
|
1401
|
-
vec3 H2 = normalize(L2 + V);
|
|
1402
|
-
float sp2 = pow(max(dot(N, H2), 0.0), 50.0) * 0.3;
|
|
1403
|
-
vec3 L3 = normalize(vec3(0.1, 0.3, 1.0));
|
|
1404
|
-
float spB = pow(max(dot(N, L3), 0.0), 6.0) * 0.1;
|
|
1405
|
-
vec3 L4 = normalize(vec3(0.0, 0.9, 0.4));
|
|
1406
|
-
vec3 H4 = normalize(L4 + V);
|
|
1407
|
-
float sp4 = pow(max(dot(N, H4), 0.0), 120.0) * 0.6;
|
|
1408
|
-
float totalSpec = (sp1 + sp2 + spB + sp4) * u_spec;
|
|
1409
|
-
|
|
1410
|
-
// \u2500\u2500 Inner border / stroke highlight \u2500\u2500
|
|
1411
|
-
float borderWidth = 1.5;
|
|
1412
|
-
float innerStroke = smoothstep(-borderWidth - 1.0, -borderWidth, sdf)
|
|
1413
|
-
* (1.0 - smoothstep(-1.0, 0.0, sdf));
|
|
1414
|
-
float topBias = 0.5 + 0.5 * (-v_localPx.y / half_.y);
|
|
1415
|
-
innerStroke *= (0.4 + 0.6 * topBias);
|
|
1416
|
-
|
|
1417
|
-
// \u2500\u2500 Edge highlight & inner glow \u2500\u2500
|
|
1418
|
-
float rim = edge * u_edgeHL * 0.22;
|
|
1419
|
-
float innerGlow = smoothstep(5.0, 0.0, -sdf) * u_edgeHL * 0.15;
|
|
1420
|
-
|
|
1421
|
-
// \u2500\u2500 Environment-like reflection (fake) \u2500\u2500
|
|
1422
|
-
float envRefl = (N.y * 0.5 + 0.5) * fres * 0.08;
|
|
1423
|
-
|
|
1424
|
-
// \u2500\u2500 Composite \u2500\u2500
|
|
1425
|
-
vec3 fin = col;
|
|
1426
|
-
fin += vec3(totalSpec);
|
|
1427
|
-
fin += vec3(rim + innerGlow);
|
|
1428
|
-
fin += vec3(innerStroke * u_edgeHL * 0.55);
|
|
1429
|
-
fin += vec3(envRefl);
|
|
1430
|
-
fin = mix(fin, vec3(1.0), fres * 0.2);
|
|
1431
|
-
|
|
1432
|
-
gl_FragColor = vec4(fin, mask * u_alpha);
|
|
1433
|
-
}`;
|
|
1434
|
-
|
|
1435
|
-
// src/GlassRenderer.ts
|
|
1436
|
-
var GlassRenderer = class {
|
|
1437
|
-
constructor() {
|
|
1438
|
-
this.fboCache = /* @__PURE__ */ new Map();
|
|
1439
|
-
this.activeFBOs = null;
|
|
1440
|
-
this.bgTex = null;
|
|
1441
|
-
this.width = 0;
|
|
1442
|
-
this.height = 0;
|
|
1443
|
-
this.contextLost = false;
|
|
1444
|
-
this.canvas = document.createElement("canvas");
|
|
1445
|
-
this.canvas.style.display = "none";
|
|
1446
|
-
document.body.appendChild(this.canvas);
|
|
1447
|
-
this.cropCanvas = document.createElement("canvas");
|
|
1448
|
-
this.cropCtx = this.cropCanvas.getContext("2d");
|
|
1449
|
-
const gl = this.canvas.getContext("webgl", {
|
|
1450
|
-
alpha: true,
|
|
1451
|
-
premultipliedAlpha: false,
|
|
1452
|
-
antialias: false,
|
|
1453
|
-
preserveDrawingBuffer: true
|
|
1454
|
-
});
|
|
1455
|
-
if (!gl) {
|
|
1456
|
-
throw new Error("LiquidGlass: WebGL is not supported in this browser.");
|
|
1457
|
-
}
|
|
1458
|
-
this.gl = gl;
|
|
1459
|
-
this._initPrograms();
|
|
1460
|
-
this._initBuffers();
|
|
1461
|
-
this._onContextLost = (e) => {
|
|
1462
|
-
e.preventDefault();
|
|
1463
|
-
this.contextLost = true;
|
|
1464
|
-
console.warn("LiquidGlass: WebGL context lost.");
|
|
1465
|
-
};
|
|
1466
|
-
this._onContextRestored = () => {
|
|
1467
|
-
console.info("LiquidGlass: WebGL context restored \u2014 reinitialising.");
|
|
1468
|
-
this.contextLost = false;
|
|
1469
|
-
this._initPrograms();
|
|
1470
|
-
this._initBuffers();
|
|
1471
|
-
for (const fboSet of this.fboCache.values()) {
|
|
1472
|
-
this._freeFBOSet(fboSet);
|
|
1473
|
-
}
|
|
1474
|
-
this.fboCache.clear();
|
|
1475
|
-
this.activeFBOs = null;
|
|
1476
|
-
this.bgTex = null;
|
|
1477
|
-
};
|
|
1478
|
-
this.canvas.addEventListener("webglcontextlost", this._onContextLost);
|
|
1479
|
-
this.canvas.addEventListener("webglcontextrestored", this._onContextRestored);
|
|
1480
|
-
}
|
|
1481
|
-
// ────────────────────────────────────────────
|
|
1482
|
-
// Initialisation
|
|
1483
|
-
// ────────────────────────────────────────────
|
|
1484
|
-
_initPrograms() {
|
|
1485
|
-
this.blitP = this._link(VS_QUAD, FS_BLIT);
|
|
1486
|
-
this.blitU = this._uloc(this.blitP, ["u_tex", "u_scale", "u_offset"]);
|
|
1487
|
-
this.blurP = this._link(VS_QUAD, FS_BLUR);
|
|
1488
|
-
this.blurU = this._uloc(this.blurP, ["u_tex", "u_dir"]);
|
|
1489
|
-
this.glassP = this._link(VS_GLASS, FS_GLASS);
|
|
1490
|
-
this.glassU = this._uloc(this.glassP, [
|
|
1491
|
-
"u_bgTex",
|
|
1492
|
-
"u_blurTex",
|
|
1493
|
-
"u_center",
|
|
1494
|
-
"u_size",
|
|
1495
|
-
"u_radius",
|
|
1496
|
-
"u_res",
|
|
1497
|
-
"u_pad",
|
|
1498
|
-
"u_refract",
|
|
1499
|
-
"u_chroma",
|
|
1500
|
-
"u_edgeHL",
|
|
1501
|
-
"u_spec",
|
|
1502
|
-
"u_fresnel",
|
|
1503
|
-
"u_distort",
|
|
1504
|
-
"u_alpha",
|
|
1505
|
-
"u_sat",
|
|
1506
|
-
"u_tint",
|
|
1507
|
-
"u_zRadius",
|
|
1508
|
-
"u_brightness",
|
|
1509
|
-
"u_shadowAlpha",
|
|
1510
|
-
"u_shadowSpread",
|
|
1511
|
-
"u_shadowOffY",
|
|
1512
|
-
"u_bevelMode"
|
|
1513
|
-
]);
|
|
1514
|
-
}
|
|
1515
|
-
_initBuffers() {
|
|
1516
|
-
const gl = this.gl;
|
|
1517
|
-
this.quadBuf = gl.createBuffer();
|
|
1518
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuf);
|
|
1519
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
1520
|
-
this.panelBuf = gl.createBuffer();
|
|
1521
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.panelBuf);
|
|
1522
|
-
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), gl.STATIC_DRAW);
|
|
1523
|
-
}
|
|
1524
|
-
// ────────────────────────────────────────────
|
|
1525
|
-
// Resize
|
|
1526
|
-
// ────────────────────────────────────────────
|
|
1527
|
-
resize(width, height) {
|
|
1528
|
-
this.width = width;
|
|
1529
|
-
this.height = height;
|
|
1530
|
-
for (const fboSet of this.fboCache.values()) {
|
|
1531
|
-
this._freeFBOSet(fboSet);
|
|
1532
|
-
}
|
|
1533
|
-
this.fboCache.clear();
|
|
1534
|
-
this.activeFBOs = null;
|
|
1535
|
-
this.canvas.width = 0;
|
|
1536
|
-
this.canvas.height = 0;
|
|
1537
|
-
}
|
|
1538
|
-
// ────────────────────────────────────────────
|
|
1539
|
-
// Background upload
|
|
1540
|
-
// ────────────────────────────────────────────
|
|
1541
|
-
uploadAndBlur(sourceCanvas, sourceX, sourceY, width, height, blurAmount) {
|
|
1542
|
-
if (this.contextLost) return;
|
|
1543
|
-
const gl = this.gl;
|
|
1544
|
-
if (!this._setActiveSize(width, height)) return;
|
|
1545
|
-
const W = this.width;
|
|
1546
|
-
const H = this.height;
|
|
1547
|
-
const fboSet = this.activeFBOs;
|
|
1548
|
-
this.cropCanvas.width = W;
|
|
1549
|
-
this.cropCanvas.height = H;
|
|
1550
|
-
this.cropCtx.clearRect(0, 0, W, H);
|
|
1551
|
-
this.cropCtx.drawImage(sourceCanvas, -sourceX, -sourceY);
|
|
1552
|
-
if (!this.bgTex) {
|
|
1553
|
-
this.bgTex = gl.createTexture();
|
|
1554
|
-
}
|
|
1555
|
-
gl.bindTexture(gl.TEXTURE_2D, this.bgTex);
|
|
1556
|
-
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
1557
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.cropCanvas);
|
|
1558
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1559
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1560
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1561
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1562
|
-
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1563
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fboSet.bg.fbo);
|
|
1564
|
-
gl.viewport(0, 0, W, H);
|
|
1565
|
-
gl.useProgram(this.blitP);
|
|
1566
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
1567
|
-
gl.bindTexture(gl.TEXTURE_2D, this.bgTex);
|
|
1568
|
-
gl.uniform1i(this.blitU.u_tex, 0);
|
|
1569
|
-
gl.uniform2f(this.blitU.u_scale, 1, 1);
|
|
1570
|
-
gl.uniform2f(this.blitU.u_offset, 0, 0);
|
|
1571
|
-
this._drawQuad(this.blitP, this.quadBuf);
|
|
1572
|
-
const bw = fboSet.blurA.w;
|
|
1573
|
-
const bh = fboSet.blurA.h;
|
|
1574
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fboSet.blurA.fbo);
|
|
1575
|
-
gl.viewport(0, 0, bw, bh);
|
|
1576
|
-
gl.bindTexture(gl.TEXTURE_2D, fboSet.bg.tex);
|
|
1577
|
-
this._drawQuad(this.blitP, this.quadBuf);
|
|
1578
|
-
if (blurAmount > 0) {
|
|
1579
|
-
const spread = blurAmount * 2.5;
|
|
1580
|
-
gl.useProgram(this.blurP);
|
|
1581
|
-
gl.uniform1i(this.blurU.u_tex, 0);
|
|
1582
|
-
for (let i = 0; i < BLUR_ITERATIONS; i++) {
|
|
1583
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fboSet.blurB.fbo);
|
|
1584
|
-
gl.viewport(0, 0, bw, bh);
|
|
1585
|
-
gl.bindTexture(gl.TEXTURE_2D, fboSet.blurA.tex);
|
|
1586
|
-
gl.uniform2f(this.blurU.u_dir, spread / bw, 0);
|
|
1587
|
-
this._drawQuad(this.blurP, this.quadBuf);
|
|
1588
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fboSet.blurA.fbo);
|
|
1589
|
-
gl.bindTexture(gl.TEXTURE_2D, fboSet.blurB.tex);
|
|
1590
|
-
gl.uniform2f(this.blurU.u_dir, 0, spread / bh);
|
|
1591
|
-
this._drawQuad(this.blurP, this.quadBuf);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
// ────────────────────────────────────────────
|
|
1596
|
-
// Glass panel rendering
|
|
1597
|
-
// ────────────────────────────────────────────
|
|
1598
|
-
renderGlassPanel(config, width, height, dpr) {
|
|
1599
|
-
if (this.contextLost) return;
|
|
1600
|
-
const gl = this.gl;
|
|
1601
|
-
const W = this.width;
|
|
1602
|
-
const H = this.height;
|
|
1603
|
-
const fboSet = this.activeFBOs;
|
|
1604
|
-
gl.enable(gl.BLEND);
|
|
1605
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1606
|
-
gl.useProgram(this.glassP);
|
|
1607
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
1608
|
-
gl.bindTexture(gl.TEXTURE_2D, fboSet.bg.tex);
|
|
1609
|
-
gl.uniform1i(this.glassU.u_bgTex, 0);
|
|
1610
|
-
gl.activeTexture(gl.TEXTURE1);
|
|
1611
|
-
gl.bindTexture(gl.TEXTURE_2D, fboSet.blurA.tex);
|
|
1612
|
-
gl.uniform1i(this.glassU.u_blurTex, 1);
|
|
1613
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1614
|
-
gl.viewport(0, this.canvas.height - H, W, H);
|
|
1615
|
-
gl.uniform2f(this.glassU.u_res, W, H);
|
|
1616
|
-
gl.uniform2f(this.glassU.u_center, W * 0.5, H * 0.5);
|
|
1617
|
-
gl.uniform2f(this.glassU.u_size, width * dpr, height * dpr);
|
|
1618
|
-
gl.uniform1f(this.glassU.u_radius, config.cornerRadius * dpr);
|
|
1619
|
-
gl.uniform1f(this.glassU.u_pad, SHADOW_PAD * dpr);
|
|
1620
|
-
gl.uniform1f(this.glassU.u_refract, config.refraction);
|
|
1621
|
-
gl.uniform1f(this.glassU.u_chroma, config.chromAberration);
|
|
1622
|
-
gl.uniform1f(this.glassU.u_edgeHL, config.edgeHighlight);
|
|
1623
|
-
gl.uniform1f(this.glassU.u_spec, config.specular);
|
|
1624
|
-
gl.uniform1f(this.glassU.u_fresnel, config.fresnel);
|
|
1625
|
-
gl.uniform1f(this.glassU.u_distort, config.distortion);
|
|
1626
|
-
gl.uniform1f(this.glassU.u_alpha, config.opacity);
|
|
1627
|
-
gl.uniform1f(this.glassU.u_sat, config.saturation);
|
|
1628
|
-
gl.uniform1f(this.glassU.u_tint, config.tintStrength);
|
|
1629
|
-
gl.uniform1f(this.glassU.u_zRadius, config.zRadius * dpr);
|
|
1630
|
-
gl.uniform1f(this.glassU.u_brightness, config.brightness);
|
|
1631
|
-
gl.uniform1f(this.glassU.u_shadowAlpha, config.shadowOpacity);
|
|
1632
|
-
gl.uniform1f(this.glassU.u_shadowSpread, config.shadowSpread * dpr);
|
|
1633
|
-
gl.uniform1f(this.glassU.u_shadowOffY, config.shadowOffsetY * dpr);
|
|
1634
|
-
gl.uniform1f(this.glassU.u_bevelMode, config.bevelMode);
|
|
1635
|
-
this._drawQuad(this.glassP, this.panelBuf);
|
|
1636
|
-
gl.disable(gl.BLEND);
|
|
1637
|
-
}
|
|
1638
|
-
clear() {
|
|
1639
|
-
const gl = this.gl;
|
|
1640
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1641
|
-
gl.viewport(0, this.canvas.height - this.height, this.width, this.height);
|
|
1642
|
-
gl.enable(gl.SCISSOR_TEST);
|
|
1643
|
-
gl.scissor(0, this.canvas.height - this.height, this.width, this.height);
|
|
1644
|
-
gl.clearColor(0, 0, 0, 0);
|
|
1645
|
-
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1646
|
-
gl.disable(gl.SCISSOR_TEST);
|
|
1647
|
-
}
|
|
1648
|
-
destroy() {
|
|
1649
|
-
this.canvas.removeEventListener("webglcontextlost", this._onContextLost);
|
|
1650
|
-
this.canvas.removeEventListener("webglcontextrestored", this._onContextRestored);
|
|
1651
|
-
if (!this.contextLost) {
|
|
1652
|
-
const gl = this.gl;
|
|
1653
|
-
for (const fboSet of this.fboCache.values()) {
|
|
1654
|
-
this._freeFBOSet(fboSet);
|
|
1655
|
-
}
|
|
1656
|
-
this.fboCache.clear();
|
|
1657
|
-
if (this.bgTex) gl.deleteTexture(this.bgTex);
|
|
1658
|
-
gl.deleteBuffer(this.quadBuf);
|
|
1659
|
-
gl.deleteBuffer(this.panelBuf);
|
|
1660
|
-
gl.deleteProgram(this.blitP);
|
|
1661
|
-
gl.deleteProgram(this.blurP);
|
|
1662
|
-
gl.deleteProgram(this.glassP);
|
|
1663
|
-
}
|
|
1664
|
-
this.canvas.remove();
|
|
1665
|
-
}
|
|
1666
|
-
// ────────────────────────────────────────────
|
|
1667
|
-
// FBO management
|
|
1668
|
-
// ────────────────────────────────────────────
|
|
1669
|
-
_setActiveSize(w, h) {
|
|
1670
|
-
if (w <= 0 || h <= 0) return false;
|
|
1671
|
-
this.width = w;
|
|
1672
|
-
this.height = h;
|
|
1673
|
-
if (this.canvas.width < w || this.canvas.height < h) {
|
|
1674
|
-
this.canvas.width = Math.max(this.canvas.width, w);
|
|
1675
|
-
this.canvas.height = Math.max(this.canvas.height, h);
|
|
1676
|
-
}
|
|
1677
|
-
const key = `${w}x${h}`;
|
|
1678
|
-
let fboSet = this.fboCache.get(key);
|
|
1679
|
-
if (!fboSet) {
|
|
1680
|
-
fboSet = {
|
|
1681
|
-
bg: this._makeFBO(w, h),
|
|
1682
|
-
blurA: this._makeFBO(w, h),
|
|
1683
|
-
blurB: this._makeFBO(w, h)
|
|
1684
|
-
};
|
|
1685
|
-
this.fboCache.set(key, fboSet);
|
|
1686
|
-
}
|
|
1687
|
-
this.activeFBOs = fboSet;
|
|
1688
|
-
return true;
|
|
1689
|
-
}
|
|
1690
|
-
_makeFBO(w, h) {
|
|
1691
|
-
const gl = this.gl;
|
|
1692
|
-
const tex = gl.createTexture();
|
|
1693
|
-
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1694
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
1695
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1696
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1697
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1698
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1699
|
-
const fbo = gl.createFramebuffer();
|
|
1700
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1701
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
1702
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1703
|
-
return { fbo, tex, w, h };
|
|
1704
|
-
}
|
|
1705
|
-
_freeFBO(fboObj) {
|
|
1706
|
-
if (!fboObj) return;
|
|
1707
|
-
const gl = this.gl;
|
|
1708
|
-
gl.deleteFramebuffer(fboObj.fbo);
|
|
1709
|
-
gl.deleteTexture(fboObj.tex);
|
|
1710
|
-
}
|
|
1711
|
-
_freeFBOSet(fboSet) {
|
|
1712
|
-
this._freeFBO(fboSet.bg);
|
|
1713
|
-
this._freeFBO(fboSet.blurA);
|
|
1714
|
-
this._freeFBO(fboSet.blurB);
|
|
1715
|
-
}
|
|
1716
|
-
// ────────────────────────────────────────────
|
|
1717
|
-
// Shader helpers
|
|
1718
|
-
// ────────────────────────────────────────────
|
|
1719
|
-
_compile(src, type) {
|
|
1720
|
-
const gl = this.gl;
|
|
1721
|
-
const s = gl.createShader(type);
|
|
1722
|
-
gl.shaderSource(s, src);
|
|
1723
|
-
gl.compileShader(s);
|
|
1724
|
-
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
1725
|
-
console.error("LiquidGlass shader compile error:", gl.getShaderInfoLog(s), src);
|
|
1726
|
-
return null;
|
|
1727
|
-
}
|
|
1728
|
-
return s;
|
|
1729
|
-
}
|
|
1730
|
-
_link(vsSrc, fsSrc) {
|
|
1731
|
-
const gl = this.gl;
|
|
1732
|
-
const p = gl.createProgram();
|
|
1733
|
-
gl.attachShader(p, this._compile(vsSrc, gl.VERTEX_SHADER));
|
|
1734
|
-
gl.attachShader(p, this._compile(fsSrc, gl.FRAGMENT_SHADER));
|
|
1735
|
-
gl.linkProgram(p);
|
|
1736
|
-
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
|
1737
|
-
console.error("LiquidGlass program link error:", gl.getProgramInfoLog(p));
|
|
1738
|
-
}
|
|
1739
|
-
return p;
|
|
1740
|
-
}
|
|
1741
|
-
_uloc(prog, names) {
|
|
1742
|
-
const gl = this.gl;
|
|
1743
|
-
const u = {};
|
|
1744
|
-
for (const n of names) {
|
|
1745
|
-
u[n] = gl.getUniformLocation(prog, n);
|
|
1746
|
-
}
|
|
1747
|
-
return u;
|
|
1748
|
-
}
|
|
1749
|
-
_drawQuad(prog, buf) {
|
|
1750
|
-
const gl = this.gl;
|
|
1751
|
-
const loc = gl.getAttribLocation(prog, "a_pos");
|
|
1752
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
1753
|
-
gl.enableVertexAttribArray(loc);
|
|
1754
|
-
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
1755
|
-
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
|
|
1759
|
-
// src/LiquidGlass.ts
|
|
1760
|
-
var BUTTON_CLASS = "liquid-glass-button";
|
|
1761
|
-
var STYLE_ID = "liquid-glass-button-styles";
|
|
1762
|
-
var BUTTON_CSS = `
|
|
1763
|
-
.${BUTTON_CLASS} {
|
|
1764
|
-
cursor: pointer;
|
|
1765
|
-
}
|
|
1766
|
-
`;
|
|
1767
|
-
var LiquidGlass = class _LiquidGlass {
|
|
1768
|
-
// ────────────────────────────────────────────
|
|
1769
|
-
// Constructor (prefer LiquidGlass.init)
|
|
1770
|
-
// ────────────────────────────────────────────
|
|
1771
|
-
constructor({ root, glassElements, defaults = {} }) {
|
|
1772
|
-
/** Current frames-per-second (updated every frame). */
|
|
1773
|
-
this.fps = 0;
|
|
1774
|
-
this._running = false;
|
|
1775
|
-
this._rafId = 0;
|
|
1776
|
-
this._hasDynamic = false;
|
|
1777
|
-
/**
|
|
1778
|
-
* Genuinely-global dirty flag — set by events that legitimately
|
|
1779
|
-
* affect every glass at once (resize, WebGL context restored,
|
|
1780
|
-
* structural mutation of root, end of _start). On the next frame
|
|
1781
|
-
* the entry guard promotes it into per-element dirty marks for
|
|
1782
|
-
* every glass in glassSet, then clears itself.
|
|
1783
|
-
*/
|
|
1784
|
-
this._globalDirty = true;
|
|
1785
|
-
/**
|
|
1786
|
-
* Per-element shader-render dirty set. Each entry is a glass
|
|
1787
|
-
* element that needs its WebGL pipeline to re-run on the next
|
|
1788
|
-
* frame. Drained at the end of _renderFrame.
|
|
1789
|
-
*
|
|
1790
|
-
* Mirrors _glassContentDirty (which tracks html-to-image content
|
|
1791
|
-
* captures) but for the WebGL shader pass instead of the DOM
|
|
1792
|
-
* raster pass — they have different triggers.
|
|
1793
|
-
*/
|
|
1794
|
-
this._glassDirty = /* @__PURE__ */ new Set();
|
|
1795
|
-
/**
|
|
1796
|
-
* Elements (typically wrappers, glasses themselves, or descendants
|
|
1797
|
-
* of root) explicitly marked changed via the public markChanged()
|
|
1798
|
-
* API. The next frame fans each one out into _glassDirty by
|
|
1799
|
-
* intersecting against every glass's sample rect, then clears
|
|
1800
|
-
* the set.
|
|
1801
|
-
*/
|
|
1802
|
-
this._userMarkedChanged = /* @__PURE__ */ new Set();
|
|
1803
|
-
this._capturingGlassContent = false;
|
|
1804
|
-
/**
|
|
1805
|
-
* Glass elements whose content image is stale and needs to be
|
|
1806
|
-
* re-captured. Per-element rather than a single global flag so a
|
|
1807
|
-
* mutation inside one glass subtree only re-captures that one
|
|
1808
|
-
* element instead of every glass on the page.
|
|
1809
|
-
*/
|
|
1810
|
-
this._glassContentDirty = /* @__PURE__ */ new Set();
|
|
1811
|
-
this._fpsFrames = 0;
|
|
1812
|
-
this._fpsTime = 0;
|
|
1813
|
-
this._observer = null;
|
|
1814
|
-
this._glassSubtreeObserver = null;
|
|
1815
|
-
this._sortedChildren = [];
|
|
1816
|
-
this._glassCache = /* @__PURE__ */ new Map();
|
|
1817
|
-
this._glassContentImages = /* @__PURE__ */ new Map();
|
|
1818
|
-
this._glassLastSize = /* @__PURE__ */ new Map();
|
|
1819
|
-
this._buttonStates = /* @__PURE__ */ new Map();
|
|
1820
|
-
this._buttonListeners = /* @__PURE__ */ new Map();
|
|
1821
|
-
this._drag = {
|
|
1822
|
-
active: false,
|
|
1823
|
-
element: null,
|
|
1824
|
-
startX: 0,
|
|
1825
|
-
startY: 0,
|
|
1826
|
-
origTx: 0,
|
|
1827
|
-
origTy: 0
|
|
1828
|
-
};
|
|
1829
|
-
if (!root) throw new Error("LiquidGlass: `root` element is required.");
|
|
1830
|
-
this.root = root;
|
|
1831
|
-
this.defaults = { ...DEFAULTS, ...defaults };
|
|
1832
|
-
this.glassSet = new Set(Array.from(glassElements || []));
|
|
1833
|
-
this.glassCanvases = /* @__PURE__ */ new Map();
|
|
1834
|
-
this._decodedImages = /* @__PURE__ */ new WeakMap();
|
|
1835
|
-
this.capture = new HtmlCapture(root);
|
|
1836
|
-
this.capture.onCacheUpdate = (element) => {
|
|
1837
|
-
this._markGlassesIntersecting(element);
|
|
1838
|
-
};
|
|
1839
|
-
this.renderer = new GlassRenderer();
|
|
1840
|
-
this._sceneCanvas = document.createElement("canvas");
|
|
1841
|
-
this._sceneCtx = this._sceneCanvas.getContext("2d");
|
|
1842
|
-
this.renderer.canvas.addEventListener("webglcontextrestored", () => {
|
|
1843
|
-
this._glassCache.clear();
|
|
1844
|
-
this._globalDirty = true;
|
|
1845
|
-
});
|
|
1846
|
-
this._onResize = this._handleResize.bind(this);
|
|
1847
|
-
this._onPointerDown = this._handlePointerDown.bind(this);
|
|
1848
|
-
this._onPointerMove = this._handlePointerMove.bind(this);
|
|
1849
|
-
this._onPointerUp = this._handlePointerUp.bind(this);
|
|
1850
|
-
}
|
|
1851
|
-
// ────────────────────────────────────────────
|
|
1852
|
-
// Static entry point
|
|
1853
|
-
// ────────────────────────────────────────────
|
|
1854
|
-
static async init(options) {
|
|
1855
|
-
const instance = new _LiquidGlass(options);
|
|
1856
|
-
try {
|
|
1857
|
-
await instance._start();
|
|
1858
|
-
return instance;
|
|
1859
|
-
} catch (error) {
|
|
1860
|
-
instance.destroy();
|
|
1861
|
-
throw error;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
// ────────────────────────────────────────────
|
|
1865
|
-
// Lifecycle
|
|
1866
|
-
// ────────────────────────────────────────────
|
|
1867
|
-
async _start() {
|
|
1868
|
-
this.root.style.userSelect = "none";
|
|
1869
|
-
this.root.style.webkitUserSelect = "none";
|
|
1870
|
-
this._setupGlassElements();
|
|
1871
|
-
this._hasDynamic = this._detectDynamic();
|
|
1872
|
-
this._sortedChildren = this._getSortedChildren();
|
|
1873
|
-
this._handleResize();
|
|
1874
|
-
await this.capture.prefetchFontEmbedCSS();
|
|
1875
|
-
await this._captureGlassContent();
|
|
1876
|
-
await this._prewarmStaticCaptures();
|
|
1877
|
-
window.addEventListener("resize", this._onResize);
|
|
1878
|
-
this.root.addEventListener("pointerdown", this._onPointerDown);
|
|
1879
|
-
window.addEventListener("pointermove", this._onPointerMove);
|
|
1880
|
-
window.addEventListener("pointerup", this._onPointerUp);
|
|
1881
|
-
this._observer = new MutationObserver(() => {
|
|
1882
|
-
this._sortedChildren = this._getSortedChildren();
|
|
1883
|
-
this._globalDirty = true;
|
|
1884
|
-
});
|
|
1885
|
-
this._observer.observe(this.root, { childList: true });
|
|
1886
|
-
this._glassSubtreeObserver = new MutationObserver((mutations) => {
|
|
1887
|
-
for (const mutation of mutations) {
|
|
1888
|
-
const owner = this._closestGlassAncestor(mutation.target);
|
|
1889
|
-
if (mutation.type === "attributes" && mutation.attributeName === "data-config") {
|
|
1890
|
-
if (owner) this._markGlassAndDependents(owner);
|
|
1891
|
-
continue;
|
|
1892
|
-
}
|
|
1893
|
-
if (owner) {
|
|
1894
|
-
this._glassContentDirty.add(owner);
|
|
1895
|
-
this._markGlassAndDependents(owner);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
for (const el of this.glassSet) {
|
|
1900
|
-
this._glassSubtreeObserver.observe(el, {
|
|
1901
|
-
childList: true,
|
|
1902
|
-
subtree: true,
|
|
1903
|
-
characterData: true,
|
|
1904
|
-
attributes: true,
|
|
1905
|
-
attributeFilter: ["data-config"]
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
this._glassContentDirty.clear();
|
|
1909
|
-
this._running = true;
|
|
1910
|
-
this._globalDirty = true;
|
|
1911
|
-
this._rafId = requestAnimationFrame(() => this._renderLoop());
|
|
1912
|
-
}
|
|
1913
|
-
destroy() {
|
|
1914
|
-
this._running = false;
|
|
1915
|
-
cancelAnimationFrame(this._rafId);
|
|
1916
|
-
this.root.style.removeProperty("user-select");
|
|
1917
|
-
this.root.style.removeProperty("-webkit-user-select");
|
|
1918
|
-
window.removeEventListener("resize", this._onResize);
|
|
1919
|
-
this.root.removeEventListener("pointerdown", this._onPointerDown);
|
|
1920
|
-
window.removeEventListener("pointermove", this._onPointerMove);
|
|
1921
|
-
window.removeEventListener("pointerup", this._onPointerUp);
|
|
1922
|
-
this._observer?.disconnect();
|
|
1923
|
-
this._observer = null;
|
|
1924
|
-
this._glassSubtreeObserver?.disconnect();
|
|
1925
|
-
this._glassSubtreeObserver = null;
|
|
1926
|
-
for (const [el, canvas] of this.glassCanvases) {
|
|
1927
|
-
canvas.remove();
|
|
1928
|
-
el.style.removeProperty("position");
|
|
1929
|
-
el.style.removeProperty("overflow");
|
|
1930
|
-
el.style.removeProperty("touch-action");
|
|
1931
|
-
el.classList.remove(BUTTON_CLASS);
|
|
1932
|
-
}
|
|
1933
|
-
this.glassCanvases.clear();
|
|
1934
|
-
this._glassCache.clear();
|
|
1935
|
-
this._glassContentImages.clear();
|
|
1936
|
-
this._glassLastSize.clear();
|
|
1937
|
-
for (const removers of this._buttonListeners.values()) {
|
|
1938
|
-
for (const r of removers) r();
|
|
1939
|
-
}
|
|
1940
|
-
this._buttonListeners.clear();
|
|
1941
|
-
this._buttonStates.clear();
|
|
1942
|
-
document.getElementById(STYLE_ID)?.remove();
|
|
1943
|
-
this.capture.destroy();
|
|
1944
|
-
this.renderer.destroy();
|
|
1945
|
-
}
|
|
1946
|
-
// ────────────────────────────────────────────
|
|
1947
|
-
// Glass element setup
|
|
1948
|
-
// ────────────────────────────────────────────
|
|
1949
|
-
_setupGlassElements() {
|
|
1950
|
-
let needsButtonStyles = false;
|
|
1951
|
-
for (const el of this.glassSet) {
|
|
1952
|
-
if (el.parentElement !== this.root) {
|
|
1953
|
-
console.warn("LiquidGlass: glass element must be a direct child of root, skipping.", el);
|
|
1954
|
-
this.glassSet.delete(el);
|
|
1955
|
-
continue;
|
|
1956
|
-
}
|
|
1957
|
-
const currentPosition = window.getComputedStyle(el).position;
|
|
1958
|
-
if (currentPosition === "static") {
|
|
1959
|
-
el.style.position = "relative";
|
|
1960
|
-
}
|
|
1961
|
-
el.style.overflow = "visible";
|
|
1962
|
-
const config = this._getConfig(el);
|
|
1963
|
-
if (config.floating) {
|
|
1964
|
-
el.style.touchAction = "none";
|
|
1965
|
-
}
|
|
1966
|
-
if (config.button) {
|
|
1967
|
-
el.classList.add(BUTTON_CLASS);
|
|
1968
|
-
needsButtonStyles = true;
|
|
1969
|
-
this._setupButtonListeners(el);
|
|
1970
|
-
}
|
|
1971
|
-
const canvas = document.createElement("canvas");
|
|
1972
|
-
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;";
|
|
1973
|
-
el.insertBefore(canvas, el.firstChild);
|
|
1974
|
-
this.glassCanvases.set(el, canvas);
|
|
1975
|
-
}
|
|
1976
|
-
if (needsButtonStyles && !document.getElementById(STYLE_ID)) {
|
|
1977
|
-
const style = document.createElement("style");
|
|
1978
|
-
style.id = STYLE_ID;
|
|
1979
|
-
style.textContent = BUTTON_CSS;
|
|
1980
|
-
document.head.appendChild(style);
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
/**
|
|
1984
|
-
* Walk up from a mutation target until we hit a glass element
|
|
1985
|
-
* registered on this instance. Returns null if the node isn't
|
|
1986
|
-
* inside any glass subtree (shouldn't normally happen since the
|
|
1987
|
-
* observers are scoped to glass elements, but the mutation target
|
|
1988
|
-
* may be a Text node or detached during a removal).
|
|
1989
|
-
*/
|
|
1990
|
-
_closestGlassAncestor(node) {
|
|
1991
|
-
let cur = node;
|
|
1992
|
-
while (cur) {
|
|
1993
|
-
if (cur.nodeType === 1 && this.glassSet.has(cur)) {
|
|
1994
|
-
return cur;
|
|
1995
|
-
}
|
|
1996
|
-
cur = cur.parentNode;
|
|
1997
|
-
}
|
|
1998
|
-
return null;
|
|
1999
|
-
}
|
|
2000
|
-
/**
|
|
2001
|
-
* Mark a glass element (and any glass that visually depends on it
|
|
2002
|
-
* via z-order overlap) as needing a shader re-render on the next
|
|
2003
|
-
* frame.
|
|
2004
|
-
*
|
|
2005
|
-
* `rectOverride` lets callers pass a rect that differs from the
|
|
2006
|
-
* element's current bounding box — useful for drag, where we
|
|
2007
|
-
* want to invalidate both the *old* and *new* footprints in the
|
|
2008
|
-
* same call so glasses behind the dragged panel can clear its
|
|
2009
|
-
* trail and glasses ahead can pick up its new shadow.
|
|
2010
|
-
*/
|
|
2011
|
-
_markGlassAndDependents(element, rectOverride) {
|
|
2012
|
-
if (this.glassSet.has(element)) {
|
|
2013
|
-
this._glassDirty.add(element);
|
|
2014
|
-
}
|
|
2015
|
-
const rootRect = this.root.getBoundingClientRect();
|
|
2016
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2017
|
-
const elementDOMRect = rectOverride ?? element.getBoundingClientRect();
|
|
2018
|
-
const elementBox = this._getPixelRect(
|
|
2019
|
-
elementDOMRect,
|
|
2020
|
-
rootRect,
|
|
2021
|
-
dpr,
|
|
2022
|
-
this.glassSet.has(element) ? SHADOW_PAD : 0
|
|
2023
|
-
);
|
|
2024
|
-
let seenElement = false;
|
|
2025
|
-
for (const child of this._sortedChildren) {
|
|
2026
|
-
if (child === element) {
|
|
2027
|
-
seenElement = true;
|
|
2028
|
-
continue;
|
|
2029
|
-
}
|
|
2030
|
-
if (!seenElement) continue;
|
|
2031
|
-
if (!this.glassSet.has(child)) continue;
|
|
2032
|
-
const sampleRect = this._getPixelRect(
|
|
2033
|
-
child.getBoundingClientRect(),
|
|
2034
|
-
rootRect,
|
|
2035
|
-
dpr,
|
|
2036
|
-
SHADOW_PAD
|
|
2037
|
-
);
|
|
2038
|
-
if (_LiquidGlass._rectsIntersect(elementBox, sampleRect)) {
|
|
2039
|
-
this._glassDirty.add(child);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
/**
|
|
2044
|
-
* Mark every glass element whose sample rect intersects the given
|
|
2045
|
-
* element's bounding rect, regardless of stacking order. Used by
|
|
2046
|
-
* the async cache-update callback (a wrapper's pixels just got
|
|
2047
|
-
* fresh, so any glass that samples them needs to re-render) and
|
|
2048
|
-
* by the public markChanged() API for elements outside the glass
|
|
2049
|
-
* set.
|
|
2050
|
-
*/
|
|
2051
|
-
_markGlassesIntersecting(element) {
|
|
2052
|
-
const rootRect = this.root.getBoundingClientRect();
|
|
2053
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2054
|
-
const elementBox = this._getPixelRect(
|
|
2055
|
-
element.getBoundingClientRect(),
|
|
2056
|
-
rootRect,
|
|
2057
|
-
dpr,
|
|
2058
|
-
this.glassSet.has(element) ? SHADOW_PAD : 0
|
|
2059
|
-
);
|
|
2060
|
-
for (const glass of this.glassSet) {
|
|
2061
|
-
const sampleRect = this._getPixelRect(
|
|
2062
|
-
glass.getBoundingClientRect(),
|
|
2063
|
-
rootRect,
|
|
2064
|
-
dpr,
|
|
2065
|
-
SHADOW_PAD
|
|
2066
|
-
);
|
|
2067
|
-
if (_LiquidGlass._rectsIntersect(elementBox, sampleRect)) {
|
|
2068
|
-
this._glassDirty.add(glass);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Public API: mark an element (or all glass elements when called
|
|
2074
|
-
* with no arguments) as needing a shader re-render on the next
|
|
2075
|
-
* frame. Useful for content the library can't observe on its own —
|
|
2076
|
-
* a `<canvas>` whose pixels you just updated, an `<img>` you just
|
|
2077
|
-
* swapped via JS, a wrapper whose CSS background-image you just
|
|
2078
|
-
* changed, etc.
|
|
2079
|
-
*
|
|
2080
|
-
* For elements registered via `data-dynamic`, the library already
|
|
2081
|
-
* treats them as always-dirty and re-renders affected glasses
|
|
2082
|
-
* every frame; calling markChanged() on them is a no-op but is
|
|
2083
|
-
* harmless.
|
|
2084
|
-
*
|
|
2085
|
-
* @param element The element that changed visually. Pass nothing
|
|
2086
|
-
* (or `undefined`) to mark every glass on this instance dirty.
|
|
2087
|
-
*/
|
|
2088
|
-
markChanged(element) {
|
|
2089
|
-
if (!element) {
|
|
2090
|
-
this._globalDirty = true;
|
|
2091
|
-
return;
|
|
2092
|
-
}
|
|
2093
|
-
this._userMarkedChanged.add(element);
|
|
2094
|
-
}
|
|
2095
|
-
_setupButtonListeners(el) {
|
|
2096
|
-
const state = { hover: false, pressed: false };
|
|
2097
|
-
this._buttonStates.set(el, state);
|
|
2098
|
-
const mark = () => this._markGlassAndDependents(el);
|
|
2099
|
-
const onOver = () => {
|
|
2100
|
-
state.hover = true;
|
|
2101
|
-
mark();
|
|
2102
|
-
};
|
|
2103
|
-
const onOut = () => {
|
|
2104
|
-
state.hover = false;
|
|
2105
|
-
state.pressed = false;
|
|
2106
|
-
mark();
|
|
2107
|
-
};
|
|
2108
|
-
const onDown = () => {
|
|
2109
|
-
state.pressed = true;
|
|
2110
|
-
mark();
|
|
2111
|
-
};
|
|
2112
|
-
const onUp = () => {
|
|
2113
|
-
state.pressed = false;
|
|
2114
|
-
mark();
|
|
2115
|
-
};
|
|
2116
|
-
el.addEventListener("pointerover", onOver);
|
|
2117
|
-
el.addEventListener("pointerout", onOut);
|
|
2118
|
-
el.addEventListener("pointerdown", onDown);
|
|
2119
|
-
el.addEventListener("pointerup", onUp);
|
|
2120
|
-
el.addEventListener("pointercancel", onUp);
|
|
2121
|
-
this._buttonListeners.set(el, [
|
|
2122
|
-
() => el.removeEventListener("pointerover", onOver),
|
|
2123
|
-
() => el.removeEventListener("pointerout", onOut),
|
|
2124
|
-
() => el.removeEventListener("pointerdown", onDown),
|
|
2125
|
-
() => el.removeEventListener("pointerup", onUp),
|
|
2126
|
-
() => el.removeEventListener("pointercancel", onUp)
|
|
2127
|
-
]);
|
|
2128
|
-
}
|
|
2129
|
-
// ────────────────────────────────────────────
|
|
2130
|
-
// Glass content pre-capture
|
|
2131
|
-
// ────────────────────────────────────────────
|
|
2132
|
-
/**
|
|
2133
|
-
* Re-capture the DOM content (text, icons, etc.) of glass elements
|
|
2134
|
-
* whose subtrees have been mutated since the last capture, hiding
|
|
2135
|
-
* the injected shader canvas so it isn't included.
|
|
2136
|
-
*
|
|
2137
|
-
* Pass `targets = null` to capture every glass element (used at
|
|
2138
|
-
* init and on resize); pass a Set to capture only specific ones.
|
|
2139
|
-
*
|
|
2140
|
-
* Guarded against concurrent execution: if a capture is already
|
|
2141
|
-
* running, the affected elements stay in `_glassContentDirty` and
|
|
2142
|
-
* the next render-loop tick picks them up.
|
|
2143
|
-
*/
|
|
2144
|
-
async _captureGlassContent(targets = null) {
|
|
2145
|
-
if (this._capturingGlassContent) return;
|
|
2146
|
-
this._capturingGlassContent = true;
|
|
2147
|
-
try {
|
|
2148
|
-
for (const [el, glassCanvas] of this.glassCanvases) {
|
|
2149
|
-
if (targets && !targets.has(el)) continue;
|
|
2150
|
-
const rect = el.getBoundingClientRect();
|
|
2151
|
-
const img = await this.capture.captureToCanvas(
|
|
2152
|
-
el,
|
|
2153
|
-
rect.width,
|
|
2154
|
-
rect.height,
|
|
2155
|
-
[glassCanvas]
|
|
2156
|
-
);
|
|
2157
|
-
if (img) {
|
|
2158
|
-
this._glassContentImages.set(el, img);
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
} finally {
|
|
2162
|
-
this._capturingGlassContent = false;
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
/**
|
|
2166
|
-
* Synchronously walk every non-glass direct child of root and
|
|
2167
|
-
* await its html-to-image capture so the cache is fully populated
|
|
2168
|
-
* by the time the render loop starts. Without this, the first
|
|
2169
|
-
* frame's glass shader sees an empty (white) local scene for
|
|
2170
|
-
* ~one or two frames while the async captures resolve.
|
|
2171
|
-
*/
|
|
2172
|
-
async _prewarmStaticCaptures() {
|
|
2173
|
-
for (const child of this._sortedChildren) {
|
|
2174
|
-
if (this.glassSet.has(child)) continue;
|
|
2175
|
-
const tag = child.tagName;
|
|
2176
|
-
if (tag === "CANVAS" || tag === "IMG" || tag === "VIDEO") continue;
|
|
2177
|
-
if (child.hasAttribute("data-dynamic")) continue;
|
|
2178
|
-
try {
|
|
2179
|
-
await this.capture.captureElement(child, false);
|
|
2180
|
-
} catch (err) {
|
|
2181
|
-
console.warn("LiquidGlass: prewarm capture failed:", child, err);
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
// ────────────────────────────────────────────
|
|
2186
|
-
// Child ordering & stacking context
|
|
2187
|
-
// ────────────────────────────────────────────
|
|
2188
|
-
_getSortedChildren() {
|
|
2189
|
-
const children = Array.from(this.root.children);
|
|
2190
|
-
const rootDisplay = window.getComputedStyle(this.root).display;
|
|
2191
|
-
const isFlexOrGridParent = rootDisplay === "flex" || rootDisplay === "inline-flex" || rootDisplay === "grid" || rootDisplay === "inline-grid";
|
|
2192
|
-
const tagged = children.map((el, domIndex) => {
|
|
2193
|
-
const style = window.getComputedStyle(el);
|
|
2194
|
-
const hasStackingContext = _LiquidGlass._formsStackingContext(style, isFlexOrGridParent);
|
|
2195
|
-
const rawZ = parseInt(style.zIndex, 10);
|
|
2196
|
-
const zIndex = isNaN(rawZ) ? 0 : rawZ;
|
|
2197
|
-
return { el, domIndex, hasStackingContext, zIndex };
|
|
2198
|
-
});
|
|
2199
|
-
tagged.sort((a, b) => {
|
|
2200
|
-
if (!a.hasStackingContext && b.hasStackingContext) return -1;
|
|
2201
|
-
if (a.hasStackingContext && !b.hasStackingContext) return 1;
|
|
2202
|
-
if (a.hasStackingContext && b.hasStackingContext) {
|
|
2203
|
-
if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
|
|
2204
|
-
}
|
|
2205
|
-
return a.domIndex - b.domIndex;
|
|
2206
|
-
});
|
|
2207
|
-
return tagged.map((t) => t.el);
|
|
2208
|
-
}
|
|
2209
|
-
/**
|
|
2210
|
-
* Returns true when the element forms a CSS stacking context — i.e.
|
|
2211
|
-
* when its z-index participates in painting order. Mirrors the spec:
|
|
2212
|
-
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context
|
|
2213
|
-
*
|
|
2214
|
-
* Used by `_getSortedChildren` to decide painting order for the
|
|
2215
|
-
* local scene assembly. The set of triggers needs to match the
|
|
2216
|
-
* browser's actual stacking model — otherwise overlays end up
|
|
2217
|
-
* painted before the background image and get erased.
|
|
2218
|
-
*/
|
|
2219
|
-
static _formsStackingContext(style, isFlexOrGridParent) {
|
|
2220
|
-
if (style.position !== "static") return true;
|
|
2221
|
-
if (isFlexOrGridParent && style.zIndex !== "auto") return true;
|
|
2222
|
-
if (parseFloat(style.opacity) < 1) return true;
|
|
2223
|
-
if (style.transform !== "none" && style.transform !== "") return true;
|
|
2224
|
-
if (style.filter !== "none" && style.filter !== "") return true;
|
|
2225
|
-
if (style.perspective !== "none" && style.perspective !== "") return true;
|
|
2226
|
-
if (style.clipPath !== "none" && style.clipPath !== "") return true;
|
|
2227
|
-
if (style.mixBlendMode !== "normal" && style.mixBlendMode !== "") return true;
|
|
2228
|
-
if (style.isolation === "isolate") return true;
|
|
2229
|
-
const bf = style.backdropFilter || style.webkitBackdropFilter;
|
|
2230
|
-
if (bf && bf !== "none") return true;
|
|
2231
|
-
const mask = style.maskImage || style.webkitMaskImage;
|
|
2232
|
-
if (mask && mask !== "none") return true;
|
|
2233
|
-
const contain = style.contain;
|
|
2234
|
-
if (contain && /\b(layout|paint|strict|content)\b/.test(contain)) return true;
|
|
2235
|
-
if (style.willChange) {
|
|
2236
|
-
const triggers = /* @__PURE__ */ new Set([
|
|
2237
|
-
"transform",
|
|
2238
|
-
"opacity",
|
|
2239
|
-
"filter",
|
|
2240
|
-
"backdrop-filter",
|
|
2241
|
-
"perspective",
|
|
2242
|
-
"clip-path",
|
|
2243
|
-
"mask",
|
|
2244
|
-
"mask-image",
|
|
2245
|
-
"isolation",
|
|
2246
|
-
"mix-blend-mode"
|
|
2247
|
-
]);
|
|
2248
|
-
const tokens = style.willChange.split(",").map((t) => t.trim());
|
|
2249
|
-
for (const t of tokens) {
|
|
2250
|
-
if (triggers.has(t)) return true;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
return false;
|
|
2254
|
-
}
|
|
2255
|
-
_detectDynamic() {
|
|
2256
|
-
const dynEls = this.root.querySelectorAll("[data-dynamic]");
|
|
2257
|
-
for (const el of dynEls) {
|
|
2258
|
-
if (!this.glassSet.has(el)) {
|
|
2259
|
-
return true;
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
const videos = this.root.querySelectorAll("video");
|
|
2263
|
-
for (const vid of videos) {
|
|
2264
|
-
if (!this.glassSet.has(vid)) {
|
|
2265
|
-
return true;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
return false;
|
|
2269
|
-
}
|
|
2270
|
-
// ────────────────────────────────────────────
|
|
2271
|
-
// Configuration
|
|
2272
|
-
// ────────────────────────────────────────────
|
|
2273
|
-
_getConfig(el) {
|
|
2274
|
-
const cachedEl = el;
|
|
2275
|
-
const configKey = el.dataset.config ?? "";
|
|
2276
|
-
if (cachedEl.configCacheKey !== configKey) {
|
|
2277
|
-
let perElement = {};
|
|
2278
|
-
if (configKey) {
|
|
2279
|
-
try {
|
|
2280
|
-
const parsed = JSON.parse(configKey);
|
|
2281
|
-
if (parsed && typeof parsed === "object") {
|
|
2282
|
-
perElement = parsed;
|
|
2283
|
-
} else {
|
|
2284
|
-
console.warn("LiquidGlass: data-config must decode to an object for element:", el);
|
|
2285
|
-
}
|
|
2286
|
-
} catch (_e) {
|
|
2287
|
-
console.warn("LiquidGlass: invalid JSON in data-config for element:", el);
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
cachedEl.configCache = perElement;
|
|
2291
|
-
cachedEl.configCacheKey = configKey;
|
|
2292
|
-
}
|
|
2293
|
-
const config = { ...this.defaults, ...cachedEl.configCache || {} };
|
|
2294
|
-
// StandardUI buttons derive the lens outline from their CSS radius token.
|
|
2295
|
-
if (el.dataset.glassRadius === "css") {
|
|
2296
|
-
config.cornerRadius = parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
|
|
2297
|
-
}
|
|
2298
|
-
if (config.button) {
|
|
2299
|
-
const state = this._buttonStates.get(el);
|
|
2300
|
-
if (state && !el.matches(":disabled")) {
|
|
2301
|
-
if (state.pressed) {
|
|
2302
|
-
config.zRadius = config.zRadius * 0.8;
|
|
2303
|
-
config.shadowSpread = config.shadowSpread * 1.2;
|
|
2304
|
-
} else if (state.hover) {
|
|
2305
|
-
config.brightness = config.brightness + 0.2;
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
return config;
|
|
2310
|
-
}
|
|
2311
|
-
// ────────────────────────────────────────────
|
|
2312
|
-
// Resize
|
|
2313
|
-
// ────────────────────────────────────────────
|
|
2314
|
-
_handleResize() {
|
|
2315
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2316
|
-
const rect = this.root.getBoundingClientRect();
|
|
2317
|
-
this.capture.resize(dpr);
|
|
2318
|
-
this.renderer.resize(Math.round(rect.width * dpr), Math.round(rect.height * dpr));
|
|
2319
|
-
for (const el of this.glassSet) {
|
|
2320
|
-
this._updateGlassCanvasSize(el);
|
|
2321
|
-
}
|
|
2322
|
-
this._glassCache.clear();
|
|
2323
|
-
for (const el of this.glassSet) this._glassContentDirty.add(el);
|
|
2324
|
-
this._globalDirty = true;
|
|
2325
|
-
}
|
|
2326
|
-
_updateGlassCanvasSize(el) {
|
|
2327
|
-
const canvas = this.glassCanvases.get(el);
|
|
2328
|
-
if (!canvas) return;
|
|
2329
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2330
|
-
const elW = Math.round(el.offsetWidth);
|
|
2331
|
-
const elH = Math.round(el.offsetHeight);
|
|
2332
|
-
const padW = SHADOW_PAD * 2;
|
|
2333
|
-
const padH = SHADOW_PAD * 2;
|
|
2334
|
-
canvas.width = Math.round((elW + padW) * dpr);
|
|
2335
|
-
canvas.height = Math.round((elH + padH) * dpr);
|
|
2336
|
-
canvas.style.cssText = [
|
|
2337
|
-
"position:absolute",
|
|
2338
|
-
// Absolute children start at the padding edge. Align the lens with the
|
|
2339
|
-
// outer border box used for sampling and centering the button's icon.
|
|
2340
|
-
`left:${-SHADOW_PAD - el.clientLeft}px`,
|
|
2341
|
-
`top:${-SHADOW_PAD - el.clientTop}px`,
|
|
2342
|
-
`width:${elW + padW}px`,
|
|
2343
|
-
`height:${elH + padH}px`,
|
|
2344
|
-
"pointer-events:none",
|
|
2345
|
-
"z-index:-1"
|
|
2346
|
-
].join(";") + ";";
|
|
2347
|
-
this._glassLastSize.set(el, { w: elW, h: elH });
|
|
2348
|
-
}
|
|
2349
|
-
_checkGlassSizeChanges() {
|
|
2350
|
-
let changed = false;
|
|
2351
|
-
for (const el of this.glassSet) {
|
|
2352
|
-
const w = el.offsetWidth;
|
|
2353
|
-
const h = el.offsetHeight;
|
|
2354
|
-
const last = this._glassLastSize.get(el);
|
|
2355
|
-
if (!last || Math.abs(last.w - w) > 0.5 || Math.abs(last.h - h) > 0.5) {
|
|
2356
|
-
this._updateGlassCanvasSize(el);
|
|
2357
|
-
this._glassCache.delete(el);
|
|
2358
|
-
this.capture.invalidateCache(el);
|
|
2359
|
-
this._glassContentDirty.add(el);
|
|
2360
|
-
changed = true;
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
return changed;
|
|
2364
|
-
}
|
|
2365
|
-
// ────────────────────────────────────────────
|
|
2366
|
-
// Floating (drag) behaviour — Pointer Events
|
|
2367
|
-
// ────────────────────────────────────────────
|
|
2368
|
-
/** Parse the current translate(x, y) values from an element's transform. */
|
|
2369
|
-
static _getTranslateXY(el) {
|
|
2370
|
-
const style = getComputedStyle(el);
|
|
2371
|
-
const matrix = style.transform;
|
|
2372
|
-
if (!matrix || matrix === "none") return [0, 0];
|
|
2373
|
-
const m = matrix.match(/matrix\(([^)]+)\)/);
|
|
2374
|
-
if (m) {
|
|
2375
|
-
const parts = m[1].split(",").map(Number);
|
|
2376
|
-
return [parts[4] || 0, parts[5] || 0];
|
|
2377
|
-
}
|
|
2378
|
-
return [0, 0];
|
|
2379
|
-
}
|
|
2380
|
-
_handlePointerDown(e) {
|
|
2381
|
-
for (let i = this._sortedChildren.length - 1; i >= 0; i--) {
|
|
2382
|
-
const el = this._sortedChildren[i];
|
|
2383
|
-
if (!this.glassSet.has(el)) continue;
|
|
2384
|
-
const config = this._getConfig(el);
|
|
2385
|
-
if (!config.floating) continue;
|
|
2386
|
-
const rect = el.getBoundingClientRect();
|
|
2387
|
-
const elW = el.offsetWidth;
|
|
2388
|
-
const elH = el.offsetHeight;
|
|
2389
|
-
const visualLeft = rect.left + (rect.width - elW) / 2;
|
|
2390
|
-
const visualTop = rect.top + (rect.height - elH) / 2;
|
|
2391
|
-
if (e.clientX >= visualLeft && e.clientX <= visualLeft + elW && e.clientY >= visualTop && e.clientY <= visualTop + elH) {
|
|
2392
|
-
const [tx, ty] = _LiquidGlass._getTranslateXY(el);
|
|
2393
|
-
this._drag.active = true;
|
|
2394
|
-
this._drag.element = el;
|
|
2395
|
-
this._drag.startX = e.clientX;
|
|
2396
|
-
this._drag.startY = e.clientY;
|
|
2397
|
-
this._drag.origTx = tx;
|
|
2398
|
-
this._drag.origTy = ty;
|
|
2399
|
-
el.style.cursor = "grabbing";
|
|
2400
|
-
el.setPointerCapture(e.pointerId);
|
|
2401
|
-
e.preventDefault();
|
|
2402
|
-
break;
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
_handlePointerMove(e) {
|
|
2407
|
-
if (!this._drag.active) {
|
|
2408
|
-
for (const el2 of this.glassSet) {
|
|
2409
|
-
const config = this._getConfig(el2);
|
|
2410
|
-
if (!config.floating) continue;
|
|
2411
|
-
const rect = el2.getBoundingClientRect();
|
|
2412
|
-
const elW2 = el2.offsetWidth;
|
|
2413
|
-
const elH2 = el2.offsetHeight;
|
|
2414
|
-
const visualLeft = rect.left + (rect.width - elW2) / 2;
|
|
2415
|
-
const visualTop = rect.top + (rect.height - elH2) / 2;
|
|
2416
|
-
if (e.clientX >= visualLeft && e.clientX <= visualLeft + elW2 && e.clientY >= visualTop && e.clientY <= visualTop + elH2) {
|
|
2417
|
-
el2.style.cursor = "grab";
|
|
2418
|
-
} else {
|
|
2419
|
-
el2.style.cursor = "";
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
return;
|
|
2423
|
-
}
|
|
2424
|
-
const el = this._drag.element;
|
|
2425
|
-
const dx = e.clientX - this._drag.startX;
|
|
2426
|
-
const dy = e.clientY - this._drag.startY;
|
|
2427
|
-
let newTx = this._drag.origTx + dx;
|
|
2428
|
-
let newTy = this._drag.origTy + dy;
|
|
2429
|
-
const rootRect = this.root.getBoundingClientRect();
|
|
2430
|
-
const elW = el.offsetWidth;
|
|
2431
|
-
const elH = el.offsetHeight;
|
|
2432
|
-
const elRect = el.getBoundingClientRect();
|
|
2433
|
-
const [curTx, curTy] = _LiquidGlass._getTranslateXY(el);
|
|
2434
|
-
const baseLeft = elRect.left + (elRect.width - elW) / 2 - rootRect.left - curTx;
|
|
2435
|
-
const baseTop = elRect.top + (elRect.height - elH) / 2 - rootRect.top - curTy;
|
|
2436
|
-
const margin = 10;
|
|
2437
|
-
const posLeft = baseLeft + newTx;
|
|
2438
|
-
const posTop = baseTop + newTy;
|
|
2439
|
-
const maxLeft = rootRect.width - elW - margin;
|
|
2440
|
-
const maxTop = rootRect.height - elH - margin;
|
|
2441
|
-
if (posLeft < margin) newTx += margin - posLeft;
|
|
2442
|
-
if (posTop < margin) newTy += margin - posTop;
|
|
2443
|
-
if (posLeft > maxLeft) newTx -= posLeft - maxLeft;
|
|
2444
|
-
if (posTop > maxTop) newTy -= posTop - maxTop;
|
|
2445
|
-
const oldRect = el.getBoundingClientRect();
|
|
2446
|
-
this._markGlassAndDependents(el, oldRect);
|
|
2447
|
-
el.style.transform = `translate(${newTx}px, ${newTy}px)`;
|
|
2448
|
-
this._markGlassAndDependents(el);
|
|
2449
|
-
}
|
|
2450
|
-
_handlePointerUp(_e) {
|
|
2451
|
-
if (!this._drag.active) return;
|
|
2452
|
-
const dragged = this._drag.element;
|
|
2453
|
-
dragged.style.cursor = "";
|
|
2454
|
-
this._drag.active = false;
|
|
2455
|
-
this._drag.element = null;
|
|
2456
|
-
this._markGlassAndDependents(dragged);
|
|
2457
|
-
}
|
|
2458
|
-
// ────────────────────────────────────────────
|
|
2459
|
-
// Render loop
|
|
2460
|
-
// ────────────────────────────────────────────
|
|
2461
|
-
_renderLoop() {
|
|
2462
|
-
if (!this._running) return;
|
|
2463
|
-
const now = performance.now();
|
|
2464
|
-
this._fpsFrames++;
|
|
2465
|
-
if (now - this._fpsTime >= 1e3) {
|
|
2466
|
-
this.fps = this._fpsFrames;
|
|
2467
|
-
this._fpsFrames = 0;
|
|
2468
|
-
this._fpsTime = now;
|
|
2469
|
-
}
|
|
2470
|
-
if (this._checkGlassSizeChanges()) {
|
|
2471
|
-
}
|
|
2472
|
-
if (this._glassContentDirty.size > 0 && !this._capturingGlassContent) {
|
|
2473
|
-
const targets = new Set(this._glassContentDirty);
|
|
2474
|
-
this._glassContentDirty.clear();
|
|
2475
|
-
this._captureGlassContent(targets);
|
|
2476
|
-
}
|
|
2477
|
-
try {
|
|
2478
|
-
this._renderFrame();
|
|
2479
|
-
} catch (err) {
|
|
2480
|
-
console.error("LiquidGlass: render error:", err);
|
|
2481
|
-
}
|
|
2482
|
-
this._rafId = requestAnimationFrame(() => this._renderLoop());
|
|
2483
|
-
}
|
|
2484
|
-
_renderFrame() {
|
|
2485
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2486
|
-
const rootRect = this.root.getBoundingClientRect();
|
|
2487
|
-
const isDragging = this._drag.active;
|
|
2488
|
-
if (this._userMarkedChanged.size > 0) {
|
|
2489
|
-
for (const el of this._userMarkedChanged) {
|
|
2490
|
-
this._markGlassesIntersecting(el);
|
|
2491
|
-
}
|
|
2492
|
-
this._userMarkedChanged.clear();
|
|
2493
|
-
}
|
|
2494
|
-
if (this._globalDirty) {
|
|
2495
|
-
for (const el of this.glassSet) this._glassDirty.add(el);
|
|
2496
|
-
this._globalDirty = false;
|
|
2497
|
-
}
|
|
2498
|
-
const needsRender = this._glassDirty.size > 0 || this._hasDynamic || isDragging;
|
|
2499
|
-
if (!needsRender) return;
|
|
2500
|
-
const dirtyTargets = new Set(this._glassDirty);
|
|
2501
|
-
this._glassDirty.clear();
|
|
2502
|
-
const renderedThisFrame = [];
|
|
2503
|
-
for (const child of this._sortedChildren) {
|
|
2504
|
-
if (!this.glassSet.has(child)) continue;
|
|
2505
|
-
this._renderGlassElement(
|
|
2506
|
-
child,
|
|
2507
|
-
rootRect,
|
|
2508
|
-
dpr,
|
|
2509
|
-
isDragging,
|
|
2510
|
-
dirtyTargets,
|
|
2511
|
-
renderedThisFrame
|
|
2512
|
-
);
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
/**
|
|
2516
|
-
* Render a single glass element by composing just the scene region
|
|
2517
|
-
* that can affect it, then running the shader over that local input.
|
|
2518
|
-
*
|
|
2519
|
-
* Whether the shader actually re-runs depends on:
|
|
2520
|
-
* - explicit dirty mark for this element (in `dirtyTargets`),
|
|
2521
|
-
* - any earlier glass in z-order that re-rendered this frame
|
|
2522
|
-
* and whose rect intersects this glass's sample rect,
|
|
2523
|
-
* - this glass having moved since last frame (position cache),
|
|
2524
|
-
* - this glass having dynamic contributors in its sample (video,
|
|
2525
|
-
* data-dynamic),
|
|
2526
|
-
* - or active drag involving this element.
|
|
2527
|
-
*
|
|
2528
|
-
* On render, an entry is pushed to `renderedThisFrame` so later
|
|
2529
|
-
* glasses can check whether they need to refresh too.
|
|
2530
|
-
*/
|
|
2531
|
-
_renderGlassElement(child, rootRect, dpr, isDragging, dirtyTargets, renderedThisFrame) {
|
|
2532
|
-
const config = this._getConfig(child);
|
|
2533
|
-
const elRect = child.getBoundingClientRect();
|
|
2534
|
-
const elW = child.offsetWidth;
|
|
2535
|
-
const elH = child.offsetHeight;
|
|
2536
|
-
const centerX = elRect.left - rootRect.left + elRect.width / 2;
|
|
2537
|
-
const centerY = elRect.top - rootRect.top + elRect.height / 2;
|
|
2538
|
-
const glassCanvas = this.glassCanvases.get(child);
|
|
2539
|
-
const isBeingDragged = isDragging && this._drag.element === child;
|
|
2540
|
-
const sampleRect = this._getPixelRect(elRect, rootRect, dpr, SHADOW_PAD);
|
|
2541
|
-
const cached = this._glassCache.get(child);
|
|
2542
|
-
const posChanged = !cached || Math.abs(cached.centerX - centerX) > 0.5 || Math.abs(cached.centerY - centerY) > 0.5;
|
|
2543
|
-
const hasDynamicContributors = this._hasDynamic && this._glassHasDynamicContributors(child, sampleRect, rootRect, dpr);
|
|
2544
|
-
let priorGlassChanged = false;
|
|
2545
|
-
for (const r of renderedThisFrame) {
|
|
2546
|
-
if (_LiquidGlass._rectsIntersect(r.rect, sampleRect)) {
|
|
2547
|
-
priorGlassChanged = true;
|
|
2548
|
-
break;
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
const isExplicitlyDirty = dirtyTargets.has(child);
|
|
2552
|
-
const needsShaderRender = isDragging ? isBeingDragged || isExplicitlyDirty || priorGlassChanged || hasDynamicContributors : !cached || posChanged || isExplicitlyDirty || priorGlassChanged || hasDynamicContributors;
|
|
2553
|
-
if (needsShaderRender && glassCanvas) {
|
|
2554
|
-
const renderW = glassCanvas.width;
|
|
2555
|
-
const renderH = glassCanvas.height;
|
|
2556
|
-
this._composeSceneForGlass(child, sampleRect, rootRect, dpr);
|
|
2557
|
-
this.renderer.uploadAndBlur(
|
|
2558
|
-
this._sceneCanvas,
|
|
2559
|
-
0,
|
|
2560
|
-
0,
|
|
2561
|
-
renderW,
|
|
2562
|
-
renderH,
|
|
2563
|
-
config.blurAmount
|
|
2564
|
-
);
|
|
2565
|
-
this.renderer.clear();
|
|
2566
|
-
this.renderer.renderGlassPanel(
|
|
2567
|
-
config,
|
|
2568
|
-
elW,
|
|
2569
|
-
elH,
|
|
2570
|
-
dpr
|
|
2571
|
-
);
|
|
2572
|
-
const ctx = glassCanvas.getContext("2d");
|
|
2573
|
-
ctx.clearRect(0, 0, glassCanvas.width, glassCanvas.height);
|
|
2574
|
-
ctx.drawImage(
|
|
2575
|
-
this.renderer.canvas,
|
|
2576
|
-
0,
|
|
2577
|
-
0,
|
|
2578
|
-
glassCanvas.width,
|
|
2579
|
-
glassCanvas.height,
|
|
2580
|
-
0,
|
|
2581
|
-
0,
|
|
2582
|
-
glassCanvas.width,
|
|
2583
|
-
glassCanvas.height
|
|
2584
|
-
);
|
|
2585
|
-
this._glassCache.set(child, { centerX, centerY });
|
|
2586
|
-
renderedThisFrame.push({ rect: sampleRect });
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
/**
|
|
2590
|
-
* Build the local input scene for a glass panel by walking only the
|
|
2591
|
-
* contributors that paint before it in the stacking order.
|
|
2592
|
-
*/
|
|
2593
|
-
_composeSceneForGlass(currentGlass, sampleRect, rootRect, dpr) {
|
|
2594
|
-
this._prepareSceneCanvas(sampleRect.w, sampleRect.h);
|
|
2595
|
-
for (const child of this._sortedChildren) {
|
|
2596
|
-
if (child === currentGlass) break;
|
|
2597
|
-
if (this.glassSet.has(child)) {
|
|
2598
|
-
this._drawPriorGlassToScene(child, sampleRect, rootRect, dpr);
|
|
2599
|
-
} else {
|
|
2600
|
-
this._drawNonGlassChildToScene(child, sampleRect, rootRect, dpr);
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
_prepareSceneCanvas(width, height) {
|
|
2605
|
-
if (this._sceneCanvas.width !== width || this._sceneCanvas.height !== height) {
|
|
2606
|
-
this._sceneCanvas.width = width;
|
|
2607
|
-
this._sceneCanvas.height = height;
|
|
2608
|
-
} else {
|
|
2609
|
-
this._sceneCtx.clearRect(0, 0, width, height);
|
|
2610
|
-
}
|
|
2611
|
-
this._sceneCtx.fillStyle = "#ffffff";
|
|
2612
|
-
this._sceneCtx.fillRect(0, 0, width, height);
|
|
2613
|
-
}
|
|
2614
|
-
_glassHasDynamicContributors(currentGlass, sampleRect, rootRect, dpr) {
|
|
2615
|
-
if (this._childHasDynamicContent(currentGlass)) return true;
|
|
2616
|
-
for (const child of this._sortedChildren) {
|
|
2617
|
-
if (child === currentGlass) break;
|
|
2618
|
-
if (this.glassSet.has(child)) continue;
|
|
2619
|
-
if (!this._childHasDynamicContent(child)) continue;
|
|
2620
|
-
if (this._childTouchesSample(child, sampleRect, rootRect, dpr)) {
|
|
2621
|
-
return true;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
return false;
|
|
2625
|
-
}
|
|
2626
|
-
_childHasDynamicContent(child) {
|
|
2627
|
-
if (child.hasAttribute("data-dynamic")) return true;
|
|
2628
|
-
if (child.tagName === "VIDEO") return true;
|
|
2629
|
-
return child.querySelector("[data-dynamic], video") !== null;
|
|
2630
|
-
}
|
|
2631
|
-
_drawNonGlassChildToScene(child, sampleRect, rootRect, dpr) {
|
|
2632
|
-
const tag = child.tagName;
|
|
2633
|
-
if (tag === "CANVAS" || tag === "IMG" || tag === "VIDEO") {
|
|
2634
|
-
this._drawMediaElement(child, this._sceneCtx, sampleRect, rootRect, dpr);
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
if (!this._elementTouchesSample(child, sampleRect, rootRect, dpr)) {
|
|
2638
|
-
return;
|
|
2639
|
-
}
|
|
2640
|
-
this._captureMediaDescendants(child, this._sceneCtx, sampleRect, rootRect, dpr);
|
|
2641
|
-
const isDynamic = child.hasAttribute("data-dynamic");
|
|
2642
|
-
this.capture.captureElement(child, isDynamic);
|
|
2643
|
-
const rect = this._getPixelRect(child.getBoundingClientRect(), rootRect, dpr);
|
|
2644
|
-
this.capture.drawCachedElement(
|
|
2645
|
-
child,
|
|
2646
|
-
this._sceneCtx,
|
|
2647
|
-
rect.x - sampleRect.x,
|
|
2648
|
-
rect.y - sampleRect.y,
|
|
2649
|
-
rect.w,
|
|
2650
|
-
rect.h
|
|
2651
|
-
);
|
|
2652
|
-
}
|
|
2653
|
-
/**
|
|
2654
|
-
* Recursively find and draw all img/video/canvas elements inside
|
|
2655
|
-
* a wrapper, skipping any glass elements and their injected canvases.
|
|
2656
|
-
*/
|
|
2657
|
-
_captureMediaDescendants(parent, targetCtx, sampleRect, rootRect, dpr) {
|
|
2658
|
-
const mediaEls = parent.querySelectorAll("img, video, canvas");
|
|
2659
|
-
for (const el of mediaEls) {
|
|
2660
|
-
const htmlEl = el;
|
|
2661
|
-
let isGlassCanvas = false;
|
|
2662
|
-
for (const [, gc] of this.glassCanvases) {
|
|
2663
|
-
if (gc === el) {
|
|
2664
|
-
isGlassCanvas = true;
|
|
2665
|
-
break;
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
if (isGlassCanvas) continue;
|
|
2669
|
-
this._drawMediaElement(htmlEl, targetCtx, sampleRect, rootRect, dpr);
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
/** Draw a single img/video/canvas into a local scene canvas. */
|
|
2673
|
-
_drawMediaElement(el, targetCtx, sampleRect, rootRect, dpr) {
|
|
2674
|
-
const tag = el.tagName;
|
|
2675
|
-
const r = el.getBoundingClientRect();
|
|
2676
|
-
if (!this._elementTouchesSample(el, sampleRect, rootRect, dpr)) return false;
|
|
2677
|
-
const rect = this._getPixelRect(r, rootRect, dpr);
|
|
2678
|
-
const dx = rect.x - sampleRect.x;
|
|
2679
|
-
const dy = rect.y - sampleRect.y;
|
|
2680
|
-
const dw = rect.w;
|
|
2681
|
-
const dh = rect.h;
|
|
2682
|
-
if (dw <= 0 || dh <= 0) return false;
|
|
2683
|
-
if (tag === "CANVAS") {
|
|
2684
|
-
const liveCanvas = el;
|
|
2685
|
-
if (liveCanvas.width <= 0 || liveCanvas.height <= 0) return false;
|
|
2686
|
-
targetCtx.drawImage(liveCanvas, dx, dy, dw, dh);
|
|
2687
|
-
return true;
|
|
2688
|
-
} else if (tag === "IMG") {
|
|
2689
|
-
let img = el;
|
|
2690
|
-
if (!img.complete || img.naturalWidth === 0) return false;
|
|
2691
|
-
// srcset makes naturalWidth density-corrected, but drawImage source
|
|
2692
|
-
// coordinates address the resource's pixels. Decode currentSrc without
|
|
2693
|
-
// srcset so object-fit cropping and canvas sampling use the same units.
|
|
2694
|
-
if (img.srcset) {
|
|
2695
|
-
const source = img.currentSrc || img.src;
|
|
2696
|
-
let cached = this._decodedImages.get(el);
|
|
2697
|
-
if (!cached || cached.source !== source) {
|
|
2698
|
-
const decoded = new Image();
|
|
2699
|
-
if (img.crossOrigin !== null) decoded.crossOrigin = img.crossOrigin;
|
|
2700
|
-
decoded.referrerPolicy = img.referrerPolicy;
|
|
2701
|
-
decoded.onload = () => {
|
|
2702
|
-
if (this._running) this.markChanged(el);
|
|
2703
|
-
};
|
|
2704
|
-
cached = { source, image: decoded };
|
|
2705
|
-
this._decodedImages.set(el, cached);
|
|
2706
|
-
decoded.src = source;
|
|
2707
|
-
}
|
|
2708
|
-
img = cached.image;
|
|
2709
|
-
if (!img.complete || img.naturalWidth === 0) return false;
|
|
2710
|
-
}
|
|
2711
|
-
this._drawMediaFitted(
|
|
2712
|
-
targetCtx,
|
|
2713
|
-
img,
|
|
2714
|
-
img.naturalWidth,
|
|
2715
|
-
img.naturalHeight,
|
|
2716
|
-
el,
|
|
2717
|
-
r,
|
|
2718
|
-
dx,
|
|
2719
|
-
dy,
|
|
2720
|
-
dw,
|
|
2721
|
-
dh
|
|
2722
|
-
);
|
|
2723
|
-
return true;
|
|
2724
|
-
} else if (tag === "VIDEO") {
|
|
2725
|
-
const vid = el;
|
|
2726
|
-
if (vid.readyState < 1) return false;
|
|
2727
|
-
try {
|
|
2728
|
-
this._drawMediaFitted(
|
|
2729
|
-
targetCtx,
|
|
2730
|
-
vid,
|
|
2731
|
-
vid.videoWidth,
|
|
2732
|
-
vid.videoHeight,
|
|
2733
|
-
el,
|
|
2734
|
-
r,
|
|
2735
|
-
dx,
|
|
2736
|
-
dy,
|
|
2737
|
-
dw,
|
|
2738
|
-
dh
|
|
2739
|
-
);
|
|
2740
|
-
} catch {
|
|
2741
|
-
return false;
|
|
2742
|
-
}
|
|
2743
|
-
return true;
|
|
2744
|
-
}
|
|
2745
|
-
return false;
|
|
2746
|
-
}
|
|
2747
|
-
/** Draw an img or video onto a local scene canvas, respecting object-fit. */
|
|
2748
|
-
_drawMediaFitted(targetCtx, mediaEl, natW, natH, child, r, dx, dy, dw, dh) {
|
|
2749
|
-
if (natW && natH) {
|
|
2750
|
-
const computed = getComputedStyle(child);
|
|
2751
|
-
const fit = computed.objectFit || "fill";
|
|
2752
|
-
const pos = computed.objectPosition || "50% 50%";
|
|
2753
|
-
const src = _LiquidGlass._objectFitRect(natW, natH, r.width, r.height, fit, pos);
|
|
2754
|
-
targetCtx.drawImage(mediaEl, src.sx, src.sy, src.sw, src.sh, dx, dy, dw, dh);
|
|
2755
|
-
} else {
|
|
2756
|
-
targetCtx.drawImage(mediaEl, dx, dy, dw, dh);
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
_drawPriorGlassToScene(child, sampleRect, rootRect, dpr) {
|
|
2760
|
-
const glassCanvas = this.glassCanvases.get(child);
|
|
2761
|
-
const elRect = child.getBoundingClientRect();
|
|
2762
|
-
if (glassCanvas) {
|
|
2763
|
-
const shaderRect = this._getPixelRect(elRect, rootRect, dpr, SHADOW_PAD);
|
|
2764
|
-
if (_LiquidGlass._rectsIntersect(shaderRect, sampleRect)) {
|
|
2765
|
-
this._sceneCtx.drawImage(
|
|
2766
|
-
glassCanvas,
|
|
2767
|
-
0,
|
|
2768
|
-
0,
|
|
2769
|
-
glassCanvas.width,
|
|
2770
|
-
glassCanvas.height,
|
|
2771
|
-
shaderRect.x - sampleRect.x,
|
|
2772
|
-
shaderRect.y - sampleRect.y,
|
|
2773
|
-
shaderRect.w,
|
|
2774
|
-
shaderRect.h
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
const contentImg = this._glassContentImages.get(child);
|
|
2779
|
-
if (!contentImg) return;
|
|
2780
|
-
const contentRect = this._getPixelRect(elRect, rootRect, dpr);
|
|
2781
|
-
if (!_LiquidGlass._rectsIntersect(contentRect, sampleRect)) return;
|
|
2782
|
-
this._sceneCtx.drawImage(
|
|
2783
|
-
contentImg,
|
|
2784
|
-
contentRect.x - sampleRect.x,
|
|
2785
|
-
contentRect.y - sampleRect.y,
|
|
2786
|
-
contentRect.w,
|
|
2787
|
-
contentRect.h
|
|
2788
|
-
);
|
|
2789
|
-
}
|
|
2790
|
-
_getPixelRect(rect, rootRect, dpr, pad = 0) {
|
|
2791
|
-
return {
|
|
2792
|
-
x: Math.round((rect.left - rootRect.left - pad) * dpr),
|
|
2793
|
-
y: Math.round((rect.top - rootRect.top - pad) * dpr),
|
|
2794
|
-
w: Math.round((rect.width + pad * 2) * dpr),
|
|
2795
|
-
h: Math.round((rect.height + pad * 2) * dpr)
|
|
2796
|
-
};
|
|
2797
|
-
}
|
|
2798
|
-
_childTouchesSample(child, sampleRect, rootRect, dpr) {
|
|
2799
|
-
if (this._elementTouchesSample(child, sampleRect, rootRect, dpr)) return true;
|
|
2800
|
-
for (const el of child.querySelectorAll("[data-dynamic], video")) {
|
|
2801
|
-
if (this._elementTouchesSample(el, sampleRect, rootRect, dpr)) {
|
|
2802
|
-
return true;
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
return false;
|
|
2806
|
-
}
|
|
2807
|
-
_elementTouchesSample(element, sampleRect, rootRect, dpr) {
|
|
2808
|
-
const pad = this._getPaintOverflowPad(element);
|
|
2809
|
-
const bounds = this._getPixelRect(element.getBoundingClientRect(), rootRect, dpr, pad);
|
|
2810
|
-
return _LiquidGlass._rectsIntersect(bounds, sampleRect);
|
|
2811
|
-
}
|
|
2812
|
-
_getPaintOverflowPad(element) {
|
|
2813
|
-
if (this.glassSet.has(element)) return SHADOW_PAD;
|
|
2814
|
-
const style = getComputedStyle(element);
|
|
2815
|
-
const backdropFilter = style.backdropFilter || style.webkitBackdropFilter;
|
|
2816
|
-
const maskImage = style.maskImage || style.webkitMaskImage;
|
|
2817
|
-
const paintsOutsideBounds = style.boxShadow && style.boxShadow !== "none" || style.textShadow && style.textShadow !== "none" || style.filter && style.filter !== "none" || backdropFilter && backdropFilter !== "none" || maskImage && maskImage !== "none" || style.mixBlendMode && style.mixBlendMode !== "normal";
|
|
2818
|
-
return paintsOutsideBounds ? SHADOW_PAD : 0;
|
|
2819
|
-
}
|
|
2820
|
-
static _rectsIntersect(a, b) {
|
|
2821
|
-
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
2822
|
-
}
|
|
2823
|
-
/** Compute the source rectangle for drawImage that replicates CSS object-fit / object-position. */
|
|
2824
|
-
static _objectFitRect(natW, natH, boxW, boxH, fit, pos) {
|
|
2825
|
-
let sx = 0, sy = 0, sw = natW, sh = natH;
|
|
2826
|
-
if (fit === "fill" || fit === "scale-down" && natW <= boxW && natH <= boxH) {
|
|
2827
|
-
return { sx, sy, sw, sh };
|
|
2828
|
-
}
|
|
2829
|
-
const parts = pos.split(/\s+/);
|
|
2830
|
-
const parseFrac = (v, total) => {
|
|
2831
|
-
if (v.endsWith("%")) return parseFloat(v) / 100;
|
|
2832
|
-
return parseFloat(v) / total;
|
|
2833
|
-
};
|
|
2834
|
-
const fx = parseFrac(parts[0] || "50%", boxW);
|
|
2835
|
-
const fy = parseFrac(parts[1] || "50%", boxH);
|
|
2836
|
-
if (fit === "cover") {
|
|
2837
|
-
const scale = Math.max(boxW / natW, boxH / natH);
|
|
2838
|
-
sw = boxW / scale;
|
|
2839
|
-
sh = boxH / scale;
|
|
2840
|
-
sx = (natW - sw) * fx;
|
|
2841
|
-
sy = (natH - sh) * fy;
|
|
2842
|
-
} else if (fit === "contain" || fit === "scale-down") {
|
|
2843
|
-
return { sx: 0, sy: 0, sw: natW, sh: natH };
|
|
2844
|
-
} else if (fit === "none") {
|
|
2845
|
-
sw = boxW;
|
|
2846
|
-
sh = boxH;
|
|
2847
|
-
sx = (natW - sw) * fx;
|
|
2848
|
-
sy = (natH - sh) * fy;
|
|
2849
|
-
}
|
|
2850
|
-
sx = Math.max(0, Math.min(sx, natW - 1));
|
|
2851
|
-
sy = Math.max(0, Math.min(sy, natH - 1));
|
|
2852
|
-
sw = Math.min(sw, natW - sx);
|
|
2853
|
-
sh = Math.min(sh, natH - sy);
|
|
2854
|
-
return { sx, sy, sw, sh };
|
|
2855
|
-
}
|
|
2856
|
-
};
|
|
2857
|
-
export {
|
|
2858
|
-
DEFAULTS,
|
|
2859
|
-
LiquidGlass,
|
|
2860
|
-
invalidateFontEmbedCache
|
|
2861
|
-
};
|
|
2862
|
-
//# sourceMappingURL=index.js.map
|