lensfx 0.1.2 → 0.1.3

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.
@@ -0,0 +1,1694 @@
1
+ 'use strict';
2
+
3
+ // src/engine/math.ts
4
+ function erf(x) {
5
+ return Math.tanh(1.7724538509 * x);
6
+ }
7
+ function integrateDome(R, t) {
8
+ let sum = 0;
9
+ for (let n = 0; n <= 200; n++) {
10
+ const a = n / 200 * t;
11
+ const slope = a / Math.sqrt(R * R - a * a);
12
+ sum += n === 0 || n === 200 ? 0.5 * slope : slope;
13
+ }
14
+ return sum / 200;
15
+ }
16
+ function computeDomeConstants(domeDepth, halfW, halfH) {
17
+ const a = Math.max(0.01, Math.min(domeDepth, Math.min(halfW, halfH) - 1));
18
+ const Rx = (halfW * halfW + a * a) / (2 * a);
19
+ const Ry = (halfH * halfH + a * a) / (2 * a);
20
+ const intX = integrateDome(Rx, halfW);
21
+ const intY = integrateDome(Ry, halfH);
22
+ return {
23
+ Rx,
24
+ Ry,
25
+ scaleX: intX > 0 ? 0.5 / intX : 1,
26
+ scaleY: intY > 0 ? 0.5 / intY : 1
27
+ };
28
+ }
29
+ function domeGradient(pos, R, scale) {
30
+ const p = Math.min(pos, 0.999 * R);
31
+ return p / Math.sqrt(R * R - p * p) * scale;
32
+ }
33
+ function colorMatrixValues(ratioX, ratioY) {
34
+ return `${ratioX} 0 0 0 ${0.5 * (1 - ratioX)} 0 ${ratioY} 0 0 ${0.5 * (1 - ratioY)} 0 0 1 0 0 0 0 0 1 0`;
35
+ }
36
+ var maskCache = /* @__PURE__ */ new Map();
37
+ function getSvgMaskDataUri(width, height, radius) {
38
+ const n = Math.max(1, Math.round(width));
39
+ const l = Math.max(1, Math.round(height));
40
+ const i = Math.max(0, Math.min(Math.round(radius), Math.min(n, l) / 2));
41
+ const key = `${n}|${l}|${i}`;
42
+ const cached = maskCache.get(key);
43
+ if (cached) return { uri: cached, key };
44
+ const w = Math.max(0, n - 1);
45
+ const h = Math.max(0, l - 1);
46
+ const r = Math.max(0, i - 0.5);
47
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${n} ${l}' preserveAspectRatio='none'><rect x='0.5' y='0.5' width='${w}' height='${h}' rx='${r}' ry='${r}' fill='black'/></svg>`;
48
+ const uri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
49
+ maskCache.set(key, uri);
50
+ return { uri, key };
51
+ }
52
+ function createDisplacementMapGeneratorSync(mapSize) {
53
+ let canvas = null;
54
+ let ctx = null;
55
+ let imgData = null;
56
+ let domeConstants = null;
57
+ let cachedDomeDepth = -Infinity;
58
+ let cachedHalfW = -Infinity;
59
+ let cachedHalfH = -Infinity;
60
+ let xLookup = null;
61
+ let cachedF = 0;
62
+ let dirtyDome = true;
63
+ return {
64
+ /**
65
+ * Generate a new displacement map and return a PNG data URL.
66
+ * Reuses internal canvas memory; safe to call every animation frame.
67
+ */
68
+ generate(input) {
69
+ if (!canvas) {
70
+ canvas = document.createElement("canvas");
71
+ canvas.width = mapSize;
72
+ canvas.height = mapSize;
73
+ ctx = canvas.getContext("2d");
74
+ imgData = ctx.createImageData(mapSize, mapSize);
75
+ }
76
+ const {
77
+ lensHalfWidth: x,
78
+ lensHalfHeight: y,
79
+ borderRadius: b,
80
+ depth: M,
81
+ sdfBoundary: R,
82
+ edgeFalloff: S,
83
+ specularRotation: w = 45,
84
+ glowStrength: A = 0,
85
+ glowSpread: v = 1,
86
+ glowExponent: E = 1.5,
87
+ edgeStrength: $ = 0,
88
+ edgeWidth: C = 3,
89
+ edgeExponent: k = 1.5,
90
+ domeDepth: j = 0,
91
+ splayAmount: I = 1
92
+ } = input;
93
+ const T = imgData.data;
94
+ const F = mapSize >> 1;
95
+ const D = Math.min(b, Math.min(x, y));
96
+ const L = Math.max(0, x - M);
97
+ const B = Math.max(0, y - M);
98
+ const H = Math.max(0, Math.min(b, Math.min(L, B)));
99
+ const O = M > 0 ? 1 / (M * Math.SQRT2) : 1e6;
100
+ const hasSpecular = A > 0 || $ > 0;
101
+ const W = w * Math.PI / 180;
102
+ const U = Math.cos(W);
103
+ const G = Math.sin(W);
104
+ const N = (1 - v) * Math.SQRT2;
105
+ const q = v * Math.SQRT2;
106
+ const z = q > 1e-3 ? 1 / q : 0;
107
+ const Y = C > 0 ? 1 / C : 0;
108
+ const X = 2 * x / mapSize;
109
+ const P = 2 * y / mapSize;
110
+ const V = 1 / x;
111
+ const K = 1 / y;
112
+ const hasDome = j > 0;
113
+ const hasSplay = I < 1;
114
+ if (hasDome) {
115
+ if (!domeConstants || Math.abs(j - cachedDomeDepth) > 0.5 || Math.abs(x - cachedHalfW) > 1 || Math.abs(y - cachedHalfH) > 1) {
116
+ domeConstants = computeDomeConstants(j, x, y);
117
+ cachedDomeDepth = j;
118
+ cachedHalfW = x;
119
+ cachedHalfH = y;
120
+ dirtyDome = true;
121
+ }
122
+ if (cachedF !== F) {
123
+ xLookup = new Float32Array(F);
124
+ cachedF = F;
125
+ dirtyDome = true;
126
+ }
127
+ if (dirtyDome && xLookup && domeConstants) {
128
+ const r2 = domeConstants.Rx * domeConstants.Rx;
129
+ const clampVal = 0.999 * domeConstants.Rx;
130
+ for (let col = 0; col < F; col++) {
131
+ const ox = -((col + 0.5) * X - x);
132
+ const clamped = ox < clampVal ? ox : clampVal;
133
+ xLookup[col] = clamped / Math.sqrt(r2 - clamped * clamped) * domeConstants.scaleX;
134
+ }
135
+ dirtyDome = false;
136
+ }
137
+ }
138
+ const domeX = hasDome ? xLookup : null;
139
+ const minHalf = 0.5 * Math.min(x, y);
140
+ const invMinHalf = minHalf > 0 ? 1 / minHalf : 0;
141
+ const startTime = performance.now();
142
+ for (let t = 0; t < F; t++) {
143
+ const rowOpposite = mapSize - 1 - t;
144
+ const oy = -((t + 0.5) * P - y);
145
+ const iy = oy - y + D;
146
+ const oSdfY = S ? oy - B + H : 0;
147
+ const gy = hasDome && domeConstants ? domeGradient(oy, domeConstants.Ry, domeConstants.scaleY) : oy * K > 1 ? 1 : oy * K;
148
+ const rawGy = oy * K > 1 ? 1 : oy * K;
149
+ const splayY = hasSplay ? Math.max(0, 1 - (y - oy) * invMinHalf) : 0;
150
+ for (let n = 0; n < F; n++) {
151
+ const colOpposite = mapSize - 1 - n;
152
+ const ox = -((n + 0.5) * X - x);
153
+ const ix = ox - x + D;
154
+ const gxMax = ix > 0 ? ix : 0;
155
+ const gyMax = iy > 0 ? iy : 0;
156
+ const dist2 = gxMax * gxMax + gyMax * gyMax;
157
+ const sdf = (dist2 > 0 ? Math.sqrt(dist2) : 0) + (ix > iy ? ix > 0 ? 0 : ix : iy > 0 ? 0 : iy) - D;
158
+ const idxTL = (t * mapSize + n) * 4;
159
+ const idxTR = (t * mapSize + colOpposite) * 4;
160
+ const idxBL = (rowOpposite * mapSize + n) * 4;
161
+ const idxBR = (rowOpposite * mapSize + colOpposite) * 4;
162
+ if (!R || sdf < 0) {
163
+ let specTL = 128, specTR = 128, specBL = 128, specBR = 128;
164
+ let curGx = domeX ? domeX[n] : ox * V > 1 ? 1 : ox * V;
165
+ let curGy = gy;
166
+ if (hasSplay) {
167
+ const splayFac = 1 - I;
168
+ const effY = splayY * splayFac;
169
+ const effX = Math.max(0, 1 - (x - ox) * invMinHalf) * splayFac;
170
+ if (effY > 1e-3 || effX > 1e-3) {
171
+ const prevGx = curGx;
172
+ const prevGy = curGy;
173
+ curGx = prevGx * (1 - effY);
174
+ curGy = prevGy * (1 - effX);
175
+ const prevMag = Math.sqrt(prevGx * prevGx + prevGy * prevGy);
176
+ const newMag = Math.sqrt(curGx * curGx + curGy * curGy);
177
+ if (newMag > 1e-3) {
178
+ const norm = prevMag / newMag;
179
+ curGx *= norm;
180
+ curGy *= norm;
181
+ }
182
+ }
183
+ }
184
+ let falloff = 1;
185
+ if (S) {
186
+ const fx = ox - L + H;
187
+ const fy = oSdfY;
188
+ const mx = fx > 0 ? fx : 0;
189
+ const my = fy > 0 ? fy : 0;
190
+ const falloffSdf = Math.sqrt(mx * mx + my * my) + (fx > fy ? fx > 0 ? 0 : fx : fy > 0 ? 0 : fy) - H;
191
+ falloff = 0.5 * (1 + erf(falloffSdf * O));
192
+ }
193
+ const hx = 0.5 * curGx * falloff;
194
+ const hy = 0.5 * curGy * falloff;
195
+ const rTL = (0.5 + hx) * 255 + 0.5 | 0;
196
+ const rTR = (0.5 - hx) * 255 + 0.5 | 0;
197
+ const gTL = (0.5 + hy) * 255 + 0.5 | 0;
198
+ const gBL = (0.5 - hy) * 255 + 0.5 | 0;
199
+ if (hasSpecular) {
200
+ const rawGx = (ox * V > 1 ? 1 : ox * V) * U;
201
+ const rawGyProj = rawGy * G;
202
+ const dot1 = rawGx + rawGyProj;
203
+ const dot2 = rawGx - rawGyProj;
204
+ const absDot1 = dot1 < 0 ? -dot1 : dot1;
205
+ const absDot2 = dot2 < 0 ? -dot2 : dot2;
206
+ let edgeAtten = 0;
207
+ if ($ > 0) {
208
+ edgeAtten = sdf < 0 ? 1 + sdf * Y : 0;
209
+ if (edgeAtten < 0) edgeAtten = 0;
210
+ }
211
+ let glow1 = 0, glow2 = 0;
212
+ if (A > 0) {
213
+ const s1 = (absDot1 - N) * z;
214
+ glow1 += A * Math.pow(s1 < 0 ? 0 : s1 > 1 ? 1 : s1, E) * falloff;
215
+ const s2 = (absDot2 - N) * z;
216
+ glow2 += A * Math.pow(s2 < 0 ? 0 : s2 > 1 ? 1 : s2, E) * falloff;
217
+ }
218
+ if ($ > 0) {
219
+ glow1 += $ * edgeAtten * Math.pow(absDot1, k);
220
+ glow2 += $ * edgeAtten * Math.pow(absDot2, k);
221
+ }
222
+ if (glow1 > 1) glow1 = 1;
223
+ if (glow2 > 1) glow2 = 1;
224
+ specTL = 127 * glow1 + 128 + 0.5 | 0;
225
+ specBR = 127 * glow1 + 128 + 0.5 | 0;
226
+ specTR = 127 * glow2 + 128 + 0.5 | 0;
227
+ specBL = 127 * glow2 + 128 + 0.5 | 0;
228
+ }
229
+ T[idxTL] = rTL;
230
+ T[idxTL + 1] = gTL;
231
+ T[idxTL + 2] = specTL;
232
+ T[idxTL + 3] = 255;
233
+ T[idxTR] = rTR;
234
+ T[idxTR + 1] = gTL;
235
+ T[idxTR + 2] = specTR;
236
+ T[idxTR + 3] = 255;
237
+ T[idxBL] = rTL;
238
+ T[idxBL + 1] = gBL;
239
+ T[idxBL + 2] = specBL;
240
+ T[idxBL + 3] = 255;
241
+ T[idxBR] = rTR;
242
+ T[idxBR + 1] = gBL;
243
+ T[idxBR + 2] = specBR;
244
+ T[idxBR + 3] = 255;
245
+ } else {
246
+ for (const idx of [idxTL, idxTR, idxBL, idxBR]) {
247
+ T[idx] = 128;
248
+ T[idx + 1] = 128;
249
+ T[idx + 2] = 128;
250
+ T[idx + 3] = 255;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ const loopMs = performance.now() - startTime;
256
+ ctx.putImageData(imgData, 0, 0);
257
+ const encodeStart = performance.now();
258
+ const dataUrl = canvas.toDataURL();
259
+ const encodeMs = performance.now() - encodeStart;
260
+ return { dataUrl, loopMs, encodeMs };
261
+ },
262
+ /** Release canvas memory. Call in React cleanup (useEffect return). */
263
+ dispose() {
264
+ if (canvas) {
265
+ canvas.width = 0;
266
+ canvas.height = 0;
267
+ canvas = null;
268
+ }
269
+ ctx = null;
270
+ imgData = null;
271
+ domeConstants = null;
272
+ cachedDomeDepth = -Infinity;
273
+ cachedHalfW = -Infinity;
274
+ cachedHalfH = -Infinity;
275
+ dirtyDome = true;
276
+ }
277
+ };
278
+ }
279
+ async function generateDisplacementMapAsync(canvas, params) {
280
+ const {
281
+ canvasSize: r,
282
+ lensHalfWidth: n,
283
+ lensHalfHeight: l,
284
+ borderRadius: i,
285
+ depth: o,
286
+ sdfBoundary: u,
287
+ edgeFalloff: c,
288
+ specularRotation: f = 45,
289
+ glowStrength: d = 0,
290
+ glowSpread: g = 1,
291
+ glowExponent: h = 1.5,
292
+ edgeStrength: p = 0,
293
+ edgeWidth: m = 3,
294
+ edgeExponent: xx = 1.5,
295
+ domeDepth: y = 0,
296
+ splayAmount: bv = 1
297
+ } = params;
298
+ canvas.width = r;
299
+ canvas.height = r;
300
+ const ctx = canvas.getContext("2d");
301
+ if (!ctx) return null;
302
+ const imgD = ctx.createImageData(r, r);
303
+ const S = Math.min(i, Math.min(n, l));
304
+ const w = Math.max(0, n - o);
305
+ const A = Math.max(0, l - o);
306
+ const v = Math.max(0, Math.min(i, Math.min(w, A)));
307
+ const E = o > 0 ? 1 / (o * Math.SQRT2) : 1e6;
308
+ const hasSpecular = d > 0 || p > 0;
309
+ const C = f * Math.PI / 180;
310
+ const k = Math.cos(C);
311
+ const j = Math.sin(C);
312
+ const II = (1 - g) * Math.SQRT2;
313
+ const TT = g * Math.SQRT2;
314
+ const F = y > 0 ? computeDomeConstants(y, n, l) : null;
315
+ const D = bv < 1;
316
+ const Lv = 0.5 * Math.min(n, l);
317
+ const Bv = Lv > 0 ? 1 / Lv : 0;
318
+ for (let rowBatch = 0; rowBatch < r; rowBatch += 64) {
319
+ const endRow = Math.min(rowBatch + 64, r);
320
+ for (let row = rowBatch; row < endRow; row++) {
321
+ for (let col = 0; col < r; col++) {
322
+ const idx = (row * r + col) * 4;
323
+ const ox = (col + 0.5) / r * (2 * n) - n;
324
+ const oy = (row + 0.5) / r * (2 * l) - l;
325
+ const absX = Math.abs(ox);
326
+ const absY = Math.abs(oy);
327
+ const Mv = absX - n + S;
328
+ const Cy = absY - l + S;
329
+ const clampX = Math.max(Mv, 0);
330
+ const clampY = Math.max(Cy, 0);
331
+ const dist2 = clampX * clampX + clampY * clampY;
332
+ const sdf = (dist2 > 0 ? Math.sqrt(dist2) : 0) + Math.min(Math.max(Mv, Cy), 0) - S;
333
+ if (!u || sdf < 0) {
334
+ let gx;
335
+ let gy;
336
+ if (F) {
337
+ gx = Math.sign(ox) * domeGradient(absX, F.Rx, F.scaleX);
338
+ gy = Math.sign(oy) * domeGradient(absY, F.Ry, F.scaleY);
339
+ } else {
340
+ gx = Math.max(-1, Math.min(1, ox / n));
341
+ gy = Math.max(-1, Math.min(1, oy / l));
342
+ }
343
+ let effGx = gx;
344
+ let effGy = gy;
345
+ if (D) {
346
+ const attenY = Math.max(0, 1 - (l - absY) * Bv) * (1 - bv);
347
+ const attenX = Math.max(0, 1 - (n - absX) * Bv) * (1 - bv);
348
+ if (attenY > 1e-3 || attenX > 1e-3) {
349
+ const oldGx = effGx;
350
+ const oldGy = effGy;
351
+ effGx = oldGx * (1 - attenY);
352
+ effGy = oldGy * (1 - attenX);
353
+ const oldMag = Math.sqrt(oldGx * oldGx + oldGy * oldGy);
354
+ const newMag = Math.sqrt(effGx * effGx + effGy * effGy);
355
+ if (newMag > 1e-3) {
356
+ const norm = oldMag / newMag;
357
+ effGx *= norm;
358
+ effGy *= norm;
359
+ }
360
+ }
361
+ }
362
+ let falloff = 1;
363
+ if (c) {
364
+ const innerX = absX - w + v;
365
+ const innerY = absY - A + v;
366
+ const innerSdf = Math.sqrt(Math.max(innerX, 0) ** 2 + Math.max(innerY, 0) ** 2) + Math.min(Math.max(innerX, innerY), 0) - v;
367
+ falloff = 0.5 * (1 + erf(innerSdf * E));
368
+ }
369
+ imgD.data[idx] = Math.round((0.5 - 0.5 * effGx * falloff) * 255);
370
+ imgD.data[idx + 1] = Math.round((0.5 - 0.5 * effGy * falloff) * 255);
371
+ if (hasSpecular) {
372
+ const dot = Math.abs(
373
+ Math.max(-1, Math.min(1, ox / n)) * k + Math.max(-1, Math.min(1, oy / l)) * j
374
+ );
375
+ let glow = 0;
376
+ if (d > 0) {
377
+ const spread = TT > 1e-3 ? Math.min(1, Math.max(0, dot - II) / TT) : 0;
378
+ glow += d * Math.pow(spread, h) * falloff;
379
+ }
380
+ if (p > 0) {
381
+ const edgeVal = sdf < 0 ? Math.max(0, 1 + sdf / m) : 0;
382
+ glow += p * edgeVal * Math.pow(dot, xx);
383
+ }
384
+ glow = Math.min(1, glow);
385
+ imgD.data[idx + 2] = Math.round(127 * glow + 128);
386
+ } else {
387
+ imgD.data[idx + 2] = 128;
388
+ }
389
+ } else {
390
+ imgD.data[idx] = 128;
391
+ imgD.data[idx + 1] = 128;
392
+ imgD.data[idx + 2] = 128;
393
+ }
394
+ imgD.data[idx + 3] = 255;
395
+ }
396
+ }
397
+ if (rowBatch + 64 < r) {
398
+ await new Promise((resolve) => setTimeout(resolve, 0));
399
+ }
400
+ }
401
+ ctx.putImageData(imgD, 0, 0);
402
+ const blob = await new Promise(
403
+ (resolve) => canvas.toBlob(resolve, "image/png")
404
+ );
405
+ return blob ? URL.createObjectURL(blob) : null;
406
+ }
407
+
408
+ // src/createLens.ts
409
+ var SVG_NS = "http://www.w3.org/2000/svg";
410
+ var nextId = 0;
411
+ function createLens(target, options = {}) {
412
+ if (typeof document === "undefined") {
413
+ throw new Error("[lensfx] createLens can only be called in a browser environment.");
414
+ }
415
+ const resolved = typeof target === "string" ? document.querySelector(target) : target;
416
+ if (!resolved) {
417
+ throw new Error(`[lensfx] Target element "${target}" was not found.`);
418
+ }
419
+ const container = resolved;
420
+ let currentOptions = { ...options };
421
+ let targetEl = container;
422
+ if (currentOptions.refractionTarget) {
423
+ const customTarget = typeof currentOptions.refractionTarget === "string" ? container.querySelector(currentOptions.refractionTarget) || document.querySelector(currentOptions.refractionTarget) : currentOptions.refractionTarget;
424
+ if (customTarget) {
425
+ targetEl = customTarget;
426
+ }
427
+ }
428
+ const computedStyle = window.getComputedStyle(container);
429
+ const originalPosition = container.style.position;
430
+ if (computedStyle.position === "static") {
431
+ container.style.position = "relative";
432
+ }
433
+ const lensId = `lensfx-${++nextId}-${Math.random().toString(36).substring(2, 7)}`;
434
+ const filterId = `${lensId}-filter`;
435
+ const svgEl = document.createElementNS(SVG_NS, "svg");
436
+ svgEl.setAttribute("class", "lensfx-svg-root");
437
+ svgEl.setAttribute("aria-hidden", "true");
438
+ svgEl.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:0;top:0;";
439
+ const defsEl = document.createElementNS(SVG_NS, "defs");
440
+ const filterEl = document.createElementNS(SVG_NS, "filter");
441
+ filterEl.id = filterId;
442
+ filterEl.setAttribute("filterUnits", "objectBoundingBox");
443
+ filterEl.setAttribute("primitiveUnits", "objectBoundingBox");
444
+ filterEl.setAttribute("color-interpolation-filters", "sRGB");
445
+ filterEl.setAttribute("x", "0");
446
+ filterEl.setAttribute("y", "0");
447
+ filterEl.setAttribute("width", "1");
448
+ filterEl.setAttribute("height", "1");
449
+ defsEl.appendChild(filterEl);
450
+ svgEl.appendChild(defsEl);
451
+ container.appendChild(svgEl);
452
+ const overlayEl = document.createElement("div");
453
+ overlayEl.className = "lensfx-lens-overlay";
454
+ overlayEl.style.cssText = "position:absolute;top:0;left:0;pointer-events:none;box-sizing:border-box;will-change:transform;";
455
+ container.appendChild(overlayEl);
456
+ let mapDataUrl = currentOptions.displacementMapUrl || "";
457
+ let currentMapSize = currentOptions.mapSize || 256;
458
+ let generator = createDisplacementMapGeneratorSync(currentMapSize);
459
+ let feImageEl = null;
460
+ let currentMaskKey = null;
461
+ let isDestroyed = false;
462
+ let curX = currentOptions.x ?? 0.5;
463
+ let curY = currentOptions.y ?? 0.5;
464
+ function getDimensions() {
465
+ const width = currentOptions.width ?? currentOptions.lensW ?? 160;
466
+ const height = currentOptions.height ?? currentOptions.lensH ?? 100;
467
+ const radius = currentOptions.radius ?? currentOptions.borderRadius ?? Math.min(width, height) / 4;
468
+ return { width, height, radius };
469
+ }
470
+ function renderMap() {
471
+ if (isDestroyed) return;
472
+ if (currentOptions.displacementMapUrl) {
473
+ mapDataUrl = currentOptions.displacementMapUrl;
474
+ if (feImageEl) feImageEl.setAttribute("href", mapDataUrl);
475
+ currentOptions.onMapChange?.(mapDataUrl);
476
+ return;
477
+ }
478
+ const { width, height, radius } = getDimensions();
479
+ const mapSize = currentOptions.mapSize || 256;
480
+ if (mapSize !== currentMapSize) {
481
+ generator.dispose();
482
+ currentMapSize = mapSize;
483
+ generator = createDisplacementMapGeneratorSync(currentMapSize);
484
+ }
485
+ const depth = currentOptions.depth ?? 2;
486
+ const result = generator.generate({
487
+ lensHalfWidth: width / 2,
488
+ lensHalfHeight: height / 2,
489
+ borderRadius: radius,
490
+ depth,
491
+ sdfBoundary: currentOptions.sdfBoundary ?? true,
492
+ edgeFalloff: currentOptions.edgeFalloff ?? true,
493
+ specularRotation: currentOptions.specularRotation ?? 45,
494
+ glowStrength: currentOptions.glowStrength ?? 0.3,
495
+ glowSpread: currentOptions.glowSpread ?? 1,
496
+ glowExponent: currentOptions.glowExponent ?? 1.5,
497
+ edgeStrength: currentOptions.edgeStrength ?? 0.4,
498
+ edgeWidth: currentOptions.edgeWidth ?? 3,
499
+ edgeExponent: 1.5,
500
+ domeDepth: currentOptions.domeDepth ?? 6,
501
+ splayAmount: currentOptions.splayAmount ?? 0.8
502
+ });
503
+ mapDataUrl = result.dataUrl;
504
+ if (feImageEl) feImageEl.setAttribute("href", mapDataUrl);
505
+ currentOptions.onMapChange?.(mapDataUrl);
506
+ }
507
+ function buildFilterPipeline() {
508
+ while (filterEl.firstChild) {
509
+ filterEl.removeChild(filterEl.firstChild);
510
+ }
511
+ currentOptions.depth ?? 2;
512
+ const chroma = currentOptions.chroma ?? currentOptions.chromaAmount ?? 0.6;
513
+ const scaleX = currentOptions.scaleX ?? 0.25;
514
+ const scaleY = currentOptions.scaleY ?? 0.25;
515
+ const maxScale = Math.max(scaleX, scaleY);
516
+ const scaleRatioX = maxScale > 0 ? scaleX / maxScale : 0;
517
+ const scaleRatioY = maxScale > 0 ? scaleY / maxScale : 0;
518
+ const needsMatrix = scaleRatioX !== 1 || scaleRatioY !== 1;
519
+ const matrixString = colorMatrixValues(scaleRatioX, scaleRatioY);
520
+ const feFlood = document.createElementNS(SVG_NS, "feFlood");
521
+ feFlood.setAttribute("flood-color", "rgb(128,128,128)");
522
+ feFlood.setAttribute("flood-opacity", "1");
523
+ feFlood.setAttribute("result", "mapBg");
524
+ filterEl.appendChild(feFlood);
525
+ feImageEl = document.createElementNS(SVG_NS, "feImage");
526
+ feImageEl.setAttribute("href", mapDataUrl);
527
+ feImageEl.setAttribute("preserveAspectRatio", "none");
528
+ feImageEl.setAttribute("result", "rawMap");
529
+ filterEl.appendChild(feImageEl);
530
+ const feComp = document.createElementNS(SVG_NS, "feComposite");
531
+ feComp.setAttribute("in", "rawMap");
532
+ feComp.setAttribute("in2", "mapBg");
533
+ feComp.setAttribute("operator", "over");
534
+ feComp.setAttribute("result", "map");
535
+ filterEl.appendChild(feComp);
536
+ if (needsMatrix) {
537
+ const feMat = document.createElementNS(SVG_NS, "feColorMatrix");
538
+ feMat.setAttribute("in", "map");
539
+ feMat.setAttribute("type", "matrix");
540
+ feMat.setAttribute("values", matrixString);
541
+ feMat.setAttribute("result", "scaledMap");
542
+ filterEl.appendChild(feMat);
543
+ }
544
+ const mapSource = needsMatrix ? "scaledMap" : "map";
545
+ let sourceGraphic = "SourceGraphic";
546
+ if (currentOptions.blur && currentOptions.blur > 0) {
547
+ const feBlur = document.createElementNS(SVG_NS, "feGaussianBlur");
548
+ feBlur.setAttribute("in", "SourceGraphic");
549
+ const cRect = container.getBoundingClientRect();
550
+ const bw = cRect.width > 0 ? currentOptions.blur / cRect.width : 0.01;
551
+ const bh = cRect.height > 0 ? currentOptions.blur / cRect.height : 0.01;
552
+ feBlur.setAttribute("stdDeviation", `${bw} ${bh}`);
553
+ feBlur.setAttribute("result", "blurred");
554
+ filterEl.appendChild(feBlur);
555
+ sourceGraphic = "blurred";
556
+ }
557
+ if (chroma > 0) {
558
+ const dispR = document.createElementNS(SVG_NS, "feDisplacementMap");
559
+ dispR.setAttribute("in", sourceGraphic);
560
+ dispR.setAttribute("in2", mapSource);
561
+ dispR.setAttribute("scale", String(maxScale * (1 + 0.2 * chroma)));
562
+ dispR.setAttribute("xChannelSelector", "R");
563
+ dispR.setAttribute("yChannelSelector", "G");
564
+ filterEl.appendChild(dispR);
565
+ const matR = document.createElementNS(SVG_NS, "feColorMatrix");
566
+ matR.setAttribute("type", "matrix");
567
+ matR.setAttribute("values", "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0");
568
+ matR.setAttribute("result", "dispR");
569
+ filterEl.appendChild(matR);
570
+ const dispG = document.createElementNS(SVG_NS, "feDisplacementMap");
571
+ dispG.setAttribute("in", sourceGraphic);
572
+ dispG.setAttribute("in2", mapSource);
573
+ dispG.setAttribute("scale", String(maxScale * (1 + 0.1 * chroma)));
574
+ dispG.setAttribute("xChannelSelector", "R");
575
+ dispG.setAttribute("yChannelSelector", "G");
576
+ filterEl.appendChild(dispG);
577
+ const matG = document.createElementNS(SVG_NS, "feColorMatrix");
578
+ matG.setAttribute("type", "matrix");
579
+ matG.setAttribute("values", "0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0");
580
+ matG.setAttribute("result", "dispG");
581
+ filterEl.appendChild(matG);
582
+ const dispB = document.createElementNS(SVG_NS, "feDisplacementMap");
583
+ dispB.setAttribute("in", sourceGraphic);
584
+ dispB.setAttribute("in2", mapSource);
585
+ dispB.setAttribute("scale", String(maxScale));
586
+ dispB.setAttribute("xChannelSelector", "R");
587
+ dispB.setAttribute("yChannelSelector", "G");
588
+ filterEl.appendChild(dispB);
589
+ const matB = document.createElementNS(SVG_NS, "feColorMatrix");
590
+ matB.setAttribute("type", "matrix");
591
+ matB.setAttribute("values", "0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0");
592
+ matB.setAttribute("result", "dispB");
593
+ filterEl.appendChild(matB);
594
+ const compRG = document.createElementNS(SVG_NS, "feComposite");
595
+ compRG.setAttribute("in", "dispR");
596
+ compRG.setAttribute("in2", "dispG");
597
+ compRG.setAttribute("operator", "arithmetic");
598
+ compRG.setAttribute("k1", "0");
599
+ compRG.setAttribute("k2", "1");
600
+ compRG.setAttribute("k3", "1");
601
+ compRG.setAttribute("k4", "0");
602
+ filterEl.appendChild(compRG);
603
+ const compRGB = document.createElementNS(SVG_NS, "feComposite");
604
+ compRGB.setAttribute("in2", "dispB");
605
+ compRGB.setAttribute("operator", "arithmetic");
606
+ compRGB.setAttribute("k1", "0");
607
+ compRGB.setAttribute("k2", "1");
608
+ compRGB.setAttribute("k3", "1");
609
+ compRGB.setAttribute("k4", "0");
610
+ compRGB.setAttribute("result", "lensResult");
611
+ filterEl.appendChild(compRGB);
612
+ } else {
613
+ const dispSingle = document.createElementNS(SVG_NS, "feDisplacementMap");
614
+ dispSingle.setAttribute("in", sourceGraphic);
615
+ dispSingle.setAttribute("in2", mapSource);
616
+ dispSingle.setAttribute("scale", String(maxScale));
617
+ dispSingle.setAttribute("xChannelSelector", "R");
618
+ dispSingle.setAttribute("yChannelSelector", "G");
619
+ dispSingle.setAttribute("result", "lensResult");
620
+ filterEl.appendChild(dispSingle);
621
+ }
622
+ const specStrength = currentOptions.specularStrength ?? 1;
623
+ if (specStrength > 0) {
624
+ const specMat = document.createElementNS(SVG_NS, "feColorMatrix");
625
+ specMat.setAttribute("in", "map");
626
+ specMat.setAttribute("type", "matrix");
627
+ specMat.setAttribute("values", "0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 1 0 -0.50196");
628
+ specMat.setAttribute("result", "specMask");
629
+ filterEl.appendChild(specMat);
630
+ const specComp = document.createElementNS(SVG_NS, "feComposite");
631
+ specComp.setAttribute("in", "specMask");
632
+ specComp.setAttribute("in2", "lensResult");
633
+ specComp.setAttribute("operator", "arithmetic");
634
+ specComp.setAttribute("k1", "0");
635
+ specComp.setAttribute("k2", String(specStrength));
636
+ specComp.setAttribute("k3", "1");
637
+ specComp.setAttribute("k4", "0");
638
+ specComp.setAttribute("result", "lensResult");
639
+ filterEl.appendChild(specComp);
640
+ }
641
+ const lensMask = document.createElementNS(SVG_NS, "feFlood");
642
+ lensMask.setAttribute("flood-color", "black");
643
+ lensMask.setAttribute("flood-opacity", "1");
644
+ lensMask.setAttribute("result", "lensMask");
645
+ filterEl.appendChild(lensMask);
646
+ const holeComp = document.createElementNS(SVG_NS, "feComposite");
647
+ holeComp.setAttribute("in", "SourceGraphic");
648
+ holeComp.setAttribute("in2", "lensMask");
649
+ holeComp.setAttribute("operator", "out");
650
+ holeComp.setAttribute("result", "holedSG");
651
+ filterEl.appendChild(holeComp);
652
+ const finalComp = document.createElementNS(SVG_NS, "feComposite");
653
+ finalComp.setAttribute("in", "lensResult");
654
+ finalComp.setAttribute("in2", "holedSG");
655
+ finalComp.setAttribute("operator", "over");
656
+ filterEl.appendChild(finalComp);
657
+ }
658
+ function updateLayout() {
659
+ if (isDestroyed) return;
660
+ const rect = container.getBoundingClientRect();
661
+ const cw = rect.width || container.clientWidth || 300;
662
+ const ch = rect.height || container.clientHeight || 200;
663
+ const { width, height, radius } = getDimensions();
664
+ const px = curX <= 1 && curX >= 0 ? curX * cw : curX;
665
+ const py = curY <= 1 && curY >= 0 ? curY * ch : pyCoordinate(curY, ch);
666
+ function pyCoordinate(val, containerH) {
667
+ return val <= 1 && val >= 0 ? val * containerH : val;
668
+ }
669
+ const lensLeft = px - width / 2;
670
+ const lensTop = py - height / 2;
671
+ if (feImageEl && cw > 0 && ch > 0) {
672
+ feImageEl.setAttribute("x", String(lensLeft / cw));
673
+ feImageEl.setAttribute("y", String(lensTop / ch));
674
+ feImageEl.setAttribute("width", String(width / cw));
675
+ feImageEl.setAttribute("height", String(height / ch));
676
+ }
677
+ overlayEl.style.width = `${width}px`;
678
+ overlayEl.style.height = `${height}px`;
679
+ overlayEl.style.borderRadius = `${radius}px`;
680
+ overlayEl.style.transform = `translate3d(${lensLeft}px, ${lensTop}px, 0)`;
681
+ if (currentOptions.shadow === false) {
682
+ overlayEl.style.boxShadow = "none";
683
+ } else if (typeof currentOptions.shadow === "string") {
684
+ overlayEl.style.boxShadow = currentOptions.shadow;
685
+ } else {
686
+ overlayEl.style.boxShadow = "0 8px 32px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 1px rgba(255, 255, 255, 0.35), inset 0 -1px 1px rgba(0, 0, 0, 0.15)";
687
+ }
688
+ if (currentOptions.border === false) {
689
+ overlayEl.style.border = "none";
690
+ } else if (typeof currentOptions.border === "string") {
691
+ overlayEl.style.border = currentOptions.border;
692
+ } else {
693
+ overlayEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
694
+ }
695
+ const tintColor = currentOptions.tintColor ?? "rgba(255, 255, 255, 0.05)";
696
+ overlayEl.style.background = tintColor;
697
+ const tintBlur = currentOptions.tintBlur ?? 0;
698
+ if (tintBlur > 0) {
699
+ const blurCss = `blur(${tintBlur}px)`;
700
+ overlayEl.style.backdropFilter = blurCss;
701
+ overlayEl.style.setProperty("-webkit-backdrop-filter", blurCss);
702
+ } else {
703
+ overlayEl.style.backdropFilter = "none";
704
+ overlayEl.style.setProperty("-webkit-backdrop-filter", "none");
705
+ }
706
+ const { uri, key } = getSvgMaskDataUri(width, height, radius);
707
+ if (currentMaskKey !== key) {
708
+ const maskCss = `url("${uri}")`;
709
+ overlayEl.style.maskImage = maskCss;
710
+ overlayEl.style.setProperty("-webkit-mask-image", maskCss);
711
+ overlayEl.style.maskSize = "100% 100%";
712
+ overlayEl.style.setProperty("-webkit-mask-size", "100% 100%");
713
+ currentMaskKey = key;
714
+ }
715
+ const filterStyle = `url("#${filterId}")`;
716
+ if (targetEl.style.filter !== filterStyle) {
717
+ targetEl.style.filter = filterStyle;
718
+ }
719
+ }
720
+ let isPointerActive = false;
721
+ const onPointerMove = (e) => {
722
+ if (!currentOptions.interactive || isDestroyed) return;
723
+ const rect = container.getBoundingClientRect();
724
+ const x = e.clientX - rect.left;
725
+ const y = e.clientY - rect.top;
726
+ setPosition(x, y);
727
+ };
728
+ function setInteractive(interactive) {
729
+ currentOptions.interactive = interactive;
730
+ if (interactive && !isPointerActive) {
731
+ container.addEventListener("pointermove", onPointerMove);
732
+ isPointerActive = true;
733
+ } else if (!interactive && isPointerActive) {
734
+ container.removeEventListener("pointermove", onPointerMove);
735
+ isPointerActive = false;
736
+ }
737
+ }
738
+ function setPosition(x, y) {
739
+ curX = x;
740
+ curY = y;
741
+ currentOptions.onPositionChange?.(x, y);
742
+ updateLayout();
743
+ }
744
+ function update(newOptions) {
745
+ if (isDestroyed) return;
746
+ ({ ...currentOptions });
747
+ currentOptions = { ...currentOptions, ...newOptions };
748
+ if (newOptions.x !== void 0) curX = newOptions.x;
749
+ if (newOptions.y !== void 0) curY = newOptions.y;
750
+ if (newOptions.interactive !== void 0) {
751
+ setInteractive(newOptions.interactive);
752
+ }
753
+ const needsRegen = newOptions.width !== void 0 || newOptions.lensW !== void 0 || newOptions.height !== void 0 || newOptions.lensH !== void 0 || newOptions.radius !== void 0 || newOptions.borderRadius !== void 0 || newOptions.depth !== void 0 || newOptions.domeDepth !== void 0 || newOptions.specularRotation !== void 0 || newOptions.glowStrength !== void 0 || newOptions.glowSpread !== void 0 || newOptions.edgeStrength !== void 0 || newOptions.edgeWidth !== void 0 || newOptions.splayAmount !== void 0 || newOptions.displacementMapUrl !== void 0 || newOptions.mapSize !== void 0;
754
+ if (needsRegen) {
755
+ renderMap();
756
+ }
757
+ const needsPipelineRebuild = newOptions.chroma !== void 0 || newOptions.chromaAmount !== void 0 || newOptions.scaleX !== void 0 || newOptions.scaleY !== void 0 || newOptions.specularStrength !== void 0 || newOptions.blur !== void 0;
758
+ if (needsPipelineRebuild || needsRegen) {
759
+ buildFilterPipeline();
760
+ }
761
+ updateLayout();
762
+ }
763
+ let resizeObserver = null;
764
+ if (typeof ResizeObserver !== "undefined") {
765
+ resizeObserver = new ResizeObserver(() => {
766
+ updateLayout();
767
+ });
768
+ resizeObserver.observe(container);
769
+ }
770
+ renderMap();
771
+ buildFilterPipeline();
772
+ updateLayout();
773
+ if (currentOptions.interactive) {
774
+ setInteractive(true);
775
+ }
776
+ function destroy() {
777
+ if (isDestroyed) return;
778
+ isDestroyed = true;
779
+ if (isPointerActive) {
780
+ container.removeEventListener("pointermove", onPointerMove);
781
+ isPointerActive = false;
782
+ }
783
+ if (resizeObserver) {
784
+ resizeObserver.disconnect();
785
+ resizeObserver = null;
786
+ }
787
+ if (targetEl.style.filter.includes(filterId)) {
788
+ targetEl.style.filter = "";
789
+ }
790
+ if (container.style.position === "relative" && originalPosition === "") {
791
+ container.style.position = "";
792
+ }
793
+ if (svgEl.parentNode) {
794
+ svgEl.parentNode.removeChild(svgEl);
795
+ }
796
+ if (overlayEl.parentNode) {
797
+ overlayEl.parentNode.removeChild(overlayEl);
798
+ }
799
+ generator.dispose();
800
+ }
801
+ return {
802
+ el: container,
803
+ targetEl,
804
+ svgEl,
805
+ filterEl,
806
+ overlayEl,
807
+ get options() {
808
+ return currentOptions;
809
+ },
810
+ get mapDataUrl() {
811
+ return mapDataUrl;
812
+ },
813
+ setPosition,
814
+ update,
815
+ setInteractive,
816
+ destroy
817
+ };
818
+ }
819
+
820
+ // src/engine/types.ts
821
+ var DEFAULT_LENS_PARAMS = {
822
+ lensW: 90,
823
+ lensH: 60,
824
+ depth: 0,
825
+ chromaAmount: 0,
826
+ scaleX: 0,
827
+ scaleY: 0,
828
+ mapSize: 256,
829
+ borderRadius: 0,
830
+ blurAmount: 0,
831
+ sdfBoundary: false,
832
+ edgeFalloff: false,
833
+ brightness: 0,
834
+ specularStrength: 0,
835
+ specularRotation: 0,
836
+ glowStrength: 0,
837
+ glowSpread: 1,
838
+ glowExponent: 1.5,
839
+ tint: 0,
840
+ edgeStrength: 0,
841
+ edgeWidth: 3,
842
+ edgeExponent: 1.5,
843
+ specularDark: false,
844
+ domeDepth: 0,
845
+ splayAmount: 0
846
+ };
847
+ var SWITCH_GLASS_DEFAULTS = {
848
+ lensW: 90,
849
+ lensH: 60,
850
+ borderRadius: 30,
851
+ mapSize: 256,
852
+ depth: 2,
853
+ chromaAmount: 1,
854
+ scaleX: 0.25,
855
+ scaleY: 0.25,
856
+ sdfBoundary: true,
857
+ edgeFalloff: true,
858
+ domeDepth: 6,
859
+ splayAmount: 0.4,
860
+ blurAmount: 0,
861
+ brightness: 0.06,
862
+ specularStrength: 1,
863
+ specularRotation: 45,
864
+ specularDark: false,
865
+ tint: 0,
866
+ glowStrength: 0,
867
+ glowSpread: 0.5,
868
+ glowExponent: 1.5,
869
+ edgeStrength: 0,
870
+ edgeWidth: 2,
871
+ edgeExponent: 1.5,
872
+ edgeShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
873
+ edgeInsetShadow: "0 -4px 10px rgba(0, 0, 0, 0.12)"
874
+ };
875
+ var SWITCH_DARK_OVERRIDES = {
876
+ brightness: 0.12,
877
+ glowStrength: 0.4,
878
+ edgeStrength: 0.5
879
+ };
880
+ var SWITCH_LIGHT_OVERRIDES = {
881
+ brightness: -0.02,
882
+ specularRotation: 30,
883
+ specularStrength: 1.5,
884
+ glowStrength: 0.4,
885
+ glowSpread: 0.5,
886
+ glowExponent: 2,
887
+ edgeStrength: 1,
888
+ edgeWidth: 1.5,
889
+ edgeExponent: 1
890
+ };
891
+ var SLIDER_GLASS_DEFAULTS = {
892
+ lensW: 90,
893
+ lensH: 60,
894
+ borderRadius: 30,
895
+ mapSize: 256,
896
+ depth: 2,
897
+ chromaAmount: 0.65,
898
+ scaleX: 0.06,
899
+ scaleY: 0.06,
900
+ sdfBoundary: true,
901
+ edgeFalloff: true,
902
+ domeDepth: 5,
903
+ splayAmount: 0.5,
904
+ blurAmount: 0,
905
+ brightness: 0.06,
906
+ specularStrength: 1.5,
907
+ specularRotation: 45,
908
+ specularDark: false,
909
+ tint: 0,
910
+ glowStrength: 0.4,
911
+ glowSpread: 0.5,
912
+ glowExponent: 1.5,
913
+ edgeStrength: 0,
914
+ edgeWidth: 3,
915
+ edgeExponent: 1.5,
916
+ edgeShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
917
+ edgeInsetShadow: "0 -4px 10px rgba(0, 0, 0, 0.12)",
918
+ restEdgeShadow: "0 1.333px 5.333px var(--lensfx-shadow-strong, rgba(0,0,0,0.3))"
919
+ };
920
+ var SLIDER_DARK_OVERRIDES = {
921
+ scaleX: 0.133,
922
+ scaleY: 0.135,
923
+ brightness: 0.12,
924
+ specularRotation: 45,
925
+ glowExponent: 1.5,
926
+ edgeStrength: 0.5,
927
+ edgeWidth: 1,
928
+ edgeExponent: 1.5
929
+ };
930
+ var SLIDER_LIGHT_OVERRIDES = {
931
+ scaleX: 0.1,
932
+ scaleY: 0.1,
933
+ brightness: -0.02,
934
+ specularRotation: 30,
935
+ glowExponent: 2,
936
+ edgeStrength: 1,
937
+ edgeWidth: 1,
938
+ edgeExponent: 1
939
+ };
940
+ var SLIDER_SAFARI_OVERRIDES = {
941
+ scaleY: 0.25
942
+ };
943
+
944
+ // src/webgl/VideoRefractionRenderer.ts
945
+ var VERTEX_SHADER = `
946
+ attribute vec2 a_pos;
947
+ varying vec2 v_uv;
948
+ void main() {
949
+ // Flip Y so UV (0,0) = top-left, matching HTML/CSS coordinates.
950
+ v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);
951
+ gl_Position = vec4(a_pos, 0.0, 1.0);
952
+ }
953
+ `;
954
+ var BLUR_FRAGMENT_SHADER = `
955
+ precision highp float;
956
+ uniform sampler2D u_source;
957
+ uniform vec2 u_dir;
958
+ varying vec2 v_uv;
959
+
960
+ void main() {
961
+ vec4 c = texture2D(u_source, v_uv) * 0.2042;
962
+ c += (texture2D(u_source, v_uv + 1.0 * u_dir) + texture2D(u_source, v_uv - 1.0 * u_dir)) * 0.1801;
963
+ c += (texture2D(u_source, v_uv + 2.0 * u_dir) + texture2D(u_source, v_uv - 2.0 * u_dir)) * 0.1240;
964
+ c += (texture2D(u_source, v_uv + 3.0 * u_dir) + texture2D(u_source, v_uv - 3.0 * u_dir)) * 0.0663;
965
+ c += (texture2D(u_source, v_uv + 4.0 * u_dir) + texture2D(u_source, v_uv - 4.0 * u_dir)) * 0.0276;
966
+ gl_FragColor = c;
967
+ }
968
+ `;
969
+ var REFRACTION_FRAGMENT_SHADER = `
970
+ #extension GL_OES_standard_derivatives : enable
971
+ precision highp float;
972
+
973
+ uniform sampler2D u_video;
974
+ uniform sampler2D u_map;
975
+ uniform sampler2D u_blurred;
976
+ uniform float u_baseScale[3];
977
+ uniform float u_ratioX[3];
978
+ uniform float u_ratioY[3];
979
+ uniform float u_chromaAmount;
980
+ uniform float u_specStrength;
981
+ uniform float u_adaptStrength;
982
+ uniform float u_specLumaLow;
983
+ uniform float u_specLumaHigh;
984
+ uniform float u_hasBlur;
985
+ uniform vec4 u_bbox; // (x, y, w, h) normalised, y=0 at top
986
+ uniform vec3 u_circles[3]; // (cx, cy, radius) in bbox pixel space
987
+ uniform float u_scale[3]; // per-circle geometric press scale (1 = unscaled)
988
+ uniform vec4 u_bar; // (cx, cy, w, h) in bbox pixel space; w<=0 means inactive
989
+ uniform float u_barRadius; // corner radius for the rounded-rect bar lens
990
+ uniform float u_barBaseScale;
991
+ uniform float u_barRatioX;
992
+ uniform float u_barRatioY;
993
+ uniform vec2 u_bboxSize; // bbox (width, height) in pixels
994
+
995
+ varying vec2 v_uv;
996
+
997
+ void main() {
998
+ vec2 uv = v_uv;
999
+ vec2 bMin = u_bbox.xy;
1000
+ vec2 bSize = u_bbox.zw;
1001
+ vec2 bMax = bMin + bSize;
1002
+
1003
+ // Outside bounding box -> pass-through video
1004
+ if (uv.x < bMin.x || uv.x > bMax.x || uv.y < bMin.y || uv.y > bMax.y) {
1005
+ gl_FragColor = texture2D(u_video, uv);
1006
+ return;
1007
+ }
1008
+
1009
+ // Map fragment UV -> displacement texture UV
1010
+ vec2 mUV = (uv - bMin) / bSize;
1011
+
1012
+ // Analytical SDF mask (pixel-perfect at any resolution). Tracks the
1013
+ // dominant region so per-region scale/ratio can be picked up via the
1014
+ // loop-constant index (safe in WebGL 1). After the circle loop we test the
1015
+ // rounded-rect "bar" region (when active) and take whichever has the
1016
+ // larger mask value. domCenter/domScale track the dominant lens so the
1017
+ // displacement map is sampled in its (press-)scaled space below.
1018
+ vec2 pxPos = mUV * u_bboxSize;
1019
+ float mask = 0.0;
1020
+ float baseScale = 0.0;
1021
+ float ratioX = 0.0;
1022
+ float ratioY = 0.0;
1023
+
1024
+ // Map-sample scale assignment, tracked SEPARATELY from the tight SDF mask: a
1025
+ // pixel adopts a circle's press scale across the union of its original and
1026
+ // scaled radius (R * max(1, s)). That way the annulus a shrunk lens vacates
1027
+ // samples empty space instead of leaving the original rim stuck behind.
1028
+ // domScale = 1 (bar + background) -> identity sample.
1029
+ vec2 domCenter = pxPos;
1030
+ float domScale = 1.0;
1031
+ float domDist = 1.0; // nearest normalised assignment distance (<1 = inside)
1032
+
1033
+ for (int i = 0; i < 3; i++) {
1034
+ if (u_circles[i].z < 0.1) continue; // skip inactive circles
1035
+ float s = max(u_scale[i], 0.001);
1036
+ float R = u_circles[i].z;
1037
+ float rho = length(pxPos - u_circles[i].xy);
1038
+
1039
+ // Visible lens SDF at the scaled radius -> blur / specular gating.
1040
+ float dist = rho - R * s;
1041
+ float aa = fwidth(dist);
1042
+ float m = 1.0 - smoothstep(-aa, aa, dist);
1043
+ if (m > mask) {
1044
+ mask = m;
1045
+ baseScale = u_baseScale[i];
1046
+ ratioX = u_ratioX[i];
1047
+ ratioY = u_ratioY[i];
1048
+ }
1049
+
1050
+ // Scale assignment over max(original, scaled) radius, PLUS a small FIXED
1051
+ // pixel margin so the region boundary clears the map's hard content edge at
1052
+ // r = R. A *fractional* margin scales with R, so a big lens (play) would
1053
+ // swallow its neighbours' regions and drag their rings in. 4px clears the
1054
+ // edge's AA band without ever reaching the gap to the next control.
1055
+ float nd = rho / (R * max(1.0, s) + 4.0);
1056
+ if (nd < domDist) {
1057
+ domDist = nd;
1058
+ domCenter = u_circles[i].xy;
1059
+ domScale = s;
1060
+ }
1061
+ }
1062
+
1063
+ // Bar (rounded-rect) SDF - only when u_bar.zw > 0. The bar never scales, so
1064
+ // it keeps the identity map sample (domScale stays 1 for bar + background).
1065
+ if (u_bar.z > 0.1 && u_bar.w > 0.1) {
1066
+ vec2 q = abs(pxPos - u_bar.xy) - u_bar.zw * 0.5 + vec2(u_barRadius);
1067
+ float barDist =
1068
+ length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - u_barRadius;
1069
+ float aaBar = fwidth(barDist);
1070
+ float mBar = 1.0 - smoothstep(-aaBar, aaBar, barDist);
1071
+ if (mBar > mask) {
1072
+ mask = mBar;
1073
+ baseScale = u_barBaseScale;
1074
+ ratioX = u_barRatioX;
1075
+ ratioY = u_barRatioY;
1076
+ }
1077
+ }
1078
+
1079
+ // Sample the displacement map at the dominant lens's scaled coordinate, so a
1080
+ // per-lens press scale shrinks the baked pattern around its center in
1081
+ // lockstep with the SDF radius above. domScale = 1 -> identity (unscaled).
1082
+ vec2 scaledPx = domCenter + (pxPos - domCenter) / domScale;
1083
+ vec4 d = texture2D(u_map, scaledPx / u_bboxSize);
1084
+
1085
+ // mapMatrix (feColorMatrix equivalent)
1086
+ float sr = d.r * ratioX + 0.5 * (1.0 - ratioX);
1087
+ float sg = d.g * ratioY + 0.5 * (1.0 - ratioY);
1088
+
1089
+ // Displacement offset in full-video UV space
1090
+ vec2 off = vec2(sr - 0.5, sg - 0.5) * baseScale;
1091
+
1092
+ // Choose source: blurred or raw video
1093
+ // Inside lens -> blurred (if blur active), outside lens -> raw
1094
+ // Blend by mask so the transition at edges is clean
1095
+ vec4 color;
1096
+ if (u_chromaAmount > 0.001) {
1097
+ float sR = 1.0 + u_chromaAmount * 0.2;
1098
+ float sG = 1.0 + u_chromaAmount * 0.1;
1099
+ if (u_hasBlur > 0.5) {
1100
+ color.r = mix(
1101
+ texture2D(u_video, clamp(uv + off * sR, 0.0, 1.0)).r,
1102
+ texture2D(u_blurred, clamp(uv + off * sR, 0.0, 1.0)).r,
1103
+ mask
1104
+ );
1105
+ color.g = mix(
1106
+ texture2D(u_video, clamp(uv + off * sG, 0.0, 1.0)).g,
1107
+ texture2D(u_blurred, clamp(uv + off * sG, 0.0, 1.0)).g,
1108
+ mask
1109
+ );
1110
+ color.b = mix(
1111
+ texture2D(u_video, clamp(uv + off, 0.0, 1.0)).b,
1112
+ texture2D(u_blurred, clamp(uv + off, 0.0, 1.0)).b,
1113
+ mask
1114
+ );
1115
+ } else {
1116
+ color.r = texture2D(u_video, clamp(uv + off * sR, 0.0, 1.0)).r;
1117
+ color.g = texture2D(u_video, clamp(uv + off * sG, 0.0, 1.0)).g;
1118
+ color.b = texture2D(u_video, clamp(uv + off, 0.0, 1.0)).b;
1119
+ }
1120
+ color.a = 1.0;
1121
+ } else {
1122
+ vec2 duv = clamp(uv + off, 0.0, 1.0);
1123
+ if (u_hasBlur > 0.5) {
1124
+ color = mix(texture2D(u_video, duv), texture2D(u_blurred, duv), mask);
1125
+ } else {
1126
+ color = texture2D(u_video, duv);
1127
+ }
1128
+ }
1129
+
1130
+ // Adaptive specular + brightness (Apple-style vibrancy)
1131
+ // Compute perceptual luminance of the displaced video sample
1132
+ float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
1133
+
1134
+ // Specular: blend between additive (dark bg) and multiplicative (bright bg)
1135
+ float spec = d.b - 0.502;
1136
+ float lumaLo = min(u_specLumaLow, u_specLumaHigh);
1137
+ float lumaHi = max(u_specLumaLow, u_specLumaHigh);
1138
+ float darkBlend = smoothstep(lumaLo, lumaHi, luma);
1139
+ vec3 specAdd = color.rgb + spec * u_specStrength;
1140
+ vec3 specMul = color.rgb * (1.0 - spec * u_specStrength);
1141
+ color.rgb = mix(specAdd, specMul, darkBlend);
1142
+ color.rgb = max(color.rgb, vec3(0.0));
1143
+
1144
+ // Brightness: pull toward mid-gray - darkens bright areas, brightens dark
1145
+ float correction = (0.5 - luma) * u_adaptStrength * mask;
1146
+ color.rgb += correction;
1147
+
1148
+ gl_FragColor = color;
1149
+ }
1150
+ `;
1151
+ function createShader(gl, type, src) {
1152
+ const shader = gl.createShader(type);
1153
+ if (!shader) return null;
1154
+ gl.shaderSource(shader, src);
1155
+ gl.compileShader(shader);
1156
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1157
+ console.error("Shader compile error:", gl.getShaderInfoLog(shader));
1158
+ gl.deleteShader(shader);
1159
+ return null;
1160
+ }
1161
+ return shader;
1162
+ }
1163
+ function createProgram(gl, vsSrc, fsSrc) {
1164
+ const vs = createShader(gl, gl.VERTEX_SHADER, vsSrc);
1165
+ const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSrc);
1166
+ if (!vs || !fs) return null;
1167
+ const prog = gl.createProgram();
1168
+ if (!prog) return null;
1169
+ gl.attachShader(prog, vs);
1170
+ gl.attachShader(prog, fs);
1171
+ gl.linkProgram(prog);
1172
+ gl.detachShader(prog, vs);
1173
+ gl.detachShader(prog, fs);
1174
+ gl.deleteShader(vs);
1175
+ gl.deleteShader(fs);
1176
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
1177
+ console.error("Program link error:", gl.getProgramInfoLog(prog));
1178
+ gl.deleteProgram(prog);
1179
+ return null;
1180
+ }
1181
+ return prog;
1182
+ }
1183
+ var VideoRefractionRenderer = class {
1184
+ constructor(canvas) {
1185
+ this.blurFboA = null;
1186
+ this.blurTexA = null;
1187
+ this.blurFboB = null;
1188
+ this.blurTexB = null;
1189
+ this.fboWidth = 0;
1190
+ this.fboHeight = 0;
1191
+ const gl = canvas.getContext("webgl", { alpha: false, antialias: false, premultipliedAlpha: false });
1192
+ if (!gl) throw new Error("WebGL not supported");
1193
+ this.gl = gl;
1194
+ gl.getExtension("OES_standard_derivatives");
1195
+ const quad = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
1196
+ this.quadBuffer = gl.createBuffer();
1197
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
1198
+ gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
1199
+ this.blurProgram = createProgram(gl, VERTEX_SHADER, BLUR_FRAGMENT_SHADER);
1200
+ this.refractionProgram = createProgram(gl, VERTEX_SHADER, REFRACTION_FRAGMENT_SHADER);
1201
+ this.videoTexture = gl.createTexture();
1202
+ gl.bindTexture(gl.TEXTURE_2D, this.videoTexture);
1203
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1204
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1205
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1206
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1207
+ this.mapTexture = gl.createTexture();
1208
+ gl.bindTexture(gl.TEXTURE_2D, this.mapTexture);
1209
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1210
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1211
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1212
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1213
+ }
1214
+ setDisplacementMap(image) {
1215
+ const gl = this.gl;
1216
+ gl.bindTexture(gl.TEXTURE_2D, this.mapTexture);
1217
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
1218
+ }
1219
+ initBlurFBO(width, height) {
1220
+ const gl = this.gl;
1221
+ const halfW = width >> 1;
1222
+ const halfH = height >> 1;
1223
+ if (this.fboWidth === halfW && this.fboHeight === halfH) return;
1224
+ this.fboWidth = halfW;
1225
+ this.fboHeight = halfH;
1226
+ const createFBO = () => {
1227
+ const tex = gl.createTexture();
1228
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1229
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1230
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1231
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1232
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1233
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, halfW, halfH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
1234
+ const fbo = gl.createFramebuffer();
1235
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
1236
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
1237
+ return { fbo, tex };
1238
+ };
1239
+ const resA = createFBO();
1240
+ this.blurFboA = resA.fbo;
1241
+ this.blurTexA = resA.tex;
1242
+ const resB = createFBO();
1243
+ this.blurFboB = resB.fbo;
1244
+ this.blurTexB = resB.tex;
1245
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1246
+ }
1247
+ render(video, config) {
1248
+ const gl = this.gl;
1249
+ const width = gl.canvas.width;
1250
+ const height = gl.canvas.height;
1251
+ gl.activeTexture(gl.TEXTURE0);
1252
+ gl.bindTexture(gl.TEXTURE_2D, this.videoTexture);
1253
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
1254
+ if (config.hasBlur) {
1255
+ this.initBlurFBO(width, height);
1256
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFboA);
1257
+ gl.viewport(0, 0, this.fboWidth, this.fboHeight);
1258
+ gl.useProgram(this.blurProgram);
1259
+ const aPosBlur = gl.getAttribLocation(this.blurProgram, "a_pos");
1260
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
1261
+ gl.enableVertexAttribArray(aPosBlur);
1262
+ gl.vertexAttribPointer(aPosBlur, 2, gl.FLOAT, false, 0, 0);
1263
+ gl.uniform1i(gl.getUniformLocation(this.blurProgram, "u_source"), 0);
1264
+ gl.uniform2f(gl.getUniformLocation(this.blurProgram, "u_dir"), 1 / this.fboWidth, 0);
1265
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1266
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFboB);
1267
+ gl.activeTexture(gl.TEXTURE2);
1268
+ gl.bindTexture(gl.TEXTURE_2D, this.blurTexA);
1269
+ gl.uniform1i(gl.getUniformLocation(this.blurProgram, "u_source"), 2);
1270
+ gl.uniform2f(gl.getUniformLocation(this.blurProgram, "u_dir"), 0, 1 / this.fboHeight);
1271
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1272
+ }
1273
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1274
+ gl.viewport(0, 0, width, height);
1275
+ gl.useProgram(this.refractionProgram);
1276
+ const aPos = gl.getAttribLocation(this.refractionProgram, "a_pos");
1277
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
1278
+ gl.enableVertexAttribArray(aPos);
1279
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
1280
+ gl.uniform1i(gl.getUniformLocation(this.refractionProgram, "u_video"), 0);
1281
+ gl.activeTexture(gl.TEXTURE1);
1282
+ gl.bindTexture(gl.TEXTURE_2D, this.mapTexture);
1283
+ gl.uniform1i(gl.getUniformLocation(this.refractionProgram, "u_map"), 1);
1284
+ if (config.hasBlur && this.blurTexB) {
1285
+ gl.activeTexture(gl.TEXTURE2);
1286
+ gl.bindTexture(gl.TEXTURE_2D, this.blurTexB);
1287
+ gl.uniform1i(gl.getUniformLocation(this.refractionProgram, "u_blurred"), 2);
1288
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_hasBlur"), 1);
1289
+ } else {
1290
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_hasBlur"), 0);
1291
+ }
1292
+ const circlesArr = new Float32Array(9);
1293
+ const scaleArr = new Float32Array(3);
1294
+ const baseScaleArr = new Float32Array(3);
1295
+ const ratioXArr = new Float32Array(3);
1296
+ const ratioYArr = new Float32Array(3);
1297
+ for (let i = 0; i < 3; i++) {
1298
+ if (i < config.circles.length) {
1299
+ const c = config.circles[i];
1300
+ circlesArr[i * 3 + 0] = c.cx;
1301
+ circlesArr[i * 3 + 1] = c.cy;
1302
+ circlesArr[i * 3 + 2] = c.radius;
1303
+ scaleArr[i] = c.scale;
1304
+ baseScaleArr[i] = c.baseScale;
1305
+ ratioXArr[i] = c.ratioX;
1306
+ ratioYArr[i] = c.ratioY;
1307
+ }
1308
+ }
1309
+ gl.uniform3fv(gl.getUniformLocation(this.refractionProgram, "u_circles"), circlesArr);
1310
+ gl.uniform1fv(gl.getUniformLocation(this.refractionProgram, "u_scale"), scaleArr);
1311
+ gl.uniform1fv(gl.getUniformLocation(this.refractionProgram, "u_baseScale"), baseScaleArr);
1312
+ gl.uniform1fv(gl.getUniformLocation(this.refractionProgram, "u_ratioX"), ratioXArr);
1313
+ gl.uniform1fv(gl.getUniformLocation(this.refractionProgram, "u_ratioY"), ratioYArr);
1314
+ if (config.bar) {
1315
+ gl.uniform4f(
1316
+ gl.getUniformLocation(this.refractionProgram, "u_bar"),
1317
+ config.bar.cx,
1318
+ config.bar.cy,
1319
+ config.bar.w,
1320
+ config.bar.h
1321
+ );
1322
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_barRadius"), config.bar.radius);
1323
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_barBaseScale"), config.bar.baseScale);
1324
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_barRatioX"), config.bar.ratioX);
1325
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_barRatioY"), config.bar.ratioY);
1326
+ } else {
1327
+ gl.uniform4f(gl.getUniformLocation(this.refractionProgram, "u_bar"), 0, 0, -1, -1);
1328
+ }
1329
+ gl.uniform4f(
1330
+ gl.getUniformLocation(this.refractionProgram, "u_bbox"),
1331
+ config.bbox.x,
1332
+ config.bbox.y,
1333
+ config.bbox.w,
1334
+ config.bbox.h
1335
+ );
1336
+ gl.uniform2f(
1337
+ gl.getUniformLocation(this.refractionProgram, "u_bboxSize"),
1338
+ config.bboxSize.w,
1339
+ config.bboxSize.h
1340
+ );
1341
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_chromaAmount"), config.chromaAmount);
1342
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_specStrength"), config.specStrength);
1343
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_adaptStrength"), config.adaptStrength);
1344
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_specLumaLow"), config.specLumaLow);
1345
+ gl.uniform1f(gl.getUniformLocation(this.refractionProgram, "u_specLumaHigh"), config.specLumaHigh);
1346
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1347
+ }
1348
+ dispose() {
1349
+ const gl = this.gl;
1350
+ gl.deleteBuffer(this.quadBuffer);
1351
+ gl.deleteProgram(this.blurProgram);
1352
+ gl.deleteProgram(this.refractionProgram);
1353
+ gl.deleteTexture(this.videoTexture);
1354
+ gl.deleteTexture(this.mapTexture);
1355
+ if (this.blurFboA) gl.deleteFramebuffer(this.blurFboA);
1356
+ if (this.blurTexA) gl.deleteTexture(this.blurTexA);
1357
+ if (this.blurFboB) gl.deleteFramebuffer(this.blurFboB);
1358
+ if (this.blurTexB) gl.deleteTexture(this.blurTexB);
1359
+ }
1360
+ };
1361
+
1362
+ // src/webgl/QRRefractionRenderer.ts
1363
+ var QR_VERTEX_SHADER = `#version 300 es
1364
+ in vec2 a_position;
1365
+ out vec2 v_uv;
1366
+
1367
+ void main() {
1368
+ v_uv = (a_position * 0.5) + 0.5;
1369
+ gl_Position = vec4(a_position, 0.0, 1.0);
1370
+ }
1371
+ `;
1372
+ var QR_FRAGMENT_SHADER = `#version 300 es
1373
+ precision highp float;
1374
+ precision highp sampler2D;
1375
+
1376
+ in vec2 v_uv;
1377
+ out vec4 fragColor;
1378
+
1379
+ uniform float u_dotRadius;
1380
+ uniform vec3 u_backgroundColor;
1381
+ uniform sampler2D u_PaintingTexture;
1382
+
1383
+ // QR dot grid for the O(1) occupancy lookup (replaces a per-pixel scan over
1384
+ // every dot). Occupancy is matrixLength x matrixLength, R8, 0/1 per module.
1385
+ uniform sampler2D u_occupancyTexture;
1386
+ uniform float u_gridOriginUV; // padding / size, in UV
1387
+ uniform float u_cellUV; // cellSize / size, in UV
1388
+ uniform float u_invCellUV; // 1 / cellUV - avoids a per-pixel divide
1389
+ uniform int u_matrixLength;
1390
+
1391
+ uniform sampler2D u_displacementMap;
1392
+ uniform sampler2D u_paintingColorTexture;
1393
+ uniform int u_displacementActive;
1394
+ uniform vec2 u_lensOrigin;
1395
+ uniform vec2 u_lensSize;
1396
+ uniform vec2 u_displacementScale;
1397
+ uniform float u_chromaAmount;
1398
+
1399
+ // Eye uniforms (3 corner finder groups)
1400
+ uniform vec2 u_eyeCenter[3];
1401
+ uniform vec3 u_eyeHalf[3]; // x=outer, y=mid, z=inner half-size
1402
+ uniform vec3 u_eyeRadius[3]; // x=outer, y=mid, z=inner corner radius
1403
+ uniform vec3 u_eyeColor[3]; // fill color per group (animated)
1404
+ uniform float u_eyeScale[3]; // hover scale per group
1405
+ uniform float u_eyeRefractionScale; // 0-1 multiplier for eye displacement
1406
+
1407
+ // QR dots sit on a uniform grid, and the radius (~0.35 of a cell) never
1408
+ // spills past a cell boundary, so the nearest dot is always the one in pos's
1409
+ // own cell. Map pos -> module (i,j), check occupancy, then test distance to
1410
+ // that module's center.
1411
+ float testDot(vec2 pos, float r2) {
1412
+ int i = int(floor((pos.x - u_gridOriginUV) * u_invCellUV));
1413
+ int j = int(floor((pos.y - u_gridOriginUV) * u_invCellUV));
1414
+ if (i < 0 || i >= u_matrixLength || j < 0 || j >= u_matrixLength)
1415
+ return 1.0;
1416
+ if (texelFetch(u_occupancyTexture, ivec2(i, j), 0).r < 0.5)
1417
+ return 1.0;
1418
+ vec2 c = vec2(u_gridOriginUV) + (vec2(float(i), float(j)) + 0.5) * u_cellUV;
1419
+ vec2 d = pos - c;
1420
+ return dot(d, d) < r2 ? 0.0 : 1.0;
1421
+ }
1422
+
1423
+ float roundedRectSDF(vec2 p, float half_, float radius) {
1424
+ vec2 d = abs(p) - vec2(half_) + vec2(radius);
1425
+ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius;
1426
+ }
1427
+
1428
+ // Test all eye groups. Returns vec4(color, 1.0) if inside an eye, vec4(0) if not.
1429
+ vec4 testEyes(vec2 pos) {
1430
+ for (int g = 0; g < 3; g++) {
1431
+ vec2 local = (pos - u_eyeCenter[g]) / u_eyeScale[g];
1432
+ if (roundedRectSDF(local, u_eyeHalf[g].z, u_eyeRadius[g].z) < 0.0)
1433
+ return vec4(u_eyeColor[g], 1.0);
1434
+ if (roundedRectSDF(local, u_eyeHalf[g].y, u_eyeRadius[g].y) < 0.0)
1435
+ return vec4(u_backgroundColor, 1.0);
1436
+ if (roundedRectSDF(local, u_eyeHalf[g].x, u_eyeRadius[g].x) < 0.0)
1437
+ return vec4(u_eyeColor[g], 1.0);
1438
+ }
1439
+ return vec4(0.0);
1440
+ }
1441
+
1442
+ // Test scene (eyes + dots) at a position, returning color for a non-displaced pixel.
1443
+ // Returns vec4(color, 1.0) for solid, vec4(0, 0, 0, -1.0) for dot hole (discard).
1444
+ vec4 sampleStatic(vec2 pos, float r2) {
1445
+ vec4 eye = testEyes(pos);
1446
+ if (eye.a > 0.5) return eye;
1447
+ float hole = testDot(pos, r2);
1448
+ if (hole < 0.5) return vec4(0.0, 0.0, 0.0, -1.0);
1449
+ return vec4(u_backgroundColor, 1.0);
1450
+ }
1451
+
1452
+ void main() {
1453
+ vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y);
1454
+ float paint = texture(u_PaintingTexture, uv).r;
1455
+ float r2 = pow(u_dotRadius * (1.0 - paint), 2.0);
1456
+
1457
+ if (u_displacementActive == 0) {
1458
+ vec4 s = sampleStatic(uv, r2);
1459
+ if (s.a < 0.0) discard;
1460
+ fragColor = s;
1461
+ return;
1462
+ }
1463
+
1464
+ vec2 lensUV = (uv - u_lensOrigin) / u_lensSize;
1465
+ bool insideLens = lensUV.x >= 0.0 && lensUV.x <= 1.0 &&
1466
+ lensUV.y >= 0.0 && lensUV.y <= 1.0;
1467
+
1468
+ if (!insideLens) {
1469
+ vec4 s = sampleStatic(uv, r2);
1470
+ if (s.a < 0.0) discard;
1471
+ fragColor = s;
1472
+ return;
1473
+ }
1474
+
1475
+ vec4 dispSample = texture(u_displacementMap, lensUV);
1476
+ if (dispSample.a < 0.01) {
1477
+ vec4 s = sampleStatic(uv, r2);
1478
+ if (s.a < 0.0) discard;
1479
+ fragColor = s;
1480
+ return;
1481
+ }
1482
+ vec2 disp = (dispSample.rg - 0.5) * u_displacementScale;
1483
+
1484
+ float scaleR = 1.0 + u_chromaAmount * 2.0;
1485
+ float scaleG = 1.0 + u_chromaAmount * 1.0;
1486
+
1487
+ vec2 uvR = uv + disp * scaleR;
1488
+ vec2 uvG = uv + disp * scaleG;
1489
+ vec2 uvB = uv + disp;
1490
+
1491
+ // Attenuated displacement for eyes
1492
+ vec2 eyeDisp = disp * u_eyeRefractionScale;
1493
+ vec2 eyeUvR = uv + eyeDisp * scaleR;
1494
+ vec2 eyeUvG = uv + eyeDisp * scaleG;
1495
+ vec2 eyeUvB = uv + eyeDisp;
1496
+
1497
+ // Per-channel: test eyes at attenuated displacement, dots at full displacement
1498
+ vec4 eyeR = testEyes(eyeUvR);
1499
+ vec4 eyeG = testEyes(eyeUvG);
1500
+ vec4 eyeB = testEyes(eyeUvB);
1501
+
1502
+ float r, g, b;
1503
+
1504
+ if (eyeR.a > 0.5) {
1505
+ r = eyeR.r;
1506
+ } else {
1507
+ float holeR = testDot(uvR, r2);
1508
+ float paintR = texture(u_paintingColorTexture, uvR).r;
1509
+ r = mix(paintR, u_backgroundColor.r, holeR);
1510
+ }
1511
+
1512
+ if (eyeG.a > 0.5) {
1513
+ g = eyeG.g;
1514
+ } else {
1515
+ float holeG = testDot(uvG, r2);
1516
+ float paintG = texture(u_paintingColorTexture, uvG).g;
1517
+ g = mix(paintG, u_backgroundColor.g, holeG);
1518
+ }
1519
+
1520
+ if (eyeB.a > 0.5) {
1521
+ b = eyeB.b;
1522
+ } else {
1523
+ float holeB = testDot(uvB, r2);
1524
+ float paintB = texture(u_paintingColorTexture, uvB).b;
1525
+ b = mix(paintB, u_backgroundColor.b, holeB);
1526
+ }
1527
+
1528
+ fragColor = vec4(r, g, b, 1.0);
1529
+ }
1530
+ `;
1531
+ function createShader300(gl, type, src) {
1532
+ const shader = gl.createShader(type);
1533
+ if (!shader) throw new Error("Failed to create shader");
1534
+ gl.shaderSource(shader, src);
1535
+ gl.compileShader(shader);
1536
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1537
+ const info = gl.getShaderInfoLog(shader);
1538
+ gl.deleteShader(shader);
1539
+ throw new Error(`Shader compilation error: ${info ?? ""}`);
1540
+ }
1541
+ return shader;
1542
+ }
1543
+ var QRRefractionRenderer = class {
1544
+ constructor(init) {
1545
+ const gl = init.canvas.getContext("webgl2", { alpha: true, antialias: false, premultipliedAlpha: false });
1546
+ if (!gl) throw new Error("WebGL 2 not supported");
1547
+ this.gl = gl;
1548
+ this.matrixLength = init.matrixLength;
1549
+ this.gridOriginUV = init.gridOriginUV;
1550
+ this.cellUV = init.cellUV;
1551
+ this.dotRadius = init.dotRadius;
1552
+ this.backgroundColor = init.backgroundColor ?? [1, 1, 1];
1553
+ const vs = createShader300(gl, gl.VERTEX_SHADER, QR_VERTEX_SHADER);
1554
+ const fs = createShader300(gl, gl.FRAGMENT_SHADER, QR_FRAGMENT_SHADER);
1555
+ const prog = gl.createProgram();
1556
+ gl.attachShader(prog, vs);
1557
+ gl.attachShader(prog, fs);
1558
+ gl.linkProgram(prog);
1559
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
1560
+ throw new Error(`QR Program link error: ${gl.getProgramInfoLog(prog) ?? ""}`);
1561
+ }
1562
+ this.program = prog;
1563
+ const quad = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
1564
+ this.quadBuffer = gl.createBuffer();
1565
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
1566
+ gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
1567
+ this.occupancyTexture = gl.createTexture();
1568
+ gl.bindTexture(gl.TEXTURE_2D, this.occupancyTexture);
1569
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1570
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1571
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
1572
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
1573
+ gl.texImage2D(
1574
+ gl.TEXTURE_2D,
1575
+ 0,
1576
+ gl.R8,
1577
+ this.matrixLength,
1578
+ this.matrixLength,
1579
+ 0,
1580
+ gl.RED,
1581
+ gl.UNSIGNED_BYTE,
1582
+ init.occupancyMatrix
1583
+ );
1584
+ this.displacementTexture = gl.createTexture();
1585
+ gl.bindTexture(gl.TEXTURE_2D, this.displacementTexture);
1586
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1587
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1588
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1589
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1590
+ const blank1x1 = new Uint8Array([0, 0, 0, 0]);
1591
+ this.paintingTexture = gl.createTexture();
1592
+ gl.bindTexture(gl.TEXTURE_2D, this.paintingTexture);
1593
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, blank1x1);
1594
+ this.paintingColorTexture = gl.createTexture();
1595
+ gl.bindTexture(gl.TEXTURE_2D, this.paintingColorTexture);
1596
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, blank1x1);
1597
+ }
1598
+ setDisplacementMap(image) {
1599
+ const gl = this.gl;
1600
+ gl.bindTexture(gl.TEXTURE_2D, this.displacementTexture);
1601
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
1602
+ }
1603
+ render(params) {
1604
+ const gl = this.gl;
1605
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
1606
+ gl.useProgram(this.program);
1607
+ const aPos = gl.getAttribLocation(this.program, "a_position");
1608
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
1609
+ gl.enableVertexAttribArray(aPos);
1610
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
1611
+ gl.activeTexture(gl.TEXTURE0);
1612
+ gl.bindTexture(gl.TEXTURE_2D, this.occupancyTexture);
1613
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_occupancyTexture"), 0);
1614
+ gl.activeTexture(gl.TEXTURE1);
1615
+ gl.bindTexture(gl.TEXTURE_2D, this.displacementTexture);
1616
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_displacementMap"), 1);
1617
+ gl.activeTexture(gl.TEXTURE2);
1618
+ gl.bindTexture(gl.TEXTURE_2D, this.paintingTexture);
1619
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_PaintingTexture"), 2);
1620
+ gl.activeTexture(gl.TEXTURE3);
1621
+ gl.bindTexture(gl.TEXTURE_2D, this.paintingColorTexture);
1622
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_paintingColorTexture"), 3);
1623
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_dotRadius"), this.dotRadius);
1624
+ gl.uniform3fv(gl.getUniformLocation(this.program, "u_backgroundColor"), this.backgroundColor);
1625
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_gridOriginUV"), this.gridOriginUV);
1626
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_cellUV"), this.cellUV);
1627
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_invCellUV"), 1 / this.cellUV);
1628
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_matrixLength"), this.matrixLength);
1629
+ gl.uniform1i(gl.getUniformLocation(this.program, "u_displacementActive"), params.displacementActive ? 1 : 0);
1630
+ gl.uniform2f(gl.getUniformLocation(this.program, "u_lensOrigin"), params.lensOrigin[0], params.lensOrigin[1]);
1631
+ gl.uniform2f(gl.getUniformLocation(this.program, "u_lensSize"), params.lensSize[0], params.lensSize[1]);
1632
+ gl.uniform2f(gl.getUniformLocation(this.program, "u_displacementScale"), params.displacementScale[0], params.displacementScale[1]);
1633
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_chromaAmount"), params.chromaAmount);
1634
+ gl.uniform1f(gl.getUniformLocation(this.program, "u_eyeRefractionScale"), params.eyeRefractionScale ?? 0.2);
1635
+ const centers = new Float32Array(6);
1636
+ const halfs = new Float32Array(9);
1637
+ const radii = new Float32Array(9);
1638
+ const colors = new Float32Array(9);
1639
+ const scales = new Float32Array(3);
1640
+ for (let i = 0; i < 3; i++) {
1641
+ if (i < params.eyes.length) {
1642
+ const e = params.eyes[i];
1643
+ centers[i * 2 + 0] = e.center[0];
1644
+ centers[i * 2 + 1] = e.center[1];
1645
+ halfs[i * 3 + 0] = e.half[0];
1646
+ halfs[i * 3 + 1] = e.half[1];
1647
+ halfs[i * 3 + 2] = e.half[2];
1648
+ radii[i * 3 + 0] = e.radius[0];
1649
+ radii[i * 3 + 1] = e.radius[1];
1650
+ radii[i * 3 + 2] = e.radius[2];
1651
+ colors[i * 3 + 0] = e.color[0];
1652
+ colors[i * 3 + 1] = e.color[1];
1653
+ colors[i * 3 + 2] = e.color[2];
1654
+ scales[i] = e.scale;
1655
+ }
1656
+ }
1657
+ gl.uniform2fv(gl.getUniformLocation(this.program, "u_eyeCenter"), centers);
1658
+ gl.uniform3fv(gl.getUniformLocation(this.program, "u_eyeHalf"), halfs);
1659
+ gl.uniform3fv(gl.getUniformLocation(this.program, "u_eyeRadius"), radii);
1660
+ gl.uniform3fv(gl.getUniformLocation(this.program, "u_eyeColor"), colors);
1661
+ gl.uniform1fv(gl.getUniformLocation(this.program, "u_eyeScale"), scales);
1662
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
1663
+ }
1664
+ dispose() {
1665
+ const gl = this.gl;
1666
+ gl.deleteBuffer(this.quadBuffer);
1667
+ gl.deleteProgram(this.program);
1668
+ gl.deleteTexture(this.occupancyTexture);
1669
+ gl.deleteTexture(this.displacementTexture);
1670
+ gl.deleteTexture(this.paintingTexture);
1671
+ gl.deleteTexture(this.paintingColorTexture);
1672
+ }
1673
+ };
1674
+
1675
+ exports.DEFAULT_LENS_PARAMS = DEFAULT_LENS_PARAMS;
1676
+ exports.QRRefractionRenderer = QRRefractionRenderer;
1677
+ exports.SLIDER_DARK_OVERRIDES = SLIDER_DARK_OVERRIDES;
1678
+ exports.SLIDER_GLASS_DEFAULTS = SLIDER_GLASS_DEFAULTS;
1679
+ exports.SLIDER_LIGHT_OVERRIDES = SLIDER_LIGHT_OVERRIDES;
1680
+ exports.SLIDER_SAFARI_OVERRIDES = SLIDER_SAFARI_OVERRIDES;
1681
+ exports.SWITCH_DARK_OVERRIDES = SWITCH_DARK_OVERRIDES;
1682
+ exports.SWITCH_GLASS_DEFAULTS = SWITCH_GLASS_DEFAULTS;
1683
+ exports.SWITCH_LIGHT_OVERRIDES = SWITCH_LIGHT_OVERRIDES;
1684
+ exports.VideoRefractionRenderer = VideoRefractionRenderer;
1685
+ exports.colorMatrixValues = colorMatrixValues;
1686
+ exports.computeDomeConstants = computeDomeConstants;
1687
+ exports.createDisplacementMapGeneratorSync = createDisplacementMapGeneratorSync;
1688
+ exports.createLens = createLens;
1689
+ exports.domeGradient = domeGradient;
1690
+ exports.erf = erf;
1691
+ exports.generateDisplacementMapAsync = generateDisplacementMapAsync;
1692
+ exports.getSvgMaskDataUri = getSvgMaskDataUri;
1693
+ //# sourceMappingURL=vanilla.js.map
1694
+ //# sourceMappingURL=vanilla.js.map