vireglass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +216 -0
  4. package/android/build.gradle +15 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/expo/modules/glasslens/GlassBackdropView.kt +335 -0
  7. package/android/src/main/java/expo/modules/glasslens/GlassLensModule.kt +49 -0
  8. package/android/src/main/java/expo/modules/glasslens/GlassLensView.kt +463 -0
  9. package/android/src/main/java/expo/modules/glasslens/GlassProbeView.kt +76 -0
  10. package/dist/adaptation-BSbbdExK.d.cts +78 -0
  11. package/dist/adaptation-BSbbdExK.d.ts +78 -0
  12. package/dist/adapters-BPB0D5nr.d.cts +357 -0
  13. package/dist/adapters-BgSc-smX.d.ts +357 -0
  14. package/dist/chunk-HF2AWZEZ.js +1741 -0
  15. package/dist/chunk-HF2AWZEZ.js.map +1 -0
  16. package/dist/chunk-JNRZNMJJ.js +20 -0
  17. package/dist/chunk-JNRZNMJJ.js.map +1 -0
  18. package/dist/index.cjs +2159 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +294 -0
  21. package/dist/index.d.ts +294 -0
  22. package/dist/index.js +286 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/material-CkUHUBoy.d.cts +408 -0
  25. package/dist/material-CkUHUBoy.d.ts +408 -0
  26. package/dist/react.cjs +73 -0
  27. package/dist/react.cjs.map +1 -0
  28. package/dist/react.d.cts +25 -0
  29. package/dist/react.d.ts +25 -0
  30. package/dist/react.js +59 -0
  31. package/dist/react.js.map +1 -0
  32. package/dist/web/index.cjs +1990 -0
  33. package/dist/web/index.cjs.map +1 -0
  34. package/dist/web/index.d.cts +148 -0
  35. package/dist/web/index.d.ts +148 -0
  36. package/dist/web/index.js +657 -0
  37. package/dist/web/index.js.map +1 -0
  38. package/expo-module.config.json +6 -0
  39. package/package.json +127 -0
  40. package/src/accessibility.ts +51 -0
  41. package/src/adaptation.ts +85 -0
  42. package/src/adapters.ts +362 -0
  43. package/src/concentric.ts +25 -0
  44. package/src/geometry.ts +151 -0
  45. package/src/glass-scale.ts +37 -0
  46. package/src/group-model.ts +224 -0
  47. package/src/index.ts +17 -0
  48. package/src/lens-shader.ts +547 -0
  49. package/src/material.ts +553 -0
  50. package/src/native/environment.ts +70 -0
  51. package/src/native/glass-group.tsx +107 -0
  52. package/src/native/glass-ink.tsx +44 -0
  53. package/src/native/glass-surface.tsx +477 -0
  54. package/src/native/index.ts +18 -0
  55. package/src/native/lens.ts +106 -0
  56. package/src/native/provider.tsx +58 -0
  57. package/src/native/surface-registry.ts +51 -0
  58. package/src/native/system-accessibility.ts +90 -0
  59. package/src/optics.ts +177 -0
  60. package/src/react.ts +102 -0
  61. package/src/reference-scene.ts +222 -0
  62. package/src/scroll-edge.ts +30 -0
  63. package/src/sdf.ts +149 -0
  64. package/src/surface-shader.ts +287 -0
  65. package/src/targets/agsl.ts +8 -0
  66. package/src/targets/glsl.ts +140 -0
  67. package/src/touch-response.ts +195 -0
  68. package/src/web/gl.ts +119 -0
  69. package/src/web/index.ts +3 -0
  70. package/src/web/probe.ts +312 -0
  71. package/src/web/reference-draw.ts +142 -0
  72. package/src/web/renderer.ts +524 -0
