liquid-gl 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/liquidGL.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /*
2
- * liquidGL – Liquid Glass - Powered by WebGL
2
+ * liquidGL – Liquid Glass - Powered by WebGPU/WebGL
3
3
  * -----------------------------------------------------------------------------
4
4
  *
5
5
  * Author: NaughtyDuk© – https://liquidgl.naughtyduk.com
6
6
  * Licence: MIT
7
- * Version: v2.1.0
7
+ * Version: v2.2.0
8
8
  */
9
9
 
10
10
  const liquidGL = (() => {
@@ -12,6 +12,13 @@ const liquidGL = (() => {
12
12
 
13
13
  const RECAPTURE_INTERVAL_MS = 250;
14
14
 
15
+ const ENGINE_CHAINS = {
16
+ auto: ["webgpu", "webgl2", "webgl", "experimental-webgl"],
17
+ webgpu: ["webgpu"],
18
+ webgl2: ["webgl2", "webgl", "experimental-webgl"],
19
+ webgl: ["webgl", "experimental-webgl"],
20
+ };
21
+
15
22
  /* --------------------------------------------------
16
23
  * Utilities
17
24
  * ------------------------------------------------*/
@@ -1804,176 +1811,33 @@ const liquidGL = (() => {
1804
1811
  })();
1805
1812
 
1806
1813
  /* --------------------------------------------------
1807
- * Shared renderer (one per page)
1814
+ * Render backends
1808
1815
  * ------------------------------------------------*/
1809
- class liquidGLRenderer {
1810
- constructor(snapshotSelector, snapshotResolution = 1.0) {
1811
- this._naughtyQueued = false;
1812
- this.canvas = document.createElement("canvas");
1813
- this.canvas.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;`;
1814
- this.canvas.setAttribute("data-liquid-ignore", "");
1815
- document.body.appendChild(this.canvas);
1816
+ class WebGLBackend {
1817
+ constructor(canvas, contexts = ["webgl2", "webgl", "experimental-webgl"]) {
1818
+ this.kind = "webgl";
1819
+ this.canvas = canvas;
1816
1820
 
1817
1821
  const ctxAttribs = {
1818
1822
  alpha: true,
1819
1823
  premultipliedAlpha: true,
1820
1824
  preserveDrawingBuffer: true,
1821
1825
  };
1822
- this.gl =
1823
- this.canvas.getContext("webgl2", ctxAttribs) ||
1824
- this.canvas.getContext("webgl", ctxAttribs) ||
1825
- this.canvas.getContext("experimental-webgl", ctxAttribs);
1826
- if (!this.gl) throw new Error("liquidGL: WebGL unavailable");
1827
-
1828
- this.lenses = [];
1829
- this.texture = null;
1830
- this.textureWidth = 0;
1831
- this.textureHeight = 0;
1832
- this.scaleFactor = 1;
1833
- this.startTime = Date.now();
1834
- this._scrollUpdateCounter = 0;
1835
-
1836
- this._initGL();
1837
-
1838
- this.snapshotTarget =
1839
- document.querySelector(snapshotSelector) || document.body;
1840
- if (!this.snapshotTarget) this.snapshotTarget = document.body;
1841
-
1842
- this._isScrolling = false;
1843
- let lastScrollY = window.scrollY;
1844
- let scrollTimeout;
1845
- const scrollCheck = () => {
1846
- if (window.scrollY !== lastScrollY) {
1847
- this._isScrolling = true;
1848
- lastScrollY = window.scrollY;
1849
- clearTimeout(scrollTimeout);
1850
- scrollTimeout = setTimeout(() => {
1851
- this._isScrolling = false;
1852
- }, 200);
1853
- }
1854
- requestAnimationFrame(scrollCheck);
1855
- };
1856
- requestAnimationFrame(scrollCheck);
1857
-
1858
- const onResize = debounce(() => {
1859
- if (this._capturing || this._isScrolling) return;
1860
-
1861
- if (window.visualViewport && window.visualViewport.scale !== 1) {
1862
- return;
1863
- }
1864
-
1865
- this._dynamicNodes.forEach((node) => {
1866
- const meta = this._dynMeta.get(node.el);
1867
- if (meta) {
1868
- meta.needsRecapture = true;
1869
- meta.prevDrawRect = null;
1870
- meta.lastCapture = null;
1871
- }
1872
- });
1873
-
1874
- this._resizeCanvas();
1875
- this.lenses.forEach((l) => l.updateMetrics());
1876
- this.captureSnapshot();
1877
- }, RECAPTURE_INTERVAL_MS);
1878
- window.addEventListener("resize", onResize, { passive: true });
1879
-
1880
- if ("ResizeObserver" in window) {
1881
- new ResizeObserver(onResize).observe(this.snapshotTarget);
1826
+ let gl = null;
1827
+ for (const name of contexts) {
1828
+ gl = canvas.getContext(name, ctxAttribs);
1829
+ if (gl) break;
1882
1830
  }
1831
+ if (!gl) throw new Error("liquidGL: WebGL unavailable");
1883
1832
 
1884
- /* --------------------------------------------------
1885
- * Dynamic DOM elements (non-video, e.g. animating text)
1886
- * ------------------------------------------------*/
1887
- this._dynamicNodes = [];
1888
- this._dynMeta = new WeakMap();
1889
- this._lastDynamicUpdate = 0;
1890
-
1891
- const styleEl = document.createElement("style");
1892
- styleEl.id = "liquid-gl-dynamic-styles";
1893
- document.head.appendChild(styleEl);
1894
- this._dynamicStyleSheet = styleEl.sheet;
1895
-
1896
- this._snapshotResolution = Math.max(
1897
- 0.1,
1898
- Math.min(3.0, snapshotResolution),
1899
- );
1900
- this._pendingReveal = [];
1901
-
1902
- this._resizeCanvas();
1903
- this.captureSnapshot();
1904
-
1905
- /* --------------------------------------------------
1906
- * Dynamic media (video) support
1907
- * ------------------------------------------------*/
1908
- this._videoNodes = Array.from(
1909
- this.snapshotTarget.querySelectorAll("video"),
1910
- );
1911
- this._videoNodes = this._videoNodes.filter((v) => !this._isIgnored(v));
1912
- this._tmpCanvas = document.createElement("canvas");
1913
- this._tmpCtx = this._tmpCanvas.getContext("2d");
1914
-
1915
- this._videoFrameState = new WeakMap();
1916
-
1917
- this._videoAlphaState = new WeakMap();
1918
-
1919
- this.canvas.style.opacity = "0";
1920
-
1921
- this.useExternalTicker = false;
1922
-
1923
- /* --------------------------------------------------
1924
- * Inline worker for heavy dynamic nodes
1925
- * ------------------------------------------------*/
1926
- this._workerEnabled =
1927
- typeof OffscreenCanvas !== "undefined" &&
1928
- typeof Worker !== "undefined" &&
1929
- typeof ImageBitmap !== "undefined";
1930
-
1931
- if (this._workerEnabled) {
1932
- const workerSrc = `
1933
- /* dynamic-element worker (runs in its own thread) */
1934
- self.onmessage = async (e) => {
1935
- const { id, width, height, snap, dyn } = e.data;
1936
- const off = new OffscreenCanvas(width, height);
1937
- const ctx = off.getContext('2d');
1938
-
1939
- ctx.drawImage(snap, 0, 0, width, height);
1940
- ctx.drawImage(dyn, 0, 0, width, height);
1941
-
1942
- const bmp = await off.transferToImageBitmap();
1943
- self.postMessage({ id, bmp }, [bmp]);
1944
- };
1945
- `;
1946
- const blob = new Blob([workerSrc], { type: "application/javascript" });
1947
- this._dynWorker = new Worker(URL.createObjectURL(blob), {
1948
- type: "module",
1949
- });
1950
-
1951
- this._dynJobs = new Map();
1952
-
1953
- this._dynWorker.onmessage = (e) => {
1954
- const { id, bmp } = e.data;
1955
- const meta = this._dynJobs.get(id);
1956
- if (!meta) return;
1957
- this._dynJobs.delete(id);
1833
+ this.gl = gl;
1834
+ this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
1835
+ this.texture = null;
1958
1836
 
1959
- const { x, y, w, h } = meta;
1960
- const gl = this.gl;
1961
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
1962
- gl.texSubImage2D(
1963
- gl.TEXTURE_2D,
1964
- 0,
1965
- x,
1966
- y,
1967
- gl.RGBA,
1968
- gl.UNSIGNED_BYTE,
1969
- bmp,
1970
- );
1971
- };
1972
- }
1837
+ this._initLensProgram();
1973
1838
  }
1974
1839
 
1975
- /* ----------------------------- */
1976
- _initGL() {
1840
+ _initLensProgram() {
1977
1841
  const vsSource = `
1978
1842
  attribute vec2 a_position;
1979
1843
  varying vec2 v_uv;
@@ -2132,8 +1996,8 @@ const liquidGL = (() => {
2132
1996
  gl_FragColor = final;
2133
1997
  }`;
2134
1998
 
2135
- this.program = createProgram(this.gl, vsSource, fsSource);
2136
1999
  const gl = this.gl;
2000
+ this.program = createProgram(gl, vsSource, fsSource);
2137
2001
  if (!this.program) throw new Error("liquidGL: Shader failed");
2138
2002
 
2139
2003
  const posBuf = gl.createBuffer();
@@ -2177,64 +2041,1149 @@ const liquidGL = (() => {
2177
2041
  };
2178
2042
  }
2179
2043
 
2180
- /* ----------------------------- */
2181
- _resizeCanvas() {
2182
- const dpr = Math.min(2, window.devicePixelRatio || 1);
2183
- this.canvas.width = innerWidth * dpr;
2184
- this.canvas.height = innerHeight * dpr;
2185
- this.canvas.style.width = `${innerWidth}px`;
2186
- this.canvas.style.height = `${innerHeight}px`;
2044
+ resize() {
2187
2045
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
2188
2046
  }
2189
2047
 
2190
- /* ----------------------------- */
2191
- async captureSnapshot() {
2192
- if (this._capturing) return;
2193
- this._capturing = true;
2048
+ uploadSnapshot(srcCanvas) {
2049
+ const gl = this.gl;
2050
+ if (!this.texture) this.texture = gl.createTexture();
2051
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2052
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2053
+ gl.texImage2D(
2054
+ gl.TEXTURE_2D,
2055
+ 0,
2056
+ gl.RGBA,
2057
+ gl.RGBA,
2058
+ gl.UNSIGNED_BYTE,
2059
+ srcCanvas,
2060
+ );
2061
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2062
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2063
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2064
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2065
+ this._vFboTexture = null;
2066
+ return true;
2067
+ }
2194
2068
 
2195
- const undos = [];
2069
+ uploadRegion(x, y, source) {
2070
+ if (!this.texture) return;
2071
+ const gl = this.gl;
2072
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2073
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2074
+ gl.texSubImage2D(
2075
+ gl.TEXTURE_2D,
2076
+ 0,
2077
+ x,
2078
+ y,
2079
+ gl.RGBA,
2080
+ gl.UNSIGNED_BYTE,
2081
+ source,
2082
+ );
2083
+ }
2196
2084
 
2197
- const attemptCapture = async (
2198
- attempt = 1,
2199
- maxAttempts = 3,
2200
- delayMs = 500,
2201
- ) => {
2202
- try {
2203
- const fullW = this.snapshotTarget.scrollWidth;
2204
- const fullH = this.snapshotTarget.scrollHeight;
2205
- const maxTex = this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE) || 8192;
2206
- const MAX_MOBILE_DIM = 4096;
2207
- const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.userAgent);
2085
+ _initVideoBlit() {
2086
+ if (this._vBlitReady !== undefined) return this._vBlitReady;
2208
2087
 
2209
- let scale = Math.min(
2210
- this._snapshotResolution,
2211
- maxTex / fullW,
2212
- maxTex / fullH,
2213
- );
2088
+ const gl = this.gl;
2214
2089
 
2215
- if (isMobileSafari) {
2216
- const over = (Math.max(fullW, fullH) * scale) / MAX_MOBILE_DIM;
2217
- if (over > 1) scale = scale / over;
2218
- }
2090
+ const vs = `
2091
+ attribute vec2 a_position;
2092
+ varying vec2 v_uv;
2093
+ void main(){
2094
+ v_uv = (a_position + 1.0) * 0.5;
2095
+ gl_Position = vec4(a_position, 0.0, 1.0);
2096
+ }`;
2219
2097
 
2220
- const maxArea = isMobileSafari ? 4096 * 4096 : 16384 * 16384;
2221
- if (fullW * fullH * scale * scale > maxArea) {
2222
- scale = Math.sqrt(maxArea / (fullW * fullH));
2223
- console.warn(
2224
- `liquidGL: snapshot area capped, resolution reduced to ${scale.toFixed(
2225
- 3,
2226
- )} for a ${fullW}x${fullH} document.`,
2227
- );
2228
- }
2098
+ const fs = `
2099
+ precision mediump float;
2100
+ varying vec2 v_uv;
2101
+ uniform sampler2D u_src;
2102
+ uniform vec4 u_srcRect;
2103
+ void main(){
2104
+ gl_FragColor = texture2D(u_src, u_srcRect.xy + v_uv * u_srcRect.zw);
2105
+ }`;
2229
2106
 
2230
- this.scaleFactor = Math.max(0.1, scale);
2107
+ const prog = createProgram(gl, vs, fs);
2108
+ if (!prog) {
2109
+ this._vBlitReady = false;
2110
+ return false;
2111
+ }
2231
2112
 
2232
- this.canvas.style.visibility = "hidden";
2233
- undos.push(() => (this.canvas.style.visibility = "visible"));
2113
+ this._vProg = prog;
2114
+ this._vPosLoc = gl.getAttribLocation(prog, "a_position");
2115
+ this._vU = {
2116
+ src: gl.getUniformLocation(prog, "u_src"),
2117
+ srcRect: gl.getUniformLocation(prog, "u_srcRect"),
2118
+ };
2234
2119
 
2235
- const lensElements = this.lenses
2236
- .flatMap((lens) => [lens.el, lens._shadowEl])
2237
- .filter(Boolean);
2120
+ this._vTex = gl.createTexture();
2121
+ gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2122
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2123
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2124
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2125
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2126
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2127
+
2128
+ this._vFbo = gl.createFramebuffer();
2129
+ this._vFboTexture = null;
2130
+
2131
+ this._vBlitReady = true;
2132
+ return true;
2133
+ }
2134
+
2135
+ _restoreLensProgramState() {
2136
+ const gl = this.gl;
2137
+ gl.useProgram(this.program);
2138
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2139
+ gl.enableVertexAttribArray(this._posLoc);
2140
+ gl.vertexAttribPointer(this._posLoc, 2, gl.FLOAT, false, 0, 0);
2141
+ gl.activeTexture(gl.TEXTURE0);
2142
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2143
+ gl.uniform1i(this.u.tex, 0);
2144
+ }
2145
+
2146
+ blitVideo(vid, dstX, dstY, dstW, dstH, srcRect) {
2147
+ if (!this.texture || !this._initVideoBlit()) return false;
2148
+ const gl = this.gl;
2149
+
2150
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this._vFbo);
2151
+
2152
+ if (this._vFboTexture !== this.texture) {
2153
+ gl.framebufferTexture2D(
2154
+ gl.FRAMEBUFFER,
2155
+ gl.COLOR_ATTACHMENT0,
2156
+ gl.TEXTURE_2D,
2157
+ this.texture,
2158
+ 0,
2159
+ );
2160
+ if (
2161
+ gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
2162
+ ) {
2163
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2164
+ this._vBlitReady = false;
2165
+ return false;
2166
+ }
2167
+ this._vFboTexture = this.texture;
2168
+ }
2169
+
2170
+ gl.activeTexture(gl.TEXTURE0);
2171
+ gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2172
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2173
+
2174
+ try {
2175
+ gl.texImage2D(
2176
+ gl.TEXTURE_2D,
2177
+ 0,
2178
+ gl.RGBA,
2179
+ gl.RGBA,
2180
+ gl.UNSIGNED_BYTE,
2181
+ vid,
2182
+ );
2183
+ } catch (e) {
2184
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2185
+ this._restoreLensProgramState();
2186
+ return false;
2187
+ }
2188
+
2189
+ gl.useProgram(this._vProg);
2190
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2191
+ gl.enableVertexAttribArray(this._vPosLoc);
2192
+ gl.vertexAttribPointer(this._vPosLoc, 2, gl.FLOAT, false, 0, 0);
2193
+
2194
+ gl.uniform1i(this._vU.src, 0);
2195
+ gl.uniform4f(
2196
+ this._vU.srcRect,
2197
+ srcRect.u,
2198
+ srcRect.v,
2199
+ srcRect.uw,
2200
+ srcRect.vh,
2201
+ );
2202
+
2203
+ gl.viewport(dstX, dstY, dstW, dstH);
2204
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2205
+
2206
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2207
+ this._restoreLensProgramState();
2208
+
2209
+ return true;
2210
+ }
2211
+
2212
+ beginFrame(width, height, time) {
2213
+ const gl = this.gl;
2214
+ gl.clearColor(0, 0, 0, 0);
2215
+ gl.clear(gl.COLOR_BUFFER_BIT);
2216
+ gl.useProgram(this.program);
2217
+ gl.activeTexture(gl.TEXTURE0);
2218
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2219
+ gl.uniform1i(this.u.tex, 0);
2220
+ gl.uniform1f(this.u.time, time);
2221
+ }
2222
+
2223
+ drawLens(lens, p) {
2224
+ const gl = this.gl;
2225
+
2226
+ gl.viewport(p.x, p.y, p.w, p.h);
2227
+ gl.uniform2f(this.u.res, p.w, p.h);
2228
+ gl.uniform2f(this.u.subpixel, p.subX, p.subY);
2229
+ gl.uniform2f(this.u.boxSize, p.boxW, p.boxH);
2230
+ gl.uniform4f(
2231
+ this.u.bounds,
2232
+ p.bounds[0],
2233
+ p.bounds[1],
2234
+ p.bounds[2],
2235
+ p.bounds[3],
2236
+ );
2237
+ gl.uniform2f(this.u.textureResolution, p.texW, p.texH);
2238
+ gl.uniform1f(this.u.refraction, p.refraction);
2239
+ gl.uniform1f(this.u.aberration, p.aberration);
2240
+ gl.uniform1f(this.u.bevelDepth, p.bevelDepth);
2241
+ gl.uniform1f(this.u.bevelWidth, p.bevelWidth);
2242
+ gl.uniform1f(this.u.frost, p.frost);
2243
+ gl.uniform1f(this.u.radius, p.radius);
2244
+ gl.uniform1i(this.u.specular, p.specular);
2245
+ gl.uniform1f(this.u.revealProgress, p.revealProgress);
2246
+ gl.uniform1i(this.u.revealType, p.revealType);
2247
+ gl.uniform1f(this.u.magnify, p.magnify);
2248
+ gl.uniform1f(this.u.tiltX, p.tiltX);
2249
+ gl.uniform1f(this.u.tiltY, p.tiltY);
2250
+
2251
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2252
+ }
2253
+
2254
+ endFrame() {}
2255
+
2256
+ clearRegions(rects) {
2257
+ const gl = this.gl;
2258
+ rects.forEach(({ x, y, w, h }) => {
2259
+ gl.enable(gl.SCISSOR_TEST);
2260
+ gl.scissor(x, y, w, h);
2261
+ gl.clearColor(0, 0, 0, 0);
2262
+ gl.clear(gl.COLOR_BUFFER_BIT);
2263
+ gl.disable(gl.SCISSOR_TEST);
2264
+ });
2265
+ }
2266
+ }
2267
+
2268
+ /* --------------------------------------------------
2269
+ * WGSL sources (WebGPU backend)
2270
+ * ------------------------------------------------*/
2271
+ const WEBGPU_LENS_WGSL = `
2272
+ struct LensUniforms {
2273
+ resolution: vec2<f32>,
2274
+ textureResolution: vec2<f32>,
2275
+ bounds: vec4<f32>,
2276
+ subpixel: vec2<f32>,
2277
+ boxSize: vec2<f32>,
2278
+ refraction: f32,
2279
+ aberration: f32,
2280
+ bevelDepth: f32,
2281
+ bevelWidth: f32,
2282
+ frost: f32,
2283
+ radius: f32,
2284
+ time: f32,
2285
+ specular: f32,
2286
+ revealProgress: f32,
2287
+ revealType: f32,
2288
+ tiltX: f32,
2289
+ tiltY: f32,
2290
+ magnify: f32,
2291
+ };
2292
+
2293
+ @group(0) @binding(0) var u_tex: texture_2d<f32>;
2294
+ @group(0) @binding(1) var u_samp: sampler;
2295
+ @group(1) @binding(0) var<uniform> u: LensUniforms;
2296
+
2297
+ struct VSOut {
2298
+ @builtin(position) pos: vec4<f32>,
2299
+ @location(0) uv: vec2<f32>,
2300
+ };
2301
+
2302
+ @vertex
2303
+ fn vs(@location(0) a_position: vec2<f32>) -> VSOut {
2304
+ var o: VSOut;
2305
+ o.uv = (a_position + vec2<f32>(1.0)) * 0.5;
2306
+ o.pos = vec4<f32>(a_position, 0.0, 1.0);
2307
+ return o;
2308
+ }
2309
+
2310
+ fn udRoundBox(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
2311
+ return length(max(abs(p) - b + vec2<f32>(r), vec2<f32>(0.0))) - r;
2312
+ }
2313
+
2314
+ fn cornerNormal(p: vec2<f32>, b: vec2<f32>, r: f32, fb: vec2<f32>) -> vec2<f32> {
2315
+ let q = abs(p) - b + vec2<f32>(r);
2316
+ let m = max(q, vec2<f32>(0.0));
2317
+ let l = length(m);
2318
+ if (l <= 0.0) { return fb; }
2319
+ let w = smoothstep(0.0, max(r * 0.5, 1.0), min(m.x, m.y));
2320
+ if (w <= 0.0) { return fb; }
2321
+ let s = vec2<f32>(select(1.0, -1.0, p.x < 0.0), select(1.0, -1.0, p.y < 0.0));
2322
+ return normalize(mix(fb, s * (m / l), w));
2323
+ }
2324
+
2325
+ fn random2(st: vec2<f32>) -> f32 {
2326
+ return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453123);
2327
+ }
2328
+
2329
+ fn edgeFactor(p_px: vec2<f32>, b_px: vec2<f32>, radius_px: f32) -> f32 {
2330
+ let d = -udRoundBox(p_px, b_px, radius_px);
2331
+ let bevel_px = u.bevelWidth * min(u.boxSize.x, u.boxSize.y);
2332
+ return 1.0 - smoothstep(0.0, bevel_px, d);
2333
+ }
2334
+
2335
+ @fragment
2336
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2337
+ var p = in.uv - vec2<f32>(0.5);
2338
+ p.x = p.x * (u.resolution.x / u.resolution.y);
2339
+
2340
+ let p_px = in.uv * u.resolution - u.subpixel - 0.5 * u.boxSize;
2341
+ let b_px = 0.5 * u.boxSize;
2342
+
2343
+ let edge = edgeFactor(p_px, b_px, u.radius);
2344
+ let offsetAmt = edge * u.refraction + pow(edge, 10.0) * u.bevelDepth;
2345
+ let centreBlend = smoothstep(0.15, 0.45, length(p));
2346
+ let refractDir = cornerNormal(p_px, b_px, u.radius, normalize(p));
2347
+ let offset = refractDir * offsetAmt * centreBlend;
2348
+
2349
+ let tiltRefractionScale = 0.05;
2350
+ let deg2rad = 0.017453292519943295;
2351
+ let tiltOffset = vec2<f32>(tan(u.tiltY * deg2rad), -tan(u.tiltX * deg2rad)) * tiltRefractionScale;
2352
+
2353
+ let localUV = (in.uv - vec2<f32>(0.5)) / vec2<f32>(u.magnify) + vec2<f32>(0.5);
2354
+ let flippedUV = vec2<f32>(localUV.x, 1.0 - localUV.y);
2355
+ let mapped = u.bounds.xy + flippedUV * u.bounds.zw;
2356
+ let refracted = mapped + offset - tiltOffset;
2357
+
2358
+ let oob = max(max(-refracted.x, refracted.x - 1.0), max(-refracted.y, refracted.y - 1.0));
2359
+ let blend = 1.0 - smoothstep(0.0, 0.01, oob);
2360
+ let sampleUV = mix(mapped, refracted, blend);
2361
+
2362
+ let baseCol = textureSample(u_tex, u_samp, mapped);
2363
+
2364
+ let texel = vec2<f32>(1.0) / u.textureResolution;
2365
+ var refrCol: vec4<f32>;
2366
+
2367
+ let chroma = offset * u.aberration;
2368
+
2369
+ if (u.frost > 0.0) {
2370
+ let radius = u.frost * 4.0;
2371
+ var sum = vec4<f32>(0.0);
2372
+
2373
+ for (var i = 0; i < 16; i = i + 1) {
2374
+ let fi = f32(i);
2375
+ let angle = random2(in.uv + vec2<f32>(fi)) * 6.283185;
2376
+ let dist = sqrt(random2(in.uv - vec2<f32>(fi))) * radius;
2377
+ let foff = vec2<f32>(cos(angle), sin(angle)) * texel * dist;
2378
+ if (u.aberration > 0.0) {
2379
+ let c0 = textureSample(u_tex, u_samp, sampleUV + foff - chroma);
2380
+ let c1 = textureSample(u_tex, u_samp, sampleUV + foff);
2381
+ let c2 = textureSample(u_tex, u_samp, sampleUV + foff + chroma);
2382
+ sum = sum + vec4<f32>(c0.r, c1.g, c2.b, c1.a);
2383
+ } else {
2384
+ sum = sum + textureSample(u_tex, u_samp, sampleUV + foff);
2385
+ }
2386
+ }
2387
+ refrCol = sum / 16.0;
2388
+ } else {
2389
+ refrCol = textureSample(u_tex, u_samp, sampleUV);
2390
+ refrCol = refrCol + textureSample(u_tex, u_samp, sampleUV + vec2<f32>(texel.x, 0.0));
2391
+ refrCol = refrCol + textureSample(u_tex, u_samp, sampleUV + vec2<f32>(-texel.x, 0.0));
2392
+ refrCol = refrCol + textureSample(u_tex, u_samp, sampleUV + vec2<f32>(0.0, texel.y));
2393
+ refrCol = refrCol + textureSample(u_tex, u_samp, sampleUV + vec2<f32>(0.0, -texel.y));
2394
+ refrCol = refrCol / 5.0;
2395
+
2396
+ if (u.aberration > 0.0) {
2397
+ let chromaR = textureSample(u_tex, u_samp, sampleUV - chroma).r;
2398
+ let chromaB = textureSample(u_tex, u_samp, sampleUV + chroma).b;
2399
+ refrCol = vec4<f32>(chromaR, refrCol.g, chromaB, refrCol.a);
2400
+ }
2401
+ }
2402
+
2403
+ if (refrCol.a < 0.1) {
2404
+ refrCol = baseCol;
2405
+ }
2406
+
2407
+ var finalCol = refrCol;
2408
+
2409
+ let dmask = udRoundBox(p_px, b_px, u.radius);
2410
+ let inShape = 1.0 - smoothstep(-0.5, 0.5, dmask);
2411
+
2412
+ if (u.specular > 0.5) {
2413
+ let lp1 = vec2<f32>(sin(u.time * 0.2), cos(u.time * 0.3)) * 0.6 + vec2<f32>(0.5);
2414
+ let lp2 = vec2<f32>(sin(u.time * -0.4 + 1.5), cos(u.time * 0.25 - 0.5)) * 0.6 + vec2<f32>(0.5);
2415
+ var h = 0.0;
2416
+ h = h + smoothstep(0.4, 0.0, distance(in.uv, lp1)) * 0.1;
2417
+ h = h + smoothstep(0.5, 0.0, distance(in.uv, lp2)) * 0.08;
2418
+ finalCol = vec4<f32>(finalCol.rgb + vec3<f32>(h), finalCol.a);
2419
+ }
2420
+
2421
+ if (u.revealType == 1.0) {
2422
+ finalCol = vec4<f32>(finalCol.rgb * u.revealProgress, finalCol.a * u.revealProgress);
2423
+ }
2424
+
2425
+ finalCol = vec4<f32>(finalCol.rgb * inShape, finalCol.a * inShape);
2426
+
2427
+ return finalCol;
2428
+ }`;
2429
+
2430
+ const WEBGPU_BLIT_WGSL = `
2431
+ @group(0) @binding(0) var u_src: texture_2d<f32>;
2432
+ @group(0) @binding(1) var u_samp: sampler;
2433
+ @group(1) @binding(0) var<uniform> u_srcRect: vec4<f32>;
2434
+
2435
+ struct VSOut {
2436
+ @builtin(position) pos: vec4<f32>,
2437
+ @location(0) uv: vec2<f32>,
2438
+ };
2439
+
2440
+ @vertex
2441
+ fn vs(@location(0) a_position: vec2<f32>) -> VSOut {
2442
+ var o: VSOut;
2443
+ o.uv = (a_position + vec2<f32>(1.0)) * 0.5;
2444
+ o.pos = vec4<f32>(a_position, 0.0, 1.0);
2445
+ return o;
2446
+ }
2447
+
2448
+ @fragment
2449
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2450
+ let uv = vec2<f32>(in.uv.x, 1.0 - in.uv.y);
2451
+ return textureSample(u_src, u_samp, u_srcRect.xy + uv * u_srcRect.zw);
2452
+ }`;
2453
+
2454
+ const WEBGPU_CLEAR_WGSL = `
2455
+ @vertex
2456
+ fn vs(@location(0) a_position: vec2<f32>) -> @builtin(position) vec4<f32> {
2457
+ return vec4<f32>(a_position, 0.0, 1.0);
2458
+ }
2459
+
2460
+ @fragment
2461
+ fn fs() -> @location(0) vec4<f32> {
2462
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
2463
+ }`;
2464
+
2465
+ const GPU_UNIFORM_FLOATS = 64;
2466
+
2467
+ class WebGPUBackend {
2468
+ static async create(canvas) {
2469
+ if (typeof navigator === "undefined" || !("gpu" in navigator))
2470
+ return null;
2471
+ try {
2472
+ const adapter = await navigator.gpu.requestAdapter();
2473
+ if (!adapter) return null;
2474
+ const device = await adapter.requestDevice();
2475
+
2476
+ const lensModule = device.createShaderModule({
2477
+ code: WEBGPU_LENS_WGSL,
2478
+ });
2479
+ const blitModule = device.createShaderModule({
2480
+ code: WEBGPU_BLIT_WGSL,
2481
+ });
2482
+ const clearModule = device.createShaderModule({
2483
+ code: WEBGPU_CLEAR_WGSL,
2484
+ });
2485
+ const infos = await Promise.all([
2486
+ lensModule.getCompilationInfo(),
2487
+ blitModule.getCompilationInfo(),
2488
+ clearModule.getCompilationInfo(),
2489
+ ]);
2490
+ const hasError = infos.some((info) =>
2491
+ info.messages.some((m) => m.type === "error"),
2492
+ );
2493
+ if (hasError) return null;
2494
+
2495
+ return new WebGPUBackend(
2496
+ canvas,
2497
+ device,
2498
+ lensModule,
2499
+ blitModule,
2500
+ clearModule,
2501
+ );
2502
+ } catch (e) {
2503
+ return null;
2504
+ }
2505
+ }
2506
+
2507
+ constructor(canvas, device, lensModule, blitModule, clearModule) {
2508
+ this.kind = "webgpu";
2509
+ this.canvas = canvas;
2510
+ this.device = device;
2511
+ this.maxTextureSize = device.limits.maxTextureDimension2D || 8192;
2512
+
2513
+ this.ctx = canvas.getContext("webgpu");
2514
+ if (!this.ctx)
2515
+ throw new Error("liquidGL: WebGPU canvas context unavailable");
2516
+ this.format = navigator.gpu.getPreferredCanvasFormat();
2517
+ this.ctx.configure({
2518
+ device,
2519
+ format: this.format,
2520
+ alphaMode: "premultiplied",
2521
+ });
2522
+
2523
+ device.lost.then((info) => {
2524
+ if (info.reason !== "destroyed") {
2525
+ console.warn("liquidGL: WebGPU device lost:", info.message);
2526
+ }
2527
+ });
2528
+
2529
+ const quad = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
2530
+ this._vb = device.createBuffer({
2531
+ size: quad.byteLength,
2532
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2533
+ });
2534
+ device.queue.writeBuffer(this._vb, 0, quad);
2535
+
2536
+ this._sampler = device.createSampler({
2537
+ magFilter: "linear",
2538
+ minFilter: "linear",
2539
+ addressModeU: "clamp-to-edge",
2540
+ addressModeV: "clamp-to-edge",
2541
+ });
2542
+
2543
+ this._texSampLayout = device.createBindGroupLayout({
2544
+ entries: [
2545
+ {
2546
+ binding: 0,
2547
+ visibility: GPUShaderStage.FRAGMENT,
2548
+ texture: {},
2549
+ },
2550
+ {
2551
+ binding: 1,
2552
+ visibility: GPUShaderStage.FRAGMENT,
2553
+ sampler: {},
2554
+ },
2555
+ ],
2556
+ });
2557
+ this._dynamicUniformLayout = device.createBindGroupLayout({
2558
+ entries: [
2559
+ {
2560
+ binding: 0,
2561
+ visibility: GPUShaderStage.FRAGMENT,
2562
+ buffer: { type: "uniform", hasDynamicOffset: true },
2563
+ },
2564
+ ],
2565
+ });
2566
+ this._staticUniformLayout = device.createBindGroupLayout({
2567
+ entries: [
2568
+ {
2569
+ binding: 0,
2570
+ visibility: GPUShaderStage.FRAGMENT,
2571
+ buffer: { type: "uniform" },
2572
+ },
2573
+ ],
2574
+ });
2575
+
2576
+ const quadBufferLayout = {
2577
+ arrayStride: 8,
2578
+ attributes: [{ shaderLocation: 0, offset: 0, format: "float32x2" }],
2579
+ };
2580
+
2581
+ this._lensPipe = device.createRenderPipeline({
2582
+ layout: device.createPipelineLayout({
2583
+ bindGroupLayouts: [this._texSampLayout, this._dynamicUniformLayout],
2584
+ }),
2585
+ vertex: {
2586
+ module: lensModule,
2587
+ entryPoint: "vs",
2588
+ buffers: [quadBufferLayout],
2589
+ },
2590
+ fragment: {
2591
+ module: lensModule,
2592
+ entryPoint: "fs",
2593
+ targets: [{ format: this.format }],
2594
+ },
2595
+ primitive: { topology: "triangle-list" },
2596
+ });
2597
+
2598
+ this._blitPipe = device.createRenderPipeline({
2599
+ layout: device.createPipelineLayout({
2600
+ bindGroupLayouts: [this._texSampLayout, this._staticUniformLayout],
2601
+ }),
2602
+ vertex: {
2603
+ module: blitModule,
2604
+ entryPoint: "vs",
2605
+ buffers: [quadBufferLayout],
2606
+ },
2607
+ fragment: {
2608
+ module: blitModule,
2609
+ entryPoint: "fs",
2610
+ targets: [{ format: "rgba8unorm" }],
2611
+ },
2612
+ primitive: { topology: "triangle-list" },
2613
+ });
2614
+
2615
+ this._clearPipe = device.createRenderPipeline({
2616
+ layout: device.createPipelineLayout({ bindGroupLayouts: [] }),
2617
+ vertex: {
2618
+ module: clearModule,
2619
+ entryPoint: "vs",
2620
+ buffers: [quadBufferLayout],
2621
+ },
2622
+ fragment: {
2623
+ module: clearModule,
2624
+ entryPoint: "fs",
2625
+ targets: [{ format: this.format }],
2626
+ },
2627
+ primitive: { topology: "triangle-list" },
2628
+ });
2629
+
2630
+ this.texture = null;
2631
+ this._texBindGroup = null;
2632
+ this._videoTex = null;
2633
+ this._videoBindGroup = null;
2634
+ this._videoTexW = 0;
2635
+ this._videoTexH = 0;
2636
+
2637
+ this._blitUniform = device.createBuffer({
2638
+ size: 16,
2639
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2640
+ });
2641
+ this._blitBindGroup = device.createBindGroup({
2642
+ layout: this._staticUniformLayout,
2643
+ entries: [{ binding: 0, resource: { buffer: this._blitUniform } }],
2644
+ });
2645
+
2646
+ this._uniformBuf = null;
2647
+ this._uniformBindGroup = null;
2648
+ this._uniformCapacity = 0;
2649
+
2650
+ this._enc = null;
2651
+ this._drawQueue = [];
2652
+ this._frameTime = 0;
2653
+ }
2654
+
2655
+ resize() {}
2656
+
2657
+ _ensureUniformCapacity(lensCount) {
2658
+ const need = lensCount * 256;
2659
+ if (this._uniformBuf && this._uniformCapacity >= need) return;
2660
+ if (this._uniformBuf) this._uniformBuf.destroy();
2661
+ const cap = Math.max(need, 256 * 8);
2662
+ this._uniformBuf = this.device.createBuffer({
2663
+ size: cap,
2664
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2665
+ });
2666
+ this._uniformBindGroup = this.device.createBindGroup({
2667
+ layout: this._dynamicUniformLayout,
2668
+ entries: [
2669
+ {
2670
+ binding: 0,
2671
+ resource: { buffer: this._uniformBuf, offset: 0, size: 112 },
2672
+ },
2673
+ ],
2674
+ });
2675
+ this._uniformCapacity = cap;
2676
+ }
2677
+
2678
+ uploadSnapshot(srcCanvas) {
2679
+ const w = srcCanvas.width;
2680
+ const h = srcCanvas.height;
2681
+ if (
2682
+ !this.texture ||
2683
+ this.texture.width !== w ||
2684
+ this.texture.height !== h
2685
+ ) {
2686
+ if (this.texture) this.texture.destroy();
2687
+ this.texture = this.device.createTexture({
2688
+ size: [w, h],
2689
+ format: "rgba8unorm",
2690
+ usage:
2691
+ GPUTextureUsage.TEXTURE_BINDING |
2692
+ GPUTextureUsage.COPY_DST |
2693
+ GPUTextureUsage.RENDER_ATTACHMENT,
2694
+ });
2695
+ this._texBindGroup = this.device.createBindGroup({
2696
+ layout: this._texSampLayout,
2697
+ entries: [
2698
+ { binding: 0, resource: this.texture.createView() },
2699
+ { binding: 1, resource: this._sampler },
2700
+ ],
2701
+ });
2702
+ }
2703
+ try {
2704
+ this.device.queue.copyExternalImageToTexture(
2705
+ { source: srcCanvas },
2706
+ { texture: this.texture },
2707
+ { width: w, height: h },
2708
+ );
2709
+ } catch (e) {
2710
+ console.error("liquidGL: WebGPU snapshot upload failed", e);
2711
+ return false;
2712
+ }
2713
+ return true;
2714
+ }
2715
+
2716
+ uploadRegion(x, y, source) {
2717
+ if (!this.texture || !source) return;
2718
+ const w = Math.min(source.width, this.texture.width - x);
2719
+ const h = Math.min(source.height, this.texture.height - y);
2720
+ if (w <= 0 || h <= 0 || x < 0 || y < 0) return;
2721
+ try {
2722
+ this.device.queue.copyExternalImageToTexture(
2723
+ { source },
2724
+ { texture: this.texture, origin: [x, y] },
2725
+ { width: w, height: h },
2726
+ );
2727
+ } catch (e) {
2728
+ console.warn("liquidGL: WebGPU region upload failed", e);
2729
+ }
2730
+ }
2731
+
2732
+ blitVideo(vid, dstX, dstY, dstW, dstH, srcRect) {
2733
+ if (!this.texture) return false;
2734
+ const device = this.device;
2735
+ const vw = vid.videoWidth;
2736
+ const vh = vid.videoHeight;
2737
+ if (!vw || !vh) return false;
2738
+
2739
+ try {
2740
+ if (!this._vCanvas) {
2741
+ this._vCanvas = document.createElement("canvas");
2742
+ this._vCtx = this._vCanvas.getContext("2d");
2743
+ }
2744
+ if (this._vCanvas.width !== vw || this._vCanvas.height !== vh) {
2745
+ this._vCanvas.width = vw;
2746
+ this._vCanvas.height = vh;
2747
+ }
2748
+ this._vCtx.drawImage(vid, 0, 0, vw, vh);
2749
+
2750
+ if (
2751
+ !this._videoTex ||
2752
+ this._videoTexW !== vw ||
2753
+ this._videoTexH !== vh
2754
+ ) {
2755
+ if (this._videoTex) this._videoTex.destroy();
2756
+ this._videoTex = device.createTexture({
2757
+ size: [vw, vh],
2758
+ format: "rgba8unorm",
2759
+ usage:
2760
+ GPUTextureUsage.TEXTURE_BINDING |
2761
+ GPUTextureUsage.COPY_DST |
2762
+ GPUTextureUsage.RENDER_ATTACHMENT,
2763
+ });
2764
+ this._videoTexW = vw;
2765
+ this._videoTexH = vh;
2766
+ this._videoBindGroup = device.createBindGroup({
2767
+ layout: this._texSampLayout,
2768
+ entries: [
2769
+ { binding: 0, resource: this._videoTex.createView() },
2770
+ { binding: 1, resource: this._sampler },
2771
+ ],
2772
+ });
2773
+ }
2774
+
2775
+ device.queue.copyExternalImageToTexture(
2776
+ { source: this._vCanvas },
2777
+ { texture: this._videoTex },
2778
+ { width: vw, height: vh },
2779
+ );
2780
+ device.queue.writeBuffer(
2781
+ this._blitUniform,
2782
+ 0,
2783
+ new Float32Array([srcRect.u, srcRect.v, srcRect.uw, srcRect.vh]),
2784
+ );
2785
+
2786
+ const enc = device.createCommandEncoder();
2787
+ const pass = enc.beginRenderPass({
2788
+ colorAttachments: [
2789
+ {
2790
+ view: this.texture.createView(),
2791
+ loadOp: "load",
2792
+ storeOp: "store",
2793
+ },
2794
+ ],
2795
+ });
2796
+ pass.setPipeline(this._blitPipe);
2797
+ pass.setVertexBuffer(0, this._vb);
2798
+ pass.setBindGroup(0, this._videoBindGroup);
2799
+ pass.setBindGroup(1, this._blitBindGroup);
2800
+ pass.setViewport(
2801
+ dstX,
2802
+ dstY,
2803
+ Math.max(1, dstW),
2804
+ Math.max(1, dstH),
2805
+ 0,
2806
+ 1,
2807
+ );
2808
+ pass.draw(6);
2809
+ pass.end();
2810
+ device.queue.submit([enc.finish()]);
2811
+ return true;
2812
+ } catch (e) {
2813
+ return false;
2814
+ }
2815
+ }
2816
+
2817
+ beginFrame(width, height, time) {
2818
+ this._frameTime = time;
2819
+ this._drawQueue.length = 0;
2820
+ this._enc = this.device.createCommandEncoder();
2821
+ }
2822
+
2823
+ drawLens(lens, p) {
2824
+ const cx = Math.max(0, p.x);
2825
+ const cy = Math.max(0, p.y);
2826
+ const cw = Math.min(this.canvas.width - cx, p.w);
2827
+ const ch = Math.min(this.canvas.height - cy, p.h);
2828
+ if (cw <= 0 || ch <= 0) return;
2829
+ this._drawQueue.push({ p, x: cx, y: cy, w: cw, h: ch });
2830
+ }
2831
+
2832
+ endFrame() {
2833
+ const device = this.device;
2834
+ const draws = this._drawQueue;
2835
+
2836
+ this._ensureUniformCapacity(Math.max(1, draws.length));
2837
+
2838
+ if (draws.length) {
2839
+ const data = new Float32Array(GPU_UNIFORM_FLOATS * draws.length);
2840
+ for (let i = 0; i < draws.length; i++) {
2841
+ const p = draws[i].p;
2842
+ const o = i * GPU_UNIFORM_FLOATS;
2843
+ data[o] = p.w;
2844
+ data[o + 1] = p.h;
2845
+ data[o + 2] = p.texW;
2846
+ data[o + 3] = p.texH;
2847
+ data[o + 4] = p.bounds[0];
2848
+ data[o + 5] = p.bounds[1];
2849
+ data[o + 6] = p.bounds[2];
2850
+ data[o + 7] = p.bounds[3];
2851
+ data[o + 8] = p.subX;
2852
+ data[o + 9] = p.subY;
2853
+ data[o + 10] = p.boxW;
2854
+ data[o + 11] = p.boxH;
2855
+ data[o + 12] = p.refraction;
2856
+ data[o + 13] = p.aberration;
2857
+ data[o + 14] = p.bevelDepth;
2858
+ data[o + 15] = p.bevelWidth;
2859
+ data[o + 16] = p.frost;
2860
+ data[o + 17] = p.radius;
2861
+ data[o + 18] = this._frameTime;
2862
+ data[o + 19] = p.specular;
2863
+ data[o + 20] = p.revealProgress;
2864
+ data[o + 21] = p.revealType;
2865
+ data[o + 22] = p.tiltX;
2866
+ data[o + 23] = p.tiltY;
2867
+ data[o + 24] = p.magnify;
2868
+ }
2869
+ device.queue.writeBuffer(this._uniformBuf, 0, data);
2870
+ }
2871
+
2872
+ const view = this.ctx.getCurrentTexture().createView();
2873
+ const pass = this._enc.beginRenderPass({
2874
+ colorAttachments: [
2875
+ {
2876
+ view,
2877
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
2878
+ loadOp: "clear",
2879
+ storeOp: "store",
2880
+ },
2881
+ ],
2882
+ });
2883
+ if (draws.length) {
2884
+ pass.setPipeline(this._lensPipe);
2885
+ pass.setVertexBuffer(0, this._vb);
2886
+ pass.setBindGroup(0, this._texBindGroup);
2887
+ for (let i = 0; i < draws.length; i++) {
2888
+ const d = draws[i];
2889
+ pass.setViewport(d.x, d.y, d.w, d.h, 0, 1);
2890
+ pass.setBindGroup(1, this._uniformBindGroup, [i * 256]);
2891
+ pass.draw(6);
2892
+ }
2893
+ }
2894
+ pass.end();
2895
+
2896
+ device.queue.submit([this._enc.finish()]);
2897
+ this._enc = null;
2898
+ }
2899
+
2900
+ clearRegions(rects) {
2901
+ if (!rects.length) return;
2902
+ const device = this.device;
2903
+ const enc = device.createCommandEncoder();
2904
+ const pass = enc.beginRenderPass({
2905
+ colorAttachments: [
2906
+ {
2907
+ view: this.ctx.getCurrentTexture().createView(),
2908
+ loadOp: "load",
2909
+ storeOp: "store",
2910
+ },
2911
+ ],
2912
+ });
2913
+ pass.setPipeline(this._clearPipe);
2914
+ pass.setVertexBuffer(0, this._vb);
2915
+ rects.forEach(({ x, y, w, h }) => {
2916
+ const cx = Math.max(0, Math.min(this.canvas.width, x));
2917
+ const cy = Math.max(0, Math.min(this.canvas.height, y));
2918
+ const cw = Math.max(0, Math.min(this.canvas.width - cx, w));
2919
+ const ch = Math.max(0, Math.min(this.canvas.height - cy, h));
2920
+ if (cw > 0 && ch > 0) {
2921
+ pass.setScissorRect(cx, cy, cw, ch);
2922
+ pass.draw(6);
2923
+ }
2924
+ });
2925
+ pass.end();
2926
+ device.queue.submit([enc.finish()]);
2927
+ }
2928
+ }
2929
+
2930
+ /* --------------------------------------------------
2931
+ * Shared renderer (one per page)
2932
+ * ------------------------------------------------*/
2933
+ class liquidGLRenderer {
2934
+ constructor(snapshotSelector, snapshotResolution = 1.0, engine = "auto") {
2935
+ this._engine = engine;
2936
+ this._naughtyQueued = false;
2937
+ this.canvas = document.createElement("canvas");
2938
+ this.canvas.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;`;
2939
+ this.canvas.setAttribute("data-liquid-ignore", "");
2940
+ document.body.appendChild(this.canvas);
2941
+
2942
+ this.backend = null;
2943
+ this._backendFailed = false;
2944
+ this._pendingLensActivation = [];
2945
+
2946
+ this.lenses = [];
2947
+ this.hasTexture = false;
2948
+ this.textureWidth = 0;
2949
+ this.textureHeight = 0;
2950
+ this.scaleFactor = 1;
2951
+ this.startTime = Date.now();
2952
+ this._scrollUpdateCounter = 0;
2953
+
2954
+ this._backendReady = this._selectBackend();
2955
+
2956
+ this.snapshotTarget =
2957
+ document.querySelector(snapshotSelector) || document.body;
2958
+ if (!this.snapshotTarget) this.snapshotTarget = document.body;
2959
+
2960
+ this._isScrolling = false;
2961
+ let lastScrollY = window.scrollY;
2962
+ let scrollTimeout;
2963
+ const scrollCheck = () => {
2964
+ if (window.scrollY !== lastScrollY) {
2965
+ this._isScrolling = true;
2966
+ lastScrollY = window.scrollY;
2967
+ clearTimeout(scrollTimeout);
2968
+ scrollTimeout = setTimeout(() => {
2969
+ this._isScrolling = false;
2970
+ }, 200);
2971
+ }
2972
+ requestAnimationFrame(scrollCheck);
2973
+ };
2974
+ requestAnimationFrame(scrollCheck);
2975
+
2976
+ const onResize = debounce(() => {
2977
+ if (this._capturing || this._isScrolling) return;
2978
+
2979
+ if (window.visualViewport && window.visualViewport.scale !== 1) {
2980
+ return;
2981
+ }
2982
+
2983
+ this._dynamicNodes.forEach((node) => {
2984
+ const meta = this._dynMeta.get(node.el);
2985
+ if (meta) {
2986
+ meta.needsRecapture = true;
2987
+ meta.prevDrawRect = null;
2988
+ meta.lastCapture = null;
2989
+ }
2990
+ });
2991
+
2992
+ this._resizeCanvas();
2993
+ this.lenses.forEach((l) => l.updateMetrics());
2994
+ this.captureSnapshot();
2995
+ }, RECAPTURE_INTERVAL_MS);
2996
+ window.addEventListener("resize", onResize, { passive: true });
2997
+
2998
+ if ("ResizeObserver" in window) {
2999
+ new ResizeObserver(onResize).observe(this.snapshotTarget);
3000
+ }
3001
+
3002
+ /* --------------------------------------------------
3003
+ * Dynamic DOM elements (non-video, e.g. animating text)
3004
+ * ------------------------------------------------*/
3005
+ this._dynamicNodes = [];
3006
+ this._dynMeta = new WeakMap();
3007
+ this._lastDynamicUpdate = 0;
3008
+
3009
+ const styleEl = document.createElement("style");
3010
+ styleEl.id = "liquid-gl-dynamic-styles";
3011
+ document.head.appendChild(styleEl);
3012
+ this._dynamicStyleSheet = styleEl.sheet;
3013
+
3014
+ this._snapshotResolution = Math.max(
3015
+ 0.1,
3016
+ Math.min(3.0, snapshotResolution),
3017
+ );
3018
+ this._pendingReveal = [];
3019
+
3020
+ this._resizeCanvas();
3021
+ this.captureSnapshot();
3022
+
3023
+ /* --------------------------------------------------
3024
+ * Dynamic media (video) support
3025
+ * ------------------------------------------------*/
3026
+ this._videoNodes = Array.from(
3027
+ this.snapshotTarget.querySelectorAll("video"),
3028
+ );
3029
+ this._videoNodes = this._videoNodes.filter((v) => !this._isIgnored(v));
3030
+ this._tmpCanvas = document.createElement("canvas");
3031
+ this._tmpCtx = this._tmpCanvas.getContext("2d");
3032
+
3033
+ this._videoFrameState = new WeakMap();
3034
+
3035
+ this._videoAlphaState = new WeakMap();
3036
+
3037
+ this.canvas.style.opacity = "0";
3038
+
3039
+ this.useExternalTicker = false;
3040
+
3041
+ /* --------------------------------------------------
3042
+ * Inline worker for heavy dynamic nodes
3043
+ * ------------------------------------------------*/
3044
+ this._workerEnabled =
3045
+ typeof OffscreenCanvas !== "undefined" &&
3046
+ typeof Worker !== "undefined" &&
3047
+ typeof ImageBitmap !== "undefined";
3048
+
3049
+ if (this._workerEnabled) {
3050
+ const workerSrc = `
3051
+ /* dynamic-element worker (runs in its own thread) */
3052
+ self.onmessage = async (e) => {
3053
+ const { id, width, height, snap, dyn } = e.data;
3054
+ const off = new OffscreenCanvas(width, height);
3055
+ const ctx = off.getContext('2d');
3056
+
3057
+ ctx.drawImage(snap, 0, 0, width, height);
3058
+ ctx.drawImage(dyn, 0, 0, width, height);
3059
+
3060
+ const bmp = await off.transferToImageBitmap();
3061
+ self.postMessage({ id, bmp }, [bmp]);
3062
+ };
3063
+ `;
3064
+ const blob = new Blob([workerSrc], { type: "application/javascript" });
3065
+ this._dynWorker = new Worker(URL.createObjectURL(blob), {
3066
+ type: "module",
3067
+ });
3068
+
3069
+ this._dynJobs = new Map();
3070
+
3071
+ this._dynWorker.onmessage = (e) => {
3072
+ const { id, bmp } = e.data;
3073
+ const meta = this._dynJobs.get(id);
3074
+ if (!meta) return;
3075
+ this._dynJobs.delete(id);
3076
+
3077
+ const { x, y } = meta;
3078
+ if (!this.backend || !this.hasTexture) return;
3079
+ this.backend.uploadRegion(x, y, bmp);
3080
+ };
3081
+ }
3082
+ }
3083
+
3084
+ /* ----------------------------- */
3085
+ async _selectBackend() {
3086
+ const chain = ENGINE_CHAINS[this._engine] || ENGINE_CHAINS.auto;
3087
+ let backend = null;
3088
+
3089
+ if (chain[0] === "webgpu") {
3090
+ try {
3091
+ backend = await WebGPUBackend.create(this.canvas);
3092
+ } catch (e) {
3093
+ backend = null;
3094
+ }
3095
+ }
3096
+
3097
+ if (!backend) {
3098
+ const glChain = chain.filter((c) => c !== "webgpu");
3099
+ if (glChain.length) {
3100
+ try {
3101
+ backend = new WebGLBackend(this.canvas, glChain);
3102
+ } catch (e) {
3103
+ backend = null;
3104
+ }
3105
+ }
3106
+ }
3107
+
3108
+ this.backend = backend;
3109
+
3110
+ if (!backend) {
3111
+ this._backendFailed = true;
3112
+ console.warn(
3113
+ "liquidGL: No GPU backend available – lenses will keep their original styles.",
3114
+ );
3115
+ return false;
3116
+ }
3117
+
3118
+ const pending = this._pendingLensActivation.splice(0);
3119
+ pending.forEach((ln) => ln._activate());
3120
+ return true;
3121
+ }
3122
+
3123
+ /* ----------------------------- */
3124
+ _resizeCanvas() {
3125
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
3126
+ this.canvas.width = innerWidth * dpr;
3127
+ this.canvas.height = innerHeight * dpr;
3128
+ this.canvas.style.width = `${innerWidth}px`;
3129
+ this.canvas.style.height = `${innerHeight}px`;
3130
+ if (this.backend) this.backend.resize();
3131
+ }
3132
+
3133
+ /* ----------------------------- */
3134
+ async captureSnapshot() {
3135
+ if (this._capturing) return;
3136
+ this._capturing = true;
3137
+
3138
+ const ready = await this._backendReady;
3139
+ if (!ready) {
3140
+ this._capturing = false;
3141
+ return false;
3142
+ }
3143
+
3144
+ const undos = [];
3145
+
3146
+ const attemptCapture = async (
3147
+ attempt = 1,
3148
+ maxAttempts = 3,
3149
+ delayMs = 500,
3150
+ ) => {
3151
+ try {
3152
+ const fullW = this.snapshotTarget.scrollWidth;
3153
+ const fullH = this.snapshotTarget.scrollHeight;
3154
+ const maxTex = (this.backend && this.backend.maxTextureSize) || 8192;
3155
+ const MAX_MOBILE_DIM = 4096;
3156
+ const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.userAgent);
3157
+
3158
+ let scale = Math.min(
3159
+ this._snapshotResolution,
3160
+ maxTex / fullW,
3161
+ maxTex / fullH,
3162
+ );
3163
+
3164
+ if (isMobileSafari) {
3165
+ const over = (Math.max(fullW, fullH) * scale) / MAX_MOBILE_DIM;
3166
+ if (over > 1) scale = scale / over;
3167
+ }
3168
+
3169
+ const maxArea = isMobileSafari ? 4096 * 4096 : 16384 * 16384;
3170
+ if (fullW * fullH * scale * scale > maxArea) {
3171
+ scale = Math.sqrt(maxArea / (fullW * fullH));
3172
+ console.warn(
3173
+ `liquidGL: snapshot area capped, resolution reduced to ${scale.toFixed(
3174
+ 3,
3175
+ )} for a ${fullW}x${fullH} document.`,
3176
+ );
3177
+ }
3178
+
3179
+ this.scaleFactor = Math.max(0.1, scale);
3180
+
3181
+ this.canvas.style.visibility = "hidden";
3182
+ undos.push(() => (this.canvas.style.visibility = "visible"));
3183
+
3184
+ const lensElements = this.lenses
3185
+ .flatMap((lens) => [lens.el, lens._shadowEl])
3186
+ .filter(Boolean);
2238
3187
 
2239
3188
  lensElements.forEach((el) => {
2240
3189
  el.setAttribute("data-liquidgl-hide", "");
@@ -2335,30 +3284,16 @@ const liquidGL = (() => {
2335
3284
  return false;
2336
3285
  }
2337
3286
  this.staticSnapshotCanvas = srcCanvas;
2338
- const gl = this.gl;
2339
- if (!this.texture) this.texture = gl.createTexture();
2340
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2341
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2342
- gl.texImage2D(
2343
- gl.TEXTURE_2D,
2344
- 0,
2345
- gl.RGBA,
2346
- gl.RGBA,
2347
- gl.UNSIGNED_BYTE,
2348
- srcCanvas,
2349
- );
2350
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2351
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2352
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2353
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3287
+ if (!this.backend || !this.backend.uploadSnapshot(srcCanvas)) {
3288
+ return false;
3289
+ }
2354
3290
 
3291
+ this.hasTexture = true;
2355
3292
  this.textureWidth = srcCanvas.width;
2356
3293
  this.textureHeight = srcCanvas.height;
2357
3294
 
2358
3295
  if (this._videoFrameState) this._videoFrameState = new WeakMap();
2359
3296
 
2360
- this._vFboTexture = null;
2361
-
2362
3297
  this.render();
2363
3298
 
2364
3299
  if (this._pendingReveal.length) {
@@ -2379,7 +3314,13 @@ const liquidGL = (() => {
2379
3314
  this.canvas.style.zIndex = maxZ - 1;
2380
3315
  }
2381
3316
 
2382
- if (!this.texture) {
3317
+ if (this.backend) {
3318
+ lens._activate();
3319
+ } else {
3320
+ this._pendingLensActivation.push(lens);
3321
+ }
3322
+
3323
+ if (!this.hasTexture) {
2383
3324
  this._pendingReveal.push(lens);
2384
3325
  } else {
2385
3326
  lens._reveal();
@@ -2389,8 +3330,8 @@ const liquidGL = (() => {
2389
3330
 
2390
3331
  /* ----------------------------- */
2391
3332
  render() {
2392
- const gl = this.gl;
2393
- if (!this.texture) return;
3333
+ const backend = this.backend;
3334
+ if (!backend || !this.hasTexture) return;
2394
3335
 
2395
3336
  this.lenses.forEach((ln) => {
2396
3337
  if (ln._isSticky && !ln._mirrorActive) ln.updateMetrics();
@@ -2400,15 +3341,9 @@ const liquidGL = (() => {
2400
3341
  this._scrollUpdateCounter++;
2401
3342
  }
2402
3343
 
2403
- gl.clearColor(0, 0, 0, 0);
2404
- gl.clear(gl.COLOR_BUFFER_BIT);
2405
- gl.useProgram(this.program);
2406
- gl.activeTexture(gl.TEXTURE0);
2407
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2408
- gl.uniform1i(this.u.tex, 0);
2409
-
2410
3344
  const time = (Date.now() - this.startTime) / 1000;
2411
- gl.uniform1f(this.u.time, time);
3345
+
3346
+ backend.beginFrame(this.canvas.width, this.canvas.height, time);
2412
3347
 
2413
3348
  this._updateDynamicVideos();
2414
3349
 
@@ -2424,6 +3359,8 @@ const liquidGL = (() => {
2424
3359
  this._renderLens(lens);
2425
3360
  });
2426
3361
 
3362
+ backend.endFrame();
3363
+
2427
3364
  this.lenses.forEach((ln) => {
2428
3365
  if (ln._mirrorActive && ln._mirrorCtx) {
2429
3366
  const mirror = ln._mirror;
@@ -2439,6 +3376,7 @@ const liquidGL = (() => {
2439
3376
  });
2440
3377
 
2441
3378
  const dpr = Math.min(2, window.devicePixelRatio || 1);
3379
+ const clearRects = [];
2442
3380
  this.lenses.forEach((ln) => {
2443
3381
  if (ln._mirrorActive && ln.rectPx) {
2444
3382
  const { left, top, width, height } = ln.rectPx;
@@ -2457,19 +3395,15 @@ const liquidGL = (() => {
2457
3395
  Math.round(height * dpr) + expand * 2,
2458
3396
  );
2459
3397
  if (w > 0 && h > 0) {
2460
- gl.enable(gl.SCISSOR_TEST);
2461
- gl.scissor(x, y, w, h);
2462
- gl.clearColor(0, 0, 0, 0);
2463
- gl.clear(gl.COLOR_BUFFER_BIT);
2464
- gl.disable(gl.SCISSOR_TEST);
3398
+ clearRects.push({ x, y, w, h });
2465
3399
  }
2466
3400
  }
2467
3401
  });
3402
+ backend.clearRegions(clearRects);
2468
3403
  }
2469
3404
 
2470
3405
  /* ----------------------------- */
2471
3406
  _renderLens(lens) {
2472
- const gl = this.gl;
2473
3407
  const rect = lens.rectPx;
2474
3408
  if (!rect) return;
2475
3409
 
@@ -2492,11 +3426,6 @@ const liquidGL = (() => {
2492
3426
  const h = Math.round(topPx + rect.height * dpr) - yTop;
2493
3427
  const y = this.canvas.height - (yTop + h);
2494
3428
 
2495
- gl.viewport(x, y, w, h);
2496
- gl.uniform2f(this.u.res, w, h);
2497
- gl.uniform2f(this.u.subpixel, leftPx - x, topPx - yTop);
2498
- gl.uniform2f(this.u.boxSize, rect.width * dpr, rect.height * dpr);
2499
-
2500
3429
  const snapRect =
2501
3430
  this._frameSnapRect || this.snapshotTarget.getBoundingClientRect();
2502
3431
  const docX = rect.left - snapRect.left;
@@ -2505,22 +3434,6 @@ const liquidGL = (() => {
2505
3434
  const topUV = (docY * this.scaleFactor) / this.textureHeight;
2506
3435
  const wUV = (rect.width * this.scaleFactor) / this.textureWidth;
2507
3436
  const hUV = (rect.height * this.scaleFactor) / this.textureHeight;
2508
- gl.uniform4f(this.u.bounds, leftUV, topUV, wUV, hUV);
2509
-
2510
- gl.uniform2f(
2511
- this.u.textureResolution,
2512
- this.textureWidth,
2513
- this.textureHeight,
2514
- );
2515
- gl.uniform1f(this.u.refraction, lens.options.refraction);
2516
- gl.uniform1f(this.u.aberration, lens.options.aberration || 0);
2517
- gl.uniform1f(this.u.bevelDepth, lens.options.bevelDepth);
2518
- gl.uniform1f(this.u.bevelWidth, lens.options.bevelWidth);
2519
- gl.uniform1f(this.u.frost, lens.options.frost);
2520
- gl.uniform1f(this.u.radius, lens.radiusGl);
2521
- gl.uniform1i(this.u.specular, lens.options.specular ? 1 : 0);
2522
- gl.uniform1f(this.u.revealProgress, lens._revealProgress || 1.0);
2523
- gl.uniform1i(this.u.revealType, lens.revealTypeIndex || 0);
2524
3437
 
2525
3438
  const mag = Math.max(
2526
3439
  0.001,
@@ -2529,12 +3442,32 @@ const liquidGL = (() => {
2529
3442
  lens.options.magnify !== undefined ? lens.options.magnify : 1.0,
2530
3443
  ),
2531
3444
  );
2532
- gl.uniform1f(this.u.magnify, mag);
2533
-
2534
- gl.uniform1f(this.u.tiltX, lens.tiltX || 0);
2535
- gl.uniform1f(this.u.tiltY, lens.tiltY || 0);
2536
3445
 
2537
- gl.drawArrays(gl.TRIANGLES, 0, 6);
3446
+ this.backend.drawLens(lens, {
3447
+ x,
3448
+ y,
3449
+ w,
3450
+ h,
3451
+ subX: leftPx - x,
3452
+ subY: topPx - yTop,
3453
+ boxW: rect.width * dpr,
3454
+ boxH: rect.height * dpr,
3455
+ bounds: [leftUV, topUV, wUV, hUV],
3456
+ texW: this.textureWidth,
3457
+ texH: this.textureHeight,
3458
+ refraction: lens.options.refraction,
3459
+ aberration: lens.options.aberration || 0,
3460
+ bevelDepth: lens.options.bevelDepth,
3461
+ bevelWidth: lens.options.bevelWidth,
3462
+ frost: lens.options.frost,
3463
+ radius: lens.radiusGl,
3464
+ specular: lens.options.specular ? 1 : 0,
3465
+ revealProgress: lens._revealProgress || 1.0,
3466
+ revealType: lens.revealTypeIndex || 0,
3467
+ magnify: mag,
3468
+ tiltX: lens.tiltX || 0,
3469
+ tiltY: lens.tiltY || 0,
3470
+ });
2538
3471
  }
2539
3472
 
2540
3473
  /* ----------------------------- */
@@ -2552,56 +3485,6 @@ const liquidGL = (() => {
2552
3485
  ctx.closePath();
2553
3486
  }
2554
3487
 
2555
- _initVideoBlit() {
2556
- if (this._vBlitReady !== undefined) return this._vBlitReady;
2557
-
2558
- const gl = this.gl;
2559
-
2560
- const vs = `
2561
- attribute vec2 a_position;
2562
- varying vec2 v_uv;
2563
- void main(){
2564
- v_uv = (a_position + 1.0) * 0.5;
2565
- gl_Position = vec4(a_position, 0.0, 1.0);
2566
- }`;
2567
-
2568
- const fs = `
2569
- precision mediump float;
2570
- varying vec2 v_uv;
2571
- uniform sampler2D u_src;
2572
- uniform vec4 u_srcRect;
2573
- void main(){
2574
- gl_FragColor = texture2D(u_src, u_srcRect.xy + v_uv * u_srcRect.zw);
2575
- }`;
2576
-
2577
- const prog = createProgram(gl, vs, fs);
2578
- if (!prog) {
2579
- this._vBlitReady = false;
2580
- return false;
2581
- }
2582
-
2583
- this._vProg = prog;
2584
- this._vPosLoc = gl.getAttribLocation(prog, "a_position");
2585
- this._vU = {
2586
- src: gl.getUniformLocation(prog, "u_src"),
2587
- srcRect: gl.getUniformLocation(prog, "u_srcRect"),
2588
- };
2589
-
2590
- this._vTex = gl.createTexture();
2591
- gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2592
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2593
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2594
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2595
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2596
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2597
-
2598
- this._vFbo = gl.createFramebuffer();
2599
- this._vFboTexture = null;
2600
-
2601
- this._vBlitReady = true;
2602
- return true;
2603
- }
2604
-
2605
3488
  _videoIsOpaque(vid) {
2606
3489
  if (!vid.videoWidth || !vid.videoHeight) return false;
2607
3490
 
@@ -2636,92 +3519,15 @@ const liquidGL = (() => {
2636
3519
  return opaque;
2637
3520
  }
2638
3521
 
2639
- _blitVideoToTexture(vid, dstX, dstY, dstW, dstH, srcRect) {
2640
- const gl = this.gl;
2641
-
2642
- gl.bindFramebuffer(gl.FRAMEBUFFER, this._vFbo);
2643
-
2644
- if (this._vFboTexture !== this.texture) {
2645
- gl.framebufferTexture2D(
2646
- gl.FRAMEBUFFER,
2647
- gl.COLOR_ATTACHMENT0,
2648
- gl.TEXTURE_2D,
2649
- this.texture,
2650
- 0,
2651
- );
2652
- if (
2653
- gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
2654
- ) {
2655
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2656
- this._vBlitReady = false;
2657
- return false;
2658
- }
2659
- this._vFboTexture = this.texture;
2660
- }
2661
-
2662
- gl.activeTexture(gl.TEXTURE0);
2663
- gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2664
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2665
-
2666
- try {
2667
- gl.texImage2D(
2668
- gl.TEXTURE_2D,
2669
- 0,
2670
- gl.RGBA,
2671
- gl.RGBA,
2672
- gl.UNSIGNED_BYTE,
2673
- vid,
2674
- );
2675
- } catch (e) {
2676
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2677
- this._restoreLensProgramState();
2678
- return false;
2679
- }
2680
-
2681
- gl.useProgram(this._vProg);
2682
- gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2683
- gl.enableVertexAttribArray(this._vPosLoc);
2684
- gl.vertexAttribPointer(this._vPosLoc, 2, gl.FLOAT, false, 0, 0);
2685
-
2686
- gl.uniform1i(this._vU.src, 0);
2687
- gl.uniform4f(
2688
- this._vU.srcRect,
2689
- srcRect.u,
2690
- srcRect.v,
2691
- srcRect.uw,
2692
- srcRect.vh,
2693
- );
2694
-
2695
- gl.viewport(dstX, dstY, dstW, dstH);
2696
- gl.drawArrays(gl.TRIANGLES, 0, 6);
2697
-
2698
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2699
- this._restoreLensProgramState();
2700
-
2701
- return true;
2702
- }
2703
-
2704
- _restoreLensProgramState() {
2705
- const gl = this.gl;
2706
- gl.useProgram(this.program);
2707
- gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2708
- gl.enableVertexAttribArray(this._posLoc);
2709
- gl.vertexAttribPointer(this._posLoc, 2, gl.FLOAT, false, 0, 0);
2710
- gl.activeTexture(gl.TEXTURE0);
2711
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2712
- gl.uniform1i(this.u.tex, 0);
2713
- }
2714
-
2715
3522
  /* ----------------------------- */
2716
3523
  _updateDynamicVideos() {
2717
3524
  if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
2718
3525
  if (
2719
- !this.texture ||
3526
+ !this.hasTexture ||
2720
3527
  !this.staticSnapshotCanvas ||
2721
3528
  !this._videoNodes.length
2722
3529
  )
2723
3530
  return;
2724
- const gl = this.gl;
2725
3531
 
2726
3532
  const snapRect = this.snapshotTarget.getBoundingClientRect();
2727
3533
 
@@ -2803,9 +3609,8 @@ const liquidGL = (() => {
2803
3609
 
2804
3610
  if (
2805
3611
  !isRounded &&
2806
- this._initVideoBlit() &&
2807
3612
  this._videoIsOpaque(vid) &&
2808
- this._blitVideoToTexture(vid, dstX, dstY, updW, updH, {
3613
+ this.backend.blitVideo(vid, dstX, dstY, updW, updH, {
2809
3614
  u: srcX / drawW,
2810
3615
  v: srcY / drawH,
2811
3616
  uw: updW / drawW,
@@ -2856,25 +3661,14 @@ const liquidGL = (() => {
2856
3661
  return;
2857
3662
  }
2858
3663
 
2859
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2860
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2861
- gl.texSubImage2D(
2862
- gl.TEXTURE_2D,
2863
- 0,
2864
- dstX,
2865
- dstY,
2866
- gl.RGBA,
2867
- gl.UNSIGNED_BYTE,
2868
- this._tmpCanvas,
2869
- );
3664
+ this.backend.uploadRegion(dstX, dstY, this._tmpCanvas);
2870
3665
  });
2871
3666
  }
2872
3667
 
2873
3668
  /* ----------------------------- */
2874
3669
  _updateDynamicNodes() {
2875
3670
  if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
2876
- const gl = this.gl;
2877
- if (!this.texture || !this._dynMeta) return;
3671
+ if (!this.hasTexture || !this._dynMeta) return;
2878
3672
  const snapRect = this.snapshotTarget.getBoundingClientRect();
2879
3673
  const maxLensZ = this._getMaxLensZ();
2880
3674
 
@@ -2979,16 +3773,7 @@ const liquidGL = (() => {
2979
3773
  w,
2980
3774
  h,
2981
3775
  );
2982
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2983
- gl.texSubImage2D(
2984
- gl.TEXTURE_2D,
2985
- 0,
2986
- x,
2987
- y,
2988
- gl.RGBA,
2989
- gl.UNSIGNED_BYTE,
2990
- eraseCanvas,
2991
- );
3776
+ this.backend.uploadRegion(x, y, eraseCanvas);
2992
3777
  }
2993
3778
  }
2994
3779
 
@@ -3082,16 +3867,7 @@ const liquidGL = (() => {
3082
3867
  this._compositeCtx.drawImage(meta.lastCapture, 0, 0, drawW, drawH);
3083
3868
  this._compositeCtx.restore();
3084
3869
 
3085
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
3086
- gl.texSubImage2D(
3087
- gl.TEXTURE_2D,
3088
- 0,
3089
- dstX,
3090
- dstY,
3091
- gl.RGBA,
3092
- gl.UNSIGNED_BYTE,
3093
- compositeCanvas,
3094
- );
3870
+ this.backend.uploadRegion(dstX, dstY, compositeCanvas);
3095
3871
 
3096
3872
  if (this._workerEnabled && meta._heavyAnim) {
3097
3873
  const jobId = `${Date.now()}_${Math.random()}`;
@@ -3401,6 +4177,13 @@ const liquidGL = (() => {
3401
4177
  this._revealProgress = this.revealTypeIndex === 0 ? 1 : 0;
3402
4178
  this.tiltX = 0;
3403
4179
  this.tiltY = 0;
4180
+ this._activated = false;
4181
+ }
4182
+
4183
+ /* ----------------------------- */
4184
+ _activate() {
4185
+ if (this._activated) return;
4186
+ this._activated = true;
3404
4187
 
3405
4188
  this.originalShadow = this.el.style.boxShadow;
3406
4189
  this.originalOpacity = this.el.style.opacity;
@@ -3619,6 +4402,7 @@ const liquidGL = (() => {
3619
4402
 
3620
4403
  /* ----------------------------- */
3621
4404
  _reveal() {
4405
+ if (!this._activated) return;
3622
4406
  if (this.revealTypeIndex === 0) {
3623
4407
  this.el.style.opacity = this.originalOpacity || 1;
3624
4408
  this.renderer.canvas.style.opacity = "1";
@@ -4070,342 +4854,6 @@ const liquidGL = (() => {
4070
4854
  }
4071
4855
  }
4072
4856
 
4073
- /* --------------------------------------------------
4074
- * Helper GUI System
4075
- * ------------------------------------------------*/
4076
- let helperGUIs = [];
4077
- let lilGuiLoaded = false;
4078
- let lilGuiLoadPromise = null;
4079
-
4080
- function loadLilGui() {
4081
- if (lilGuiLoaded) {
4082
- return Promise.resolve();
4083
- }
4084
- if (lilGuiLoadPromise) {
4085
- return lilGuiLoadPromise;
4086
- }
4087
-
4088
- lilGuiLoadPromise = new Promise((resolve, reject) => {
4089
- if (typeof lil !== "undefined") {
4090
- lilGuiLoaded = true;
4091
- injectHelperStyles();
4092
- resolve();
4093
- return;
4094
- }
4095
-
4096
- const script = document.createElement("script");
4097
- script.src =
4098
- "https://cdn.jsdelivr.net/npm/lil-gui@0.19.1/dist/lil-gui.umd.min.js";
4099
- script.integrity =
4100
- "sha384-2eNPNc7Cms+nVcpmQPotBpthLWCwjAGbkp0Y+3MUQqwPbmTpMFmbh2a230Gkns0x";
4101
- script.crossOrigin = "anonymous";
4102
- script.onload = () => {
4103
- lilGuiLoaded = true;
4104
- injectHelperStyles();
4105
- resolve();
4106
- };
4107
- script.onerror = () => {
4108
- reject(new Error("Failed to load lil-gui"));
4109
- };
4110
- document.head.appendChild(script);
4111
- });
4112
-
4113
- return lilGuiLoadPromise;
4114
- }
4115
-
4116
- function injectHelperStyles() {
4117
- if (document.getElementById("liquidgl-helper-styles")) return;
4118
-
4119
- const style = document.createElement("style");
4120
- style.id = "liquidgl-helper-styles";
4121
- style.textContent = `
4122
- @media screen and (max-width: 768px) {
4123
- .lil-gui.root.liquidgl-helper {
4124
- width: 61vw;
4125
- }
4126
- }
4127
-
4128
- .lil-gui.root.liquidgl-helper,
4129
- .lil-gui.liquidgl-helper .lil-gui {
4130
- --background-color: rgb(9 9 11 / 85%);
4131
- --widget-color: rgb(39 39 42 / 50%);
4132
- --hover-color: rgb(39 39 42 / 70%);
4133
- --focus-color: rgb(39 39 42 / 90%);
4134
- --number-color: #fafafa;
4135
- --string-color: #fafafa;
4136
- --font-size: 13px;
4137
- --input-font-size: 13px;
4138
- --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
4139
- --font-family-mono: monospace;
4140
- --padding: 10px;
4141
- --spacing: 10px;
4142
- --widget-height: 28px;
4143
- --title-height: 28px;
4144
- --name-width: 45%;
4145
- --slider-knob-width: 4px;
4146
- --slider-input-width: 27%;
4147
- --color-input-width: 27%;
4148
- --slider-input-min-width: 45px;
4149
- --color-input-min-width: 45px;
4150
- --folder-indent: 8px;
4151
- --widget-padding: 0 10px;
4152
- --widget-border-radius: 4px;
4153
- --checkbox-size: 16px;
4154
- --scrollbar-width: 6px;
4155
- }
4156
-
4157
- .lil-gui.root.liquidgl-helper {
4158
- border-radius: 12px !important;
4159
- border: 0.5px solid #1e1e20 !important;
4160
- backdrop-filter: blur(16px);
4161
- box-shadow: 0 4px 16px rgb(0 0 0 / 20%) !important;
4162
- position: fixed !important;
4163
- top: 1rem !important;
4164
- right: 1rem !important;
4165
- left: auto !important;
4166
- z-index: 999999999 !important;
4167
- }
4168
-
4169
- .lil-gui.liquidgl-helper .title {
4170
- background: transparent;
4171
- border-bottom: 0.5px solid #1e1e20;
4172
- border-radius: 12px 12px 0 0 !important;
4173
- }
4174
-
4175
- .lil-gui.liquidgl-helper .title button {
4176
- padding: 12px 16px !important;
4177
- }
4178
-
4179
- .lil-gui.liquidgl-helper.closed .title {
4180
- border-radius: 12px !important;
4181
- border-bottom: none;
4182
- }
4183
-
4184
- .lil-gui.liquidgl-helper .children {
4185
- border-top: 0.5px solid #1e1e20;
4186
- }
4187
-
4188
- @media (max-width: 768px) {
4189
- .lil-gui.root.liquidgl-helper {
4190
- top: 1rem !important;
4191
- right: 1rem !important;
4192
- }
4193
- }
4194
- `;
4195
- document.head.appendChild(style);
4196
- }
4197
-
4198
- function createHelperGUI(lenses, options, instanceIndex) {
4199
- if (typeof lil === "undefined") return;
4200
-
4201
- const lensList = (Array.isArray(lenses) ? lenses : [lenses]).filter(
4202
- Boolean,
4203
- );
4204
- if (!lensList.length) return;
4205
-
4206
- const gui = new lil.GUI({
4207
- title: `liquidGL Helper ${instanceIndex + 1}`,
4208
- closeFolders: true,
4209
- });
4210
- gui.domElement.classList.add("liquidgl-helper");
4211
- gui.$title.style.cursor = "default";
4212
-
4213
- const topOffset = 1 + instanceIndex * 3;
4214
- gui.domElement.style.top = `${topOffset}rem`;
4215
-
4216
- const state = lensList[0].options;
4217
-
4218
- const applyToLenses = (key, value) => {
4219
- lensList.forEach((ln) => {
4220
- if (!ln) return;
4221
- ln.options[key] = value;
4222
- if (key === "shadow") ln.setShadow(value);
4223
- if (key === "tilt") ln.setTilt(value);
4224
- });
4225
- };
4226
-
4227
- const refractionFolder = gui.addFolder("Refraction");
4228
- refractionFolder
4229
- .add(state, "refraction", 0, 0.1, 0.001)
4230
- .name("Refraction")
4231
- .onChange((v) => applyToLenses("refraction", v));
4232
- refractionFolder
4233
- .add(state, "aberration", 0, 1, 0.01)
4234
- .name("Aberration")
4235
- .onChange((v) => applyToLenses("aberration", v));
4236
- refractionFolder
4237
- .add(state, "bevelDepth", 0, 0.2, 0.001)
4238
- .name("Bevel Depth")
4239
- .onChange((v) => applyToLenses("bevelDepth", v));
4240
- refractionFolder
4241
- .add(state, "bevelWidth", 0, 0.5, 0.001)
4242
- .name("Bevel Width")
4243
- .onChange((v) => applyToLenses("bevelWidth", v));
4244
- refractionFolder
4245
- .add(state, "magnify", 1, 5, 0.1)
4246
- .name("Magnify")
4247
- .onChange((v) => applyToLenses("magnify", v));
4248
-
4249
- const surfaceFolder = gui.addFolder("Surface");
4250
- surfaceFolder
4251
- .add(state, "frost", 0, 10, 0.1)
4252
- .name("Frost")
4253
- .onChange((v) => applyToLenses("frost", v));
4254
- surfaceFolder
4255
- .add(state, "specular")
4256
- .name("Specular")
4257
- .onChange((v) => applyToLenses("specular", v));
4258
- surfaceFolder
4259
- .add(state, "shadow")
4260
- .name("Shadow")
4261
- .onChange((v) => applyToLenses("shadow", v));
4262
-
4263
- const tiltFolder = gui.addFolder("Tilt");
4264
- tiltFolder
4265
- .add(state, "tilt")
4266
- .name("Tilt")
4267
- .onChange((v) => applyToLenses("tilt", v));
4268
- tiltFolder
4269
- .add(state, "tiltFactor", 0, 25, 0.1)
4270
- .name("Tilt Factor")
4271
- .onChange((v) => applyToLenses("tiltFactor", v));
4272
- tiltFolder
4273
- .add(state, "tiltEase", 0, 1000, 10)
4274
- .name("Tilt Ease")
4275
- .onChange((v) => applyToLenses("tiltEase", v));
4276
-
4277
- const initFolder = gui.addFolder("Initialisation");
4278
- const reinitState = {
4279
- reveal: options.reveal,
4280
- resolution: options.resolution,
4281
- };
4282
-
4283
- initFolder
4284
- .add(reinitState, "reveal", ["none", "fade"])
4285
- .name("Reveal")
4286
- .onFinishChange((value) => {
4287
- if (
4288
- confirm(
4289
- "Changing reveal applies on the next initialisation. Continue?",
4290
- )
4291
- ) {
4292
- applyToLenses("reveal", value);
4293
- } else {
4294
- reinitState.reveal = options.reveal;
4295
- gui.controllersRecursive().forEach((c) => c.updateDisplay());
4296
- }
4297
- });
4298
-
4299
- initFolder
4300
- .add(reinitState, "resolution", 0.5, 3, 0.25)
4301
- .name("Resolution")
4302
- .onFinishChange((value) => {
4303
- if (
4304
- confirm(
4305
- "Changing resolution re-captures the page snapshot. Continue?",
4306
- )
4307
- ) {
4308
- applyToLenses("resolution", value);
4309
- const renderer = window.__liquidGLRenderer__;
4310
- if (renderer) {
4311
- renderer._snapshotResolution = Math.max(0.1, Math.min(3.0, value));
4312
- renderer.captureSnapshot();
4313
- }
4314
- } else {
4315
- reinitState.resolution = options.resolution;
4316
- gui.controllersRecursive().forEach((c) => c.updateDisplay());
4317
- }
4318
- });
4319
-
4320
- const copyButton = {
4321
- copySettings: () => {
4322
- const code = generateInitCode(options);
4323
- const controller = gui.controllers.find(
4324
- (c) => c.property === "copySettings",
4325
- );
4326
-
4327
- if (!controller) return;
4328
-
4329
- const originalName = controller._name;
4330
- controller.disable();
4331
- controller.name("✓ Copied");
4332
-
4333
- const copySuccess = () => {
4334
- setTimeout(() => {
4335
- controller.name(originalName);
4336
- controller.enable();
4337
- }, 1500);
4338
- };
4339
-
4340
- if (navigator.clipboard && navigator.clipboard.writeText) {
4341
- navigator.clipboard
4342
- .writeText(code)
4343
- .then(() => {
4344
- copySuccess();
4345
- })
4346
- .catch((err) => {
4347
- console.error("Clipboard error:", err);
4348
- fallbackCopy(code);
4349
- });
4350
- } else {
4351
- fallbackCopy(code);
4352
- }
4353
-
4354
- function fallbackCopy(text) {
4355
- const textarea = document.createElement("textarea");
4356
- textarea.value = text;
4357
- textarea.style.position = "fixed";
4358
- textarea.style.opacity = "0";
4359
- document.body.appendChild(textarea);
4360
- textarea.select();
4361
- try {
4362
- document.execCommand("copy");
4363
- copySuccess();
4364
- } catch (err) {
4365
- console.error("Copy failed:", err);
4366
- controller.name(originalName);
4367
- controller.enable();
4368
- prompt("Copy this code manually:", text);
4369
- }
4370
- document.body.removeChild(textarea);
4371
- }
4372
- },
4373
- };
4374
- gui.add(copyButton, "copySettings").name("Copy settings");
4375
-
4376
- refractionFolder.close();
4377
- surfaceFolder.close();
4378
- tiltFolder.close();
4379
- initFolder.close();
4380
-
4381
- helperGUIs.push({ gui, lenses: lensList });
4382
- return gui;
4383
- }
4384
-
4385
- function generateInitCode(options) {
4386
- const lines = ["liquidGL({"];
4387
-
4388
- lines.push(` target: "${options.target}",`);
4389
- lines.push(` snapshot: "${options.snapshot}",`);
4390
- lines.push(` resolution: ${options.resolution},`);
4391
- lines.push(` refraction: ${options.refraction},`);
4392
- lines.push(` aberration: ${options.aberration},`);
4393
- lines.push(` bevelDepth: ${options.bevelDepth},`);
4394
- lines.push(` bevelWidth: ${options.bevelWidth},`);
4395
- lines.push(` frost: ${options.frost},`);
4396
- lines.push(` shadow: ${options.shadow},`);
4397
- lines.push(` specular: ${options.specular},`);
4398
- lines.push(` reveal: "${options.reveal}",`);
4399
- lines.push(` tilt: ${options.tilt},`);
4400
- lines.push(` tiltFactor: ${options.tiltFactor},`);
4401
- lines.push(` tiltEase: ${options.tiltEase},`);
4402
- lines.push(` magnify: ${options.magnify},`);
4403
- lines.push(` helper: false,`);
4404
- lines.push(`});`);
4405
-
4406
- return lines.join("\n");
4407
- }
4408
-
4409
4857
  /* --------------------------------------------------
4410
4858
  * Public API
4411
4859
  * ------------------------------------------------*/
@@ -4414,6 +4862,7 @@ const liquidGL = (() => {
4414
4862
  target: ".liquidGL",
4415
4863
  snapshot: "body",
4416
4864
  resolution: 2.0,
4865
+ engine: "auto",
4417
4866
  refraction: 0.01,
4418
4867
  aberration: 0,
4419
4868
  bevelDepth: 0.08,
@@ -4431,20 +4880,40 @@ const liquidGL = (() => {
4431
4880
  };
4432
4881
  const options = { ...defaults, ...userOptions };
4433
4882
 
4434
- if (typeof window.__liquidGLNoWebGL__ === "undefined") {
4435
- const testCanvas = document.createElement("canvas");
4436
- const testCtx =
4437
- testCanvas.getContext("webgl2") ||
4438
- testCanvas.getContext("webgl") ||
4439
- testCanvas.getContext("experimental-webgl");
4440
- window.__liquidGLNoWebGL__ = !testCtx;
4883
+ const engineRaw =
4884
+ options.engine !== "auto"
4885
+ ? options.engine
4886
+ : new URLSearchParams(window.location.search).get("liquidGL-engine") ||
4887
+ "auto";
4888
+ const engineKey = String(engineRaw).toLowerCase();
4889
+ if (ENGINE_CHAINS[engineKey]) {
4890
+ options.engine = engineKey;
4891
+ } else {
4892
+ console.warn(`liquidGL: Unknown engine "${engineRaw}" – using "auto".`);
4893
+ options.engine = "auto";
4441
4894
  }
4442
4895
 
4443
- const noWebGL = window.__liquidGLNoWebGL__;
4896
+ const chain = ENGINE_CHAINS[options.engine];
4897
+ const hasWebGPU =
4898
+ chain[0] === "webgpu" &&
4899
+ typeof navigator !== "undefined" &&
4900
+ "gpu" in navigator;
4901
+ let hasWebGL = false;
4902
+ const glChain = chain.filter((c) => c !== "webgpu");
4903
+ if (glChain.length) {
4904
+ const testCanvas = document.createElement("canvas");
4905
+ for (const name of glChain) {
4906
+ if (testCanvas.getContext(name)) {
4907
+ hasWebGL = true;
4908
+ break;
4909
+ }
4910
+ }
4911
+ }
4912
+ const noGPU = !hasWebGPU && !hasWebGL;
4444
4913
 
4445
- if (noWebGL) {
4914
+ if (window.__liquidGLNoWebGL__ === true || noGPU) {
4446
4915
  console.warn(
4447
- "liquidGL: WebGL not available – falling back to CSS backdrop-filter.",
4916
+ "liquidGL: WebGPU/WebGL not available – falling back to CSS backdrop-filter.",
4448
4917
  );
4449
4918
  const fallbackNodes = document.querySelectorAll(options.target);
4450
4919
  fallbackNodes.forEach((node) => {
@@ -4461,7 +4930,11 @@ const liquidGL = (() => {
4461
4930
 
4462
4931
  let renderer = window.__liquidGLRenderer__;
4463
4932
  if (!renderer) {
4464
- renderer = new liquidGLRenderer(options.snapshot, options.resolution);
4933
+ renderer = new liquidGLRenderer(
4934
+ options.snapshot,
4935
+ options.resolution,
4936
+ options.engine,
4937
+ );
4465
4938
  window.__liquidGLRenderer__ = renderer;
4466
4939
  }
4467
4940
 
@@ -4486,14 +4959,13 @@ const liquidGL = (() => {
4486
4959
  }
4487
4960
 
4488
4961
  if (options.helper) {
4489
- loadLilGui()
4490
- .then(() => {
4491
- const instanceIndex = helperGUIs.length;
4492
- createHelperGUI(instances, options, instanceIndex);
4493
- })
4494
- .catch((err) => {
4495
- console.error("liquidGL: Failed to load helper GUI:", err);
4496
- });
4962
+ if (typeof window.__liquidGLHelper__ === "function") {
4963
+ window.__liquidGLHelper__(instances, options);
4964
+ } else {
4965
+ console.error(
4966
+ "liquidGL Helper Not Found - ensure liquidGL-helper.js is available in your project",
4967
+ );
4968
+ }
4497
4969
  }
4498
4970
 
4499
4971
  return instances.length === 1 ? instances[0] : instances;
@@ -4598,6 +5070,7 @@ const liquidGL = (() => {
4598
5070
 
4599
5071
  return { lenis, locomotiveScroll: loco };
4600
5072
  };
5073
+
4601
5074
  return window.liquidGL;
4602
5075
  })();
4603
5076