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 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);
@@ -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
- data[i] = neutral;
732
- data[i + 1] = neutral;
733
- data[i + 2] = neutral;
734
- data[i + 3] = 255;
735
- continue;
736
- }
737
- const [r, g, b] = spectralHue(e, bevel, rim, thick, optics);
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.setAttribute("scale", String(map.scale * (Number.isFinite(ratio) ? ratio : 1)));
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 opticsNow = optics();
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
- `a${appear.toFixed(3)}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}`
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(opticsNow, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false };
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 key = `a${appear.toFixed(3)}|m${morphKey()}|${geometry.width}x${geometry.height}r${geometry.cornerRadius}@${devicePixelRatio()}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}:${opticsNow.blur}`;
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(opticsNow, geometry, devicePixelRatio(), morphState());
2112
+ return buildDisplacementMap(full, geometry, devicePixelRatio(), morphState());
2074
2113
  });
2075
- const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => {
2114
+ const hue = hasSpectralEdge(full) ? cached(`h|${key}`, () => {
2076
2115
  bump("fullBakes");
2077
- return buildSpectralMap(opticsNow, geometry, devicePixelRatio());
2116
+ return buildSpectralMap(full, geometry, devicePixelRatio());
2078
2117
  }) : null;
2079
- const signature = filterSignature(opticsNow.blur, opticsNow.ior, opticsNow.iorSpread, !!hue);
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, opticsNow, map.scale, hue);
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;