vireglass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +216 -0
  4. package/android/build.gradle +15 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/expo/modules/glasslens/GlassBackdropView.kt +335 -0
  7. package/android/src/main/java/expo/modules/glasslens/GlassLensModule.kt +49 -0
  8. package/android/src/main/java/expo/modules/glasslens/GlassLensView.kt +463 -0
  9. package/android/src/main/java/expo/modules/glasslens/GlassProbeView.kt +76 -0
  10. package/dist/adaptation-BSbbdExK.d.cts +78 -0
  11. package/dist/adaptation-BSbbdExK.d.ts +78 -0
  12. package/dist/adapters-BPB0D5nr.d.cts +357 -0
  13. package/dist/adapters-BgSc-smX.d.ts +357 -0
  14. package/dist/chunk-HF2AWZEZ.js +1741 -0
  15. package/dist/chunk-HF2AWZEZ.js.map +1 -0
  16. package/dist/chunk-JNRZNMJJ.js +20 -0
  17. package/dist/chunk-JNRZNMJJ.js.map +1 -0
  18. package/dist/index.cjs +2159 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +294 -0
  21. package/dist/index.d.ts +294 -0
  22. package/dist/index.js +286 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/material-CkUHUBoy.d.cts +408 -0
  25. package/dist/material-CkUHUBoy.d.ts +408 -0
  26. package/dist/react.cjs +73 -0
  27. package/dist/react.cjs.map +1 -0
  28. package/dist/react.d.cts +25 -0
  29. package/dist/react.d.ts +25 -0
  30. package/dist/react.js +59 -0
  31. package/dist/react.js.map +1 -0
  32. package/dist/web/index.cjs +1990 -0
  33. package/dist/web/index.cjs.map +1 -0
  34. package/dist/web/index.d.cts +148 -0
  35. package/dist/web/index.d.ts +148 -0
  36. package/dist/web/index.js +657 -0
  37. package/dist/web/index.js.map +1 -0
  38. package/expo-module.config.json +6 -0
  39. package/package.json +127 -0
  40. package/src/accessibility.ts +51 -0
  41. package/src/adaptation.ts +85 -0
  42. package/src/adapters.ts +362 -0
  43. package/src/concentric.ts +25 -0
  44. package/src/geometry.ts +151 -0
  45. package/src/glass-scale.ts +37 -0
  46. package/src/group-model.ts +224 -0
  47. package/src/index.ts +17 -0
  48. package/src/lens-shader.ts +547 -0
  49. package/src/material.ts +553 -0
  50. package/src/native/environment.ts +70 -0
  51. package/src/native/glass-group.tsx +107 -0
  52. package/src/native/glass-ink.tsx +44 -0
  53. package/src/native/glass-surface.tsx +477 -0
  54. package/src/native/index.ts +18 -0
  55. package/src/native/lens.ts +106 -0
  56. package/src/native/provider.tsx +58 -0
  57. package/src/native/surface-registry.ts +51 -0
  58. package/src/native/system-accessibility.ts +90 -0
  59. package/src/optics.ts +177 -0
  60. package/src/react.ts +102 -0
  61. package/src/reference-scene.ts +222 -0
  62. package/src/scroll-edge.ts +30 -0
  63. package/src/sdf.ts +149 -0
  64. package/src/surface-shader.ts +287 -0
  65. package/src/targets/agsl.ts +8 -0
  66. package/src/targets/glsl.ts +140 -0
  67. package/src/touch-response.ts +195 -0
  68. package/src/web/gl.ts +119 -0
  69. package/src/web/index.ts +3 -0
  70. package/src/web/probe.ts +312 -0
  71. package/src/web/reference-draw.ts +142 -0
  72. package/src/web/renderer.ts +524 -0
