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,1990 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/geometry.ts
|
|
4
|
+
var halfMinDp = (g) => Math.max(Math.min(g.width, g.height) / 2, 1);
|
|
5
|
+
var MAX_STRETCH = 0.34;
|
|
6
|
+
var SIZE_REF_DP = 24;
|
|
7
|
+
var sizeGain = (g) => Math.min(Math.max(Math.sqrt(halfMinDp(g) / SIZE_REF_DP), 0.8), 2.4);
|
|
8
|
+
var MAX_BEVEL_FRACTION = 0.65;
|
|
9
|
+
var bevelFraction = (g, o) => Math.min(MAX_BEVEL_FRACTION, o.bevelDp * sizeGain(g) / halfMinDp(g));
|
|
10
|
+
var bevelDp = (g, o) => Math.max(bevelFraction(g, o) * halfMinDp(g), 1);
|
|
11
|
+
var thicknessDp = (g, o) => o.thicknessDp * sizeGain(g);
|
|
12
|
+
var RIM_FRACTION = 0.85;
|
|
13
|
+
var rimDp = (g, o) => thicknessDp(g, o) * RIM_FRACTION;
|
|
14
|
+
var shadowReachDp = (g) => Math.min(Math.max(halfMinDp(g) * 0.65, 6), 36);
|
|
15
|
+
function morphReachDp(g, morph) {
|
|
16
|
+
if (!morph || morph.smoothing <= 0) return 0;
|
|
17
|
+
return Math.max(
|
|
18
|
+
0,
|
|
19
|
+
Math.abs(morph.offsetX) + morph.width / 2 - g.width / 2,
|
|
20
|
+
Math.abs(morph.offsetY) + morph.height / 2 - g.height / 2
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
var PAD_STEP = 8;
|
|
24
|
+
var quantise = (v) => Math.ceil(v / PAD_STEP) * PAD_STEP;
|
|
25
|
+
function lensPadDp(g, o, morph, dragLimit = 0) {
|
|
26
|
+
const sampling = Math.max(o.blur, o.gatherRadiusDp);
|
|
27
|
+
const stretch = halfMinDp(g) * MAX_STRETCH;
|
|
28
|
+
return quantise(sampling + dragLimit + stretch + morphReachDp(g, morph) + 2);
|
|
29
|
+
}
|
|
30
|
+
function surfacePadDp(g, dragLimit = 0, morph) {
|
|
31
|
+
return quantise(shadowReachDp(g) * 1.2 + dragLimit + morphReachDp(g, morph) + 2);
|
|
32
|
+
}
|
|
33
|
+
var SHADOW_FLAT = 0.8;
|
|
34
|
+
var SHADOW_BUSY_GAIN = 6;
|
|
35
|
+
var SHADOW_BUSY_RISE = 1.2;
|
|
36
|
+
function shadowOpacity(busy) {
|
|
37
|
+
return SHADOW_FLAT + Math.min(Math.max(busy, 0) * SHADOW_BUSY_GAIN, SHADOW_BUSY_RISE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/sdf.ts
|
|
41
|
+
var VG_SDF = `
|
|
42
|
+
float vgRoundRect(float2 p, float2 halfSize, float corner) {
|
|
43
|
+
float2 q = abs(p) - halfSize + corner;
|
|
44
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - corner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
float vgSmin(float a, float b, float k) {
|
|
48
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
49
|
+
return mix(b, a, h) - k * h * (1.0 - h);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A scene is a single shape, and when k > 0 a smooth union with one or two neighbors. A third
|
|
53
|
+
// shape exists to break the control into pieces (docs/reference.md \xA75): there are two bridges
|
|
54
|
+
// there. It's turned off with a zero size, not its own k: the fused body shares one bridge.
|
|
55
|
+
float vgScene(float2 p, float2 halfSize, float corner,
|
|
56
|
+
float2 offsetB, float2 halfB, float cornerB, float k,
|
|
57
|
+
float2 offsetC, float2 halfC, float cornerC) {
|
|
58
|
+
float a = vgRoundRect(p, halfSize, corner);
|
|
59
|
+
if (k <= 0.0) { return a; }
|
|
60
|
+
float ab = vgSmin(a, vgRoundRect(p - offsetB, halfB, cornerB), k);
|
|
61
|
+
if (halfC.x <= 0.0) { return ab; }
|
|
62
|
+
return vgSmin(ab, vgRoundRect(p - offsetC, halfC, cornerC), k);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Response to a finger. It's the FIELD around the touch point that deforms, not the shape's
|
|
66
|
+
// bounding box: scaling width to pull the right edge would also pull the left one \u2014 liquid
|
|
67
|
+
// doesn't behave that way. Here the displacement falls off with distance from the finger, so the
|
|
68
|
+
// far edge stays put.
|
|
69
|
+
//
|
|
70
|
+
// The order of the terms matters: drag shifts the field, press pulls it toward the finger, the
|
|
71
|
+
// wave rides on top of the already-shifted field \u2014 otherwise the ripple decouples from the
|
|
72
|
+
// deformation and lives a life of its own.
|
|
73
|
+
float2 vgTouchWarp(float2 p, float2 touch, float2 pull, float press, float radius,
|
|
74
|
+
float waveAmp, float wavePhase) {
|
|
75
|
+
if (radius <= 0.0) { return p; }
|
|
76
|
+
// Under the finger the whole element grows (M 3:51; HIG: interactive "expands").
|
|
77
|
+
p /= 1.0 + 0.06 * press;
|
|
78
|
+
float2 d = p - touch;
|
|
79
|
+
float r = length(d);
|
|
80
|
+
|
|
81
|
+
// The finger is a BLOB, not a point, but not a hard stamp either: a wide peak, soft edges. A
|
|
82
|
+
// flat plateau with a sharp cutoff at the edge shifts the region as a block, and a straight
|
|
83
|
+
// line under the glass breaks into a step \u2014 the deformation reads as rectangular even though
|
|
84
|
+
// the finger is round. A double smoothstep gives the same contact width without that step.
|
|
85
|
+
//
|
|
86
|
+
// The influence cuts off at the radius rather than fading into an infinite tail like a
|
|
87
|
+
// Gaussian: with a tail the deformation spreads into a "rubber mattress" bulge.
|
|
88
|
+
float k = clamp((radius - r) / radius, 0.0, 1.0);
|
|
89
|
+
float s = k * k * (3.0 - 2.0 * k);
|
|
90
|
+
float core = s * s * (3.0 - 2.0 * s);
|
|
91
|
+
// A ring around the blob: material displaced from under the finger has to end up somewhere.
|
|
92
|
+
// Without this ridge the shape just balloons, and a dense medium doesn't behave that way.
|
|
93
|
+
float rim = k * k * (1.0 - k) * 4.0;
|
|
94
|
+
|
|
95
|
+
float2 q = p - pull * (core - 0.42 * rim);
|
|
96
|
+
q -= d * (press * 0.16 * core);
|
|
97
|
+
|
|
98
|
+
// The wave has its own scale, twice the drag radius: the ripple has to reach the far edge,
|
|
99
|
+
// otherwise it reads as jitter under the finger rather than a wave across the surface. The
|
|
100
|
+
// wavelength is short: in a dense medium ripples are frequent and small, long shallow swells
|
|
101
|
+
// are water.
|
|
102
|
+
if (waveAmp > 0.0 && r > 0.0001) {
|
|
103
|
+
float span = radius * 2.0;
|
|
104
|
+
float ring = sin(r / (span * 0.17) - wavePhase * 6.2831853) * exp(-r / (span * 0.7));
|
|
105
|
+
q -= (d / r) * ring * waveAmp;
|
|
106
|
+
}
|
|
107
|
+
return q;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Analytical normal of a single shape: radial on the rounded part, axis-aligned on the straight
|
|
111
|
+
// segments.
|
|
112
|
+
float2 vgRoundRectNormal(float2 p, float2 halfSize, float corner) {
|
|
113
|
+
float2 q = abs(p) - halfSize + corner;
|
|
114
|
+
float2 g = (q.x > 0.0 && q.y > 0.0)
|
|
115
|
+
? normalize(max(q, float2(0.0001)))
|
|
116
|
+
: (q.x > q.y ? float2(1.0, 0.0) : float2(0.0, 1.0));
|
|
117
|
+
return g * sign(p);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Normal of the union \u2014 ANALYTICAL, no more finite differences here.
|
|
121
|
+
//
|
|
122
|
+
// Derivation: smin has two terms, mix(b,a,h) and \u2212k\xB7h\xB7(1\u2212h). Their derivatives with respect to h
|
|
123
|
+
// contain a factor of (1\u22122h) with opposite signs and cancel exactly, because h is linear in
|
|
124
|
+
// (b\u2212a)/k. What's left is mix(\u2207b, \u2207a, h) \u2014 that is, the two shapes' normals, blended with the
|
|
125
|
+
// same weight that blends the distances themselves.
|
|
126
|
+
//
|
|
127
|
+
// The previous version took four ADDITIONAL scene evaluations per pixel (each one \u2014 two shapes
|
|
128
|
+
// plus smin) and was still an approximation. Here there are two shapes, exactly.
|
|
129
|
+
float2 vgSceneNormal(float2 p, float2 halfSize, float corner,
|
|
130
|
+
float2 offsetB, float2 halfB, float cornerB, float k,
|
|
131
|
+
float2 offsetC, float2 halfC, float cornerC) {
|
|
132
|
+
float2 na = vgRoundRectNormal(p, halfSize, corner);
|
|
133
|
+
if (k <= 0.0) { return na; }
|
|
134
|
+
float2 q = p - offsetB;
|
|
135
|
+
float a = vgRoundRect(p, halfSize, corner);
|
|
136
|
+
float b = vgRoundRect(q, halfB, cornerB);
|
|
137
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
138
|
+
float2 nab = normalize(mix(vgRoundRectNormal(q, halfB, cornerB), na, h) + float2(1e-5, 1e-5));
|
|
139
|
+
if (halfC.x <= 0.0) { return nab; }
|
|
140
|
+
// Same derivation as for the pair: normals blend with the same weight as the distances.
|
|
141
|
+
float2 qc = p - offsetC;
|
|
142
|
+
float ab = vgSmin(a, b, k);
|
|
143
|
+
float c = vgRoundRect(qc, halfC, cornerC);
|
|
144
|
+
float hc = clamp(0.5 + 0.5 * (c - ab) / k, 0.0, 1.0);
|
|
145
|
+
return normalize(mix(vgRoundRectNormal(qc, halfC, cornerC), nab, hc) + float2(1e-5, 1e-5));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Position within the bevel: 0 is the flat middle, 1 is the very edge. This one value feeds the
|
|
149
|
+
// mask, thickness, refraction, aberrations and the width of the light rim.
|
|
150
|
+
float vgBevelT(float sd, float bevel) {
|
|
151
|
+
return clamp((sd + bevel) / bevel, 0.0, 1.0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Slope of the bevel profile \u2014 spherical: t / sqrt(1 - t\xB2), like a spherical cap. The previous t\xB2
|
|
155
|
+
// kept the slope near zero through almost the whole bevel and then shot up right at the edge, so
|
|
156
|
+
// all the optics bunched into a narrow ring and the element read as a PUCK \u2014 a flat top and a
|
|
157
|
+
// wall around the rim. Here curvature is spread across the bevel, and the rim stops being a line.
|
|
158
|
+
//
|
|
159
|
+
// This still doesn't control the field of view: only the bevel bends, the middle stays flat,
|
|
160
|
+
// otherwise the surface reads as a soap bubble.
|
|
161
|
+
float vgBevelSlope(float t) {
|
|
162
|
+
return min(t * inversesqrt(max(1.0 - t * t * 0.94, 0.02)), 3.2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// PROGRESS FRACTION. Progress is an active state turned into a FIELD: to the left of the boundary
|
|
166
|
+
// the element is active, to the right it stays regular glass, and the boundary moves. A negative
|
|
167
|
+
// value turns the field off entirely \u2014 zero is taken by the start of the track and can't also
|
|
168
|
+
// mean off.
|
|
169
|
+
//
|
|
170
|
+
// The boundary is SOFT. A hard seam reads as two different materials glued together rather than
|
|
171
|
+
// two states of one; the transition width is a fraction of the half-width, so it doesn't read as
|
|
172
|
+
// a thread on a large element and doesn't swallow a small one whole.
|
|
173
|
+
float vgProgress(float2 p, float2 halfSize, float progress) {
|
|
174
|
+
if (progress < 0.0) { return 0.0; }
|
|
175
|
+
float edge = mix(-halfSize.x, halfSize.x, clamp(progress, 0.0, 1.0));
|
|
176
|
+
float soft = max(halfSize.x * 0.05, 1.0);
|
|
177
|
+
return 1.0 - smoothstep(edge - soft, edge + soft, p.x);
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
var VG_FALLOFF = 2.6;
|
|
181
|
+
|
|
182
|
+
// src/lens-shader.ts
|
|
183
|
+
var LENS_SHADER = `
|
|
184
|
+
uniform shader content;
|
|
185
|
+
|
|
186
|
+
// Known only to the view itself: its own size and its own place on screen.
|
|
187
|
+
uniform float2 u_center;
|
|
188
|
+
uniform float u_reach;
|
|
189
|
+
uniform float2 u_contentMin;
|
|
190
|
+
uniform float2 u_contentMax;
|
|
191
|
+
// Backdrop estimate UNDER the glass from the native probe: lightness, variegation and average
|
|
192
|
+
// color, already smoothed over time. Negative lightness means the probe hasn't reported yet \u2014
|
|
193
|
+
// fall back to our own samples.
|
|
194
|
+
uniform float u_probeLuma;
|
|
195
|
+
uniform float u_probeBusy;
|
|
196
|
+
// The endpoints of the lightness range under the glass: right at a black/white border the
|
|
197
|
+
// average reads as a gray that says "everything's fine," while the ink drowns over the light
|
|
198
|
+
// half.
|
|
199
|
+
uniform float2 u_probeRange;
|
|
200
|
+
// Slope of lightness across the surface, fraction per half-size: tinting is a gradient \u2014 one
|
|
201
|
+
// density per element wouldn't separate the ink from both halves of the backdrop.
|
|
202
|
+
uniform float2 u_probeSlope;
|
|
203
|
+
uniform float3 u_probe;
|
|
204
|
+
|
|
205
|
+
// Material \u2014 over the shared channel from JS, already in device pixels.
|
|
206
|
+
uniform float2 u_halfSize;
|
|
207
|
+
uniform float u_corner;
|
|
208
|
+
uniform float u_bevel;
|
|
209
|
+
uniform float u_thick;
|
|
210
|
+
uniform float u_rim;
|
|
211
|
+
uniform float u_ior;
|
|
212
|
+
uniform float u_iorSpread;
|
|
213
|
+
uniform float2 u_light;
|
|
214
|
+
uniform float u_appear;
|
|
215
|
+
uniform float4 u_accent;
|
|
216
|
+
uniform float u_frost;
|
|
217
|
+
uniform float u_ink;
|
|
218
|
+
uniform float u_legibility;
|
|
219
|
+
uniform float u_dim;
|
|
220
|
+
uniform float u_presence;
|
|
221
|
+
uniform float u_progress;
|
|
222
|
+
uniform float u_adaptRadius;
|
|
223
|
+
uniform float u_bodyDensity;
|
|
224
|
+
uniform float u_edgeLight;
|
|
225
|
+
uniform float u_fresnel;
|
|
226
|
+
uniform float u_specular;
|
|
227
|
+
uniform float u_reflectReach;
|
|
228
|
+
uniform float u_film;
|
|
229
|
+
uniform float u_iridescence;
|
|
230
|
+
uniform float u_diffraction;
|
|
231
|
+
uniform float u_colorPickup;
|
|
232
|
+
uniform float2 u_morphOffset;
|
|
233
|
+
uniform float2 u_morphHalf;
|
|
234
|
+
uniform float u_morphCorner;
|
|
235
|
+
uniform float u_morphK;
|
|
236
|
+
uniform float2 u_morph2Offset;
|
|
237
|
+
uniform float2 u_morph2Half;
|
|
238
|
+
uniform float u_morph2Corner;
|
|
239
|
+
uniform float u_debug;
|
|
240
|
+
uniform float2 u_touch;
|
|
241
|
+
uniform float2 u_pull;
|
|
242
|
+
uniform float u_touchPress;
|
|
243
|
+
uniform float u_touchRadius;
|
|
244
|
+
uniform float2 u_wave;
|
|
245
|
+
|
|
246
|
+
${VG_SDF}
|
|
247
|
+
|
|
248
|
+
// Sample count for the disc gather: fewer than this reads as a staircase of copies at a sharp edge.
|
|
249
|
+
const int VG_FROST_TAPS = 20;
|
|
250
|
+
// The sample count grows with the disc's area: estimator noise is the spread under the glass
|
|
251
|
+
// divided by the square root of the sample count. The ceiling is cost: texture sampling is the
|
|
252
|
+
// most expensive thing here.
|
|
253
|
+
const int VG_FROST_TAPS_MAX = 64;
|
|
254
|
+
// The radius correction for backdrop compression at the rim is capped: at the silhouette it goes
|
|
255
|
+
// to infinity.
|
|
256
|
+
const float VG_FOOTPRINT_MAX = 1.8;
|
|
257
|
+
const float VG_TAU = 6.28318530718;
|
|
258
|
+
// Ceiling on body lightness under light ink (and the mirrored floor under dark ink). This is a
|
|
259
|
+
// ceiling, not a difference: "0.14 darker than white" is a lightness of 0.86, at which white text
|
|
260
|
+
// is invisible.
|
|
261
|
+
const float VG_BODY_CAP_LOOSE = 0.62;
|
|
262
|
+
const float VG_BODY_CAP_TIGHT = 0.38;
|
|
263
|
+
// Tint lightness in both directions: real glass is never coal-black or paper-white.
|
|
264
|
+
const float VG_TINT_DARK = 0.07;
|
|
265
|
+
const float VG_TINT_LIGHT = 0.94;
|
|
266
|
+
// Reference channel wavelengths, nm \u2014 for diffraction and interference.
|
|
267
|
+
const float3 VG_LAMBDA = float3(610.0, 550.0, 460.0);
|
|
268
|
+
// Film index of refraction: every thin film on glass sits around this value.
|
|
269
|
+
const float VG_FILM_IOR = 1.35;
|
|
270
|
+
// Ceiling on the profile's slope right at the silhouette: it goes to infinity there.
|
|
271
|
+
const float VG_SLOPE_MAX = 40.0;
|
|
272
|
+
// Fraction of the gather radius that scattering reaches over a busy backdrop under ink (M 11:47).
|
|
273
|
+
// Set against the reference: there, structure under the capsule fades by a factor of 9-10, not
|
|
274
|
+
// thirty.
|
|
275
|
+
const float VG_SCATTER_MAX = 0.20;
|
|
276
|
+
const float VG_SCATTER_BASE = 0.1;
|
|
277
|
+
// The response to structure under the glass saturates early: what competes with the ink isn't
|
|
278
|
+
// the area of foreign text but the mere fact that it's there (a line under a tile gives a busy
|
|
279
|
+
// of about 0.12).
|
|
280
|
+
const float VG_STRUCTURE_GAIN = 20.0;
|
|
281
|
+
// How much denser the body with ink gets over a BUSY backdrop than over a calm one. There's no
|
|
282
|
+
// flat floor here: legibility picks up its own targeted requirement further down, and base
|
|
283
|
+
// frosting comes from VG_MATTE_LIFT.
|
|
284
|
+
const float VG_GROUND_SPAN = 0.06;
|
|
285
|
+
// Scattered light on a frosted element is an ADDITION on top of the backdrop, not a fraction of
|
|
286
|
+
// the way to the tint: the fraction goes to zero once the canvas reaches the tint's lightness,
|
|
287
|
+
// and darkens past it (docs/benchmarks.md).
|
|
288
|
+
const float VG_MATTE_LIFT = 0.10;
|
|
289
|
+
// The fraction of backdrop spread that survives through to the body past scattering: the
|
|
290
|
+
// legibility requirement is computed from this edge, not from the spot's average lightness.
|
|
291
|
+
const float VG_BUSY_EDGE = 0.75;
|
|
292
|
+
// Light concentration: the body is a touch lighter than what's beneath it (M 2:29).
|
|
293
|
+
const float VG_CONCENTRATE = 0.01;
|
|
294
|
+
// The lightness the medium pulls content under the glass toward, and the strength of that pull.
|
|
295
|
+
// The medium both removes light and mixes in scattered light: over a light backdrop the body
|
|
296
|
+
// darkens, over a dark one it lightens.
|
|
297
|
+
const float VG_MEDIUM_LUMA = 0.40;
|
|
298
|
+
const float VG_MEDIUM_PULL = 0.07;
|
|
299
|
+
// How much ambient light reaches the body on top of the pull.
|
|
300
|
+
const float VG_AMBIENT_SPILL = 0.01;
|
|
301
|
+
|
|
302
|
+
float vgLuma(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }
|
|
303
|
+
|
|
304
|
+
// Un-premultiply into ordinary color. One division, done in float: near zero, half's step is too
|
|
305
|
+
// coarse, and several divisions in a row raise noise.
|
|
306
|
+
float3 vgUnpack(half4 c) {
|
|
307
|
+
float a = float(c.a);
|
|
308
|
+
return a > 0.004 ? float3(c.rgb) / a : float3(0.0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The capture ends at the screen edge: samples are clamped to the rectangle where content
|
|
312
|
+
// actually exists, otherwise full-width glass picked up a band of emptiness along its edges.
|
|
313
|
+
float2 vgInContent(float2 q) { return clamp(q, u_contentMin, u_contentMax); }
|
|
314
|
+
|
|
315
|
+
// Hue without lightness. Below the threshold the color has no hue, and the division blows up.
|
|
316
|
+
float3 vgHue(float3 c) {
|
|
317
|
+
float l = vgLuma(c);
|
|
318
|
+
if (l < 0.02) { return float3(1.0); }
|
|
319
|
+
return clamp(c / l, float3(0.0), float3(2.0));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Edge profile \u2014 a convex squircle: x = 0 at the silhouette, 1 where the rounding turns flat.
|
|
323
|
+
float vgRimQ(float x) {
|
|
324
|
+
float k = 1.0 - x;
|
|
325
|
+
return sqrt(sqrt(max(1.0 - k * k * k * k, 0.0)));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
float vgRimQd(float x) {
|
|
329
|
+
float k = 1.0 - x;
|
|
330
|
+
return k * k * k / pow(max(1.0 - k * k * k * k, 1e-5), 0.75);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Glass height above the content at distance e from the silhouette.
|
|
334
|
+
float vgHeight(float e, float w) {
|
|
335
|
+
return mix(u_rim, u_thick, vgRimQ(clamp(e / w, 0.0, 1.0)));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Slope of the top face at the same point: the derivative of height with respect to distance
|
|
339
|
+
// from the silhouette.
|
|
340
|
+
float vgSlope(float e, float w) {
|
|
341
|
+
if (e >= w) { return 0.0; }
|
|
342
|
+
return min((u_thick - u_rim) / w * vgRimQd(clamp(e / w, 0.0, 1.0)), VG_SLOPE_MAX);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Where a ray falling from above lands on a face with normal N: Snell's law refraction, then a
|
|
346
|
+
// path of length z through the medium to the content. The convex face bends the ray toward the
|
|
347
|
+
// normal \u2014 that is, inward relative to the shape.
|
|
348
|
+
float2 vgShift(float3 N, float z, float ior) {
|
|
349
|
+
float3 T = refract(float3(0.0, 0.0, -1.0), N, 1.0 / ior);
|
|
350
|
+
return z * T.xy / max(-T.z, 0.05);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Thin-film interference: optical path difference 2\xB7n\xB7d\xB7cos\u03B8t, each channel with its own \u03BB.
|
|
354
|
+
// Returns a hue normalized to its own average \u2014 it colors, but doesn't lighten.
|
|
355
|
+
float3 vgInterference(float cosI) {
|
|
356
|
+
float sinT2 = (1.0 - cosI * cosI) / (VG_FILM_IOR * VG_FILM_IOR);
|
|
357
|
+
float cosT = sqrt(max(1.0 - sinT2, 0.0));
|
|
358
|
+
float opd = 2.0 * VG_FILM_IOR * u_film * cosT;
|
|
359
|
+
float3 i = 0.5 + 0.5 * cos(VG_TAU * opd / VG_LAMBDA + 3.14159265);
|
|
360
|
+
return i / max((i.r + i.g + i.b) / 3.0, 0.001);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Edge diffraction: fringes get denser the sharper the bevel. Also a hue, not a brightness.
|
|
364
|
+
float3 vgDiffraction(float distFromEdge, float bevel) {
|
|
365
|
+
float phase = VG_TAU * 4.0 * distFromEdge / max(bevel, 1.0);
|
|
366
|
+
float3 d = 0.5 + 0.5 * cos(phase * (550.0 / VG_LAMBDA));
|
|
367
|
+
return d / max((d.r + d.g + d.b) / 3.0, 0.001);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
float vgHash(float2 p) {
|
|
371
|
+
return fract(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Soft compression instead of a hard clamp: over a white backdrop, additions used to knock a
|
|
375
|
+
// channel to one, and the glass turned into a flat blob with no detail.
|
|
376
|
+
float3 vgSoftClip(float3 c) {
|
|
377
|
+
float3 over = max(c - 0.86, float3(0.0));
|
|
378
|
+
return min(c, float3(0.86)) + over * 0.14 / (0.14 + over);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Our own blur: the capture is left clean, the shader does the blurring \u2014 averaging in float
|
|
382
|
+
// smooths banding on its own and adds no noise of its own. A golden-angle spiral, rotated to its
|
|
383
|
+
// own angle at every pixel: with a shared angle, neighboring pixels took the same samples, and
|
|
384
|
+
// texture under the glass read in clumps. Accumulated premultiplied \u2014 one shared un-premultiply
|
|
385
|
+
// at the end.
|
|
386
|
+
float4 vgGather(float2 q, float radius, float2 seed) {
|
|
387
|
+
float4 acc = float4(content.eval(vgInContent(q)));
|
|
388
|
+
if (radius <= 0.25) { return acc; }
|
|
389
|
+
if (radius < 4.0) {
|
|
390
|
+
float r = radius * 0.7;
|
|
391
|
+
acc += float4(content.eval(vgInContent(q + float2(r, 0.0))))
|
|
392
|
+
+ float4(content.eval(vgInContent(q - float2(r, 0.0))))
|
|
393
|
+
+ float4(content.eval(vgInContent(q + float2(0.0, r))))
|
|
394
|
+
+ float4(content.eval(vgInContent(q - float2(0.0, r))));
|
|
395
|
+
return acc * 0.2;
|
|
396
|
+
}
|
|
397
|
+
float ca = cos(2.39996323);
|
|
398
|
+
float sa = sin(2.39996323);
|
|
399
|
+
float a0 = vgHash(seed) * 6.28318530718;
|
|
400
|
+
float2 dir = float2(cos(a0), sin(a0));
|
|
401
|
+
float wide = radius * 0.5;
|
|
402
|
+
int taps = int(clamp(float(VG_FROST_TAPS) * wide * wide, float(VG_FROST_TAPS), float(VG_FROST_TAPS_MAX)));
|
|
403
|
+
for (int i = 1; i <= VG_FROST_TAPS_MAX; i++) {
|
|
404
|
+
if (i > taps) { break; }
|
|
405
|
+
dir = float2(dir.x * ca - dir.y * sa, dir.x * sa + dir.y * ca);
|
|
406
|
+
float r = radius * sqrt(float(i) / float(taps));
|
|
407
|
+
acc += float4(content.eval(vgInContent(q + dir * r)));
|
|
408
|
+
}
|
|
409
|
+
return acc / float(taps + 1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
half4 main(float2 xy) {
|
|
413
|
+
float2 p = vgTouchWarp(xy - u_center, u_touch, u_pull, u_touchPress, u_touchRadius, u_wave.x, u_wave.y);
|
|
414
|
+
float sd = vgScene(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
415
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
416
|
+
// There's nothing outside the glass \u2014 except the light it spills onto the backdrop under the
|
|
417
|
+
// finger (M 3:38): the same concentrated ambient as inside, so no white halo appears over a
|
|
418
|
+
// dark backdrop.
|
|
419
|
+
if (sd > 1.0) {
|
|
420
|
+
float spill = u_touchPress * exp(-sd / max(min(u_halfSize.x, u_halfSize.y) * 0.5, 4.0)) * 0.5 * u_appear;
|
|
421
|
+
if (spill < 0.004) { return half4(0.0); }
|
|
422
|
+
float3 lit = clamp((u_probeLuma >= 0.0 ? u_probe : float3(0.35)) * 1.6 + float3(0.1), float3(0.0), float3(1.0));
|
|
423
|
+
return half4(half3(lit * spill), half(spill));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
float bevel = max(u_bevel, 1.0);
|
|
427
|
+
float e = max(-sd, 0.0);
|
|
428
|
+
// 0 in the flat middle, 1 at the silhouette.
|
|
429
|
+
float t = 1.0 - clamp(e / bevel, 0.0, 1.0);
|
|
430
|
+
float2 n = vgSceneNormal(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
431
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
432
|
+
|
|
433
|
+
// "backdrop" mode hands back the content as-is \u2014 a reference point for comparing the optics.
|
|
434
|
+
float on = u_debug > 5.5 && u_debug < 6.5 ? 0.0 : 1.0;
|
|
435
|
+
float lens = on * u_appear;
|
|
436
|
+
|
|
437
|
+
float z = vgHeight(e, bevel);
|
|
438
|
+
float slope = vgSlope(e, bevel);
|
|
439
|
+
float3 N = normalize(float3(n * slope, 1.0));
|
|
440
|
+
float2 shift = vgShift(N, z, u_ior) * lens;
|
|
441
|
+
float2 s = u_center + p + shift;
|
|
442
|
+
|
|
443
|
+
// The area the lens gathers into one pixel: a one-pixel step inward on screen, measured
|
|
444
|
+
// against the sampling step. At the silhouette it's large \u2014 a stripe of backdrop is compressed
|
|
445
|
+
// into a line there, and a point sample gives grain.
|
|
446
|
+
float e1 = e + 1.0;
|
|
447
|
+
float3 N1 = normalize(float3(n * vgSlope(e1, bevel), 1.0));
|
|
448
|
+
float2 shift1 = vgShift(N1, vgHeight(e1, bevel), u_ior) * lens;
|
|
449
|
+
float footprint = length(shift1 - shift - n);
|
|
450
|
+
float spread = max(footprint - 1.0, 0.0) * 0.5 * on;
|
|
451
|
+
|
|
452
|
+
// Dispersion: the same ray with each channel's own index. Blue bends more than red.
|
|
453
|
+
float2 dR = vgShift(N, z, u_ior - 0.4 * u_iorSpread) * lens - shift;
|
|
454
|
+
float2 dB = vgShift(N, z, u_ior + 0.6 * u_iorSpread) * lens - shift;
|
|
455
|
+
float chroma = length(dB - dR);
|
|
456
|
+
|
|
457
|
+
float3 rgb;
|
|
458
|
+
float srcA;
|
|
459
|
+
|
|
460
|
+
if (chroma + spread < 0.25) {
|
|
461
|
+
// In the flat body all the samples would land at the same point, and the body is almost the
|
|
462
|
+
// whole area of the glass.
|
|
463
|
+
half4 c = content.eval(vgInContent(s));
|
|
464
|
+
srcA = float(c.a);
|
|
465
|
+
rgb = srcA > 0.004 ? float3(c.rgb) / srcA : float3(0.0);
|
|
466
|
+
} else {
|
|
467
|
+
// Accumulate premultiplied and divide once per pair: channels from different points with
|
|
468
|
+
// different alpha would otherwise produce a colored fringe at content edges that isn't
|
|
469
|
+
// really there.
|
|
470
|
+
float2 ev = n * spread;
|
|
471
|
+
half4 c0 = content.eval(vgInContent(s + dR - ev));
|
|
472
|
+
half4 c1 = content.eval(vgInContent(s + dR + ev));
|
|
473
|
+
half4 c2 = content.eval(vgInContent(s - ev));
|
|
474
|
+
half4 c3 = content.eval(vgInContent(s + ev));
|
|
475
|
+
half4 c4 = content.eval(vgInContent(s + dB - ev));
|
|
476
|
+
half4 c5 = content.eval(vgInContent(s + dB + ev));
|
|
477
|
+
|
|
478
|
+
float aR = float(c0.a + c1.a) * 0.5;
|
|
479
|
+
float aG = float(c2.a + c3.a) * 0.5;
|
|
480
|
+
float aB = float(c4.a + c5.a) * 0.5;
|
|
481
|
+
rgb = float3(
|
|
482
|
+
aR > 0.004 ? float(c0.r + c1.r) * 0.5 / aR : 0.0,
|
|
483
|
+
aG > 0.004 ? float(c2.g + c3.g) * 0.5 / aG : 0.0,
|
|
484
|
+
aB > 0.004 ? float(c4.b + c5.b) * 0.5 / aB : 0.0);
|
|
485
|
+
srcA = (aR + aG + aB) / 3.0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// "backdrop" mode checks the CAPTURE, not a simplified version of the optics. What follows are
|
|
489
|
+
// layers that aren't tied to the lens (body, medium, scattering), and they used to conflate
|
|
490
|
+
// "did the pixels arrive" with "how did we process them." Here the element must DISAPPEAR: a
|
|
491
|
+
// visible silhouette means a capture loss.
|
|
492
|
+
if (u_debug > 5.5 && u_debug < 6.5) {
|
|
493
|
+
float bypass = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));
|
|
494
|
+
return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * bypass), half(bypass));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// The transparent variant's dimming layer holds legibility instead of the body. A
|
|
498
|
+
// multiplication, not a tint: the content has to stay visible, just quieter.
|
|
499
|
+
rgb *= 1.0 - u_dim * u_appear;
|
|
500
|
+
|
|
501
|
+
// REFLECTION. Fresnel from the face's actual slope: at the silhouette the view is grazing and
|
|
502
|
+
// reflection is nearly total, which is why the rim reads as a thin line on its own. What's
|
|
503
|
+
// reflected is the element's surroundings and the key light.
|
|
504
|
+
float cosT = clamp(N.z, 0.0, 1.0);
|
|
505
|
+
float f0 = (u_ior - 1.0) / (u_ior + 1.0);
|
|
506
|
+
f0 *= f0;
|
|
507
|
+
float fres = (f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0)) * u_fresnel * lens;
|
|
508
|
+
|
|
509
|
+
float lineW = clamp(min(u_halfSize.x, u_halfSize.y) * 0.03, 1.5, 5.0);
|
|
510
|
+
float3 refl;
|
|
511
|
+
if (e < lineW * 2.0 || fres > 0.01) {
|
|
512
|
+
float2 around = u_center + p + n * (u_reflectReach * mix(0.35, 1.0, clamp(slope / 3.2, 0.0, 1.0)));
|
|
513
|
+
// Two rings of eight samples total: a single offset sample is an undistorted COPY of
|
|
514
|
+
// neighboring content inside the glass (docs/material-lab.md E-33).
|
|
515
|
+
float p1 = u_reflectReach * 0.75;
|
|
516
|
+
float p2 = u_reflectReach * 0.4;
|
|
517
|
+
float d = 0.7071;
|
|
518
|
+
float4 rsum = float4(content.eval(vgInContent(around + float2(p1, 0.0))))
|
|
519
|
+
+ float4(content.eval(vgInContent(around - float2(p1, 0.0))))
|
|
520
|
+
+ float4(content.eval(vgInContent(around + float2(0.0, p1))))
|
|
521
|
+
+ float4(content.eval(vgInContent(around - float2(0.0, p1))))
|
|
522
|
+
+ float4(content.eval(vgInContent(around + float2(p2, p2) * d)))
|
|
523
|
+
+ float4(content.eval(vgInContent(around + float2(-p2, p2) * d)))
|
|
524
|
+
+ float4(content.eval(vgInContent(around + float2(p2, -p2) * d)))
|
|
525
|
+
+ float4(content.eval(vgInContent(around + float2(-p2, -p2) * d)));
|
|
526
|
+
float ra = rsum.a * 0.125;
|
|
527
|
+
refl = ra > 0.004 ? rsum.rgb * 0.125 / ra : float3(0.0);
|
|
528
|
+
} else {
|
|
529
|
+
refl = u_probeLuma >= 0.0 ? u_probe : rgb;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Light reaching the body is the same across the whole element: blending with the surroundings
|
|
533
|
+
// over the bevel's band used to paint a ring-shaped seam on the body.
|
|
534
|
+
float3 ambient = u_probeLuma >= 0.0 ? u_probe : rgb;
|
|
535
|
+
|
|
536
|
+
// Light comes from wherever the surroundings are brighter (M 11:04): the lightness slope under
|
|
537
|
+
// the element pulls the key light toward itself, and over a flat backdrop it stays at rest.
|
|
538
|
+
float2 bright = u_probeLuma >= 0.0 ? u_probeSlope : float2(0.0);
|
|
539
|
+
float brightLen = length(bright);
|
|
540
|
+
float2 L = normalize(mix(u_light, bright / max(brightLen, 1e-4), smoothstep(0.03, 0.2, brightLen)) + float2(1e-5));
|
|
541
|
+
// The key light's reflection uses the face's actual normal: at the flat top it points straight
|
|
542
|
+
// up, and there's nothing there for direction to depend on. Using the SDF normal instead
|
|
543
|
+
// painted the body as a cone.
|
|
544
|
+
float facing = dot(N.xy, L);
|
|
545
|
+
float key = u_specular * (pow(max(facing, 0.0), 3.0) + 0.45 * pow(max(-facing, 0.0), 3.0));
|
|
546
|
+
float rimFacing = dot(n, L);
|
|
547
|
+
float rimKey = u_specular * (pow(max(rimFacing, 0.0), 3.0) + 0.45 * pow(max(-rimFacing, 0.0), 3.0));
|
|
548
|
+
// Wherever the key light doesn't fall, the face reflects its shaded surroundings \u2014 hence the
|
|
549
|
+
// dark outline.
|
|
550
|
+
float3 env = refl * (0.55 + 0.45 * min(key, 1.0)) + float3(1.2 * key);
|
|
551
|
+
|
|
552
|
+
// Interference and diffraction live in the reflected ray at a grazing angle \u2014 a single hue
|
|
553
|
+
// multiplier applied to the reflection, no extra samples.
|
|
554
|
+
float3 spectral = float3(1.0);
|
|
555
|
+
if (u_iridescence > 0.001) {
|
|
556
|
+
spectral *= mix(float3(1.0), vgInterference(cosT), u_iridescence);
|
|
557
|
+
}
|
|
558
|
+
if (u_diffraction > 0.001) {
|
|
559
|
+
float w = u_diffraction * smoothstep(0.45, 1.0, t);
|
|
560
|
+
spectral *= mix(float3(1.0), vgDiffraction(e, bevel), w);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// The BACKDROP ESTIMATE arrives from the probe as one value per surface: computing it here
|
|
564
|
+
// from samples isn't an option \u2014 a kink in density used to turn the estimate's discreteness
|
|
565
|
+
// into ghost copies of text.
|
|
566
|
+
float3 wide;
|
|
567
|
+
float busy;
|
|
568
|
+
if (u_probeLuma >= 0.0) {
|
|
569
|
+
wide = u_probe;
|
|
570
|
+
busy = u_probeBusy;
|
|
571
|
+
} else {
|
|
572
|
+
// The probe hasn't reported yet (first frames) \u2014 fall back to our own samples.
|
|
573
|
+
float2 wx = float2(u_adaptRadius, 0.0);
|
|
574
|
+
float2 wy = float2(0.0, u_adaptRadius);
|
|
575
|
+
float3 w0 = vgUnpack(content.eval(vgInContent(s + wx)));
|
|
576
|
+
float3 w1 = vgUnpack(content.eval(vgInContent(s - wx)));
|
|
577
|
+
float3 w2 = vgUnpack(content.eval(vgInContent(s + wy)));
|
|
578
|
+
float3 w3 = vgUnpack(content.eval(vgInContent(s - wy)));
|
|
579
|
+
wide = (w0 + w1 + w2 + w3) * 0.25;
|
|
580
|
+
float lw = vgLuma(wide);
|
|
581
|
+
// The same statistic the probe uses (twice the mean deviation): both blur and the ink's
|
|
582
|
+
// contrast margin depend on it, and the two paths' scales must not diverge.
|
|
583
|
+
busy = clamp(0.5 * (abs(vgLuma(w0) - lw) + abs(vgLuma(w1) - lw)
|
|
584
|
+
+ abs(vgLuma(w2) - lw) + abs(vgLuma(w3) - lw)), 0.0, 1.0);
|
|
585
|
+
}
|
|
586
|
+
// Lightness AT THIS SPOT on the surface: the probe's plane, clamped to the measured range.
|
|
587
|
+
float2 nrm = p / max(u_halfSize, float2(1.0));
|
|
588
|
+
float local = u_probeLuma >= 0.0
|
|
589
|
+
? clamp(u_probeLuma + dot(u_probeSlope, nrm), u_probeRange.x, u_probeRange.y)
|
|
590
|
+
: vgLuma(wide);
|
|
591
|
+
float mean = u_probeLuma >= 0.0 ? u_probeLuma : vgLuma(wide);
|
|
592
|
+
|
|
593
|
+
// SCATTERING. Regular mutes foreign structure under its own ink with blur, not a fill (M
|
|
594
|
+
// 11:47): text under a capsule turns into blotches. Uniform across the whole lens; the radius
|
|
595
|
+
// is measured in backdrop space \u2014 the lens compresses it at the silhouette.
|
|
596
|
+
float structure = 1.0 - exp(-busy * VG_STRUCTURE_GAIN);
|
|
597
|
+
// Under ink, Regular always scatters the backdrop; over a busy one, more strongly.
|
|
598
|
+
float adaptBlur = max(
|
|
599
|
+
(structure * VG_SCATTER_MAX + VG_SCATTER_BASE) * u_legibility * u_adaptRadius
|
|
600
|
+
* min(max(footprint, 1.0), VG_FOOTPRINT_MAX),
|
|
601
|
+
u_frost) * u_appear;
|
|
602
|
+
if (adaptBlur > 0.5) {
|
|
603
|
+
float reach = max(u_reach - length(p), 1.0);
|
|
604
|
+
float4 g = vgGather(s, min(adaptBlur, reach), xy);
|
|
605
|
+
float ga = g.a;
|
|
606
|
+
float3 blurred = ga > 0.004 ? g.rgb / ga : float3(0.0);
|
|
607
|
+
rgb = mix(rgb, blurred, smoothstep(0.5, 2.0, adaptBlur));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// AFTER scattering, not before: scattering is a property of TRANSMITTED light and replaces rgb
|
|
611
|
+
// wholesale, so doing it the other way around erased the reflection over the whole element
|
|
612
|
+
// (issue #106).
|
|
613
|
+
rgb = mix(rgb, env * spectral, fres);
|
|
614
|
+
|
|
615
|
+
// BODY. Tint and dynamic range under the glass compress just enough for ink over it to stay
|
|
616
|
+
// legible (M 6:42). u_ink is ink POLARITY (1 light, 0 dark): the requirements are computed at
|
|
617
|
+
// the two ends and blended by it, otherwise the glass dips toward the middle during a recolor.
|
|
618
|
+
float strict = clamp(u_legibility * 2.0, 0.0, 1.0);
|
|
619
|
+
float capLight = mix(VG_BODY_CAP_LOOSE, VG_BODY_CAP_TIGHT, strict);
|
|
620
|
+
float floorDark = 1.0 - capLight;
|
|
621
|
+
float pol = clamp(u_ink, 0.0, 1.0);
|
|
622
|
+
// The requirement fades together with legibility: an element with no ink is promised
|
|
623
|
+
// transparent glass by the model.
|
|
624
|
+
float demand = clamp(u_legibility * 4.0, 0.0, 1.0);
|
|
625
|
+
|
|
626
|
+
// Where there's no ink, the glass tints away from the backdrop. The direction is chosen by the
|
|
627
|
+
// average lightness under the element, not the local spot's lightness: otherwise on a gradient
|
|
628
|
+
// the threshold cuts across the body as a diagonal step.
|
|
629
|
+
float darkSide = smoothstep(0.42, 0.58, mean);
|
|
630
|
+
float away = mix(VG_TINT_LIGHT, VG_TINT_DARK, darkSide);
|
|
631
|
+
float tintLuma = mix(away, mix(VG_TINT_LIGHT, VG_TINT_DARK, pol), demand);
|
|
632
|
+
|
|
633
|
+
// How much medium it takes to push lightness under the ink past the threshold \u2014 locally.
|
|
634
|
+
// Computed from whichever edge of the backdrop's spread is closer to the ink: a light ink
|
|
635
|
+
// drowns in a light spot right beneath it, and the spot's average lightness knows nothing
|
|
636
|
+
// about that spot.
|
|
637
|
+
float edge = busy * VG_BUSY_EDGE;
|
|
638
|
+
float inkHi = min(local + edge, 1.0);
|
|
639
|
+
float inkLo = max(local - edge, 0.0);
|
|
640
|
+
float needForLight = inkHi > capLight
|
|
641
|
+
? clamp((inkHi - capLight) / max(inkHi - VG_TINT_DARK, 1e-4), 0.0, 0.92)
|
|
642
|
+
: 0.0;
|
|
643
|
+
float needForDark = inkLo < floorDark
|
|
644
|
+
? clamp((floorDark - inkLo) / max(VG_TINT_LIGHT - inkLo, 1e-4), 0.0, 0.92)
|
|
645
|
+
: 0.0;
|
|
646
|
+
float needForInk = mix(needForDark, needForLight, pol) * demand;
|
|
647
|
+
|
|
648
|
+
float ground = structure * u_legibility * VG_GROUND_SPAN;
|
|
649
|
+
float density = max(max(u_bodyDensity, ground), needForInk);
|
|
650
|
+
|
|
651
|
+
// Glass has no color of its own \u2014 only the color of what's beneath it (HIG "Color").
|
|
652
|
+
float3 tintHue = mix(float3(1.0), vgHue(wide * 0.5 + ambient * 0.5), u_colorPickup);
|
|
653
|
+
// The element appears by the lens and body building up, not by fading in (M 2:55): at
|
|
654
|
+
// u_appear = 0 it's indistinguishable from the backdrop beneath it.
|
|
655
|
+
rgb = mix(rgb, tintHue * tintLuma, density * u_appear);
|
|
656
|
+
rgb += tintHue * (u_legibility * VG_MATTE_LIFT * u_appear);
|
|
657
|
+
|
|
658
|
+
// TINTING \u2014 colored glass, not a fill (M 16:31, 17:03): the backdrop's lightness drives the
|
|
659
|
+
// tone \u2014 deeper over dark, lighter over light \u2014 and the content's texture still shows through
|
|
660
|
+
// the color.
|
|
661
|
+
if (u_accent.a > 0.0) {
|
|
662
|
+
float3 tone = u_accent.rgb * mix(0.88, 1.08, local);
|
|
663
|
+
float3 through = tone * (0.85 + 0.3 * vgLuma(rgb));
|
|
664
|
+
rgb = mix(rgb, clamp(through, float3(0.0), float3(1.0)), u_accent.a * u_appear);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// CONTRAST COMPRESSION. The medium removes some light and mixes in scattered light, so content
|
|
668
|
+
// under the glass moves toward the middle rather than simply getting lighter (M 2:35). The
|
|
669
|
+
// previous layer only added light and, over a light backdrop, pushed the body the opposite way
|
|
670
|
+
// from the reference.
|
|
671
|
+
rgb = mix(rgb, vgHue(ambient) * VG_MEDIUM_LUMA, VG_MEDIUM_PULL * u_appear);
|
|
672
|
+
// Ambient light reaches the body the same way across the whole element.
|
|
673
|
+
rgb += ambient * u_edgeLight * VG_AMBIENT_SPILL * u_appear;
|
|
674
|
+
// Glass concentrates light; the played portion is the segment where there's more of it.
|
|
675
|
+
float glow = VG_CONCENTRATE + 0.08 * vgProgress(p, u_halfSize, u_progress);
|
|
676
|
+
rgb += (1.0 - rgb) * glow * lens;
|
|
677
|
+
|
|
678
|
+
// Under the finger and during morphing, glass concentrates light into a spot (M 4:56, 5:11).
|
|
679
|
+
// This is a CONCENTRATION of the surroundings, not the glass's own whiteness: over a dark
|
|
680
|
+
// backdrop the element lightens but never turns white, and ink over it stays legible.
|
|
681
|
+
if (u_touchPress > 0.001) {
|
|
682
|
+
float2 fromTouch = p - u_touch;
|
|
683
|
+
float spotR = max(min(u_halfSize.x, u_halfSize.y) * 1.2, 1.0);
|
|
684
|
+
float spot = u_touchPress * (0.3 + 0.55 * exp(-dot(fromTouch, fromTouch) / (spotR * spotR)));
|
|
685
|
+
rgb = mix(rgb, clamp(ambient * 1.6 + float3(0.1), float3(0.0), float3(1.0)), spot * lens);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// RIM LIGHT \u2014 a separate layer, a line about 1 pt wide along the silhouette (M 2:36, 11:04):
|
|
689
|
+
// brighter where the face faces the key light, weaker opposite it; where no light falls, a
|
|
690
|
+
// dark outline. This line is what holds the element's presence, not a fill on the body: over a
|
|
691
|
+
// flat backdrop a control is visible along its whole silhouette because of it.
|
|
692
|
+
float line = (1.0 - smoothstep(0.0, lineW, e)) * lens;
|
|
693
|
+
float outline = (1.0 - smoothstep(0.0, lineW * 0.6, e)) * lens;
|
|
694
|
+
float lit = clamp(max(rimKey * 1.4, u_presence * 2.0), 0.0, 1.0);
|
|
695
|
+
// The dark rim is its own layer around the whole silhouette, and the highlight sits ON TOP of
|
|
696
|
+
// it (iOS 27). While the outline used to fade via a (1 \u2212 lit) multiplier, the silhouette read
|
|
697
|
+
// as a single arc: highlight OR shadow.
|
|
698
|
+
rgb *= 1.0 - 0.22 * outline;
|
|
699
|
+
rgb = mix(rgb, mix(refl, float3(1.0), 0.75), line * lit);
|
|
700
|
+
|
|
701
|
+
if (u_debug > 9.5 && u_debug < 10.5) { rgb = spectral * 0.5; }
|
|
702
|
+
if (u_debug > 10.5 && u_debug < 11.5) { rgb = float3(density, needForInk, structure); }
|
|
703
|
+
// WHAT THE PROBE SAID ABOUT THE BACKDROP \u2014 three values that drive all the tinting: the
|
|
704
|
+
// average lightness under the element, the vertical lightness slope, and the lightness AT THIS
|
|
705
|
+
// SPOT. The slope is shown offset and squeezed by half: it's signed and rarely exceeds a
|
|
706
|
+
// magnitude of one half.
|
|
707
|
+
if (u_debug > 11.5) { rgb = float3(max(u_probeLuma, 0.0), 0.5 + u_probeSlope.y * 0.5, local); }
|
|
708
|
+
|
|
709
|
+
rgb = vgSoftClip(rgb);
|
|
710
|
+
|
|
711
|
+
// Sample alpha has to survive to the result: un-premultiplying color by its original alpha and
|
|
712
|
+
// then returning it with a different one (the shape mask) would make a transparent backdrop
|
|
713
|
+
// opaque and blow it out.
|
|
714
|
+
float alpha = srcA * (1.0 - smoothstep(-1.0, 1.0, sd));
|
|
715
|
+
return half4(half3(clamp(rgb, float3(0.0), float3(1.0)) * alpha), half(alpha));
|
|
716
|
+
}
|
|
717
|
+
`;
|
|
718
|
+
var EFFECTS = [
|
|
719
|
+
"backdrop",
|
|
720
|
+
"blur",
|
|
721
|
+
"refraction",
|
|
722
|
+
"fresnel",
|
|
723
|
+
"bevel",
|
|
724
|
+
"specular",
|
|
725
|
+
"dispersion",
|
|
726
|
+
"tint",
|
|
727
|
+
"environment",
|
|
728
|
+
"legibility",
|
|
729
|
+
"interference",
|
|
730
|
+
"diffraction"
|
|
731
|
+
];
|
|
732
|
+
EFFECTS.reduce(
|
|
733
|
+
(acc, e) => ({ ...acc, [e]: true }),
|
|
734
|
+
{}
|
|
735
|
+
);
|
|
736
|
+
var DEBUG_MODES = [
|
|
737
|
+
"normal",
|
|
738
|
+
"sdf",
|
|
739
|
+
"mask",
|
|
740
|
+
"edge",
|
|
741
|
+
"fresnel",
|
|
742
|
+
"refraction",
|
|
743
|
+
"backdrop",
|
|
744
|
+
"specular",
|
|
745
|
+
"dispersion",
|
|
746
|
+
"normals",
|
|
747
|
+
"spectral",
|
|
748
|
+
"adapt",
|
|
749
|
+
"probe"
|
|
750
|
+
];
|
|
751
|
+
function debugIndex(mode) {
|
|
752
|
+
return DEBUG_MODES.indexOf(mode);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/adapters.ts
|
|
756
|
+
var NO_TOUCH = {
|
|
757
|
+
x: 0,
|
|
758
|
+
y: 0,
|
|
759
|
+
pullX: 0,
|
|
760
|
+
pullY: 0,
|
|
761
|
+
press: 0,
|
|
762
|
+
radius: 0,
|
|
763
|
+
waveAmp: 0,
|
|
764
|
+
wavePhase: 0
|
|
765
|
+
};
|
|
766
|
+
var NO_MORPH = { offsetX: 0, offsetY: 0, width: 0, height: 0, cornerRadius: 0, smoothing: 0 };
|
|
767
|
+
var NO_PROGRESS = -1;
|
|
768
|
+
var ACCENT_AMOUNT = 0.8;
|
|
769
|
+
var NO_ACCENT = [0, 0, 0, 0];
|
|
770
|
+
var shapes = /* @__PURE__ */ new Map();
|
|
771
|
+
function channel(entries) {
|
|
772
|
+
const uniformValues = [];
|
|
773
|
+
let shape = shapes.get(entries.length);
|
|
774
|
+
let same = shape !== void 0;
|
|
775
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
776
|
+
const [name, value] = entries[i];
|
|
777
|
+
const v = typeof value === "number" ? [value] : value;
|
|
778
|
+
if (same && (shape.names[i] !== name || shape.sizes[i] !== v.length)) same = false;
|
|
779
|
+
for (const x of v) uniformValues.push(x);
|
|
780
|
+
}
|
|
781
|
+
if (!same) {
|
|
782
|
+
shape = {
|
|
783
|
+
names: entries.map((e) => e[0]),
|
|
784
|
+
sizes: entries.map((e) => typeof e[1] === "number" ? 1 : e[1].length)
|
|
785
|
+
};
|
|
786
|
+
shapes.set(entries.length, shape);
|
|
787
|
+
}
|
|
788
|
+
return { uniformNames: shape.names, uniformSizes: shape.sizes, uniformValues };
|
|
789
|
+
}
|
|
790
|
+
function toLensProps(optics, geometry, density, options = {}) {
|
|
791
|
+
const morph = options.morph ?? NO_MORPH;
|
|
792
|
+
const morph2 = options.morph2 ?? NO_MORPH;
|
|
793
|
+
const touch = options.touch ?? NO_TOUCH;
|
|
794
|
+
const g = options.groupProbe;
|
|
795
|
+
const group = g && g.length >= 9 ? [
|
|
796
|
+
["u_probeLuma", g[0]],
|
|
797
|
+
["u_probeBusy", g[1]],
|
|
798
|
+
["u_probeRange", [g[2], g[3]]],
|
|
799
|
+
["u_probeSlope", [g[4], g[5]]],
|
|
800
|
+
["u_probe", [g[6], g[7], g[8]]]
|
|
801
|
+
] : [];
|
|
802
|
+
const d = density;
|
|
803
|
+
const halfW = geometry.width * d / 2;
|
|
804
|
+
const halfH = geometry.height * d / 2;
|
|
805
|
+
const halfMin = Math.min(halfW, halfH);
|
|
806
|
+
return {
|
|
807
|
+
shaderSource: LENS_SHADER,
|
|
808
|
+
glassWidth: geometry.width,
|
|
809
|
+
glassHeight: geometry.height,
|
|
810
|
+
...channel([
|
|
811
|
+
["u_halfSize", [halfW, halfH]],
|
|
812
|
+
["u_corner", Math.min(geometry.cornerRadius * d, halfMin)],
|
|
813
|
+
["u_bevel", Math.max(bevelDp(geometry, optics) * d, 1)],
|
|
814
|
+
["u_thick", thicknessDp(geometry, optics) * d],
|
|
815
|
+
["u_rim", rimDp(geometry, optics) * d],
|
|
816
|
+
["u_ior", optics.ior],
|
|
817
|
+
["u_iorSpread", optics.iorSpread],
|
|
818
|
+
["u_light", options.light ?? REST_LIGHT],
|
|
819
|
+
["u_appear", options.appear ?? 1],
|
|
820
|
+
["u_accent", options.accent ? [...options.accent.color, options.accent.amount ?? ACCENT_AMOUNT] : NO_ACCENT],
|
|
821
|
+
// Haze from surface roughness. Lives in the same disc-gather pass as adaptive scattering,
|
|
822
|
+
// and fades out toward the bevel: that's a different job there — bending the ray and
|
|
823
|
+
// splitting it.
|
|
824
|
+
["u_frost", optics.blur * d],
|
|
825
|
+
["u_ink", optics.ink],
|
|
826
|
+
["u_legibility", optics.legibility],
|
|
827
|
+
["u_dim", optics.dimming],
|
|
828
|
+
["u_presence", optics.presence],
|
|
829
|
+
["u_adaptRadius", optics.adaptRadius * d],
|
|
830
|
+
["u_bodyDensity", optics.bodyDensity],
|
|
831
|
+
["u_edgeLight", optics.edgeLight],
|
|
832
|
+
["u_fresnel", optics.fresnel],
|
|
833
|
+
["u_specular", optics.specular],
|
|
834
|
+
// The rim gathers light from the element's vicinity — this is a radius around the shape,
|
|
835
|
+
// not its bevel.
|
|
836
|
+
["u_reflectReach", optics.gatherRadiusDp * d],
|
|
837
|
+
["u_film", optics.film],
|
|
838
|
+
["u_iridescence", optics.iridescence],
|
|
839
|
+
["u_diffraction", optics.diffraction],
|
|
840
|
+
["u_colorPickup", optics.colorPickup],
|
|
841
|
+
["u_morphOffset", [morph.offsetX * d, morph.offsetY * d]],
|
|
842
|
+
["u_morphHalf", [morph.width * d / 2, morph.height * d / 2]],
|
|
843
|
+
["u_morphCorner", morph.cornerRadius * d],
|
|
844
|
+
["u_morphK", morph.smoothing * d],
|
|
845
|
+
["u_morph2Offset", [morph2.offsetX * d, morph2.offsetY * d]],
|
|
846
|
+
["u_morph2Half", [morph2.width * d / 2, morph2.height * d / 2]],
|
|
847
|
+
["u_morph2Corner", morph2.cornerRadius * d],
|
|
848
|
+
["u_touch", [touch.x * d, touch.y * d]],
|
|
849
|
+
["u_pull", [touch.pullX * d, touch.pullY * d]],
|
|
850
|
+
["u_touchPress", touch.press],
|
|
851
|
+
["u_touchRadius", touch.radius * d],
|
|
852
|
+
["u_wave", [touch.waveAmp * d, touch.wavePhase]],
|
|
853
|
+
["u_progress", options.progress ?? NO_PROGRESS],
|
|
854
|
+
["u_debug", debugIndex(options.debug ?? "normal")],
|
|
855
|
+
...group
|
|
856
|
+
])
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function toSurfaceUniforms(optics, geometry, options = {}) {
|
|
860
|
+
const morph = options.morph ?? NO_MORPH;
|
|
861
|
+
const morph2 = options.morph2 ?? NO_MORPH;
|
|
862
|
+
const touch = options.touch ?? NO_TOUCH;
|
|
863
|
+
const pad = surfacePadDp(geometry, options.dragLimit ?? 0, morph);
|
|
864
|
+
return {
|
|
865
|
+
u_center: [geometry.width / 2 + pad, geometry.height / 2 + pad],
|
|
866
|
+
u_halfSize: [geometry.width / 2, geometry.height / 2],
|
|
867
|
+
u_corner: Math.min(geometry.cornerRadius, halfMinDp(geometry)),
|
|
868
|
+
u_bevel: bevelDp(geometry, optics),
|
|
869
|
+
u_thickness: bevelFraction(geometry, optics),
|
|
870
|
+
u_morphOffset: [morph.offsetX, morph.offsetY],
|
|
871
|
+
u_morphHalf: [morph.width / 2, morph.height / 2],
|
|
872
|
+
u_morphCorner: morph.cornerRadius,
|
|
873
|
+
u_morphK: morph.smoothing,
|
|
874
|
+
u_morph2Offset: [morph2.offsetX, morph2.offsetY],
|
|
875
|
+
u_morph2Half: [morph2.width / 2, morph2.height / 2],
|
|
876
|
+
u_morph2Corner: morph2.cornerRadius,
|
|
877
|
+
u_edgeDensity: optics.edgeDensity,
|
|
878
|
+
u_dispersion: optics.dispersion,
|
|
879
|
+
u_refraction: optics.refraction,
|
|
880
|
+
// Tint density is suppressed when the lens is drawing the body: painting it twice would
|
|
881
|
+
// double the fill, and adaptation is impossible on the surface anyway — it can't see the
|
|
882
|
+
// backdrop. The color itself stays: it drives absorption at the rim.
|
|
883
|
+
u_tint: [
|
|
884
|
+
optics.tint.r,
|
|
885
|
+
optics.tint.g,
|
|
886
|
+
optics.tint.b,
|
|
887
|
+
options.bodyInLens ? 0 : optics.tintStrength
|
|
888
|
+
],
|
|
889
|
+
u_shadow: options.shadow ?? 1,
|
|
890
|
+
// Neutral by default: without an ambient sample, the shadow stays exactly as it was.
|
|
891
|
+
u_ambient: options.ambient ?? [0, 0, 0],
|
|
892
|
+
u_shadowReach: shadowReachDp(geometry),
|
|
893
|
+
u_touch: [touch.x, touch.y],
|
|
894
|
+
u_pull: [touch.pullX, touch.pullY],
|
|
895
|
+
u_touchPress: touch.press,
|
|
896
|
+
u_touchRadius: touch.radius,
|
|
897
|
+
u_wave: [touch.waveAmp, touch.wavePhase],
|
|
898
|
+
u_presence: optics.presence,
|
|
899
|
+
u_progress: options.progress ?? NO_PROGRESS,
|
|
900
|
+
u_appear: options.appear ?? 1,
|
|
901
|
+
// Zero by default: buttons sink in, the way every element behaved before this rule existed.
|
|
902
|
+
u_lift: options.lift ?? 0,
|
|
903
|
+
u_debug: debugIndex(options.debug ?? "normal")
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
var SURFACE_LENGTH_UNIFORMS = [
|
|
907
|
+
"u_halfSize",
|
|
908
|
+
"u_corner",
|
|
909
|
+
"u_bevel",
|
|
910
|
+
"u_morphOffset",
|
|
911
|
+
"u_morphHalf",
|
|
912
|
+
"u_morphCorner",
|
|
913
|
+
"u_morphK",
|
|
914
|
+
"u_morph2Offset",
|
|
915
|
+
"u_morph2Half",
|
|
916
|
+
"u_morph2Corner",
|
|
917
|
+
"u_shadowReach",
|
|
918
|
+
"u_touch",
|
|
919
|
+
"u_pull",
|
|
920
|
+
"u_touchRadius"
|
|
921
|
+
// `satisfies`, not just `as const`: otherwise a typo in the name compiles silently and makes it
|
|
922
|
+
// to runtime — the field with the garbage name gets NaN, and the real one is left un-multiplied.
|
|
923
|
+
];
|
|
924
|
+
function toDeviceSurfaceUniforms(raw, density) {
|
|
925
|
+
const scaled = { ...raw };
|
|
926
|
+
for (const key of SURFACE_LENGTH_UNIFORMS) {
|
|
927
|
+
const value = scaled[key];
|
|
928
|
+
scaled[key] = Array.isArray(value) ? value.map((v) => v * density) : value * density;
|
|
929
|
+
}
|
|
930
|
+
scaled.u_wave = [raw.u_wave[0] * density, raw.u_wave[1]];
|
|
931
|
+
return scaled;
|
|
932
|
+
}
|
|
933
|
+
var REST_LIGHT = [-0.577, -0.817];
|
|
934
|
+
var DYNAMIC_UNIFORMS = ["u_press", "u_active"];
|
|
935
|
+
var ICON_UNIFORMS = ["u_iconOn", "u_iconScale", "u_inkIdle", "u_inkActive"];
|
|
936
|
+
var OVERLAY_UNIFORMS = ["u_overlayOn"];
|
|
937
|
+
|
|
938
|
+
// src/surface-shader.ts
|
|
939
|
+
var SURFACE_SHADER = `
|
|
940
|
+
uniform shader u_icon;
|
|
941
|
+
/** App-colored content ON the glass \u2014 cover art, a thumbnail. A separate layer from the ink
|
|
942
|
+
* mask: that one is single-channel and colored by polarity, this one carries its own color
|
|
943
|
+
* as-is. */
|
|
944
|
+
uniform shader u_overlay;
|
|
945
|
+
|
|
946
|
+
uniform float2 u_center;
|
|
947
|
+
uniform float2 u_halfSize;
|
|
948
|
+
uniform float u_corner;
|
|
949
|
+
uniform float u_bevel;
|
|
950
|
+
uniform float u_thickness;
|
|
951
|
+
uniform float2 u_morphOffset;
|
|
952
|
+
uniform float2 u_morphHalf;
|
|
953
|
+
uniform float u_morphCorner;
|
|
954
|
+
uniform float u_morphK;
|
|
955
|
+
uniform float2 u_morph2Offset;
|
|
956
|
+
uniform float2 u_morph2Half;
|
|
957
|
+
uniform float u_morph2Corner;
|
|
958
|
+
|
|
959
|
+
uniform float u_press;
|
|
960
|
+
uniform float u_active;
|
|
961
|
+
/* 0 \u2014 the element sinks in under the finger, 1 \u2014 it rises into glass (reference \xA75). */
|
|
962
|
+
uniform float u_lift;
|
|
963
|
+
|
|
964
|
+
uniform float u_edgeDensity;
|
|
965
|
+
uniform float u_dispersion;
|
|
966
|
+
uniform float u_refraction;
|
|
967
|
+
uniform float4 u_tint;
|
|
968
|
+
uniform float u_shadow;
|
|
969
|
+
uniform float u_shadowReach;
|
|
970
|
+
uniform float3 u_ambient;
|
|
971
|
+
uniform float u_presence;
|
|
972
|
+
uniform float u_progress;
|
|
973
|
+
uniform float u_appear;
|
|
974
|
+
uniform float u_debug;
|
|
975
|
+
|
|
976
|
+
uniform float u_iconOn;
|
|
977
|
+
uniform float u_overlayOn;
|
|
978
|
+
uniform float u_iconScale;
|
|
979
|
+
uniform float4 u_inkIdle;
|
|
980
|
+
uniform float4 u_inkActive;
|
|
981
|
+
uniform float2 u_touch;
|
|
982
|
+
uniform float2 u_pull;
|
|
983
|
+
uniform float u_touchPress;
|
|
984
|
+
uniform float u_touchRadius;
|
|
985
|
+
uniform float2 u_wave;
|
|
986
|
+
|
|
987
|
+
${VG_SDF}
|
|
988
|
+
|
|
989
|
+
const float VG_FALLOFF = ${VG_FALLOFF};
|
|
990
|
+
|
|
991
|
+
// Tint density in the flat middle; at the bevel it's multiplied by u_edgeDensity.
|
|
992
|
+
const float VG_BODY_DENSITY = 0.19;
|
|
993
|
+
/* Fraction of the ambient hue in the shadow. The shadow has to stay a shadow, not a colored
|
|
994
|
+
* blob. */
|
|
995
|
+
const float VG_SHADOW_TINT = 1.0;
|
|
996
|
+
// How much weaker the shadow is right at the outline than under the middle of the gap, and what
|
|
997
|
+
// fraction of the offset it takes to reach full depth. In the reference the shadow's minimum
|
|
998
|
+
// sits below the rim, not right at it.
|
|
999
|
+
const float VG_GAP_LIGHT = 0.76;
|
|
1000
|
+
const float VG_GAP_REACH = 0.20;
|
|
1001
|
+
/* Depth of the ink beneath the surface, dp. The normal displaces it by exactly this much right
|
|
1002
|
+
* at the rim. */
|
|
1003
|
+
const float VG_INK_DEPTH = 4.0;
|
|
1004
|
+
/* Ink defocus under the finger (reference \xA76): a glyph loses its edge and drowns in milk. Given
|
|
1005
|
+
* as a FRACTION of the touch blob, not a number: the blob already arrives in device units, so
|
|
1006
|
+
* the radius automatically follows both screen density and the element's size. At rest it's
|
|
1007
|
+
* zero, and the sample stays a single one. */
|
|
1008
|
+
const float VG_INK_DEFOCUS = 0.08;
|
|
1009
|
+
|
|
1010
|
+
half4 vgPack(half3 c, float a) { return half4(c * half(a), half(a)); }
|
|
1011
|
+
|
|
1012
|
+
// What bleeds into the shadow is the COLOR of the surroundings, not its brightness: the hue's own
|
|
1013
|
+
// lightness is subtracted out of it, otherwise the shadow pales and loses depth instead of
|
|
1014
|
+
// warming up. Strength is set by how much light there is around: over a nearly black backdrop
|
|
1015
|
+
// there's nothing to bleed into the shadow.
|
|
1016
|
+
float3 vgShadowTint(float3 ambient) {
|
|
1017
|
+
float peak = max(max(ambient.r, ambient.g), max(ambient.b, 0.001));
|
|
1018
|
+
float3 hue = ambient / peak;
|
|
1019
|
+
float lumaWeights = dot(ambient, float3(0.2126, 0.7152, 0.0722));
|
|
1020
|
+
return (hue - dot(hue, float3(0.2126, 0.7152, 0.0722))) * lumaWeights;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
half3 vgHeat(float v) {
|
|
1024
|
+
float x = clamp(v, 0.0, 1.0);
|
|
1025
|
+
return half3(half(clamp(x * 2.2 - 0.2, 0.0, 1.0)),
|
|
1026
|
+
half(clamp(1.0 - abs(x - 0.5) * 2.2, 0.0, 1.0)),
|
|
1027
|
+
half(clamp(1.2 - x * 2.4, 0.0, 1.0)));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
half4 main(float2 xy) {
|
|
1031
|
+
float2 p = vgTouchWarp(xy - u_center, u_touch, u_pull, u_touchPress, u_touchRadius, u_wave.x, u_wave.y);
|
|
1032
|
+
|
|
1033
|
+
// Inverse deformation: stretch A along the pull vector, compression 1/sqrt(A) across it. This
|
|
1034
|
+
// exact law mirrors the transform of the live backdrop under the canvas, otherwise the two
|
|
1035
|
+
// would drift apart. There's NO motion geometry here \u2014 no pull, no press-driven bulge. All of
|
|
1036
|
+
// that is done by a single wrapper transform, the same one that carries the native lens: the
|
|
1037
|
+
// shader doesn't touch it, and two pipelines driving one motion would drift apart by a frame,
|
|
1038
|
+
// making the layers visible as separate.
|
|
1039
|
+
|
|
1040
|
+
float halfMin = max(min(u_halfSize.x, u_halfSize.y), 1.0);
|
|
1041
|
+
float bevel = max(u_bevel, 1.0);
|
|
1042
|
+
|
|
1043
|
+
float sd = vgScene(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
1044
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
1045
|
+
float t = vgBevelT(sd, bevel);
|
|
1046
|
+
float2 n = vgSceneNormal(p, u_halfSize, u_corner, u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
1047
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
1048
|
+
float3 N = normalize(float3(n * vgBevelSlope(t), 1.0));
|
|
1049
|
+
|
|
1050
|
+
// Progress is an active state turned into a field: the played fraction shines and glows
|
|
1051
|
+
// exactly as much as an active element shines whole. This does NOT touch ink: recoloring text
|
|
1052
|
+
// as the track advances would mean changing it mid-word.
|
|
1053
|
+
float lit = max(u_active, vgProgress(p, u_halfSize, u_progress));
|
|
1054
|
+
|
|
1055
|
+
// The highlight and the rim are computed by the lens: reflection is a function of the
|
|
1056
|
+
// surroundings, and only the lens can see them.
|
|
1057
|
+
|
|
1058
|
+
if (u_debug > 0.5) {
|
|
1059
|
+
float inMask = 1.0 - smoothstep(-1.0, 1.0, sd);
|
|
1060
|
+
if (u_debug < 1.5) {
|
|
1061
|
+
float band = abs(fract(sd / (halfMin * 0.22)) - 0.5) * 2.0;
|
|
1062
|
+
half3 c = sd < 0.0 ? half3(0.20, 0.62, 1.0) : half3(1.0, 0.42, 0.22);
|
|
1063
|
+
return vgPack(c * half(0.25 + band * 0.75), 1.0);
|
|
1064
|
+
}
|
|
1065
|
+
if (u_debug < 2.5) { return vgPack(half3(1.0), inMask); }
|
|
1066
|
+
if (u_debug < 3.5) { return vgPack(vgHeat(t), inMask); }
|
|
1067
|
+
// Fresnel now lives in the lens; here we show its SHAPE \u2014 how grazing the view is.
|
|
1068
|
+
if (u_debug < 4.5) { return vgPack(vgHeat(1.0 - clamp(N.z, 0.0, 1.0)), inMask); }
|
|
1069
|
+
if (u_debug < 5.5) {
|
|
1070
|
+
float push = pow(t, VG_FALLOFF) * u_refraction;
|
|
1071
|
+
return vgPack(vgHeat(push), inMask);
|
|
1072
|
+
}
|
|
1073
|
+
if (u_debug < 6.5) { return half4(0.0); }
|
|
1074
|
+
if (u_debug < 7.5) { return half4(0.0); }
|
|
1075
|
+
// Dispersion is computed by the lens; here is the field it grows along.
|
|
1076
|
+
if (u_debug < 8.5) { return vgPack(vgHeat(u_dispersion * t * t), inMask); }
|
|
1077
|
+
if (u_debug < 9.5) { return vgPack(half3(N * 0.5 + 0.5), inMask); }
|
|
1078
|
+
// spectral and adapt are shown by the LENS \u2014 the surface has to get out of the way.
|
|
1079
|
+
return half4(0.0);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// The shadow and the halo live OUTSIDE the shape: inside, their place is taken by the glass
|
|
1083
|
+
// itself. Separation from the content rests entirely on the shadow \u2014 without it the surface
|
|
1084
|
+
// would sit ON the picture, not above it. The shadow only lives at the rim and outside it: at
|
|
1085
|
+
// sd < \u22121 the outside multiplier is already zero. Computing it deep inside the shape would be
|
|
1086
|
+
// a wasted FULL SDF evaluation for every such pixel, and the body covers almost the whole
|
|
1087
|
+
// area. The branch here is on the coordinate, but only the threads right at the rim actually
|
|
1088
|
+
// diverge.
|
|
1089
|
+
float shade = 0.0;
|
|
1090
|
+
float halo = 0.0;
|
|
1091
|
+
if (sd > -1.0) {
|
|
1092
|
+
float outside = smoothstep(-1.0, 1.0, sd);
|
|
1093
|
+
// Under the finger a button moves TOWARD the backdrop, and the shadow tightens: it's exactly
|
|
1094
|
+
// the gap between the two. A control rising into glass (a switch knob, a slider handle \u2014
|
|
1095
|
+
// reference \xA75) moves AWAY from it, and then the shadow spreads out instead. Only the app
|
|
1096
|
+
// knows which one applies, so the direction arrives as a fraction: 0 is sinking in, the way
|
|
1097
|
+
// every element behaved before this rule existed.
|
|
1098
|
+
float reach = u_shadowReach * (1.0 + mix(-0.25, 0.55, u_lift) * u_press);
|
|
1099
|
+
// The shadow is soft and wide, shifted downward (M 11:58): it's what holds the element's
|
|
1100
|
+
// separation from the content.
|
|
1101
|
+
float sdDrop = vgScene(p - float2(0.0, reach * 0.35), u_halfSize, u_corner,
|
|
1102
|
+
u_morphOffset, u_morphHalf, u_morphCorner, u_morphK,
|
|
1103
|
+
u_morph2Offset, u_morph2Half, u_morph2Corner);
|
|
1104
|
+
float amb = 1.0 - smoothstep(-reach * 0.3, reach, sdDrop);
|
|
1105
|
+
// THE GAP UNDER A RAISED ELEMENT IS LIT: right at the outline the shadow is WEAKER than
|
|
1106
|
+
// further down. Otherwise the minimum sits right against the silhouette \u2014 both parts of the
|
|
1107
|
+
// shadow are monotonic in distance.
|
|
1108
|
+
float gap = smoothstep(0.0, max(reach * VG_GAP_REACH, 1.0), max(sd, 0.0));
|
|
1109
|
+
shade = amb * amb * 0.13 * mix(VG_GAP_LIGHT, 1.0, gap) * outside * u_shadow * u_appear;
|
|
1110
|
+
halo = 1.0 - smoothstep(0.0, reach * 0.30, max(sd, 0.0));
|
|
1111
|
+
halo = halo * halo * lit * 0.10 * outside * u_appear;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (sd > 1.0) {
|
|
1115
|
+
// Ambient light bleeds into the shadow (219 @8:22): it isn't a black hole, it's tinted by
|
|
1116
|
+
// whatever lies nearby. Otherwise an element over colored content would hang over a gray
|
|
1117
|
+
// blob.
|
|
1118
|
+
half3 spill = max(half3(half(halo)) + half3(vgShadowTint(u_ambient)) * half(VG_SHADOW_TINT * shade * (1.0 - halo)), half3(0.0));
|
|
1119
|
+
return half4(spill, half(halo + shade * (1.0 - halo)));
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
float density = u_tint.w;
|
|
1123
|
+
float inner = clamp(-sd / halfMin, 0.0, 1.0);
|
|
1124
|
+
half3 col = half3(u_tint.rgb);
|
|
1125
|
+
|
|
1126
|
+
// The tint is LIGHT and weak, not dark: dark glass over dark content disappears, and has to be
|
|
1127
|
+
// held together with a heavy rim \u2014 which makes the surface read as a chrome bead. Tint density
|
|
1128
|
+
// at the bevel is a separate value: physically the bevel bends light harder, but it doesn't
|
|
1129
|
+
// get hazier. Coupled together, the two produced a milky ring around the whole outline.
|
|
1130
|
+
float body = mix(VG_BODY_DENSITY, VG_BODY_DENSITY * u_edgeDensity, smoothstep(0.10, 0.62, t)) * density;
|
|
1131
|
+
float vignette = smoothstep(0.15, 1.0, inner) * 0.19 * density;
|
|
1132
|
+
// There's NO density boost on press here. It was meant as "the element stands out more under
|
|
1133
|
+
// the finger," but it pulls the body toward the tint, and the tint depends on polarity: over a
|
|
1134
|
+
// light backdrop (dark polarity) press ended up DARKENING the element. Presence is shown
|
|
1135
|
+
// instead by the vgTouchWarp field's deformation and the highlight blooming below \u2014 neither
|
|
1136
|
+
// cares about the sign of polarity.
|
|
1137
|
+
float a = max(body, vignette);
|
|
1138
|
+
|
|
1139
|
+
// Active state does NOT touch the body's color. Mixing in a fixed gray here flipped the
|
|
1140
|
+
// effect's sign depending on the backdrop: over dark the element lightened, over light it
|
|
1141
|
+
// darkened, for the exact same state. Active state is shown instead by the highlight and rim
|
|
1142
|
+
// lighting above: neither cares about the backdrop.
|
|
1143
|
+
|
|
1144
|
+
// THE ICON SITS BENEATH THE SURFACE, it isn't stuck on top of it: it used to be mixed in last,
|
|
1145
|
+
// over the highlight, and read as a flat sticker on volumetric glass. At the rim the normal
|
|
1146
|
+
// displaces it, like everything seen through the glass, while the highlight sits ON TOP \u2014 it
|
|
1147
|
+
// lives on the surface itself.
|
|
1148
|
+
//
|
|
1149
|
+
// There is NO shadow under the icon here. It used to be made from the difference of two offset
|
|
1150
|
+
// mask samples and produced a second outline along the edge \u2014 the icon's borders looked
|
|
1151
|
+
// ragged.
|
|
1152
|
+
// THE INK'S SHIFT IS SET BY ITS OWN DEPTH, NOT BY BEVEL WIDTH. The ink sits right at the
|
|
1153
|
+
// surface, and it's displaced by exactly the thin layer of glass above it \u2014 not by how wide a
|
|
1154
|
+
// bevel this particular piece of glass happens to have. It used to be a fraction of the bevel:
|
|
1155
|
+
// on thin glass the shift came out to 4 dp and everything lined up, but on thick glass it was
|
|
1156
|
+
// 8, and cover art inset from the edge by 6 stretched toward the rim and spilled past its
|
|
1157
|
+
// bounds. The ceiling is absolute: ink has one depth regardless of the glass.
|
|
1158
|
+
float2 inkShift = n * (min(u_bevel * 0.5, VG_INK_DEPTH) * t);
|
|
1159
|
+
float2 inkUv = u_center + p - inkShift;
|
|
1160
|
+
|
|
1161
|
+
// INK LOSES SHARPNESS UNDER THE FINGER (reference \xA76): in the reference, a glyph under
|
|
1162
|
+
// pressure drowns in milk rather than simply getting lighter. This branch is uniform across
|
|
1163
|
+
// the whole element: press arrives as a uniform, so defocus costs nothing while the element
|
|
1164
|
+
// isn't being touched.
|
|
1165
|
+
float defocus = VG_INK_DEFOCUS * u_touchRadius * u_touchPress;
|
|
1166
|
+
float2 inkDx = float2(defocus, 0.0);
|
|
1167
|
+
float2 inkDy = float2(0.0, defocus);
|
|
1168
|
+
|
|
1169
|
+
half4 ink;
|
|
1170
|
+
half4 over;
|
|
1171
|
+
if (defocus > 0.01) {
|
|
1172
|
+
// A cross around the center with double weight in the middle: the ink mask is
|
|
1173
|
+
// high-contrast, and four samples are enough for a stroke to stop holding its edge.
|
|
1174
|
+
ink = (u_icon.eval((inkUv - inkDx) * u_iconScale)
|
|
1175
|
+
+ u_icon.eval((inkUv + inkDx) * u_iconScale)
|
|
1176
|
+
+ u_icon.eval((inkUv - inkDy) * u_iconScale)
|
|
1177
|
+
+ u_icon.eval((inkUv + inkDy) * u_iconScale)
|
|
1178
|
+
+ u_icon.eval(inkUv * u_iconScale) * 2.0) * half(u_iconOn / 6.0);
|
|
1179
|
+
// Colored content lives in the same plane as the ink and blurs together with it: different
|
|
1180
|
+
// sharpness for two layers in one plane would read as a defect, not a press.
|
|
1181
|
+
over = (u_overlay.eval((inkUv - inkDx) * u_iconScale)
|
|
1182
|
+
+ u_overlay.eval((inkUv + inkDx) * u_iconScale)
|
|
1183
|
+
+ u_overlay.eval((inkUv - inkDy) * u_iconScale)
|
|
1184
|
+
+ u_overlay.eval((inkUv + inkDy) * u_iconScale)
|
|
1185
|
+
+ u_overlay.eval(inkUv * u_iconScale) * 2.0) * half(u_overlayOn * u_appear / 6.0);
|
|
1186
|
+
} else {
|
|
1187
|
+
ink = u_icon.eval(inkUv * u_iconScale) * half(u_iconOn);
|
|
1188
|
+
over = u_overlay.eval(inkUv * u_iconScale) * half(u_overlayOn * u_appear);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// The backing under the ink is held by the LENS (body density from the probe), not the
|
|
1192
|
+
// surface: legibility is a property of the material, and computing it here a second time
|
|
1193
|
+
// would give two different answers to one question.
|
|
1194
|
+
|
|
1195
|
+
// Layers are composited premultiplied-over: in straight alpha, a nearly transparent dark body
|
|
1196
|
+
// used to pull the color of semi-transparent ink and the finger's light toward itself \u2014 letter
|
|
1197
|
+
// edges grayed out, pressing darkened things.
|
|
1198
|
+
half3 pm = col * half(a);
|
|
1199
|
+
half inkA = ink.g * half(mix(0.82, 1.0, u_active) * u_appear);
|
|
1200
|
+
half3 inkCol = mix(half3(u_inkIdle.rgb), half3(u_inkActive.rgb), half(u_active));
|
|
1201
|
+
pm = pm * (1.0 - inkA) + inkCol * inkA;
|
|
1202
|
+
a = a + float(inkA) * (1.0 - a);
|
|
1203
|
+
|
|
1204
|
+
// COLORED CONTENT LIVES IN THE SAME PLACE AS THE INK \u2014 inside the material, at the same
|
|
1205
|
+
// coordinate. Otherwise deformation would carry them apart: on press, the title and artist
|
|
1206
|
+
// name would shake together with the surface while the cover art stayed put, because it used
|
|
1207
|
+
// to be a separate layer on top of the glass. Polarity doesn't touch it \u2014 it has its own
|
|
1208
|
+
// color, and there's nothing to swap it for.
|
|
1209
|
+
pm = pm * (1.0 - over.a) + over.rgb * over.a;
|
|
1210
|
+
a = a + float(over.a) * (1.0 - a);
|
|
1211
|
+
|
|
1212
|
+
// Light from the finger is gathered by the LENS: it's a concentration of the surroundings, not
|
|
1213
|
+
// the glass's own whiteness. The surface is left with only what the glass spills outward \u2014 the
|
|
1214
|
+
// halo above.
|
|
1215
|
+
|
|
1216
|
+
float mask = 1.0 - smoothstep(-1.0, 1.0, sd);
|
|
1217
|
+
pm = clamp(pm, half3(0.0), half3(1.0)) * half(mask);
|
|
1218
|
+
a *= mask;
|
|
1219
|
+
pm = max(pm + half3(vgShadowTint(u_ambient)) * half(VG_SHADOW_TINT * shade * (1.0 - a)), half3(0.0));
|
|
1220
|
+
return half4(pm, half(a + shade * (1.0 - a)));
|
|
1221
|
+
}
|
|
1222
|
+
`;
|
|
1223
|
+
|
|
1224
|
+
// src/targets/glsl.ts
|
|
1225
|
+
function sizeUniformName(sampler) {
|
|
1226
|
+
return sampler.startsWith("u_") ? `${sampler}Size` : `u_${sampler}Size`;
|
|
1227
|
+
}
|
|
1228
|
+
function convertVecTypes(src) {
|
|
1229
|
+
return src.replace(/\bfloat([234])\b/g, "vec$1");
|
|
1230
|
+
}
|
|
1231
|
+
function convertHalfTypes(src) {
|
|
1232
|
+
return src.replace(/\bhalf([234])\b/g, "vec$1").replace(/\bhalf\b/g, "float");
|
|
1233
|
+
}
|
|
1234
|
+
function convertShaderUniforms(src) {
|
|
1235
|
+
return src.replace(/uniform\s+shader\s+(\w+)\s*;/g, (_match, name) => {
|
|
1236
|
+
return `uniform sampler2D ${name};
|
|
1237
|
+
uniform vec2 ${sizeUniformName(name)};`;
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function convertEvalCalls(src) {
|
|
1241
|
+
const samplers = /* @__PURE__ */ new Set();
|
|
1242
|
+
const declRe = /uniform\s+sampler2D\s+(\w+)\s*;/g;
|
|
1243
|
+
for (let m = declRe.exec(src); m; m = declRe.exec(src)) samplers.add(m[1]);
|
|
1244
|
+
if (samplers.size === 0) return src;
|
|
1245
|
+
const callRe = /(\w+)\.eval\(/g;
|
|
1246
|
+
let out = "";
|
|
1247
|
+
let cursor = 0;
|
|
1248
|
+
for (let m = callRe.exec(src); m; m = callRe.exec(src)) {
|
|
1249
|
+
const name = m[1];
|
|
1250
|
+
if (!samplers.has(name)) continue;
|
|
1251
|
+
const argStart = m.index + m[0].length;
|
|
1252
|
+
let depth = 1;
|
|
1253
|
+
let i = argStart;
|
|
1254
|
+
while (i < src.length && depth > 0) {
|
|
1255
|
+
if (src[i] === "(") depth += 1;
|
|
1256
|
+
else if (src[i] === ")") depth -= 1;
|
|
1257
|
+
i += 1;
|
|
1258
|
+
}
|
|
1259
|
+
const args = src.slice(argStart, i - 1);
|
|
1260
|
+
out += src.slice(cursor, m.index);
|
|
1261
|
+
out += `texture(${name}, (${args}) / ${sizeUniformName(name)})`;
|
|
1262
|
+
cursor = i;
|
|
1263
|
+
callRe.lastIndex = i;
|
|
1264
|
+
}
|
|
1265
|
+
out += src.slice(cursor);
|
|
1266
|
+
return out;
|
|
1267
|
+
}
|
|
1268
|
+
function convertReturns(mainBody) {
|
|
1269
|
+
return mainBody.replace(/\breturn\s+([^;]+);/g, "fragColor = $1; return;");
|
|
1270
|
+
}
|
|
1271
|
+
var ENTRY_RE = /half4\s+main\s*\(\s*float2\s+(\w+)\s*\)\s*\{/;
|
|
1272
|
+
function convertEntryPoint(src) {
|
|
1273
|
+
const match = ENTRY_RE.exec(src);
|
|
1274
|
+
if (!match) {
|
|
1275
|
+
throw new Error('vireglass/targets/glsl: entry point "half4 main(float2 xy)" not found');
|
|
1276
|
+
}
|
|
1277
|
+
const bodyStart = match.index + match[0].length;
|
|
1278
|
+
let depth = 1;
|
|
1279
|
+
let i = bodyStart;
|
|
1280
|
+
while (i < src.length && depth > 0) {
|
|
1281
|
+
if (src[i] === "{") depth += 1;
|
|
1282
|
+
else if (src[i] === "}") depth -= 1;
|
|
1283
|
+
i += 1;
|
|
1284
|
+
}
|
|
1285
|
+
const bodyEnd = i - 1;
|
|
1286
|
+
const param = match[1];
|
|
1287
|
+
const body = convertReturns(src.slice(bodyStart, bodyEnd));
|
|
1288
|
+
const head = src.slice(0, match.index);
|
|
1289
|
+
const tail = src.slice(i);
|
|
1290
|
+
return `${head}void main() {
|
|
1291
|
+
vec2 ${param} = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);
|
|
1292
|
+
${body}}${tail}`;
|
|
1293
|
+
}
|
|
1294
|
+
function addPrologue(src) {
|
|
1295
|
+
return "#version 300 es\nprecision highp float;\n\nout vec4 fragColor;\nuniform vec2 u_resolution;\n\n" + src;
|
|
1296
|
+
}
|
|
1297
|
+
function toGLSL(shaderSource) {
|
|
1298
|
+
let out = shaderSource;
|
|
1299
|
+
out = convertEntryPoint(out);
|
|
1300
|
+
out = convertShaderUniforms(out);
|
|
1301
|
+
out = convertEvalCalls(out);
|
|
1302
|
+
out = convertVecTypes(out);
|
|
1303
|
+
out = convertHalfTypes(out);
|
|
1304
|
+
out = addPrologue(out);
|
|
1305
|
+
return out;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/web/gl.ts
|
|
1309
|
+
var FULLSCREEN_TRIANGLE_VERTEX_SOURCE = `#version 300 es
|
|
1310
|
+
const vec2 VG_POS[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
1311
|
+
void main() {
|
|
1312
|
+
gl_Position = vec4(VG_POS[gl_VertexID], 0.0, 1.0);
|
|
1313
|
+
}
|
|
1314
|
+
`;
|
|
1315
|
+
function compileShader(gl, type, source) {
|
|
1316
|
+
const shader = gl.createShader(type);
|
|
1317
|
+
if (!shader) throw new Error("vireglass/web: gl.createShader returned null");
|
|
1318
|
+
gl.shaderSource(shader, source);
|
|
1319
|
+
gl.compileShader(shader);
|
|
1320
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
1321
|
+
const log = gl.getShaderInfoLog(shader) ?? "(no log)";
|
|
1322
|
+
gl.deleteShader(shader);
|
|
1323
|
+
throw new Error(`vireglass/web: shader failed to compile:
|
|
1324
|
+
${log}`);
|
|
1325
|
+
}
|
|
1326
|
+
return shader;
|
|
1327
|
+
}
|
|
1328
|
+
function createProgram(gl, vertexSource, fragmentSource) {
|
|
1329
|
+
const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
1330
|
+
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
1331
|
+
const program = gl.createProgram();
|
|
1332
|
+
if (!program) throw new Error("vireglass/web: gl.createProgram returned null");
|
|
1333
|
+
gl.attachShader(program, vertex);
|
|
1334
|
+
gl.attachShader(program, fragment);
|
|
1335
|
+
gl.linkProgram(program);
|
|
1336
|
+
gl.deleteShader(vertex);
|
|
1337
|
+
gl.deleteShader(fragment);
|
|
1338
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
1339
|
+
const log = gl.getProgramInfoLog(program) ?? "(no log)";
|
|
1340
|
+
gl.deleteProgram(program);
|
|
1341
|
+
throw new Error(`vireglass/web: program failed to link:
|
|
1342
|
+
${log}`);
|
|
1343
|
+
}
|
|
1344
|
+
return program;
|
|
1345
|
+
}
|
|
1346
|
+
function drawFullscreenTriangle(gl) {
|
|
1347
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
1348
|
+
}
|
|
1349
|
+
function createTexture(gl, options) {
|
|
1350
|
+
const texture = gl.createTexture();
|
|
1351
|
+
if (!texture) throw new Error("vireglass/web: gl.createTexture returned null");
|
|
1352
|
+
const internalFormat = options.internalFormat ?? gl.RGBA8;
|
|
1353
|
+
const format = options.format ?? gl.RGBA;
|
|
1354
|
+
const type = options.type ?? gl.UNSIGNED_BYTE;
|
|
1355
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1356
|
+
gl.texImage2D(
|
|
1357
|
+
gl.TEXTURE_2D,
|
|
1358
|
+
0,
|
|
1359
|
+
internalFormat,
|
|
1360
|
+
options.width,
|
|
1361
|
+
options.height,
|
|
1362
|
+
0,
|
|
1363
|
+
format,
|
|
1364
|
+
type,
|
|
1365
|
+
null
|
|
1366
|
+
);
|
|
1367
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1368
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1369
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1370
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1371
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1372
|
+
return texture;
|
|
1373
|
+
}
|
|
1374
|
+
function createFramebuffer(gl, texture) {
|
|
1375
|
+
const fbo = gl.createFramebuffer();
|
|
1376
|
+
if (!fbo) throw new Error("vireglass/web: gl.createFramebuffer returned null");
|
|
1377
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1378
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
1379
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
1380
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1381
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
1382
|
+
throw new Error(`vireglass/web: FBO incomplete, status ${status}`);
|
|
1383
|
+
}
|
|
1384
|
+
return fbo;
|
|
1385
|
+
}
|
|
1386
|
+
function bindTextureAt(gl, unit, texture, program, uniformName) {
|
|
1387
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1388
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1389
|
+
const loc = gl.getUniformLocation(program, uniformName);
|
|
1390
|
+
gl.uniform1i(loc, unit);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/web/probe.ts
|
|
1394
|
+
var PROBE_GRID_WIDTH = 48;
|
|
1395
|
+
var PROBE_GRID_HEIGHT = 96;
|
|
1396
|
+
var DOWNSAMPLE_FRAGMENT_SOURCE = `#version 300 es
|
|
1397
|
+
precision highp float;
|
|
1398
|
+
out vec4 fragColor;
|
|
1399
|
+
uniform sampler2D u_content;
|
|
1400
|
+
uniform vec2 u_contentSize;
|
|
1401
|
+
uniform vec2 u_gridSize;
|
|
1402
|
+
|
|
1403
|
+
// Cell \u2192 content UV mapping. Does NOT correspond to the canvas's visible top/bottom \u2014 this is an
|
|
1404
|
+
// internal pass, its buffer is never displayed, only read back on the CPU with the same formula
|
|
1405
|
+
// (see rectStats below). It doesn't need to match the blit pass's orientation.
|
|
1406
|
+
void main() {
|
|
1407
|
+
vec2 uv = gl_FragCoord.xy / u_gridSize;
|
|
1408
|
+
vec3 sum = vec3(0.0);
|
|
1409
|
+
const int N = 5;
|
|
1410
|
+
vec2 block = u_contentSize / u_gridSize;
|
|
1411
|
+
vec2 origin = uv * u_contentSize;
|
|
1412
|
+
for (int y = 0; y < N; y++) {
|
|
1413
|
+
for (int x = 0; x < N; x++) {
|
|
1414
|
+
vec2 p = origin + (vec2(float(x), float(y)) + 0.5) * (block / float(N));
|
|
1415
|
+
sum += texture(u_content, p / u_contentSize).rgb;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
fragColor = vec4(sum / float(N * N), 1.0);
|
|
1419
|
+
}
|
|
1420
|
+
`;
|
|
1421
|
+
function pickDownsampleFormat(gl) {
|
|
1422
|
+
const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
|
|
1423
|
+
return hasFloat ? { internalFormat: gl.RGBA32F, type: gl.FLOAT, floatPrecision: true } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE, floatPrecision: false };
|
|
1424
|
+
}
|
|
1425
|
+
function luma(r, g, b) {
|
|
1426
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1427
|
+
}
|
|
1428
|
+
function rectStats(buffer, floatPrecision, gridW, gridH, contentWidth, contentHeight, rect) {
|
|
1429
|
+
const x0 = rect.centerX - rect.halfWidth;
|
|
1430
|
+
const x1 = rect.centerX + rect.halfWidth;
|
|
1431
|
+
const y0 = rect.centerY - rect.halfHeight;
|
|
1432
|
+
const y1 = rect.centerY + rect.halfHeight;
|
|
1433
|
+
const lumas = [];
|
|
1434
|
+
const nx = [];
|
|
1435
|
+
const ny = [];
|
|
1436
|
+
let sr = 0;
|
|
1437
|
+
let sg = 0;
|
|
1438
|
+
let sb = 0;
|
|
1439
|
+
const norm = floatPrecision ? 1 : 255;
|
|
1440
|
+
for (let row = 0; row < gridH; row++) {
|
|
1441
|
+
const cy = (row + 0.5) / gridH * contentHeight;
|
|
1442
|
+
if (cy < y0 || cy > y1) continue;
|
|
1443
|
+
for (let col = 0; col < gridW; col++) {
|
|
1444
|
+
const cx = (col + 0.5) / gridW * contentWidth;
|
|
1445
|
+
if (cx < x0 || cx > x1) continue;
|
|
1446
|
+
const i = (row * gridW + col) * 4;
|
|
1447
|
+
const r = buffer[i] / norm;
|
|
1448
|
+
const g = buffer[i + 1] / norm;
|
|
1449
|
+
const b = buffer[i + 2] / norm;
|
|
1450
|
+
lumas.push(luma(r, g, b));
|
|
1451
|
+
nx.push((cx - rect.centerX) / Math.max(rect.halfWidth, 1));
|
|
1452
|
+
ny.push((cy - rect.centerY) / Math.max(rect.halfHeight, 1));
|
|
1453
|
+
sr += r;
|
|
1454
|
+
sg += g;
|
|
1455
|
+
sb += b;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const n = lumas.length;
|
|
1459
|
+
if (n === 0) return null;
|
|
1460
|
+
let mean = 0;
|
|
1461
|
+
for (const v of lumas) mean += v;
|
|
1462
|
+
mean /= n;
|
|
1463
|
+
let busy = 0;
|
|
1464
|
+
for (const v of lumas) busy += Math.abs(v - mean);
|
|
1465
|
+
busy = busy / n * 2;
|
|
1466
|
+
const sorted = [...lumas].sort((a, b) => a - b);
|
|
1467
|
+
const percentile = (p) => {
|
|
1468
|
+
const idx = p * (n - 1);
|
|
1469
|
+
const lo = Math.floor(idx);
|
|
1470
|
+
const hi = Math.ceil(idx);
|
|
1471
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
|
|
1472
|
+
};
|
|
1473
|
+
const slope = (xs) => {
|
|
1474
|
+
let mx = 0;
|
|
1475
|
+
for (const v of xs) mx += v;
|
|
1476
|
+
mx /= n;
|
|
1477
|
+
let sxx = 0;
|
|
1478
|
+
let sxl = 0;
|
|
1479
|
+
for (let i = 0; i < n; i++) {
|
|
1480
|
+
const dx = xs[i] - mx;
|
|
1481
|
+
sxx += dx * dx;
|
|
1482
|
+
sxl += dx * (lumas[i] - mean);
|
|
1483
|
+
}
|
|
1484
|
+
return sxx > 1e-6 ? sxl / sxx : 0;
|
|
1485
|
+
};
|
|
1486
|
+
return {
|
|
1487
|
+
luma: mean,
|
|
1488
|
+
busy: Math.min(busy, 1),
|
|
1489
|
+
lo: percentile(0.1),
|
|
1490
|
+
hi: percentile(0.9),
|
|
1491
|
+
slopeX: slope(nx),
|
|
1492
|
+
slopeY: slope(ny),
|
|
1493
|
+
r: sr / n,
|
|
1494
|
+
g: sg / n,
|
|
1495
|
+
b: sb / n
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function createProbe(gl, vertexSource) {
|
|
1499
|
+
const program = createProgram(gl, vertexSource, DOWNSAMPLE_FRAGMENT_SOURCE);
|
|
1500
|
+
const format = pickDownsampleFormat(gl);
|
|
1501
|
+
const texture = gl.createTexture();
|
|
1502
|
+
if (!texture) throw new Error("vireglass/web/probe: gl.createTexture returned null");
|
|
1503
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1504
|
+
gl.texImage2D(
|
|
1505
|
+
gl.TEXTURE_2D,
|
|
1506
|
+
0,
|
|
1507
|
+
format.internalFormat,
|
|
1508
|
+
PROBE_GRID_WIDTH,
|
|
1509
|
+
PROBE_GRID_HEIGHT,
|
|
1510
|
+
0,
|
|
1511
|
+
gl.RGBA,
|
|
1512
|
+
format.type,
|
|
1513
|
+
null
|
|
1514
|
+
);
|
|
1515
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
1516
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
1517
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1518
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1519
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1520
|
+
const fbo = createFramebuffer(gl, texture);
|
|
1521
|
+
const byteSize = PROBE_GRID_WIDTH * PROBE_GRID_HEIGHT * 4 * (format.floatPrecision ? 4 : 1);
|
|
1522
|
+
const slots = [0, 1].map(() => {
|
|
1523
|
+
const buffer = gl.createBuffer();
|
|
1524
|
+
if (!buffer) throw new Error("vireglass/web/probe: gl.createBuffer returned null");
|
|
1525
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buffer);
|
|
1526
|
+
gl.bufferData(gl.PIXEL_PACK_BUFFER, byteSize, gl.STREAM_READ);
|
|
1527
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
|
1528
|
+
return { buffer, sync: null, pending: false };
|
|
1529
|
+
});
|
|
1530
|
+
let cursor = 0;
|
|
1531
|
+
let latestRaw = null;
|
|
1532
|
+
const contentLoc = gl.getUniformLocation(program, "u_content");
|
|
1533
|
+
const contentSizeLoc = gl.getUniformLocation(program, "u_contentSize");
|
|
1534
|
+
const gridSizeLoc = gl.getUniformLocation(program, "u_gridSize");
|
|
1535
|
+
function pollReady(slot) {
|
|
1536
|
+
if (!slot.pending || !slot.sync) return;
|
|
1537
|
+
const status = gl.clientWaitSync(slot.sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0);
|
|
1538
|
+
if (status === gl.TIMEOUT_EXPIRED || status === gl.WAIT_FAILED) return;
|
|
1539
|
+
gl.deleteSync(slot.sync);
|
|
1540
|
+
slot.sync = null;
|
|
1541
|
+
slot.pending = false;
|
|
1542
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, slot.buffer);
|
|
1543
|
+
const out = format.floatPrecision ? new Float32Array(PROBE_GRID_WIDTH * PROBE_GRID_HEIGHT * 4) : new Uint8Array(PROBE_GRID_WIDTH * PROBE_GRID_HEIGHT * 4);
|
|
1544
|
+
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, out);
|
|
1545
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
|
1546
|
+
latestRaw = out;
|
|
1547
|
+
}
|
|
1548
|
+
function sample(contentTexture, contentWidth, contentHeight, rect) {
|
|
1549
|
+
for (const slot2 of slots) pollReady(slot2);
|
|
1550
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
1551
|
+
gl.viewport(0, 0, PROBE_GRID_WIDTH, PROBE_GRID_HEIGHT);
|
|
1552
|
+
gl.useProgram(program);
|
|
1553
|
+
bindTextureAt(gl, 0, contentTexture, program, "u_content");
|
|
1554
|
+
gl.uniform1i(contentLoc, 0);
|
|
1555
|
+
gl.uniform2f(contentSizeLoc, contentWidth, contentHeight);
|
|
1556
|
+
gl.uniform2f(gridSizeLoc, PROBE_GRID_WIDTH, PROBE_GRID_HEIGHT);
|
|
1557
|
+
gl.disable(gl.BLEND);
|
|
1558
|
+
drawFullscreenTriangle(gl);
|
|
1559
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1560
|
+
const slot = slots[cursor];
|
|
1561
|
+
cursor = (cursor + 1) % slots.length;
|
|
1562
|
+
if (!slot.pending) {
|
|
1563
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo);
|
|
1564
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, slot.buffer);
|
|
1565
|
+
gl.readPixels(0, 0, PROBE_GRID_WIDTH, PROBE_GRID_HEIGHT, gl.RGBA, format.type, 0);
|
|
1566
|
+
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
|
|
1567
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
|
1568
|
+
slot.sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
1569
|
+
slot.pending = true;
|
|
1570
|
+
}
|
|
1571
|
+
return statsFor(contentWidth, contentHeight, rect);
|
|
1572
|
+
}
|
|
1573
|
+
function statsFor(contentWidth, contentHeight, rect) {
|
|
1574
|
+
if (!latestRaw) return null;
|
|
1575
|
+
return rectStats(
|
|
1576
|
+
latestRaw,
|
|
1577
|
+
format.floatPrecision,
|
|
1578
|
+
PROBE_GRID_WIDTH,
|
|
1579
|
+
PROBE_GRID_HEIGHT,
|
|
1580
|
+
contentWidth,
|
|
1581
|
+
contentHeight,
|
|
1582
|
+
rect
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
function destroy() {
|
|
1586
|
+
for (const slot of slots) {
|
|
1587
|
+
if (slot.sync) gl.deleteSync(slot.sync);
|
|
1588
|
+
gl.deleteBuffer(slot.buffer);
|
|
1589
|
+
}
|
|
1590
|
+
gl.deleteFramebuffer(fbo);
|
|
1591
|
+
gl.deleteTexture(texture);
|
|
1592
|
+
gl.deleteProgram(program);
|
|
1593
|
+
}
|
|
1594
|
+
return { sample, statsFor, destroy };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/web/renderer.ts
|
|
1598
|
+
var BLIT_FRAGMENT_SOURCE = `#version 300 es
|
|
1599
|
+
precision highp float;
|
|
1600
|
+
out vec4 fragColor;
|
|
1601
|
+
uniform sampler2D u_content;
|
|
1602
|
+
uniform vec2 u_resolution;
|
|
1603
|
+
void main() {
|
|
1604
|
+
// The same Y flip the transpiler does for the lens/surface (targets/glsl.ts): content is
|
|
1605
|
+
// uploaded from the 2D canvas WITHOUT flipping, so its V=0 is the scene's top row. This keeps
|
|
1606
|
+
// the same order here, otherwise the backdrop and what the lens refracts through it drift
|
|
1607
|
+
// apart vertically.
|
|
1608
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
1609
|
+
uv.y = 1.0 - uv.y;
|
|
1610
|
+
fragColor = texture(u_content, uv);
|
|
1611
|
+
}
|
|
1612
|
+
`;
|
|
1613
|
+
function locationCache(gl, program) {
|
|
1614
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1615
|
+
return (name) => {
|
|
1616
|
+
let loc = cache.get(name);
|
|
1617
|
+
if (loc === void 0) {
|
|
1618
|
+
loc = gl.getUniformLocation(program, name);
|
|
1619
|
+
cache.set(name, loc);
|
|
1620
|
+
}
|
|
1621
|
+
return loc;
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function setUniform(gl, loc, value) {
|
|
1625
|
+
if (!loc) return;
|
|
1626
|
+
if (typeof value === "number") {
|
|
1627
|
+
gl.uniform1f(loc, value);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
switch (value.length) {
|
|
1631
|
+
case 1:
|
|
1632
|
+
gl.uniform1f(loc, value[0]);
|
|
1633
|
+
break;
|
|
1634
|
+
case 2:
|
|
1635
|
+
gl.uniform2f(loc, value[0], value[1]);
|
|
1636
|
+
break;
|
|
1637
|
+
case 3:
|
|
1638
|
+
gl.uniform3f(loc, value[0], value[1], value[2]);
|
|
1639
|
+
break;
|
|
1640
|
+
case 4:
|
|
1641
|
+
gl.uniform4f(loc, value[0], value[1], value[2], value[3]);
|
|
1642
|
+
break;
|
|
1643
|
+
default:
|
|
1644
|
+
throw new Error(`vireglass/web: unsupported uniform size (${value.length})`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function applyChannel(gl, get, names, sizes, values) {
|
|
1648
|
+
let cursor = 0;
|
|
1649
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
1650
|
+
const size = sizes[i];
|
|
1651
|
+
setUniform(gl, get(names[i]), values.slice(cursor, cursor + size));
|
|
1652
|
+
cursor += size;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function applyObject(gl, get, values) {
|
|
1656
|
+
for (const name of Object.keys(values)) setUniform(gl, get(name), values[name]);
|
|
1657
|
+
}
|
|
1658
|
+
function createVireGlassRenderer(canvas) {
|
|
1659
|
+
const context = canvas.getContext("webgl2", {
|
|
1660
|
+
preserveDrawingBuffer: true,
|
|
1661
|
+
alpha: false,
|
|
1662
|
+
antialias: false
|
|
1663
|
+
});
|
|
1664
|
+
if (!context) throw new Error("vireglass/web: WebGL2 unavailable");
|
|
1665
|
+
const gl = context;
|
|
1666
|
+
const blitProgram = createProgram(gl, FULLSCREEN_TRIANGLE_VERTEX_SOURCE, BLIT_FRAGMENT_SOURCE);
|
|
1667
|
+
const lensProgram = createProgram(gl, FULLSCREEN_TRIANGLE_VERTEX_SOURCE, toGLSL(LENS_SHADER));
|
|
1668
|
+
const surfaceProgram = createProgram(
|
|
1669
|
+
gl,
|
|
1670
|
+
FULLSCREEN_TRIANGLE_VERTEX_SOURCE,
|
|
1671
|
+
toGLSL(SURFACE_SHADER)
|
|
1672
|
+
);
|
|
1673
|
+
const probe = createProbe(gl, FULLSCREEN_TRIANGLE_VERTEX_SOURCE);
|
|
1674
|
+
const blitLoc = locationCache(gl, blitProgram);
|
|
1675
|
+
const lensLoc = locationCache(gl, lensProgram);
|
|
1676
|
+
const surfaceLoc = locationCache(gl, surfaceProgram);
|
|
1677
|
+
let width = canvas.width;
|
|
1678
|
+
let height = canvas.height;
|
|
1679
|
+
let sceneCanvas = document.createElement("canvas");
|
|
1680
|
+
let sceneCtx = sceneCanvas.getContext("2d");
|
|
1681
|
+
if (!sceneCtx) throw new Error("vireglass/web: scene 2D context unavailable");
|
|
1682
|
+
const settled = /* @__PURE__ */ new Map();
|
|
1683
|
+
const SETTLE = 0.12;
|
|
1684
|
+
function settleStats(index, fresh) {
|
|
1685
|
+
const prev = settled.get(index);
|
|
1686
|
+
if (!prev) {
|
|
1687
|
+
settled.set(index, fresh);
|
|
1688
|
+
return fresh;
|
|
1689
|
+
}
|
|
1690
|
+
const mix = (a, b) => a + (b - a) * SETTLE;
|
|
1691
|
+
const next = {
|
|
1692
|
+
luma: mix(prev.luma, fresh.luma),
|
|
1693
|
+
busy: mix(prev.busy, fresh.busy),
|
|
1694
|
+
lo: mix(prev.lo, fresh.lo),
|
|
1695
|
+
hi: mix(prev.hi, fresh.hi),
|
|
1696
|
+
slopeX: mix(prev.slopeX, fresh.slopeX),
|
|
1697
|
+
slopeY: mix(prev.slopeY, fresh.slopeY),
|
|
1698
|
+
r: mix(prev.r, fresh.r),
|
|
1699
|
+
g: mix(prev.g, fresh.g),
|
|
1700
|
+
b: mix(prev.b, fresh.b)
|
|
1701
|
+
};
|
|
1702
|
+
settled.set(index, next);
|
|
1703
|
+
return next;
|
|
1704
|
+
}
|
|
1705
|
+
let contentTexture = createTexture(gl, {
|
|
1706
|
+
width: Math.max(width, 1),
|
|
1707
|
+
height: Math.max(height, 1)
|
|
1708
|
+
});
|
|
1709
|
+
const iconTexture = createTexture(gl, { width: 1, height: 1 });
|
|
1710
|
+
const colorTexture = createTexture(gl, { width: 1, height: 1 });
|
|
1711
|
+
gl.bindTexture(gl.TEXTURE_2D, iconTexture);
|
|
1712
|
+
gl.texSubImage2D(
|
|
1713
|
+
gl.TEXTURE_2D,
|
|
1714
|
+
0,
|
|
1715
|
+
0,
|
|
1716
|
+
0,
|
|
1717
|
+
1,
|
|
1718
|
+
1,
|
|
1719
|
+
gl.RGBA,
|
|
1720
|
+
gl.UNSIGNED_BYTE,
|
|
1721
|
+
new Uint8Array([0, 0, 0, 0])
|
|
1722
|
+
);
|
|
1723
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1724
|
+
function resize(widthPx, heightPx) {
|
|
1725
|
+
width = Math.max(1, Math.round(widthPx));
|
|
1726
|
+
height = Math.max(1, Math.round(heightPx));
|
|
1727
|
+
canvas.width = width;
|
|
1728
|
+
canvas.height = height;
|
|
1729
|
+
gl.viewport(0, 0, width, height);
|
|
1730
|
+
sceneCanvas = document.createElement("canvas");
|
|
1731
|
+
sceneCanvas.width = width;
|
|
1732
|
+
sceneCanvas.height = height;
|
|
1733
|
+
sceneCtx = sceneCanvas.getContext("2d");
|
|
1734
|
+
if (!sceneCtx) throw new Error("vireglass/web: scene 2D context unavailable");
|
|
1735
|
+
gl.deleteTexture(contentTexture);
|
|
1736
|
+
contentTexture = createTexture(gl, { width, height });
|
|
1737
|
+
}
|
|
1738
|
+
function render(options) {
|
|
1739
|
+
if (!sceneCtx) throw new Error("vireglass/web: renderer not initialized (resize was never called)");
|
|
1740
|
+
sceneCtx.clearRect(0, 0, width, height);
|
|
1741
|
+
options.scene(sceneCtx, width, height, options.offsetX ?? 0, options.offsetY ?? 0, options.density);
|
|
1742
|
+
gl.bindTexture(gl.TEXTURE_2D, contentTexture);
|
|
1743
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, sceneCanvas);
|
|
1744
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1745
|
+
if (options.colorLayer) {
|
|
1746
|
+
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
|
|
1747
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.colorLayer);
|
|
1748
|
+
}
|
|
1749
|
+
if (options.iconMask) {
|
|
1750
|
+
gl.bindTexture(gl.TEXTURE_2D, iconTexture);
|
|
1751
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.iconMask);
|
|
1752
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
1753
|
+
}
|
|
1754
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1755
|
+
gl.viewport(0, 0, width, height);
|
|
1756
|
+
gl.disable(gl.BLEND);
|
|
1757
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1758
|
+
gl.useProgram(blitProgram);
|
|
1759
|
+
bindTextureAt(gl, 0, contentTexture, blitProgram, "u_content");
|
|
1760
|
+
setUniform(gl, blitLoc("u_resolution"), [width, height]);
|
|
1761
|
+
drawFullscreenTriangle(gl);
|
|
1762
|
+
const probes = [];
|
|
1763
|
+
for (let index = 0; index < options.pieces.length; index += 1) {
|
|
1764
|
+
const piece = options.pieces[index];
|
|
1765
|
+
const halfWidth = piece.geometry.width * options.density / 2;
|
|
1766
|
+
const halfHeight = piece.geometry.height * options.density / 2;
|
|
1767
|
+
const rect = {
|
|
1768
|
+
centerX: piece.centerX,
|
|
1769
|
+
centerY: piece.centerY,
|
|
1770
|
+
halfWidth,
|
|
1771
|
+
halfHeight
|
|
1772
|
+
};
|
|
1773
|
+
const fresh = index === 0 ? probe.sample(contentTexture, width, height, rect) : probe.statsFor(width, height, rect);
|
|
1774
|
+
const stats = fresh ? settleStats(index, fresh) : null;
|
|
1775
|
+
probes.push(stats);
|
|
1776
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1777
|
+
gl.viewport(0, 0, width, height);
|
|
1778
|
+
gl.enable(gl.BLEND);
|
|
1779
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
1780
|
+
const reachOf = (m) => m ? Math.hypot(m.offsetX, m.offsetY) + Math.max(m.width, m.height) / 2 + m.smoothing : 0;
|
|
1781
|
+
const morphReach = Math.max(reachOf(piece.morph), reachOf(piece.morph2));
|
|
1782
|
+
const touchReach = piece.touch ? Math.hypot(piece.touch.pullX, piece.touch.pullY) + piece.touch.waveAmp * 2 + Math.max(piece.geometry.width, piece.geometry.height) * 0.05 * piece.touch.press : 0;
|
|
1783
|
+
const padPx = (lensPadDp(piece.geometry, piece.optics) + surfacePadDp(piece.geometry, 0) + morphReach + touchReach) * options.density;
|
|
1784
|
+
const left = Math.max(0, Math.floor(piece.centerX - halfWidth - padPx));
|
|
1785
|
+
const right = Math.min(width, Math.ceil(piece.centerX + halfWidth + padPx));
|
|
1786
|
+
const top = Math.max(0, Math.floor(height - (piece.centerY + halfHeight + padPx)));
|
|
1787
|
+
const bottom = Math.min(height, Math.ceil(height - (piece.centerY - halfHeight - padPx)));
|
|
1788
|
+
gl.enable(gl.SCISSOR_TEST);
|
|
1789
|
+
gl.scissor(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
|
|
1790
|
+
gl.useProgram(lensProgram);
|
|
1791
|
+
bindTextureAt(gl, 0, contentTexture, lensProgram, "content");
|
|
1792
|
+
setUniform(gl, lensLoc("u_contentSize"), [width, height]);
|
|
1793
|
+
setUniform(gl, lensLoc("u_resolution"), [width, height]);
|
|
1794
|
+
setUniform(gl, lensLoc("u_center"), [piece.centerX, piece.centerY]);
|
|
1795
|
+
setUniform(gl, lensLoc("u_reach"), Math.max(width, height));
|
|
1796
|
+
setUniform(gl, lensLoc("u_contentMin"), [1, 1]);
|
|
1797
|
+
setUniform(gl, lensLoc("u_contentMax"), [width - 1, height - 1]);
|
|
1798
|
+
if (stats) {
|
|
1799
|
+
setUniform(gl, lensLoc("u_probeLuma"), stats.luma);
|
|
1800
|
+
setUniform(gl, lensLoc("u_probeBusy"), stats.busy);
|
|
1801
|
+
setUniform(gl, lensLoc("u_probeRange"), [stats.lo, stats.hi]);
|
|
1802
|
+
setUniform(gl, lensLoc("u_probeSlope"), [stats.slopeX, stats.slopeY]);
|
|
1803
|
+
setUniform(gl, lensLoc("u_probe"), [stats.r, stats.g, stats.b]);
|
|
1804
|
+
} else {
|
|
1805
|
+
setUniform(gl, lensLoc("u_probeLuma"), -1);
|
|
1806
|
+
}
|
|
1807
|
+
const lens = toLensProps(piece.optics, piece.geometry, options.density, {
|
|
1808
|
+
debug: options.debug,
|
|
1809
|
+
morph: piece.morph,
|
|
1810
|
+
morph2: piece.morph2,
|
|
1811
|
+
touch: piece.touch,
|
|
1812
|
+
progress: piece.progress,
|
|
1813
|
+
light: piece.light,
|
|
1814
|
+
appear: piece.appear,
|
|
1815
|
+
accent: piece.accent
|
|
1816
|
+
});
|
|
1817
|
+
applyChannel(gl, lensLoc, lens.uniformNames, lens.uniformSizes, lens.uniformValues);
|
|
1818
|
+
drawFullscreenTriangle(gl);
|
|
1819
|
+
gl.useProgram(surfaceProgram);
|
|
1820
|
+
const rawSurface = toSurfaceUniforms(piece.optics, piece.geometry, {
|
|
1821
|
+
debug: options.debug,
|
|
1822
|
+
morph: piece.morph,
|
|
1823
|
+
morph2: piece.morph2,
|
|
1824
|
+
bodyInLens: true,
|
|
1825
|
+
touch: piece.touch,
|
|
1826
|
+
progress: piece.progress,
|
|
1827
|
+
// The shadow is denser over text, weaker over a flat backdrop (M 11:47). The rule lives
|
|
1828
|
+
// in the core: while it lived here, Android didn't have it at all.
|
|
1829
|
+
shadow: stats ? shadowOpacity(stats.busy) : 1,
|
|
1830
|
+
// The probe has already computed the ambient color — the shadow gets it for free. Taken
|
|
1831
|
+
// as-is: `settleStats` has already smoothed it, and rounding would bring back exactly the
|
|
1832
|
+
// steps the smoothing was written to remove.
|
|
1833
|
+
ambient: stats ? [stats.r, stats.g, stats.b] : void 0,
|
|
1834
|
+
appear: piece.appear,
|
|
1835
|
+
lift: piece.lift
|
|
1836
|
+
});
|
|
1837
|
+
applyObject(gl, surfaceLoc, {
|
|
1838
|
+
...toDeviceSurfaceUniforms(rawSurface, options.density),
|
|
1839
|
+
u_center: [piece.centerX, piece.centerY]
|
|
1840
|
+
});
|
|
1841
|
+
setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[0]), piece.press ?? 0);
|
|
1842
|
+
setUniform(gl, surfaceLoc(DYNAMIC_UNIFORMS[1]), piece.active ?? 0);
|
|
1843
|
+
const hasIcon = Boolean(piece.icon && options.iconMask);
|
|
1844
|
+
setUniform(gl, surfaceLoc(ICON_UNIFORMS[0]), hasIcon ? 1 : 0);
|
|
1845
|
+
setUniform(gl, surfaceLoc(ICON_UNIFORMS[1]), 1);
|
|
1846
|
+
setUniform(gl, surfaceLoc(ICON_UNIFORMS[2]), piece.inkIdle ?? [1, 1, 1, 1]);
|
|
1847
|
+
setUniform(gl, surfaceLoc(ICON_UNIFORMS[3]), piece.inkActive ?? [1, 1, 1, 1]);
|
|
1848
|
+
bindTextureAt(gl, 1, iconTexture, surfaceProgram, "u_icon");
|
|
1849
|
+
setUniform(gl, surfaceLoc("u_iconSize"), hasIcon ? [width, height] : [1, 1]);
|
|
1850
|
+
const hasColor = Boolean(piece.overlay && options.colorLayer);
|
|
1851
|
+
setUniform(gl, surfaceLoc(OVERLAY_UNIFORMS[0]), hasColor ? 1 : 0);
|
|
1852
|
+
bindTextureAt(gl, 2, colorTexture, surfaceProgram, "u_overlay");
|
|
1853
|
+
setUniform(gl, surfaceLoc("u_overlaySize"), hasColor ? [width, height] : [1, 1]);
|
|
1854
|
+
setUniform(gl, surfaceLoc("u_resolution"), [width, height]);
|
|
1855
|
+
drawFullscreenTriangle(gl);
|
|
1856
|
+
}
|
|
1857
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
1858
|
+
return { probes };
|
|
1859
|
+
}
|
|
1860
|
+
function destroy() {
|
|
1861
|
+
probe.destroy();
|
|
1862
|
+
gl.deleteProgram(blitProgram);
|
|
1863
|
+
gl.deleteProgram(lensProgram);
|
|
1864
|
+
gl.deleteProgram(surfaceProgram);
|
|
1865
|
+
gl.deleteTexture(contentTexture);
|
|
1866
|
+
gl.deleteTexture(iconTexture);
|
|
1867
|
+
gl.deleteTexture(colorTexture);
|
|
1868
|
+
}
|
|
1869
|
+
return { resize, render, destroy };
|
|
1870
|
+
}
|
|
1871
|
+
var REFERENCE_SCENE_WIDTH = 336;
|
|
1872
|
+
var REFERENCE_SCENE_HEIGHT = 252;
|
|
1873
|
+
var REFERENCE_SURROUND = "#3c1f4a";
|
|
1874
|
+
var refGray = (level) => {
|
|
1875
|
+
const v = Math.round(Math.min(Math.max(level, 0), 1) * 255).toString(16).padStart(2, "0");
|
|
1876
|
+
return `#${v}${v}${v}`;
|
|
1877
|
+
};
|
|
1878
|
+
var refCheckerCells = (layer, widthDp, heightDp) => {
|
|
1879
|
+
const cells = [];
|
|
1880
|
+
const lo = layer.level - layer.amp;
|
|
1881
|
+
const hi = layer.level + layer.amp;
|
|
1882
|
+
let seed = 1;
|
|
1883
|
+
for (let y = 0; y < heightDp; y += layer.cellDp) {
|
|
1884
|
+
for (let x = 0; x < widthDp; x += layer.cellDp) {
|
|
1885
|
+
seed = seed * 1103515245 + 12345 & 2147483647;
|
|
1886
|
+
cells.push({ xDp: x, yDp: y, level: (seed >> 16) % 2 ? hi : lo });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return cells;
|
|
1890
|
+
};
|
|
1891
|
+
var refGradientSteps = (layer) => Array.from(
|
|
1892
|
+
{ length: layer.steps },
|
|
1893
|
+
(_, i) => layer.from + (layer.to - layer.from) * i / Math.max(layer.steps - 1, 1)
|
|
1894
|
+
);
|
|
1895
|
+
|
|
1896
|
+
// src/web/reference-draw.ts
|
|
1897
|
+
function drawLayer(ctx, layer, x, y, w, h, d) {
|
|
1898
|
+
switch (layer.kind) {
|
|
1899
|
+
case "flat": {
|
|
1900
|
+
ctx.fillStyle = refGray(layer.level);
|
|
1901
|
+
ctx.fillRect(x, y, w, h);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
case "stripes": {
|
|
1905
|
+
ctx.fillStyle = refGray(layer.level);
|
|
1906
|
+
ctx.fillRect(x, y, w, h);
|
|
1907
|
+
ctx.fillStyle = refGray(layer.other);
|
|
1908
|
+
for (let at = 0; at < w; at += layer.periodDp * d) {
|
|
1909
|
+
ctx.fillRect(x + at, y, layer.widthDp * d, h);
|
|
1910
|
+
}
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
case "checker": {
|
|
1914
|
+
for (const cell of refCheckerCells(layer, w / d, h / d)) {
|
|
1915
|
+
ctx.fillStyle = refGray(cell.level);
|
|
1916
|
+
ctx.fillRect(x + cell.xDp * d, y + cell.yDp * d, layer.cellDp * d + 1, layer.cellDp * d + 1);
|
|
1917
|
+
}
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
case "step": {
|
|
1921
|
+
ctx.fillStyle = refGray(layer.left);
|
|
1922
|
+
ctx.fillRect(x, y, w / 2, h);
|
|
1923
|
+
ctx.fillStyle = refGray(layer.right);
|
|
1924
|
+
ctx.fillRect(x + w / 2, y, w - w / 2, h);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
case "bar": {
|
|
1928
|
+
ctx.fillStyle = refGray(layer.level);
|
|
1929
|
+
ctx.fillRect(x, y, w, h);
|
|
1930
|
+
const t = layer.thicknessDp * d;
|
|
1931
|
+
ctx.fillStyle = refGray(layer.barLevel);
|
|
1932
|
+
ctx.fillRect(x, y + Math.round((h - t) / 2), w, t);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
case "grid": {
|
|
1936
|
+
ctx.fillStyle = refGray(layer.level);
|
|
1937
|
+
ctx.fillRect(x, y, w, h);
|
|
1938
|
+
const thin = 1;
|
|
1939
|
+
const step = layer.stepDp * d;
|
|
1940
|
+
ctx.fillStyle = refGray(layer.lineLevel);
|
|
1941
|
+
for (let at = 0; at <= w; at += step) ctx.fillRect(x + Math.round(at), y, thin, h);
|
|
1942
|
+
for (let at = 0; at <= h; at += step) ctx.fillRect(x, y + Math.round(at), w, thin);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
case "gradient": {
|
|
1946
|
+
const steps = refGradientSteps(layer);
|
|
1947
|
+
const band = h / steps.length;
|
|
1948
|
+
for (let i = 0; i < steps.length; i += 1) {
|
|
1949
|
+
ctx.fillStyle = refGray(steps[i]);
|
|
1950
|
+
ctx.fillRect(x, y + Math.round(i * band), w, Math.ceil(band) + 1);
|
|
1951
|
+
}
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function drawReferenceScene(ctx, scene, width, height, options) {
|
|
1957
|
+
const d = options.density;
|
|
1958
|
+
const bands = scene.bands(options.level ?? scene.level);
|
|
1959
|
+
const fill = options.fit !== "band";
|
|
1960
|
+
let x;
|
|
1961
|
+
let y;
|
|
1962
|
+
let w;
|
|
1963
|
+
let scale;
|
|
1964
|
+
if (fill) {
|
|
1965
|
+
x = 0;
|
|
1966
|
+
y = 0;
|
|
1967
|
+
w = width;
|
|
1968
|
+
scale = height / (REFERENCE_SCENE_HEIGHT * d);
|
|
1969
|
+
} else {
|
|
1970
|
+
ctx.fillStyle = REFERENCE_SURROUND;
|
|
1971
|
+
ctx.fillRect(0, 0, width, height);
|
|
1972
|
+
w = Math.round(REFERENCE_SCENE_WIDTH * d);
|
|
1973
|
+
x = Math.round((width - w) / 2);
|
|
1974
|
+
y = Math.round((height - REFERENCE_SCENE_HEIGHT * d) / 2);
|
|
1975
|
+
scale = 1;
|
|
1976
|
+
}
|
|
1977
|
+
for (const band of bands) {
|
|
1978
|
+
const h = Math.round(band.heightDp * d * scale);
|
|
1979
|
+
drawLayer(ctx, band.layer, x, y, w, h, d);
|
|
1980
|
+
y += h;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
exports.PROBE_GRID_HEIGHT = PROBE_GRID_HEIGHT;
|
|
1985
|
+
exports.PROBE_GRID_WIDTH = PROBE_GRID_WIDTH;
|
|
1986
|
+
exports.createProbe = createProbe;
|
|
1987
|
+
exports.createVireGlassRenderer = createVireGlassRenderer;
|
|
1988
|
+
exports.drawReferenceScene = drawReferenceScene;
|
|
1989
|
+
//# sourceMappingURL=index.cjs.map
|
|
1990
|
+
//# sourceMappingURL=index.cjs.map
|