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