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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +216 -0
- package/android/build.gradle +15 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/expo/modules/glasslens/GlassBackdropView.kt +335 -0
- package/android/src/main/java/expo/modules/glasslens/GlassLensModule.kt +49 -0
- package/android/src/main/java/expo/modules/glasslens/GlassLensView.kt +463 -0
- package/android/src/main/java/expo/modules/glasslens/GlassProbeView.kt +76 -0
- package/dist/adaptation-BSbbdExK.d.cts +78 -0
- package/dist/adaptation-BSbbdExK.d.ts +78 -0
- package/dist/adapters-BPB0D5nr.d.cts +357 -0
- package/dist/adapters-BgSc-smX.d.ts +357 -0
- package/dist/chunk-HF2AWZEZ.js +1741 -0
- package/dist/chunk-HF2AWZEZ.js.map +1 -0
- package/dist/chunk-JNRZNMJJ.js +20 -0
- package/dist/chunk-JNRZNMJJ.js.map +1 -0
- package/dist/index.cjs +2159 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +294 -0
- package/dist/index.d.ts +294 -0
- package/dist/index.js +286 -0
- package/dist/index.js.map +1 -0
- package/dist/material-CkUHUBoy.d.cts +408 -0
- package/dist/material-CkUHUBoy.d.ts +408 -0
- package/dist/react.cjs +73 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +25 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +59 -0
- package/dist/react.js.map +1 -0
- package/dist/web/index.cjs +1990 -0
- package/dist/web/index.cjs.map +1 -0
- package/dist/web/index.d.cts +148 -0
- package/dist/web/index.d.ts +148 -0
- package/dist/web/index.js +657 -0
- package/dist/web/index.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +127 -0
- package/src/accessibility.ts +51 -0
- package/src/adaptation.ts +85 -0
- package/src/adapters.ts +362 -0
- package/src/concentric.ts +25 -0
- package/src/geometry.ts +151 -0
- package/src/glass-scale.ts +37 -0
- package/src/group-model.ts +224 -0
- package/src/index.ts +17 -0
- package/src/lens-shader.ts +547 -0
- package/src/material.ts +553 -0
- package/src/native/environment.ts +70 -0
- package/src/native/glass-group.tsx +107 -0
- package/src/native/glass-ink.tsx +44 -0
- package/src/native/glass-surface.tsx +477 -0
- package/src/native/index.ts +18 -0
- package/src/native/lens.ts +106 -0
- package/src/native/provider.tsx +58 -0
- package/src/native/surface-registry.ts +51 -0
- package/src/native/system-accessibility.ts +90 -0
- package/src/optics.ts +177 -0
- package/src/react.ts +102 -0
- package/src/reference-scene.ts +222 -0
- package/src/scroll-edge.ts +30 -0
- package/src/sdf.ts +149 -0
- package/src/surface-shader.ts +287 -0
- package/src/targets/agsl.ts +8 -0
- package/src/targets/glsl.ts +140 -0
- package/src/touch-response.ts +195 -0
- package/src/web/gl.ts +119 -0
- package/src/web/index.ts +3 -0
- package/src/web/probe.ts +312 -0
- package/src/web/reference-draw.ts +142 -0
- package/src/web/renderer.ts +524 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { VG_SDF } from './sdf';
|
|
2
|
+
|
|
3
|
+
// The lens source is assembled HERE and ships to the native view as a prop: AGSL and SKSL are one
|
|
4
|
+
// language, so the lens's and the surface's geometry are literally the same string (`VG_SDF`).
|
|
5
|
+
//
|
|
6
|
+
// Glass is a thick plate with a convex rounded edge (docs/reference.md §1). A ray from above
|
|
7
|
+
// refracts at the top face and lands on the content shifted INWARD relative to the shape: the
|
|
8
|
+
// middle isn't shifted, the rim shows what lies beneath the body of the element.
|
|
9
|
+
//
|
|
10
|
+
// Uniforms, apart from the view's own geometry, arrive over a SHARED CHANNEL (`adapters.ts` →
|
|
11
|
+
// `uniformNames`/`uniformSizes`/`uniformValues`). There are no more separate Props per value:
|
|
12
|
+
// those were exactly what let a typo silently turn the lens off (docs/material-lab.md E-01).
|
|
13
|
+
export const LENS_SHADER = `
|
|
14
|
+
uniform shader content;
|
|
15
|
+
|
|
16
|
+
// Known only to the view itself: its own size and its own place on screen.
|
|
17
|
+
uniform float2 u_center;
|
|
18
|
+
uniform float u_reach;
|
|
19
|
+
uniform float2 u_contentMin;
|
|
20
|
+
uniform float2 u_contentMax;
|
|
21
|
+
// Backdrop estimate UNDER the glass from the native probe: lightness, variegation and average
|
|
22
|
+
// color, already smoothed over time. Negative lightness means the probe hasn't reported yet —
|
|
23
|
+
// fall back to our own samples.
|
|
24
|
+
uniform float u_probeLuma;
|
|
25
|
+
uniform float u_probeBusy;
|
|
26
|
+
// The endpoints of the lightness range under the glass: right at a black/white border the
|
|
27
|
+
// average reads as a gray that says "everything's fine," while the ink drowns over the light
|
|
28
|
+
// half.
|
|
29
|
+
uniform float2 u_probeRange;
|
|
30
|
+
// Slope of lightness across the surface, fraction per half-size: tinting is a gradient — one
|
|
31
|
+
// density per element wouldn't separate the ink from both halves of the backdrop.
|
|
32
|
+
uniform float2 u_probeSlope;
|
|
33
|
+
uniform float3 u_probe;
|
|
34
|
+
|
|
35
|
+
// Material — over the shared channel from JS, already in device pixels.
|
|
36
|
+
uniform float2 u_halfSize;
|
|
37
|
+
uniform float u_corner;
|
|
38
|
+
uniform float u_bevel;
|
|
39
|
+
uniform float u_thick;
|
|
40
|
+
uniform float u_rim;
|
|
41
|
+
uniform float u_ior;
|
|
42
|
+
uniform float u_iorSpread;
|
|
43
|
+
uniform float2 u_light;
|
|
44
|
+
uniform float u_appear;
|
|
45
|
+
uniform float4 u_accent;
|
|
46
|
+
uniform float u_frost;
|
|
47
|
+
uniform float u_ink;
|
|
48
|
+
uniform float u_legibility;
|
|
49
|
+
uniform float u_dim;
|
|
50
|
+
uniform float u_presence;
|
|
51
|
+
uniform float u_progress;
|
|
52
|
+
uniform float u_adaptRadius;
|
|
53
|
+
uniform float u_bodyDensity;
|
|
54
|
+
uniform float u_edgeLight;
|
|
55
|
+
uniform float u_fresnel;
|
|
56
|
+
uniform float u_specular;
|
|
57
|
+
uniform float u_reflectReach;
|
|
58
|
+
uniform float u_film;
|
|
59
|
+
uniform float u_iridescence;
|
|
60
|
+
uniform float u_diffraction;
|
|
61
|
+
uniform float u_colorPickup;
|
|
62
|
+
uniform float2 u_morphOffset;
|
|
63
|
+
uniform float2 u_morphHalf;
|
|
64
|
+
uniform float u_morphCorner;
|
|
65
|
+
uniform float u_morphK;
|
|
66
|
+
uniform float2 u_morph2Offset;
|
|
67
|
+
uniform float2 u_morph2Half;
|
|
68
|
+
uniform float u_morph2Corner;
|
|
69
|
+
uniform float u_debug;
|
|
70
|
+
uniform float2 u_touch;
|
|
71
|
+
uniform float2 u_pull;
|
|
72
|
+
uniform float u_touchPress;
|
|
73
|
+
uniform float u_touchRadius;
|
|
74
|
+
uniform float2 u_wave;
|
|
75
|
+
|
|
76
|
+
${VG_SDF}
|
|
77
|
+
|
|
78
|
+
// Sample count for the disc gather: fewer than this reads as a staircase of copies at a sharp edge.
|
|
79
|
+
const int VG_FROST_TAPS = 20;
|
|
80
|
+
// The sample count grows with the disc's area: estimator noise is the spread under the glass
|
|
81
|
+
// divided by the square root of the sample count. The ceiling is cost: texture sampling is the
|
|
82
|
+
// most expensive thing here.
|
|
83
|
+
const int VG_FROST_TAPS_MAX = 64;
|
|
84
|
+
// The radius correction for backdrop compression at the rim is capped: at the silhouette it goes
|
|
85
|
+
// to infinity.
|
|
86
|
+
const float VG_FOOTPRINT_MAX = 1.8;
|
|
87
|
+
const float VG_TAU = 6.28318530718;
|
|
88
|
+
// Ceiling on body lightness under light ink (and the mirrored floor under dark ink). This is a
|
|
89
|
+
// ceiling, not a difference: "0.14 darker than white" is a lightness of 0.86, at which white text
|
|
90
|
+
// is invisible.
|
|
91
|
+
const float VG_BODY_CAP_LOOSE = 0.62;
|
|
92
|
+
const float VG_BODY_CAP_TIGHT = 0.38;
|
|
93
|
+
// Tint lightness in both directions: real glass is never coal-black or paper-white.
|
|
94
|
+
const float VG_TINT_DARK = 0.07;
|
|
95
|
+
const float VG_TINT_LIGHT = 0.94;
|
|
96
|
+
// Reference channel wavelengths, nm — for diffraction and interference.
|
|
97
|
+
const float3 VG_LAMBDA = float3(610.0, 550.0, 460.0);
|
|
98
|
+
// Film index of refraction: every thin film on glass sits around this value.
|
|
99
|
+
const float VG_FILM_IOR = 1.35;
|
|
100
|
+
// Ceiling on the profile's slope right at the silhouette: it goes to infinity there.
|
|
101
|
+
const float VG_SLOPE_MAX = 40.0;
|
|
102
|
+
// Fraction of the gather radius that scattering reaches over a busy backdrop under ink (M 11:47).
|
|
103
|
+
// Set against the reference: there, structure under the capsule fades by a factor of 9-10, not
|
|
104
|
+
// thirty.
|
|
105
|
+
const float VG_SCATTER_MAX = 0.20;
|
|
106
|
+
const float VG_SCATTER_BASE = 0.1;
|
|
107
|
+
// The response to structure under the glass saturates early: what competes with the ink isn't
|
|
108
|
+
// the area of foreign text but the mere fact that it's there (a line under a tile gives a busy
|
|
109
|
+
// of about 0.12).
|
|
110
|
+
const float VG_STRUCTURE_GAIN = 20.0;
|
|
111
|
+
// How much denser the body with ink gets over a BUSY backdrop than over a calm one. There's no
|
|
112
|
+
// flat floor here: legibility picks up its own targeted requirement further down, and base
|
|
113
|
+
// frosting comes from VG_MATTE_LIFT.
|
|
114
|
+
const float VG_GROUND_SPAN = 0.06;
|
|
115
|
+
// Scattered light on a frosted element is an ADDITION on top of the backdrop, not a fraction of
|
|
116
|
+
// the way to the tint: the fraction goes to zero once the canvas reaches the tint's lightness,
|
|
117
|
+
// and darkens past it (docs/benchmarks.md).
|
|
118
|
+
const float VG_MATTE_LIFT = 0.10;
|
|
119
|
+
// The fraction of backdrop spread that survives through to the body past scattering: the
|
|
120
|
+
// legibility requirement is computed from this edge, not from the spot's average lightness.
|
|
121
|
+
const float VG_BUSY_EDGE = 0.75;
|
|
122
|
+
// Light concentration: the body is a touch lighter than what's beneath it (M 2:29).
|
|
123
|
+
const float VG_CONCENTRATE = 0.01;
|
|
124
|
+
// The lightness the medium pulls content under the glass toward, and the strength of that pull.
|
|
125
|
+
// The medium both removes light and mixes in scattered light: over a light backdrop the body
|
|
126
|
+
// darkens, over a dark one it lightens.
|
|
127
|
+
const float VG_MEDIUM_LUMA = 0.40;
|
|
128
|
+
const float VG_MEDIUM_PULL = 0.07;
|
|
129
|
+
// How much ambient light reaches the body on top of the pull.
|
|
130
|
+
const float VG_AMBIENT_SPILL = 0.01;
|
|
131
|
+
|
|
132
|
+
float vgLuma(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }
|
|
133
|
+
|
|
134
|
+
// Un-premultiply into ordinary color. One division, done in float: near zero, half's step is too
|
|
135
|
+
// coarse, and several divisions in a row raise noise.
|
|
136
|
+
float3 vgUnpack(half4 c) {
|
|
137
|
+
float a = float(c.a);
|
|
138
|
+
return a > 0.004 ? float3(c.rgb) / a : float3(0.0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The capture ends at the screen edge: samples are clamped to the rectangle where content
|
|
142
|
+
// actually exists, otherwise full-width glass picked up a band of emptiness along its edges.
|
|
143
|
+
float2 vgInContent(float2 q) { return clamp(q, u_contentMin, u_contentMax); }
|
|
144
|
+
|
|
145
|
+
// Hue without lightness. Below the threshold the color has no hue, and the division blows up.
|
|
146
|
+
float3 vgHue(float3 c) {
|
|
147
|
+
float l = vgLuma(c);
|
|
148
|
+
if (l < 0.02) { return float3(1.0); }
|
|
149
|
+
return clamp(c / l, float3(0.0), float3(2.0));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Edge profile — a convex squircle: x = 0 at the silhouette, 1 where the rounding turns flat.
|
|
153
|
+
float vgRimQ(float x) {
|
|
154
|
+
float k = 1.0 - x;
|
|
155
|
+
return sqrt(sqrt(max(1.0 - k * k * k * k, 0.0)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
float vgRimQd(float x) {
|
|
159
|
+
float k = 1.0 - x;
|
|
160
|
+
return k * k * k / pow(max(1.0 - k * k * k * k, 1e-5), 0.75);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Glass height above the content at distance e from the silhouette.
|
|
164
|
+
float vgHeight(float e, float w) {
|
|
165
|
+
return mix(u_rim, u_thick, vgRimQ(clamp(e / w, 0.0, 1.0)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Slope of the top face at the same point: the derivative of height with respect to distance
|
|
169
|
+
// from the silhouette.
|
|
170
|
+
float vgSlope(float e, float w) {
|
|
171
|
+
if (e >= w) { return 0.0; }
|
|
172
|
+
return min((u_thick - u_rim) / w * vgRimQd(clamp(e / w, 0.0, 1.0)), VG_SLOPE_MAX);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Where a ray falling from above lands on a face with normal N: Snell's law refraction, then a
|
|
176
|
+
// path of length z through the medium to the content. The convex face bends the ray toward the
|
|
177
|
+
// normal — that is, inward relative to the shape.
|
|
178
|
+
float2 vgShift(float3 N, float z, float ior) {
|
|
179
|
+
float3 T = refract(float3(0.0, 0.0, -1.0), N, 1.0 / ior);
|
|
180
|
+
return z * T.xy / max(-T.z, 0.05);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Thin-film interference: optical path difference 2·n·d·cosθt, each channel with its own λ.
|
|
184
|
+
// Returns a hue normalized to its own average — it colors, but doesn't lighten.
|
|
185
|
+
float3 vgInterference(float cosI) {
|
|
186
|
+
float sinT2 = (1.0 - cosI * cosI) / (VG_FILM_IOR * VG_FILM_IOR);
|
|
187
|
+
float cosT = sqrt(max(1.0 - sinT2, 0.0));
|
|
188
|
+
float opd = 2.0 * VG_FILM_IOR * u_film * cosT;
|
|
189
|
+
float3 i = 0.5 + 0.5 * cos(VG_TAU * opd / VG_LAMBDA + 3.14159265);
|
|
190
|
+
return i / max((i.r + i.g + i.b) / 3.0, 0.001);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Edge diffraction: fringes get denser the sharper the bevel. Also a hue, not a brightness.
|
|
194
|
+
float3 vgDiffraction(float distFromEdge, float bevel) {
|
|
195
|
+
float phase = VG_TAU * 4.0 * distFromEdge / max(bevel, 1.0);
|
|
196
|
+
float3 d = 0.5 + 0.5 * cos(phase * (550.0 / VG_LAMBDA));
|
|
197
|
+
return d / max((d.r + d.g + d.b) / 3.0, 0.001);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
float vgHash(float2 p) {
|
|
201
|
+
return fract(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Soft compression instead of a hard clamp: over a white backdrop, additions used to knock a
|
|
205
|
+
// channel to one, and the glass turned into a flat blob with no detail.
|
|
206
|
+
float3 vgSoftClip(float3 c) {
|
|
207
|
+
float3 over = max(c - 0.86, float3(0.0));
|
|
208
|
+
return min(c, float3(0.86)) + over * 0.14 / (0.14 + over);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Our own blur: the capture is left clean, the shader does the blurring — averaging in float
|
|
212
|
+
// smooths banding on its own and adds no noise of its own. A golden-angle spiral, rotated to its
|
|
213
|
+
// own angle at every pixel: with a shared angle, neighboring pixels took the same samples, and
|
|
214
|
+
// texture under the glass read in clumps. Accumulated premultiplied — one shared un-premultiply
|
|
215
|
+
// at the end.
|
|
216
|
+
float4 vgGather(float2 q, float radius, float2 seed) {
|
|
217
|
+
float4 acc = float4(content.eval(vgInContent(q)));
|
|
218
|
+
if (radius <= 0.25) { return acc; }
|
|
219
|
+
if (radius < 4.0) {
|
|
220
|
+
float r = radius * 0.7;
|
|
221
|
+
acc += float4(content.eval(vgInContent(q + float2(r, 0.0))))
|
|
222
|
+
+ float4(content.eval(vgInContent(q - float2(r, 0.0))))
|
|
223
|
+
+ float4(content.eval(vgInContent(q + float2(0.0, r))))
|
|
224
|
+
+ float4(content.eval(vgInContent(q - float2(0.0, r))));
|
|
225
|
+
return acc * 0.2;
|
|
226
|
+
}
|
|
227
|
+
float ca = cos(2.39996323);
|
|
228
|
+
float sa = sin(2.39996323);
|
|
229
|
+
float a0 = vgHash(seed) * 6.28318530718;
|
|
230
|
+
float2 dir = float2(cos(a0), sin(a0));
|
|
231
|
+
float wide = radius * 0.5;
|
|
232
|
+
int taps = int(clamp(float(VG_FROST_TAPS) * wide * wide, float(VG_FROST_TAPS), float(VG_FROST_TAPS_MAX)));
|
|
233
|
+
for (int i = 1; i <= VG_FROST_TAPS_MAX; i++) {
|
|
234
|
+
if (i > taps) { break; }
|
|
235
|
+
dir = float2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);
|
|
236
|
+
float r = radius * sqrt(float(i) / float(taps));
|
|
237
|
+
acc += float4(content.eval(vgInContent(q + dir * r)));
|
|
238
|
+
}
|
|
239
|
+
return acc / float(taps + 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
half4 main(float2 xy) {
|
|
243
|
+
float2 p = vgTouchWarp(xy - u_center, u_touch, u_pull, u_touchPress, u_touchRadius, u_wave.x, u_wave.y);
|
|
244
|
+
float sd = vgScene(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
245
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
246
|
+
// There's nothing outside the glass — except the light it spills onto the backdrop under the
|
|
247
|
+
// finger (M 3:38): the same concentrated ambient as inside, so no white halo appears over a
|
|
248
|
+
// dark backdrop.
|
|
249
|
+
if (sd > 1.0) {
|
|
250
|
+
float spill = u_touchPress * exp(-sd / max(min(u_halfSize.x, u_halfSize.y) * 0.5, 4.0)) * 0.5 * u_appear;
|
|
251
|
+
if (spill < 0.004) { return half4(0.0); }
|
|
252
|
+
float3 lit = clamp((u_probeLuma >= 0.0 ? u_probe : float3(0.35)) * 1.6 + float3(0.1), float3(0.0), float3(1.0));
|
|
253
|
+
return half4(half3(lit * spill), half(spill));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
float bevel = max(u_bevel, 1.0);
|
|
257
|
+
float e = max(-sd, 0.0);
|
|
258
|
+
// 0 in the flat middle, 1 at the silhouette.
|
|
259
|
+
float t = 1.0 - clamp(e / bevel, 0.0, 1.0);
|
|
260
|
+
float2 n = vgSceneNormal(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
261
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
262
|
+
|
|
263
|
+
// "backdrop" mode hands back the content as-is — a reference point for comparing the optics.
|
|
264
|
+
float on = u_debug > 5.5 && u_debug < 6.5 ? 0.0 : 1.0;
|
|
265
|
+
float lens = on * u_appear;
|
|
266
|
+
|
|
267
|
+
float z = vgHeight(e, bevel);
|
|
268
|
+
float slope = vgSlope(e, bevel);
|
|
269
|
+
float3 N = normalize(float3(n * slope, 1.0));
|
|
270
|
+
float2 shift = vgShift(N, z, u_ior) * lens;
|
|
271
|
+
float2 s = u_center + p + shift;
|
|
272
|
+
|
|
273
|
+
// The area the lens gathers into one pixel: a one-pixel step inward on screen, measured
|
|
274
|
+
// against the sampling step. At the silhouette it's large — a stripe of backdrop is compressed
|
|
275
|
+
// into a line there, and a point sample gives grain.
|
|
276
|
+
float e1 = e + 1.0;
|
|
277
|
+
float3 N1 = normalize(float3(n * vgSlope(e1, bevel), 1.0));
|
|
278
|
+
float2 shift1 = vgShift(N1, vgHeight(e1, bevel), u_ior) * lens;
|
|
279
|
+
float footprint = length(shift1 - shift - n);
|
|
280
|
+
float spread = max(footprint - 1.0, 0.0) * 0.5 * on;
|
|
281
|
+
|
|
282
|
+
// Dispersion: the same ray with each channel's own index. Blue bends more than red.
|
|
283
|
+
float2 dR = vgShift(N, z, u_ior - 0.4 * u_iorSpread) * lens - shift;
|
|
284
|
+
float2 dB = vgShift(N, z, u_ior + 0.6 * u_iorSpread) * lens - shift;
|
|
285
|
+
float chroma = length(dB - dR);
|
|
286
|
+
|
|
287
|
+
float3 rgb;
|
|
288
|
+
float srcA;
|
|
289
|
+
|
|
290
|
+
if (chroma + spread < 0.25) {
|
|
291
|
+
// In the flat body all the samples would land at the same point, and the body is almost the
|
|
292
|
+
// whole area of the glass.
|
|
293
|
+
half4 c = content.eval(vgInContent(s));
|
|
294
|
+
srcA = float(c.a);
|
|
295
|
+
rgb = srcA > 0.004 ? float3(c.rgb) / srcA : float3(0.0);
|
|
296
|
+
} else {
|
|
297
|
+
// Accumulate premultiplied and divide once per pair: channels from different points with
|
|
298
|
+
// different alpha would otherwise produce a colored fringe at content edges that isn't
|
|
299
|
+
// really there.
|
|
300
|
+
float2 ev = n * spread;
|
|
301
|
+
half4 c0 = content.eval(vgInContent(s + dR - ev));
|
|
302
|
+
half4 c1 = content.eval(vgInContent(s + dR + ev));
|
|
303
|
+
half4 c2 = content.eval(vgInContent(s - ev));
|
|
304
|
+
half4 c3 = content.eval(vgInContent(s + ev));
|
|
305
|
+
half4 c4 = content.eval(vgInContent(s + dB - ev));
|
|
306
|
+
half4 c5 = content.eval(vgInContent(s + dB + ev));
|
|
307
|
+
|
|
308
|
+
float aR = float(c0.a + c1.a) * 0.5;
|
|
309
|
+
float aG = float(c2.a + c3.a) * 0.5;
|
|
310
|
+
float aB = float(c4.a + c5.a) * 0.5;
|
|
311
|
+
rgb = float3(
|
|
312
|
+
aR > 0.004 ? float(c0.r + c1.r) * 0.5 / aR : 0.0,
|
|
313
|
+
aG > 0.004 ? float(c2.g + c3.g) * 0.5 / aG : 0.0,
|
|
314
|
+
aB > 0.004 ? float(c4.b + c5.b) * 0.5 / aB : 0.0);
|
|
315
|
+
srcA = (aR + aG + aB) / 3.0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// "backdrop" mode checks the CAPTURE, not a simplified version of the optics. What follows are
|
|
319
|
+
// layers that aren't tied to the lens (body, medium, scattering), and they used to conflate
|
|
320
|
+
// "did the pixels arrive" with "how did we process them." Here the element must DISAPPEAR: a
|
|
321
|
+
// visible silhouette means a capture loss.
|
|
322
|
+
if (u_debug > 5.5 && u_debug < 6.5) {
|
|
323
|
+
float bypass = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));
|
|
324
|
+
return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * bypass), half(bypass));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The transparent variant's dimming layer holds legibility instead of the body. A
|
|
328
|
+
// multiplication, not a tint: the content has to stay visible, just quieter.
|
|
329
|
+
rgb *= 1.0 - u_dim * u_appear;
|
|
330
|
+
|
|
331
|
+
// REFLECTION. Fresnel from the face's actual slope: at the silhouette the view is grazing and
|
|
332
|
+
// reflection is nearly total, which is why the rim reads as a thin line on its own. What's
|
|
333
|
+
// reflected is the element's surroundings and the key light.
|
|
334
|
+
float cosT = clamp(N.z, 0.0, 1.0);
|
|
335
|
+
float f0 = (u_ior - 1.0) / (u_ior + 1.0);
|
|
336
|
+
f0 *= f0;
|
|
337
|
+
float fres = (f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0)) * u_fresnel * lens;
|
|
338
|
+
|
|
339
|
+
float lineW = clamp(min(u_halfSize.x, u_halfSize.y) * 0.03, 1.5, 5.0);
|
|
340
|
+
float3 refl;
|
|
341
|
+
if (e < lineW * 2.0 || fres > 0.01) {
|
|
342
|
+
float2 around = u_center + p + n * (u_reflectReach * mix(0.35, 1.0, clamp(slope / 3.2, 0.0, 1.0)));
|
|
343
|
+
// Two rings of eight samples total: a single offset sample is an undistorted COPY of
|
|
344
|
+
// neighboring content inside the glass (docs/material-lab.md E-33).
|
|
345
|
+
float p1 = u_reflectReach * 0.75;
|
|
346
|
+
float p2 = u_reflectReach * 0.4;
|
|
347
|
+
float d = 0.7071;
|
|
348
|
+
float4 rsum = float4(content.eval(vgInContent(around + float2(p1, 0.0))))
|
|
349
|
+
+ float4(content.eval(vgInContent(around - float2(p1, 0.0))))
|
|
350
|
+
+ float4(content.eval(vgInContent(around + float2(0.0, p1))))
|
|
351
|
+
+ float4(content.eval(vgInContent(around - float2(0.0, p1))))
|
|
352
|
+
+ float4(content.eval(vgInContent(around + float2(p2, p2) * d)))
|
|
353
|
+
+ float4(content.eval(vgInContent(around + float2(-p2, p2) * d)))
|
|
354
|
+
+ float4(content.eval(vgInContent(around + float2(p2, -p2) * d)))
|
|
355
|
+
+ float4(content.eval(vgInContent(around + float2(-p2, -p2) * d)));
|
|
356
|
+
float ra = rsum.a * 0.125;
|
|
357
|
+
refl = ra > 0.004 ? rsum.rgb * 0.125 / ra : float3(0.0);
|
|
358
|
+
} else {
|
|
359
|
+
refl = u_probeLuma >= 0.0 ? u_probe : rgb;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Light reaching the body is the same across the whole element: blending with the surroundings
|
|
363
|
+
// over the bevel's band used to paint a ring-shaped seam on the body.
|
|
364
|
+
float3 ambient = u_probeLuma >= 0.0 ? u_probe : rgb;
|
|
365
|
+
|
|
366
|
+
// Light comes from wherever the surroundings are brighter (M 11:04): the lightness slope under
|
|
367
|
+
// the element pulls the key light toward itself, and over a flat backdrop it stays at rest.
|
|
368
|
+
float2 bright = u_probeLuma >= 0.0 ? u_probeSlope : float2(0.0);
|
|
369
|
+
float brightLen = length(bright);
|
|
370
|
+
float2 L = normalize(mix(u_light, bright / max(brightLen, 1e-4), smoothstep(0.03, 0.2, brightLen)) + float2(1e-5));
|
|
371
|
+
// The key light's reflection uses the face's actual normal: at the flat top it points straight
|
|
372
|
+
// up, and there's nothing there for direction to depend on. Using the SDF normal instead
|
|
373
|
+
// painted the body as a cone.
|
|
374
|
+
float facing = dot(N.xy, L);
|
|
375
|
+
float key = u_specular * (pow(max(facing, 0.0), 3.0) + 0.45 * pow(max(-facing, 0.0), 3.0));
|
|
376
|
+
float rimFacing = dot(n, L);
|
|
377
|
+
float rimKey = u_specular * (pow(max(rimFacing, 0.0), 3.0) + 0.45 * pow(max(-rimFacing, 0.0), 3.0));
|
|
378
|
+
// Wherever the key light doesn't fall, the face reflects its shaded surroundings — hence the
|
|
379
|
+
// dark outline.
|
|
380
|
+
float3 env = refl * (0.55 + 0.45 * min(key, 1.0)) + float3(1.2 * key);
|
|
381
|
+
|
|
382
|
+
// Interference and diffraction live in the reflected ray at a grazing angle — a single hue
|
|
383
|
+
// multiplier applied to the reflection, no extra samples.
|
|
384
|
+
float3 spectral = float3(1.0);
|
|
385
|
+
if (u_iridescence > 0.001) {
|
|
386
|
+
spectral *= mix(float3(1.0), vgInterference(cosT), u_iridescence);
|
|
387
|
+
}
|
|
388
|
+
if (u_diffraction > 0.001) {
|
|
389
|
+
float w = u_diffraction * smoothstep(0.45, 1.0, t);
|
|
390
|
+
spectral *= mix(float3(1.0), vgDiffraction(e, bevel), w);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The BACKDROP ESTIMATE arrives from the probe as one value per surface: computing it here
|
|
394
|
+
// from samples isn't an option — a kink in density used to turn the estimate's discreteness
|
|
395
|
+
// into ghost copies of text.
|
|
396
|
+
float3 wide;
|
|
397
|
+
float busy;
|
|
398
|
+
if (u_probeLuma >= 0.0) {
|
|
399
|
+
wide = u_probe;
|
|
400
|
+
busy = u_probeBusy;
|
|
401
|
+
} else {
|
|
402
|
+
// The probe hasn't reported yet (first frames) — fall back to our own samples.
|
|
403
|
+
float2 wx = float2(u_adaptRadius, 0.0);
|
|
404
|
+
float2 wy = float2(0.0, u_adaptRadius);
|
|
405
|
+
float3 w0 = vgUnpack(content.eval(vgInContent(s + wx)));
|
|
406
|
+
float3 w1 = vgUnpack(content.eval(vgInContent(s - wx)));
|
|
407
|
+
float3 w2 = vgUnpack(content.eval(vgInContent(s + wy)));
|
|
408
|
+
float3 w3 = vgUnpack(content.eval(vgInContent(s - wy)));
|
|
409
|
+
wide = (w0 + w1 + w2 + w3) * 0.25;
|
|
410
|
+
float lw = vgLuma(wide);
|
|
411
|
+
// The same statistic the probe uses (twice the mean deviation): both blur and the ink's
|
|
412
|
+
// contrast margin depend on it, and the two paths' scales must not diverge.
|
|
413
|
+
busy = clamp(0.5 * (abs(vgLuma(w0) - lw) + abs(vgLuma(w1) - lw)
|
|
414
|
+
+ abs(vgLuma(w2) - lw) + abs(vgLuma(w3) - lw)), 0.0, 1.0);
|
|
415
|
+
}
|
|
416
|
+
// Lightness AT THIS SPOT on the surface: the probe's plane, clamped to the measured range.
|
|
417
|
+
float2 nrm = p / max(u_halfSize, float2(1.0));
|
|
418
|
+
float local = u_probeLuma >= 0.0
|
|
419
|
+
? clamp(u_probeLuma + dot(u_probeSlope, nrm), u_probeRange.x, u_probeRange.y)
|
|
420
|
+
: vgLuma(wide);
|
|
421
|
+
float mean = u_probeLuma >= 0.0 ? u_probeLuma : vgLuma(wide);
|
|
422
|
+
|
|
423
|
+
// SCATTERING. Regular mutes foreign structure under its own ink with blur, not a fill (M
|
|
424
|
+
// 11:47): text under a capsule turns into blotches. Uniform across the whole lens; the radius
|
|
425
|
+
// is measured in backdrop space — the lens compresses it at the silhouette.
|
|
426
|
+
float structure = 1.0 - exp(-busy * VG_STRUCTURE_GAIN);
|
|
427
|
+
// Under ink, Regular always scatters the backdrop; over a busy one, more strongly.
|
|
428
|
+
float adaptBlur = max(
|
|
429
|
+
(structure * VG_SCATTER_MAX + VG_SCATTER_BASE) * u_legibility * u_adaptRadius
|
|
430
|
+
* min(max(footprint, 1.0), VG_FOOTPRINT_MAX),
|
|
431
|
+
u_frost) * u_appear;
|
|
432
|
+
if (adaptBlur > 0.5) {
|
|
433
|
+
float reach = max(u_reach - length(p), 1.0);
|
|
434
|
+
float4 g = vgGather(s, min(adaptBlur, reach), xy);
|
|
435
|
+
float ga = g.a;
|
|
436
|
+
float3 blurred = ga > 0.004 ? g.rgb / ga : float3(0.0);
|
|
437
|
+
rgb = mix(rgb, blurred, smoothstep(0.5, 2.0, adaptBlur));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// AFTER scattering, not before: scattering is a property of TRANSMITTED light and replaces rgb
|
|
441
|
+
// wholesale, so doing it the other way around erased the reflection over the whole element
|
|
442
|
+
// (issue #106).
|
|
443
|
+
rgb = mix(rgb, env * spectral, fres);
|
|
444
|
+
|
|
445
|
+
// BODY. Tint and dynamic range under the glass compress just enough for ink over it to stay
|
|
446
|
+
// legible (M 6:42). u_ink is ink POLARITY (1 light, 0 dark): the requirements are computed at
|
|
447
|
+
// the two ends and blended by it, otherwise the glass dips toward the middle during a recolor.
|
|
448
|
+
float strict = clamp(u_legibility * 2.0, 0.0, 1.0);
|
|
449
|
+
float capLight = mix(VG_BODY_CAP_LOOSE, VG_BODY_CAP_TIGHT, strict);
|
|
450
|
+
float floorDark = 1.0 - capLight;
|
|
451
|
+
float pol = clamp(u_ink, 0.0, 1.0);
|
|
452
|
+
// The requirement fades together with legibility: an element with no ink is promised
|
|
453
|
+
// transparent glass by the model.
|
|
454
|
+
float demand = clamp(u_legibility * 4.0, 0.0, 1.0);
|
|
455
|
+
|
|
456
|
+
// Where there's no ink, the glass tints away from the backdrop. The direction is chosen by the
|
|
457
|
+
// average lightness under the element, not the local spot's lightness: otherwise on a gradient
|
|
458
|
+
// the threshold cuts across the body as a diagonal step.
|
|
459
|
+
float darkSide = smoothstep(0.42, 0.58, mean);
|
|
460
|
+
float away = mix(VG_TINT_LIGHT, VG_TINT_DARK, darkSide);
|
|
461
|
+
float tintLuma = mix(away, mix(VG_TINT_LIGHT, VG_TINT_DARK, pol), demand);
|
|
462
|
+
|
|
463
|
+
// How much medium it takes to push lightness under the ink past the threshold — locally.
|
|
464
|
+
// Computed from whichever edge of the backdrop's spread is closer to the ink: a light ink
|
|
465
|
+
// drowns in a light spot right beneath it, and the spot's average lightness knows nothing
|
|
466
|
+
// about that spot.
|
|
467
|
+
float edge = busy * VG_BUSY_EDGE;
|
|
468
|
+
float inkHi = min(local + edge, 1.0);
|
|
469
|
+
float inkLo = max(local - edge, 0.0);
|
|
470
|
+
float needForLight = inkHi > capLight
|
|
471
|
+
? clamp((inkHi - capLight) / max(inkHi - VG_TINT_DARK, 1e-4), 0.0, 0.92)
|
|
472
|
+
: 0.0;
|
|
473
|
+
float needForDark = inkLo < floorDark
|
|
474
|
+
? clamp((floorDark - inkLo) / max(VG_TINT_LIGHT - inkLo, 1e-4), 0.0, 0.92)
|
|
475
|
+
: 0.0;
|
|
476
|
+
float needForInk = mix(needForDark, needForLight, pol) * demand;
|
|
477
|
+
|
|
478
|
+
float ground = structure * u_legibility * VG_GROUND_SPAN;
|
|
479
|
+
float density = max(max(u_bodyDensity, ground), needForInk);
|
|
480
|
+
|
|
481
|
+
// Glass has no color of its own — only the color of what's beneath it (HIG "Color").
|
|
482
|
+
float3 tintHue = mix(float3(1.0), vgHue(wide * 0.5 + ambient * 0.5), u_colorPickup);
|
|
483
|
+
// The element appears by the lens and body building up, not by fading in (M 2:55): at
|
|
484
|
+
// u_appear = 0 it's indistinguishable from the backdrop beneath it.
|
|
485
|
+
rgb = mix(rgb, tintHue * tintLuma, density * u_appear);
|
|
486
|
+
rgb += tintHue * (u_legibility * VG_MATTE_LIFT * u_appear);
|
|
487
|
+
|
|
488
|
+
// TINTING — colored glass, not a fill (M 16:31, 17:03): the backdrop's lightness drives the
|
|
489
|
+
// tone — deeper over dark, lighter over light — and the content's texture still shows through
|
|
490
|
+
// the color.
|
|
491
|
+
if (u_accent.a > 0.0) {
|
|
492
|
+
float3 tone = u_accent.rgb * mix(0.88, 1.08, local);
|
|
493
|
+
float3 through = tone * (0.85 + 0.3 * vgLuma(rgb));
|
|
494
|
+
rgb = mix(rgb, clamp(through, float3(0.0), float3(1.0)), u_accent.a * u_appear);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// CONTRAST COMPRESSION. The medium removes some light and mixes in scattered light, so content
|
|
498
|
+
// under the glass moves toward the middle rather than simply getting lighter (M 2:35). The
|
|
499
|
+
// previous layer only added light and, over a light backdrop, pushed the body the opposite way
|
|
500
|
+
// from the reference.
|
|
501
|
+
rgb = mix(rgb, vgHue(ambient) * VG_MEDIUM_LUMA, VG_MEDIUM_PULL * u_appear);
|
|
502
|
+
// Ambient light reaches the body the same way across the whole element.
|
|
503
|
+
rgb += ambient * u_edgeLight * VG_AMBIENT_SPILL * u_appear;
|
|
504
|
+
// Glass concentrates light; the played portion is the segment where there's more of it.
|
|
505
|
+
float glow = VG_CONCENTRATE + 0.08 * vgProgress(p, u_halfSize, u_progress);
|
|
506
|
+
rgb += (1.0 - rgb) * glow * lens;
|
|
507
|
+
|
|
508
|
+
// Under the finger and during morphing, glass concentrates light into a spot (M 4:56, 5:11).
|
|
509
|
+
// This is a CONCENTRATION of the surroundings, not the glass's own whiteness: over a dark
|
|
510
|
+
// backdrop the element lightens but never turns white, and ink over it stays legible.
|
|
511
|
+
if (u_touchPress > 0.001) {
|
|
512
|
+
float2 fromTouch = p - u_touch;
|
|
513
|
+
float spotR = max(min(u_halfSize.x, u_halfSize.y) * 1.2, 1.0);
|
|
514
|
+
float spot = u_touchPress * (0.3 + 0.55 * exp(-dot(fromTouch, fromTouch) / (spotR * spotR)));
|
|
515
|
+
rgb = mix(rgb, clamp(ambient * 1.6 + float3(0.1), float3(0.0), float3(1.0)), spot * lens);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// RIM LIGHT — a separate layer, a line about 1 pt wide along the silhouette (M 2:36, 11:04):
|
|
519
|
+
// brighter where the face faces the key light, weaker opposite it; where no light falls, a
|
|
520
|
+
// dark outline. This line is what holds the element's presence, not a fill on the body: over a
|
|
521
|
+
// flat backdrop a control is visible along its whole silhouette because of it.
|
|
522
|
+
float line = (1.0 - smoothstep(0.0, lineW, e)) * lens;
|
|
523
|
+
float outline = (1.0 - smoothstep(0.0, lineW * 0.6, e)) * lens;
|
|
524
|
+
float lit = clamp(max(rimKey * 1.4, u_presence * 2.0), 0.0, 1.0);
|
|
525
|
+
// The dark rim is its own layer around the whole silhouette, and the highlight sits ON TOP of
|
|
526
|
+
// it (iOS 27). While the outline used to fade via a (1 − lit) multiplier, the silhouette read
|
|
527
|
+
// as a single arc: highlight OR shadow.
|
|
528
|
+
rgb *= 1.0 - 0.22 * outline;
|
|
529
|
+
rgb = mix(rgb, mix(refl, float3(1.0), 0.75), line * lit);
|
|
530
|
+
|
|
531
|
+
if (u_debug > 9.5 && u_debug < 10.5) { rgb = spectral * 0.5; }
|
|
532
|
+
if (u_debug > 10.5 && u_debug < 11.5) { rgb = float3(density, needForInk, structure); }
|
|
533
|
+
// WHAT THE PROBE SAID ABOUT THE BACKDROP — three values that drive all the tinting: the
|
|
534
|
+
// average lightness under the element, the vertical lightness slope, and the lightness AT THIS
|
|
535
|
+
// SPOT. The slope is shown offset and squeezed by half: it's signed and rarely exceeds a
|
|
536
|
+
// magnitude of one half.
|
|
537
|
+
if (u_debug > 11.5) { rgb = float3(max(u_probeLuma, 0.0), 0.5 + u_probeSlope.y * 0.5, local); }
|
|
538
|
+
|
|
539
|
+
rgb = vgSoftClip(rgb);
|
|
540
|
+
|
|
541
|
+
// Sample alpha has to survive to the result: un-premultiplying color by its original alpha and
|
|
542
|
+
// then returning it with a different one (the shape mask) would make a transparent backdrop
|
|
543
|
+
// opaque and blow it out.
|
|
544
|
+
float alpha = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));
|
|
545
|
+
return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * alpha), half(alpha));
|
|
546
|
+
}
|
|
547
|
+
`;
|