vireglass 3.1.1 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/dom.cjs +68 -27
- package/dist/dom.cjs.map +1 -1
- package/dist/dom.d.cts +6 -1
- package/dist/dom.d.ts +6 -1
- package/dist/dom.js +70 -30
- package/dist/dom.js.map +1 -1
- package/package.json +1 -1
- package/src/dom/displacement.ts +9 -2
- package/src/dom/index.ts +46 -16
- package/src/dom/spectral-map.ts +36 -12
package/README.md
CHANGED
|
@@ -153,6 +153,10 @@ 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
|
+
`glass.setAppear(t)` brings the element in and out by how much light it bends, not by opacity. The
|
|
157
|
+
maps are baked once at full presence and appear is written in place on the filter, so driving it
|
|
158
|
+
every frame costs no bake and no new filter, at rest or interim.
|
|
159
|
+
|
|
156
160
|
### Morphing elements
|
|
157
161
|
|
|
158
162
|
```ts
|
|
@@ -168,7 +172,7 @@ backdrop probe, a full-resolution displacement map (and spectral map) and a new
|
|
|
168
172
|
That is the difference between 60 fps and 10 to 20. While interim, the element does none of it: it
|
|
169
173
|
reuses its last sample, keeps its filter, and the browser stretches the existing map over the live
|
|
170
174
|
box. The body, rim, shadow and custom properties still update every frame. A change of the morph
|
|
171
|
-
shapes passed to `setMorph
|
|
175
|
+
shapes passed to `setMorph` or of the system accessibility settings rebuilds the map
|
|
172
176
|
at reduced resolution and shows it in place, where the browser decodes lazily only once it has been
|
|
173
177
|
decoded. `setInterim(false)` does one full-resolution rebuild and one probe (with a supplied
|
|
174
178
|
`sample` it does not probe). Repeating `setInterim(false)` is a no-op.
|
package/dist/dom.cjs
CHANGED
|
@@ -579,13 +579,15 @@ function bevelProfile(t) {
|
|
|
579
579
|
const raw = Math.min(t / Math.sqrt(Math.max(1 - t * t * BEVEL.sphere, BEVEL.floor)), BEVEL.slopeMax);
|
|
580
580
|
return raw / BEVEL.slopeMax;
|
|
581
581
|
}
|
|
582
|
+
function displacementScale(optics, geometry) {
|
|
583
|
+
return Math.min(optics.refraction * thicknessDp(geometry, optics) * SCALE_GAIN, MAX_SCALE);
|
|
584
|
+
}
|
|
582
585
|
function renderDisplacementPixels(optics, geometry, dpr, state = {}) {
|
|
583
586
|
const bevel = bevelDp(geometry, optics);
|
|
584
587
|
const density = mapDensity(geometry, bevel, dpr);
|
|
585
588
|
const width = Math.max(1, Math.round(geometry.width * density));
|
|
586
589
|
const height = Math.max(1, Math.round(geometry.height * density));
|
|
587
|
-
const
|
|
588
|
-
const scale = Math.min(rawScale, MAX_SCALE);
|
|
590
|
+
const scale = displacementScale(optics, geometry);
|
|
589
591
|
const smoothing = state.smoothing ?? 0;
|
|
590
592
|
const shapes = [state.morph, state.morph2, ...state.morphs ?? []];
|
|
591
593
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
@@ -709,6 +711,18 @@ function mapDensity2(bevel, dpr) {
|
|
|
709
711
|
if (bevel <= 0) return dpr;
|
|
710
712
|
return Math.min(dpr, BEVEL_SAMPLES2 / bevel);
|
|
711
713
|
}
|
|
714
|
+
var DEPTH_STEPS = 2048;
|
|
715
|
+
function hueAlongDepth(bevel, rim, thick, optics) {
|
|
716
|
+
const table = new Float64Array((DEPTH_STEPS + 1) * 3);
|
|
717
|
+
for (let k = 0; k <= DEPTH_STEPS; k += 1) {
|
|
718
|
+
const s = k / DEPTH_STEPS;
|
|
719
|
+
const [r, g, b] = spectralHue(s * s * bevel, bevel, rim, thick, optics);
|
|
720
|
+
table[k * 3] = r;
|
|
721
|
+
table[k * 3 + 1] = g;
|
|
722
|
+
table[k * 3 + 2] = b;
|
|
723
|
+
}
|
|
724
|
+
return table;
|
|
725
|
+
}
|
|
712
726
|
function renderSpectralPixels(optics, geometry, dpr) {
|
|
713
727
|
const bevel = bevelDp(geometry, optics);
|
|
714
728
|
const density = mapDensity2(bevel, dpr);
|
|
@@ -720,24 +734,28 @@ function renderSpectralPixels(optics, geometry, dpr) {
|
|
|
720
734
|
const halfW = geometry.width / 2;
|
|
721
735
|
const halfH = geometry.height / 2;
|
|
722
736
|
const neutral = Math.round(1 / HEADROOM * 255);
|
|
737
|
+
const table = hueAlongDepth(bevel, rim, thick, optics);
|
|
738
|
+
const toByte = 255 / HEADROOM;
|
|
739
|
+
new Uint32Array(data.buffer).fill(new Uint32Array(new Uint8ClampedArray([neutral, neutral, neutral, 255]).buffer)[0]);
|
|
740
|
+
const corner = Math.max(geometry.cornerRadius, bevel);
|
|
723
741
|
for (let py = 0; py < height; py += 1) {
|
|
724
742
|
const y = (py + 0.5) / density - halfH;
|
|
743
|
+
const ay = Math.abs(y);
|
|
744
|
+
const plain = Math.max(ay <= halfH - bevel ? halfW - corner : -1, ay <= halfH - corner ? halfW - bevel : -1);
|
|
725
745
|
for (let px = 0; px < width; px += 1) {
|
|
726
746
|
const x = (px + 0.5) / density - halfW;
|
|
747
|
+
if (Math.abs(x) <= plain) continue;
|
|
727
748
|
const i = (py * width + px) * 4;
|
|
728
749
|
const sd = sdfRoundedRect(x, y, geometry.width, geometry.height, geometry.cornerRadius);
|
|
729
750
|
const e = Math.max(-sd, 0);
|
|
730
|
-
if (e >= bevel)
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
data[i] = Math.round(r / HEADROOM * 255);
|
|
739
|
-
data[i + 1] = Math.round(g / HEADROOM * 255);
|
|
740
|
-
data[i + 2] = Math.round(b / HEADROOM * 255);
|
|
751
|
+
if (e >= bevel) continue;
|
|
752
|
+
const u = Math.sqrt(e / bevel) * DEPTH_STEPS;
|
|
753
|
+
const k = Math.min(Math.floor(u), DEPTH_STEPS - 1);
|
|
754
|
+
const f = u - k;
|
|
755
|
+
const at = k * 3;
|
|
756
|
+
data[i] = Math.round((table[at] + (table[at + 3] - table[at]) * f) * toByte);
|
|
757
|
+
data[i + 1] = Math.round((table[at + 1] + (table[at + 4] - table[at + 1]) * f) * toByte);
|
|
758
|
+
data[i + 2] = Math.round((table[at + 2] + (table[at + 5] - table[at + 2]) * f) * toByte);
|
|
741
759
|
data[i + 3] = 255;
|
|
742
760
|
}
|
|
743
761
|
}
|
|
@@ -1807,6 +1825,9 @@ function getFilterHost(host) {
|
|
|
1807
1825
|
(host instanceof ShadowRoot ? host : host.body).appendChild(svg);
|
|
1808
1826
|
return svg;
|
|
1809
1827
|
}
|
|
1828
|
+
function writeAttr(node, name, value) {
|
|
1829
|
+
if (node.getAttribute(name) !== value) node.setAttribute(name, value);
|
|
1830
|
+
}
|
|
1810
1831
|
function buildFilterElement(id, mapUrl, optics, scale, spectral) {
|
|
1811
1832
|
const filter = document.createElementNS(SVG_NS, "filter");
|
|
1812
1833
|
filter.setAttribute("id", id);
|
|
@@ -1892,6 +1913,7 @@ function withSpectral(filter, spectral) {
|
|
|
1892
1913
|
product.setAttribute("in2", "hue");
|
|
1893
1914
|
product.setAttribute("operator", "arithmetic");
|
|
1894
1915
|
product.setAttribute("k1", String(spectral.headroom));
|
|
1916
|
+
product.setAttribute("data-vireglass-headroom", String(spectral.headroom));
|
|
1895
1917
|
product.setAttribute("k2", "0");
|
|
1896
1918
|
product.setAttribute("k3", "0");
|
|
1897
1919
|
product.setAttribute("k4", "0");
|
|
@@ -1949,6 +1971,7 @@ function attachGlass(el, opts = {}) {
|
|
|
1949
1971
|
let filterEl = null;
|
|
1950
1972
|
let destroyed = false;
|
|
1951
1973
|
let mapKey = "";
|
|
1974
|
+
let shownGeometry = null;
|
|
1952
1975
|
let interim = false;
|
|
1953
1976
|
let settling = false;
|
|
1954
1977
|
let lastSample = null;
|
|
@@ -2011,31 +2034,46 @@ function attachGlass(el, opts = {}) {
|
|
|
2011
2034
|
settling = true;
|
|
2012
2035
|
apply();
|
|
2013
2036
|
}
|
|
2014
|
-
function retarget(f, map, hue) {
|
|
2037
|
+
function retarget(f, map, hue, geometry) {
|
|
2015
2038
|
const images = f.querySelectorAll("feImage");
|
|
2016
2039
|
images[0]?.setAttribute("href", map.url);
|
|
2017
2040
|
if (hue) images[1]?.setAttribute("href", hue.url);
|
|
2041
|
+
shownGeometry = geometry;
|
|
2042
|
+
present(f);
|
|
2043
|
+
}
|
|
2044
|
+
function present(f) {
|
|
2045
|
+
if (!shownGeometry) return;
|
|
2046
|
+
const o = optics();
|
|
2047
|
+
const scale = displacementScale(o, shownGeometry);
|
|
2018
2048
|
for (const pass of f.querySelectorAll("feDisplacementMap")) {
|
|
2019
2049
|
const ratio = Number(pass.getAttribute("data-vireglass-ratio") ?? 1);
|
|
2020
|
-
pass
|
|
2050
|
+
writeAttr(pass, "scale", String(scale * (Number.isFinite(ratio) ? ratio : 1)));
|
|
2051
|
+
}
|
|
2052
|
+
const frost = f.querySelector("feGaussianBlur");
|
|
2053
|
+
if (frost) writeAttr(frost, "stdDeviation", String(Math.max(o.blur, 0)));
|
|
2054
|
+
const hue = f.querySelector('feComposite[in2="hue"]');
|
|
2055
|
+
if (hue) {
|
|
2056
|
+
const headroom = Number(hue.getAttribute("data-vireglass-headroom") ?? 1);
|
|
2057
|
+
writeAttr(hue, "k1", String(headroom * appear));
|
|
2058
|
+
writeAttr(hue, "k2", String(1 - appear));
|
|
2021
2059
|
}
|
|
2022
2060
|
}
|
|
2023
2061
|
function requestInterimMap() {
|
|
2024
2062
|
if (!svgSupported || !filterEl || destroyed) return;
|
|
2025
2063
|
const geometry = opts.geometry ?? measureGeometry(el);
|
|
2026
2064
|
if (isEmpty(geometry)) return;
|
|
2027
|
-
const
|
|
2065
|
+
const full = settledOptics();
|
|
2028
2066
|
const key = interimKey(
|
|
2029
2067
|
geometry.width,
|
|
2030
2068
|
geometry.height,
|
|
2031
2069
|
geometry.cornerRadius,
|
|
2032
2070
|
JSON.stringify([morph?.smoothing, morph?.shape, morph?.shape2, morph?.shapes]),
|
|
2033
|
-
|
|
2071
|
+
`${full.refraction}:${full.refractionScale}:${full.bevelDp}`
|
|
2034
2072
|
);
|
|
2035
2073
|
let rec = interimMaps.get(key);
|
|
2036
2074
|
if (!rec) {
|
|
2037
2075
|
bump("reducedBakes");
|
|
2038
|
-
rec = { map: buildDisplacementMap(
|
|
2076
|
+
rec = { map: buildDisplacementMap(full, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false, geometry };
|
|
2039
2077
|
interimMaps.set(key, rec);
|
|
2040
2078
|
}
|
|
2041
2079
|
const mine = seq += 1;
|
|
@@ -2044,7 +2082,7 @@ function attachGlass(el, opts = {}) {
|
|
|
2044
2082
|
const show = () => {
|
|
2045
2083
|
if (destroyed || filterEl !== forFilter || mine <= shownSeq) return;
|
|
2046
2084
|
shownSeq = mine;
|
|
2047
|
-
retarget(forFilter, record.map, null);
|
|
2085
|
+
retarget(forFilter, record.map, null, record.geometry);
|
|
2048
2086
|
};
|
|
2049
2087
|
if (record.ready) {
|
|
2050
2088
|
show();
|
|
@@ -2066,17 +2104,18 @@ function attachGlass(el, opts = {}) {
|
|
|
2066
2104
|
wasLight = light;
|
|
2067
2105
|
const body = opts.body === false ? null : bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear);
|
|
2068
2106
|
if (svgSupported) {
|
|
2069
|
-
const
|
|
2107
|
+
const full = settledOptics();
|
|
2108
|
+
const key = `m${morphKey()}|${geometry.width}x${geometry.height}r${geometry.cornerRadius}@${devicePixelRatio()}:${full.refraction}:${full.refractionScale}:${full.bevelDp}:${full.blur}:${full.thicknessDp}:${full.film}:${full.iridescence}:${full.diffraction}`;
|
|
2070
2109
|
if (interim && filterEl) ; else if (isEmpty(geometry)) ; else if (key !== mapKey || !filterEl) {
|
|
2071
2110
|
const map = cached(`d|${key}`, () => {
|
|
2072
2111
|
bump("fullBakes");
|
|
2073
|
-
return buildDisplacementMap(
|
|
2112
|
+
return buildDisplacementMap(full, geometry, devicePixelRatio(), morphState());
|
|
2074
2113
|
});
|
|
2075
|
-
const hue = hasSpectralEdge(
|
|
2114
|
+
const hue = hasSpectralEdge(full) ? cached(`h|${key}`, () => {
|
|
2076
2115
|
bump("fullBakes");
|
|
2077
|
-
return buildSpectralMap(
|
|
2116
|
+
return buildSpectralMap(full, geometry, devicePixelRatio());
|
|
2078
2117
|
}) : null;
|
|
2079
|
-
const signature = filterSignature(
|
|
2118
|
+
const signature = filterSignature(full.blur, full.ior, full.iorSpread, !!hue);
|
|
2080
2119
|
if (settling && filterEl?.getAttribute("data-vireglass-signature") === signature) {
|
|
2081
2120
|
settling = false;
|
|
2082
2121
|
const mine = seq += 1;
|
|
@@ -2085,21 +2124,23 @@ function attachGlass(el, opts = {}) {
|
|
|
2085
2124
|
void Promise.all([decoded(map.url), hue ? decoded(hue.url) : void 0]).then(() => {
|
|
2086
2125
|
if (destroyed || filterEl !== forFilter || mapKey !== forKey || mine <= shownSeq) return;
|
|
2087
2126
|
shownSeq = mine;
|
|
2088
|
-
retarget(forFilter, map, hue);
|
|
2127
|
+
retarget(forFilter, map, hue, geometry);
|
|
2089
2128
|
});
|
|
2090
2129
|
} else {
|
|
2091
2130
|
settling = false;
|
|
2092
2131
|
invalidate();
|
|
2093
2132
|
const defs = getFilterHost(host).querySelector("defs");
|
|
2094
|
-
const next = buildFilterElement(id, map.url,
|
|
2133
|
+
const next = buildFilterElement(id, map.url, full, map.scale, hue);
|
|
2095
2134
|
next.setAttribute("data-vireglass-signature", signature);
|
|
2096
2135
|
if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
|
|
2097
2136
|
else defs.appendChild(next);
|
|
2098
2137
|
bump("filterSwaps");
|
|
2099
2138
|
filterEl = next;
|
|
2139
|
+
shownGeometry = geometry;
|
|
2100
2140
|
}
|
|
2101
2141
|
mapKey = key;
|
|
2102
2142
|
}
|
|
2143
|
+
if (filterEl) present(filterEl);
|
|
2103
2144
|
el.style.setProperty("backdrop-filter", `url(#${id})`);
|
|
2104
2145
|
el.style.setProperty("-webkit-backdrop-filter", `url(#${id})`);
|
|
2105
2146
|
} else {
|
|
@@ -2300,7 +2341,6 @@ function attachGlass(el, opts = {}) {
|
|
|
2300
2341
|
if (next === appear) return;
|
|
2301
2342
|
appear = next;
|
|
2302
2343
|
apply();
|
|
2303
|
-
if (interim) requestInterimMap();
|
|
2304
2344
|
},
|
|
2305
2345
|
setAccent(next) {
|
|
2306
2346
|
accent = next ?? void 0;
|
|
@@ -2363,6 +2403,7 @@ exports.clearMapCache = clearMapCache;
|
|
|
2363
2403
|
exports.compositeLayers = compositeLayers;
|
|
2364
2404
|
exports.contrastRimCss = contrastRimCss;
|
|
2365
2405
|
exports.createGlassGroup = createGlassGroup;
|
|
2406
|
+
exports.displacementScale = displacementScale;
|
|
2366
2407
|
exports.hasSpectralEdge = hasSpectralEdge;
|
|
2367
2408
|
exports.lightConicAngle = lightConicAngle;
|
|
2368
2409
|
exports.mapCacheSize = mapCacheSize;
|