vireglass 3.1.1 → 3.2.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 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`, of `setAppear` or of the system accessibility settings rebuilds the map
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 rawScale = optics.refraction * thicknessDp(geometry, optics) * SCALE_GAIN;
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);
@@ -1807,6 +1809,9 @@ function getFilterHost(host) {
1807
1809
  (host instanceof ShadowRoot ? host : host.body).appendChild(svg);
1808
1810
  return svg;
1809
1811
  }
1812
+ function writeAttr(node, name, value) {
1813
+ if (node.getAttribute(name) !== value) node.setAttribute(name, value);
1814
+ }
1810
1815
  function buildFilterElement(id, mapUrl, optics, scale, spectral) {
1811
1816
  const filter = document.createElementNS(SVG_NS, "filter");
1812
1817
  filter.setAttribute("id", id);
@@ -1892,6 +1897,7 @@ function withSpectral(filter, spectral) {
1892
1897
  product.setAttribute("in2", "hue");
1893
1898
  product.setAttribute("operator", "arithmetic");
1894
1899
  product.setAttribute("k1", String(spectral.headroom));
1900
+ product.setAttribute("data-vireglass-headroom", String(spectral.headroom));
1895
1901
  product.setAttribute("k2", "0");
1896
1902
  product.setAttribute("k3", "0");
1897
1903
  product.setAttribute("k4", "0");
@@ -1949,6 +1955,7 @@ function attachGlass(el, opts = {}) {
1949
1955
  let filterEl = null;
1950
1956
  let destroyed = false;
1951
1957
  let mapKey = "";
1958
+ let shownGeometry = null;
1952
1959
  let interim = false;
1953
1960
  let settling = false;
1954
1961
  let lastSample = null;
@@ -2011,31 +2018,46 @@ function attachGlass(el, opts = {}) {
2011
2018
  settling = true;
2012
2019
  apply();
2013
2020
  }
2014
- function retarget(f, map, hue) {
2021
+ function retarget(f, map, hue, geometry) {
2015
2022
  const images = f.querySelectorAll("feImage");
2016
2023
  images[0]?.setAttribute("href", map.url);
2017
2024
  if (hue) images[1]?.setAttribute("href", hue.url);
2025
+ shownGeometry = geometry;
2026
+ present(f);
2027
+ }
2028
+ function present(f) {
2029
+ if (!shownGeometry) return;
2030
+ const o = optics();
2031
+ const scale = displacementScale(o, shownGeometry);
2018
2032
  for (const pass of f.querySelectorAll("feDisplacementMap")) {
2019
2033
  const ratio = Number(pass.getAttribute("data-vireglass-ratio") ?? 1);
2020
- pass.setAttribute("scale", String(map.scale * (Number.isFinite(ratio) ? ratio : 1)));
2034
+ writeAttr(pass, "scale", String(scale * (Number.isFinite(ratio) ? ratio : 1)));
2035
+ }
2036
+ const frost = f.querySelector("feGaussianBlur");
2037
+ if (frost) writeAttr(frost, "stdDeviation", String(Math.max(o.blur, 0)));
2038
+ const hue = f.querySelector('feComposite[in2="hue"]');
2039
+ if (hue) {
2040
+ const headroom = Number(hue.getAttribute("data-vireglass-headroom") ?? 1);
2041
+ writeAttr(hue, "k1", String(headroom * appear));
2042
+ writeAttr(hue, "k2", String(1 - appear));
2021
2043
  }
2022
2044
  }
2023
2045
  function requestInterimMap() {
2024
2046
  if (!svgSupported || !filterEl || destroyed) return;
2025
2047
  const geometry = opts.geometry ?? measureGeometry(el);
2026
2048
  if (isEmpty(geometry)) return;
2027
- const opticsNow = optics();
2049
+ const full = settledOptics();
2028
2050
  const key = interimKey(
2029
2051
  geometry.width,
2030
2052
  geometry.height,
2031
2053
  geometry.cornerRadius,
2032
2054
  JSON.stringify([morph?.smoothing, morph?.shape, morph?.shape2, morph?.shapes]),
2033
- `a${appear.toFixed(3)}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}`
2055
+ `${full.refraction}:${full.refractionScale}:${full.bevelDp}`
2034
2056
  );
2035
2057
  let rec = interimMaps.get(key);
2036
2058
  if (!rec) {
2037
2059
  bump("reducedBakes");
2038
- rec = { map: buildDisplacementMap(opticsNow, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false };
2060
+ rec = { map: buildDisplacementMap(full, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false, geometry };
2039
2061
  interimMaps.set(key, rec);
2040
2062
  }
2041
2063
  const mine = seq += 1;
@@ -2044,7 +2066,7 @@ function attachGlass(el, opts = {}) {
2044
2066
  const show = () => {
2045
2067
  if (destroyed || filterEl !== forFilter || mine <= shownSeq) return;
2046
2068
  shownSeq = mine;
2047
- retarget(forFilter, record.map, null);
2069
+ retarget(forFilter, record.map, null, record.geometry);
2048
2070
  };
2049
2071
  if (record.ready) {
2050
2072
  show();
@@ -2066,17 +2088,18 @@ function attachGlass(el, opts = {}) {
2066
2088
  wasLight = light;
2067
2089
  const body = opts.body === false ? null : bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear);
2068
2090
  if (svgSupported) {
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}`;
2091
+ const full = settledOptics();
2092
+ const key = `m${morphKey()}|${geometry.width}x${geometry.height}r${geometry.cornerRadius}@${devicePixelRatio()}:${full.refraction}:${full.refractionScale}:${full.bevelDp}:${full.blur}`;
2070
2093
  if (interim && filterEl) ; else if (isEmpty(geometry)) ; else if (key !== mapKey || !filterEl) {
2071
2094
  const map = cached(`d|${key}`, () => {
2072
2095
  bump("fullBakes");
2073
- return buildDisplacementMap(opticsNow, geometry, devicePixelRatio(), morphState());
2096
+ return buildDisplacementMap(full, geometry, devicePixelRatio(), morphState());
2074
2097
  });
2075
- const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => {
2098
+ const hue = hasSpectralEdge(full) ? cached(`h|${key}`, () => {
2076
2099
  bump("fullBakes");
2077
- return buildSpectralMap(opticsNow, geometry, devicePixelRatio());
2100
+ return buildSpectralMap(full, geometry, devicePixelRatio());
2078
2101
  }) : null;
2079
- const signature = filterSignature(opticsNow.blur, opticsNow.ior, opticsNow.iorSpread, !!hue);
2102
+ const signature = filterSignature(full.blur, full.ior, full.iorSpread, !!hue);
2080
2103
  if (settling && filterEl?.getAttribute("data-vireglass-signature") === signature) {
2081
2104
  settling = false;
2082
2105
  const mine = seq += 1;
@@ -2085,21 +2108,23 @@ function attachGlass(el, opts = {}) {
2085
2108
  void Promise.all([decoded(map.url), hue ? decoded(hue.url) : void 0]).then(() => {
2086
2109
  if (destroyed || filterEl !== forFilter || mapKey !== forKey || mine <= shownSeq) return;
2087
2110
  shownSeq = mine;
2088
- retarget(forFilter, map, hue);
2111
+ retarget(forFilter, map, hue, geometry);
2089
2112
  });
2090
2113
  } else {
2091
2114
  settling = false;
2092
2115
  invalidate();
2093
2116
  const defs = getFilterHost(host).querySelector("defs");
2094
- const next = buildFilterElement(id, map.url, opticsNow, map.scale, hue);
2117
+ const next = buildFilterElement(id, map.url, full, map.scale, hue);
2095
2118
  next.setAttribute("data-vireglass-signature", signature);
2096
2119
  if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
2097
2120
  else defs.appendChild(next);
2098
2121
  bump("filterSwaps");
2099
2122
  filterEl = next;
2123
+ shownGeometry = geometry;
2100
2124
  }
2101
2125
  mapKey = key;
2102
2126
  }
2127
+ if (filterEl) present(filterEl);
2103
2128
  el.style.setProperty("backdrop-filter", `url(#${id})`);
2104
2129
  el.style.setProperty("-webkit-backdrop-filter", `url(#${id})`);
2105
2130
  } else {
@@ -2300,7 +2325,6 @@ function attachGlass(el, opts = {}) {
2300
2325
  if (next === appear) return;
2301
2326
  appear = next;
2302
2327
  apply();
2303
- if (interim) requestInterimMap();
2304
2328
  },
2305
2329
  setAccent(next) {
2306
2330
  accent = next ?? void 0;
@@ -2363,6 +2387,7 @@ exports.clearMapCache = clearMapCache;
2363
2387
  exports.compositeLayers = compositeLayers;
2364
2388
  exports.contrastRimCss = contrastRimCss;
2365
2389
  exports.createGlassGroup = createGlassGroup;
2390
+ exports.displacementScale = displacementScale;
2366
2391
  exports.hasSpectralEdge = hasSpectralEdge;
2367
2392
  exports.lightConicAngle = lightConicAngle;
2368
2393
  exports.mapCacheSize = mapCacheSize;