vireglass 0.1.0 → 2.0.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.
Files changed (97) hide show
  1. package/README.md +83 -9
  2. package/dist/adapters-BfeAopGO.d.ts +154 -0
  3. package/dist/adapters-DugC6Qri.d.cts +154 -0
  4. package/dist/chunk-3PGYF2NK.js +360 -0
  5. package/dist/chunk-3PGYF2NK.js.map +1 -0
  6. package/dist/{chunk-HF2AWZEZ.js → chunk-IQOBZGCF.js} +210 -606
  7. package/dist/chunk-IQOBZGCF.js.map +1 -0
  8. package/dist/chunk-JF3FK254.js +377 -0
  9. package/dist/chunk-JF3FK254.js.map +1 -0
  10. package/dist/chunk-P2ZOODXD.js +58 -0
  11. package/dist/chunk-P2ZOODXD.js.map +1 -0
  12. package/dist/chunk-TTOKBARR.js +456 -0
  13. package/dist/chunk-TTOKBARR.js.map +1 -0
  14. package/dist/dom.cjs +2055 -0
  15. package/dist/dom.cjs.map +1 -0
  16. package/dist/dom.d.cts +347 -0
  17. package/dist/dom.d.ts +347 -0
  18. package/dist/dom.js +1082 -0
  19. package/dist/dom.js.map +1 -0
  20. package/dist/geometry-GvXtt-_S.d.ts +105 -0
  21. package/dist/geometry-RhKd7CM5.d.cts +105 -0
  22. package/dist/group-model-8u4GKrUu.d.cts +194 -0
  23. package/dist/group-model-vYSmEVia.d.ts +194 -0
  24. package/dist/index.cjs +737 -244
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +122 -113
  27. package/dist/index.d.ts +122 -113
  28. package/dist/index.js +6 -277
  29. package/dist/index.js.map +1 -1
  30. package/dist/law.cjs +478 -0
  31. package/dist/law.cjs.map +1 -0
  32. package/dist/law.d.cts +543 -0
  33. package/dist/law.d.ts +543 -0
  34. package/dist/law.js +3 -0
  35. package/dist/law.js.map +1 -0
  36. package/dist/{material-CkUHUBoy.d.cts → material-CnUmdKg1.d.cts} +1 -1
  37. package/dist/{material-CkUHUBoy.d.ts → material-CnUmdKg1.d.ts} +1 -1
  38. package/dist/react.d.cts +1 -1
  39. package/dist/react.d.ts +1 -1
  40. package/dist/reference.cjs +221 -0
  41. package/dist/reference.cjs.map +1 -0
  42. package/dist/reference.d.cts +183 -0
  43. package/dist/reference.d.ts +183 -0
  44. package/dist/reference.js +197 -0
  45. package/dist/reference.js.map +1 -0
  46. package/dist/web/index.cjs +247 -165
  47. package/dist/web/index.cjs.map +1 -1
  48. package/dist/web/index.d.cts +4 -29
  49. package/dist/web/index.d.ts +4 -29
  50. package/dist/web/index.js +5 -90
  51. package/dist/web/index.js.map +1 -1
  52. package/docs/adr-001-rendering.md +288 -0
  53. package/docs/architecture.md +161 -0
  54. package/docs/benchmarks.md +191 -0
  55. package/docs/dom.md +160 -0
  56. package/docs/law.md +65 -0
  57. package/docs/material-lab.md +799 -0
  58. package/docs/platform-parity.md +266 -0
  59. package/docs/reference.md +397 -0
  60. package/package.json +24 -3
  61. package/src/accent.ts +35 -0
  62. package/src/accessibility.ts +30 -16
  63. package/src/adapters.ts +71 -3
  64. package/src/appear.ts +45 -0
  65. package/src/concentric.ts +66 -0
  66. package/src/dom/body.ts +98 -0
  67. package/src/dom/displacement.ts +163 -0
  68. package/src/dom/group.ts +124 -0
  69. package/src/dom/index.ts +739 -0
  70. package/src/dom/map-cache.ts +37 -0
  71. package/src/dom/nesting.ts +32 -0
  72. package/src/dom/preferences.ts +53 -0
  73. package/src/dom/probe.ts +163 -0
  74. package/src/dom/rim.ts +103 -0
  75. package/src/dom/scroll-edge.ts +106 -0
  76. package/src/dom/shadow.ts +56 -0
  77. package/src/dom/spectral-map.ts +102 -0
  78. package/src/dom/support.ts +47 -0
  79. package/src/geometry.ts +41 -14
  80. package/src/glass-scale.ts +3 -5
  81. package/src/index.ts +3 -1
  82. package/src/law.ts +562 -0
  83. package/src/lens-shader.ts +53 -30
  84. package/src/native/glass-surface.tsx +8 -14
  85. package/src/native/provider.tsx +22 -2
  86. package/src/optics.ts +43 -62
  87. package/src/reference.ts +9 -0
  88. package/src/scroll-edge.ts +22 -7
  89. package/src/sdf.ts +195 -8
  90. package/src/shadow-tint.ts +23 -0
  91. package/src/spectral.ts +110 -0
  92. package/src/surface-shader.ts +8 -6
  93. package/src/touch-response.ts +42 -27
  94. package/src/web/index.ts +0 -1
  95. package/dist/adapters-BPB0D5nr.d.cts +0 -357
  96. package/dist/adapters-BgSc-smX.d.ts +0 -357
  97. package/dist/chunk-HF2AWZEZ.js.map +0 -1
