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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REFERENCE CANVASES — the set that both benches draw and that the gates run against.
|
|
3
|
+
*
|
|
4
|
+
* There used to be three kinds of canvas, each in a different place: the web bench had its own
|
|
5
|
+
* zones, the mobile lab had its own, and a third set lived inside `check-optics.mjs`, invisible
|
|
6
|
+
* to everyone else. So the gate measured one thing, the eye looked at another, and there was
|
|
7
|
+
* nothing to confirm the question "does Android differ?" against.
|
|
8
|
+
*
|
|
9
|
+
* The canvases are described as DATA, not drawing code: the canvas and the native views draw
|
|
10
|
+
* differently, but both platforms lay out the same list of layers. Everything is in dp and in
|
|
11
|
+
* fractions — fractions of the stage kept the stripes the same relative thickness on benches of
|
|
12
|
+
* different sizes.
|
|
13
|
+
*
|
|
14
|
+
* Each canvas answers for one promise of the material (`what`), and that's also the caption on
|
|
15
|
+
* the bench. A canvas can't be added to only one bench: platform parity would go back to being
|
|
16
|
+
* a matter of impression.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { capsuleGeometry, circleGeometry, roundedRectGeometry, type VireGlassGeometry } from './geometry';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* BENCH SHAPES — shared too. Element size feeds into the optics through sizeGain: a 120 circle
|
|
23
|
+
* and a 150 circle are glass of different thickness, and screenshots from two benches on
|
|
24
|
+
* different shapes are as incomparable as on different canvases. The values match the ones the
|
|
25
|
+
* reference comparisons were shot on.
|
|
26
|
+
*/
|
|
27
|
+
export const REFERENCE_SHAPES = {
|
|
28
|
+
circle: circleGeometry(120),
|
|
29
|
+
capsule: capsuleGeometry(300, 110),
|
|
30
|
+
tile: roundedRectGeometry(220, 120, 32),
|
|
31
|
+
} satisfies Record<string, VireGlassGeometry>;
|
|
32
|
+
|
|
33
|
+
export type VireGlassRefShapeName = keyof typeof REFERENCE_SHAPES;
|
|
34
|
+
|
|
35
|
+
/** A stripe's layer. Lightness is 0..1, sizes are dp: gray, not color (there's nothing to
|
|
36
|
+
* measure color with). */
|
|
37
|
+
export type VireGlassRefLayer =
|
|
38
|
+
| { readonly kind: 'flat'; readonly level: number }
|
|
39
|
+
/** Vertical stripes: wider than the bevel, so the body absorbs them rather than the rim. */
|
|
40
|
+
| { readonly kind: 'stripes'; readonly level: number; readonly other: number; readonly periodDp: number; readonly widthDp: number }
|
|
41
|
+
/** A pseudo-random checkerboard — a "cover": structure that fights the ink drawn over the glass. */
|
|
42
|
+
| { readonly kind: 'checker'; readonly level: number; readonly amp: number; readonly cellDp: number }
|
|
43
|
+
/** Left and right halves at different lightness. */
|
|
44
|
+
| { readonly kind: 'step'; readonly left: number; readonly right: number }
|
|
45
|
+
/** A dark bar across the middle of the stripe — measures bulging at the rim against it. */
|
|
46
|
+
| { readonly kind: 'bar'; readonly level: number; readonly barLevel: number; readonly thicknessDp: number }
|
|
47
|
+
/** A fine grid: any lens distortion shows up on a straight line right away. */
|
|
48
|
+
| { readonly kind: 'grid'; readonly level: number; readonly lineLevel: number; readonly stepDp: number }
|
|
49
|
+
/** A stepped gradient. Steps, not a smooth fill: the native views can't draw a gradient without
|
|
50
|
+
* an image. */
|
|
51
|
+
| { readonly kind: 'gradient'; readonly from: number; readonly to: number; readonly steps: number };
|
|
52
|
+
|
|
53
|
+
export type VireGlassRefBand = {
|
|
54
|
+
/** Stripe thickness in dp. The sum over a canvas is `REFERENCE_SCENE_HEIGHT`. */
|
|
55
|
+
readonly heightDp: number;
|
|
56
|
+
readonly layer: VireGlassRefLayer;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Canvas size IN DP, one for all. Height is kept within the mobile lab's zone (0.42 of the
|
|
61
|
+
* screen height), width within the narrowest phone (360 dp) with room to spare for the
|
|
62
|
+
* surrounding field.
|
|
63
|
+
*
|
|
64
|
+
* Width matters NO LESS than height. The pattern is measured from the edge of the canvas, so on
|
|
65
|
+
* a canvas spanning the full width of the stage, the phase of the pattern under the element
|
|
66
|
+
* depended on screen width: 24 dp-period stripes landed under the center differently on a 406 dp
|
|
67
|
+
* phone versus a fifteen-hundred-pixel canvas, and the checkerboard put an entirely different
|
|
68
|
+
* cell arrangement under the element. A fixed-size panel removes this whole class of discrepancy
|
|
69
|
+
* at once.
|
|
70
|
+
*/
|
|
71
|
+
export const REFERENCE_SCENE_WIDTH = 336;
|
|
72
|
+
export const REFERENCE_SCENE_HEIGHT = 252;
|
|
73
|
+
|
|
74
|
+
/** The field around the canvas. Deliberately off the stripe levels and not gray: it shows where
|
|
75
|
+
* the reference area ends, so its border can't be mistaken for a stripe in a screenshot. */
|
|
76
|
+
export const REFERENCE_SURROUND = '#3c1f4a';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Where the element sits on the canvas — dp from the panel's top-left corner. Position is as much
|
|
80
|
+
* a part of the contract as canvas size and shape: a bench that parks the element its own way
|
|
81
|
+
* shows it over DIFFERENT stripes, and there's nothing left to compare screenshots against.
|
|
82
|
+
*
|
|
83
|
+
* This has drifted before: the web bench centered the element within the VISIBLE area, while the
|
|
84
|
+
* canvas was drawn centered on the full canvas element — with the control panel open, those are
|
|
85
|
+
* two different points.
|
|
86
|
+
*/
|
|
87
|
+
export const REFERENCE_PIECE_AT = {
|
|
88
|
+
xDp: REFERENCE_SCENE_WIDTH / 2,
|
|
89
|
+
yDp: REFERENCE_SCENE_HEIGHT / 2,
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
export type VireGlassRefScene = {
|
|
93
|
+
readonly name: string;
|
|
94
|
+
/** What this canvas checks. Also the caption on the bench — otherwise the set turns to mush. */
|
|
95
|
+
readonly what: string;
|
|
96
|
+
/** Stripes at a given backdrop lightness: benches call with `level`, the gate sweeps the whole range. */
|
|
97
|
+
readonly bands: (level: number) => readonly VireGlassRefBand[];
|
|
98
|
+
/** Default lightness. Canvases that don't need it simply ignore it. */
|
|
99
|
+
readonly level: number;
|
|
100
|
+
/**
|
|
101
|
+
* Whether the canvas is fit for NUMERIC platform comparison. A lightness profile needs a
|
|
102
|
+
* backdrop that's uniform along the row; on a periodic grid of pixel-wide lines, the average
|
|
103
|
+
* is determined by how the platform rasterizes that line, not by the material. Such a canvas
|
|
104
|
+
* stays in the set — distortion of a straight line is visible to the eye at a glance — but it's
|
|
105
|
+
* excluded from the comparison table.
|
|
106
|
+
*/
|
|
107
|
+
readonly measurable?: false;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const whole = (layer: VireGlassRefLayer): readonly VireGlassRefBand[] => [
|
|
111
|
+
{ heightDp: REFERENCE_SCENE_HEIGHT, layer },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/** Stripe contrast is the same at every level, so transmission compares fairly between steps. */
|
|
115
|
+
const stripeLevel = (level: number) => (level < 0.5 ? level + 0.22 : level - 0.22);
|
|
116
|
+
|
|
117
|
+
export const REFERENCE_SCENES: readonly VireGlassRefScene[] = [
|
|
118
|
+
{
|
|
119
|
+
name: 'flat',
|
|
120
|
+
what: 'the body standing off the backdrop: the element must be visible over a flat canvas',
|
|
121
|
+
level: 0.5,
|
|
122
|
+
bands: (level) => whole({ kind: 'flat', level }),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'stripes',
|
|
126
|
+
what: 'the window: the brightness range inside the element is a noticeable fraction of the range outside it',
|
|
127
|
+
level: 0.5,
|
|
128
|
+
bands: (level) => whole({ kind: 'stripes', level, other: stripeLevel(level), periodDp: 24, widthDp: 10 }),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'busy',
|
|
132
|
+
what: 'ink against content: both the ink and whatever is under the glass have to live over a busy cover',
|
|
133
|
+
level: 0.5,
|
|
134
|
+
bands: (level) => whole({ kind: 'checker', level, amp: 0.28, cellDp: 16 }),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'bar',
|
|
138
|
+
what: 'the rim gathers content: at the silhouette the lens bulges out whatever sits behind it',
|
|
139
|
+
level: 0.93,
|
|
140
|
+
bands: (level) => whole({ kind: 'bar', level, barLevel: 0.12, thicknessDp: 20 }),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'steps',
|
|
144
|
+
what: 'the body profile at five levels at once — platforms are compared against it',
|
|
145
|
+
level: 0.5,
|
|
146
|
+
bands: () => [
|
|
147
|
+
{ heightDp: 36, layer: { kind: 'flat', level: 0.1 } },
|
|
148
|
+
{ heightDp: 36, layer: { kind: 'flat', level: 0.3 } },
|
|
149
|
+
{ heightDp: 36, layer: { kind: 'flat', level: 0.5 } },
|
|
150
|
+
{ heightDp: 36, layer: { kind: 'flat', level: 0.69 } },
|
|
151
|
+
{ heightDp: 36, layer: { kind: 'flat', level: 0.9 } },
|
|
152
|
+
{ heightDp: 36, layer: { kind: 'step', left: 0.05, right: 0.28 } },
|
|
153
|
+
{ heightDp: 36, layer: { kind: 'bar', level: 0.92, barLevel: 0.12, thicknessDp: 6 } },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'edge',
|
|
158
|
+
what: 'tint at a hard break: one half under the glass is black, the other white',
|
|
159
|
+
level: 0.5,
|
|
160
|
+
bands: () => whole({ kind: 'step', left: 0.03, right: 0.95 }),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'gradient',
|
|
164
|
+
what: 'ink polarity: where the automatic switch flips the ink from light to dark',
|
|
165
|
+
level: 0.5,
|
|
166
|
+
bands: () => whole({ kind: 'gradient', from: 0.04, to: 0.96, steps: 28 }),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'grid',
|
|
170
|
+
what: 'lens distortion: drift, tearing and jitter of a straight line show up immediately (by eye, not by number)',
|
|
171
|
+
level: 0.91,
|
|
172
|
+
measurable: false,
|
|
173
|
+
bands: (level) => whole({ kind: 'grid', level, lineLevel: level > 0.5 ? level - 0.14 : level + 0.14, stepDp: 12 }),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
export type VireGlassRefSceneName = (typeof REFERENCE_SCENES)[number]['name'];
|
|
178
|
+
|
|
179
|
+
export const referenceScene = (name: string): VireGlassRefScene => {
|
|
180
|
+
const scene = REFERENCE_SCENES.find((s) => s.name === name);
|
|
181
|
+
if (!scene) throw new Error(`vireglass: no reference canvas "${name}"`);
|
|
182
|
+
return scene;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Lightness to hex gray. One conversion for both platforms — otherwise they'd drift apart. */
|
|
186
|
+
export const refGray = (level: number): string => {
|
|
187
|
+
const v = Math.round(Math.min(Math.max(level, 0), 1) * 255)
|
|
188
|
+
.toString(16)
|
|
189
|
+
.padStart(2, '0');
|
|
190
|
+
return `#${v}${v}${v}`;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Checkerboard cells as POSITIONS, not as drawing: the canvas loops over them, the native views
|
|
195
|
+
* lay them out as a list, and both platforms get the exact same arrangement. The generator is
|
|
196
|
+
* its own and deliberately simple — `Math.random` would give two platforms different canvases.
|
|
197
|
+
*/
|
|
198
|
+
export const refCheckerCells = (
|
|
199
|
+
layer: Extract<VireGlassRefLayer, { kind: 'checker' }>,
|
|
200
|
+
widthDp: number,
|
|
201
|
+
heightDp: number,
|
|
202
|
+
): readonly { readonly xDp: number; readonly yDp: number; readonly level: number }[] => {
|
|
203
|
+
const cells: { xDp: number; yDp: number; level: number }[] = [];
|
|
204
|
+
const lo = layer.level - layer.amp;
|
|
205
|
+
const hi = layer.level + layer.amp;
|
|
206
|
+
let seed = 1;
|
|
207
|
+
for (let y = 0; y < heightDp; y += layer.cellDp) {
|
|
208
|
+
for (let x = 0; x < widthDp; x += layer.cellDp) {
|
|
209
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
210
|
+
cells.push({ xDp: x, yDp: y, level: (seed >> 16) % 2 ? hi : lo });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return cells;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Gradient steps — also positions: the same list for the canvas and for the native views. */
|
|
217
|
+
export const refGradientSteps = (
|
|
218
|
+
layer: Extract<VireGlassRefLayer, { kind: 'gradient' }>,
|
|
219
|
+
): readonly number[] =>
|
|
220
|
+
Array.from({ length: layer.steps }, (_, i) =>
|
|
221
|
+
layer.from + ((layer.to - layer.from) * i) / Math.max(layer.steps - 1, 1),
|
|
222
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Scroll edge effect (reference 219 @8:52) — the SCREEN's job, not the material's: it sits over
|
|
2
|
+
// the content beneath the panel, and the glass sees an already-dimmed backdrop. Only the general
|
|
3
|
+
// rules live here.
|
|
4
|
+
|
|
5
|
+
export type ScrollEdgeStyle = 'dissolve' | 'dim' | 'hard';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The edge style follows the style of the nearest glass: light ink means dark glass, and the
|
|
9
|
+
* content beneath it fades into a light dimming; dark ink means light glass, and the content
|
|
10
|
+
* dissolves into the background. A pinned view under the panel (column headers) gets a flat
|
|
11
|
+
* band with no gradient.
|
|
12
|
+
*/
|
|
13
|
+
export function scrollEdgeStyle(inkLight: boolean, pinned = false): ScrollEdgeStyle {
|
|
14
|
+
if (pinned) return 'hard';
|
|
15
|
+
return inkLight ? 'dim' : 'dissolve';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** How far the content has to slide under the panel for the effect to fully engage, dp. */
|
|
19
|
+
export const SCROLL_EDGE_ENGAGE_DP = 24;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Edge strength as a function of scroll. With no scroll, content near the top doesn't slide
|
|
23
|
+
* under the panel yet — so there's no effect (a mail screen at the top: the header sits on a
|
|
24
|
+
* clean background). At the bottom, content slides under it for as long as there's room left
|
|
25
|
+
* to scroll.
|
|
26
|
+
*/
|
|
27
|
+
export function scrollEdgeStrength(side: 'top' | 'bottom', scroll: number, maxScroll: number): number {
|
|
28
|
+
const room = side === 'top' ? scroll : maxScroll - scroll;
|
|
29
|
+
return Math.min(Math.max(room / SCROLL_EDGE_ENGAGE_DP, 0), 1);
|
|
30
|
+
}
|
package/src/sdf.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The single description of VireGlass geometry. Both shaders draw it from here: the SKSL surface
|
|
2
|
+
// (`surface-shader.ts`) and the AGSL lens (`lens-shader.ts`, shipped as a string into the native
|
|
3
|
+
// view). AGSL and SKSL are the same language, so the SDF no longer needs duplicating in Kotlin:
|
|
4
|
+
// before, the surface's circle and the lens's rounded rectangle described different pieces of
|
|
5
|
+
// glass, and the bevel constant had to be kept in sync by hand (docs/adr-001-rendering.md §3).
|
|
6
|
+
export const VG_SDF = `
|
|
7
|
+
float vgRoundRect(float2 p, float2 halfSize, float corner) {
|
|
8
|
+
float2 q = abs(p) - halfSize + corner;
|
|
9
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, float2(0.0))) - corner;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
float vgSmin(float a, float b, float k) {
|
|
13
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
14
|
+
return mix(b, a, h) - k * h * (1.0 - h);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A scene is a single shape, and when k > 0 a smooth union with one or two neighbors. A third
|
|
18
|
+
// shape exists to break the control into pieces (docs/reference.md §5): there are two bridges
|
|
19
|
+
// there. It's turned off with a zero size, not its own k: the fused body shares one bridge.
|
|
20
|
+
float vgScene(float2 p, float2 halfSize, float corner,
|
|
21
|
+
float2 offsetB, float2 halfB, float cornerB, float k,
|
|
22
|
+
float2 offsetC, float2 halfC, float cornerC) {
|
|
23
|
+
float a = vgRoundRect(p, halfSize, corner);
|
|
24
|
+
if (k <= 0.0) { return a; }
|
|
25
|
+
float ab = vgSmin(a, vgRoundRect(p - offsetB, halfB, cornerB), k);
|
|
26
|
+
if (halfC.x <= 0.0) { return ab; }
|
|
27
|
+
return vgSmin(ab, vgRoundRect(p - offsetC, halfC, cornerC), k);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Response to a finger. It's the FIELD around the touch point that deforms, not the shape's
|
|
31
|
+
// bounding box: scaling width to pull the right edge would also pull the left one — liquid
|
|
32
|
+
// doesn't behave that way. Here the displacement falls off with distance from the finger, so the
|
|
33
|
+
// far edge stays put.
|
|
34
|
+
//
|
|
35
|
+
// The order of the terms matters: drag shifts the field, press pulls it toward the finger, the
|
|
36
|
+
// wave rides on top of the already-shifted field — otherwise the ripple decouples from the
|
|
37
|
+
// deformation and lives a life of its own.
|
|
38
|
+
float2 vgTouchWarp(float2 p, float2 touch, float2 pull, float press, float radius,
|
|
39
|
+
float waveAmp, float wavePhase) {
|
|
40
|
+
if (radius <= 0.0) { return p; }
|
|
41
|
+
// Under the finger the whole element grows (M 3:51; HIG: interactive "expands").
|
|
42
|
+
p /= 1.0 + 0.06 * press;
|
|
43
|
+
float2 d = p - touch;
|
|
44
|
+
float r = length(d);
|
|
45
|
+
|
|
46
|
+
// The finger is a BLOB, not a point, but not a hard stamp either: a wide peak, soft edges. A
|
|
47
|
+
// flat plateau with a sharp cutoff at the edge shifts the region as a block, and a straight
|
|
48
|
+
// line under the glass breaks into a step — the deformation reads as rectangular even though
|
|
49
|
+
// the finger is round. A double smoothstep gives the same contact width without that step.
|
|
50
|
+
//
|
|
51
|
+
// The influence cuts off at the radius rather than fading into an infinite tail like a
|
|
52
|
+
// Gaussian: with a tail the deformation spreads into a "rubber mattress" bulge.
|
|
53
|
+
float k = clamp((radius - r) / radius, 0.0, 1.0);
|
|
54
|
+
float s = k * k * (3.0 - 2.0 * k);
|
|
55
|
+
float core = s * s * (3.0 - 2.0 * s);
|
|
56
|
+
// A ring around the blob: material displaced from under the finger has to end up somewhere.
|
|
57
|
+
// Without this ridge the shape just balloons, and a dense medium doesn't behave that way.
|
|
58
|
+
float rim = k * k * (1.0 - k) * 4.0;
|
|
59
|
+
|
|
60
|
+
float2 q = p - pull * (core - 0.42 * rim);
|
|
61
|
+
q -= d * (press * 0.16 * core);
|
|
62
|
+
|
|
63
|
+
// The wave has its own scale, twice the drag radius: the ripple has to reach the far edge,
|
|
64
|
+
// otherwise it reads as jitter under the finger rather than a wave across the surface. The
|
|
65
|
+
// wavelength is short: in a dense medium ripples are frequent and small, long shallow swells
|
|
66
|
+
// are water.
|
|
67
|
+
if (waveAmp > 0.0 && r > 0.0001) {
|
|
68
|
+
float span = radius * 2.0;
|
|
69
|
+
float ring = sin(r / (span * 0.17) - wavePhase * 6.2831853) * exp(-r / (span * 0.7));
|
|
70
|
+
q -= (d / r) * ring * waveAmp;
|
|
71
|
+
}
|
|
72
|
+
return q;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Analytical normal of a single shape: radial on the rounded part, axis-aligned on the straight
|
|
76
|
+
// segments.
|
|
77
|
+
float2 vgRoundRectNormal(float2 p, float2 halfSize, float corner) {
|
|
78
|
+
float2 q = abs(p) - halfSize + corner;
|
|
79
|
+
float2 g = (q.x > 0.0 && q.y > 0.0)
|
|
80
|
+
? normalize(max(q, float2(0.0001)))
|
|
81
|
+
: (q.x > q.y ? float2(1.0, 0.0) : float2(0.0, 1.0));
|
|
82
|
+
return g * sign(p);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Normal of the union — ANALYTICAL, no more finite differences here.
|
|
86
|
+
//
|
|
87
|
+
// Derivation: smin has two terms, mix(b,a,h) and −k·h·(1−h). Their derivatives with respect to h
|
|
88
|
+
// contain a factor of (1−2h) with opposite signs and cancel exactly, because h is linear in
|
|
89
|
+
// (b−a)/k. What's left is mix(∇b, ∇a, h) — that is, the two shapes' normals, blended with the
|
|
90
|
+
// same weight that blends the distances themselves.
|
|
91
|
+
//
|
|
92
|
+
// The previous version took four ADDITIONAL scene evaluations per pixel (each one — two shapes
|
|
93
|
+
// plus smin) and was still an approximation. Here there are two shapes, exactly.
|
|
94
|
+
float2 vgSceneNormal(float2 p, float2 halfSize, float corner,
|
|
95
|
+
float2 offsetB, float2 halfB, float cornerB, float k,
|
|
96
|
+
float2 offsetC, float2 halfC, float cornerC) {
|
|
97
|
+
float2 na = vgRoundRectNormal(p, halfSize, corner);
|
|
98
|
+
if (k <= 0.0) { return na; }
|
|
99
|
+
float2 q = p - offsetB;
|
|
100
|
+
float a = vgRoundRect(p, halfSize, corner);
|
|
101
|
+
float b = vgRoundRect(q, halfB, cornerB);
|
|
102
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
103
|
+
float2 nab = normalize(mix(vgRoundRectNormal(q, halfB, cornerB), na, h) + float2(1e-5, 1e-5));
|
|
104
|
+
if (halfC.x <= 0.0) { return nab; }
|
|
105
|
+
// Same derivation as for the pair: normals blend with the same weight as the distances.
|
|
106
|
+
float2 qc = p - offsetC;
|
|
107
|
+
float ab = vgSmin(a, b, k);
|
|
108
|
+
float c = vgRoundRect(qc, halfC, cornerC);
|
|
109
|
+
float hc = clamp(0.5 + 0.5 * (c - ab) / k, 0.0, 1.0);
|
|
110
|
+
return normalize(mix(vgRoundRectNormal(qc, halfC, cornerC), nab, hc) + float2(1e-5, 1e-5));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Position within the bevel: 0 is the flat middle, 1 is the very edge. This one value feeds the
|
|
114
|
+
// mask, thickness, refraction, aberrations and the width of the light rim.
|
|
115
|
+
float vgBevelT(float sd, float bevel) {
|
|
116
|
+
return clamp((sd + bevel) / bevel, 0.0, 1.0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Slope of the bevel profile — spherical: t / sqrt(1 - t²), like a spherical cap. The previous t²
|
|
120
|
+
// kept the slope near zero through almost the whole bevel and then shot up right at the edge, so
|
|
121
|
+
// all the optics bunched into a narrow ring and the element read as a PUCK — a flat top and a
|
|
122
|
+
// wall around the rim. Here curvature is spread across the bevel, and the rim stops being a line.
|
|
123
|
+
//
|
|
124
|
+
// This still doesn't control the field of view: only the bevel bends, the middle stays flat,
|
|
125
|
+
// otherwise the surface reads as a soap bubble.
|
|
126
|
+
float vgBevelSlope(float t) {
|
|
127
|
+
return min(t * inversesqrt(max(1.0 - t * t * 0.94, 0.02)), 3.2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// PROGRESS FRACTION. Progress is an active state turned into a FIELD: to the left of the boundary
|
|
131
|
+
// the element is active, to the right it stays regular glass, and the boundary moves. A negative
|
|
132
|
+
// value turns the field off entirely — zero is taken by the start of the track and can't also
|
|
133
|
+
// mean off.
|
|
134
|
+
//
|
|
135
|
+
// The boundary is SOFT. A hard seam reads as two different materials glued together rather than
|
|
136
|
+
// two states of one; the transition width is a fraction of the half-width, so it doesn't read as
|
|
137
|
+
// a thread on a large element and doesn't swallow a small one whole.
|
|
138
|
+
float vgProgress(float2 p, float2 halfSize, float progress) {
|
|
139
|
+
if (progress < 0.0) { return 0.0; }
|
|
140
|
+
float edge = mix(-halfSize.x, halfSize.x, clamp(progress, 0.0, 1.0));
|
|
141
|
+
float soft = max(halfSize.x * 0.05, 1.0);
|
|
142
|
+
return 1.0 - smoothstep(edge - soft, edge + soft, p.x);
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
/** Rate at which the offset builds toward the rim. Shared between the lens (actual sampling
|
|
147
|
+
* offset) and the surface (visualizing the displacement field in debug mode) — otherwise they'd
|
|
148
|
+
* drift apart. */
|
|
149
|
+
export const VG_FALLOFF = 2.6;
|