@@ -0,0 +1,224 @@
1
+ import { CONFIRMATIONS, FLIP_LUMA, RETURN_LUMA, type BackdropSample } from './adaptation';
2
+
3
+ /**
4
+ * GLASS GROUP ESTIMATE — the pure part of the surface group (`glass-group.tsx`).
5
+ *
6
+ * Everything that decides lives here: the lightness plane from members' samples, the group's
7
+ * aggregates, the polarity requirement and its hysteresis. None of these rules need React or the
8
+ * native layer, so they live apart from the component and are checked by a unit test.
9
+ */
10
+
11
+ export type GroupMember = { x: number; y: number; sample: BackdropSample; legibility: number };
12
+
13
+ /** Group estimate: center and average lightness, slope along x, the lightest spot, and the
14
+ * measurement values shared by everyone — [variegation, lo, hi, r, g, b]. */
15
+ export type GroupPlane = {
16
+ mx: number;
17
+ ml: number;
18
+ slope: number;
19
+ base: number;
20
+ rest: number[];
21
+ };
22
+
23
+ /** Input to the polarity decision: the lightness the group judges by, and the strictest of the requirements. */
24
+ export type GroupDecision = { decisive: number; legibility: number };
25
+
26
+ /** Fraction of a new sample in the smoothed estimate. Samples come in from every member, so the
27
+ * group collects roughly twenty a second — a quarter is enough for a response within ~200 ms. */
28
+ export const SMOOTH = 0.25;
29
+
30
+ /** The remainder below which smoothing snaps the value to the sample. An exponential never
31
+ * actually reaches its target, and the estimate kept "changing" by denormals for two more
32
+ * minutes after the backdrop had settled. */
33
+ export const SETTLE = 1e-6;
34
+
35
+ /**
36
+ * Fraction of the slope that reaches the members.
37
+ *
38
+ * A plane through four points nearly interpolates them: right at a black/white border, buttons
39
+ * were getting 0.0, 0.3, 0.7 and 1.0 — that is, exactly their own individual values, and the
40
+ * group fell apart into independent elements again. Density is set by the group's LIGHTEST spot;
41
+ * the slope only lets it off slightly at the dark end.
42
+ */
43
+ export const GRADIENT = 0.35;
44
+
45
+ const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
46
+
47
+ /**
48
+ * Lightness plane from members' samples. The slope is a least-squares fit along x: the group's
49
+ * members stand in a row, and one axis is enough — there's no vertical spread among them. The
50
+ * coordinates are centered, so the normal equations decouple.
51
+ */
52
+ function fitLuma(members: readonly GroupMember[]): {
53
+ mx: number;
54
+ ml: number;
55
+ slope: number;
56
+ } {
57
+ const n = members.length;
58
+ let mx = 0;
59
+ let ml = 0;
60
+ for (const m of members) {
61
+ mx += m.x;
62
+ ml += m.sample.luma;
63
+ }
64
+ mx /= n;
65
+ ml /= n;
66
+ let sxx = 0;
67
+ let sxl = 0;
68
+ for (const m of members) {
69
+ const dx = m.x - mx;
70
+ sxx += dx * dx;
71
+ sxl += dx * m.sample.luma;
72
+ }
73
+ return { mx, ml, slope: sxx > 1e-6 ? sxl / sxx : 0 };
74
+ }
75
+
76
+ /** Group estimate from its members. An empty group has nothing to estimate — that's `null`. */
77
+ export function aggregate(
78
+ members: readonly GroupMember[],
79
+ ): { plane: GroupPlane; decision: GroupDecision } | null {
80
+ if (members.length === 0) return null;
81
+
82
+ const { mx, ml, slope } = fitLuma(members);
83
+ let busy = 0;
84
+ let lo = 1;
85
+ let hi = 0;
86
+ // The group's lightest SPOT: density for everyone is computed from it. Otherwise, right at a
87
+ // black/white border the group would hold to the average, a light ink would never darken
88
+ // against anything, and it would drown over the light half.
89
+ let base = 0;
90
+ let r = 0;
91
+ let g = 0;
92
+ let b = 0;
93
+ // The legibility requirement is the group's strictest: polarity is one for all, and going by
94
+ // the weakest requirement would leave a neighbor with pickier optics illegible.
95
+ let legibility = 0;
96
+ for (const m of members) {
97
+ busy = Math.max(busy, m.sample.busy);
98
+ lo = Math.min(lo, m.sample.lo);
99
+ hi = Math.max(hi, m.sample.hi);
100
+ base = Math.max(base, m.sample.luma);
101
+ r += m.sample.r;
102
+ g += m.sample.g;
103
+ b += m.sample.b;
104
+ legibility = Math.max(legibility, m.legibility);
105
+ }
106
+ const k = members.length;
107
+ return {
108
+ plane: { mx, ml, slope, base, rest: [busy, lo, hi, r / k, g / k, b / k] },
109
+ // The polarity decision is one per group, by its AVERAGE lightness. Deciding on its own,
110
+ // each button used to end up a different color over a busy backdrop.
111
+ decision: { decisive: ml * 0.75 + hi * 0.25, legibility },
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Smoothing runs over samples, not frames: the native lens smooths ITS OWN estimate, and the
117
+ * group estimate overrides it with an already-finished value. Without this the group's tone
118
+ * changed in steps — the probe's 180 ms cadence is too coarse for the adaptation to read as
119
+ * unnoticeable.
120
+ */
121
+ export function smoothPlane(prev: GroupPlane | null, next: GroupPlane): GroupPlane {
122
+ if (!prev) return next;
123
+ const e = (was: number, now: number) =>
124
+ Math.abs(now - was) < SETTLE ? now : was + (now - was) * SMOOTH;
125
+ const ml = e(prev.ml, next.ml);
126
+ const slope = e(prev.slope, next.slope);
127
+ const base = e(prev.base, next.base);
128
+ const rest = next.rest.map((v, i) => e(prev.rest[i], v));
129
+ // A converged estimate is handed back as the same object: otherwise `setPlane` would repaint
130
+ // the group on every sample — twenty times a second, even on a still screen.
131
+ const same =
132
+ next.mx === prev.mx &&
133
+ ml === prev.ml &&
134
+ slope === prev.slope &&
135
+ base === prev.base &&
136
+ rest.every((v, i) => v === prev.rest[i]);
137
+ return same ? prev : { mx: next.mx, ml, slope, base, rest };
138
+ }
139
+
140
+ /** The sample a member at position `x` hands its own lens — in the order of its prop. */
141
+ export function probeValuesAt(plane: GroupPlane, x: number): number[] {
142
+ // Down from the group's lightest spot — by exactly as much as that spot is darker along the
143
+ // plane, and even then only a GRADIENT fraction of it. This lets the dark end off on density,
144
+ // without canceling it: the group stays a single material.
145
+ const here = plane.ml + plane.slope * (x - plane.mx);
146
+ const luma = clamp01(plane.base - (plane.base - here) * GRADIENT);
147
+ // The slope doesn't carry over into the lens: the group's is measured in screen dp, while the
148
+ // shader multiplies `u_probeSlope` by a normalized position within the element — different
149
+ // units.
150
+ return [luma, plane.rest[0], plane.rest[1], plane.rest[2], 0, 0, plane.rest[3], plane.rest[4], plane.rest[5]];
151
+ }
152
+
153
+ /** Whether the group needs to flip ink polarity given the current one (1 light, 0 dark). */
154
+ function wantsPolarityChange(decision: GroupDecision, polarity: number): boolean {
155
+ return polarity === 1 ? decision.decisive >= FLIP_LUMA : decision.decisive < RETURN_LUMA;
156
+ }
157
+
158
+ export type GroupSink = {
159
+ /** The group estimate has been recomputed: members take their values from it. */
160
+ plane: (plane: GroupPlane | null) => void;
161
+ /** The group flips ink polarity for everyone at once. */
162
+ flip: (polarity: number) => void;
163
+ };
164
+
165
+ export type GroupState = {
166
+ report: (
167
+ id: string,
168
+ x: number,
169
+ y: number,
170
+ sample: BackdropSample,
171
+ legibility: number,
172
+ ) => void;
173
+ release: (id: string) => void;
174
+ plane: () => GroupPlane | null;
175
+ polarity: () => number;
176
+ confirmations: () => number;
177
+ };
178
+
179
+ /** Group state: members, the smoothed estimate, and polarity hysteresis. */
180
+ export function createGroupState(sink: GroupSink): GroupState {
181
+ const members = new Map<string, GroupMember>();
182
+ let plane: GroupPlane | null = null;
183
+ let pending = 0;
184
+ let polarity = 1;
185
+
186
+ const measure = (): GroupDecision | null => {
187
+ const next = aggregate([...members.values()]);
188
+ if (!next) {
189
+ pending = 0;
190
+ plane = null;
191
+ sink.plane(null);
192
+ return null;
193
+ }
194
+ plane = smoothPlane(plane, next.plane);
195
+ sink.plane(plane);
196
+ return next.decision;
197
+ };
198
+
199
+ return {
200
+ report(id, x, y, sample, legibility) {
201
+ members.set(id, { x, y, sample, legibility });
202
+ const decision = measure();
203
+ if (!decision) return;
204
+ if (!wantsPolarityChange(decision, polarity)) {
205
+ pending = 0;
206
+ return;
207
+ }
208
+ pending += 1;
209
+ if (pending < CONFIRMATIONS) return;
210
+ pending = 0;
211
+ polarity = polarity === 1 ? 0 : 1;
212
+ sink.flip(polarity);
213
+ },
214
+ // Only recomputes the estimate: losing a member isn't a backdrop sample and can't count as a
215
+ // polarity confirmation — unmounting the group used to rack up CONFIRMATIONS instantly and
216
+ // trigger a recolor nobody was left to cancel.
217
+ release(id) {
218
+ if (members.delete(id)) measure();
219
+ },
220
+ plane: () => plane,
221
+ polarity: () => polarity,
222
+ confirmations: () => pending,
223
+ };
224
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from './material';
2
+ export * from './optics';
3
+ export * from './geometry';
4
+ export * from './sdf';
5
+ export * from './touch-response';
6
+ export * from './lens-shader';
7
+ export * from './surface-shader';
8
+ export * from './adaptation';
9
+ export * from './accessibility';
10
+ export * from './glass-scale';
11
+ export * from './scroll-edge';
12
+ export * from './concentric';
13
+ export * from './reference-scene';
14
+ export * from './group-model';
15
+ export * from './adapters';
16
+ export * from './targets/agsl';
17
+ export * from './targets/glsl';