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
package/src/material.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// VireGlass material model.
|
|
2
|
+
//
|
|
3
|
+
// Two distinct objects, and they must not be confused:
|
|
4
|
+
//
|
|
5
|
+
// VireGlassMaterial — CAUSES. What a piece of glass is described by: index of refraction,
|
|
6
|
+
// thickness, bevel width, roughness, absorption color. This is what gets
|
|
7
|
+
// configured.
|
|
8
|
+
// VireGlassOptics — EFFECTS. Eighteen values the shaders need. Not configured: they're
|
|
9
|
+
// derived (`resolveOptics`, physics in `optics.ts`).
|
|
10
|
+
//
|
|
11
|
+
// Before v4, what was exposed were the effects themselves — eighteen independent sliders. Half of
|
|
12
|
+
// them are physically dependent, so combining them produced states that don't occur in real
|
|
13
|
+
// glass: a thin bevel with a dense white rim, rim optics as a fraction of the element's own size.
|
|
14
|
+
// Every such case was patched by adding yet another slider (`edgeDensity`, a size compensation) —
|
|
15
|
+
// that is, compensating for physics that wasn't there. Here there's one cause, and the effects
|
|
16
|
+
// are derived.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
absorption,
|
|
20
|
+
blur as blurFrom,
|
|
21
|
+
dispersion as dispersionFrom,
|
|
22
|
+
edgeDensity as edgeDensityFrom,
|
|
23
|
+
gatherRadius as gatherRadiusFrom,
|
|
24
|
+
fresnelStrength,
|
|
25
|
+
FRESNEL_EXPONENT,
|
|
26
|
+
mediumTint,
|
|
27
|
+
refractionScale as refractionScaleFrom,
|
|
28
|
+
refractionStrength,
|
|
29
|
+
specularPower as specularPowerFrom,
|
|
30
|
+
bodyDensity as bodyDensityFrom,
|
|
31
|
+
edgeLight as edgeLightFrom,
|
|
32
|
+
iridescence as iridescenceFrom,
|
|
33
|
+
diffraction as diffractionFrom,
|
|
34
|
+
colorPickup as colorPickupFrom,
|
|
35
|
+
specularStrength,
|
|
36
|
+
iorSpread as iorSpreadFrom,
|
|
37
|
+
} from './optics';
|
|
38
|
+
|
|
39
|
+
export type VireGlassTint = { r: number; g: number; b: number };
|
|
40
|
+
|
|
41
|
+
export type VireGlassMaterial = {
|
|
42
|
+
/** Index of refraction of the medium. Water 1.33, glass 1.5, sapphire 1.77. */
|
|
43
|
+
ior: number;
|
|
44
|
+
/** Glass thickness, dp. Drives center magnification and absorption. */
|
|
45
|
+
thickness: number;
|
|
46
|
+
/** Bevel width, dp. Absolute: on real glass the rim doesn't depend on the piece's size. */
|
|
47
|
+
bevel: number;
|
|
48
|
+
/** Surface roughness: backdrop haziness and highlight blur. */
|
|
49
|
+
roughness: number;
|
|
50
|
+
/** Response to device orientation, 0 means the light is locked to the screen. */
|
|
51
|
+
environment: number;
|
|
52
|
+
/** Not physics but a legibility requirement: how strongly the glass has to separate its own
|
|
53
|
+
* lightness from the ink drawn over it. 0 means the glass is simply transparent. */
|
|
54
|
+
legibility: number;
|
|
55
|
+
/** Dimming layer UNDER the glass, 0..1. For the transparent (Clear) variant this is what
|
|
56
|
+
* provides legibility: that glass has no adaptation, and without dimming the ink drowns over
|
|
57
|
+
* bright content.
|
|
58
|
+
*
|
|
59
|
+
* Not to be confused with the mobile surface's `dim` prop: that one compensates for a platform
|
|
60
|
+
* capture quirk (BlurView can't see a scrim drawn over the content), while this is a property
|
|
61
|
+
* of the material itself. */
|
|
62
|
+
dimming: number;
|
|
63
|
+
/** Lightness of what the app draws OVER the glass: 1 is light icons and text, 0 is dark. A
|
|
64
|
+
* cause, not a knob: the calling screen knows it. */
|
|
65
|
+
ink: number;
|
|
66
|
+
/** Minimum PRESENCE of the element itself: how far its body must stand off in lightness from
|
|
67
|
+
* the backdrop behind it. Not the same as `legibility` — that's about the ink OVER the glass,
|
|
68
|
+
* this is about the element itself. Over a uniform backdrop the glass has nothing to refract,
|
|
69
|
+
* and the element disappears; for a control that's unacceptable. 0 means no presence is
|
|
70
|
+
* required.
|
|
71
|
+
*
|
|
72
|
+
* The sign is taken FROM THE BACKDROP, not from ink polarity: over dark the body lightens,
|
|
73
|
+
* over light it darkens. So the requirement works the same way on any backdrop. */
|
|
74
|
+
presence: number;
|
|
75
|
+
/** Surface film thickness, nm. This is where interference comes from: the optical path
|
|
76
|
+
* difference in the film becomes comparable to the wavelength, and the reflection picks up
|
|
77
|
+
* iridescence. 0 means no film. */
|
|
78
|
+
film: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const MATERIAL_RANGES = {
|
|
82
|
+
ior: [1, 2],
|
|
83
|
+
thickness: [0, 60],
|
|
84
|
+
bevel: [0, 40],
|
|
85
|
+
roughness: [0, 1],
|
|
86
|
+
environment: [0, 1],
|
|
87
|
+
legibility: [0, 1],
|
|
88
|
+
dimming: [0, 0.5],
|
|
89
|
+
ink: [0, 1],
|
|
90
|
+
presence: [0, 0.6],
|
|
91
|
+
film: [0, 900],
|
|
92
|
+
} as const satisfies Record<string, readonly [number, number]>;
|
|
93
|
+
|
|
94
|
+
export type VireGlassNumericKey = keyof typeof MATERIAL_RANGES;
|
|
95
|
+
|
|
96
|
+
/** Radius over which the LOCAL backdrop lightness is taken, dp. Not per-pixel: otherwise the
|
|
97
|
+
* glass chases individual strokes and a halo appears around letters beneath it. */
|
|
98
|
+
export const ADAPT_RADIUS = 22;
|
|
99
|
+
|
|
100
|
+
/** Lens dimming on product surfaces — a flat fill over the backdrop. */
|
|
101
|
+
export const PRODUCT_DIM = 0.2;
|
|
102
|
+
|
|
103
|
+
const clamp = (v: number, lo: number, hi: number) => (v < lo ? lo : v > hi ? hi : v);
|
|
104
|
+
|
|
105
|
+
/** The values the shaders need. Assembled from the material, never set by hand. */
|
|
106
|
+
export type VireGlassOptics = {
|
|
107
|
+
blur: number;
|
|
108
|
+
refraction: number;
|
|
109
|
+
refractionScale: number;
|
|
110
|
+
/** Bevel width in dp — an absolute property of the medium. */
|
|
111
|
+
bevelDp: number;
|
|
112
|
+
/** Radius over which the rim gathers light around the element. */
|
|
113
|
+
gatherRadiusDp: number;
|
|
114
|
+
fresnel: number;
|
|
115
|
+
fresnelPower: number;
|
|
116
|
+
specular: number;
|
|
117
|
+
specularPower: number;
|
|
118
|
+
dispersion: number;
|
|
119
|
+
tint: VireGlassTint;
|
|
120
|
+
tintStrength: number;
|
|
121
|
+
edgeDensity: number;
|
|
122
|
+
environment: number;
|
|
123
|
+
legibility: number;
|
|
124
|
+
/** Dimming of the content under the glass — the very layer of the Clear variant. */
|
|
125
|
+
dimming: number;
|
|
126
|
+
ink: number;
|
|
127
|
+
/** Minimum presence of the element against the backdrop — in units of lightness. */
|
|
128
|
+
presence: number;
|
|
129
|
+
adaptRadius: number;
|
|
130
|
+
bodyDensity: number;
|
|
131
|
+
edgeLight: number;
|
|
132
|
+
/** Film thickness, nm — passed straight to the shader: the path difference is computed in the
|
|
133
|
+
* same units. */
|
|
134
|
+
film: number;
|
|
135
|
+
/** Strength of reflection iridescence. */
|
|
136
|
+
iridescence: number;
|
|
137
|
+
/** Strength of the edge diffraction fringes. */
|
|
138
|
+
diffraction: number;
|
|
139
|
+
/** How much the body is tinted by the color of its surroundings. */
|
|
140
|
+
colorPickup: number;
|
|
141
|
+
/** Index of refraction: the shader computes ray bending from it via Snell's law. */
|
|
142
|
+
ior: number;
|
|
143
|
+
/** Plate thickness, dp — before the correction for element size. */
|
|
144
|
+
thicknessDp: number;
|
|
145
|
+
/** Spread of the index between the red and blue channels — dispersion. */
|
|
146
|
+
iorSpread: number;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// The product default — "water": lower index of refraction, thinner medium and bevel, an almost
|
|
150
|
+
// smooth surface. Chosen on-device at the bench.
|
|
151
|
+
export const VIREGLASS_MATERIAL_V4: VireGlassMaterial = {
|
|
152
|
+
ior: 1.33,
|
|
153
|
+
thickness: 14,
|
|
154
|
+
bevel: 5,
|
|
155
|
+
roughness: 0.05,
|
|
156
|
+
environment: 0.27,
|
|
157
|
+
legibility: 0.26,
|
|
158
|
+
dimming: 0,
|
|
159
|
+
ink: 1,
|
|
160
|
+
presence: 0.05,
|
|
161
|
+
film: 340,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Glass, not water. v4's index of refraction, 1.33, is water: almost no reflection at the rim,
|
|
167
|
+
* almost no dispersion, 2% magnification. On screen that surface honestly reads as "nothing much",
|
|
168
|
+
* and it was being mistaken for glass that wasn't rendering at all.
|
|
169
|
+
*
|
|
170
|
+
* 1.5 is ordinary glass. The effects follow from it on their own: reflection at the rim twice as
|
|
171
|
+
* strong, dispersion one and a half times, ambient color pickup at its ceiling. Thickness is
|
|
172
|
+
* raised a little too: it drives magnification, but absorption comes along with it, and
|
|
173
|
+
* absorption is blackness.
|
|
174
|
+
*/
|
|
175
|
+
export const VIREGLASS_MATERIAL_V5: VireGlassMaterial = {
|
|
176
|
+
ior: 1.5,
|
|
177
|
+
thickness: 28,
|
|
178
|
+
bevel: 6,
|
|
179
|
+
roughness: 0.06,
|
|
180
|
+
environment: 0.27,
|
|
181
|
+
legibility: 0.26,
|
|
182
|
+
dimming: 0,
|
|
183
|
+
ink: 1,
|
|
184
|
+
presence: 0.05,
|
|
185
|
+
film: 340,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Frosted sheet glass. The sheet sits over a LIVE screen, and it can't be transparent: transport
|
|
190
|
+
* buttons and progress used to read right through it — the layout looked broken. Dimming doesn't
|
|
191
|
+
* fix this (a flat scrim over a sharp picture doesn't hide it), so roughness is raised instead: it
|
|
192
|
+
* drives `u_frost`, a floor on backdrop blur that the adaptive-blur ceiling doesn't reach.
|
|
193
|
+
*/
|
|
194
|
+
export const VIREGLASS_SHEET_MATERIAL: VireGlassMaterial = {
|
|
195
|
+
...VIREGLASS_MATERIAL_V5,
|
|
196
|
+
roughness: 0.85,
|
|
197
|
+
};
|
|
198
|
+
/** The product's material. Consumers use this, not a specific version by name. */
|
|
199
|
+
/**
|
|
200
|
+
* Glass under large text (the lyrics panel in the player).
|
|
201
|
+
*
|
|
202
|
+
* The product material with NO change to optics — only `legibility` is raised. That's exactly its
|
|
203
|
+
* reason to exist: "text on me must stay readable." It adds density locally — denser over light
|
|
204
|
+
* and busy backdrops, more transparent over an even dark one.
|
|
205
|
+
*
|
|
206
|
+
* A dedicated thick material (thickness 52, ior 1.58) used to sit here and was removed: medium
|
|
207
|
+
* absorption grows with thickness, and the panel picked up a black fringe around its perimeter —
|
|
208
|
+
* a sooty plate instead of glass. Don't turn thickness into a legibility knob; that's what
|
|
209
|
+
* `legibility` is for.
|
|
210
|
+
*/
|
|
211
|
+
export const VIREGLASS_LYRICS_MATERIAL: VireGlassMaterial = {
|
|
212
|
+
...VIREGLASS_MATERIAL_V5,
|
|
213
|
+
legibility: 0.95,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Glass for controls — buttons, tiles, anything that gets pressed.
|
|
218
|
+
*
|
|
219
|
+
* THICK AND CLEAR, not the baseline: wider bevel, thickness up by half, higher index of
|
|
220
|
+
* refraction and almost no roughness. Baseline glass is a lens over a calm backdrop; a control has
|
|
221
|
+
* to read as an object you can pick up, and that's not done by a fill but by volume: a wide bevel
|
|
222
|
+
* gives the rim something to catch, and thickness gives it depth.
|
|
223
|
+
*
|
|
224
|
+
* Thickness is set by BUILDUP AT THE RIM: on the reference control the stripe image under the
|
|
225
|
+
* element bulges there by a factor of 1.85; ours came out at 1.36 at 30. Saturation sets in around
|
|
226
|
+
* 44, and any extra thickness past that only adds absorption — hence 44, not more.
|
|
227
|
+
*
|
|
228
|
+
* It also drags `tintStrength` (= `absorption(thickness)`) along with it, and on the path WITHOUT
|
|
229
|
+
* a native lens that sets body density: there the element is denser by 2.4 percentage points. On
|
|
230
|
+
* the web the surface has no body at all; the effect only shows on Android below 13.
|
|
231
|
+
*
|
|
232
|
+
* `presence` is raised for the same reason: a piece of background is allowed to disappear over a
|
|
233
|
+
* uniform canvas, a control isn't — it has to be visible before it's been tapped.
|
|
234
|
+
*
|
|
235
|
+
* IMPORTANT about size. The bevel is specified by the medium in dp and doesn't depend on the
|
|
236
|
+
* element's size, but the geometry clamps it to half the smaller half-size (`MAX_BEVEL_FRACTION`).
|
|
237
|
+
* On a 52 dp button that's 13 instead of 16 — a small element made of this glass ends up being
|
|
238
|
+
* bevel all the way through. That's by design: a small piece of thick glass is supposed to look
|
|
239
|
+
* like a lens, not a slab.
|
|
240
|
+
*/
|
|
241
|
+
export const VIREGLASS_CONTROL_MATERIAL: VireGlassMaterial = {
|
|
242
|
+
...VIREGLASS_MATERIAL_V5,
|
|
243
|
+
ior: 1.69,
|
|
244
|
+
thickness: 44,
|
|
245
|
+
bevel: 8,
|
|
246
|
+
roughness: 0.035,
|
|
247
|
+
presence: 0.176,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const VIREGLASS_MATERIAL = VIREGLASS_MATERIAL_V5;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* An element that CARRIES app ink (an icon, a line of text, cover art) has to keep it legible —
|
|
254
|
+
* that's the reason `legibility` exists. An element with no ink has nothing to separate lightness
|
|
255
|
+
* from, so the requirement is turned off for it: the model just promises it plain transparent
|
|
256
|
+
* glass.
|
|
257
|
+
*
|
|
258
|
+
* The rule lives here, not with the consumer: otherwise the same icon button would get different
|
|
259
|
+
* legibility on the web and on Android.
|
|
260
|
+
*/
|
|
261
|
+
export function materialForInk(material: VireGlassMaterial, carriesInk: boolean): VireGlassMaterial {
|
|
262
|
+
// For the transparent variant, legibility is held by the dimming layer, not the body. Raising
|
|
263
|
+
// its requirement would just turn it into ordinary glass, and the variants don't mix (219 §Clear).
|
|
264
|
+
if (material.dimming > 0) return material;
|
|
265
|
+
return { ...material, legibility: carriesInk ? VIREGLASS_LYRICS_MATERIAL.legibility : 0 };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The transparent material variant (Clear). It has no adaptation — the glass stays consistently
|
|
270
|
+
* more transparent, content beneath it shows through almost as-is, and legibility of the ink is
|
|
271
|
+
* held by the dimming layer instead. Fit only where the reference's three conditions hold: the
|
|
272
|
+
* element sits over media, the content can tolerate dimming, and the ink over it is large and
|
|
273
|
+
* bright.
|
|
274
|
+
*/
|
|
275
|
+
export const VIREGLASS_CLEAR_MATERIAL: VireGlassMaterial = {
|
|
276
|
+
...VIREGLASS_MATERIAL,
|
|
277
|
+
legibility: 0,
|
|
278
|
+
presence: 0,
|
|
279
|
+
// Apple's own numbers: 0.35 in the HIG Materials text, 0.3 in its own sample code. We take the
|
|
280
|
+
// lower one — it comes from working code, not from prose, and Clear has to stay a window.
|
|
281
|
+
dimming: 0.3,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* ACTIVE STATE as a state of the MEDIUM, not a highlight drawn over it.
|
|
286
|
+
*
|
|
287
|
+
* An active element is denser, clearer glass: higher index of refraction, thicker body, wider
|
|
288
|
+
* bevel, less roughness. A bright rim, a strong highlight and greater presence all follow from
|
|
289
|
+
* these causes on their own — assembling them separately would produce a state that doesn't occur
|
|
290
|
+
* in real glass.
|
|
291
|
+
*
|
|
292
|
+
* `environment` is deliberately left untouched here. It sets how much the body is tinted by
|
|
293
|
+
* what's beneath it, and at full strength an active button over cover art turned into a colored
|
|
294
|
+
* blob: the state read as "smudged" rather than "selected." A state indicator must not depend on
|
|
295
|
+
* the backdrop — otherwise any image sliding underneath the element would fake it.
|
|
296
|
+
*/
|
|
297
|
+
export function activeMaterial(material: VireGlassMaterial, on: number): VireGlassMaterial {
|
|
298
|
+
const k = Math.min(Math.max(on, 0), 1);
|
|
299
|
+
return {
|
|
300
|
+
...material,
|
|
301
|
+
ior: material.ior + 0.35 * k,
|
|
302
|
+
thickness: material.thickness * (1 + 0.9 * k),
|
|
303
|
+
bevel: material.bevel * (1 + 1.8 * k),
|
|
304
|
+
roughness: material.roughness * (1 - 0.75 * k),
|
|
305
|
+
presence: material.presence + 0.12 * k,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function resolveMaterial(patch: Partial<VireGlassMaterial> = {}): VireGlassMaterial {
|
|
310
|
+
const m = { ...VIREGLASS_MATERIAL, ...patch };
|
|
311
|
+
const out = { ...m };
|
|
312
|
+
for (const key of Object.keys(MATERIAL_RANGES) as VireGlassNumericKey[]) {
|
|
313
|
+
const [lo, hi] = MATERIAL_RANGES[key];
|
|
314
|
+
out[key] = clamp(out[key], lo, hi);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Causes → effects. All of the transition's physics lives in `optics.ts`. */
|
|
320
|
+
export function resolveOptics(patch: Partial<VireGlassMaterial> = {}): VireGlassOptics {
|
|
321
|
+
const m = resolveMaterial(patch);
|
|
322
|
+
return {
|
|
323
|
+
blur: blurFrom(m.roughness),
|
|
324
|
+
refraction: refractionStrength(m.ior),
|
|
325
|
+
refractionScale: refractionScaleFrom(m.ior, m.thickness),
|
|
326
|
+
bevelDp: m.bevel,
|
|
327
|
+
gatherRadiusDp: gatherRadiusFrom(m.bevel),
|
|
328
|
+
fresnel: fresnelStrength(m.ior),
|
|
329
|
+
fresnelPower: FRESNEL_EXPONENT,
|
|
330
|
+
specular: specularStrength(m.ior, m.roughness),
|
|
331
|
+
specularPower: specularPowerFrom(m.roughness),
|
|
332
|
+
dispersion: dispersionFrom(m.ior),
|
|
333
|
+
tint: mediumTint(m.ior),
|
|
334
|
+
tintStrength: absorption(m.thickness),
|
|
335
|
+
edgeDensity: edgeDensityFrom(m.thickness, m.bevel),
|
|
336
|
+
environment: m.environment,
|
|
337
|
+
legibility: m.legibility,
|
|
338
|
+
dimming: m.dimming,
|
|
339
|
+
ink: m.ink,
|
|
340
|
+
presence: m.presence,
|
|
341
|
+
adaptRadius: ADAPT_RADIUS,
|
|
342
|
+
bodyDensity: bodyDensityFrom(m.thickness),
|
|
343
|
+
edgeLight: edgeLightFrom(m.ior),
|
|
344
|
+
film: m.film,
|
|
345
|
+
iridescence: iridescenceFrom(m.ior, m.film),
|
|
346
|
+
diffraction: diffractionFrom(m.ior),
|
|
347
|
+
colorPickup: colorPickupFrom(m.ior),
|
|
348
|
+
ior: m.ior,
|
|
349
|
+
thicknessDp: m.thickness,
|
|
350
|
+
iorSpread: iorSpreadFrom(m.ior),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Frozen snapshots of the EFFECTS from the previous model. Kept as reference points until v4
|
|
356
|
+
* settles: they can't be converted to causes — some of the values are internally inconsistent
|
|
357
|
+
* (exactly why the move was made in the first place), and any "equivalent" material would be a
|
|
358
|
+
* lie.
|
|
359
|
+
*/
|
|
360
|
+
export const LEGACY_OPTICS = {
|
|
361
|
+
'v3-manual': {
|
|
362
|
+
blur: 4.32,
|
|
363
|
+
refraction: 0.49,
|
|
364
|
+
refractionScale: 1.05,
|
|
365
|
+
bevelDp: 12.6,
|
|
366
|
+
gatherRadiusDp: 40,
|
|
367
|
+
fresnel: 0.77,
|
|
368
|
+
fresnelPower: 2.86,
|
|
369
|
+
specular: 0.3,
|
|
370
|
+
specularPower: 74.58,
|
|
371
|
+
dispersion: 0.54,
|
|
372
|
+
tint: { r: 0.4, g: 0.4, b: 0.44 },
|
|
373
|
+
tintStrength: 0.35,
|
|
374
|
+
edgeDensity: 1.5,
|
|
375
|
+
environment: 0.27,
|
|
376
|
+
legibility: 0.55,
|
|
377
|
+
dimming: 0,
|
|
378
|
+
ink: 1,
|
|
379
|
+
presence: 0,
|
|
380
|
+
adaptRadius: ADAPT_RADIUS,
|
|
381
|
+
bodyDensity: 0.14,
|
|
382
|
+
edgeLight: 0.35,
|
|
383
|
+
film: 0,
|
|
384
|
+
iridescence: 0,
|
|
385
|
+
diffraction: 0,
|
|
386
|
+
colorPickup: 0,
|
|
387
|
+
ior: 1.5,
|
|
388
|
+
thicknessDp: 16,
|
|
389
|
+
iorSpread: 0,
|
|
390
|
+
},
|
|
391
|
+
'v2': {
|
|
392
|
+
blur: 5,
|
|
393
|
+
refraction: 0.95,
|
|
394
|
+
refractionScale: 1.34,
|
|
395
|
+
bevelDp: 14,
|
|
396
|
+
gatherRadiusDp: 40,
|
|
397
|
+
fresnel: 0.72,
|
|
398
|
+
fresnelPower: 2.4,
|
|
399
|
+
specular: 0.38,
|
|
400
|
+
specularPower: 46,
|
|
401
|
+
dispersion: 0.3,
|
|
402
|
+
tint: { r: 0.4, g: 0.4, b: 0.44 },
|
|
403
|
+
tintStrength: 0.1,
|
|
404
|
+
edgeDensity: 3.63,
|
|
405
|
+
environment: 0,
|
|
406
|
+
legibility: 0,
|
|
407
|
+
dimming: 0,
|
|
408
|
+
ink: 1,
|
|
409
|
+
presence: 0,
|
|
410
|
+
adaptRadius: ADAPT_RADIUS,
|
|
411
|
+
bodyDensity: 0.14,
|
|
412
|
+
edgeLight: 0.35,
|
|
413
|
+
film: 0,
|
|
414
|
+
iridescence: 0,
|
|
415
|
+
diffraction: 0,
|
|
416
|
+
colorPickup: 0,
|
|
417
|
+
ior: 1.5,
|
|
418
|
+
thicknessDp: 16,
|
|
419
|
+
iorSpread: 0,
|
|
420
|
+
},
|
|
421
|
+
'v1': {
|
|
422
|
+
blur: 12,
|
|
423
|
+
refraction: 0.55,
|
|
424
|
+
refractionScale: 1.14,
|
|
425
|
+
bevelDp: 10,
|
|
426
|
+
gatherRadiusDp: 40,
|
|
427
|
+
fresnel: 0.5,
|
|
428
|
+
fresnelPower: 3.2,
|
|
429
|
+
specular: 0.42,
|
|
430
|
+
specularPower: 58,
|
|
431
|
+
dispersion: 0.22,
|
|
432
|
+
tint: { r: 0.4, g: 0.4, b: 0.44 },
|
|
433
|
+
tintStrength: 0.16,
|
|
434
|
+
edgeDensity: 3.63,
|
|
435
|
+
environment: 0,
|
|
436
|
+
legibility: 0,
|
|
437
|
+
dimming: 0,
|
|
438
|
+
ink: 1,
|
|
439
|
+
presence: 0,
|
|
440
|
+
adaptRadius: ADAPT_RADIUS,
|
|
441
|
+
bodyDensity: 0.14,
|
|
442
|
+
edgeLight: 0.35,
|
|
443
|
+
film: 0,
|
|
444
|
+
iridescence: 0,
|
|
445
|
+
diffraction: 0,
|
|
446
|
+
colorPickup: 0,
|
|
447
|
+
ior: 1.5,
|
|
448
|
+
thicknessDp: 16,
|
|
449
|
+
iorSpread: 0,
|
|
450
|
+
},
|
|
451
|
+
} as const satisfies Record<string, VireGlassOptics>;
|
|
452
|
+
|
|
453
|
+
export type LegacyOpticsName = keyof typeof LEGACY_OPTICS;
|
|
454
|
+
export const LEGACY_NAMES = Object.keys(LEGACY_OPTICS) as LegacyOpticsName[];
|
|
455
|
+
|
|
456
|
+
/** Material presets: points in the space of CAUSES, not a set of ready-made effects. */
|
|
457
|
+
export const MATERIAL_PRESETS = {
|
|
458
|
+
water: VIREGLASS_MATERIAL_V4,
|
|
459
|
+
glass: { ...VIREGLASS_MATERIAL_V4, ior: 1.45, thickness: 25, bevel: 12.6, roughness: 0.17 },
|
|
460
|
+
crystal: { ...VIREGLASS_MATERIAL_V4, ior: 1.7, thickness: 30, bevel: 16, roughness: 0.02 },
|
|
461
|
+
frosted: { ...VIREGLASS_MATERIAL_V4, roughness: 0.55 },
|
|
462
|
+
thick: { ...VIREGLASS_MATERIAL_V4, thickness: 48, bevel: 22 },
|
|
463
|
+
thin: { ...VIREGLASS_MATERIAL_V4, thickness: 4, bevel: 3 },
|
|
464
|
+
iridescent: { ...VIREGLASS_MATERIAL_V4, ior: 1.5, thickness: 8, bevel: 6, film: 620 },
|
|
465
|
+
} as const satisfies Record<string, VireGlassMaterial>;
|
|
466
|
+
|
|
467
|
+
export type MaterialPresetName = keyof typeof MATERIAL_PRESETS;
|
|
468
|
+
export const PRESET_NAMES = Object.keys(MATERIAL_PRESETS) as MaterialPresetName[];
|
|
469
|
+
|
|
470
|
+
export const EFFECTS = [
|
|
471
|
+
'backdrop',
|
|
472
|
+
'blur',
|
|
473
|
+
'refraction',
|
|
474
|
+
'fresnel',
|
|
475
|
+
'bevel',
|
|
476
|
+
'specular',
|
|
477
|
+
'dispersion',
|
|
478
|
+
'tint',
|
|
479
|
+
'environment',
|
|
480
|
+
'legibility',
|
|
481
|
+
'interference',
|
|
482
|
+
'diffraction',
|
|
483
|
+
] as const;
|
|
484
|
+
|
|
485
|
+
export type VireGlassEffect = (typeof EFFECTS)[number];
|
|
486
|
+
export type VireGlassToggles = Record<VireGlassEffect, boolean>;
|
|
487
|
+
|
|
488
|
+
export const ALL_EFFECTS_ON: VireGlassToggles = EFFECTS.reduce(
|
|
489
|
+
(acc, e) => ({ ...acc, [e]: true }),
|
|
490
|
+
{} as VireGlassToggles,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* A toggle zeroes out an EFFECT, it isn't a branch in the shader: the ON/OFF comparison runs on
|
|
495
|
+
* one shader variant, otherwise two different programs would be getting compared. It operates on
|
|
496
|
+
* optics, not on the material: a cause can't be zeroed out — it drags half a dozen effects along
|
|
497
|
+
* with it. `backdrop` isn't handled by a toggle here: it decides whether to mount the BlurView at
|
|
498
|
+
* all.
|
|
499
|
+
*/
|
|
500
|
+
export function applyToggles(
|
|
501
|
+
optics: VireGlassOptics,
|
|
502
|
+
toggles: Partial<VireGlassToggles> = {},
|
|
503
|
+
): VireGlassOptics {
|
|
504
|
+
const on = { ...ALL_EFFECTS_ON, ...toggles };
|
|
505
|
+
const o = { ...optics, tint: { ...optics.tint } };
|
|
506
|
+
if (!on.blur) o.blur = 0;
|
|
507
|
+
if (!on.refraction) {
|
|
508
|
+
o.refraction = 0;
|
|
509
|
+
o.refractionScale = 1;
|
|
510
|
+
o.ior = 1;
|
|
511
|
+
}
|
|
512
|
+
if (!on.fresnel) o.fresnel = 0;
|
|
513
|
+
if (!on.bevel) o.bevelDp = 1;
|
|
514
|
+
if (!on.specular) o.specular = 0;
|
|
515
|
+
if (!on.dispersion) {
|
|
516
|
+
o.dispersion = 0;
|
|
517
|
+
o.iorSpread = 0;
|
|
518
|
+
}
|
|
519
|
+
if (!on.tint) o.tintStrength = 0;
|
|
520
|
+
if (!on.environment) o.environment = 0;
|
|
521
|
+
if (!on.legibility) {
|
|
522
|
+
o.legibility = 0;
|
|
523
|
+
// The dimming layer is the same answer to the legibility requirement, just for the
|
|
524
|
+
// transparent variant.
|
|
525
|
+
o.dimming = 0;
|
|
526
|
+
}
|
|
527
|
+
if (!on.interference) o.iridescence = 0;
|
|
528
|
+
if (!on.diffraction) o.diffraction = 0;
|
|
529
|
+
return o;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Order = the `u_debug` value in both shaders. Bench-only. */
|
|
533
|
+
export const DEBUG_MODES = [
|
|
534
|
+
'normal',
|
|
535
|
+
'sdf',
|
|
536
|
+
'mask',
|
|
537
|
+
'edge',
|
|
538
|
+
'fresnel',
|
|
539
|
+
'refraction',
|
|
540
|
+
'backdrop',
|
|
541
|
+
'specular',
|
|
542
|
+
'dispersion',
|
|
543
|
+
'normals',
|
|
544
|
+
'spectral',
|
|
545
|
+
'adapt',
|
|
546
|
+
'probe',
|
|
547
|
+
] as const;
|
|
548
|
+
|
|
549
|
+
export type VireGlassDebugMode = (typeof DEBUG_MODES)[number];
|
|
550
|
+
|
|
551
|
+
export function debugIndex(mode: VireGlassDebugMode): number {
|
|
552
|
+
return DEBUG_MODES.indexOf(mode);
|
|
553
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Accelerometer } from 'expo-sensors';
|
|
3
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
|
+
import { REST_LIGHT } from '../adapters';
|
|
5
|
+
|
|
6
|
+
// Raw accelerometer readings don't go straight into the shader. Pipeline: sensor → normalized
|
|
7
|
+
// tilt → low-pass filter → deadzone → light direction. Without the filter and the deadzone, a
|
|
8
|
+
// phone at rest still shows visible highlight jitter — sensor noise exceeds the highlight's
|
|
9
|
+
// noticeability threshold.
|
|
10
|
+
const INTERVAL_MS = 50;
|
|
11
|
+
const SMOOTHING = 0.12;
|
|
12
|
+
const DEADZONE = 0.008;
|
|
13
|
+
/** How far the key light swings at full tilt, radians. */
|
|
14
|
+
const MAX_SWING = 0.55;
|
|
15
|
+
|
|
16
|
+
function rotate(v: readonly number[], angle: number): number[] {
|
|
17
|
+
const c = Math.cos(angle);
|
|
18
|
+
const s = Math.sin(angle);
|
|
19
|
+
return [v[0] * c - v[1] * s, v[0] * s + v[1] * c];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tilt is shared across every surface — there's one sensor per device. The subscription is
|
|
23
|
+
// shared too: with `environment > 0` in the default material, otherwise each piece of glass (up
|
|
24
|
+
// to six at once) got its own listener and recomputed the same filter from scratch.
|
|
25
|
+
const listeners = new Set<(tilt: number) => void>();
|
|
26
|
+
let subscription: { remove: () => void } | null = null;
|
|
27
|
+
let tilt = 0;
|
|
28
|
+
|
|
29
|
+
function subscribe(cb: (tilt: number) => void): () => void {
|
|
30
|
+
listeners.add(cb);
|
|
31
|
+
if (!subscription) {
|
|
32
|
+
Accelerometer.setUpdateInterval(INTERVAL_MS);
|
|
33
|
+
subscription = Accelerometer.addListener(({ x, y }) => {
|
|
34
|
+
const raw = Math.max(-1, Math.min(1, x * 0.7 + y * 0.3));
|
|
35
|
+
tilt += (raw - tilt) * SMOOTHING;
|
|
36
|
+
for (const listener of listeners) listener(tilt);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
listeners.delete(cb);
|
|
41
|
+
if (listeners.size === 0) {
|
|
42
|
+
subscription?.remove();
|
|
43
|
+
subscription = null;
|
|
44
|
+
tilt = 0;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Key light direction. `strength = 0` doesn't subscribe to the sensor at all and keeps the light
|
|
51
|
+
* locked to the screen.
|
|
52
|
+
*/
|
|
53
|
+
export function useEnvironmentLight(strength: number): SharedValue<readonly number[]> {
|
|
54
|
+
const light = useSharedValue<readonly number[]>(REST_LIGHT);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (strength <= 0) {
|
|
58
|
+
light.value = REST_LIGHT;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let emitted = 0;
|
|
62
|
+
return subscribe((next) => {
|
|
63
|
+
if (Math.abs(next - emitted) < DEADZONE) return;
|
|
64
|
+
emitted = next;
|
|
65
|
+
light.value = rotate(REST_LIGHT, -next * MAX_SWING * strength);
|
|
66
|
+
});
|
|
67
|
+
}, [strength, light]);
|
|
68
|
+
|
|
69
|
+
return light;
|
|
70
|
+
}
|