@@ -0,0 +1,294 @@
1
+ import { V as VireGlassOptics } from './material-CkUHUBoy.cjs';
2
+ export { A as ADAPT_RADIUS, a as ALL_EFFECTS_ON, D as DEBUG_MODES, E as EFFECTS, L as LEGACY_NAMES, b as LEGACY_OPTICS, c as LegacyOpticsName, M as MATERIAL_PRESETS, d as MATERIAL_RANGES, e as MaterialPresetName, P as PRESET_NAMES, f as PRODUCT_DIM, g as VIREGLASS_CLEAR_MATERIAL, h as VIREGLASS_CONTROL_MATERIAL, i as VIREGLASS_LYRICS_MATERIAL, j as VIREGLASS_MATERIAL, k as VIREGLASS_MATERIAL_V4, l as VIREGLASS_MATERIAL_V5, m as VIREGLASS_SHEET_MATERIAL, n as VireGlassDebugMode, o as VireGlassEffect, p as VireGlassMaterial, q as VireGlassNumericKey, r as VireGlassTint, s as VireGlassToggles, t as activeMaterial, u as applyToggles, v as debugIndex, w as materialForInk, x as resolveMaterial, y as resolveOptics } from './material-CkUHUBoy.cjs';
3
+ export { A as ACCENT_AMOUNT, D as DYNAMIC_UNIFORMS, I as ICON_UNIFORMS, M as MAX_BEVEL_FRACTION, a as MAX_STRETCH, O as OVERLAY_UNIFORMS, R as REFERENCE_PIECE_AT, b as REFERENCE_SCENES, c as REFERENCE_SCENE_HEIGHT, d as REFERENCE_SCENE_WIDTH, e as REFERENCE_SHAPES, f as REFERENCE_SURROUND, g as REST_LIGHT, h as RIM_FRACTION, S as SURFACE_LENGTH_UNIFORMS, i as SurfaceUniforms, V as VireGlassAccent, j as VireGlassGeometry, k as VireGlassMorph, l as VireGlassRefBand, m as VireGlassRefLayer, n as VireGlassRefScene, o as VireGlassRefSceneName, p as VireGlassRefShapeName, q as VireGlassTouch, r as bevelDp, s as bevelFraction, t as capsuleGeometry, u as circleGeometry, v as halfMinDp, w as lensMagnify, x as lensPadDp, y as morphBetween, z as morphReachDp, B as refCheckerCells, C as refGradientSteps, E as refGray, F as referenceScene, G as rimDp, H as roundedRectGeometry, J as shadowOpacity, K as shadowOpacityFrom, L as shadowReachDp, N as sizeGain, P as surfacePadDp, Q as thicknessDp, T as toDeviceSurfaceUniforms, U as toLensProps, W as toSurfaceUniforms } from './adapters-BPB0D5nr.cjs';
4
+ import { B as BackdropSample } from './adaptation-BSbbdExK.cjs';
5
+ export { C as CONFIRMATIONS, F as FADE_MS, a as FLIP_LUMA, I as INK_DARK, b as INK_LIGHT, R as RETURN_LUMA, c as ambientFrom, d as decisiveLuma, s as shouldInkBeLight } from './adaptation-BSbbdExK.cjs';
6
+
7
+ /** Reflectance at normal incidence: ((n−1)/(n+1))². For glass, n=1.5 gives 0.04. */
8
+ declare const fresnelF0: (ior: number) => number;
9
+ declare const fresnelStrength: (ior: number) => number;
10
+ /** Exponent in the Schlick approximation — a constant of the model, not a knob. */
11
+ declare const FRESNEL_EXPONENT = 5;
12
+ /** Strength of ray bending. As n→1 the medium is indistinguishable from air; n=1.6 is taken as
13
+ * full strength. */
14
+ declare const refractionStrength: (ior: number) => number;
15
+ declare const refractionScale: (ior: number, thicknessDp: number) => number;
16
+ declare const dispersion: (ior: number) => number;
17
+ declare const iorSpread: (ior: number) => number;
18
+ declare const absorption: (pathDp: number) => number;
19
+ /**
20
+ * Density at the bevel relative to the body. Not a separate quantity: at the rim the ray travels
21
+ * LONGER through the medium by the bevel's width, and absorption over that path is greater. This
22
+ * used to be a separate `edgeDensity` slider, which is how a thin bevel with a milky rim got
23
+ * built — a state that doesn't occur in real glass.
24
+ */
25
+ declare const edgeDensity: (thicknessDp: number, bevelDp: number) => number;
26
+ declare const blur: (roughness: number) => number;
27
+ /** A smooth surface gives a narrow highlight, a rough one a spread-out one. */
28
+ declare const specularPower: (roughness: number) => number;
29
+ /** Highlight brightness — the same reflectance as Fresnel, damped by roughness. */
30
+ declare const specularStrength: (ior: number, roughness: number) => number;
31
+ declare function mediumTint(ior: number): {
32
+ r: number;
33
+ g: number;
34
+ b: number;
35
+ };
36
+ declare const gatherRadius: (bevelDp: number) => number;
37
+ declare const bodyDensity: (thicknessDp: number) => number;
38
+ /** How much the rim picks up the light and color of its surroundings. The same reflectance as
39
+ * Fresnel: no separate knob — a dense medium returns more of its surroundings by construction. */
40
+ declare const edgeLight: (ior: number) => number;
41
+ /**
42
+ * THE SPECTRAL PART. Three phenomena — dispersion, diffraction and interference — share one
43
+ * cause: index of refraction and phase both depend on wavelength. So there are no separate
44
+ * "rainbow strength" knobs here: everything is derived from the medium's density and film
45
+ * thickness.
46
+ */
47
+ /**
48
+ * Rim iridescence is visible exactly as much as the REFLECTION is bright: interference lives in
49
+ * the reflected ray, not the transmitted one. Hence the tie to Fresnel — there's no strength knob
50
+ * of its own.
51
+ */
52
+ declare const iridescence: (ior: number, filmNm: number) => number;
53
+ /**
54
+ * Edge diffraction. Same λ-dependence as dispersion, so it shares its strength; the bevel sets
55
+ * the fringe frequency — wide bevels give sparse fringes, sharp ones frequent (in the shader this
56
+ * is divided by bevel width). The 0.5 normalization reflects the stylized render: at the physical
57
+ * amplitude the fringes aren't visible at all.
58
+ */
59
+ declare const diffraction: (ior: number) => number;
60
+ /**
61
+ * How much the body picks up the COLOR of its surroundings. This is multiple internal reflection:
62
+ * the denser the medium, the more light it circulates inside itself, and the more strongly it's
63
+ * tinted by what's around it. The ceiling is deliberately low — the tint has to stay a medium, not
64
+ * become a flat color fill.
65
+ */
66
+ declare const colorPickup: (ior: number) => number;
67
+
68
+ declare const VG_SDF = "\nfloat vgRoundRect(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - corner;\n}\n\nfloat vgSmin(float a, float b, float k) {\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n return mix(b, a, h) - k * h * (1.0 - h);\n}\n\n// A scene is a single shape, and when k > 0 a smooth union with one or two neighbors. A third\n// shape exists to break the control into pieces (docs/reference.md \u00A75): there are two bridges\n// there. It's turned off with a zero size, not its own k: the fused body shares one bridge.\nfloat vgScene(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float a = vgRoundRect(p, halfSize, corner);\n if (k <= 0.0) { return a; }\n float ab = vgSmin(a, vgRoundRect(p - offsetB, halfB, cornerB), k);\n if (halfC.x <= 0.0) { return ab; }\n return vgSmin(ab, vgRoundRect(p - offsetC, halfC, cornerC), k);\n}\n\n// Response to a finger. It's the FIELD around the touch point that deforms, not the shape's\n// bounding box: scaling width to pull the right edge would also pull the left one \u2014 liquid\n// doesn't behave that way. Here the displacement falls off with distance from the finger, so the\n// far edge stays put.\n//\n// The order of the terms matters: drag shifts the field, press pulls it toward the finger, the\n// wave rides on top of the already-shifted field \u2014 otherwise the ripple decouples from the\n// deformation and lives a life of its own.\nfloat2 vgTouchWarp(float2 p, float2 touch, float2 pull, float press, float radius,\n float waveAmp, float wavePhase) {\n if (radius <= 0.0) { return p; }\n // Under the finger the whole element grows (M 3:51; HIG: interactive \"expands\").\n p /= 1.0 + 0.06 * press;\n float2 d = p - touch;\n float r = length(d);\n\n // The finger is a BLOB, not a point, but not a hard stamp either: a wide peak, soft edges. A\n // flat plateau with a sharp cutoff at the edge shifts the region as a block, and a straight\n // line under the glass breaks into a step \u2014 the deformation reads as rectangular even though\n // the finger is round. A double smoothstep gives the same contact width without that step.\n //\n // The influence cuts off at the radius rather than fading into an infinite tail like a\n // Gaussian: with a tail the deformation spreads into a \"rubber mattress\" bulge.\n float k = clamp((radius - r) / radius, 0.0, 1.0);\n float s = k * k * (3.0 - 2.0 * k);\n float core = s * s * (3.0 - 2.0 * s);\n // A ring around the blob: material displaced from under the finger has to end up somewhere.\n // Without this ridge the shape just balloons, and a dense medium doesn't behave that way.\n float rim = k * k * (1.0 - k) * 4.0;\n\n float2 q = p - pull * (core - 0.42 * rim);\n q -= d * (press * 0.16 * core);\n\n // The wave has its own scale, twice the drag radius: the ripple has to reach the far edge,\n // otherwise it reads as jitter under the finger rather than a wave across the surface. The\n // wavelength is short: in a dense medium ripples are frequent and small, long shallow swells\n // are water.\n if (waveAmp > 0.0 && r > 0.0001) {\n float span = radius * 2.0;\n float ring = sin(r / (span * 0.17) - wavePhase * 6.2831853) * exp(-r / (span * 0.7));\n q -= (d / r) * ring * waveAmp;\n }\n return q;\n}\n\n// Analytical normal of a single shape: radial on the rounded part, axis-aligned on the straight\n// segments.\nfloat2 vgRoundRectNormal(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n float2 g = (q.x > 0.0 && q.y > 0.0)\n ? normalize(max(q, float2(0.0001)))\n : (q.x > q.y ? float2(1.0, 0.0) : float2(0.0, 1.0));\n return g * sign(p);\n}\n\n// Normal of the union \u2014 ANALYTICAL, no more finite differences here.\n//\n// Derivation: smin has two terms, mix(b,a,h) and \u2212k\u00B7h\u00B7(1\u2212h). Their derivatives with respect to h\n// contain a factor of (1\u22122h) with opposite signs and cancel exactly, because h is linear in\n// (b\u2212a)/k. What's left is mix(\u2207b, \u2207a, h) \u2014 that is, the two shapes' normals, blended with the\n// same weight that blends the distances themselves.\n//\n// The previous version took four ADDITIONAL scene evaluations per pixel (each one \u2014 two shapes\n// plus smin) and was still an approximation. Here there are two shapes, exactly.\nfloat2 vgSceneNormal(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float2 na = vgRoundRectNormal(p, halfSize, corner);\n if (k <= 0.0) { return na; }\n float2 q = p - offsetB;\n float a = vgRoundRect(p, halfSize, corner);\n float b = vgRoundRect(q, halfB, cornerB);\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n float2 nab = normalize(mix(vgRoundRectNormal(q, halfB, cornerB), na, h) + float2(1e-5, 1e-5));\n if (halfC.x <= 0.0) { return nab; }\n // Same derivation as for the pair: normals blend with the same weight as the distances.\n float2 qc = p - offsetC;\n float ab = vgSmin(a, b, k);\n float c = vgRoundRect(qc, halfC, cornerC);\n float hc = clamp(0.5 + 0.5 * (c - ab) / k, 0.0, 1.0);\n return normalize(mix(vgRoundRectNormal(qc, halfC, cornerC), nab, hc) + float2(1e-5, 1e-5));\n}\n\n// Position within the bevel: 0 is the flat middle, 1 is the very edge. This one value feeds the\n// mask, thickness, refraction, aberrations and the width of the light rim.\nfloat vgBevelT(float sd, float bevel) {\n return clamp((sd + bevel) / bevel, 0.0, 1.0);\n}\n\n// Slope of the bevel profile \u2014 spherical: t / sqrt(1 - t\u00B2), like a spherical cap. The previous t\u00B2\n// kept the slope near zero through almost the whole bevel and then shot up right at the edge, so\n// all the optics bunched into a narrow ring and the element read as a PUCK \u2014 a flat top and a\n// wall around the rim. Here curvature is spread across the bevel, and the rim stops being a line.\n//\n// This still doesn't control the field of view: only the bevel bends, the middle stays flat,\n// otherwise the surface reads as a soap bubble.\nfloat vgBevelSlope(float t) {\n return min(t * inversesqrt(max(1.0 - t * t * 0.94, 0.02)), 3.2);\n}\n\n// PROGRESS FRACTION. Progress is an active state turned into a FIELD: to the left of the boundary\n// the element is active, to the right it stays regular glass, and the boundary moves. A negative\n// value turns the field off entirely \u2014 zero is taken by the start of the track and can't also\n// mean off.\n//\n// The boundary is SOFT. A hard seam reads as two different materials glued together rather than\n// two states of one; the transition width is a fraction of the half-width, so it doesn't read as\n// a thread on a large element and doesn't swallow a small one whole.\nfloat vgProgress(float2 p, float2 halfSize, float progress) {\n if (progress < 0.0) { return 0.0; }\n float edge = mix(-halfSize.x, halfSize.x, clamp(progress, 0.0, 1.0));\n float soft = max(halfSize.x * 0.05, 1.0);\n return 1.0 - smoothstep(edge - soft, edge + soft, p.x);\n}\n";
69
+ /** Rate at which the offset builds toward the rim. Shared between the lens (actual sampling
70
+ * offset) and the surface (visualizing the displacement field in debug mode) — otherwise they'd
71
+ * drift apart. */
72
+ declare const VG_FALLOFF = 2.6;
73
+
74
+ type DeformSample = {
75
+ /** Touch point relative to the element's center, CSS pixels. */
76
+ touchX: number;
77
+ touchY: number;
78
+ pullX: number;
79
+ pullY: number;
80
+ press: number;
81
+ active: number;
82
+ /** Wave amplitude in CSS pixels and its phase in turns. */
83
+ waveAmp: number;
84
+ wavePhase: number;
85
+ };
86
+ declare function createDeform(): {
87
+ grab: (x: number, y: number, waveStart: number) => void;
88
+ drag: (dx: number, dy: number, limit: number) => void;
89
+ release: (waveStart: number) => void;
90
+ step: (dt: number) => void;
91
+ idle: () => boolean;
92
+ sample: () => DeformSample;
93
+ };
94
+ /**
95
+ * RAISING INTO GLASS (reference §5, M 4:10–4:30). A control that isn't glass at rest — a switch
96
+ * knob, a slider handle — stops being frosted under the finger and becomes a lens: whatever is
97
+ * beneath it shows through.
98
+ *
99
+ * Glass comes in FASTER than frost leaves: the two fractions overlap, otherwise midway through
100
+ * the transition the backdrop flashes through the knob for an instant — a hole where the control
101
+ * should be.
102
+ *
103
+ * Lifting off the backdrop is NOT part of this. The shader's `u_lift` is a direction ("this kind
104
+ * of element sinks in or rises"), not a magnitude — how far it has already risen is carried by
105
+ * the press value that `u_lift` gets multiplied by. Returning a third fraction here would mean
106
+ * multiplying press by itself — which is exactly what happened the first time the web bench wired
107
+ * this up.
108
+ */
109
+ declare function raiseIntoGlass(press: number): {
110
+ glass: number;
111
+ solid: number;
112
+ };
113
+
114
+ declare const LENS_SHADER = "\nuniform shader content;\n\n// Known only to the view itself: its own size and its own place on screen.\nuniform float2 u_center;\nuniform float u_reach;\nuniform float2 u_contentMin;\nuniform float2 u_contentMax;\n// Backdrop estimate UNDER the glass from the native probe: lightness, variegation and average\n// color, already smoothed over time. Negative lightness means the probe hasn't reported yet \u2014\n// fall back to our own samples.\nuniform float u_probeLuma;\nuniform float u_probeBusy;\n// The endpoints of the lightness range under the glass: right at a black/white border the\n// average reads as a gray that says \"everything's fine,\" while the ink drowns over the light\n// half.\nuniform float2 u_probeRange;\n// Slope of lightness across the surface, fraction per half-size: tinting is a gradient \u2014 one\n// density per element wouldn't separate the ink from both halves of the backdrop.\nuniform float2 u_probeSlope;\nuniform float3 u_probe;\n\n// Material \u2014 over the shared channel from JS, already in device pixels.\nuniform float2 u_halfSize;\nuniform float u_corner;\nuniform float u_bevel;\nuniform float u_thick;\nuniform float u_rim;\nuniform float u_ior;\nuniform float u_iorSpread;\nuniform float2 u_light;\nuniform float u_appear;\nuniform float4 u_accent;\nuniform float u_frost;\nuniform float u_ink;\nuniform float u_legibility;\nuniform float u_dim;\nuniform float u_presence;\nuniform float u_progress;\nuniform float u_adaptRadius;\nuniform float u_bodyDensity;\nuniform float u_edgeLight;\nuniform float u_fresnel;\nuniform float u_specular;\nuniform float u_reflectReach;\nuniform float u_film;\nuniform float u_iridescence;\nuniform float u_diffraction;\nuniform float u_colorPickup;\nuniform float2 u_morphOffset;\nuniform float2 u_morphHalf;\nuniform float u_morphCorner;\nuniform float u_morphK;\nuniform float2 u_morph2Offset;\nuniform float2 u_morph2Half;\nuniform float u_morph2Corner;\nuniform float u_debug;\nuniform float2 u_touch;\nuniform float2 u_pull;\nuniform float u_touchPress;\nuniform float u_touchRadius;\nuniform float2 u_wave;\n\n\nfloat vgRoundRect(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - corner;\n}\n\nfloat vgSmin(float a, float b, float k) {\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n return mix(b, a, h) - k * h * (1.0 - h);\n}\n\n// A scene is a single shape, and when k > 0 a smooth union with one or two neighbors. A third\n// shape exists to break the control into pieces (docs/reference.md \u00A75): there are two bridges\n// there. It's turned off with a zero size, not its own k: the fused body shares one bridge.\nfloat vgScene(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float a = vgRoundRect(p, halfSize, corner);\n if (k <= 0.0) { return a; }\n float ab = vgSmin(a, vgRoundRect(p - offsetB, halfB, cornerB), k);\n if (halfC.x <= 0.0) { return ab; }\n return vgSmin(ab, vgRoundRect(p - offsetC, halfC, cornerC), k);\n}\n\n// Response to a finger. It's the FIELD around the touch point that deforms, not the shape's\n// bounding box: scaling width to pull the right edge would also pull the left one \u2014 liquid\n// doesn't behave that way. Here the displacement falls off with distance from the finger, so the\n// far edge stays put.\n//\n// The order of the terms matters: drag shifts the field, press pulls it toward the finger, the\n// wave rides on top of the already-shifted field \u2014 otherwise the ripple decouples from the\n// deformation and lives a life of its own.\nfloat2 vgTouchWarp(float2 p, float2 touch, float2 pull, float press, float radius,\n float waveAmp, float wavePhase) {\n if (radius <= 0.0) { return p; }\n // Under the finger the whole element grows (M 3:51; HIG: interactive \"expands\").\n p /= 1.0 + 0.06 * press;\n float2 d = p - touch;\n float r = length(d);\n\n // The finger is a BLOB, not a point, but not a hard stamp either: a wide peak, soft edges. A\n // flat plateau with a sharp cutoff at the edge shifts the region as a block, and a straight\n // line under the glass breaks into a step \u2014 the deformation reads as rectangular even though\n // the finger is round. A double smoothstep gives the same contact width without that step.\n //\n // The influence cuts off at the radius rather than fading into an infinite tail like a\n // Gaussian: with a tail the deformation spreads into a \"rubber mattress\" bulge.\n float k = clamp((radius - r) / radius, 0.0, 1.0);\n float s = k * k * (3.0 - 2.0 * k);\n float core = s * s * (3.0 - 2.0 * s);\n // A ring around the blob: material displaced from under the finger has to end up somewhere.\n // Without this ridge the shape just balloons, and a dense medium doesn't behave that way.\n float rim = k * k * (1.0 - k) * 4.0;\n\n float2 q = p - pull * (core - 0.42 * rim);\n q -= d * (press * 0.16 * core);\n\n // The wave has its own scale, twice the drag radius: the ripple has to reach the far edge,\n // otherwise it reads as jitter under the finger rather than a wave across the surface. The\n // wavelength is short: in a dense medium ripples are frequent and small, long shallow swells\n // are water.\n if (waveAmp > 0.0 && r > 0.0001) {\n float span = radius * 2.0;\n float ring = sin(r / (span * 0.17) - wavePhase * 6.2831853) * exp(-r / (span * 0.7));\n q -= (d / r) * ring * waveAmp;\n }\n return q;\n}\n\n// Analytical normal of a single shape: radial on the rounded part, axis-aligned on the straight\n// segments.\nfloat2 vgRoundRectNormal(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n float2 g = (q.x > 0.0 && q.y > 0.0)\n ? normalize(max(q, float2(0.0001)))\n : (q.x > q.y ? float2(1.0, 0.0) : float2(0.0, 1.0));\n return g * sign(p);\n}\n\n// Normal of the union \u2014 ANALYTICAL, no more finite differences here.\n//\n// Derivation: smin has two terms, mix(b,a,h) and \u2212k\u00B7h\u00B7(1\u2212h). Their derivatives with respect to h\n// contain a factor of (1\u22122h) with opposite signs and cancel exactly, because h is linear in\n// (b\u2212a)/k. What's left is mix(\u2207b, \u2207a, h) \u2014 that is, the two shapes' normals, blended with the\n// same weight that blends the distances themselves.\n//\n// The previous version took four ADDITIONAL scene evaluations per pixel (each one \u2014 two shapes\n// plus smin) and was still an approximation. Here there are two shapes, exactly.\nfloat2 vgSceneNormal(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float2 na = vgRoundRectNormal(p, halfSize, corner);\n if (k <= 0.0) { return na; }\n float2 q = p - offsetB;\n float a = vgRoundRect(p, halfSize, corner);\n float b = vgRoundRect(q, halfB, cornerB);\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n float2 nab = normalize(mix(vgRoundRectNormal(q, halfB, cornerB), na, h) + float2(1e-5, 1e-5));\n if (halfC.x <= 0.0) { return nab; }\n // Same derivation as for the pair: normals blend with the same weight as the distances.\n float2 qc = p - offsetC;\n float ab = vgSmin(a, b, k);\n float c = vgRoundRect(qc, halfC, cornerC);\n float hc = clamp(0.5 + 0.5 * (c - ab) / k, 0.0, 1.0);\n return normalize(mix(vgRoundRectNormal(qc, halfC, cornerC), nab, hc) + float2(1e-5, 1e-5));\n}\n\n// Position within the bevel: 0 is the flat middle, 1 is the very edge. This one value feeds the\n// mask, thickness, refraction, aberrations and the width of the light rim.\nfloat vgBevelT(float sd, float bevel) {\n return clamp((sd + bevel) / bevel, 0.0, 1.0);\n}\n\n// Slope of the bevel profile \u2014 spherical: t / sqrt(1 - t\u00B2), like a spherical cap. The previous t\u00B2\n// kept the slope near zero through almost the whole bevel and then shot up right at the edge, so\n// all the optics bunched into a narrow ring and the element read as a PUCK \u2014 a flat top and a\n// wall around the rim. Here curvature is spread across the bevel, and the rim stops being a line.\n//\n// This still doesn't control the field of view: only the bevel bends, the middle stays flat,\n// otherwise the surface reads as a soap bubble.\nfloat vgBevelSlope(float t) {\n return min(t * inversesqrt(max(1.0 - t * t * 0.94, 0.02)), 3.2);\n}\n\n// PROGRESS FRACTION. Progress is an active state turned into a FIELD: to the left of the boundary\n// the element is active, to the right it stays regular glass, and the boundary moves. A negative\n// value turns the field off entirely \u2014 zero is taken by the start of the track and can't also\n// mean off.\n//\n// The boundary is SOFT. A hard seam reads as two different materials glued together rather than\n// two states of one; the transition width is a fraction of the half-width, so it doesn't read as\n// a thread on a large element and doesn't swallow a small one whole.\nfloat vgProgress(float2 p, float2 halfSize, float progress) {\n if (progress < 0.0) { return 0.0; }\n float edge = mix(-halfSize.x, halfSize.x, clamp(progress, 0.0, 1.0));\n float soft = max(halfSize.x * 0.05, 1.0);\n return 1.0 - smoothstep(edge - soft, edge + soft, p.x);\n}\n\n\n// Sample count for the disc gather: fewer than this reads as a staircase of copies at a sharp edge.\nconst int VG_FROST_TAPS = 20;\n// The sample count grows with the disc's area: estimator noise is the spread under the glass\n// divided by the square root of the sample count. The ceiling is cost: texture sampling is the\n// most expensive thing here.\nconst int VG_FROST_TAPS_MAX = 64;\n// The radius correction for backdrop compression at the rim is capped: at the silhouette it goes\n// to infinity.\nconst float VG_FOOTPRINT_MAX = 1.8;\nconst float VG_TAU = 6.28318530718;\n// Ceiling on body lightness under light ink (and the mirrored floor under dark ink). This is a\n// ceiling, not a difference: \"0.14 darker than white\" is a lightness of 0.86, at which white text\n// is invisible.\nconst float VG_BODY_CAP_LOOSE = 0.62;\nconst float VG_BODY_CAP_TIGHT = 0.38;\n// Tint lightness in both directions: real glass is never coal-black or paper-white.\nconst float VG_TINT_DARK = 0.07;\nconst float VG_TINT_LIGHT = 0.94;\n// Reference channel wavelengths, nm \u2014 for diffraction and interference.\nconst float3 VG_LAMBDA = float3(610.0, 550.0, 460.0);\n// Film index of refraction: every thin film on glass sits around this value.\nconst float VG_FILM_IOR = 1.35;\n// Ceiling on the profile's slope right at the silhouette: it goes to infinity there.\nconst float VG_SLOPE_MAX = 40.0;\n// Fraction of the gather radius that scattering reaches over a busy backdrop under ink (M 11:47).\n// Set against the reference: there, structure under the capsule fades by a factor of 9-10, not\n// thirty.\nconst float VG_SCATTER_MAX = 0.20;\nconst float VG_SCATTER_BASE = 0.1;\n// The response to structure under the glass saturates early: what competes with the ink isn't\n// the area of foreign text but the mere fact that it's there (a line under a tile gives a busy\n// of about 0.12).\nconst float VG_STRUCTURE_GAIN = 20.0;\n// How much denser the body with ink gets over a BUSY backdrop than over a calm one. There's no\n// flat floor here: legibility picks up its own targeted requirement further down, and base\n// frosting comes from VG_MATTE_LIFT.\nconst float VG_GROUND_SPAN = 0.06;\n// Scattered light on a frosted element is an ADDITION on top of the backdrop, not a fraction of\n// the way to the tint: the fraction goes to zero once the canvas reaches the tint's lightness,\n// and darkens past it (docs/benchmarks.md).\nconst float VG_MATTE_LIFT = 0.10;\n// The fraction of backdrop spread that survives through to the body past scattering: the\n// legibility requirement is computed from this edge, not from the spot's average lightness.\nconst float VG_BUSY_EDGE = 0.75;\n// Light concentration: the body is a touch lighter than what's beneath it (M 2:29).\nconst float VG_CONCENTRATE = 0.01;\n// The lightness the medium pulls content under the glass toward, and the strength of that pull.\n// The medium both removes light and mixes in scattered light: over a light backdrop the body\n// darkens, over a dark one it lightens.\nconst float VG_MEDIUM_LUMA = 0.40;\nconst float VG_MEDIUM_PULL = 0.07;\n// How much ambient light reaches the body on top of the pull.\nconst float VG_AMBIENT_SPILL = 0.01;\n\nfloat vgLuma(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }\n\n// Un-premultiply into ordinary color. One division, done in float: near zero, half's step is too\n// coarse, and several divisions in a row raise noise.\nfloat3 vgUnpack(half4 c) {\n float a = float(c.a);\n return a > 0.004 ? float3(c.rgb) / a : float3(0.0);\n}\n\n// The capture ends at the screen edge: samples are clamped to the rectangle where content\n// actually exists, otherwise full-width glass picked up a band of emptiness along its edges.\nfloat2 vgInContent(float2 q) { return clamp(q, u_contentMin, u_contentMax); }\n\n// Hue without lightness. Below the threshold the color has no hue, and the division blows up.\nfloat3 vgHue(float3 c) {\n float l = vgLuma(c);\n if (l < 0.02) { return float3(1.0); }\n return clamp(c / l, float3(0.0), float3(2.0));\n}\n\n// Edge profile \u2014 a convex squircle: x = 0 at the silhouette, 1 where the rounding turns flat.\nfloat vgRimQ(float x) {\n float k = 1.0 - x;\n return sqrt(sqrt(max(1.0 - k * k * k * k, 0.0)));\n}\n\nfloat vgRimQd(float x) {\n float k = 1.0 - x;\n return k * k * k / pow(max(1.0 - k * k * k * k, 1e-5), 0.75);\n}\n\n// Glass height above the content at distance e from the silhouette.\nfloat vgHeight(float e, float w) {\n return mix(u_rim, u_thick, vgRimQ(clamp(e / w, 0.0, 1.0)));\n}\n\n// Slope of the top face at the same point: the derivative of height with respect to distance\n// from the silhouette.\nfloat vgSlope(float e, float w) {\n if (e >= w) { return 0.0; }\n return min((u_thick - u_rim) / w * vgRimQd(clamp(e / w, 0.0, 1.0)), VG_SLOPE_MAX);\n}\n\n// Where a ray falling from above lands on a face with normal N: Snell's law refraction, then a\n// path of length z through the medium to the content. The convex face bends the ray toward the\n// normal \u2014 that is, inward relative to the shape.\nfloat2 vgShift(float3 N, float z, float ior) {\n float3 T = refract(float3(0.0, 0.0, -1.0), N, 1.0 / ior);\n return z * T.xy / max(-T.z, 0.05);\n}\n\n// Thin-film interference: optical path difference 2\u00B7n\u00B7d\u00B7cos\u03B8t, each channel with its own \u03BB.\n// Returns a hue normalized to its own average \u2014 it colors, but doesn't lighten.\nfloat3 vgInterference(float cosI) {\n float sinT2 = (1.0 - cosI * cosI) / (VG_FILM_IOR * VG_FILM_IOR);\n float cosT = sqrt(max(1.0 - sinT2, 0.0));\n float opd = 2.0 * VG_FILM_IOR * u_film * cosT;\n float3 i = 0.5 + 0.5 * cos(VG_TAU * opd / VG_LAMBDA + 3.14159265);\n return i / max((i.r + i.g + i.b) / 3.0, 0.001);\n}\n\n// Edge diffraction: fringes get denser the sharper the bevel. Also a hue, not a brightness.\nfloat3 vgDiffraction(float distFromEdge, float bevel) {\n float phase = VG_TAU * 4.0 * distFromEdge / max(bevel, 1.0);\n float3 d = 0.5 + 0.5 * cos(phase * (550.0 / VG_LAMBDA));\n return d / max((d.r + d.g + d.b) / 3.0, 0.001);\n}\n\nfloat vgHash(float2 p) {\n return fract(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);\n}\n\n// Soft compression instead of a hard clamp: over a white backdrop, additions used to knock a\n// channel to one, and the glass turned into a flat blob with no detail.\nfloat3 vgSoftClip(float3 c) {\n float3 over = max(c - 0.86, float3(0.0));\n return min(c, float3(0.86)) + over * 0.14 / (0.14 + over);\n}\n\n// Our own blur: the capture is left clean, the shader does the blurring \u2014 averaging in float\n// smooths banding on its own and adds no noise of its own. A golden-angle spiral, rotated to its\n// own angle at every pixel: with a shared angle, neighboring pixels took the same samples, and\n// texture under the glass read in clumps. Accumulated premultiplied \u2014 one shared un-premultiply\n// at the end.\nfloat4 vgGather(float2 q, float radius, float2 seed) {\n float4 acc = float4(content.eval(vgInContent(q)));\n if (radius <= 0.25) { return acc; }\n if (radius < 4.0) {\n float r = radius * 0.7;\n acc += float4(content.eval(vgInContent(q + float2(r, 0.0))))\n + float4(content.eval(vgInContent(q - float2(r, 0.0))))\n + float4(content.eval(vgInContent(q + float2(0.0, r))))\n + float4(content.eval(vgInContent(q - float2(0.0, r))));\n return acc * 0.2;\n }\n float ca = cos(2.39996323);\n float sa = sin(2.39996323);\n float a0 = vgHash(seed) * 6.28318530718;\n float2 dir = float2(cos(a0), sin(a0));\n float wide = radius * 0.5;\n int taps = int(clamp(float(VG_FROST_TAPS) * wide * wide, float(VG_FROST_TAPS), float(VG_FROST_TAPS_MAX)));\n for (int i = 1; i <= VG_FROST_TAPS_MAX; i++) {\n if (i > taps) { break; }\n dir = float2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);\n float r = radius * sqrt(float(i) / float(taps));\n acc += float4(content.eval(vgInContent(q + dir * r)));\n }\n return acc / float(taps + 1);\n}\n\nhalf4 main(float2 xy) {\n float2 p = vgTouchWarp(xy - u_center, u_touch, u_pull, u_touchPress, u_touchRadius, u_wave.x, u_wave.y);\n float sd = vgScene(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,\n u_morph2Offset, u_morph2Half, u_morph2Corner);\n // There's nothing outside the glass \u2014 except the light it spills onto the backdrop under the\n // finger (M 3:38): the same concentrated ambient as inside, so no white halo appears over a\n // dark backdrop.\n if (sd > 1.0) {\n float spill = u_touchPress * exp(-sd / max(min(u_halfSize.x, u_halfSize.y) * 0.5, 4.0)) * 0.5 * u_appear;\n if (spill < 0.004) { return half4(0.0); }\n float3 lit = clamp((u_probeLuma >= 0.0 ? u_probe : float3(0.35)) * 1.6 + float3(0.1), float3(0.0), float3(1.0));\n return half4(half3(lit * spill), half(spill));\n }\n\n float bevel = max(u_bevel, 1.0);\n float e = max(-sd, 0.0);\n // 0 in the flat middle, 1 at the silhouette.\n float t = 1.0 - clamp(e / bevel, 0.0, 1.0);\n float2 n = vgSceneNormal(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,\n u_morph2Offset, u_morph2Half, u_morph2Corner);\n\n // \"backdrop\" mode hands back the content as-is \u2014 a reference point for comparing the optics.\n float on = u_debug > 5.5 && u_debug < 6.5 ? 0.0 : 1.0;\n float lens = on * u_appear;\n\n float z = vgHeight(e, bevel);\n float slope = vgSlope(e, bevel);\n float3 N = normalize(float3(n * slope, 1.0));\n float2 shift = vgShift(N, z, u_ior) * lens;\n float2 s = u_center + p + shift;\n\n // The area the lens gathers into one pixel: a one-pixel step inward on screen, measured\n // against the sampling step. At the silhouette it's large \u2014 a stripe of backdrop is compressed\n // into a line there, and a point sample gives grain.\n float e1 = e + 1.0;\n float3 N1 = normalize(float3(n * vgSlope(e1, bevel), 1.0));\n float2 shift1 = vgShift(N1, vgHeight(e1, bevel), u_ior) * lens;\n float footprint = length(shift1 - shift - n);\n float spread = max(footprint - 1.0, 0.0) * 0.5 * on;\n\n // Dispersion: the same ray with each channel's own index. Blue bends more than red.\n float2 dR = vgShift(N, z, u_ior - 0.4 * u_iorSpread) * lens - shift;\n float2 dB = vgShift(N, z, u_ior + 0.6 * u_iorSpread) * lens - shift;\n float chroma = length(dB - dR);\n\n float3 rgb;\n float srcA;\n\n if (chroma + spread < 0.25) {\n // In the flat body all the samples would land at the same point, and the body is almost the\n // whole area of the glass.\n half4 c = content.eval(vgInContent(s));\n srcA = float(c.a);\n rgb = srcA > 0.004 ? float3(c.rgb) / srcA : float3(0.0);\n } else {\n // Accumulate premultiplied and divide once per pair: channels from different points with\n // different alpha would otherwise produce a colored fringe at content edges that isn't\n // really there.\n float2 ev = n * spread;\n half4 c0 = content.eval(vgInContent(s + dR - ev));\n half4 c1 = content.eval(vgInContent(s + dR + ev));\n half4 c2 = content.eval(vgInContent(s - ev));\n half4 c3 = content.eval(vgInContent(s + ev));\n half4 c4 = content.eval(vgInContent(s + dB - ev));\n half4 c5 = content.eval(vgInContent(s + dB + ev));\n\n float aR = float(c0.a + c1.a) * 0.5;\n float aG = float(c2.a + c3.a) * 0.5;\n float aB = float(c4.a + c5.a) * 0.5;\n rgb = float3(\n aR > 0.004 ? float(c0.r + c1.r) * 0.5 / aR : 0.0,\n aG > 0.004 ? float(c2.g + c3.g) * 0.5 / aG : 0.0,\n aB > 0.004 ? float(c4.b + c5.b) * 0.5 / aB : 0.0);\n srcA = (aR + aG + aB) / 3.0;\n }\n\n // \"backdrop\" mode checks the CAPTURE, not a simplified version of the optics. What follows are\n // layers that aren't tied to the lens (body, medium, scattering), and they used to conflate\n // \"did the pixels arrive\" with \"how did we process them.\" Here the element must DISAPPEAR: a\n // visible silhouette means a capture loss.\n if (u_debug > 5.5 && u_debug < 6.5) {\n float bypass = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));\n return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * bypass), half(bypass));\n }\n\n // The transparent variant's dimming layer holds legibility instead of the body. A\n // multiplication, not a tint: the content has to stay visible, just quieter.\n rgb *= 1.0 - u_dim * u_appear;\n\n // REFLECTION. Fresnel from the face's actual slope: at the silhouette the view is grazing and\n // reflection is nearly total, which is why the rim reads as a thin line on its own. What's\n // reflected is the element's surroundings and the key light.\n float cosT = clamp(N.z, 0.0, 1.0);\n float f0 = (u_ior - 1.0) / (u_ior + 1.0);\n f0 *= f0;\n float fres = (f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0)) * u_fresnel * lens;\n\n float lineW = clamp(min(u_halfSize.x, u_halfSize.y) * 0.03, 1.5, 5.0);\n float3 refl;\n if (e < lineW * 2.0 || fres > 0.01) {\n float2 around = u_center + p + n * (u_reflectReach * mix(0.35, 1.0, clamp(slope / 3.2, 0.0, 1.0)));\n // Two rings of eight samples total: a single offset sample is an undistorted COPY of\n // neighboring content inside the glass (docs/material-lab.md E-33).\n float p1 = u_reflectReach * 0.75;\n float p2 = u_reflectReach * 0.4;\n float d = 0.7071;\n float4 rsum = float4(content.eval(vgInContent(around + float2(p1, 0.0))))\n + float4(content.eval(vgInContent(around - float2(p1, 0.0))))\n + float4(content.eval(vgInContent(around + float2(0.0, p1))))\n + float4(content.eval(vgInContent(around - float2(0.0, p1))))\n + float4(content.eval(vgInContent(around + float2(p2, p2) * d)))\n + float4(content.eval(vgInContent(around + float2(-p2, p2) * d)))\n + float4(content.eval(vgInContent(around + float2(p2, -p2) * d)))\n + float4(content.eval(vgInContent(around + float2(-p2, -p2) * d)));\n float ra = rsum.a * 0.125;\n refl = ra > 0.004 ? rsum.rgb * 0.125 / ra : float3(0.0);\n } else {\n refl = u_probeLuma >= 0.0 ? u_probe : rgb;\n }\n\n // Light reaching the body is the same across the whole element: blending with the surroundings\n // over the bevel's band used to paint a ring-shaped seam on the body.\n float3 ambient = u_probeLuma >= 0.0 ? u_probe : rgb;\n\n // Light comes from wherever the surroundings are brighter (M 11:04): the lightness slope under\n // the element pulls the key light toward itself, and over a flat backdrop it stays at rest.\n float2 bright = u_probeLuma >= 0.0 ? u_probeSlope : float2(0.0);\n float brightLen = length(bright);\n float2 L = normalize(mix(u_light, bright / max(brightLen, 1e-4), smoothstep(0.03, 0.2, brightLen)) + float2(1e-5));\n // The key light's reflection uses the face's actual normal: at the flat top it points straight\n // up, and there's nothing there for direction to depend on. Using the SDF normal instead\n // painted the body as a cone.\n float facing = dot(N.xy, L);\n float key = u_specular * (pow(max(facing, 0.0), 3.0) + 0.45 * pow(max(-facing, 0.0), 3.0));\n float rimFacing = dot(n, L);\n float rimKey = u_specular * (pow(max(rimFacing, 0.0), 3.0) + 0.45 * pow(max(-rimFacing, 0.0), 3.0));\n // Wherever the key light doesn't fall, the face reflects its shaded surroundings \u2014 hence the\n // dark outline.\n float3 env = refl * (0.55 + 0.45 * min(key, 1.0)) + float3(1.2 * key);\n\n // Interference and diffraction live in the reflected ray at a grazing angle \u2014 a single hue\n // multiplier applied to the reflection, no extra samples.\n float3 spectral = float3(1.0);\n if (u_iridescence > 0.001) {\n spectral *= mix(float3(1.0), vgInterference(cosT), u_iridescence);\n }\n if (u_diffraction > 0.001) {\n float w = u_diffraction * smoothstep(0.45, 1.0, t);\n spectral *= mix(float3(1.0), vgDiffraction(e, bevel), w);\n }\n\n // The BACKDROP ESTIMATE arrives from the probe as one value per surface: computing it here\n // from samples isn't an option \u2014 a kink in density used to turn the estimate's discreteness\n // into ghost copies of text.\n float3 wide;\n float busy;\n if (u_probeLuma >= 0.0) {\n wide = u_probe;\n busy = u_probeBusy;\n } else {\n // The probe hasn't reported yet (first frames) \u2014 fall back to our own samples.\n float2 wx = float2(u_adaptRadius, 0.0);\n float2 wy = float2(0.0, u_adaptRadius);\n float3 w0 = vgUnpack(content.eval(vgInContent(s + wx)));\n float3 w1 = vgUnpack(content.eval(vgInContent(s - wx)));\n float3 w2 = vgUnpack(content.eval(vgInContent(s + wy)));\n float3 w3 = vgUnpack(content.eval(vgInContent(s - wy)));\n wide = (w0 + w1 + w2 + w3) * 0.25;\n float lw = vgLuma(wide);\n // The same statistic the probe uses (twice the mean deviation): both blur and the ink's\n // contrast margin depend on it, and the two paths' scales must not diverge.\n busy = clamp(0.5 * (abs(vgLuma(w0) - lw) + abs(vgLuma(w1) - lw)\n + abs(vgLuma(w2) - lw) + abs(vgLuma(w3) - lw)), 0.0, 1.0);\n }\n // Lightness AT THIS SPOT on the surface: the probe's plane, clamped to the measured range.\n float2 nrm = p / max(u_halfSize, float2(1.0));\n float local = u_probeLuma >= 0.0\n ? clamp(u_probeLuma + dot(u_probeSlope, nrm), u_probeRange.x, u_probeRange.y)\n : vgLuma(wide);\n float mean = u_probeLuma >= 0.0 ? u_probeLuma : vgLuma(wide);\n\n // SCATTERING. Regular mutes foreign structure under its own ink with blur, not a fill (M\n // 11:47): text under a capsule turns into blotches. Uniform across the whole lens; the radius\n // is measured in backdrop space \u2014 the lens compresses it at the silhouette.\n float structure = 1.0 - exp(-busy * VG_STRUCTURE_GAIN);\n // Under ink, Regular always scatters the backdrop; over a busy one, more strongly.\n float adaptBlur = max(\n (structure * VG_SCATTER_MAX + VG_SCATTER_BASE) * u_legibility * u_adaptRadius\n * min(max(footprint, 1.0), VG_FOOTPRINT_MAX),\n u_frost) * u_appear;\n if (adaptBlur > 0.5) {\n float reach = max(u_reach - length(p), 1.0);\n float4 g = vgGather(s, min(adaptBlur, reach), xy);\n float ga = g.a;\n float3 blurred = ga > 0.004 ? g.rgb / ga : float3(0.0);\n rgb = mix(rgb, blurred, smoothstep(0.5, 2.0, adaptBlur));\n }\n\n // AFTER scattering, not before: scattering is a property of TRANSMITTED light and replaces rgb\n // wholesale, so doing it the other way around erased the reflection over the whole element\n // (issue #106).\n rgb = mix(rgb, env * spectral, fres);\n\n // BODY. Tint and dynamic range under the glass compress just enough for ink over it to stay\n // legible (M 6:42). u_ink is ink POLARITY (1 light, 0 dark): the requirements are computed at\n // the two ends and blended by it, otherwise the glass dips toward the middle during a recolor.\n float strict = clamp(u_legibility * 2.0, 0.0, 1.0);\n float capLight = mix(VG_BODY_CAP_LOOSE, VG_BODY_CAP_TIGHT, strict);\n float floorDark = 1.0 - capLight;\n float pol = clamp(u_ink, 0.0, 1.0);\n // The requirement fades together with legibility: an element with no ink is promised\n // transparent glass by the model.\n float demand = clamp(u_legibility * 4.0, 0.0, 1.0);\n\n // Where there's no ink, the glass tints away from the backdrop. The direction is chosen by the\n // average lightness under the element, not the local spot's lightness: otherwise on a gradient\n // the threshold cuts across the body as a diagonal step.\n float darkSide = smoothstep(0.42, 0.58, mean);\n float away = mix(VG_TINT_LIGHT, VG_TINT_DARK, darkSide);\n float tintLuma = mix(away, mix(VG_TINT_LIGHT, VG_TINT_DARK, pol), demand);\n\n // How much medium it takes to push lightness under the ink past the threshold \u2014 locally.\n // Computed from whichever edge of the backdrop's spread is closer to the ink: a light ink\n // drowns in a light spot right beneath it, and the spot's average lightness knows nothing\n // about that spot.\n float edge = busy * VG_BUSY_EDGE;\n float inkHi = min(local + edge, 1.0);\n float inkLo = max(local - edge, 0.0);\n float needForLight = inkHi > capLight\n ? clamp((inkHi - capLight) / max(inkHi - VG_TINT_DARK, 1e-4), 0.0, 0.92)\n : 0.0;\n float needForDark = inkLo < floorDark\n ? clamp((floorDark - inkLo) / max(VG_TINT_LIGHT - inkLo, 1e-4), 0.0, 0.92)\n : 0.0;\n float needForInk = mix(needForDark, needForLight, pol) * demand;\n\n float ground = structure * u_legibility * VG_GROUND_SPAN;\n float density = max(max(u_bodyDensity, ground), needForInk);\n\n // Glass has no color of its own \u2014 only the color of what's beneath it (HIG \"Color\").\n float3 tintHue = mix(float3(1.0), vgHue(wide * 0.5 + ambient * 0.5), u_colorPickup);\n // The element appears by the lens and body building up, not by fading in (M 2:55): at\n // u_appear = 0 it's indistinguishable from the backdrop beneath it.\n rgb = mix(rgb, tintHue * tintLuma, density * u_appear);\n rgb += tintHue * (u_legibility * VG_MATTE_LIFT * u_appear);\n\n // TINTING \u2014 colored glass, not a fill (M 16:31, 17:03): the backdrop's lightness drives the\n // tone \u2014 deeper over dark, lighter over light \u2014 and the content's texture still shows through\n // the color.\n if (u_accent.a > 0.0) {\n float3 tone = u_accent.rgb * mix(0.88, 1.08, local);\n float3 through = tone * (0.85 + 0.3 * vgLuma(rgb));\n rgb = mix(rgb, clamp(through, float3(0.0), float3(1.0)), u_accent.a * u_appear);\n }\n\n // CONTRAST COMPRESSION. The medium removes some light and mixes in scattered light, so content\n // under the glass moves toward the middle rather than simply getting lighter (M 2:35). The\n // previous layer only added light and, over a light backdrop, pushed the body the opposite way\n // from the reference.\n rgb = mix(rgb, vgHue(ambient) * VG_MEDIUM_LUMA, VG_MEDIUM_PULL * u_appear);\n // Ambient light reaches the body the same way across the whole element.\n rgb += ambient * u_edgeLight * VG_AMBIENT_SPILL * u_appear;\n // Glass concentrates light; the played portion is the segment where there's more of it.\n float glow = VG_CONCENTRATE + 0.08 * vgProgress(p, u_halfSize, u_progress);\n rgb += (1.0 - rgb) * glow * lens;\n\n // Under the finger and during morphing, glass concentrates light into a spot (M 4:56, 5:11).\n // This is a CONCENTRATION of the surroundings, not the glass's own whiteness: over a dark\n // backdrop the element lightens but never turns white, and ink over it stays legible.\n if (u_touchPress > 0.001) {\n float2 fromTouch = p - u_touch;\n float spotR = max(min(u_halfSize.x, u_halfSize.y) * 1.2, 1.0);\n float spot = u_touchPress * (0.3 + 0.55 * exp(-dot(fromTouch, fromTouch) / (spotR * spotR)));\n rgb = mix(rgb, clamp(ambient * 1.6 + float3(0.1), float3(0.0), float3(1.0)), spot * lens);\n }\n\n // RIM LIGHT \u2014 a separate layer, a line about 1 pt wide along the silhouette (M 2:36, 11:04):\n // brighter where the face faces the key light, weaker opposite it; where no light falls, a\n // dark outline. This line is what holds the element's presence, not a fill on the body: over a\n // flat backdrop a control is visible along its whole silhouette because of it.\n float line = (1.0 - smoothstep(0.0, lineW, e)) * lens;\n float outline = (1.0 - smoothstep(0.0, lineW * 0.6, e)) * lens;\n float lit = clamp(max(rimKey * 1.4, u_presence * 2.0), 0.0, 1.0);\n // The dark rim is its own layer around the whole silhouette, and the highlight sits ON TOP of\n // it (iOS 27). While the outline used to fade via a (1 \u2212 lit) multiplier, the silhouette read\n // as a single arc: highlight OR shadow.\n rgb *= 1.0 - 0.22 * outline;\n rgb = mix(rgb, mix(refl, float3(1.0), 0.75), line * lit);\n\n if (u_debug > 9.5 && u_debug < 10.5) { rgb = spectral * 0.5; }\n if (u_debug > 10.5 && u_debug < 11.5) { rgb = float3(density, needForInk, structure); }\n // WHAT THE PROBE SAID ABOUT THE BACKDROP \u2014 three values that drive all the tinting: the\n // average lightness under the element, the vertical lightness slope, and the lightness AT THIS\n // SPOT. The slope is shown offset and squeezed by half: it's signed and rarely exceeds a\n // magnitude of one half.\n if (u_debug > 11.5) { rgb = float3(max(u_probeLuma, 0.0), 0.5 + u_probeSlope.y * 0.5, local); }\n\n rgb = vgSoftClip(rgb);\n\n // Sample alpha has to survive to the result: un-premultiplying color by its original alpha and\n // then returning it with a different one (the shape mask) would make a transparent backdrop\n // opaque and blow it out.\n float alpha = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));\n return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * alpha), half(alpha));\n}\n";
115
+
116
+ /** Order matches `DEBUG_MODES` in `material.ts`: the index goes straight into `u_debug`. */
117
+ declare const SURFACE_SHADER = "\nuniform shader u_icon;\n/** App-colored content ON the glass \u2014 cover art, a thumbnail. A separate layer from the ink\n * mask: that one is single-channel and colored by polarity, this one carries its own color\n * as-is. */\nuniform shader u_overlay;\n\nuniform float2 u_center;\nuniform float2 u_halfSize;\nuniform float u_corner;\nuniform float u_bevel;\nuniform float u_thickness;\nuniform float2 u_morphOffset;\nuniform float2 u_morphHalf;\nuniform float u_morphCorner;\nuniform float u_morphK;\nuniform float2 u_morph2Offset;\nuniform float2 u_morph2Half;\nuniform float u_morph2Corner;\n\nuniform float u_press;\nuniform float u_active;\n/* 0 \u2014 the element sinks in under the finger, 1 \u2014 it rises into glass (reference \u00A75). */\nuniform float u_lift;\n\nuniform float u_edgeDensity;\nuniform float u_dispersion;\nuniform float u_refraction;\nuniform float4 u_tint;\nuniform float u_shadow;\nuniform float u_shadowReach;\nuniform float3 u_ambient;\nuniform float u_presence;\nuniform float u_progress;\nuniform float u_appear;\nuniform float u_debug;\n\nuniform float u_iconOn;\nuniform float u_overlayOn;\nuniform float u_iconScale;\nuniform float4 u_inkIdle;\nuniform float4 u_inkActive;\nuniform float2 u_touch;\nuniform float2 u_pull;\nuniform float u_touchPress;\nuniform float u_touchRadius;\nuniform float2 u_wave;\n\n\nfloat vgRoundRect(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - corner;\n}\n\nfloat vgSmin(float a, float b, float k) {\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n return mix(b, a, h) - k * h * (1.0 - h);\n}\n\n// A scene is a single shape, and when k > 0 a smooth union with one or two neighbors. A third\n// shape exists to break the control into pieces (docs/reference.md \u00A75): there are two bridges\n// there. It's turned off with a zero size, not its own k: the fused body shares one bridge.\nfloat vgScene(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float a = vgRoundRect(p, halfSize, corner);\n if (k <= 0.0) { return a; }\n float ab = vgSmin(a, vgRoundRect(p - offsetB, halfB, cornerB), k);\n if (halfC.x <= 0.0) { return ab; }\n return vgSmin(ab, vgRoundRect(p - offsetC, halfC, cornerC), k);\n}\n\n// Response to a finger. It's the FIELD around the touch point that deforms, not the shape's\n// bounding box: scaling width to pull the right edge would also pull the left one \u2014 liquid\n// doesn't behave that way. Here the displacement falls off with distance from the finger, so the\n// far edge stays put.\n//\n// The order of the terms matters: drag shifts the field, press pulls it toward the finger, the\n// wave rides on top of the already-shifted field \u2014 otherwise the ripple decouples from the\n// deformation and lives a life of its own.\nfloat2 vgTouchWarp(float2 p, float2 touch, float2 pull, float press, float radius,\n float waveAmp, float wavePhase) {\n if (radius <= 0.0) { return p; }\n // Under the finger the whole element grows (M 3:51; HIG: interactive \"expands\").\n p /= 1.0 + 0.06 * press;\n float2 d = p - touch;\n float r = length(d);\n\n // The finger is a BLOB, not a point, but not a hard stamp either: a wide peak, soft edges. A\n // flat plateau with a sharp cutoff at the edge shifts the region as a block, and a straight\n // line under the glass breaks into a step \u2014 the deformation reads as rectangular even though\n // the finger is round. A double smoothstep gives the same contact width without that step.\n //\n // The influence cuts off at the radius rather than fading into an infinite tail like a\n // Gaussian: with a tail the deformation spreads into a \"rubber mattress\" bulge.\n float k = clamp((radius - r) / radius, 0.0, 1.0);\n float s = k * k * (3.0 - 2.0 * k);\n float core = s * s * (3.0 - 2.0 * s);\n // A ring around the blob: material displaced from under the finger has to end up somewhere.\n // Without this ridge the shape just balloons, and a dense medium doesn't behave that way.\n float rim = k * k * (1.0 - k) * 4.0;\n\n float2 q = p - pull * (core - 0.42 * rim);\n q -= d * (press * 0.16 * core);\n\n // The wave has its own scale, twice the drag radius: the ripple has to reach the far edge,\n // otherwise it reads as jitter under the finger rather than a wave across the surface. The\n // wavelength is short: in a dense medium ripples are frequent and small, long shallow swells\n // are water.\n if (waveAmp > 0.0 && r > 0.0001) {\n float span = radius * 2.0;\n float ring = sin(r / (span * 0.17) - wavePhase * 6.2831853) * exp(-r / (span * 0.7));\n q -= (d / r) * ring * waveAmp;\n }\n return q;\n}\n\n// Analytical normal of a single shape: radial on the rounded part, axis-aligned on the straight\n// segments.\nfloat2 vgRoundRectNormal(float2 p, float2 halfSize, float corner) {\n float2 q = abs(p) - halfSize + corner;\n float2 g = (q.x > 0.0 && q.y > 0.0)\n ? normalize(max(q, float2(0.0001)))\n : (q.x > q.y ? float2(1.0, 0.0) : float2(0.0, 1.0));\n return g * sign(p);\n}\n\n// Normal of the union \u2014 ANALYTICAL, no more finite differences here.\n//\n// Derivation: smin has two terms, mix(b,a,h) and \u2212k\u00B7h\u00B7(1\u2212h). Their derivatives with respect to h\n// contain a factor of (1\u22122h) with opposite signs and cancel exactly, because h is linear in\n// (b\u2212a)/k. What's left is mix(\u2207b, \u2207a, h) \u2014 that is, the two shapes' normals, blended with the\n// same weight that blends the distances themselves.\n//\n// The previous version took four ADDITIONAL scene evaluations per pixel (each one \u2014 two shapes\n// plus smin) and was still an approximation. Here there are two shapes, exactly.\nfloat2 vgSceneNormal(float2 p, float2 halfSize, float corner,\n float2 offsetB, float2 halfB, float cornerB, float k,\n float2 offsetC, float2 halfC, float cornerC) {\n float2 na = vgRoundRectNormal(p, halfSize, corner);\n if (k <= 0.0) { return na; }\n float2 q = p - offsetB;\n float a = vgRoundRect(p, halfSize, corner);\n float b = vgRoundRect(q, halfB, cornerB);\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n float2 nab = normalize(mix(vgRoundRectNormal(q, halfB, cornerB), na, h) + float2(1e-5, 1e-5));\n if (halfC.x <= 0.0) { return nab; }\n // Same derivation as for the pair: normals blend with the same weight as the distances.\n float2 qc = p - offsetC;\n float ab = vgSmin(a, b, k);\n float c = vgRoundRect(qc, halfC, cornerC);\n float hc = clamp(0.5 + 0.5 * (c - ab) / k, 0.0, 1.0);\n return normalize(mix(vgRoundRectNormal(qc, halfC, cornerC), nab, hc) + float2(1e-5, 1e-5));\n}\n\n// Position within the bevel: 0 is the flat middle, 1 is the very edge. This one value feeds the\n// mask, thickness, refraction, aberrations and the width of the light rim.\nfloat vgBevelT(float sd, float bevel) {\n return clamp((sd + bevel) / bevel, 0.0, 1.0);\n}\n\n// Slope of the bevel profile \u2014 spherical: t / sqrt(1 - t\u00B2), like a spherical cap. The previous t\u00B2\n// kept the slope near zero through almost the whole bevel and then shot up right at the edge, so\n// all the optics bunched into a narrow ring and the element read as a PUCK \u2014 a flat top and a\n// wall around the rim. Here curvature is spread across the bevel, and the rim stops being a line.\n//\n// This still doesn't control the field of view: only the bevel bends, the middle stays flat,\n// otherwise the surface reads as a soap bubble.\nfloat vgBevelSlope(float t) {\n return min(t * inversesqrt(max(1.0 - t * t * 0.94, 0.02)), 3.2);\n}\n\n// PROGRESS FRACTION. Progress is an active state turned into a FIELD: to the left of the boundary\n// the element is active, to the right it stays regular glass, and the boundary moves. A negative\n// value turns the field off entirely \u2014 zero is taken by the start of the track and can't also\n// mean off.\n//\n// The boundary is SOFT. A hard seam reads as two different materials glued together rather than\n// two states of one; the transition width is a fraction of the half-width, so it doesn't read as\n// a thread on a large element and doesn't swallow a small one whole.\nfloat vgProgress(float2 p, float2 halfSize, float progress) {\n if (progress < 0.0) { return 0.0; }\n float edge = mix(-halfSize.x, halfSize.x, clamp(progress, 0.0, 1.0));\n float soft = max(halfSize.x * 0.05, 1.0);\n return 1.0 - smoothstep(edge - soft, edge + soft, p.x);\n}\n\n\nconst float VG_FALLOFF = 2.6;\n\n// Tint density in the flat middle; at the bevel it's multiplied by u_edgeDensity.\nconst float VG_BODY_DENSITY = 0.19;\n/* Fraction of the ambient hue in the shadow. The shadow has to stay a shadow, not a colored\n * blob. */\nconst float VG_SHADOW_TINT = 1.0;\n// How much weaker the shadow is right at the outline than under the middle of the gap, and what\n// fraction of the offset it takes to reach full depth. In the reference the shadow's minimum\n// sits below the rim, not right at it.\nconst float VG_GAP_LIGHT = 0.76;\nconst float VG_GAP_REACH = 0.20;\n/* Depth of the ink beneath the surface, dp. The normal displaces it by exactly this much right\n * at the rim. */\nconst float VG_INK_DEPTH = 4.0;\n/* Ink defocus under the finger (reference \u00A76): a glyph loses its edge and drowns in milk. Given\n * as a FRACTION of the touch blob, not a number: the blob already arrives in device units, so\n * the radius automatically follows both screen density and the element's size. At rest it's\n * zero, and the sample stays a single one. */\nconst float VG_INK_DEFOCUS = 0.08;\n\nhalf4 vgPack(half3 c, float a) { return half4(c * half(a), half(a)); }\n\n// What bleeds into the shadow is the COLOR of the surroundings, not its brightness: the hue's own\n// lightness is subtracted out of it, otherwise the shadow pales and loses depth instead of\n// warming up. Strength is set by how much light there is around: over a nearly black backdrop\n// there's nothing to bleed into the shadow.\nfloat3 vgShadowTint(float3 ambient) {\n float peak = max(max(ambient.r, ambient.g), max(ambient.b, 0.001));\n float3 hue = ambient / peak;\n float lumaWeights = dot(ambient, float3(0.2126, 0.7152, 0.0722));\n return (hue - dot(hue, float3(0.2126, 0.7152, 0.0722))) * lumaWeights;\n}\n\nhalf3 vgHeat(float v) {\n float x = clamp(v, 0.0, 1.0);\n return half3(half(clamp(x * 2.2 - 0.2, 0.0, 1.0)),\n half(clamp(1.0 - abs(x - 0.5) * 2.2, 0.0, 1.0)),\n half(clamp(1.2 - x * 2.4, 0.0, 1.0)));\n}\n\nhalf4 main(float2 xy) {\n float2 p = vgTouchWarp(xy - u_center, u_touch, u_pull, u_touchPress, u_touchRadius, u_wave.x, u_wave.y);\n\n // Inverse deformation: stretch A along the pull vector, compression 1/sqrt(A) across it. This\n // exact law mirrors the transform of the live backdrop under the canvas, otherwise the two\n // would drift apart. There's NO motion geometry here \u2014 no pull, no press-driven bulge. All of\n // that is done by a single wrapper transform, the same one that carries the native lens: the\n // shader doesn't touch it, and two pipelines driving one motion would drift apart by a frame,\n // making the layers visible as separate.\n\n float halfMin = max(min(u_halfSize.x, u_halfSize.y), 1.0);\n float bevel = max(u_bevel, 1.0);\n\n float sd = vgScene(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,\n u_morph2Offset, u_morph2Half, u_morph2Corner);\n float t = vgBevelT(sd, bevel);\n float2 n = vgSceneNormal(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,\n u_morph2Offset, u_morph2Half, u_morph2Corner);\n float3 N = normalize(float3(n * vgBevelSlope(t), 1.0));\n\n // Progress is an active state turned into a field: the played fraction shines and glows\n // exactly as much as an active element shines whole. This does NOT touch ink: recoloring text\n // as the track advances would mean changing it mid-word.\n float lit = max(u_active, vgProgress(p, u_halfSize, u_progress));\n\n // The highlight and the rim are computed by the lens: reflection is a function of the\n // surroundings, and only the lens can see them.\n\n if (u_debug > 0.5) {\n float inMask = 1.0 - smoothstep(-1.0, 1.0, sd);\n if (u_debug < 1.5) {\n float band = abs(fract(sd / (halfMin * 0.22)) - 0.5) * 2.0;\n half3 c = sd < 0.0 ? half3(0.20, 0.62, 1.0) : half3(1.0, 0.42, 0.22);\n return vgPack(c * half(0.25 + band * 0.75), 1.0);\n }\n if (u_debug < 2.5) { return vgPack(half3(1.0), inMask); }\n if (u_debug < 3.5) { return vgPack(vgHeat(t), inMask); }\n // Fresnel now lives in the lens; here we show its SHAPE \u2014 how grazing the view is.\n if (u_debug < 4.5) { return vgPack(vgHeat(1.0 - clamp(N.z, 0.0, 1.0)), inMask); }\n if (u_debug < 5.5) {\n float push = pow(t, VG_FALLOFF) * u_refraction;\n return vgPack(vgHeat(push), inMask);\n }\n if (u_debug < 6.5) { return half4(0.0); }\n if (u_debug < 7.5) { return half4(0.0); }\n // Dispersion is computed by the lens; here is the field it grows along.\n if (u_debug < 8.5) { return vgPack(vgHeat(u_dispersion * t * t), inMask); }\n if (u_debug < 9.5) { return vgPack(half3(N * 0.5 + 0.5), inMask); }\n // spectral and adapt are shown by the LENS \u2014 the surface has to get out of the way.\n return half4(0.0);\n }\n\n // The shadow and the halo live OUTSIDE the shape: inside, their place is taken by the glass\n // itself. Separation from the content rests entirely on the shadow \u2014 without it the surface\n // would sit ON the picture, not above it. The shadow only lives at the rim and outside it: at\n // sd < \u22121 the outside multiplier is already zero. Computing it deep inside the shape would be\n // a wasted FULL SDF evaluation for every such pixel, and the body covers almost the whole\n // area. The branch here is on the coordinate, but only the threads right at the rim actually\n // diverge.\n float shade = 0.0;\n float halo = 0.0;\n if (sd > -1.0) {\n float outside = smoothstep(-1.0, 1.0, sd);\n // Under the finger a button moves TOWARD the backdrop, and the shadow tightens: it's exactly\n // the gap between the two. A control rising into glass (a switch knob, a slider handle \u2014\n // reference \u00A75) moves AWAY from it, and then the shadow spreads out instead. Only the app\n // knows which one applies, so the direction arrives as a fraction: 0 is sinking in, the way\n // every element behaved before this rule existed.\n float reach = u_shadowReach * (1.0 + mix(-0.25, 0.55, u_lift) * u_press);\n // The shadow is soft and wide, shifted downward (M 11:58): it's what holds the element's\n // separation from the content.\n float sdDrop = vgScene(p - float2(0.0, reach * 0.35), u_halfSize, u_corner,\n u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,\n u_morph2Offset, u_morph2Half, u_morph2Corner);\n float amb = 1.0 - smoothstep(-reach * 0.3, reach, sdDrop);\n // THE GAP UNDER A RAISED ELEMENT IS LIT: right at the outline the shadow is WEAKER than\n // further down. Otherwise the minimum sits right against the silhouette \u2014 both parts of the\n // shadow are monotonic in distance.\n float gap = smoothstep(0.0, max(reach * VG_GAP_REACH, 1.0), max(sd, 0.0));\n shade = amb * amb * 0.13 * mix(VG_GAP_LIGHT, 1.0, gap) * outside * u_shadow * u_appear;\n halo = 1.0 - smoothstep(0.0, reach * 0.30, max(sd, 0.0));\n halo = halo * halo * lit * 0.10 * outside * u_appear;\n }\n\n if (sd > 1.0) {\n // Ambient light bleeds into the shadow (219 @8:22): it isn't a black hole, it's tinted by\n // whatever lies nearby. Otherwise an element over colored content would hang over a gray\n // blob.\n half3 spill = max(half3(half(halo)) + half3(vgShadowTint(u_ambient)) * half(VG_SHADOW_TINT * shade * (1.0 - halo)), half3(0.0));\n return half4(spill, half(halo + shade * (1.0 - halo)));\n }\n\n float density = u_tint.w;\n float inner = clamp(-sd / halfMin, 0.0, 1.0);\n half3 col = half3(u_tint.rgb);\n\n // The tint is LIGHT and weak, not dark: dark glass over dark content disappears, and has to be\n // held together with a heavy rim \u2014 which makes the surface read as a chrome bead. Tint density\n // at the bevel is a separate value: physically the bevel bends light harder, but it doesn't\n // get hazier. Coupled together, the two produced a milky ring around the whole outline.\n float body = mix(VG_BODY_DENSITY, VG_BODY_DENSITY * u_edgeDensity, smoothstep(0.10, 0.62, t)) * density;\n float vignette = smoothstep(0.15, 1.0, inner) * 0.19 * density;\n // There's NO density boost on press here. It was meant as \"the element stands out more under\n // the finger,\" but it pulls the body toward the tint, and the tint depends on polarity: over a\n // light backdrop (dark polarity) press ended up DARKENING the element. Presence is shown\n // instead by the vgTouchWarp field's deformation and the highlight blooming below \u2014 neither\n // cares about the sign of polarity.\n float a = max(body, vignette);\n\n // Active state does NOT touch the body's color. Mixing in a fixed gray here flipped the\n // effect's sign depending on the backdrop: over dark the element lightened, over light it\n // darkened, for the exact same state. Active state is shown instead by the highlight and rim\n // lighting above: neither cares about the backdrop.\n\n // THE ICON SITS BENEATH THE SURFACE, it isn't stuck on top of it: it used to be mixed in last,\n // over the highlight, and read as a flat sticker on volumetric glass. At the rim the normal\n // displaces it, like everything seen through the glass, while the highlight sits ON TOP \u2014 it\n // lives on the surface itself.\n //\n // There is NO shadow under the icon here. It used to be made from the difference of two offset\n // mask samples and produced a second outline along the edge \u2014 the icon's borders looked\n // ragged.\n // THE INK'S SHIFT IS SET BY ITS OWN DEPTH, NOT BY BEVEL WIDTH. The ink sits right at the\n // surface, and it's displaced by exactly the thin layer of glass above it \u2014 not by how wide a\n // bevel this particular piece of glass happens to have. It used to be a fraction of the bevel:\n // on thin glass the shift came out to 4 dp and everything lined up, but on thick glass it was\n // 8, and cover art inset from the edge by 6 stretched toward the rim and spilled past its\n // bounds. The ceiling is absolute: ink has one depth regardless of the glass.\n float2 inkShift = n * (min(u_bevel * 0.5, VG_INK_DEPTH) * t);\n float2 inkUv = u_center + p - inkShift;\n\n // INK LOSES SHARPNESS UNDER THE FINGER (reference \u00A76): in the reference, a glyph under\n // pressure drowns in milk rather than simply getting lighter. This branch is uniform across\n // the whole element: press arrives as a uniform, so defocus costs nothing while the element\n // isn't being touched.\n float defocus = VG_INK_DEFOCUS * u_touchRadius * u_touchPress;\n float2 inkDx = float2(defocus, 0.0);\n float2 inkDy = float2(0.0, defocus);\n\n half4 ink;\n half4 over;\n if (defocus > 0.01) {\n // A cross around the center with double weight in the middle: the ink mask is\n // high-contrast, and four samples are enough for a stroke to stop holding its edge.\n ink = (u_icon.eval((inkUv - inkDx) * u_iconScale)\n + u_icon.eval((inkUv + inkDx) * u_iconScale)\n + u_icon.eval((inkUv - inkDy) * u_iconScale)\n + u_icon.eval((inkUv + inkDy) * u_iconScale)\n + u_icon.eval(inkUv * u_iconScale) * 2.0) * half(u_iconOn / 6.0);\n // Colored content lives in the same plane as the ink and blurs together with it: different\n // sharpness for two layers in one plane would read as a defect, not a press.\n over = (u_overlay.eval((inkUv - inkDx) * u_iconScale)\n + u_overlay.eval((inkUv + inkDx) * u_iconScale)\n + u_overlay.eval((inkUv - inkDy) * u_iconScale)\n + u_overlay.eval((inkUv + inkDy) * u_iconScale)\n + u_overlay.eval(inkUv * u_iconScale) * 2.0) * half(u_overlayOn * u_appear / 6.0);\n } else {\n ink = u_icon.eval(inkUv * u_iconScale) * half(u_iconOn);\n over = u_overlay.eval(inkUv * u_iconScale) * half(u_overlayOn * u_appear);\n }\n\n // The backing under the ink is held by the LENS (body density from the probe), not the\n // surface: legibility is a property of the material, and computing it here a second time\n // would give two different answers to one question.\n\n // Layers are composited premultiplied-over: in straight alpha, a nearly transparent dark body\n // used to pull the color of semi-transparent ink and the finger's light toward itself \u2014 letter\n // edges grayed out, pressing darkened things.\n half3 pm = col * half(a);\n half inkA = ink.g * half(mix(0.82, 1.0, u_active) * u_appear);\n half3 inkCol = mix(half3(u_inkIdle.rgb), half3(u_inkActive.rgb), half(u_active));\n pm = pm * (1.0 - inkA) + inkCol * inkA;\n a = a + float(inkA) * (1.0 - a);\n\n // COLORED CONTENT LIVES IN THE SAME PLACE AS THE INK \u2014 inside the material, at the same\n // coordinate. Otherwise deformation would carry them apart: on press, the title and artist\n // name would shake together with the surface while the cover art stayed put, because it used\n // to be a separate layer on top of the glass. Polarity doesn't touch it \u2014 it has its own\n // color, and there's nothing to swap it for.\n pm = pm * (1.0 - over.a) + over.rgb * over.a;\n a = a + float(over.a) * (1.0 - a);\n\n // Light from the finger is gathered by the LENS: it's a concentration of the surroundings, not\n // the glass's own whiteness. The surface is left with only what the glass spills outward \u2014 the\n // halo above.\n\n float mask = 1.0 - smoothstep(-1.0, 1.0, sd);\n pm = clamp(pm, half3(0.0), half3(1.0)) * half(mask);\n a *= mask;\n pm = max(pm + half3(vgShadowTint(u_ambient)) * half(VG_SHADOW_TINT * shade * (1.0 - a)), half3(0.0));\n return half4(pm, half(a + shade * (1.0 - a)));\n}\n";
118
+
119
+ type VireGlassAccessibility = {
120
+ /** The glass becomes more frosted and hides content beneath it more strongly (219 @18:22). */
121
+ reduceTransparency: boolean;
122
+ /** The element moves nearly to black or white and picks up a contrasting border (219 @18:29). */
123
+ increaseContrast: boolean;
124
+ /** The material's springiness is turned off, effects are quieter (219 @18:35). Doesn't touch
125
+ * optics: this is about motion, applied by whoever computes that motion. */
126
+ reduceMotion: boolean;
127
+ };
128
+ declare const NO_ACCESSIBILITY: VireGlassAccessibility;
129
+ /** A system setting acts on the whole glass and outranks the material preset (219 @18:45): even
130
+ * a transparent preset moves to the edge of the scale under contrast — the user needs contrast
131
+ * more than they need aesthetics. */
132
+ declare function applyAccessibility(optics: VireGlassOptics, mods?: VireGlassAccessibility): VireGlassOptics;
133
+
134
+ /** The point on the scale where the material stays exactly as it is today. */
135
+ declare const GLASS_SCALE_DEFAULT = 0.35;
136
+ /**
137
+ * `scale` 0 is ultra clear, 1 is fully tinted. Toward the transparent end, the legibility
138
+ * requirement and the dimming layer are relaxed (the user chose clarity); toward the tinted end,
139
+ * body density grows.
140
+ *
141
+ * Accessibility settings apply AFTER the scale: they set floors, and the user's chosen clarity
142
+ * does not override them (219 @18:45).
143
+ */
144
+ declare function applyGlassScale(optics: VireGlassOptics, scale: number): VireGlassOptics;
145
+
146
+ type ScrollEdgeStyle = 'dissolve' | 'dim' | 'hard';
147
+ /**
148
+ * The edge style follows the style of the nearest glass: light ink means dark glass, and the
149
+ * content beneath it fades into a light dimming; dark ink means light glass, and the content
150
+ * dissolves into the background. A pinned view under the panel (column headers) gets a flat
151
+ * band with no gradient.
152
+ */
153
+ declare function scrollEdgeStyle(inkLight: boolean, pinned?: boolean): ScrollEdgeStyle;
154
+ /** How far the content has to slide under the panel for the effect to fully engage, dp. */
155
+ declare const SCROLL_EDGE_ENGAGE_DP = 24;
156
+ /**
157
+ * Edge strength as a function of scroll. With no scroll, content near the top doesn't slide
158
+ * under the panel yet — so there's no effect (a mail screen at the top: the header sits on a
159
+ * clean background). At the bottom, content slides under it for as long as there's room left
160
+ * to scroll.
161
+ */
162
+ declare function scrollEdgeStrength(side: 'top' | 'bottom', scroll: number, maxScroll: number): number;
163
+
164
+ /**
165
+ * Radius of a nested shape: outer radius minus inset. Zero is a square corner; it never goes
166
+ * negative — once the inset exceeds the outer radius, the corner is simply a right angle.
167
+ *
168
+ * `minimum` — a floor on the radius (analogous to `concentric(minimum:)` in SwiftUI): far from
169
+ * the edge, honest concentricity yields zero and the nested element drops out of the family with
170
+ * a sharp corner. The floor breaks concentricity on purpose — here kinship between shapes matters
171
+ * more than a shared center of curvature.
172
+ */
173
+ declare function concentricRadius(outerRadius: number, inset: number, minimum?: number): number;
174
+ /**
175
+ * The inset at which an inner shape with a given radius stays concentric with the outer one.
176
+ * The inverse problem: the element's radius is known (its size dictates it), and what's needed
177
+ * is the margin around it.
178
+ */
179
+ declare function concentricInset(outerRadius: number, innerRadius: number): number;
180
+
181
+ /**
182
+ * GLASS GROUP ESTIMATE — the pure part of the surface group (`glass-group.tsx`).
183
+ *
184
+ * Everything that decides lives here: the lightness plane from members' samples, the group's
185
+ * aggregates, the polarity requirement and its hysteresis. None of these rules need React or the
186
+ * native layer, so they live apart from the component and are checked by a unit test.
187
+ */
188
+ type GroupMember = {
189
+ x: number;
190
+ y: number;
191
+ sample: BackdropSample;
192
+ legibility: number;
193
+ };
194
+ /** Group estimate: center and average lightness, slope along x, the lightest spot, and the
195
+ * measurement values shared by everyone — [variegation, lo, hi, r, g, b]. */
196
+ type GroupPlane = {
197
+ mx: number;
198
+ ml: number;
199
+ slope: number;
200
+ base: number;
201
+ rest: number[];
202
+ };
203
+ /** Input to the polarity decision: the lightness the group judges by, and the strictest of the requirements. */
204
+ type GroupDecision = {
205
+ decisive: number;
206
+ legibility: number;
207
+ };
208
+ /** Fraction of a new sample in the smoothed estimate. Samples come in from every member, so the
209
+ * group collects roughly twenty a second — a quarter is enough for a response within ~200 ms. */
210
+ declare const SMOOTH = 0.25;
211
+ /** The remainder below which smoothing snaps the value to the sample. An exponential never
212
+ * actually reaches its target, and the estimate kept "changing" by denormals for two more
213
+ * minutes after the backdrop had settled. */
214
+ declare const SETTLE = 0.000001;
215
+ /**
216
+ * Fraction of the slope that reaches the members.
217
+ *
218
+ * A plane through four points nearly interpolates them: right at a black/white border, buttons
219
+ * were getting 0.0, 0.3, 0.7 and 1.0 — that is, exactly their own individual values, and the
220
+ * group fell apart into independent elements again. Density is set by the group's LIGHTEST spot;
221
+ * the slope only lets it off slightly at the dark end.
222
+ */
223
+ declare const GRADIENT = 0.35;
224
+ /** Group estimate from its members. An empty group has nothing to estimate — that's `null`. */
225
+ declare function aggregate(members: readonly GroupMember[]): {
226
+ plane: GroupPlane;
227
+ decision: GroupDecision;
228
+ } | null;
229
+ /**
230
+ * Smoothing runs over samples, not frames: the native lens smooths ITS OWN estimate, and the
231
+ * group estimate overrides it with an already-finished value. Without this the group's tone
232
+ * changed in steps — the probe's 180 ms cadence is too coarse for the adaptation to read as
233
+ * unnoticeable.
234
+ */
235
+ declare function smoothPlane(prev: GroupPlane | null, next: GroupPlane): GroupPlane;
236
+ /** The sample a member at position `x` hands its own lens — in the order of its prop. */
237
+ declare function probeValuesAt(plane: GroupPlane, x: number): number[];
238
+ type GroupSink = {
239
+ /** The group estimate has been recomputed: members take their values from it. */
240
+ plane: (plane: GroupPlane | null) => void;
241
+ /** The group flips ink polarity for everyone at once. */
242
+ flip: (polarity: number) => void;
243
+ };
244
+ type GroupState = {
245
+ report: (id: string, x: number, y: number, sample: BackdropSample, legibility: number) => void;
246
+ release: (id: string) => void;
247
+ plane: () => GroupPlane | null;
248
+ polarity: () => number;
249
+ confirmations: () => number;
250
+ };
251
+ /** Group state: members, the smoothed estimate, and polarity hysteresis. */
252
+ declare function createGroupState(sink: GroupSink): GroupState;
253
+
254
+ declare function toAGSL(shaderSource: string): string;
255
+
256
+ /** float2/float3/float4 → vec2/vec3/vec4. */
257
+ declare function convertVecTypes(src: string): string;
258
+ /** half/half2/half3/half4 → float/vec2/vec3/vec4, including half(x) → float(x) casts. */
259
+ declare function convertHalfTypes(src: string): string;
260
+ /** `uniform shader NAME;` → `uniform sampler2D NAME;` + a new `uniform vec2 <sizeUniform>;`. */
261
+ declare function convertShaderUniforms(src: string): string;
262
+ /**
263
+ * `NAME.eval(coord)` → `texture(NAME, (coord) / <sizeUniform>)` for each declared
264
+ * `uniform sampler2D`. The bounds of `coord` are found by scanning parens, not with a regex: the
265
+ * arguments are nested calls (`content.eval(vgInContent(s + dR - e))`) with commas and
266
+ * parentheses inside.
267
+ */
268
+ declare function convertEvalCalls(src: string): string;
269
+ /**
270
+ * Early `return <expression>;` inside `main` → `fragColor = <expression>; return;`. Takes ONLY
271
+ * the body of main (see `convertEntryPoint`) — ordinary `return`s in helper functions live
272
+ * outside this text and are untouched. Expressions never contain a `;` of their own (shaders have
273
+ * none — the only `;` lives inside `for(...)`, which isn't a return), so `[^;]+` is safe.
274
+ */
275
+ declare function convertReturns(mainBody: string): string;
276
+ /**
277
+ * `half4 main(float2 xy) { ... }` → `void main() { vec2 xy = <Y-flipped coordinate>; ...; }`.
278
+ * AGSL gives `xy` in screen pixels, top-left origin, y down; `gl_FragCoord` is bottom-left
279
+ * origin, y up. The flip via `u_resolution.y - gl_FragCoord.y` restores the original convention
280
+ * (both coordinate systems center the pixel at `+0.5`, so no X shift or constant is needed).
281
+ * `out vec4 fragColor` and `uniform vec2 u_resolution` are declared in the prologue (`addPrologue`).
282
+ */
283
+ declare function convertEntryPoint(src: string): string;
284
+ /** `#version 300 es` + `precision highp float;` — no equivalent in AGSL, WebGL2 requires them as
285
+ * the file's first lines. `out vec4 fragColor` and `uniform vec2 u_resolution` ride along here
286
+ * too: both new, with no counterpart in the source, needed by exactly one entry point
287
+ * (`convertEntryPoint`). */
288
+ declare function addPrologue(src: string): string;
289
+ /** Full translation AGSL/SkSL → GLSL ES 3.0. Order matters: the entry point is found by its
290
+ * literal AGSL signature BEFORE type conversion, otherwise `half4 main(float2 xy)` can't be
291
+ * found. */
292
+ declare function toGLSL(shaderSource: string): string;
293
+
294
+ export { BackdropSample, type DeformSample, FRESNEL_EXPONENT, GLASS_SCALE_DEFAULT, GRADIENT, type GroupDecision, type GroupMember, type GroupPlane, type GroupSink, type GroupState, LENS_SHADER, NO_ACCESSIBILITY, SCROLL_EDGE_ENGAGE_DP, SETTLE, SMOOTH, SURFACE_SHADER, type ScrollEdgeStyle, VG_FALLOFF, VG_SDF, type VireGlassAccessibility, VireGlassOptics, absorption, addPrologue, aggregate, applyAccessibility, applyGlassScale, blur, bodyDensity, colorPickup, concentricInset, concentricRadius, convertEntryPoint, convertEvalCalls, convertHalfTypes, convertReturns, convertShaderUniforms, convertVecTypes, createDeform, createGroupState, diffraction, dispersion, edgeDensity, edgeLight, fresnelF0, fresnelStrength, gatherRadius, iorSpread, iridescence, mediumTint, probeValuesAt, raiseIntoGlass, refractionScale, refractionStrength, scrollEdgeStrength, scrollEdgeStyle, smoothPlane, specularPower, specularStrength, toAGSL, toGLSL };