vireglass 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/dom.cjs +194 -13
- package/dist/dom.cjs.map +1 -1
- package/dist/dom.d.cts +23 -1
- package/dist/dom.d.ts +23 -1
- package/dist/dom.js +193 -14
- package/dist/dom.js.map +1 -1
- package/package.json +2 -1
- package/src/dom/index.ts +174 -12
- package/src/dom/interim.ts +47 -0
- package/src/dom/stats.ts +28 -0
package/README.md
CHANGED
|
@@ -153,6 +153,36 @@ glass.setMorph({ smoothing: 34, shape: { offsetX: 150, offsetY: -78, width: 170,
|
|
|
153
153
|
Two shapes joined through the smooth union — §5's merging and splitting. What the shapes mean is
|
|
154
154
|
your choreography; the material only knows how two silhouettes join.
|
|
155
155
|
|
|
156
|
+
### Morphing elements
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { attachGlass, renderStats, resetRenderStats } from 'vireglass/dom';
|
|
160
|
+
|
|
161
|
+
glass.setInterim(true); // before a morph, a drag or a growth
|
|
162
|
+
// ...animate the element's box, and `setMorph` shapes if it flows into others...
|
|
163
|
+
glass.setInterim(false); // when it lands
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A glass element that changes shape every frame used to re-run the whole pipeline every frame: a
|
|
167
|
+
backdrop probe, a full-resolution displacement map (and spectral map) and a new filter element.
|
|
168
|
+
That is the difference between 60 fps and 10 to 20. While interim, the element does none of it: it
|
|
169
|
+
reuses its last sample, keeps its filter, and the browser stretches the existing map over the live
|
|
170
|
+
box. The body, rim, shadow and custom properties still update every frame. A change of the morph
|
|
171
|
+
shapes passed to `setMorph`, of `setAppear` or of the system accessibility settings rebuilds the map
|
|
172
|
+
at reduced resolution and shows it in place, where the browser decodes lazily only once it has been
|
|
173
|
+
decoded. `setInterim(false)` does one full-resolution rebuild and one probe (with a supplied
|
|
174
|
+
`sample` it does not probe). Repeating a call is a no-op.
|
|
175
|
+
|
|
176
|
+
Always pair `setInterim(true)` with `setInterim(false)`. As a safety net an element that gets no call
|
|
177
|
+
at all for 2 seconds (no resize, morph, appear or update) leaves interim by itself, exactly as
|
|
178
|
+
`setInterim(false)` would, so a morph that never reported landing cannot leave the glass on a
|
|
179
|
+
stretched map. A pause longer than that in the middle of a morph ends interim early.
|
|
180
|
+
|
|
181
|
+
`renderStats()` returns `{ probes, fullBakes, reducedBakes, filterSwaps }`, counted since the last
|
|
182
|
+
`resetRenderStats()`. Every map counts: a rebuild with a spectral edge is two full bakes, one for
|
|
183
|
+
each map; a cache hit is none. It is the hook for a performance gate that counts work instead of
|
|
184
|
+
timing it: drive a morph, then assert the counters stayed at zero.
|
|
185
|
+
|
|
156
186
|
`attachGlass` writes CSS custom properties you can use directly:
|
|
157
187
|
|
|
158
188
|
| | |
|
package/dist/dom.cjs
CHANGED
|
@@ -1024,6 +1024,55 @@ function probeBackdrop(el, opts = {}) {
|
|
|
1024
1024
|
return statsFromSamples(samples);
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
|
+
// src/dom/stats.ts
|
|
1028
|
+
var counts = { probes: 0, fullBakes: 0, reducedBakes: 0, filterSwaps: 0 };
|
|
1029
|
+
function renderStats() {
|
|
1030
|
+
return { ...counts };
|
|
1031
|
+
}
|
|
1032
|
+
function resetRenderStats() {
|
|
1033
|
+
counts.probes = 0;
|
|
1034
|
+
counts.fullBakes = 0;
|
|
1035
|
+
counts.reducedBakes = 0;
|
|
1036
|
+
counts.filterSwaps = 0;
|
|
1037
|
+
}
|
|
1038
|
+
function bump(key) {
|
|
1039
|
+
counts[key] += 1;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/dom/interim.ts
|
|
1043
|
+
function quantise(value, step = 2) {
|
|
1044
|
+
return Math.round(value / step) * step;
|
|
1045
|
+
}
|
|
1046
|
+
function interimKey(width, height, cornerRadius, morph, extra) {
|
|
1047
|
+
return `i|${quantise(width)}x${quantise(height)}r${quantise(cornerRadius)}|${morph}|${extra}`;
|
|
1048
|
+
}
|
|
1049
|
+
function filterSignature(blur2, ior, iorSpread2, hasHue) {
|
|
1050
|
+
return `${blur2}|${ior}|${iorSpread2 > 1e-4 ? iorSpread2 : 0}|${hasHue ? "h" : "-"}`;
|
|
1051
|
+
}
|
|
1052
|
+
function createLru(limit) {
|
|
1053
|
+
const store2 = /* @__PURE__ */ new Map();
|
|
1054
|
+
return {
|
|
1055
|
+
get(key) {
|
|
1056
|
+
const hit = store2.get(key);
|
|
1057
|
+
if (hit === void 0) return void 0;
|
|
1058
|
+
store2.delete(key);
|
|
1059
|
+
store2.set(key, hit);
|
|
1060
|
+
return hit;
|
|
1061
|
+
},
|
|
1062
|
+
set(key, value) {
|
|
1063
|
+
store2.delete(key);
|
|
1064
|
+
store2.set(key, value);
|
|
1065
|
+
if (store2.size > limit) {
|
|
1066
|
+
const oldest = store2.keys().next();
|
|
1067
|
+
if (!oldest.done) store2.delete(oldest.value);
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
get size() {
|
|
1071
|
+
return store2.size;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1027
1076
|
// src/dom/support.ts
|
|
1028
1077
|
var backdropFilterSupport = null;
|
|
1029
1078
|
var svgBackdropFilterSupport = null;
|
|
@@ -1687,7 +1736,7 @@ function createGlassGroup(members, opts = {}) {
|
|
|
1687
1736
|
}
|
|
1688
1737
|
function update() {
|
|
1689
1738
|
if (destroyed) return;
|
|
1690
|
-
const
|
|
1739
|
+
const probe2 = opts.probe ?? probeBackdrop;
|
|
1691
1740
|
members.forEach(({ el, legibility }, index) => {
|
|
1692
1741
|
const rect = el.getBoundingClientRect();
|
|
1693
1742
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
@@ -1698,7 +1747,7 @@ function createGlassGroup(members, opts = {}) {
|
|
|
1698
1747
|
String(index),
|
|
1699
1748
|
rect.left + rect.width / 2,
|
|
1700
1749
|
rect.top + rect.height / 2,
|
|
1701
|
-
|
|
1750
|
+
probe2(el),
|
|
1702
1751
|
legibility ?? 0
|
|
1703
1752
|
);
|
|
1704
1753
|
});
|
|
@@ -1862,6 +1911,18 @@ function fallbackFilterCss(optics) {
|
|
|
1862
1911
|
const saturate = 1 + optics.colorPickup;
|
|
1863
1912
|
return `blur(${blurPx}px) saturate(${saturate})`;
|
|
1864
1913
|
}
|
|
1914
|
+
function probe(el) {
|
|
1915
|
+
bump("probes");
|
|
1916
|
+
return probeBackdrop(el);
|
|
1917
|
+
}
|
|
1918
|
+
function decoded(url) {
|
|
1919
|
+
if (typeof Image === "undefined") return Promise.resolve();
|
|
1920
|
+
const img = new Image();
|
|
1921
|
+
img.src = url;
|
|
1922
|
+
return typeof img.decode === "function" ? img.decode().catch(() => void 0) : Promise.resolve();
|
|
1923
|
+
}
|
|
1924
|
+
var INTERIM_IDLE_MS = 2e3;
|
|
1925
|
+
var isEmpty = (g) => quantise(g.width) < 1 || quantise(g.height) < 1;
|
|
1865
1926
|
var to2553 = (v) => Math.round(Math.min(Math.max(v, 0), 1) * 255);
|
|
1866
1927
|
function rgbCss(r, g, b) {
|
|
1867
1928
|
return `rgb(${to2553(r)} ${to2553(g)} ${to2553(b)})`;
|
|
@@ -1888,6 +1949,13 @@ function attachGlass(el, opts = {}) {
|
|
|
1888
1949
|
let filterEl = null;
|
|
1889
1950
|
let destroyed = false;
|
|
1890
1951
|
let mapKey = "";
|
|
1952
|
+
let interim = false;
|
|
1953
|
+
let settling = false;
|
|
1954
|
+
let lastSample = null;
|
|
1955
|
+
let seq = 0;
|
|
1956
|
+
let shownSeq = 0;
|
|
1957
|
+
let idleTimer = null;
|
|
1958
|
+
const interimMaps = createLru(8);
|
|
1891
1959
|
let morph = null;
|
|
1892
1960
|
const onlyWhenTouched = opts.variant === "interactive";
|
|
1893
1961
|
let appear = onlyWhenTouched ? 0 : opts.appear ?? 1;
|
|
@@ -1899,6 +1967,7 @@ function attachGlass(el, opts = {}) {
|
|
|
1899
1967
|
mapKey = "";
|
|
1900
1968
|
deform.setElastic(elasticAllowed(opts.accessibility ?? a11y));
|
|
1901
1969
|
apply();
|
|
1970
|
+
if (interim) requestInterimMap();
|
|
1902
1971
|
});
|
|
1903
1972
|
warnIfNested(el);
|
|
1904
1973
|
const host = hostFor(el);
|
|
@@ -1924,24 +1993,111 @@ function attachGlass(el, opts = {}) {
|
|
|
1924
1993
|
const part = (s) => s ? `${s.offsetX},${s.offsetY},${s.width},${s.height},${s.cornerRadius}` : "-";
|
|
1925
1994
|
return `${morph.smoothing}/${part(morph.shape)}/${part(morph.shape2)}`;
|
|
1926
1995
|
}
|
|
1996
|
+
function invalidate() {
|
|
1997
|
+
seq += 1;
|
|
1998
|
+
shownSeq = seq;
|
|
1999
|
+
}
|
|
2000
|
+
function keepInterim() {
|
|
2001
|
+
if (idleTimer !== null) clearTimeout(idleTimer);
|
|
2002
|
+
idleTimer = setTimeout(leaveInterim, INTERIM_IDLE_MS);
|
|
2003
|
+
}
|
|
2004
|
+
function leaveInterim() {
|
|
2005
|
+
if (idleTimer !== null) clearTimeout(idleTimer);
|
|
2006
|
+
idleTimer = null;
|
|
2007
|
+
if (destroyed || !interim) return;
|
|
2008
|
+
interim = false;
|
|
2009
|
+
invalidate();
|
|
2010
|
+
mapKey = "";
|
|
2011
|
+
settling = true;
|
|
2012
|
+
apply();
|
|
2013
|
+
}
|
|
2014
|
+
function retarget(f, map, hue) {
|
|
2015
|
+
const images = f.querySelectorAll("feImage");
|
|
2016
|
+
images[0]?.setAttribute("href", map.url);
|
|
2017
|
+
if (hue) images[1]?.setAttribute("href", hue.url);
|
|
2018
|
+
for (const pass of f.querySelectorAll("feDisplacementMap")) {
|
|
2019
|
+
const ratio = Number(pass.getAttribute("data-vireglass-ratio") ?? 1);
|
|
2020
|
+
pass.setAttribute("scale", String(map.scale * (Number.isFinite(ratio) ? ratio : 1)));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function requestInterimMap() {
|
|
2024
|
+
if (!svgSupported || !filterEl || destroyed) return;
|
|
2025
|
+
const geometry = opts.geometry ?? measureGeometry(el);
|
|
2026
|
+
if (isEmpty(geometry)) return;
|
|
2027
|
+
const opticsNow = optics();
|
|
2028
|
+
const key = interimKey(
|
|
2029
|
+
geometry.width,
|
|
2030
|
+
geometry.height,
|
|
2031
|
+
geometry.cornerRadius,
|
|
2032
|
+
JSON.stringify([morph?.smoothing, morph?.shape, morph?.shape2, morph?.shapes]),
|
|
2033
|
+
`a${appear.toFixed(3)}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}`
|
|
2034
|
+
);
|
|
2035
|
+
let rec = interimMaps.get(key);
|
|
2036
|
+
if (!rec) {
|
|
2037
|
+
bump("reducedBakes");
|
|
2038
|
+
rec = { map: buildDisplacementMap(opticsNow, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false };
|
|
2039
|
+
interimMaps.set(key, rec);
|
|
2040
|
+
}
|
|
2041
|
+
const mine = seq += 1;
|
|
2042
|
+
const record = rec;
|
|
2043
|
+
const forFilter = filterEl;
|
|
2044
|
+
const show = () => {
|
|
2045
|
+
if (destroyed || filterEl !== forFilter || mine <= shownSeq) return;
|
|
2046
|
+
shownSeq = mine;
|
|
2047
|
+
retarget(forFilter, record.map, null);
|
|
2048
|
+
};
|
|
2049
|
+
if (record.ready) {
|
|
2050
|
+
show();
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
void decoded(record.map.url).then(() => {
|
|
2054
|
+
record.ready = true;
|
|
2055
|
+
show();
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1927
2058
|
function apply() {
|
|
1928
2059
|
if (destroyed) return;
|
|
2060
|
+
if (interim) keepInterim();
|
|
1929
2061
|
const geometry = opts.geometry ?? measureGeometry(el);
|
|
1930
2062
|
const opticsNow = optics();
|
|
1931
|
-
const sample = supplied ??
|
|
2063
|
+
const sample = supplied ?? (interim && lastSample ? lastSample : probe(el));
|
|
2064
|
+
lastSample = sample;
|
|
1932
2065
|
const light = shouldInkBeLight(sample, wasLight);
|
|
1933
2066
|
wasLight = light;
|
|
1934
2067
|
const body = opts.body === false ? null : bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear);
|
|
1935
2068
|
if (svgSupported) {
|
|
1936
2069
|
const key = `a${appear.toFixed(3)}|m${morphKey()}|${geometry.width}x${geometry.height}r${geometry.cornerRadius}@${devicePixelRatio()}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}:${opticsNow.blur}`;
|
|
1937
|
-
if (key !== mapKey || !filterEl) {
|
|
1938
|
-
const map = cached(`d|${key}`, () =>
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2070
|
+
if (interim && filterEl) ; else if (isEmpty(geometry)) ; else if (key !== mapKey || !filterEl) {
|
|
2071
|
+
const map = cached(`d|${key}`, () => {
|
|
2072
|
+
bump("fullBakes");
|
|
2073
|
+
return buildDisplacementMap(opticsNow, geometry, devicePixelRatio(), morphState());
|
|
2074
|
+
});
|
|
2075
|
+
const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => {
|
|
2076
|
+
bump("fullBakes");
|
|
2077
|
+
return buildSpectralMap(opticsNow, geometry, devicePixelRatio());
|
|
2078
|
+
}) : null;
|
|
2079
|
+
const signature = filterSignature(opticsNow.blur, opticsNow.ior, opticsNow.iorSpread, !!hue);
|
|
2080
|
+
if (settling && filterEl?.getAttribute("data-vireglass-signature") === signature) {
|
|
2081
|
+
settling = false;
|
|
2082
|
+
const mine = seq += 1;
|
|
2083
|
+
const forFilter = filterEl;
|
|
2084
|
+
const forKey = key;
|
|
2085
|
+
void Promise.all([decoded(map.url), hue ? decoded(hue.url) : void 0]).then(() => {
|
|
2086
|
+
if (destroyed || filterEl !== forFilter || mapKey !== forKey || mine <= shownSeq) return;
|
|
2087
|
+
shownSeq = mine;
|
|
2088
|
+
retarget(forFilter, map, hue);
|
|
2089
|
+
});
|
|
2090
|
+
} else {
|
|
2091
|
+
settling = false;
|
|
2092
|
+
invalidate();
|
|
2093
|
+
const defs = getFilterHost(host).querySelector("defs");
|
|
2094
|
+
const next = buildFilterElement(id, map.url, opticsNow, map.scale, hue);
|
|
2095
|
+
next.setAttribute("data-vireglass-signature", signature);
|
|
2096
|
+
if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
|
|
2097
|
+
else defs.appendChild(next);
|
|
2098
|
+
bump("filterSwaps");
|
|
2099
|
+
filterEl = next;
|
|
2100
|
+
}
|
|
1945
2101
|
mapKey = key;
|
|
1946
2102
|
}
|
|
1947
2103
|
el.style.setProperty("backdrop-filter", `url(#${id})`);
|
|
@@ -1988,7 +2144,9 @@ function attachGlass(el, opts = {}) {
|
|
|
1988
2144
|
const base = optics();
|
|
1989
2145
|
const rise = raiseIntoGlass(sampleOut.press);
|
|
1990
2146
|
const risen = optics({ ...opts.material, roughness: (opts.material?.roughness ?? VIREGLASS_MATERIAL.roughness) * rise.solid });
|
|
1991
|
-
|
|
2147
|
+
if (interim) keepInterim();
|
|
2148
|
+
bump("reducedBakes");
|
|
2149
|
+
const map = buildDisplacementMap(risen, geometry, 1, {
|
|
1992
2150
|
...morphState(),
|
|
1993
2151
|
touch: {
|
|
1994
2152
|
x: sampleOut.touchX,
|
|
@@ -2001,6 +2159,7 @@ function attachGlass(el, opts = {}) {
|
|
|
2001
2159
|
wavePhase: sampleOut.wavePhase
|
|
2002
2160
|
}
|
|
2003
2161
|
});
|
|
2162
|
+
invalidate();
|
|
2004
2163
|
if (filterEl) {
|
|
2005
2164
|
const feImage = filterEl.querySelector("feImage");
|
|
2006
2165
|
feImage?.setAttribute("href", map.url);
|
|
@@ -2023,7 +2182,7 @@ function attachGlass(el, opts = {}) {
|
|
|
2023
2182
|
el.style.setProperty("--vireglass-glow-opacity", "0");
|
|
2024
2183
|
return;
|
|
2025
2184
|
}
|
|
2026
|
-
const [gr, gg, gb] = ambientFrom(supplied ??
|
|
2185
|
+
const [gr, gg, gb] = ambientFrom(supplied ?? (interim && lastSample ? lastSample : probe(el)));
|
|
2027
2186
|
const cx = (x + geometry.width / 2) / geometry.width * 100;
|
|
2028
2187
|
const cy = (y + geometry.height / 2) / geometry.height * 100;
|
|
2029
2188
|
const reach = halfMinDp(geometry) * TOUCH_RADIUS_FRACTION;
|
|
@@ -2047,7 +2206,9 @@ function attachGlass(el, opts = {}) {
|
|
|
2047
2206
|
lastFrameAt = 0;
|
|
2048
2207
|
el.style.setProperty("--vireglass-glow-opacity", "0");
|
|
2049
2208
|
opts.onGlow?.(null);
|
|
2209
|
+
settling = !interim;
|
|
2050
2210
|
apply();
|
|
2211
|
+
if (interim) requestInterimMap();
|
|
2051
2212
|
return;
|
|
2052
2213
|
}
|
|
2053
2214
|
frame = requestAnimationFrame(tick);
|
|
@@ -2095,6 +2256,9 @@ function attachGlass(el, opts = {}) {
|
|
|
2095
2256
|
}
|
|
2096
2257
|
function destroy() {
|
|
2097
2258
|
destroyed = true;
|
|
2259
|
+
invalidate();
|
|
2260
|
+
if (idleTimer !== null) clearTimeout(idleTimer);
|
|
2261
|
+
idleTimer = null;
|
|
2098
2262
|
stopWatchingA11y();
|
|
2099
2263
|
if (attached.get(el) === handle) attached.delete(el);
|
|
2100
2264
|
if (frame !== null) cancelAnimationFrame(frame);
|
|
@@ -2136,6 +2300,7 @@ function attachGlass(el, opts = {}) {
|
|
|
2136
2300
|
if (next === appear) return;
|
|
2137
2301
|
appear = next;
|
|
2138
2302
|
apply();
|
|
2303
|
+
if (interim) requestInterimMap();
|
|
2139
2304
|
},
|
|
2140
2305
|
setAccent(next) {
|
|
2141
2306
|
accent = next ?? void 0;
|
|
@@ -2158,9 +2323,23 @@ function attachGlass(el, opts = {}) {
|
|
|
2158
2323
|
},
|
|
2159
2324
|
setMorph(next) {
|
|
2160
2325
|
morph = next && next.smoothing > 0 ? next : null;
|
|
2326
|
+
if (interim) {
|
|
2327
|
+
apply();
|
|
2328
|
+
requestInterimMap();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2161
2331
|
mapKey = "";
|
|
2162
2332
|
apply();
|
|
2163
2333
|
},
|
|
2334
|
+
setInterim(on) {
|
|
2335
|
+
if (destroyed || on === interim) return;
|
|
2336
|
+
if (on) {
|
|
2337
|
+
interim = true;
|
|
2338
|
+
keepInterim();
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
leaveInterim();
|
|
2342
|
+
},
|
|
2164
2343
|
setSample(next) {
|
|
2165
2344
|
supplied = next ?? void 0;
|
|
2166
2345
|
apply();
|
|
@@ -2192,6 +2371,8 @@ exports.parseColor = parseColor;
|
|
|
2192
2371
|
exports.probeBackdrop = probeBackdrop;
|
|
2193
2372
|
exports.renderDisplacementPixels = renderDisplacementPixels;
|
|
2194
2373
|
exports.renderSpectralPixels = renderSpectralPixels;
|
|
2374
|
+
exports.renderStats = renderStats;
|
|
2375
|
+
exports.resetRenderStats = resetRenderStats;
|
|
2195
2376
|
exports.resetSupportCache = resetSupportCache;
|
|
2196
2377
|
exports.resolveBody = resolveBody;
|
|
2197
2378
|
exports.rimGradientCss = rimGradientCss;
|