package/dist/dom.cjs ADDED
@@ -0,0 +1,2055 @@
1
+ 'use strict';
2
+
3
+ // src/adaptation.ts
4
+ var AMBIENT_STEP = 32;
5
+ function ambientFrom(sample) {
6
+ const step = (v) => Math.round(Math.min(Math.max(v, 0), 1) * AMBIENT_STEP) / AMBIENT_STEP;
7
+ return [step(sample.r), step(sample.g), step(sample.b)];
8
+ }
9
+ var INK_LIGHT = 0.95;
10
+ var INK_DARK = 0.08;
11
+ function shouldInkBeLight(sample, wasLight) {
12
+ return decisiveLuma(sample) < (wasLight ? FLIP_LUMA : RETURN_LUMA);
13
+ }
14
+ var decisiveLuma = (sample) => sample.luma * 0.75 + (sample.hi ?? sample.luma) * 0.25;
15
+ var FLIP_LUMA = 0.62;
16
+ var RETURN_LUMA = 0.5;
17
+ var CONFIRMATIONS = 3;
18
+
19
+ // src/law.ts
20
+ var RIM = {
21
+ /**
22
+ * The key-light lobe's exponent. Two opposing arcs come out of one expression, which is why
23
+ * they are opposed rather than placed: `pow(max(facing, 0), n) + 0.45 * pow(max(-facing, 0), n)`.
24
+ *
25
+ * MEASURED off two close-ups of real end caps, frames/crops/cap158-left and cap158-right. The
26
+ * bright arc's full width at half its peak is 53 degrees in the first and 30 in the second; this
27
+ * exponent's own width is `2·acos(0.5^(1/n))`, so they imply 6 and 20. It was 3, whose arc is 75
28
+ * degrees wide — broader than either measurement, and the error is in the same direction in both.
29
+ *
30
+ * 6 is the conservative end: it reproduces the wider crop exactly, and a broader arc is the more
31
+ * forgiving error. One exponent cannot serve both, and that is not noise — a specular lobe's
32
+ * width depends on the light's angular size as well as the surface, and those are two different
33
+ * scenes. The model has no term for the light's size.
34
+ */
35
+ lobeExponent: 6,
36
+ /** The far arc, weaker than the one facing the light (§2). */
37
+ opposingArc: 0.45,
38
+ /**
39
+ * The dark edge along the whole silhouette, as a multiplicative darkening. iOS 27 made it a
40
+ * layer of its own, and it does not rule out the highlight sitting on top of it (§2).
41
+ */
42
+ darkEdge: 0.22,
43
+ /** Width of the hairline, CSS px. "About a point, not a band and not a bevel" (§2). */
44
+ widthPx: 1
45
+ };
46
+ var BODY = {
47
+ /** Lightness ceiling for the body when legibility is loose. */
48
+ capLoose: 0.62,
49
+ /** …and when it is strict. The model is calibrated at these two, not in between (E-43). */
50
+ capTight: 0.38,
51
+ /** The two ends the body tints toward. Glass has no colour of its own (HIG "Color"). */
52
+ tintDark: 0.07,
53
+ tintLight: 0.94,
54
+ /** How far the backdrop's spread reaches past its mean when deciding what the ink needs. */
55
+ busyEdge: 0.75,
56
+ /** Fine texture the body has to stop being a window for. */
57
+ groundSpan: 0.06,
58
+ /** Where the tint flips direction, by the element's AVERAGE lightness — a local threshold cuts
59
+ * a diagonal step across the body on a gradient. */
60
+ darkSideFrom: 0.42,
61
+ darkSideTo: 0.58,
62
+ /** Ceiling on any single demand, so one requirement cannot make the glass opaque on its own. */
63
+ maxDemand: 0.92
64
+ };
65
+ var SHADOW = {
66
+ /**
67
+ * Measured off reference frames 711–723: 4.0% over a flat light backdrop, 19.9% over text.
68
+ *
69
+ * REPRODUCED independently off frames/verify/m-707 and m-711 — the same capsule in the same scene
70
+ * at the same scale, once below the text over plain background and once scrolled up over it.
71
+ * Comparing the rows under the capsule against the same rows far to either side, so the
72
+ * background's own gradient cancels: 3.5% and 19.2%, peaking about 140 px below the capsule.
73
+ * Two passes, different frames and a different method, agreeing to within 0.7 of a point.
74
+ */
75
+ alphaFlat: 0.04,
76
+ alphaBusy: 0.199
77
+ };
78
+ var DISPERSION = {
79
+ /** Red bends less than the base index, blue more — and not symmetrically. */
80
+ redShift: -0.4,
81
+ blueShift: 0.6
82
+ };
83
+ var TOUCH = {
84
+ /**
85
+ * The element grows under pressure (M 3:51; HIG: interactive "expands").
86
+ *
87
+ * CHECKED, not measured: across frames/knob/switch-press, a 4x5 sheet of one switch being
88
+ * pressed, the control's extent runs 764 px in the three settled frames and 787 to 822 under the
89
+ * finger — 3 to 8 per cent. This value sits inside that, which is a consistency check and not a
90
+ * derivation: the measurement cannot separate the knob stretching from the track growing, and the
91
+ * settled frames agreeing to the pixel is the only part of it that is tight.
92
+ */
93
+ pressGrow: 0.06,
94
+ /** The ridge around the contact blob: material displaced from under the finger ends up
95
+ * somewhere. Without it the shape balloons, and a dense medium does not do that. */
96
+ ridge: 0.42,
97
+ /** How far press pulls the field toward the finger. */
98
+ pressPull: 0.16,
99
+ /** Wavelength and decay of the ripple, as fractions of its span. Short and quick: in a dense
100
+ * medium ripples are frequent and small; long shallow swells are water. */
101
+ waveLength: 0.17,
102
+ waveDecay: 0.7,
103
+ /** The ripple's span, as a multiple of the contact radius. It has to reach the far edge, or it
104
+ * reads as jitter under the finger rather than a wave across the surface. */
105
+ waveSpan: 2,
106
+ /**
107
+ * Contact radius as a fraction of the element's half-size. UNMEASURED: §5 says the response has
108
+ * to be visible UNDER the finger, which is why the blob is wide, but gives no figure.
109
+ */
110
+ radiusOfHalfSize: 1.1,
111
+ /**
112
+ * Wave impulse as a fraction of the travel limit. UNMEASURED: the core carries the ripple's
113
+ * decay and frequency, but nothing says how hard a finger strikes it.
114
+ */
115
+ waveOfTravel: 0.18,
116
+ /** The lift-off ring is weaker than the one from touching down (§5). UNMEASURED ratio. */
117
+ releaseWave: 0.6,
118
+ /**
119
+ * How far a glow reaches past the element it started on, as a multiple of that element's
120
+ * half-size. UNMEASURED: §5 says the glow spreads "onto any Liquid Glass elements nearby" and
121
+ * names no distance. This is the reach at which a neighbour in a tab bar still lights and one
122
+ * across the screen does not.
123
+ */
124
+ glowReach: 3};
125
+ var BEVEL = {
126
+ sphere: 0.94,
127
+ floor: 0.02,
128
+ slopeMax: 3.2
129
+ };
130
+ var EPSILON = 1e-5;
131
+ var LENS = {
132
+ /** Same reason, for the profile's slope. */
133
+ slopeMax: 40,
134
+ /** Every thin film on glass sits around this index. */
135
+ filmIor: 1.35,
136
+ /** Reference channel wavelengths in nm — red, green, blue — for diffraction and interference. */
137
+ lambdaR: 610,
138
+ lambdaG: 550,
139
+ lambdaB: 460
140
+ };
141
+ var SHADOW_GAP = {
142
+ /** Fraction of the ambient hue the shadow carries. It has to stay a shadow, not a coloured blob. */
143
+ tint: 1
144
+ };
145
+ var SPECTRAL = {
146
+ /** How far into the bevel the fringes reach, as a fraction of it measured from the silhouette.
147
+ * Diffraction is an EDGE effect; past this the bevel is plain glass. */
148
+ diffractionOnset: 0.45,
149
+ /**
150
+ * The same window for interference, which had none. §2 puts the iridescence on the opposing ARC —
151
+ * "the opposing arc carries an iridescent fringe" — and a fringe is a thing at an edge.
152
+ *
153
+ * Without it the term applies wherever the surface angle puts it, and across the inner bevel the
154
+ * angle barely changes, so a film that should read as a fringe reads as a flat colour filter.
155
+ * Measured on a 680x64 bar with the default preset: a multiplier of 1.68, 1.26, 0.05 held CONSTANT
156
+ * from a third of the way through the bevel to its inner edge — red boosted by two thirds and blue
157
+ * removed outright. UNMEASURED window; wider than diffraction's because interference reaches
158
+ * further in than the fringes do.
159
+ */
160
+ iridescenceOnset: 0.25,
161
+ /** Fringes across the bevel. Denser as the bevel sharpens, which is what the phase term does. */
162
+ diffractionFringes: 4,
163
+ /** The channel the hue is measured against, nm. Green is the eye's own reference. */
164
+ referenceLambda: 550
165
+ };
166
+ var ACCENT = {
167
+ /** The tone the colour takes over a dark backdrop and over a light one. "A range of tones mapped
168
+ * to content brightness underneath", rather than one flat colour. */
169
+ deep: 0.88,
170
+ light: 1.08,
171
+ /** How much of the medium the colour occupies by default. Short of one, because the content has
172
+ * to keep coming through: a fill "breaks the visual character of Liquid Glass" (@17:03). */
173
+ amount: 0.8
174
+ };
175
+ var ACCESSIBILITY = {
176
+ /** Frost floor under reduced transparency: the glass gets "frostier" (§9). */
177
+ frostMinDp: 14,
178
+ /**
179
+ * Body floor under reduced transparency. B @10:07 is the stricter statement of the two — a bar
180
+ * "gets a background when reduce transparency is enabled" — so this is a background, not a
181
+ * thicker haze. UNMEASURED: neither source gives a figure.
182
+ */
183
+ obscureMin: 0.82,
184
+ /** Under increased contrast the element is "predominantly black or white" (§9): the body goes to
185
+ * whichever pole its ink already points at, at a density that leaves it reading as that pole. */
186
+ contrastDensity: 0.94,
187
+ /** …and separates from the backdrop on its own, whatever is behind it. */
188
+ contrastPresence: 0.5,
189
+ /**
190
+ * The contrasting border §9 asks for, in units of lightness away from the body's own pole. The
191
+ * rim stops taking its colour from the environment and takes it from the element instead — an
192
+ * ambient hairline is not a contrasting border. UNMEASURED.
193
+ */
194
+ contrastRim: 0.85,
195
+ /**
196
+ * Press that survives reduced motion. §9 "decreases the intensity of some effects and disables
197
+ * any elastic properties": the spring and the ripple are elastic and go to zero, the press is an
198
+ * ease that carries the element's light, so it is damped rather than removed — an element that
199
+ * stops answering a finger altogether is not reduced motion, it is no feedback. UNMEASURED.
200
+ */
201
+ stillPress: 0.6
202
+ };
203
+ var DERIVE = {
204
+ /** Physical 4% reflectance into the render's working range. UNMEASURED. */
205
+ fresnelGain: 17.5,
206
+ /** Schlick's exponent. A constant of the approximation, not a knob. */
207
+ fresnelExponent: 5,
208
+ /** The index at which ray bending is at full strength, measured from air. UNMEASURED. */
209
+ iorFullBend: 0.6,
210
+ /** Magnification behind a plane-parallel plate, per dp of thickness. UNMEASURED. */
211
+ magnifyPerDp: 6e-3,
212
+ /** In ordinary glasses the Abbe number falls as the index rises, so dispersion grows with it.
213
+ * UNMEASURED slope. */
214
+ dispersionPerIor: 1.1,
215
+ /** Index spread between the red and blue channels. The physical value (~0.01 at an Abbe number
216
+ * of 55) is at the threshold of visibility. UNMEASURED. */
217
+ iorSpreadPerDispersion: 0.045,
218
+ /** Ceiling on how much denser the bevel may read than the body. The rim's ray travels further
219
+ * through the medium; past this the rim is milk rather than glass. UNMEASURED. */
220
+ edgeDensityMax: 4,
221
+ /**
222
+ * Frost ceiling, dp. Deliberately low: blur is a SUPPORTING device here, and past about a dozen
223
+ * dp letters under the glass stop being letters. Separating content from ink is the body's job.
224
+ *
225
+ * UNMEASURED, and an attempt is recorded here so the next one does not repeat it.
226
+ * frames/verify/m-711 has the same paragraph sharp beside the capsule and behind it, which looks
227
+ * like the measurement: match the blur that takes the sharp patch's detail down to the glassed
228
+ * one's. It gives 2 to 3 px at that frame's scale, and the figure is not trustworthy. Detail is
229
+ * lost to three things at once there — the frost, the magnification (`refractionScale` lowers the
230
+ * text's spatial frequency without blurring anything), and the body veiling its contrast — and
231
+ * the method cannot separate them. What it does bound is the total: the text behind the glass
232
+ * keeps 68% of the detail it has beside it.
233
+ *
234
+ * A clean measurement needs a frame where the glass does not magnify, or a feature of known size
235
+ * behind it to normalise the magnification out.
236
+ */
237
+ blurMaxDp: 12,
238
+ /** Highlight width from a smooth surface and from a fully rough one, as a specular exponent.
239
+ * UNMEASURED. */
240
+ specularPowerSmooth: 160,
241
+ specularPowerRough: 8,
242
+ /** How much roughness damps the highlight's brightness. UNMEASURED. */
243
+ specularRoughDamping: 0.6,
244
+ /**
245
+ * The medium's own hue by density. Water absorbs red and skews cool, ordinary glass is nearly
246
+ * neutral with a faint green, dense high-index media skew warm. Glass has no colour of its own
247
+ * (HIG "Color"), so this is never set by hand — it is tied to the index.
248
+ */
249
+ hueCool: [0.86, 1, 1.08],
250
+ hueNeutral: [1, 1.02, 0.99],
251
+ hueWarm: [1.08, 1, 0.88],
252
+ /** The index range the hue sweeps across, and where it starts. UNMEASURED. */
253
+ hueFromIor: 1.2,
254
+ hueSpan: 0.55,
255
+ /** Body lightness follows reflectance: the denser the medium, the more of its surroundings it
256
+ * returns. UNMEASURED base and slope. */
257
+ bodyLiftBase: 0.34,
258
+ bodyLiftPerF0: 2.4,
259
+ /**
260
+ * How far past the bevel the rim gathers its surroundings, as a multiple of the bevel. At a
261
+ * grazing angle the eye receives the vicinity rather than what is under the glass, and without
262
+ * this, glass on an empty black background has no source at all. UNMEASURED.
263
+ */
264
+ gatherPerBevel: 4,
265
+ /** Floor and ceiling on that radius, dp. Unbounded, a wide bevel gathers from fifty dp away and
266
+ * a READABLE copy of the neighbourhood appears inside the glass — eight samples cannot blur a
267
+ * disc that large (E-33, E-37). */
268
+ gatherMinDp: 4,
269
+ gatherMaxDp: 28,
270
+ /** The body's own density from absorption through its thickness. Small: the body only moves
271
+ * toward the tint under the legibility requirement, otherwise it dims the content. UNMEASURED. */
272
+ bodyDensity: 0.05,
273
+ /**
274
+ * Rim light on the physical scale rather than the rim highlight's. `fresnelGain` saturates at one
275
+ * for dense media, and applying it here too would raise body lightness harder than the glass had
276
+ * just separated it from the ink — eating its own legibility work. UNMEASURED.
277
+ */
278
+ edgeLightGain: 6,
279
+ /**
280
+ * Iridescence rides the REFLECTED ray, so it is tied to Fresnel and has no knob of its own.
281
+ *
282
+ * MEASURED against how coloured a real rim is. In frames/crops the brightest rim pixels have a
283
+ * channel spread — (max − min) / max — of 13% in cap158-left and 20% in cap158-right, against
284
+ * their own backdrops' 14.6% and 14.3%. So the rim is no more coloured than the scene it stands
285
+ * in: §2's "iridescent fringe" adds a few points at most, and in one of the two crops it adds
286
+ * nothing measurable.
287
+ *
288
+ * At 1.6 this put ordinary glass at 0.945 iridescence and the spectral multiplier reached a 97%
289
+ * channel spread — red boosted by two thirds and blue removed. Rendered over a coloured page that
290
+ * is a neon tube around every control, which is what sent me to measure it.
291
+ */
292
+ iridescencePerFresnel: 0.042,
293
+ /** Edge diffraction shares dispersion's λ-dependence, at the stylised amplitude: at the physical
294
+ * one the fringes are not visible at all. MEASURED with the same crops and the same target as
295
+ * iridescence — on its own it was reaching a 93% channel spread. */
296
+ diffractionPerDispersion: 0.05,
297
+ /** Multiple internal reflection — the denser the medium, the more light it circulates and the
298
+ * more it is tinted by its surroundings. UNMEASURED slope. */
299
+ colorPickupPerFresnel: 0.9,
300
+ /**
301
+ * The ceiling, which keeps it a medium rather than a fill.
302
+ *
303
+ * MEASURED, frames/concentric/shadow-zoom: the same sidebar twice, over a yellow cover and over a
304
+ * pink one, so the difference between the panels is the pickup with the sidebar's own colour
305
+ * subtracted out. Sampling a band of rows clear of the text, the bar's hue offset against the
306
+ * cover's own reaches 0.31 of it at the edge nearest the content in the yellow panel and 0.28 in
307
+ * the pink one. It was 0.42, which nothing measured.
308
+ *
309
+ * Reality is not uniform: the bleed falls to nothing about ninety pixels in, and both panels show
310
+ * the same falloff. The DOM path has one backdrop sample per element and cannot reproduce that,
311
+ * so it applies the ceiling across the whole body — right at the edge, too strong in the middle.
312
+ * That limit is in docs/dom.md rather than papered over with a lower number.
313
+ */
314
+ colorPickupMax: 0.3
315
+ };
316
+ var SIZE = {
317
+ /** Half-size at which bevel and thickness are specified as given; they grow as its square root
318
+ * from there. UNMEASURED reference size. */
319
+ referenceDp: 24,
320
+ /** Floor and ceiling on that growth. UNMEASURED. */
321
+ gainMin: 0.8,
322
+ gainMax: 2.4,
323
+ /** A bevel wider than this fraction of the half-size breaks the SDF — the roundings converge in
324
+ * the middle. A property of the geometry, not a taste. */
325
+ maxBevelFraction: 0.65
326
+ };
327
+ var SHADOW_DENSITY = {
328
+ /** Over a flat background. */
329
+ flat: 0.8,
330
+ /** How fast it rises with structure behind the element, and how far it may rise. */
331
+ busyGain: 6,
332
+ busyRise: 1.2,
333
+ /** Quantisation of the emitted value: a shadow that changes on every pixel of scroll re-lays-out
334
+ * the native view every frame. */
335
+ step: 0.05
336
+ };
337
+ var SPRING = {
338
+ /** Stiffness and damping while the finger is down. UNMEASURED. */
339
+ holdStiffness: 260,
340
+ holdDamping: 46,
341
+ /** …and after it lifts. Stiffer, and damped hard: a dense medium snaps back without ringing. */
342
+ releaseStiffness: 420,
343
+ releaseDamping: 34,
344
+ /** Time constants for the press dent, in and out, seconds. UNMEASURED. */
345
+ pressAttack: 0.07,
346
+ pressRelease: 0.16,
347
+ /** How fast the touch point catches up to the finger. Not instant: an instant jump tears the
348
+ * deformation, and on rapid taps that reads as jitter. */
349
+ pointFollow: 0.045,
350
+ /** …and how fast the element registers as being touched at all, which is what the glow rides. */
351
+ activeFollow: 0.09,
352
+ /** The ripple decays within a quarter second rather than oscillating: in a viscous medium ripples
353
+ * are short and weak. UNMEASURED. */
354
+ waveDecay: 0.22,
355
+ waveTurnsPerSecond: 3,
356
+ /** Ceilings on the accumulated ripple, as multiples of a single impulse. Impulses add up rather
357
+ * than restart — restarting cuts a running wave mid-period, which is what jolts on rapid taps. */
358
+ waveCapOnGrab: 1.6,
359
+ waveCapOnRelease: 2
360
+ };
361
+ var SCALE = {
362
+ /** Where on the scale the material stays exactly as it is by default. */
363
+ default: 0.35,
364
+ /** Body density at the fully tinted end: content under the glass has to be hidden. UNMEASURED. */
365
+ tintedDensity: 0.9
366
+ };
367
+ var SCROLL_EDGE = {
368
+ /** How far the content has to slide under the panel for the effect to fully engage, dp. */
369
+ engageDp: 24,
370
+ /** The blur the content dissolves into at the edge, dp. This is the effect itself — the content
371
+ * goes out of focus into the background rather than being covered by anything. UNMEASURED. */
372
+ dissolveBlurDp: 10,
373
+ /**
374
+ * Over dark content the glass turns dark and 219 @9:33 switches the effect "to apply a subtle
375
+ * dimming instead". Dimming is a reduction in luminance: dark content under dark glass merges
376
+ * with it, and pushing the content down is what lets the glass sit above it. UNMEASURED.
377
+ */
378
+ dimAlpha: 0.1,
379
+ /** A pinned view under the panel (column headers) gets a flat band instead of a gradual fade
380
+ * (219 @9:41) — "a stronger, more opaque boundary" (356 @12:12). UNMEASURED. */
381
+ hardBlurDp: 14,
382
+ hardAlpha: 0.18
383
+ };
384
+
385
+ // src/geometry.ts
386
+ var roundedRectGeometry = (width, height, cornerRadius) => ({ width, height, cornerRadius });
387
+ var halfMinDp = (g) => Math.max(Math.min(g.width, g.height) / 2, 1);
388
+ var MAX_STRETCH = 0.34;
389
+ var sizeDp = (g) => Math.sqrt(halfMinDp(g) * Math.max(Math.sqrt(g.width * g.height) / 2, 1));
390
+ var sizeGain = (g) => {
391
+ const raw = Math.sqrt(sizeDp(g) / SIZE.referenceDp);
392
+ if (raw <= SIZE.gainMin) return SIZE.gainMin;
393
+ const span = SIZE.gainMax - SIZE.gainMin;
394
+ return SIZE.gainMin + span * (1 - Math.exp(-(raw - SIZE.gainMin) / span));
395
+ };
396
+ var MAX_BEVEL_FRACTION = SIZE.maxBevelFraction;
397
+ var bevelFraction = (g, o) => Math.min(MAX_BEVEL_FRACTION, o.bevelDp * sizeGain(g) / halfMinDp(g));
398
+ var bevelDp = (g, o) => Math.max(bevelFraction(g, o) * halfMinDp(g), 1);
399
+ var thicknessDp = (g, o) => o.thicknessDp * sizeGain(g);
400
+ var RIM_FRACTION = 0.85;
401
+ var rimDp = (g, o) => thicknessDp(g, o) * RIM_FRACTION;
402
+ var shadowReachDp = (g) => Math.min(Math.max(halfMinDp(g) * 0.65, 6), 36);
403
+ function shadowOpacity(busy) {
404
+ return SHADOW_DENSITY.flat + Math.min(Math.max(busy, 0) * SHADOW_DENSITY.busyGain, SHADOW_DENSITY.busyRise);
405
+ }
406
+ function shadowOpacityFrom(sample) {
407
+ return Math.round(shadowOpacity(sample.busy) / SHADOW_DENSITY.step) * SHADOW_DENSITY.step;
408
+ }
409
+
410
+ // src/sdf.ts
411
+ var VG_TAU = 6.28318530718;
412
+ function sdfRoundedRect(x, y, w, h, r) {
413
+ const qx = Math.abs(x) - w / 2 + r;
414
+ const qy = Math.abs(y) - h / 2 + r;
415
+ return Math.min(Math.max(qx, qy), 0) + Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) - r;
416
+ }
417
+ function sdfRoundedRectGradient(x, y, w, h, r) {
418
+ const qx = Math.abs(x) - w / 2 + r;
419
+ const qy = Math.abs(y) - h / 2 + r;
420
+ let gx;
421
+ let gy;
422
+ if (qx > 0 && qy > 0) {
423
+ const cx = Math.max(qx, 1e-4);
424
+ const cy = Math.max(qy, 1e-4);
425
+ const len = Math.hypot(cx, cy);
426
+ gx = cx / len;
427
+ gy = cy / len;
428
+ } else if (qx > qy) {
429
+ gx = 1;
430
+ gy = 0;
431
+ } else {
432
+ gx = 0;
433
+ gy = 1;
434
+ }
435
+ return [gx * Math.sign(x), gy * Math.sign(y)];
436
+ }
437
+ function smin(a, b, k) {
438
+ const h = Math.min(Math.max(0.5 + 0.5 * (b - a) / k, 0), 1);
439
+ return b + (a - b) * h - k * h * (1 - h);
440
+ }
441
+ function sceneDistance(x, y, w, h, r, smoothing = 0, b, c) {
442
+ const a = sdfRoundedRect(x, y, w, h, r);
443
+ if (smoothing <= 0 || !b || b.width <= 0) return a;
444
+ const db = sdfRoundedRect(x - b.offsetX, y - b.offsetY, b.width, b.height, b.cornerRadius);
445
+ const ab = smin(a, db, smoothing);
446
+ if (!c || c.width <= 0) return ab;
447
+ const dc = sdfRoundedRect(x - c.offsetX, y - c.offsetY, c.width, c.height, c.cornerRadius);
448
+ return smin(ab, dc, smoothing);
449
+ }
450
+ function sceneGradient(x, y, w, h, r, smoothing = 0, b, c) {
451
+ const na = sdfRoundedRectGradient(x, y, w, h, r);
452
+ if (smoothing <= 0 || !b || b.width <= 0) return na;
453
+ const bx = x - b.offsetX;
454
+ const by = y - b.offsetY;
455
+ const da = sdfRoundedRect(x, y, w, h, r);
456
+ const db = sdfRoundedRect(bx, by, b.width, b.height, b.cornerRadius);
457
+ const hb = Math.min(Math.max(0.5 + 0.5 * (db - da) / smoothing, 0), 1);
458
+ const nb = sdfRoundedRectGradient(bx, by, b.width, b.height, b.cornerRadius);
459
+ let gx = nb[0] + (na[0] - nb[0]) * hb;
460
+ let gy = nb[1] + (na[1] - nb[1]) * hb;
461
+ if (c && c.width > 0) {
462
+ const cx = x - c.offsetX;
463
+ const cy = y - c.offsetY;
464
+ const dab = smin(da, db, smoothing);
465
+ const dc = sdfRoundedRect(cx, cy, c.width, c.height, c.cornerRadius);
466
+ const hc = Math.min(Math.max(0.5 + 0.5 * (dc - dab) / smoothing, 0), 1);
467
+ const nc = sdfRoundedRectGradient(cx, cy, c.width, c.height, c.cornerRadius);
468
+ gx = nc[0] + (gx - nc[0]) * hc;
469
+ gy = nc[1] + (gy - nc[1]) * hc;
470
+ }
471
+ gx += EPSILON;
472
+ gy += EPSILON;
473
+ const len = Math.hypot(gx, gy) || 1;
474
+ return [gx / len, gy / len];
475
+ }
476
+ function touchWarp(x, y, t) {
477
+ if (t.radius <= 0) return [x, y];
478
+ const grow = 1 + TOUCH.pressGrow * t.press;
479
+ let px = x / grow;
480
+ let py = y / grow;
481
+ const dx = px - t.x;
482
+ const dy = py - t.y;
483
+ const r = Math.hypot(dx, dy);
484
+ const k = Math.min(Math.max((t.radius - r) / t.radius, 0), 1);
485
+ const s = k * k * (3 - 2 * k);
486
+ const core = s * s * (3 - 2 * s);
487
+ const rim = k * k * (1 - k) * 4;
488
+ px -= t.pullX * (core - TOUCH.ridge * rim);
489
+ py -= t.pullY * (core - TOUCH.ridge * rim);
490
+ px -= dx * (t.press * TOUCH.pressPull * core);
491
+ py -= dy * (t.press * TOUCH.pressPull * core);
492
+ if (t.waveAmp > 0 && r > 1e-4) {
493
+ const span = t.radius * TOUCH.waveSpan;
494
+ const ring = Math.sin(r / (span * TOUCH.waveLength) - t.wavePhase * VG_TAU) * Math.exp(-r / (span * TOUCH.waveDecay));
495
+ px -= dx / r * ring * t.waveAmp;
496
+ py -= dy / r * ring * t.waveAmp;
497
+ }
498
+ return [px, py];
499
+ }
500
+
501
+ // src/dom/displacement.ts
502
+ var SCALE_GAIN = 0.75;
503
+ var MAX_STEP_PX = 0.09;
504
+ var MAX_SCALE = 255 * MAX_STEP_PX;
505
+ var BEVEL_SAMPLES = 8;
506
+ function mapDensity(geometry, bevel, dpr) {
507
+ if (bevel <= 0) return dpr;
508
+ return Math.min(dpr, BEVEL_SAMPLES / bevel);
509
+ }
510
+ function encode(displacement, scale) {
511
+ const channel = Math.round(255 * (displacement / scale + 0.5));
512
+ return channel < 0 ? 0 : channel > 255 ? 255 : channel;
513
+ }
514
+ function bevelProfile(t) {
515
+ const raw = Math.min(t / Math.sqrt(Math.max(1 - t * t * BEVEL.sphere, BEVEL.floor)), BEVEL.slopeMax);
516
+ return raw / BEVEL.slopeMax;
517
+ }
518
+ function renderDisplacementPixels(optics, geometry, dpr, state = {}) {
519
+ const bevel = bevelDp(geometry, optics);
520
+ const density = mapDensity(geometry, bevel, dpr);
521
+ const width = Math.max(1, Math.round(geometry.width * density));
522
+ const height = Math.max(1, Math.round(geometry.height * density));
523
+ const rawScale = optics.refraction * thicknessDp(geometry, optics) * SCALE_GAIN;
524
+ const scale = Math.min(rawScale, MAX_SCALE);
525
+ const smoothing = state.smoothing ?? 0;
526
+ const data = new Uint8ClampedArray(width * height * 4);
527
+ const halfW = geometry.width / 2;
528
+ const halfH = geometry.height / 2;
529
+ for (let py = 0; py < height; py += 1) {
530
+ const y = (py + 0.5) / density - halfH;
531
+ for (let px = 0; px < width; px += 1) {
532
+ const x = (px + 0.5) / density - halfW;
533
+ const i = (py * width + px) * 4;
534
+ const [wx, wy] = state.touch ? touchWarp(x, y, state.touch) : [x, y];
535
+ const sd = sceneDistance(wx, wy, geometry.width, geometry.height, geometry.cornerRadius, smoothing, state.morph, state.morph2);
536
+ const t = Math.min(Math.max((sd + bevel) / bevel, 0), 1);
537
+ if (t <= 0 || scale <= 0) {
538
+ data[i] = 128;
539
+ data[i + 1] = 128;
540
+ data[i + 2] = 128;
541
+ data[i + 3] = 255;
542
+ continue;
543
+ }
544
+ const [gx, gy] = sceneGradient(wx, wy, geometry.width, geometry.height, geometry.cornerRadius, smoothing, state.morph, state.morph2);
545
+ const magnitude = bevelProfile(t) * (scale / 2);
546
+ const dx = -gx * magnitude;
547
+ const dy = -gy * magnitude;
548
+ data[i] = encode(dx, scale);
549
+ data[i + 1] = encode(dy, scale);
550
+ data[i + 2] = 128;
551
+ data[i + 3] = 255;
552
+ }
553
+ }
554
+ return { data, width, height, scale };
555
+ }
556
+ function hasCanvasSupport() {
557
+ return typeof document !== "undefined" && typeof document.createElement === "function";
558
+ }
559
+ function buildDisplacementMap(optics, geometry, dpr, state = {}) {
560
+ if (!hasCanvasSupport()) {
561
+ throw new Error("vireglass/dom: buildDisplacementMap requires a DOM (canvas)");
562
+ }
563
+ const { data, width, height, scale } = renderDisplacementPixels(optics, geometry, dpr, state);
564
+ const canvas = document.createElement("canvas");
565
+ canvas.width = width;
566
+ canvas.height = height;
567
+ const ctx = canvas.getContext("2d");
568
+ if (!ctx) throw new Error("vireglass/dom: 2D canvas context unavailable");
569
+ const imageData = ctx.createImageData(width, height);
570
+ imageData.data.set(data);
571
+ ctx.putImageData(imageData, 0, 0);
572
+ return { url: canvas.toDataURL("image/png"), width, height, scale };
573
+ }
574
+
575
+ // src/spectral.ts
576
+ var TAU = 6.28318530718;
577
+ var LAMBDA = [LENS.lambdaR, LENS.lambdaG, LENS.lambdaB];
578
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
579
+ function toHue(v) {
580
+ const mean = Math.max((v[0] + v[1] + v[2]) / 3, 1e-3);
581
+ return [v[0] / mean, v[1] / mean, v[2] / mean];
582
+ }
583
+ function rimSlopeCurve(x) {
584
+ const k = 1 - clamp01(x);
585
+ return k * k * k / Math.pow(Math.max(1 - k * k * k * k, 1e-5), 0.75);
586
+ }
587
+ function surfaceSlope(e, bevel, rim, thick) {
588
+ if (e >= bevel || bevel <= 0) return 0;
589
+ return Math.min((thick - rim) / bevel * rimSlopeCurve(e / bevel), LENS.slopeMax);
590
+ }
591
+ function surfaceCosine(slope) {
592
+ return 1 / Math.sqrt(1 + slope * slope);
593
+ }
594
+ function interference(cosI, filmNm) {
595
+ const sinT2 = (1 - cosI * cosI) / (LENS.filmIor * LENS.filmIor);
596
+ const cosT = Math.sqrt(Math.max(1 - sinT2, 0));
597
+ const opd = 2 * LENS.filmIor * filmNm * cosT;
598
+ return toHue([
599
+ 0.5 + 0.5 * Math.cos(TAU * opd / LAMBDA[0] + Math.PI),
600
+ 0.5 + 0.5 * Math.cos(TAU * opd / LAMBDA[1] + Math.PI),
601
+ 0.5 + 0.5 * Math.cos(TAU * opd / LAMBDA[2] + Math.PI)
602
+ ]);
603
+ }
604
+ function diffraction(distFromEdge, bevel) {
605
+ const phase = TAU * SPECTRAL.diffractionFringes * distFromEdge / Math.max(bevel, 1);
606
+ const f = (lambda) => 0.5 + 0.5 * Math.cos(phase * SPECTRAL.referenceLambda / lambda);
607
+ return toHue([f(LAMBDA[0]), f(LAMBDA[1]), f(LAMBDA[2])]);
608
+ }
609
+ function smoothstep(edge0, edge1, x) {
610
+ const t = clamp01((x - edge0) / (edge1 - edge0));
611
+ return t * t * (3 - 2 * t);
612
+ }
613
+ function spectralHue(e, bevel, rim, thick, optics) {
614
+ let r = 1;
615
+ let g = 1;
616
+ let b = 1;
617
+ const t = 1 - clamp01(bevel > 0 ? e / bevel : 1);
618
+ if (optics.iridescence > 1e-3) {
619
+ const w = optics.iridescence * smoothstep(SPECTRAL.iridescenceOnset, 1, t);
620
+ if (w > 0) {
621
+ const cosI = clamp01(surfaceCosine(surfaceSlope(e, bevel, rim, thick)));
622
+ const i = interference(cosI, optics.film);
623
+ r += (i[0] - 1) * w;
624
+ g += (i[1] - 1) * w;
625
+ b += (i[2] - 1) * w;
626
+ }
627
+ }
628
+ if (optics.diffraction > 1e-3) {
629
+ const w = optics.diffraction * smoothstep(SPECTRAL.diffractionOnset, 1, t);
630
+ if (w > 0) {
631
+ const d = diffraction(e, bevel);
632
+ r += (d[0] - 1) * w;
633
+ g += (d[1] - 1) * w;
634
+ b += (d[2] - 1) * w;
635
+ }
636
+ }
637
+ return [r, g, b];
638
+ }
639
+
640
+ // src/dom/spectral-map.ts
641
+ var HEADROOM = 2;
642
+ var BEVEL_SAMPLES2 = 24;
643
+ function mapDensity2(bevel, dpr) {
644
+ if (bevel <= 0) return dpr;
645
+ return Math.min(dpr, BEVEL_SAMPLES2 / bevel);
646
+ }
647
+ function renderSpectralPixels(optics, geometry, dpr) {
648
+ const bevel = bevelDp(geometry, optics);
649
+ const density = mapDensity2(bevel, dpr);
650
+ const width = Math.max(1, Math.round(geometry.width * density));
651
+ const height = Math.max(1, Math.round(geometry.height * density));
652
+ const rim = rimDp(geometry, optics);
653
+ const thick = thicknessDp(geometry, optics);
654
+ const data = new Uint8ClampedArray(width * height * 4);
655
+ const halfW = geometry.width / 2;
656
+ const halfH = geometry.height / 2;
657
+ const neutral = Math.round(1 / HEADROOM * 255);
658
+ for (let py = 0; py < height; py += 1) {
659
+ const y = (py + 0.5) / density - halfH;
660
+ for (let px = 0; px < width; px += 1) {
661
+ const x = (px + 0.5) / density - halfW;
662
+ const i = (py * width + px) * 4;
663
+ const sd = sdfRoundedRect(x, y, geometry.width, geometry.height, geometry.cornerRadius);
664
+ const e = Math.max(-sd, 0);
665
+ if (e >= bevel) {
666
+ data[i] = neutral;
667
+ data[i + 1] = neutral;
668
+ data[i + 2] = neutral;
669
+ data[i + 3] = 255;
670
+ continue;
671
+ }
672
+ const [r, g, b] = spectralHue(e, bevel, rim, thick, optics);
673
+ data[i] = Math.round(r / HEADROOM * 255);
674
+ data[i + 1] = Math.round(g / HEADROOM * 255);
675
+ data[i + 2] = Math.round(b / HEADROOM * 255);
676
+ data[i + 3] = 255;
677
+ }
678
+ }
679
+ return { data, width, height, headroom: HEADROOM };
680
+ }
681
+ function hasSpectralEdge(optics) {
682
+ return optics.iridescence > 1e-3 || optics.diffraction > 1e-3;
683
+ }
684
+ function buildSpectralMap(optics, geometry, dpr) {
685
+ if (typeof document === "undefined" || typeof document.createElement !== "function") {
686
+ throw new Error("vireglass/dom: buildSpectralMap requires a DOM (canvas)");
687
+ }
688
+ const { data, width, height, headroom } = renderSpectralPixels(optics, geometry, dpr);
689
+ const canvas = document.createElement("canvas");
690
+ canvas.width = width;
691
+ canvas.height = height;
692
+ const ctx = canvas.getContext("2d");
693
+ if (!ctx) throw new Error("vireglass/dom: 2D canvas context unavailable");
694
+ const image = ctx.createImageData(width, height);
695
+ image.data.set(data);
696
+ ctx.putImageData(image, 0, 0);
697
+ return { url: canvas.toDataURL("image/png"), width, height, headroom };
698
+ }
699
+
700
+ // src/dom/map-cache.ts
701
+ var LIMIT = 24;
702
+ var store = /* @__PURE__ */ new Map();
703
+ function cached(key, build) {
704
+ const hit = store.get(key);
705
+ if (hit !== void 0) {
706
+ store.delete(key);
707
+ store.set(key, hit);
708
+ return hit;
709
+ }
710
+ const value = build();
711
+ store.set(key, value);
712
+ if (store.size > LIMIT) {
713
+ const oldest = store.keys().next();
714
+ if (!oldest.done) store.delete(oldest.value);
715
+ }
716
+ return value;
717
+ }
718
+ function clearMapCache() {
719
+ store.clear();
720
+ }
721
+ function mapCacheSize() {
722
+ return store.size;
723
+ }
724
+
725
+ // src/optics.ts
726
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
727
+ var clamp012 = (v) => clamp(v, 0, 1);
728
+ var fresnelF0 = (ior) => ((ior - 1) / (ior + 1)) ** 2;
729
+ var fresnelStrength = (ior) => clamp012(fresnelF0(ior) * DERIVE.fresnelGain);
730
+ var FRESNEL_EXPONENT = DERIVE.fresnelExponent;
731
+ var refractionStrength = (ior) => clamp012((ior - 1) / DERIVE.iorFullBend);
732
+ var refractionScale = (ior, thicknessDp2) => 1 + DERIVE.magnifyPerDp * thicknessDp2 * (1 - 1 / Math.max(ior, 1));
733
+ var dispersion = (ior) => clamp012((ior - 1) * DERIVE.dispersionPerIor);
734
+ var iorSpread = (ior) => dispersion(ior) * DERIVE.iorSpreadPerDispersion;
735
+ var absorption = (pathDp) => 1 - Math.exp(-0.017 * Math.max(pathDp, 0));
736
+ var edgeDensity = (thicknessDp2, bevelDp2) => {
737
+ const body = absorption(thicknessDp2);
738
+ if (body <= 0) return 1;
739
+ return clamp(absorption(thicknessDp2 + bevelDp2) / body, 1, DERIVE.edgeDensityMax);
740
+ };
741
+ var blur = (roughness) => clamp012(roughness) * DERIVE.blurMaxDp;
742
+ var specularPower = (roughness) => DERIVE.specularPowerSmooth - clamp012(roughness) * (DERIVE.specularPowerSmooth - DERIVE.specularPowerRough);
743
+ var specularStrength = (ior, roughness) => clamp012(fresnelStrength(ior) * (1 - clamp012(roughness) * DERIVE.specularRoughDamping));
744
+ var mixHue = (a, b, t) => [
745
+ a[0] + (b[0] - a[0]) * t,
746
+ a[1] + (b[1] - a[1]) * t,
747
+ a[2] + (b[2] - a[2]) * t
748
+ ];
749
+ function mediumTint(ior) {
750
+ const t = clamp012((ior - DERIVE.hueFromIor) / DERIVE.hueSpan);
751
+ const hue = t < 0.5 ? mixHue(DERIVE.hueCool, DERIVE.hueNeutral, t * 2) : mixHue(DERIVE.hueNeutral, DERIVE.hueWarm, (t - 0.5) * 2);
752
+ const lift = DERIVE.bodyLiftBase + fresnelF0(ior) * DERIVE.bodyLiftPerF0;
753
+ return { r: clamp012(hue[0] * lift), g: clamp012(hue[1] * lift), b: clamp012(hue[2] * lift) };
754
+ }
755
+ var gatherRadius = (bevelDp2) => clamp(Math.max(bevelDp2, 1) * DERIVE.gatherPerBevel, DERIVE.gatherMinDp, DERIVE.gatherMaxDp);
756
+ var bodyDensity = (thicknessDp2) => DERIVE.bodyDensity * absorption(thicknessDp2);
757
+ var edgeLight = (ior) => clamp012(fresnelF0(ior) * DERIVE.edgeLightGain);
758
+ var iridescence = (ior, filmNm) => filmNm <= 0 ? 0 : clamp012(fresnelStrength(ior) * DERIVE.iridescencePerFresnel);
759
+ var diffraction2 = (ior) => clamp012(dispersion(ior) * DERIVE.diffractionPerDispersion);
760
+ var colorPickup = (ior) => clamp(fresnelStrength(ior) * DERIVE.colorPickupPerFresnel, 0, DERIVE.colorPickupMax);
761
+
762
+ // src/material.ts
763
+ var MATERIAL_RANGES = {
764
+ ior: [1, 2],
765
+ thickness: [0, 60],
766
+ bevel: [0, 40],
767
+ roughness: [0, 1],
768
+ environment: [0, 1],
769
+ legibility: [0, 1],
770
+ dimming: [0, 0.5],
771
+ ink: [0, 1],
772
+ presence: [0, 0.6],
773
+ film: [0, 900]
774
+ };
775
+ var ADAPT_RADIUS = 22;
776
+ var clamp2 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
777
+ var VIREGLASS_MATERIAL_V5 = {
778
+ ior: 1.5,
779
+ thickness: 28,
780
+ bevel: 6,
781
+ roughness: 0.06,
782
+ environment: 0.27,
783
+ legibility: 0.26,
784
+ dimming: 0,
785
+ ink: 1,
786
+ presence: 0.05,
787
+ film: 340
788
+ };
789
+ var VIREGLASS_MATERIAL = VIREGLASS_MATERIAL_V5;
790
+ function resolveMaterial(patch = {}) {
791
+ const m = { ...VIREGLASS_MATERIAL, ...patch };
792
+ const out = { ...m };
793
+ for (const key of Object.keys(MATERIAL_RANGES)) {
794
+ const [lo, hi] = MATERIAL_RANGES[key];
795
+ out[key] = clamp2(out[key], lo, hi);
796
+ }
797
+ return out;
798
+ }
799
+ function resolveOptics(patch = {}) {
800
+ const m = resolveMaterial(patch);
801
+ return {
802
+ blur: blur(m.roughness),
803
+ refraction: refractionStrength(m.ior),
804
+ refractionScale: refractionScale(m.ior, m.thickness),
805
+ bevelDp: m.bevel,
806
+ gatherRadiusDp: gatherRadius(m.bevel),
807
+ fresnel: fresnelStrength(m.ior),
808
+ fresnelPower: FRESNEL_EXPONENT,
809
+ specular: specularStrength(m.ior, m.roughness),
810
+ specularPower: specularPower(m.roughness),
811
+ dispersion: dispersion(m.ior),
812
+ tint: mediumTint(m.ior),
813
+ tintStrength: absorption(m.thickness),
814
+ edgeDensity: edgeDensity(m.thickness, m.bevel),
815
+ environment: m.environment,
816
+ legibility: m.legibility,
817
+ dimming: m.dimming,
818
+ ink: m.ink,
819
+ presence: m.presence,
820
+ adaptRadius: ADAPT_RADIUS,
821
+ bodyDensity: bodyDensity(m.thickness),
822
+ edgeLight: edgeLight(m.ior),
823
+ film: m.film,
824
+ iridescence: iridescence(m.ior, m.film),
825
+ diffraction: diffraction2(m.ior),
826
+ colorPickup: colorPickup(m.ior),
827
+ ior: m.ior,
828
+ thicknessDp: m.thickness,
829
+ iorSpread: iorSpread(m.ior)
830
+ };
831
+ }
832
+ var EFFECTS = [
833
+ "backdrop",
834
+ "blur",
835
+ "refraction",
836
+ "fresnel",
837
+ "bevel",
838
+ "specular",
839
+ "dispersion",
840
+ "tint",
841
+ "environment",
842
+ "legibility",
843
+ "interference",
844
+ "diffraction"
845
+ ];
846
+ EFFECTS.reduce(
847
+ (acc, e) => ({ ...acc, [e]: true }),
848
+ {}
849
+ );
850
+
851
+ // src/dom/probe.ts
852
+ var GRID_COLS = 5;
853
+ var GRID_ROWS = 5;
854
+ function parseColor(value) {
855
+ const match = /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+))?\s*\)/i.exec(value);
856
+ if (!match) return null;
857
+ const r = Number(match[1]);
858
+ const g = Number(match[2]);
859
+ const b = Number(match[3]);
860
+ const a = match[4] === void 0 ? 1 : Number(match[4]);
861
+ if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b) || !Number.isFinite(a)) return null;
862
+ return { r, g, b, a };
863
+ }
864
+ function luma(r, g, b) {
865
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
866
+ }
867
+ function statsFromSamples(samples) {
868
+ const n = samples.length;
869
+ if (n === 0) return { luma: 0.5, busy: 0, lo: 0.5, hi: 0.5, r: 0.5, g: 0.5, b: 0.5 };
870
+ const lumas = [];
871
+ let sr = 0;
872
+ let sg = 0;
873
+ let sb = 0;
874
+ for (const s of samples) {
875
+ const r = s.r / 255;
876
+ const g = s.g / 255;
877
+ const b = s.b / 255;
878
+ lumas.push(luma(r, g, b));
879
+ sr += r;
880
+ sg += g;
881
+ sb += b;
882
+ }
883
+ let mean = 0;
884
+ for (const v of lumas) mean += v;
885
+ mean /= n;
886
+ let busy = 0;
887
+ for (const v of lumas) busy += Math.abs(v - mean);
888
+ busy = Math.min(busy / n * 2, 1);
889
+ const sorted = [...lumas].sort((a, b) => a - b);
890
+ const percentile = (p) => {
891
+ const idx = p * (n - 1);
892
+ const lo = Math.floor(idx);
893
+ const hi = Math.ceil(idx);
894
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
895
+ };
896
+ return {
897
+ luma: mean,
898
+ busy,
899
+ lo: percentile(0.1),
900
+ hi: percentile(0.9),
901
+ r: sr / n,
902
+ g: sg / n,
903
+ b: sb / n
904
+ };
905
+ }
906
+ function compositeLayers(layers) {
907
+ let r = 0;
908
+ let g = 0;
909
+ let b = 0;
910
+ let a = 0;
911
+ for (const layer of layers) {
912
+ const share = layer.a * (1 - a);
913
+ if (share <= 0) continue;
914
+ r += layer.r * share;
915
+ g += layer.g * share;
916
+ b += layer.b * share;
917
+ a += share;
918
+ if (a >= 0.999) break;
919
+ }
920
+ if (a <= 1e-3) return null;
921
+ return { r: r / a, g: g / a, b: b / a, a };
922
+ }
923
+ function sampleColorAt(el, x, y) {
924
+ const stack = document.elementsFromPoint(x, y);
925
+ const behind = stack.filter((node) => node !== el && !el.contains(node));
926
+ const layers = [];
927
+ for (const node of behind) {
928
+ const bg = parseColor(getComputedStyle(node).backgroundColor);
929
+ if (bg && bg.a > 1e-3) layers.push(bg);
930
+ }
931
+ const composited = compositeLayers(layers);
932
+ if (composited) return composited;
933
+ const topmost = behind[0];
934
+ if (!topmost) return null;
935
+ return parseColor(getComputedStyle(topmost).color);
936
+ }
937
+ function probeBackdrop(el, opts = {}) {
938
+ const cols = opts.cols ?? GRID_COLS;
939
+ const rows = opts.rows ?? GRID_ROWS;
940
+ const rect = el.getBoundingClientRect();
941
+ if (rect.width <= 0 || rect.height <= 0) return statsFromSamples([]);
942
+ const samples = [];
943
+ for (let j = 0; j < rows; j += 1) {
944
+ const y = rect.top + (j + 0.5) / rows * rect.height;
945
+ for (let i = 0; i < cols; i += 1) {
946
+ const x = rect.left + (i + 0.5) / cols * rect.width;
947
+ const color = sampleColorAt(el, x, y);
948
+ if (color) samples.push(color);
949
+ }
950
+ }
951
+ return statsFromSamples(samples);
952
+ }
953
+
954
+ // src/dom/support.ts
955
+ var backdropFilterSupport = null;
956
+ var svgBackdropFilterSupport = null;
957
+ function probeProperty(property, value) {
958
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
959
+ return CSS.supports(property, value);
960
+ }
961
+ function supportsBackdropFilter() {
962
+ if (backdropFilterSupport === null) {
963
+ backdropFilterSupport = probeProperty("backdrop-filter", "blur(1px)") || probeProperty("-webkit-backdrop-filter", "blur(1px)");
964
+ }
965
+ return backdropFilterSupport;
966
+ }
967
+ function supportsSvgBackdropFilter() {
968
+ if (svgBackdropFilterSupport === null) {
969
+ svgBackdropFilterSupport = probeProperty("backdrop-filter", "url(#vireglass-support-probe)");
970
+ }
971
+ return svgBackdropFilterSupport;
972
+ }
973
+ function resetSupportCache() {
974
+ backdropFilterSupport = null;
975
+ svgBackdropFilterSupport = null;
976
+ }
977
+
978
+ // src/shadow-tint.ts
979
+ var LUMA = [0.2126, 0.7152, 0.0722];
980
+ var luma2 = (c) => c[0] * LUMA[0] + c[1] * LUMA[1] + c[2] * LUMA[2];
981
+ function shadowTint(ambient) {
982
+ const peak = Math.max(ambient[0], ambient[1], ambient[2], 1e-3);
983
+ const hue = [ambient[0] / peak, ambient[1] / peak, ambient[2] / peak];
984
+ const grey = luma2(hue);
985
+ const available = luma2(ambient);
986
+ return [(hue[0] - grey) * available, (hue[1] - grey) * available, (hue[2] - grey) * available];
987
+ }
988
+
989
+ // src/dom/shadow.ts
990
+ var FLAT_DENSITY = shadowOpacity(0);
991
+ var BUSY_DENSITY = shadowOpacity(1);
992
+ function shadowAlphaFrom(sample) {
993
+ const t = (shadowOpacityFrom(sample) - FLAT_DENSITY) / (BUSY_DENSITY - FLAT_DENSITY);
994
+ const alpha = SHADOW.alphaFlat + t * (SHADOW.alphaBusy - SHADOW.alphaFlat);
995
+ return Math.min(Math.max(alpha, 0), 1);
996
+ }
997
+ var to255 = (v) => Math.round(Math.min(Math.max(v, 0), 1) * 255);
998
+ function boxShadowCss(geometry, sample, appear = 1) {
999
+ const reach = shadowReachDp(geometry);
1000
+ const present = appear < 0 ? 0 : appear > 1 ? 1 : appear;
1001
+ const alpha = shadowAlphaFrom(sample) * present;
1002
+ const ambient = "luma" in sample ? ambientFrom(sample) : null;
1003
+ if (!ambient) return `0 0 ${reach.toFixed(1)}px rgba(0, 0, 0, ${alpha.toFixed(3)})`;
1004
+ const [r, g, b] = shadowTint(ambient);
1005
+ const spill = SHADOW_GAP.tint;
1006
+ return `0 0 ${reach.toFixed(1)}px rgba(${to255(r * spill)}, ${to255(g * spill)}, ${to255(b * spill)}, ${alpha.toFixed(3)})`;
1007
+ }
1008
+
1009
+ // src/dom/body.ts
1010
+ var {
1011
+ capLoose: BODY_CAP_LOOSE,
1012
+ capTight: BODY_CAP_TIGHT,
1013
+ tintDark: TINT_DARK,
1014
+ tintLight: TINT_LIGHT,
1015
+ groundSpan: GROUND_SPAN,
1016
+ busyEdge: BUSY_EDGE,
1017
+ darkSideFrom: DARK_SIDE_FROM,
1018
+ darkSideTo: DARK_SIDE_TO,
1019
+ maxDemand: MAX_DEMAND
1020
+ } = BODY;
1021
+ var clamp3 = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
1022
+ var mix = (a, b, t) => a + (b - a) * t;
1023
+ function smoothstep2(edge0, edge1, x) {
1024
+ const t = clamp3((x - edge0) / (edge1 - edge0), 0, 1);
1025
+ return t * t * (3 - 2 * t);
1026
+ }
1027
+ function resolveBody(optics, sample, ink) {
1028
+ const strict = clamp3(optics.legibility * 2, 0, 1);
1029
+ const capLight = mix(BODY_CAP_LOOSE, BODY_CAP_TIGHT, strict);
1030
+ const floorDark = 1 - capLight;
1031
+ const pol = clamp3(ink, 0, 1);
1032
+ const demand = clamp3(optics.legibility * 4, 0, 1);
1033
+ const mean = clamp3(sample.luma, 0, 1);
1034
+ const busy = Math.max(sample.busy, 0);
1035
+ const darkSide = smoothstep2(DARK_SIDE_FROM, DARK_SIDE_TO, mean);
1036
+ const away = mix(TINT_LIGHT, TINT_DARK, darkSide);
1037
+ const tintLuma = mix(away, mix(TINT_LIGHT, TINT_DARK, pol), demand);
1038
+ const edge = busy * BUSY_EDGE;
1039
+ const inkHi = Math.min(mean + edge, 1);
1040
+ const inkLo = Math.max(mean - edge, 0);
1041
+ const needForLight = inkHi > capLight ? clamp3((inkHi - capLight) / Math.max(inkHi - TINT_DARK, 1e-4), 0, MAX_DEMAND) : 0;
1042
+ const needForDark = inkLo < floorDark ? clamp3((floorDark - inkLo) / Math.max(TINT_LIGHT - inkLo, 1e-4), 0, MAX_DEMAND) : 0;
1043
+ const needForInk = mix(needForDark, needForLight, pol) * demand;
1044
+ const ground = busy * optics.legibility * GROUND_SPAN;
1045
+ return { density: Math.max(Math.max(optics.bodyDensity, ground), needForInk), tintLuma };
1046
+ }
1047
+ function withPresence(body, optics, sample) {
1048
+ if (optics.presence <= 0) return body;
1049
+ const mean = clamp3(sample.luma, 0, 1);
1050
+ const lighter = mean < 0.5;
1051
+ const target = lighter ? Math.min(mean + optics.presence, 1) : Math.max(mean - optics.presence, 0);
1052
+ const separation = Math.abs(target - mean);
1053
+ const reached = Math.abs(body.tintLuma - mean) * body.density;
1054
+ if (reached >= separation) return body;
1055
+ const needed = separation / Math.max(Math.abs((lighter ? TINT_LIGHT : TINT_DARK) - mean), 1e-4);
1056
+ return {
1057
+ density: Math.max(body.density, Math.min(needed, MAX_DEMAND)),
1058
+ tintLuma: lighter ? Math.max(body.tintLuma, mean) : Math.min(body.tintLuma, mean)
1059
+ };
1060
+ }
1061
+
1062
+ // src/accessibility.ts
1063
+ var NO_ACCESSIBILITY = {
1064
+ reduceTransparency: false,
1065
+ increaseContrast: false,
1066
+ reduceMotion: false
1067
+ };
1068
+ function applyAccessibility(optics, mods = NO_ACCESSIBILITY) {
1069
+ if (!mods.reduceTransparency && !mods.increaseContrast) return optics;
1070
+ const out = { ...optics };
1071
+ if (mods.reduceTransparency) {
1072
+ out.blur = Math.max(out.blur, ACCESSIBILITY.frostMinDp);
1073
+ out.bodyDensity = Math.max(out.bodyDensity, ACCESSIBILITY.obscureMin);
1074
+ }
1075
+ if (mods.increaseContrast) {
1076
+ out.bodyDensity = Math.max(out.bodyDensity, ACCESSIBILITY.contrastDensity);
1077
+ out.presence = Math.max(out.presence, ACCESSIBILITY.contrastPresence);
1078
+ out.legibility = 1;
1079
+ }
1080
+ return out;
1081
+ }
1082
+ function contrastRimLuma(tintLuma) {
1083
+ const light = tintLuma < 0.5;
1084
+ return light ? Math.min(tintLuma + ACCESSIBILITY.contrastRim, BODY.tintLight) : Math.max(tintLuma - ACCESSIBILITY.contrastRim, BODY.tintDark);
1085
+ }
1086
+ function elasticAllowed(mods = NO_ACCESSIBILITY) {
1087
+ return !mods.reduceMotion;
1088
+ }
1089
+
1090
+ // src/dom/preferences.ts
1091
+ var QUERIES = {
1092
+ reduceTransparency: "(prefers-reduced-transparency: reduce)",
1093
+ increaseContrast: "(prefers-contrast: more)",
1094
+ reduceMotion: "(prefers-reduced-motion: reduce)"
1095
+ };
1096
+ function matches(query) {
1097
+ if (typeof matchMedia !== "function") return false;
1098
+ try {
1099
+ return matchMedia(query).matches;
1100
+ } catch {
1101
+ return false;
1102
+ }
1103
+ }
1104
+ function systemAccessibility() {
1105
+ if (typeof matchMedia !== "function") return NO_ACCESSIBILITY;
1106
+ return {
1107
+ reduceTransparency: matches(QUERIES.reduceTransparency),
1108
+ increaseContrast: matches(QUERIES.increaseContrast),
1109
+ reduceMotion: matches(QUERIES.reduceMotion)
1110
+ };
1111
+ }
1112
+ function watchAccessibility(onChange) {
1113
+ if (typeof matchMedia !== "function") return () => {
1114
+ };
1115
+ const lists = [];
1116
+ const handler = () => onChange(systemAccessibility());
1117
+ for (const query of Object.values(QUERIES)) {
1118
+ try {
1119
+ const list = matchMedia(query);
1120
+ list.addEventListener("change", handler);
1121
+ lists.push(list);
1122
+ } catch {
1123
+ }
1124
+ }
1125
+ return () => {
1126
+ for (const list of lists) list.removeEventListener("change", handler);
1127
+ };
1128
+ }
1129
+
1130
+ // src/glass-scale.ts
1131
+ var GLASS_SCALE_DEFAULT = SCALE.default;
1132
+ var clamp013 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1133
+ function applyGlassScale(optics, scale) {
1134
+ const s = clamp013(scale);
1135
+ if (s === GLASS_SCALE_DEFAULT) return optics;
1136
+ if (s < GLASS_SCALE_DEFAULT) {
1137
+ const k2 = s / GLASS_SCALE_DEFAULT;
1138
+ return { ...optics, legibility: optics.legibility * k2, dimming: optics.dimming * k2 };
1139
+ }
1140
+ const k = (s - GLASS_SCALE_DEFAULT) / (1 - GLASS_SCALE_DEFAULT);
1141
+ return { ...optics, bodyDensity: Math.max(optics.bodyDensity, SCALE.tintedDensity * k) };
1142
+ }
1143
+
1144
+ // src/appear.ts
1145
+ var clamp014 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1146
+ function applyAppear(optics, t) {
1147
+ const a = clamp014(t);
1148
+ if (a === 1) return optics;
1149
+ return {
1150
+ ...optics,
1151
+ refraction: optics.refraction * a,
1152
+ blur: optics.blur * a,
1153
+ bodyDensity: optics.bodyDensity * a,
1154
+ dimming: optics.dimming * a,
1155
+ edgeLight: optics.edgeLight * a,
1156
+ // A floor on how far the element must stand out from its backdrop (§3). Left alone it holds
1157
+ // the element visible at t = 0, which is the opposite of what materialising means.
1158
+ presence: optics.presence * a,
1159
+ // The spectral trio is lensing too — §1's "light bending and lensing" is not only the
1160
+ // displacement. The fringe map and the per-channel split are drawn from these, and a filter
1161
+ // graph that still paints them is an element that is not there but is casting a rainbow.
1162
+ dispersion: optics.dispersion * a,
1163
+ iridescence: optics.iridescence * a,
1164
+ diffraction: optics.diffraction * a,
1165
+ tintStrength: optics.tintStrength * a,
1166
+ // Legibility is a REQUIREMENT, not an effect: an element still arriving has no ink on it yet,
1167
+ // and holding a demand it cannot meet would darken the body exactly while it should be
1168
+ // clearing. It fades with the rest.
1169
+ legibility: optics.legibility * a
1170
+ };
1171
+ }
1172
+
1173
+ // src/accent.ts
1174
+ var clamp015 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1175
+ function accentTone(color, backdropLuma) {
1176
+ const k = ACCENT.deep + (ACCENT.light - ACCENT.deep) * clamp015(backdropLuma);
1177
+ return [clamp015(color[0] * k), clamp015(color[1] * k), clamp015(color[2] * k)];
1178
+ }
1179
+ function accentAmount(accent) {
1180
+ return clamp015(accent.amount ?? ACCENT.amount);
1181
+ }
1182
+
1183
+ // src/concentric.ts
1184
+ function concentricRadius(outerRadius, inset, minimum = 0) {
1185
+ return Math.max(outerRadius - inset, Math.max(minimum, 0));
1186
+ }
1187
+
1188
+ // src/dom/rim.ts
1189
+ var { lobeExponent: LOBE_EXPONENT, opposingArc: OPPOSING_ARC, darkEdge: DARK_EDGE } = RIM;
1190
+ var STOPS = 48;
1191
+ function lightConicAngle(light) {
1192
+ const [lx, ly] = light;
1193
+ const deg = Math.atan2(lx, -ly) * 180 / Math.PI;
1194
+ return (deg + 360) % 360;
1195
+ }
1196
+ function rimLobe(deltaDeg, specular) {
1197
+ const facing = Math.cos(deltaDeg * Math.PI / 180);
1198
+ const front = facing > 0 ? Math.pow(facing, LOBE_EXPONENT) : 0;
1199
+ const back = facing < 0 ? Math.pow(-facing, LOBE_EXPONENT) : 0;
1200
+ return specular * (front + OPPOSING_ARC * back);
1201
+ }
1202
+ var clamp016 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1203
+ var to2552 = (v) => Math.round(clamp016(v) * 255);
1204
+ function rimGradientCss(optics, ambient, light, appear = 1) {
1205
+ const present = clamp016(appear);
1206
+ const start = lightConicAngle(light);
1207
+ const [ar, ag, ab] = ambient;
1208
+ const stops = [];
1209
+ for (let i = 0; i <= STOPS; i += 1) {
1210
+ const angle = i / STOPS * 360;
1211
+ const key = rimLobe(angle, optics.specular);
1212
+ const facing = Math.cos(angle * Math.PI / 180);
1213
+ const fringe = facing < 0 ? optics.iridescence * OPPOSING_ARC : 0;
1214
+ const lift = clamp016(key * optics.fresnel);
1215
+ const r = ar + (1 - ar) * clamp016(lift + fringe);
1216
+ const g = ag + (1 - ag) * lift;
1217
+ const b = ab + (1 - ab) * clamp016(lift - fringe);
1218
+ const alpha = clamp016(DARK_EDGE + lift * (1 - DARK_EDGE)) * present;
1219
+ const colour = lift > 1e-3 ? `rgba(${to2552(r)},${to2552(g)},${to2552(b)},${alpha.toFixed(3)})` : `rgba(0,0,0,${alpha.toFixed(3)})`;
1220
+ stops.push(`${colour} ${angle.toFixed(1)}deg`);
1221
+ }
1222
+ return `conic-gradient(from ${start.toFixed(1)}deg, ${stops.join(", ")})`;
1223
+ }
1224
+ var RIM_WIDTH_PX = RIM.widthPx;
1225
+ function contrastRimCss(bodyLuma, rimLuma) {
1226
+ const v = to2552(rimLuma);
1227
+ return `conic-gradient(rgb(${v},${v},${v}) 0deg, rgb(${v},${v},${v}) 360deg)`;
1228
+ }
1229
+
1230
+ // src/dom/nesting.ts
1231
+ var WARNED = /* @__PURE__ */ new WeakSet();
1232
+ var GLASS_ATTR = "data-vireglass";
1233
+ function warnIfNested(el) {
1234
+ if (WARNED.has(el)) return;
1235
+ const outer = el.parentElement?.closest(`[${GLASS_ATTR}]`);
1236
+ if (!outer) return;
1237
+ WARNED.add(el);
1238
+ const where = outer.tagName.toLowerCase() + (outer.id ? `#${outer.id}` : "");
1239
+ console.warn(
1240
+ `vireglass: glass inside glass \u2014 this element is a descendant of <${where}>, which is also glass. Its backdrop is the outer element's output, so the refraction is applied twice and the blurs multiply. Put the glass on one of the two.`
1241
+ );
1242
+ }
1243
+ var REST_LIGHT = [-0.577, -0.817];
1244
+
1245
+ // src/touch-response.ts
1246
+ function createDeform(options = {}) {
1247
+ let elastic = options.elastic !== false;
1248
+ let pressCeiling = elastic ? 1 : ACCESSIBILITY.stillPress;
1249
+ let touchX = 0;
1250
+ let touchY = 0;
1251
+ let targetTouchX = 0;
1252
+ let targetTouchY = 0;
1253
+ let pullX = 0;
1254
+ let pullY = 0;
1255
+ let vx = 0;
1256
+ let vy = 0;
1257
+ let targetX = 0;
1258
+ let targetY = 0;
1259
+ let held = false;
1260
+ let press = 0;
1261
+ let active = 0;
1262
+ let waveAmp = 0;
1263
+ let wavePhase = 0;
1264
+ function grab(x, y, waveStart) {
1265
+ const atRest = idle();
1266
+ held = true;
1267
+ targetTouchX = x;
1268
+ targetTouchY = y;
1269
+ if (atRest) {
1270
+ touchX = x;
1271
+ touchY = y;
1272
+ }
1273
+ targetX = 0;
1274
+ targetY = 0;
1275
+ if (elastic) waveAmp = Math.min(waveAmp + waveStart, waveStart * SPRING.waveCapOnGrab);
1276
+ }
1277
+ function drag(dx, dy, limit) {
1278
+ if (!held || !elastic) return;
1279
+ const len = Math.hypot(dx, dy);
1280
+ const scale = len > 1e-3 ? limit * Math.tanh(len / limit) / len : 0;
1281
+ targetX = dx * scale;
1282
+ targetY = dy * scale;
1283
+ }
1284
+ function release(waveStart) {
1285
+ held = false;
1286
+ targetX = 0;
1287
+ targetY = 0;
1288
+ if (elastic) waveAmp = Math.min(waveAmp + waveStart, waveStart * SPRING.waveCapOnRelease);
1289
+ }
1290
+ function integrate(dt) {
1291
+ const k = held ? SPRING.holdStiffness : SPRING.releaseStiffness;
1292
+ const c = held ? SPRING.holdDamping : SPRING.releaseDamping;
1293
+ vx += (k * (targetX - pullX) - c * vx) * dt;
1294
+ vy += (k * (targetY - pullY) - c * vy) * dt;
1295
+ pullX += vx * dt;
1296
+ pullY += vy * dt;
1297
+ const pressTarget = held ? pressCeiling : 0;
1298
+ const tau = held ? SPRING.pressAttack : SPRING.pressRelease;
1299
+ press += (pressTarget - press) * (1 - Math.exp(-dt / tau));
1300
+ active += (pressTarget - active) * (1 - Math.exp(-dt / SPRING.activeFollow));
1301
+ const follow = 1 - Math.exp(-dt / SPRING.pointFollow);
1302
+ touchX += (targetTouchX - touchX) * follow;
1303
+ touchY += (targetTouchY - touchY) * follow;
1304
+ wavePhase += dt * SPRING.waveTurnsPerSecond;
1305
+ waveAmp *= Math.exp(-dt / SPRING.waveDecay);
1306
+ if (waveAmp < 0.01) waveAmp = 0;
1307
+ }
1308
+ function step(dt) {
1309
+ let rest = Math.min(Math.max(dt, 0), 0.25);
1310
+ const h = 1 / 120;
1311
+ while (rest > 1e-6) {
1312
+ const slice = Math.min(h, rest);
1313
+ integrate(slice);
1314
+ rest -= slice;
1315
+ }
1316
+ if (idle()) {
1317
+ pullX = 0;
1318
+ pullY = 0;
1319
+ vx = 0;
1320
+ vy = 0;
1321
+ press = 0;
1322
+ active = 0;
1323
+ wavePhase = 0;
1324
+ }
1325
+ }
1326
+ function idle() {
1327
+ return !held && Math.abs(pullX) < 0.05 && Math.abs(pullY) < 0.05 && Math.hypot(vx, vy) < 0.5 && press < 4e-3 && active < 4e-3 && waveAmp === 0;
1328
+ }
1329
+ function setElastic(on) {
1330
+ if (on === elastic) return;
1331
+ elastic = on;
1332
+ pressCeiling = on ? 1 : ACCESSIBILITY.stillPress;
1333
+ if (!on) {
1334
+ pullX = 0;
1335
+ pullY = 0;
1336
+ vx = 0;
1337
+ vy = 0;
1338
+ targetX = 0;
1339
+ targetY = 0;
1340
+ waveAmp = 0;
1341
+ wavePhase = 0;
1342
+ }
1343
+ }
1344
+ function sample() {
1345
+ return { touchX, touchY, pullX, pullY, press, active, waveAmp, wavePhase };
1346
+ }
1347
+ return { grab, drag, release, step, idle, sample, setElastic };
1348
+ }
1349
+ function raiseIntoGlass(press) {
1350
+ const t = press < 0 ? 0 : press > 1 ? 1 : press;
1351
+ return { glass: Math.pow(t, 0.6), solid: 1 - Math.pow(t, 1.4) };
1352
+ }
1353
+
1354
+ // src/scroll-edge.ts
1355
+ function scrollEdgeStyle(inkLight, pinned = false) {
1356
+ if (pinned) return "hard";
1357
+ return inkLight ? "dim" : "dissolve";
1358
+ }
1359
+ var SCROLL_EDGE_ENGAGE_DP = SCROLL_EDGE.engageDp;
1360
+ function scrollEdgeStrength(side, scroll, maxScroll) {
1361
+ const room = side === "top" ? scroll : maxScroll - scroll;
1362
+ return Math.min(Math.max(room / SCROLL_EDGE_ENGAGE_DP, 0), 1);
1363
+ }
1364
+
1365
+ // src/dom/scroll-edge.ts
1366
+ function paintFor(style, side) {
1367
+ const direction = side === "top" ? "to top" : "to bottom";
1368
+ if (style === "hard") {
1369
+ return {
1370
+ filter: `blur(${SCROLL_EDGE.hardBlurDp}px)`,
1371
+ mask: "",
1372
+ tone: `linear-gradient(${direction}, rgba(0,0,0,${SCROLL_EDGE.hardAlpha}) 0 100%)`
1373
+ };
1374
+ }
1375
+ const ramp = `linear-gradient(${direction}, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%)`;
1376
+ return {
1377
+ filter: `blur(${SCROLL_EDGE.dissolveBlurDp}px)`,
1378
+ mask: ramp,
1379
+ tone: style === "dim" ? `linear-gradient(${direction}, rgba(0,0,0,${SCROLL_EDGE.dimAlpha}) 0%, rgba(0,0,0,0) 100%)` : "none"
1380
+ };
1381
+ }
1382
+ function attachScrollEdge(scroller, edgeEl, opts = {}) {
1383
+ const side = opts.side ?? "top";
1384
+ let destroyed = false;
1385
+ function update() {
1386
+ if (destroyed) return;
1387
+ const maxScroll = Math.max(scroller.scrollHeight - scroller.clientHeight, 0);
1388
+ const strength = scrollEdgeStrength(side, scroller.scrollTop, maxScroll);
1389
+ const inkLight = (opts.ink?.() ?? 1) > 0.5;
1390
+ const style = scrollEdgeStyle(inkLight, opts.pinned);
1391
+ const paint = paintFor(style, side);
1392
+ edgeEl.style.setProperty("backdrop-filter", paint.filter);
1393
+ edgeEl.style.setProperty("-webkit-backdrop-filter", paint.filter);
1394
+ if (paint.mask) {
1395
+ edgeEl.style.setProperty("mask-image", paint.mask);
1396
+ edgeEl.style.setProperty("-webkit-mask-image", paint.mask);
1397
+ } else {
1398
+ edgeEl.style.removeProperty("mask-image");
1399
+ edgeEl.style.removeProperty("-webkit-mask-image");
1400
+ }
1401
+ edgeEl.style.setProperty("background-image", paint.tone);
1402
+ edgeEl.style.setProperty("opacity", strength.toFixed(3));
1403
+ }
1404
+ const onScroll = () => update();
1405
+ scroller.addEventListener("scroll", onScroll, { passive: true });
1406
+ update();
1407
+ return {
1408
+ update,
1409
+ destroy() {
1410
+ destroyed = true;
1411
+ scroller.removeEventListener("scroll", onScroll);
1412
+ edgeEl.style.removeProperty("backdrop-filter");
1413
+ edgeEl.style.removeProperty("-webkit-backdrop-filter");
1414
+ edgeEl.style.removeProperty("mask-image");
1415
+ edgeEl.style.removeProperty("-webkit-mask-image");
1416
+ edgeEl.style.removeProperty("background-image");
1417
+ edgeEl.style.removeProperty("opacity");
1418
+ }
1419
+ };
1420
+ }
1421
+
1422
+ // src/group-model.ts
1423
+ var SMOOTH = 0.25;
1424
+ var SETTLE = 1e-6;
1425
+ function fitLuma(members) {
1426
+ const n = members.length;
1427
+ let mx = 0;
1428
+ let ml = 0;
1429
+ for (const m of members) {
1430
+ mx += m.x;
1431
+ ml += m.sample.luma;
1432
+ }
1433
+ mx /= n;
1434
+ ml /= n;
1435
+ let sxx = 0;
1436
+ let sxl = 0;
1437
+ for (const m of members) {
1438
+ const dx = m.x - mx;
1439
+ sxx += dx * dx;
1440
+ sxl += dx * m.sample.luma;
1441
+ }
1442
+ return { mx, ml, slope: sxx > 1e-6 ? sxl / sxx : 0 };
1443
+ }
1444
+ function aggregate(members) {
1445
+ if (members.length === 0) return null;
1446
+ const { mx, ml, slope } = fitLuma(members);
1447
+ let busy = 0;
1448
+ let lo = 1;
1449
+ let hi = 0;
1450
+ let base = 0;
1451
+ let r = 0;
1452
+ let g = 0;
1453
+ let b = 0;
1454
+ let legibility = 0;
1455
+ for (const m of members) {
1456
+ busy = Math.max(busy, m.sample.busy);
1457
+ lo = Math.min(lo, m.sample.lo);
1458
+ hi = Math.max(hi, m.sample.hi);
1459
+ base = Math.max(base, m.sample.luma);
1460
+ r += m.sample.r;
1461
+ g += m.sample.g;
1462
+ b += m.sample.b;
1463
+ legibility = Math.max(legibility, m.legibility);
1464
+ }
1465
+ const k = members.length;
1466
+ return {
1467
+ plane: { mx, ml, slope, base, rest: [busy, lo, hi, r / k, g / k, b / k] },
1468
+ // The polarity decision is one per group, by its AVERAGE lightness. Deciding on its own,
1469
+ // each button used to end up a different color over a busy backdrop.
1470
+ decision: { decisive: ml * 0.75 + hi * 0.25, legibility }
1471
+ };
1472
+ }
1473
+ function smoothPlane(prev, next) {
1474
+ if (!prev) return next;
1475
+ const e = (was, now) => Math.abs(now - was) < SETTLE ? now : was + (now - was) * SMOOTH;
1476
+ const ml = e(prev.ml, next.ml);
1477
+ const slope = e(prev.slope, next.slope);
1478
+ const base = e(prev.base, next.base);
1479
+ const rest = next.rest.map((v, i) => e(prev.rest[i], v));
1480
+ const same = next.mx === prev.mx && ml === prev.ml && slope === prev.slope && base === prev.base && rest.every((v, i) => v === prev.rest[i]);
1481
+ return same ? prev : { mx: next.mx, ml, slope, base, rest };
1482
+ }
1483
+ function wantsPolarityChange(decision, polarity) {
1484
+ return polarity === 1 ? decision.decisive >= FLIP_LUMA : decision.decisive < RETURN_LUMA;
1485
+ }
1486
+ function createGroupState(sink) {
1487
+ const members = /* @__PURE__ */ new Map();
1488
+ let plane = null;
1489
+ let pending = 0;
1490
+ let polarity = 1;
1491
+ const measure = () => {
1492
+ const next = aggregate([...members.values()]);
1493
+ if (!next) {
1494
+ pending = 0;
1495
+ plane = null;
1496
+ return null;
1497
+ }
1498
+ plane = smoothPlane(plane, next.plane);
1499
+ return next.decision;
1500
+ };
1501
+ return {
1502
+ report(id, x, y, sample, legibility) {
1503
+ members.set(id, { x, y, sample, legibility });
1504
+ const decision = measure();
1505
+ if (!decision) return;
1506
+ if (!wantsPolarityChange(decision, polarity)) {
1507
+ pending = 0;
1508
+ return;
1509
+ }
1510
+ pending += 1;
1511
+ if (pending < CONFIRMATIONS) return;
1512
+ pending = 0;
1513
+ polarity = polarity === 1 ? 0 : 1;
1514
+ sink.flip(polarity);
1515
+ },
1516
+ // Only recomputes the estimate: losing a member isn't a backdrop sample and can't count as a
1517
+ // polarity confirmation — unmounting the group used to rack up CONFIRMATIONS instantly and
1518
+ // trigger a recolor nobody was left to cancel.
1519
+ release(id) {
1520
+ if (members.delete(id)) measure();
1521
+ },
1522
+ plane: () => plane,
1523
+ polarity: () => polarity,
1524
+ confirmations: () => pending
1525
+ };
1526
+ }
1527
+
1528
+ // src/dom/group.ts
1529
+ function createGlassGroup(members, opts = {}) {
1530
+ let polarity = 1;
1531
+ let destroyed = false;
1532
+ const state = createGroupState({
1533
+ plane: () => {
1534
+ },
1535
+ flip: (next) => {
1536
+ polarity = next;
1537
+ for (const { handle } of members) handle.update();
1538
+ }
1539
+ });
1540
+ function carryGlow(from, glow) {
1541
+ members.forEach(({ el, handle }, index) => {
1542
+ if (index === from) return;
1543
+ if (!glow) {
1544
+ handle.setNeighbourGlow(null);
1545
+ return;
1546
+ }
1547
+ const rect = el.getBoundingClientRect();
1548
+ const cx = rect.left + rect.width / 2;
1549
+ const cy = rect.top + rect.height / 2;
1550
+ const reach = Math.min(rect.width, rect.height) / 2 * TOUCH.glowReach;
1551
+ const distance = Math.hypot(glow.pageX - cx, glow.pageY - cy);
1552
+ const falloff = reach > 0 ? Math.max(0, 1 - distance / reach) : 0;
1553
+ handle.setNeighbourGlow(falloff <= 0 ? null : { ...glow, strength: glow.strength * falloff });
1554
+ });
1555
+ }
1556
+ function connect() {
1557
+ members.forEach((entry, index) => {
1558
+ entry.onGlow = (glow) => carryGlow(index, glow);
1559
+ });
1560
+ }
1561
+ function update() {
1562
+ if (destroyed) return;
1563
+ const probe = opts.probe ?? probeBackdrop;
1564
+ members.forEach(({ el, legibility }, index) => {
1565
+ const rect = el.getBoundingClientRect();
1566
+ if (rect.width <= 0 || rect.height <= 0) {
1567
+ state.release(String(index));
1568
+ return;
1569
+ }
1570
+ state.report(
1571
+ String(index),
1572
+ rect.left + rect.width / 2,
1573
+ rect.top + rect.height / 2,
1574
+ probe(el),
1575
+ legibility ?? 0
1576
+ );
1577
+ });
1578
+ }
1579
+ connect();
1580
+ update();
1581
+ return {
1582
+ update,
1583
+ ink: () => polarity,
1584
+ plane: () => state.plane(),
1585
+ carryGlow,
1586
+ destroy() {
1587
+ destroyed = true;
1588
+ carryGlow(-1, null);
1589
+ members.forEach((_, index) => state.release(String(index)));
1590
+ }
1591
+ };
1592
+ }
1593
+
1594
+ // src/dom/index.ts
1595
+ var SVG_NS = "http://www.w3.org/2000/svg";
1596
+ var FILTER_HOST_ATTR = "data-vireglass-filters";
1597
+ var filterCounter = 0;
1598
+ var attached = /* @__PURE__ */ new WeakMap();
1599
+ var TOUCH_RADIUS_FRACTION = TOUCH.radiusOfHalfSize;
1600
+ var WAVE_OF_TRAVEL = TOUCH.waveOfTravel;
1601
+ var RELEASE_WAVE = TOUCH.releaseWave;
1602
+ function waveImpulse(g) {
1603
+ return halfMinDp(g) * MAX_STRETCH * WAVE_OF_TRAVEL;
1604
+ }
1605
+ var STYLE_ATTR = "data-vireglass-styles";
1606
+ function ensureRimStyles() {
1607
+ if (document.querySelector(`style[${STYLE_ATTR}]`)) return;
1608
+ const style = document.createElement("style");
1609
+ style.setAttribute(STYLE_ATTR, "");
1610
+ style.textContent = `[${GLASS_ATTR}]::after{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;padding:var(--vireglass-rim-width,1px);background:var(--vireglass-rim,none);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude}[${GLASS_ATTR}]::before{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:var(--vireglass-glow,none);opacity:var(--vireglass-glow-opacity,0)}`;
1611
+ document.head.appendChild(style);
1612
+ }
1613
+ function getFilterHost() {
1614
+ const existing = document.querySelector(`svg[${FILTER_HOST_ATTR}]`);
1615
+ if (existing) return existing;
1616
+ const svg = document.createElementNS(SVG_NS, "svg");
1617
+ svg.setAttribute(FILTER_HOST_ATTR, "");
1618
+ svg.setAttribute("width", "0");
1619
+ svg.setAttribute("height", "0");
1620
+ svg.setAttribute("aria-hidden", "true");
1621
+ svg.style.position = "absolute";
1622
+ svg.style.pointerEvents = "none";
1623
+ svg.appendChild(document.createElementNS(SVG_NS, "defs"));
1624
+ document.body.appendChild(svg);
1625
+ return svg;
1626
+ }
1627
+ function buildFilterElement(id, mapUrl, optics, scale, spectral) {
1628
+ const filter = document.createElementNS(SVG_NS, "filter");
1629
+ filter.setAttribute("id", id);
1630
+ filter.setAttribute("filterUnits", "objectBoundingBox");
1631
+ filter.setAttribute("x", "0");
1632
+ filter.setAttribute("y", "0");
1633
+ filter.setAttribute("width", "1");
1634
+ filter.setAttribute("height", "1");
1635
+ filter.setAttribute("color-interpolation-filters", "sRGB");
1636
+ const feImage = document.createElementNS(SVG_NS, "feImage");
1637
+ feImage.setAttribute("href", mapUrl);
1638
+ feImage.setAttribute("preserveAspectRatio", "none");
1639
+ feImage.setAttribute("result", "map");
1640
+ const feBlur = document.createElementNS(SVG_NS, "feGaussianBlur");
1641
+ feBlur.setAttribute("in", "SourceGraphic");
1642
+ feBlur.setAttribute("stdDeviation", String(Math.max(optics.blur, 0)));
1643
+ feBlur.setAttribute("result", "blurred");
1644
+ filter.append(feImage, feBlur);
1645
+ const spread = optics.iorSpread;
1646
+ if (spread > 1e-4) {
1647
+ const base = refractionStrength(optics.ior);
1648
+ const ratio = (shift) => base > 0 ? refractionStrength(optics.ior + shift) / base : 1;
1649
+ const channels = [
1650
+ ["dR", scale * ratio(DISPERSION.redShift * spread), "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"],
1651
+ ["dG", scale, "0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"],
1652
+ ["dB", scale * ratio(DISPERSION.blueShift * spread), "0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"]
1653
+ ];
1654
+ for (const [name, chScale, matrix] of channels) {
1655
+ const pass = document.createElementNS(SVG_NS, "feDisplacementMap");
1656
+ pass.setAttribute("in", "blurred");
1657
+ pass.setAttribute("in2", "map");
1658
+ pass.setAttribute("scale", String(chScale));
1659
+ pass.setAttribute("xChannelSelector", "R");
1660
+ pass.setAttribute("yChannelSelector", "G");
1661
+ pass.setAttribute("result", `${name}raw`);
1662
+ const only = document.createElementNS(SVG_NS, "feColorMatrix");
1663
+ only.setAttribute("in", `${name}raw`);
1664
+ only.setAttribute("type", "matrix");
1665
+ only.setAttribute("values", matrix);
1666
+ only.setAttribute("result", name);
1667
+ filter.append(pass, only);
1668
+ }
1669
+ const rg = document.createElementNS(SVG_NS, "feComposite");
1670
+ rg.setAttribute("in", "dR");
1671
+ rg.setAttribute("in2", "dG");
1672
+ rg.setAttribute("operator", "arithmetic");
1673
+ rg.setAttribute("k1", "0");
1674
+ rg.setAttribute("k2", "1");
1675
+ rg.setAttribute("k3", "1");
1676
+ rg.setAttribute("k4", "0");
1677
+ rg.setAttribute("result", "dRG");
1678
+ const rgb = document.createElementNS(SVG_NS, "feComposite");
1679
+ rgb.setAttribute("in", "dRG");
1680
+ rgb.setAttribute("in2", "dB");
1681
+ rgb.setAttribute("operator", "arithmetic");
1682
+ rgb.setAttribute("k1", "0");
1683
+ rgb.setAttribute("k2", "1");
1684
+ rgb.setAttribute("k3", "1");
1685
+ rgb.setAttribute("k4", "0");
1686
+ rgb.setAttribute("result", "refracted");
1687
+ filter.append(rg, rgb);
1688
+ return withSpectral(filter, spectral);
1689
+ }
1690
+ const feDisplace = document.createElementNS(SVG_NS, "feDisplacementMap");
1691
+ feDisplace.setAttribute("in", "blurred");
1692
+ feDisplace.setAttribute("in2", "map");
1693
+ feDisplace.setAttribute("scale", String(scale));
1694
+ feDisplace.setAttribute("xChannelSelector", "R");
1695
+ feDisplace.setAttribute("yChannelSelector", "G");
1696
+ feDisplace.setAttribute("result", "refracted");
1697
+ filter.append(feDisplace);
1698
+ return withSpectral(filter, spectral);
1699
+ }
1700
+ function withSpectral(filter, spectral) {
1701
+ if (!spectral) return filter;
1702
+ const image = document.createElementNS(SVG_NS, "feImage");
1703
+ image.setAttribute("href", spectral.url);
1704
+ image.setAttribute("preserveAspectRatio", "none");
1705
+ image.setAttribute("result", "hue");
1706
+ const product = document.createElementNS(SVG_NS, "feComposite");
1707
+ product.setAttribute("in", "refracted");
1708
+ product.setAttribute("in2", "hue");
1709
+ product.setAttribute("operator", "arithmetic");
1710
+ product.setAttribute("k1", String(spectral.headroom));
1711
+ product.setAttribute("k2", "0");
1712
+ product.setAttribute("k3", "0");
1713
+ product.setAttribute("k4", "0");
1714
+ filter.append(image, product);
1715
+ return filter;
1716
+ }
1717
+ function measureGeometry(el) {
1718
+ const rect = el.getBoundingClientRect();
1719
+ const radius = parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
1720
+ return roundedRectGeometry(rect.width, rect.height, radius);
1721
+ }
1722
+ function devicePixelRatio() {
1723
+ return typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
1724
+ }
1725
+ function fallbackFilterCss(optics) {
1726
+ const blurPx = Math.max(optics.blur, 1.5);
1727
+ const saturate = 1 + optics.colorPickup;
1728
+ return `blur(${blurPx}px) saturate(${saturate})`;
1729
+ }
1730
+ var to2553 = (v) => Math.round(Math.min(Math.max(v, 0), 1) * 255);
1731
+ function rgbCss(r, g, b) {
1732
+ return `rgb(${to2553(r)} ${to2553(g)} ${to2553(b)})`;
1733
+ }
1734
+ function bodyCss(optics, sample, ink, accent) {
1735
+ const body = withPresence(resolveBody(optics, sample, ink), optics, sample);
1736
+ if (accent) {
1737
+ const [r2, g2, b2] = accentTone(accent.color, sample.luma);
1738
+ const amount = Math.max(accentAmount(accent), body.density);
1739
+ return `rgba(${to2553(r2)}, ${to2553(g2)}, ${to2553(b2)}, ${amount.toFixed(3)})`;
1740
+ }
1741
+ const [r, g, b] = ambientFrom(sample);
1742
+ const hue = (c) => body.tintLuma + (c - body.tintLuma) * optics.colorPickup;
1743
+ return `rgba(${to2553(hue(r))}, ${to2553(hue(g))}, ${to2553(hue(b))}, ${body.density.toFixed(3)})`;
1744
+ }
1745
+ function attachGlass(el, opts = {}) {
1746
+ if (typeof document === "undefined") {
1747
+ throw new Error("vireglass/dom: attachGlass requires a browser DOM");
1748
+ }
1749
+ attached.get(el)?.destroy();
1750
+ const id = `vireglass-filter-${filterCounter += 1}`;
1751
+ const svgSupported = supportsSvgBackdropFilter();
1752
+ let wasLight = (opts.material?.ink ?? VIREGLASS_MATERIAL.ink) > 0.5;
1753
+ let filterEl = null;
1754
+ let destroyed = false;
1755
+ let mapKey = "";
1756
+ let morph = null;
1757
+ const onlyWhenTouched = opts.variant === "interactive";
1758
+ let appear = onlyWhenTouched ? 0 : opts.appear ?? 1;
1759
+ let accent = opts.accent;
1760
+ let a11y = systemAccessibility();
1761
+ const stopWatchingA11y = watchAccessibility((next) => {
1762
+ a11y = next;
1763
+ mapKey = "";
1764
+ deform.setElastic(elasticAllowed(opts.accessibility ?? a11y));
1765
+ apply();
1766
+ });
1767
+ warnIfNested(el);
1768
+ ensureRimStyles();
1769
+ el.setAttribute(GLASS_ATTR, "");
1770
+ const positionWasStatic = getComputedStyle(el).position === "static";
1771
+ if (positionWasStatic) el.style.setProperty("position", "relative");
1772
+ function optics(patch) {
1773
+ const base = resolveOptics(patch ?? opts.material);
1774
+ const scaled = applyGlassScale(base, opts.scale ?? GLASS_SCALE_DEFAULT);
1775
+ const settled = applyAccessibility(scaled, opts.accessibility ?? a11y);
1776
+ return applyAppear(settled, appear);
1777
+ }
1778
+ function morphState() {
1779
+ if (!morph) return {};
1780
+ return { smoothing: morph.smoothing, morph: morph.shape, morph2: morph.shape2 };
1781
+ }
1782
+ function morphKey() {
1783
+ if (!morph) return "0";
1784
+ const part = (s) => s ? `${s.offsetX},${s.offsetY},${s.width},${s.height},${s.cornerRadius}` : "-";
1785
+ return `${morph.smoothing}/${part(morph.shape)}/${part(morph.shape2)}`;
1786
+ }
1787
+ function apply() {
1788
+ if (destroyed) return;
1789
+ const geometry = opts.geometry ?? measureGeometry(el);
1790
+ const opticsNow = optics();
1791
+ const sample = opts.sample ?? probeBackdrop(el);
1792
+ if (svgSupported) {
1793
+ 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}`;
1794
+ if (key !== mapKey || !filterEl) {
1795
+ const map = cached(`d|${key}`, () => buildDisplacementMap(opticsNow, geometry, devicePixelRatio(), morphState()));
1796
+ const defs = getFilterHost().querySelector("defs");
1797
+ const hue = hasSpectralEdge(opticsNow) ? cached(`h|${key}`, () => buildSpectralMap(opticsNow, geometry, devicePixelRatio())) : null;
1798
+ const next = buildFilterElement(id, map.url, opticsNow, map.scale, hue);
1799
+ if (filterEl?.parentNode) defs.replaceChild(next, filterEl);
1800
+ else defs.appendChild(next);
1801
+ filterEl = next;
1802
+ mapKey = key;
1803
+ }
1804
+ el.style.setProperty("backdrop-filter", `url(#${id})`);
1805
+ el.style.setProperty("-webkit-backdrop-filter", `url(#${id})`);
1806
+ } else {
1807
+ el.style.setProperty("backdrop-filter", fallbackFilterCss(opticsNow));
1808
+ el.style.setProperty("-webkit-backdrop-filter", fallbackFilterCss(opticsNow));
1809
+ el.style.backgroundColor = bodyCss(opticsNow, sample, wasLight ? 1 : 0, accent);
1810
+ }
1811
+ const light = shouldInkBeLight(sample, wasLight);
1812
+ wasLight = light;
1813
+ const [tr, tg, tb] = ambientFrom(sample);
1814
+ el.style.setProperty("--vireglass-ink", light ? "1" : "0");
1815
+ el.style.setProperty("--vireglass-tint", `${tr} ${tg} ${tb}`);
1816
+ el.style.setProperty("--vireglass-body-density", String(opticsNow.bodyDensity));
1817
+ const inkLevel = Math.round((light ? INK_LIGHT : INK_DARK) * 255);
1818
+ el.style.setProperty("--vireglass-ink-color", `rgb(${inkLevel} ${inkLevel} ${inkLevel})`);
1819
+ el.style.setProperty("--vireglass-tint-color", rgbCss(tr, tg, tb));
1820
+ el.style.setProperty("--vireglass-body-color", bodyCss(opticsNow, sample, light ? 1 : 0, accent));
1821
+ const pole = withPresence(resolveBody(opticsNow, sample, light ? 1 : 0), opticsNow, sample).tintLuma;
1822
+ el.style.setProperty(
1823
+ "--vireglass-rim",
1824
+ (opts.accessibility ?? a11y).increaseContrast ? contrastRimCss(pole, contrastRimLuma(pole)) : rimGradientCss(opticsNow, [tr, tg, tb], opts.light ?? REST_LIGHT, appear)
1825
+ );
1826
+ el.style.setProperty("--vireglass-rim-width", `${RIM_WIDTH_PX}px`);
1827
+ el.style.setProperty("--vireglass-radius", `${geometry.cornerRadius}px`);
1828
+ el.style.setProperty("--vireglass-radius-min", `${concentricRadius(geometry.cornerRadius, geometry.cornerRadius, 0)}px`);
1829
+ const shadow = boxShadowCss(geometry, sample, appear);
1830
+ el.style.setProperty("--vireglass-shadow", shadow);
1831
+ if (opts.shadow !== false) el.style.setProperty("box-shadow", shadow);
1832
+ }
1833
+ apply();
1834
+ const deform = createDeform({ elastic: elasticAllowed(opts.accessibility ?? a11y) });
1835
+ let frame = null;
1836
+ let lastFrameAt = 0;
1837
+ let interacting = false;
1838
+ function localPoint(e) {
1839
+ const rect = el.getBoundingClientRect();
1840
+ return [e.clientX - rect.left - rect.width / 2, e.clientY - rect.top - rect.height / 2];
1841
+ }
1842
+ function paintInteraction(sampleOut) {
1843
+ const geometry = opts.geometry ?? measureGeometry(el);
1844
+ const base = optics();
1845
+ const rise = raiseIntoGlass(sampleOut.press);
1846
+ const risen = optics({ ...opts.material, roughness: (opts.material?.roughness ?? VIREGLASS_MATERIAL.roughness) * rise.solid });
1847
+ const map = buildDisplacementMap(risen, geometry, interacting ? 1 : devicePixelRatio(), {
1848
+ ...morphState(),
1849
+ touch: {
1850
+ x: sampleOut.touchX,
1851
+ y: sampleOut.touchY,
1852
+ pullX: sampleOut.pullX,
1853
+ pullY: sampleOut.pullY,
1854
+ press: sampleOut.press,
1855
+ radius: halfMinDp(geometry) * TOUCH_RADIUS_FRACTION,
1856
+ waveAmp: sampleOut.waveAmp,
1857
+ wavePhase: sampleOut.wavePhase
1858
+ }
1859
+ });
1860
+ if (filterEl) {
1861
+ const feImage = filterEl.querySelector("feImage");
1862
+ const feDisplace = filterEl.querySelector("feDisplacementMap");
1863
+ feImage?.setAttribute("href", map.url);
1864
+ feDisplace?.setAttribute("scale", String(map.scale));
1865
+ }
1866
+ mapKey = "";
1867
+ paintGlow(sampleOut.touchX, sampleOut.touchY, sampleOut.active * base.edgeLight, geometry);
1868
+ const rect = el.getBoundingClientRect();
1869
+ opts.onGlow?.({
1870
+ pageX: rect.left + rect.width / 2 + sampleOut.touchX,
1871
+ pageY: rect.top + rect.height / 2 + sampleOut.touchY,
1872
+ strength: sampleOut.active
1873
+ });
1874
+ }
1875
+ function paintGlow(x, y, strength, geometry) {
1876
+ if (strength <= 1e-3) {
1877
+ el.style.setProperty("--vireglass-glow-opacity", "0");
1878
+ return;
1879
+ }
1880
+ const [gr, gg, gb] = ambientFrom(opts.sample ?? probeBackdrop(el));
1881
+ const cx = (x + geometry.width / 2) / geometry.width * 100;
1882
+ const cy = (y + geometry.height / 2) / geometry.height * 100;
1883
+ const reach = halfMinDp(geometry) * TOUCH_RADIUS_FRACTION;
1884
+ el.style.setProperty(
1885
+ "--vireglass-glow",
1886
+ `radial-gradient(circle ${reach.toFixed(0)}px at ${cx.toFixed(1)}% ${cy.toFixed(1)}%, rgba(${to2553(gr)},${to2553(gg)},${to2553(gb)},1) 0%, rgba(${to2553(gr)},${to2553(gg)},${to2553(gb)},0) 100%)`
1887
+ );
1888
+ el.style.setProperty("--vireglass-glow-opacity", strength.toFixed(3));
1889
+ }
1890
+ function tick(now) {
1891
+ if (destroyed) return;
1892
+ const dt = lastFrameAt ? (now - lastFrameAt) / 1e3 : 1 / 60;
1893
+ lastFrameAt = now;
1894
+ deform.step(dt);
1895
+ const s = deform.sample();
1896
+ if (onlyWhenTouched) appear = s.active;
1897
+ paintInteraction(s);
1898
+ if (deform.idle()) {
1899
+ interacting = false;
1900
+ frame = null;
1901
+ lastFrameAt = 0;
1902
+ el.style.setProperty("--vireglass-glow-opacity", "0");
1903
+ opts.onGlow?.(null);
1904
+ apply();
1905
+ return;
1906
+ }
1907
+ frame = requestAnimationFrame(tick);
1908
+ }
1909
+ function run() {
1910
+ if (frame === null && !destroyed) frame = requestAnimationFrame(tick);
1911
+ }
1912
+ let grabX = 0;
1913
+ let grabY = 0;
1914
+ function onDown(e) {
1915
+ const [x, y] = localPoint(e);
1916
+ const geometry = opts.geometry ?? measureGeometry(el);
1917
+ interacting = true;
1918
+ grabX = x;
1919
+ grabY = y;
1920
+ el.setPointerCapture?.(e.pointerId);
1921
+ deform.grab(x, y, waveImpulse(geometry));
1922
+ run();
1923
+ }
1924
+ function onMove(e) {
1925
+ if (!interacting) return;
1926
+ const [x, y] = localPoint(e);
1927
+ const geometry = opts.geometry ?? measureGeometry(el);
1928
+ deform.drag(x - grabX, y - grabY, halfMinDp(geometry) * MAX_STRETCH);
1929
+ run();
1930
+ }
1931
+ function onUp(e) {
1932
+ if (!interacting) return;
1933
+ const geometry = opts.geometry ?? measureGeometry(el);
1934
+ if (e) el.releasePointerCapture?.(e.pointerId);
1935
+ deform.release(waveImpulse(geometry) * RELEASE_WAVE);
1936
+ run();
1937
+ }
1938
+ if (opts.interactive !== false) {
1939
+ el.addEventListener("pointerdown", onDown);
1940
+ el.addEventListener("pointermove", onMove);
1941
+ el.addEventListener("pointerup", onUp);
1942
+ el.addEventListener("pointercancel", onUp);
1943
+ el.addEventListener("pointerleave", onUp);
1944
+ }
1945
+ let resizeObserver = null;
1946
+ if (!opts.geometry && typeof ResizeObserver !== "undefined") {
1947
+ resizeObserver = new ResizeObserver(() => apply());
1948
+ resizeObserver.observe(el);
1949
+ }
1950
+ function destroy() {
1951
+ destroyed = true;
1952
+ stopWatchingA11y();
1953
+ if (attached.get(el) === handle) attached.delete(el);
1954
+ if (frame !== null) cancelAnimationFrame(frame);
1955
+ frame = null;
1956
+ el.removeEventListener("pointerdown", onDown);
1957
+ el.removeEventListener("pointermove", onMove);
1958
+ el.removeEventListener("pointerup", onUp);
1959
+ el.removeEventListener("pointercancel", onUp);
1960
+ el.removeEventListener("pointerleave", onUp);
1961
+ el.style.removeProperty("--vireglass-glow");
1962
+ el.style.removeProperty("--vireglass-glow-opacity");
1963
+ resizeObserver?.disconnect();
1964
+ resizeObserver = null;
1965
+ if (filterEl?.parentNode) filterEl.parentNode.removeChild(filterEl);
1966
+ filterEl = null;
1967
+ mapKey = "";
1968
+ el.style.removeProperty("backdrop-filter");
1969
+ el.style.removeProperty("-webkit-backdrop-filter");
1970
+ el.style.removeProperty("background-color");
1971
+ el.style.removeProperty("--vireglass-ink");
1972
+ el.style.removeProperty("--vireglass-tint");
1973
+ el.style.removeProperty("--vireglass-body-density");
1974
+ el.style.removeProperty("--vireglass-ink-color");
1975
+ el.style.removeProperty("--vireglass-tint-color");
1976
+ el.style.removeProperty("--vireglass-body-color");
1977
+ el.style.removeProperty("--vireglass-rim");
1978
+ el.style.removeProperty("--vireglass-rim-width");
1979
+ el.style.removeProperty("--vireglass-shadow");
1980
+ el.style.removeProperty("--vireglass-radius");
1981
+ el.style.removeProperty("--vireglass-radius-min");
1982
+ el.style.removeProperty("box-shadow");
1983
+ el.removeAttribute(GLASS_ATTR);
1984
+ if (positionWasStatic) el.style.removeProperty("position");
1985
+ }
1986
+ const handle = {
1987
+ update: apply,
1988
+ setAppear(t) {
1989
+ const next = t < 0 ? 0 : t > 1 ? 1 : t;
1990
+ if (next === appear) return;
1991
+ appear = next;
1992
+ apply();
1993
+ },
1994
+ setAccent(next) {
1995
+ accent = next ?? void 0;
1996
+ apply();
1997
+ },
1998
+ setNeighbourGlow(glow) {
1999
+ if (destroyed || interacting) return;
2000
+ const geometry = opts.geometry ?? measureGeometry(el);
2001
+ if (!glow) {
2002
+ paintGlow(0, 0, 0, geometry);
2003
+ return;
2004
+ }
2005
+ const rect = el.getBoundingClientRect();
2006
+ paintGlow(
2007
+ glow.pageX - (rect.left + rect.width / 2),
2008
+ glow.pageY - (rect.top + rect.height / 2),
2009
+ glow.strength,
2010
+ geometry
2011
+ );
2012
+ },
2013
+ setMorph(next) {
2014
+ morph = next && next.smoothing > 0 ? next : null;
2015
+ mapKey = "";
2016
+ apply();
2017
+ },
2018
+ destroy
2019
+ };
2020
+ attached.set(el, handle);
2021
+ return handle;
2022
+ }
2023
+
2024
+ exports.RIM_WIDTH_PX = RIM_WIDTH_PX;
2025
+ exports.attachGlass = attachGlass;
2026
+ exports.attachScrollEdge = attachScrollEdge;
2027
+ exports.boxShadowCss = boxShadowCss;
2028
+ exports.buildDisplacementMap = buildDisplacementMap;
2029
+ exports.buildSpectralMap = buildSpectralMap;
2030
+ exports.cached = cached;
2031
+ exports.clearMapCache = clearMapCache;
2032
+ exports.compositeLayers = compositeLayers;
2033
+ exports.contrastRimCss = contrastRimCss;
2034
+ exports.createGlassGroup = createGlassGroup;
2035
+ exports.hasSpectralEdge = hasSpectralEdge;
2036
+ exports.lightConicAngle = lightConicAngle;
2037
+ exports.mapCacheSize = mapCacheSize;
2038
+ exports.paintFor = paintFor;
2039
+ exports.parseColor = parseColor;
2040
+ exports.probeBackdrop = probeBackdrop;
2041
+ exports.renderDisplacementPixels = renderDisplacementPixels;
2042
+ exports.renderSpectralPixels = renderSpectralPixels;
2043
+ exports.resetSupportCache = resetSupportCache;
2044
+ exports.resolveBody = resolveBody;
2045
+ exports.rimGradientCss = rimGradientCss;
2046
+ exports.rimLobe = rimLobe;
2047
+ exports.shadowAlphaFrom = shadowAlphaFrom;
2048
+ exports.statsFromSamples = statsFromSamples;
2049
+ exports.supportsBackdropFilter = supportsBackdropFilter;
2050
+ exports.supportsSvgBackdropFilter = supportsSvgBackdropFilter;
2051
+ exports.systemAccessibility = systemAccessibility;
2052
+ exports.watchAccessibility = watchAccessibility;
2053
+ exports.withPresence = withPresence;
2054
+ //# sourceMappingURL=dom.cjs.map
2055
+ //# sourceMappingURL=dom.cjs.map