vireglass 3.0.0 → 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/dist/dom.d.cts CHANGED
@@ -134,7 +134,7 @@ declare const RIM_WIDTH_PX: 1;
134
134
  * everything around it. It has to hold at every angle, including the one facing away from the key
135
135
  * light where the ordinary rim falls back to a dim dark line.
136
136
  */
137
- declare function contrastRimCss(bodyLuma: number, rimLuma: number): string;
137
+ declare function contrastRimCss(bodyLuma: number, rimLuma: number, appear?: number): string;
138
138
 
139
139
  declare function shadowAlphaFrom(sample: {
140
140
  busy: number;
@@ -309,6 +309,20 @@ declare function cached<T>(key: string, build: () => T): T;
309
309
  declare function clearMapCache(): void;
310
310
  declare function mapCacheSize(): number;
311
311
 
312
+ type RenderStats = {
313
+ /** `elementsFromPoint` backdrop probes. */
314
+ probes: number;
315
+ /** Displacement maps baked at full device resolution (cache hits are not bakes). */
316
+ fullBakes: number;
317
+ /** Displacement maps baked at reduced resolution: interim morphs and the finger path. */
318
+ reducedBakes: number;
319
+ /** Filter elements put into the shared `<defs>`, replacing or adding. */
320
+ filterSwaps: number;
321
+ };
322
+ /** The work the renderer has done since the last reset; the hook for a consumer's performance gate. */
323
+ declare function renderStats(): RenderStats;
324
+ declare function resetRenderStats(): void;
325
+
312
326
  type AttachGlassOptions = {
313
327
  material?: Partial<VireGlassMaterial>;
314
328
  /** Defaults to the element's measured box (size + `border-radius`). */
@@ -405,6 +419,14 @@ type GlassHandle = {
405
419
  strength: number;
406
420
  } | null): void;
407
421
  setMorph(morph: GlassMorph | null): void;
422
+ /**
423
+ * While on, the element is changing shape every frame (a morph, a drag, a growth). It stops
424
+ * probing the backdrop, stops rebuilding its maps and keeps its filter: the existing map is
425
+ * stretched over the live box. A change of `setMorph` shapes still updates the map, at reduced
426
+ * resolution and only once the new image has decoded. Turning it off does one full-resolution
427
+ * rebuild and one probe. Repeated calls with the same value do nothing.
428
+ */
429
+ setInterim(on: boolean): void;
408
430
  /**
409
431
  * Replaces the backdrop the glass adapts to, without re-attaching, and repaints. The same
410
432
  * object as `sample` in the options; `null` goes back to probing declared backgrounds.
@@ -414,4 +436,4 @@ type GlassHandle = {
414
436
  };
415
437
  declare function attachGlass(el: HTMLElement, opts?: AttachGlassOptions): GlassHandle;
416
438
 
417
- export { type AttachGlassOptions, type BodyBackdrop, type BodyLayer, type DisplacementMap, type DisplacementState, type EdgePaint, type GlassBody, type GlassGroupHandle, type GlassGroupOptions, type GlassHandle, type GlassMorph, type GroupEntry, type ProbeOptions, RIM_WIDTH_PX, type RgbColor, type RimAmbient, type ScrollEdgeHandle, type ScrollEdgeOptions, type SpectralMap, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
439
+ export { type AttachGlassOptions, type BodyBackdrop, type BodyLayer, type DisplacementMap, type DisplacementState, type EdgePaint, type GlassBody, type GlassGroupHandle, type GlassGroupOptions, type GlassHandle, type GlassMorph, type GroupEntry, type ProbeOptions, RIM_WIDTH_PX, type RenderStats, type RgbColor, type RimAmbient, type ScrollEdgeHandle, type ScrollEdgeOptions, type SpectralMap, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, renderStats, resetRenderStats, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
package/dist/dom.d.ts CHANGED
@@ -134,7 +134,7 @@ declare const RIM_WIDTH_PX: 1;
134
134
  * everything around it. It has to hold at every angle, including the one facing away from the key
135
135
  * light where the ordinary rim falls back to a dim dark line.
136
136
  */
137
- declare function contrastRimCss(bodyLuma: number, rimLuma: number): string;
137
+ declare function contrastRimCss(bodyLuma: number, rimLuma: number, appear?: number): string;
138
138
 
139
139
  declare function shadowAlphaFrom(sample: {
140
140
  busy: number;
@@ -309,6 +309,20 @@ declare function cached<T>(key: string, build: () => T): T;
309
309
  declare function clearMapCache(): void;
310
310
  declare function mapCacheSize(): number;
311
311
 
312
+ type RenderStats = {
313
+ /** `elementsFromPoint` backdrop probes. */
314
+ probes: number;
315
+ /** Displacement maps baked at full device resolution (cache hits are not bakes). */
316
+ fullBakes: number;
317
+ /** Displacement maps baked at reduced resolution: interim morphs and the finger path. */
318
+ reducedBakes: number;
319
+ /** Filter elements put into the shared `<defs>`, replacing or adding. */
320
+ filterSwaps: number;
321
+ };
322
+ /** The work the renderer has done since the last reset; the hook for a consumer's performance gate. */
323
+ declare function renderStats(): RenderStats;
324
+ declare function resetRenderStats(): void;
325
+
312
326
  type AttachGlassOptions = {
313
327
  material?: Partial<VireGlassMaterial>;
314
328
  /** Defaults to the element's measured box (size + `border-radius`). */
@@ -405,6 +419,14 @@ type GlassHandle = {
405
419
  strength: number;
406
420
  } | null): void;
407
421
  setMorph(morph: GlassMorph | null): void;
422
+ /**
423
+ * While on, the element is changing shape every frame (a morph, a drag, a growth). It stops
424
+ * probing the backdrop, stops rebuilding its maps and keeps its filter: the existing map is
425
+ * stretched over the live box. A change of `setMorph` shapes still updates the map, at reduced
426
+ * resolution and only once the new image has decoded. Turning it off does one full-resolution
427
+ * rebuild and one probe. Repeated calls with the same value do nothing.
428
+ */
429
+ setInterim(on: boolean): void;
408
430
  /**
409
431
  * Replaces the backdrop the glass adapts to, without re-attaching, and repaints. The same
410
432
  * object as `sample` in the options; `null` goes back to probing declared backgrounds.
@@ -414,4 +436,4 @@ type GlassHandle = {
414
436
  };
415
437
  declare function attachGlass(el: HTMLElement, opts?: AttachGlassOptions): GlassHandle;
416
438
 
417
- export { type AttachGlassOptions, type BodyBackdrop, type BodyLayer, type DisplacementMap, type DisplacementState, type EdgePaint, type GlassBody, type GlassGroupHandle, type GlassGroupOptions, type GlassHandle, type GlassMorph, type GroupEntry, type ProbeOptions, RIM_WIDTH_PX, type RgbColor, type RimAmbient, type ScrollEdgeHandle, type ScrollEdgeOptions, type SpectralMap, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
439
+ export { type AttachGlassOptions, type BodyBackdrop, type BodyLayer, type DisplacementMap, type DisplacementState, type EdgePaint, type GlassBody, type GlassGroupHandle, type GlassGroupOptions, type GlassHandle, type GlassMorph, type GroupEntry, type ProbeOptions, RIM_WIDTH_PX, type RenderStats, type RgbColor, type RimAmbient, type ScrollEdgeHandle, type ScrollEdgeOptions, type SpectralMap, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, renderStats, resetRenderStats, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
package/dist/dom.js CHANGED
@@ -340,6 +340,55 @@ function probeBackdrop(el, opts = {}) {
340
340
  return statsFromSamples(samples);
341
341
  }
342
342
 
343
+ // src/dom/stats.ts
344
+ var counts = { probes: 0, fullBakes: 0, reducedBakes: 0, filterSwaps: 0 };
345
+ function renderStats() {
346
+ return { ...counts };
347
+ }
348
+ function resetRenderStats() {
349
+ counts.probes = 0;
350
+ counts.fullBakes = 0;
351
+ counts.reducedBakes = 0;
352
+ counts.filterSwaps = 0;
353
+ }
354
+ function bump(key) {
355
+ counts[key] += 1;
356
+ }
357
+
358
+ // src/dom/interim.ts
359
+ function quantise(value, step = 2) {
360
+ return Math.round(value / step) * step;
361
+ }
362
+ function interimKey(width, height, cornerRadius, morph, extra) {
363
+ return `i|${quantise(width)}x${quantise(height)}r${quantise(cornerRadius)}|${morph}|${extra}`;
364
+ }
365
+ function filterSignature(blur, ior, iorSpread, hasHue) {
366
+ return `${blur}|${ior}|${iorSpread > 1e-4 ? iorSpread : 0}|${hasHue ? "h" : "-"}`;
367
+ }
368
+ function createLru(limit) {
369
+ const store2 = /* @__PURE__ */ new Map();
370
+ return {
371
+ get(key) {
372
+ const hit = store2.get(key);
373
+ if (hit === void 0) return void 0;
374
+ store2.delete(key);
375
+ store2.set(key, hit);
376
+ return hit;
377
+ },
378
+ set(key, value) {
379
+ store2.delete(key);
380
+ store2.set(key, value);
381
+ if (store2.size > limit) {
382
+ const oldest = store2.keys().next();
383
+ if (!oldest.done) store2.delete(oldest.value);
384
+ }
385
+ },
386
+ get size() {
387
+ return store2.size;
388
+ }
389
+ };
390
+ }
391
+
343
392
  // src/dom/support.ts
344
393
  var backdropFilterSupport = null;
345
394
  var svgBackdropFilterSupport = null;
@@ -557,9 +606,11 @@ function rimGradientCss(optics, ambient, light, appear = 1) {
557
606
  return `conic-gradient(from ${start.toFixed(1)}deg, ${line.join(", ")}), conic-gradient(${dark} 0deg, ${dark} 360deg)`;
558
607
  }
559
608
  var RIM_WIDTH_PX = RIM.widthPx;
560
- function contrastRimCss(bodyLuma, rimLuma) {
609
+ function contrastRimCss(bodyLuma, rimLuma, appear = 1) {
561
610
  const v = to2552(rimLuma);
562
- return `conic-gradient(rgb(${v},${v},${v}) 0deg, rgb(${v},${v},${v}) 360deg)`;
611
+ const present = clamp012(appear);
612
+ const colour = present >= 1 ? `rgb(${v},${v},${v})` : `rgba(${v},${v},${v},${present.toFixed(3)})`;
613
+ return `conic-gradient(${colour} 0deg, ${colour} 360deg)`;
563
614
  }
564
615
 
565
616
  // src/dom/nesting.ts
@@ -677,7 +728,7 @@ function createGlassGroup(members, opts = {}) {
677
728
  }
678
729
  function update() {
679
730
  if (destroyed) return;
680
- const probe = opts.probe ?? probeBackdrop;
731
+ const probe2 = opts.probe ?? probeBackdrop;
681
732
  members.forEach(({ el, legibility }, index) => {
682
733
  const rect = el.getBoundingClientRect();
683
734
  if (rect.width <= 0 || rect.height <= 0) {
@@ -688,7 +739,7 @@ function createGlassGroup(members, opts = {}) {
688
739
  String(index),
689
740
  rect.left + rect.width / 2,
690
741
  rect.top + rect.height / 2,
691
- probe(el),
742
+ probe2(el),
692
743
  legibility ?? 0
693
744
  );
694
745
  });
@@ -852,6 +903,18 @@ function fallbackFilterCss(optics) {
852
903
  const saturate = 1 + optics.colorPickup;
853
904
  return `blur(${blurPx}px) saturate(${saturate})`;
854
905
  }
906
+ function probe(el) {
907
+ bump("probes");
908
+ return probeBackdrop(el);
909
+ }
910
+ function decoded(url) {
911
+ if (typeof Image === "undefined") return Promise.resolve();
912
+ const img = new Image();
913
+ img.src = url;
914
+ return typeof img.decode === "function" ? img.decode().catch(() => void 0) : Promise.resolve();
915
+ }
916
+ var INTERIM_IDLE_MS = 2e3;
917
+ var isEmpty = (g) => quantise(g.width) < 1 || quantise(g.height) < 1;
855
918
  var to2553 = (v) => Math.round(Math.min(Math.max(v, 0), 1) * 255);
856
919
  function rgbCss(r, g, b) {
857
920
  return `rgb(${to2553(r)} ${to2553(g)} ${to2553(b)})`;
@@ -878,6 +941,13 @@ function attachGlass(el, opts = {}) {
878
941
  let filterEl = null;
879
942
  let destroyed = false;
880
943
  let mapKey = "";
944
+ let interim = false;
945
+ let settling = false;
946
+ let lastSample = null;
947
+ let seq = 0;
948
+ let shownSeq = 0;
949
+ let idleTimer = null;
950
+ const interimMaps = createLru(8);
881
951
  let morph = null;
882
952
  const onlyWhenTouched = opts.variant === "interactive";
883
953
  let appear = onlyWhenTouched ? 0 : opts.appear ?? 1;
@@ -889,6 +959,7 @@ function attachGlass(el, opts = {}) {
889
959
  mapKey = "";
890
960
  deform.setElastic(elasticAllowed(opts.accessibility ?? a11y));
891
961
  apply();
962
+ if (interim) requestInterimMap();
892
963
  });
893
964
  warnIfNested(el);
894
965
  const host = hostFor(el);
@@ -914,24 +985,111 @@ function attachGlass(el, opts = {}) {
914
985
  const part = (s) => s ? `${s.offsetX},${s.offsetY},${s.width},${s.height},${s.cornerRadius}` : "-";
915
986
  return `${morph.smoothing}/${part(morph.shape)}/${part(morph.shape2)}`;
916
987
  }
988
+ function invalidate() {
989
+ seq += 1;
990
+ shownSeq = seq;
991
+ }
992
+ function keepInterim() {
993
+ if (idleTimer !== null) clearTimeout(idleTimer);
994
+ idleTimer = setTimeout(leaveInterim, INTERIM_IDLE_MS);
995
+ }
996
+ function leaveInterim() {
997
+ if (idleTimer !== null) clearTimeout(idleTimer);
998
+ idleTimer = null;
999
+ if (destroyed || !interim) return;
1000
+ interim = false;
1001
+ invalidate();
1002
+ mapKey = "";
1003
+ settling = true;
1004
+ apply();
1005
+ }
1006
+ function retarget(f, map, hue) {
1007
+ const images = f.querySelectorAll("feImage");
1008
+ images[0]?.setAttribute("href", map.url);
1009
+ if (hue) images[1]?.setAttribute("href", hue.url);
1010
+ for (const pass of f.querySelectorAll("feDisplacementMap")) {
1011
+ const ratio = Number(pass.getAttribute("data-vireglass-ratio") ?? 1);
1012
+ pass.setAttribute("scale", String(map.scale * (Number.isFinite(ratio) ? ratio : 1)));
1013
+ }
1014
+ }
1015
+ function requestInterimMap() {
1016
+ if (!svgSupported || !filterEl || destroyed) return;
1017
+ const geometry = opts.geometry ?? measureGeometry(el);
1018
+ if (isEmpty(geometry)) return;
1019
+ const opticsNow = optics();
1020
+ const key = interimKey(
1021
+ geometry.width,
1022
+ geometry.height,
1023
+ geometry.cornerRadius,
1024
+ JSON.stringify([morph?.smoothing, morph?.shape, morph?.shape2, morph?.shapes]),
1025
+ `a${appear.toFixed(3)}:${opticsNow.refraction}:${opticsNow.refractionScale}:${opticsNow.bevelDp}`
1026
+ );
1027
+ let rec = interimMaps.get(key);
1028
+ if (!rec) {
1029
+ bump("reducedBakes");
1030
+ rec = { map: buildDisplacementMap(opticsNow, geometry, Math.min(1, devicePixelRatio()), morphState()), ready: false };
1031
+ interimMaps.set(key, rec);
1032
+ }
1033
+ const mine = seq += 1;
1034
+ const record = rec;
1035
+ const forFilter = filterEl;
1036
+ const show = () => {
1037
+ if (destroyed || filterEl !== forFilter || mine <= shownSeq) return;
1038
+ shownSeq = mine;
1039
+ retarget(forFilter, record.map, null);
1040
+ };
1041
+ if (record.ready) {
1042
+ show();
1043
+ return;
1044
+ }
1045
+ void decoded(record.map.url).then(() => {
1046
+ record.ready = true;
1047
+ show();
1048
+ });
1049
+ }
917
1050
  function apply() {
918
1051
  if (destroyed) return;
1052
+ if (interim) keepInterim();
919
1053
  const geometry = opts.geometry ?? measureGeometry(el);
920
1054
  const opticsNow = optics();
921
- const sample = supplied ?? probeBackdrop(el);
1055
+ const sample = supplied ?? (interim && lastSample ? lastSample : probe(el));
1056
+ lastSample = sample;
922
1057
  const light = shouldInkBeLight(sample, wasLight);
923
1058
  wasLight = light;
924
1059
  const body = opts.body === false ? null : bodyCss(settledOptics(), sample, light ? 1 : 0, accent, appear);
925
1060
  if (svgSupported) {
926
1061
  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}`;
927
- if (key !== mapKey || !filterEl) {
928
- const map = cached(`d|${key}`, () => buildDisplacementMap(opticsNow, geometry, devicePixelRatio(), morphState()));
929
- const defs = getFilterHost(host).querySelector("defs");
930
- const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => buildSpectralMap(opticsNow, geometry, devicePixelRatio())) : null;
931
- const next = buildFilterElement(id, map.url, opticsNow, map.scale, hue);
932
- if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
933
- else defs.appendChild(next);
934
- filterEl = next;
1062
+ if (interim && filterEl) ; else if (isEmpty(geometry)) ; else if (key !== mapKey || !filterEl) {
1063
+ const map = cached(`d|${key}`, () => {
1064
+ bump("fullBakes");
1065
+ return buildDisplacementMap(opticsNow, geometry, devicePixelRatio(), morphState());
1066
+ });
1067
+ const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => {
1068
+ bump("fullBakes");
1069
+ return buildSpectralMap(opticsNow, geometry, devicePixelRatio());
1070
+ }) : null;
1071
+ const signature = filterSignature(opticsNow.blur, opticsNow.ior, opticsNow.iorSpread, !!hue);
1072
+ if (settling && filterEl?.getAttribute("data-vireglass-signature") === signature) {
1073
+ settling = false;
1074
+ const mine = seq += 1;
1075
+ const forFilter = filterEl;
1076
+ const forKey = key;
1077
+ void Promise.all([decoded(map.url), hue ? decoded(hue.url) : void 0]).then(() => {
1078
+ if (destroyed || filterEl !== forFilter || mapKey !== forKey || mine <= shownSeq) return;
1079
+ shownSeq = mine;
1080
+ retarget(forFilter, map, hue);
1081
+ });
1082
+ } else {
1083
+ settling = false;
1084
+ invalidate();
1085
+ const defs = getFilterHost(host).querySelector("defs");
1086
+ const next = buildFilterElement(id, map.url, opticsNow, map.scale, hue);
1087
+ next.setAttribute("data-vireglass-signature", signature);
1088
+ if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
1089
+ else defs.appendChild(next);
1090
+ bump("filterSwaps");
1091
+ filterEl = next;
1092
+ }
935
1093
  mapKey = key;
936
1094
  }
937
1095
  el.style.setProperty("backdrop-filter", `url(#${id})`);
@@ -955,7 +1113,7 @@ function attachGlass(el, opts = {}) {
955
1113
  const pole = withPresence(resolveBody(opticsNow, sample, light ? 1 : 0), opticsNow).tintLuma;
956
1114
  el.style.setProperty(
957
1115
  "--vireglass-rim",
958
- (opts.accessibility ?? a11y).increaseContrast ? contrastRimCss(pole, contrastRimLuma(pole)) : rimGradientCss(opticsNow, [tr, tg, tb], opts.light ?? REST_LIGHT, appear)
1116
+ (opts.accessibility ?? a11y).increaseContrast ? contrastRimCss(pole, contrastRimLuma(pole), appear) : rimGradientCss(opticsNow, [tr, tg, tb], opts.light ?? REST_LIGHT, appear)
959
1117
  );
960
1118
  el.style.setProperty("--vireglass-rim-width", `${RIM_WIDTH_PX}px`);
961
1119
  el.style.setProperty("--vireglass-radius", `${geometry.cornerRadius}px`);
@@ -978,7 +1136,9 @@ function attachGlass(el, opts = {}) {
978
1136
  const base = optics();
979
1137
  const rise = raiseIntoGlass(sampleOut.press);
980
1138
  const risen = optics({ ...opts.material, roughness: (opts.material?.roughness ?? VIREGLASS_MATERIAL.roughness) * rise.solid });
981
- const map = buildDisplacementMap(risen, geometry, interacting ? 1 : devicePixelRatio(), {
1139
+ if (interim) keepInterim();
1140
+ bump("reducedBakes");
1141
+ const map = buildDisplacementMap(risen, geometry, 1, {
982
1142
  ...morphState(),
983
1143
  touch: {
984
1144
  x: sampleOut.touchX,
@@ -991,6 +1151,7 @@ function attachGlass(el, opts = {}) {
991
1151
  wavePhase: sampleOut.wavePhase
992
1152
  }
993
1153
  });
1154
+ invalidate();
994
1155
  if (filterEl) {
995
1156
  const feImage = filterEl.querySelector("feImage");
996
1157
  feImage?.setAttribute("href", map.url);
@@ -1013,7 +1174,7 @@ function attachGlass(el, opts = {}) {
1013
1174
  el.style.setProperty("--vireglass-glow-opacity", "0");
1014
1175
  return;
1015
1176
  }
1016
- const [gr, gg, gb] = ambientFrom(supplied ?? probeBackdrop(el));
1177
+ const [gr, gg, gb] = ambientFrom(supplied ?? (interim && lastSample ? lastSample : probe(el)));
1017
1178
  const cx = (x + geometry.width / 2) / geometry.width * 100;
1018
1179
  const cy = (y + geometry.height / 2) / geometry.height * 100;
1019
1180
  const reach = halfMinDp(geometry) * TOUCH_RADIUS_FRACTION;
@@ -1037,7 +1198,9 @@ function attachGlass(el, opts = {}) {
1037
1198
  lastFrameAt = 0;
1038
1199
  el.style.setProperty("--vireglass-glow-opacity", "0");
1039
1200
  opts.onGlow?.(null);
1201
+ settling = !interim;
1040
1202
  apply();
1203
+ if (interim) requestInterimMap();
1041
1204
  return;
1042
1205
  }
1043
1206
  frame = requestAnimationFrame(tick);
@@ -1085,6 +1248,9 @@ function attachGlass(el, opts = {}) {
1085
1248
  }
1086
1249
  function destroy() {
1087
1250
  destroyed = true;
1251
+ invalidate();
1252
+ if (idleTimer !== null) clearTimeout(idleTimer);
1253
+ idleTimer = null;
1088
1254
  stopWatchingA11y();
1089
1255
  if (attached.get(el) === handle) attached.delete(el);
1090
1256
  if (frame !== null) cancelAnimationFrame(frame);
@@ -1126,6 +1292,7 @@ function attachGlass(el, opts = {}) {
1126
1292
  if (next === appear) return;
1127
1293
  appear = next;
1128
1294
  apply();
1295
+ if (interim) requestInterimMap();
1129
1296
  },
1130
1297
  setAccent(next) {
1131
1298
  accent = next ?? void 0;
@@ -1148,9 +1315,23 @@ function attachGlass(el, opts = {}) {
1148
1315
  },
1149
1316
  setMorph(next) {
1150
1317
  morph = next && next.smoothing > 0 ? next : null;
1318
+ if (interim) {
1319
+ apply();
1320
+ requestInterimMap();
1321
+ return;
1322
+ }
1151
1323
  mapKey = "";
1152
1324
  apply();
1153
1325
  },
1326
+ setInterim(on) {
1327
+ if (destroyed || on === interim) return;
1328
+ if (on) {
1329
+ interim = true;
1330
+ keepInterim();
1331
+ return;
1332
+ }
1333
+ leaveInterim();
1334
+ },
1154
1335
  setSample(next) {
1155
1336
  supplied = next ?? void 0;
1156
1337
  apply();
@@ -1161,6 +1342,6 @@ function attachGlass(el, opts = {}) {
1161
1342
  return handle;
1162
1343
  }
1163
1344
 
1164
- export { RIM_WIDTH_PX, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
1345
+ export { RIM_WIDTH_PX, ambientLuma, attachGlass, attachScrollEdge, bodyLayer, boxShadowCss, buildDisplacementMap, buildSpectralMap, cached, clearMapCache, compositeLayers, contrastRimCss, createGlassGroup, hasSpectralEdge, lightConicAngle, mapCacheSize, paintFor, parseColor, probeBackdrop, renderDisplacementPixels, renderSpectralPixels, renderStats, resetRenderStats, resetSupportCache, resolveBody, rimGradientCss, rimLineAlpha, rimLobe, rimStrength, shadowAlphaFrom, statsFromSamples, supportsBackdropFilter, supportsSvgBackdropFilter, systemAccessibility, watchAccessibility, withPresence };
1165
1346
  //# sourceMappingURL=dom.js.map
1166
1347
  //# sourceMappingURL=dom.js.map