liquid-gl 2.1.0 → 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;
@@ -2064,6 +1928,8 @@ const liquidGL = (() => {
2064
1928
  vec2 texel = 1.0 / u_textureResolution;
2065
1929
  vec4 refrCol;
2066
1930
 
1931
+ vec2 chroma = offset * u_aberration;
1932
+
2067
1933
  if (u_frost > 0.0) {
2068
1934
  float radius = u_frost * 4.0;
2069
1935
  vec4 sum = vec4(0.0);
@@ -2072,8 +1938,15 @@ const liquidGL = (() => {
2072
1938
  for (int i = 0; i < SAMPLES; i++) {
2073
1939
  float angle = random(v_uv + float(i)) * 6.283185;
2074
1940
  float dist = sqrt(random(v_uv - float(i))) * radius;
2075
- vec2 offset = vec2(cos(angle), sin(angle)) * texel * dist;
2076
- sum += texture2D(u_tex, sampleUV + offset);
1941
+ vec2 foff = vec2(cos(angle), sin(angle)) * texel * dist;
1942
+ if (u_aberration > 0.0) {
1943
+ sum.r += texture2D(u_tex, sampleUV + foff - chroma).r;
1944
+ sum.g += texture2D(u_tex, sampleUV + foff).g;
1945
+ sum.b += texture2D(u_tex, sampleUV + foff + chroma).b;
1946
+ sum.a += texture2D(u_tex, sampleUV + foff).a;
1947
+ } else {
1948
+ sum += texture2D(u_tex, sampleUV + foff);
1949
+ }
2077
1950
  }
2078
1951
  refrCol = sum / float(SAMPLES);
2079
1952
  } else {
@@ -2083,12 +1956,11 @@ const liquidGL = (() => {
2083
1956
  refrCol += texture2D(u_tex, sampleUV + vec2(0.0, texel.y));
2084
1957
  refrCol += texture2D(u_tex, sampleUV + vec2(0.0, -texel.y));
2085
1958
  refrCol /= 5.0;
2086
- }
2087
1959
 
2088
- if (u_aberration > 0.0) {
2089
- vec2 chroma = offset * u_aberration;
2090
- refrCol.r = texture2D(u_tex, sampleUV - chroma).r;
2091
- refrCol.b = texture2D(u_tex, sampleUV + chroma).b;
1960
+ if (u_aberration > 0.0) {
1961
+ refrCol.r = texture2D(u_tex, sampleUV - chroma).r;
1962
+ refrCol.b = texture2D(u_tex, sampleUV + chroma).b;
1963
+ }
2092
1964
  }
2093
1965
 
2094
1966
  if (refrCol.a < 0.1) {
@@ -2124,8 +1996,8 @@ const liquidGL = (() => {
2124
1996
  gl_FragColor = final;
2125
1997
  }`;
2126
1998
 
2127
- this.program = createProgram(this.gl, vsSource, fsSource);
2128
1999
  const gl = this.gl;
2000
+ this.program = createProgram(gl, vsSource, fsSource);
2129
2001
  if (!this.program) throw new Error("liquidGL: Shader failed");
2130
2002
 
2131
2003
  const posBuf = gl.createBuffer();
@@ -2169,68 +2041,1153 @@ const liquidGL = (() => {
2169
2041
  };
2170
2042
  }
2171
2043
 
2172
- /* ----------------------------- */
2173
- _resizeCanvas() {
2174
- const dpr = Math.min(2, window.devicePixelRatio || 1);
2175
- this.canvas.width = innerWidth * dpr;
2176
- this.canvas.height = innerHeight * dpr;
2177
- this.canvas.style.width = `${innerWidth}px`;
2178
- this.canvas.style.height = `${innerHeight}px`;
2044
+ resize() {
2179
2045
  this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
2180
2046
  }
2181
2047
 
2182
- /* ----------------------------- */
2183
- async captureSnapshot() {
2184
- if (this._capturing) return;
2185
- this._capturing = true;
2186
-
2187
- const undos = [];
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
+ }
2188
2068
 
2189
- const attemptCapture = async (
2190
- attempt = 1,
2191
- maxAttempts = 3,
2192
- delayMs = 500,
2193
- ) => {
2194
- try {
2195
- const fullW = this.snapshotTarget.scrollWidth;
2196
- const fullH = this.snapshotTarget.scrollHeight;
2197
- const maxTex = this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE) || 8192;
2198
- const MAX_MOBILE_DIM = 4096;
2199
- const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.userAgent);
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
+ }
2200
2084
 
2201
- let scale = Math.min(
2202
- this._snapshotResolution,
2203
- maxTex / fullW,
2204
- maxTex / fullH,
2205
- );
2085
+ _initVideoBlit() {
2086
+ if (this._vBlitReady !== undefined) return this._vBlitReady;
2206
2087
 
2207
- if (isMobileSafari) {
2208
- const over = (Math.max(fullW, fullH) * scale) / MAX_MOBILE_DIM;
2209
- if (over > 1) scale = scale / over;
2210
- }
2088
+ const gl = this.gl;
2211
2089
 
2212
- const maxArea = isMobileSafari ? 4096 * 4096 : 16384 * 16384;
2213
- if (fullW * fullH * scale * scale > maxArea) {
2214
- scale = Math.sqrt(maxArea / (fullW * fullH));
2215
- console.warn(
2216
- `liquidGL: snapshot area capped, resolution reduced to ${scale.toFixed(
2217
- 3,
2218
- )} for a ${fullW}x${fullH} document.`,
2219
- );
2220
- }
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
+ }`;
2221
2097
 
2222
- this.scaleFactor = Math.max(0.1, scale);
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
+ }`;
2223
2106
 
2224
- this.canvas.style.visibility = "hidden";
2225
- undos.push(() => (this.canvas.style.visibility = "visible"));
2107
+ const prog = createProgram(gl, vs, fs);
2108
+ if (!prog) {
2109
+ this._vBlitReady = false;
2110
+ return false;
2111
+ }
2226
2112
 
2227
- const lensElements = this.lenses
2228
- .flatMap((lens) => [lens.el, lens._shadowEl])
2229
- .filter(Boolean);
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
+ };
2230
2119
 
2231
- lensElements.forEach((el) => {
2232
- el.setAttribute("data-liquidgl-hide", "");
2233
- undos.push(() => {
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);
3187
+
3188
+ lensElements.forEach((el) => {
3189
+ el.setAttribute("data-liquidgl-hide", "");
3190
+ undos.push(() => {
2234
3191
  el.removeAttribute("data-liquidgl-hide");
2235
3192
  });
2236
3193
  });
@@ -2327,30 +3284,16 @@ const liquidGL = (() => {
2327
3284
  return false;
2328
3285
  }
2329
3286
  this.staticSnapshotCanvas = srcCanvas;
2330
- const gl = this.gl;
2331
- if (!this.texture) this.texture = gl.createTexture();
2332
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2333
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2334
- gl.texImage2D(
2335
- gl.TEXTURE_2D,
2336
- 0,
2337
- gl.RGBA,
2338
- gl.RGBA,
2339
- gl.UNSIGNED_BYTE,
2340
- srcCanvas,
2341
- );
2342
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2343
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2344
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2345
- 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
+ }
2346
3290
 
3291
+ this.hasTexture = true;
2347
3292
  this.textureWidth = srcCanvas.width;
2348
3293
  this.textureHeight = srcCanvas.height;
2349
3294
 
2350
3295
  if (this._videoFrameState) this._videoFrameState = new WeakMap();
2351
3296
 
2352
- this._vFboTexture = null;
2353
-
2354
3297
  this.render();
2355
3298
 
2356
3299
  if (this._pendingReveal.length) {
@@ -2371,7 +3314,13 @@ const liquidGL = (() => {
2371
3314
  this.canvas.style.zIndex = maxZ - 1;
2372
3315
  }
2373
3316
 
2374
- if (!this.texture) {
3317
+ if (this.backend) {
3318
+ lens._activate();
3319
+ } else {
3320
+ this._pendingLensActivation.push(lens);
3321
+ }
3322
+
3323
+ if (!this.hasTexture) {
2375
3324
  this._pendingReveal.push(lens);
2376
3325
  } else {
2377
3326
  lens._reveal();
@@ -2381,8 +3330,8 @@ const liquidGL = (() => {
2381
3330
 
2382
3331
  /* ----------------------------- */
2383
3332
  render() {
2384
- const gl = this.gl;
2385
- if (!this.texture) return;
3333
+ const backend = this.backend;
3334
+ if (!backend || !this.hasTexture) return;
2386
3335
 
2387
3336
  this.lenses.forEach((ln) => {
2388
3337
  if (ln._isSticky && !ln._mirrorActive) ln.updateMetrics();
@@ -2392,15 +3341,9 @@ const liquidGL = (() => {
2392
3341
  this._scrollUpdateCounter++;
2393
3342
  }
2394
3343
 
2395
- gl.clearColor(0, 0, 0, 0);
2396
- gl.clear(gl.COLOR_BUFFER_BIT);
2397
- gl.useProgram(this.program);
2398
- gl.activeTexture(gl.TEXTURE0);
2399
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2400
- gl.uniform1i(this.u.tex, 0);
2401
-
2402
3344
  const time = (Date.now() - this.startTime) / 1000;
2403
- gl.uniform1f(this.u.time, time);
3345
+
3346
+ backend.beginFrame(this.canvas.width, this.canvas.height, time);
2404
3347
 
2405
3348
  this._updateDynamicVideos();
2406
3349
 
@@ -2416,6 +3359,8 @@ const liquidGL = (() => {
2416
3359
  this._renderLens(lens);
2417
3360
  });
2418
3361
 
3362
+ backend.endFrame();
3363
+
2419
3364
  this.lenses.forEach((ln) => {
2420
3365
  if (ln._mirrorActive && ln._mirrorCtx) {
2421
3366
  const mirror = ln._mirror;
@@ -2431,6 +3376,7 @@ const liquidGL = (() => {
2431
3376
  });
2432
3377
 
2433
3378
  const dpr = Math.min(2, window.devicePixelRatio || 1);
3379
+ const clearRects = [];
2434
3380
  this.lenses.forEach((ln) => {
2435
3381
  if (ln._mirrorActive && ln.rectPx) {
2436
3382
  const { left, top, width, height } = ln.rectPx;
@@ -2449,19 +3395,15 @@ const liquidGL = (() => {
2449
3395
  Math.round(height * dpr) + expand * 2,
2450
3396
  );
2451
3397
  if (w > 0 && h > 0) {
2452
- gl.enable(gl.SCISSOR_TEST);
2453
- gl.scissor(x, y, w, h);
2454
- gl.clearColor(0, 0, 0, 0);
2455
- gl.clear(gl.COLOR_BUFFER_BIT);
2456
- gl.disable(gl.SCISSOR_TEST);
3398
+ clearRects.push({ x, y, w, h });
2457
3399
  }
2458
3400
  }
2459
3401
  });
3402
+ backend.clearRegions(clearRects);
2460
3403
  }
2461
3404
 
2462
3405
  /* ----------------------------- */
2463
3406
  _renderLens(lens) {
2464
- const gl = this.gl;
2465
3407
  const rect = lens.rectPx;
2466
3408
  if (!rect) return;
2467
3409
 
@@ -2484,11 +3426,6 @@ const liquidGL = (() => {
2484
3426
  const h = Math.round(topPx + rect.height * dpr) - yTop;
2485
3427
  const y = this.canvas.height - (yTop + h);
2486
3428
 
2487
- gl.viewport(x, y, w, h);
2488
- gl.uniform2f(this.u.res, w, h);
2489
- gl.uniform2f(this.u.subpixel, leftPx - x, topPx - yTop);
2490
- gl.uniform2f(this.u.boxSize, rect.width * dpr, rect.height * dpr);
2491
-
2492
3429
  const snapRect =
2493
3430
  this._frameSnapRect || this.snapshotTarget.getBoundingClientRect();
2494
3431
  const docX = rect.left - snapRect.left;
@@ -2497,22 +3434,6 @@ const liquidGL = (() => {
2497
3434
  const topUV = (docY * this.scaleFactor) / this.textureHeight;
2498
3435
  const wUV = (rect.width * this.scaleFactor) / this.textureWidth;
2499
3436
  const hUV = (rect.height * this.scaleFactor) / this.textureHeight;
2500
- gl.uniform4f(this.u.bounds, leftUV, topUV, wUV, hUV);
2501
-
2502
- gl.uniform2f(
2503
- this.u.textureResolution,
2504
- this.textureWidth,
2505
- this.textureHeight,
2506
- );
2507
- gl.uniform1f(this.u.refraction, lens.options.refraction);
2508
- gl.uniform1f(this.u.aberration, lens.options.aberration || 0);
2509
- gl.uniform1f(this.u.bevelDepth, lens.options.bevelDepth);
2510
- gl.uniform1f(this.u.bevelWidth, lens.options.bevelWidth);
2511
- gl.uniform1f(this.u.frost, lens.options.frost);
2512
- gl.uniform1f(this.u.radius, lens.radiusGl);
2513
- gl.uniform1i(this.u.specular, lens.options.specular ? 1 : 0);
2514
- gl.uniform1f(this.u.revealProgress, lens._revealProgress || 1.0);
2515
- gl.uniform1i(this.u.revealType, lens.revealTypeIndex || 0);
2516
3437
 
2517
3438
  const mag = Math.max(
2518
3439
  0.001,
@@ -2521,12 +3442,32 @@ const liquidGL = (() => {
2521
3442
  lens.options.magnify !== undefined ? lens.options.magnify : 1.0,
2522
3443
  ),
2523
3444
  );
2524
- gl.uniform1f(this.u.magnify, mag);
2525
-
2526
- gl.uniform1f(this.u.tiltX, lens.tiltX || 0);
2527
- gl.uniform1f(this.u.tiltY, lens.tiltY || 0);
2528
3445
 
2529
- 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
+ });
2530
3471
  }
2531
3472
 
2532
3473
  /* ----------------------------- */
@@ -2544,56 +3485,6 @@ const liquidGL = (() => {
2544
3485
  ctx.closePath();
2545
3486
  }
2546
3487
 
2547
- _initVideoBlit() {
2548
- if (this._vBlitReady !== undefined) return this._vBlitReady;
2549
-
2550
- const gl = this.gl;
2551
-
2552
- const vs = `
2553
- attribute vec2 a_position;
2554
- varying vec2 v_uv;
2555
- void main(){
2556
- v_uv = (a_position + 1.0) * 0.5;
2557
- gl_Position = vec4(a_position, 0.0, 1.0);
2558
- }`;
2559
-
2560
- const fs = `
2561
- precision mediump float;
2562
- varying vec2 v_uv;
2563
- uniform sampler2D u_src;
2564
- uniform vec4 u_srcRect;
2565
- void main(){
2566
- gl_FragColor = texture2D(u_src, u_srcRect.xy + v_uv * u_srcRect.zw);
2567
- }`;
2568
-
2569
- const prog = createProgram(gl, vs, fs);
2570
- if (!prog) {
2571
- this._vBlitReady = false;
2572
- return false;
2573
- }
2574
-
2575
- this._vProg = prog;
2576
- this._vPosLoc = gl.getAttribLocation(prog, "a_position");
2577
- this._vU = {
2578
- src: gl.getUniformLocation(prog, "u_src"),
2579
- srcRect: gl.getUniformLocation(prog, "u_srcRect"),
2580
- };
2581
-
2582
- this._vTex = gl.createTexture();
2583
- gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2584
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2585
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2586
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2587
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2588
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2589
-
2590
- this._vFbo = gl.createFramebuffer();
2591
- this._vFboTexture = null;
2592
-
2593
- this._vBlitReady = true;
2594
- return true;
2595
- }
2596
-
2597
3488
  _videoIsOpaque(vid) {
2598
3489
  if (!vid.videoWidth || !vid.videoHeight) return false;
2599
3490
 
@@ -2628,92 +3519,15 @@ const liquidGL = (() => {
2628
3519
  return opaque;
2629
3520
  }
2630
3521
 
2631
- _blitVideoToTexture(vid, dstX, dstY, dstW, dstH, srcRect) {
2632
- const gl = this.gl;
2633
-
2634
- gl.bindFramebuffer(gl.FRAMEBUFFER, this._vFbo);
2635
-
2636
- if (this._vFboTexture !== this.texture) {
2637
- gl.framebufferTexture2D(
2638
- gl.FRAMEBUFFER,
2639
- gl.COLOR_ATTACHMENT0,
2640
- gl.TEXTURE_2D,
2641
- this.texture,
2642
- 0,
2643
- );
2644
- if (
2645
- gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE
2646
- ) {
2647
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2648
- this._vBlitReady = false;
2649
- return false;
2650
- }
2651
- this._vFboTexture = this.texture;
2652
- }
2653
-
2654
- gl.activeTexture(gl.TEXTURE0);
2655
- gl.bindTexture(gl.TEXTURE_2D, this._vTex);
2656
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2657
-
2658
- try {
2659
- gl.texImage2D(
2660
- gl.TEXTURE_2D,
2661
- 0,
2662
- gl.RGBA,
2663
- gl.RGBA,
2664
- gl.UNSIGNED_BYTE,
2665
- vid,
2666
- );
2667
- } catch (e) {
2668
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2669
- this._restoreLensProgramState();
2670
- return false;
2671
- }
2672
-
2673
- gl.useProgram(this._vProg);
2674
- gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2675
- gl.enableVertexAttribArray(this._vPosLoc);
2676
- gl.vertexAttribPointer(this._vPosLoc, 2, gl.FLOAT, false, 0, 0);
2677
-
2678
- gl.uniform1i(this._vU.src, 0);
2679
- gl.uniform4f(
2680
- this._vU.srcRect,
2681
- srcRect.u,
2682
- srcRect.v,
2683
- srcRect.uw,
2684
- srcRect.vh,
2685
- );
2686
-
2687
- gl.viewport(dstX, dstY, dstW, dstH);
2688
- gl.drawArrays(gl.TRIANGLES, 0, 6);
2689
-
2690
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2691
- this._restoreLensProgramState();
2692
-
2693
- return true;
2694
- }
2695
-
2696
- _restoreLensProgramState() {
2697
- const gl = this.gl;
2698
- gl.useProgram(this.program);
2699
- gl.bindBuffer(gl.ARRAY_BUFFER, this._posBuf);
2700
- gl.enableVertexAttribArray(this._posLoc);
2701
- gl.vertexAttribPointer(this._posLoc, 2, gl.FLOAT, false, 0, 0);
2702
- gl.activeTexture(gl.TEXTURE0);
2703
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2704
- gl.uniform1i(this.u.tex, 0);
2705
- }
2706
-
2707
3522
  /* ----------------------------- */
2708
3523
  _updateDynamicVideos() {
2709
3524
  if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
2710
3525
  if (
2711
- !this.texture ||
3526
+ !this.hasTexture ||
2712
3527
  !this.staticSnapshotCanvas ||
2713
3528
  !this._videoNodes.length
2714
3529
  )
2715
3530
  return;
2716
- const gl = this.gl;
2717
3531
 
2718
3532
  const snapRect = this.snapshotTarget.getBoundingClientRect();
2719
3533
 
@@ -2795,9 +3609,8 @@ const liquidGL = (() => {
2795
3609
 
2796
3610
  if (
2797
3611
  !isRounded &&
2798
- this._initVideoBlit() &&
2799
3612
  this._videoIsOpaque(vid) &&
2800
- this._blitVideoToTexture(vid, dstX, dstY, updW, updH, {
3613
+ this.backend.blitVideo(vid, dstX, dstY, updW, updH, {
2801
3614
  u: srcX / drawW,
2802
3615
  v: srcY / drawH,
2803
3616
  uw: updW / drawW,
@@ -2848,25 +3661,14 @@ const liquidGL = (() => {
2848
3661
  return;
2849
3662
  }
2850
3663
 
2851
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2852
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2853
- gl.texSubImage2D(
2854
- gl.TEXTURE_2D,
2855
- 0,
2856
- dstX,
2857
- dstY,
2858
- gl.RGBA,
2859
- gl.UNSIGNED_BYTE,
2860
- this._tmpCanvas,
2861
- );
3664
+ this.backend.uploadRegion(dstX, dstY, this._tmpCanvas);
2862
3665
  });
2863
3666
  }
2864
3667
 
2865
3668
  /* ----------------------------- */
2866
3669
  _updateDynamicNodes() {
2867
3670
  if (this._isScrolling && this._scrollUpdateCounter % 2 !== 0) return;
2868
- const gl = this.gl;
2869
- if (!this.texture || !this._dynMeta) return;
3671
+ if (!this.hasTexture || !this._dynMeta) return;
2870
3672
  const snapRect = this.snapshotTarget.getBoundingClientRect();
2871
3673
  const maxLensZ = this._getMaxLensZ();
2872
3674
 
@@ -2971,16 +3773,7 @@ const liquidGL = (() => {
2971
3773
  w,
2972
3774
  h,
2973
3775
  );
2974
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
2975
- gl.texSubImage2D(
2976
- gl.TEXTURE_2D,
2977
- 0,
2978
- x,
2979
- y,
2980
- gl.RGBA,
2981
- gl.UNSIGNED_BYTE,
2982
- eraseCanvas,
2983
- );
3776
+ this.backend.uploadRegion(x, y, eraseCanvas);
2984
3777
  }
2985
3778
  }
2986
3779
 
@@ -3074,16 +3867,7 @@ const liquidGL = (() => {
3074
3867
  this._compositeCtx.drawImage(meta.lastCapture, 0, 0, drawW, drawH);
3075
3868
  this._compositeCtx.restore();
3076
3869
 
3077
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
3078
- gl.texSubImage2D(
3079
- gl.TEXTURE_2D,
3080
- 0,
3081
- dstX,
3082
- dstY,
3083
- gl.RGBA,
3084
- gl.UNSIGNED_BYTE,
3085
- compositeCanvas,
3086
- );
3870
+ this.backend.uploadRegion(dstX, dstY, compositeCanvas);
3087
3871
 
3088
3872
  if (this._workerEnabled && meta._heavyAnim) {
3089
3873
  const jobId = `${Date.now()}_${Math.random()}`;
@@ -3393,6 +4177,13 @@ const liquidGL = (() => {
3393
4177
  this._revealProgress = this.revealTypeIndex === 0 ? 1 : 0;
3394
4178
  this.tiltX = 0;
3395
4179
  this.tiltY = 0;
4180
+ this._activated = false;
4181
+ }
4182
+
4183
+ /* ----------------------------- */
4184
+ _activate() {
4185
+ if (this._activated) return;
4186
+ this._activated = true;
3396
4187
 
3397
4188
  this.originalShadow = this.el.style.boxShadow;
3398
4189
  this.originalOpacity = this.el.style.opacity;
@@ -3611,6 +4402,7 @@ const liquidGL = (() => {
3611
4402
 
3612
4403
  /* ----------------------------- */
3613
4404
  _reveal() {
4405
+ if (!this._activated) return;
3614
4406
  if (this.revealTypeIndex === 0) {
3615
4407
  this.el.style.opacity = this.originalOpacity || 1;
3616
4408
  this.renderer.canvas.style.opacity = "1";
@@ -4062,342 +4854,6 @@ const liquidGL = (() => {
4062
4854
  }
4063
4855
  }
4064
4856
 
4065
- /* --------------------------------------------------
4066
- * Helper GUI System
4067
- * ------------------------------------------------*/
4068
- let helperGUIs = [];
4069
- let lilGuiLoaded = false;
4070
- let lilGuiLoadPromise = null;
4071
-
4072
- function loadLilGui() {
4073
- if (lilGuiLoaded) {
4074
- return Promise.resolve();
4075
- }
4076
- if (lilGuiLoadPromise) {
4077
- return lilGuiLoadPromise;
4078
- }
4079
-
4080
- lilGuiLoadPromise = new Promise((resolve, reject) => {
4081
- if (typeof lil !== "undefined") {
4082
- lilGuiLoaded = true;
4083
- injectHelperStyles();
4084
- resolve();
4085
- return;
4086
- }
4087
-
4088
- const script = document.createElement("script");
4089
- script.src =
4090
- "https://cdn.jsdelivr.net/npm/lil-gui@0.19.1/dist/lil-gui.umd.min.js";
4091
- script.integrity =
4092
- "sha384-2eNPNc7Cms+nVcpmQPotBpthLWCwjAGbkp0Y+3MUQqwPbmTpMFmbh2a230Gkns0x";
4093
- script.crossOrigin = "anonymous";
4094
- script.onload = () => {
4095
- lilGuiLoaded = true;
4096
- injectHelperStyles();
4097
- resolve();
4098
- };
4099
- script.onerror = () => {
4100
- reject(new Error("Failed to load lil-gui"));
4101
- };
4102
- document.head.appendChild(script);
4103
- });
4104
-
4105
- return lilGuiLoadPromise;
4106
- }
4107
-
4108
- function injectHelperStyles() {
4109
- if (document.getElementById("liquidgl-helper-styles")) return;
4110
-
4111
- const style = document.createElement("style");
4112
- style.id = "liquidgl-helper-styles";
4113
- style.textContent = `
4114
- @media screen and (max-width: 768px) {
4115
- .lil-gui.root.liquidgl-helper {
4116
- width: 61vw;
4117
- }
4118
- }
4119
-
4120
- .lil-gui.root.liquidgl-helper,
4121
- .lil-gui.liquidgl-helper .lil-gui {
4122
- --background-color: rgb(9 9 11 / 85%);
4123
- --widget-color: rgb(39 39 42 / 50%);
4124
- --hover-color: rgb(39 39 42 / 70%);
4125
- --focus-color: rgb(39 39 42 / 90%);
4126
- --number-color: #fafafa;
4127
- --string-color: #fafafa;
4128
- --font-size: 13px;
4129
- --input-font-size: 13px;
4130
- --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
4131
- --font-family-mono: monospace;
4132
- --padding: 10px;
4133
- --spacing: 10px;
4134
- --widget-height: 28px;
4135
- --title-height: 28px;
4136
- --name-width: 45%;
4137
- --slider-knob-width: 4px;
4138
- --slider-input-width: 27%;
4139
- --color-input-width: 27%;
4140
- --slider-input-min-width: 45px;
4141
- --color-input-min-width: 45px;
4142
- --folder-indent: 8px;
4143
- --widget-padding: 0 10px;
4144
- --widget-border-radius: 4px;
4145
- --checkbox-size: 16px;
4146
- --scrollbar-width: 6px;
4147
- }
4148
-
4149
- .lil-gui.root.liquidgl-helper {
4150
- border-radius: 12px !important;
4151
- border: 0.5px solid #1e1e20 !important;
4152
- backdrop-filter: blur(16px);
4153
- box-shadow: 0 4px 16px rgb(0 0 0 / 20%) !important;
4154
- position: fixed !important;
4155
- top: 1rem !important;
4156
- right: 1rem !important;
4157
- left: auto !important;
4158
- z-index: 999999999 !important;
4159
- }
4160
-
4161
- .lil-gui.liquidgl-helper .title {
4162
- background: transparent;
4163
- border-bottom: 0.5px solid #1e1e20;
4164
- border-radius: 12px 12px 0 0 !important;
4165
- }
4166
-
4167
- .lil-gui.liquidgl-helper .title button {
4168
- padding: 12px 16px !important;
4169
- }
4170
-
4171
- .lil-gui.liquidgl-helper.closed .title {
4172
- border-radius: 12px !important;
4173
- border-bottom: none;
4174
- }
4175
-
4176
- .lil-gui.liquidgl-helper .children {
4177
- border-top: 0.5px solid #1e1e20;
4178
- }
4179
-
4180
- @media (max-width: 768px) {
4181
- .lil-gui.root.liquidgl-helper {
4182
- top: 1rem !important;
4183
- right: 1rem !important;
4184
- }
4185
- }
4186
- `;
4187
- document.head.appendChild(style);
4188
- }
4189
-
4190
- function createHelperGUI(lenses, options, instanceIndex) {
4191
- if (typeof lil === "undefined") return;
4192
-
4193
- const lensList = (Array.isArray(lenses) ? lenses : [lenses]).filter(
4194
- Boolean,
4195
- );
4196
- if (!lensList.length) return;
4197
-
4198
- const gui = new lil.GUI({
4199
- title: `liquidGL Helper ${instanceIndex + 1}`,
4200
- closeFolders: true,
4201
- });
4202
- gui.domElement.classList.add("liquidgl-helper");
4203
- gui.$title.style.cursor = "default";
4204
-
4205
- const topOffset = 1 + instanceIndex * 3;
4206
- gui.domElement.style.top = `${topOffset}rem`;
4207
-
4208
- const state = lensList[0].options;
4209
-
4210
- const applyToLenses = (key, value) => {
4211
- lensList.forEach((ln) => {
4212
- if (!ln) return;
4213
- ln.options[key] = value;
4214
- if (key === "shadow") ln.setShadow(value);
4215
- if (key === "tilt") ln.setTilt(value);
4216
- });
4217
- };
4218
-
4219
- const refractionFolder = gui.addFolder("Refraction");
4220
- refractionFolder
4221
- .add(state, "refraction", 0, 0.1, 0.001)
4222
- .name("Refraction")
4223
- .onChange((v) => applyToLenses("refraction", v));
4224
- refractionFolder
4225
- .add(state, "aberration", 0, 1, 0.01)
4226
- .name("Aberration")
4227
- .onChange((v) => applyToLenses("aberration", v));
4228
- refractionFolder
4229
- .add(state, "bevelDepth", 0, 0.2, 0.001)
4230
- .name("Bevel Depth")
4231
- .onChange((v) => applyToLenses("bevelDepth", v));
4232
- refractionFolder
4233
- .add(state, "bevelWidth", 0, 0.5, 0.001)
4234
- .name("Bevel Width")
4235
- .onChange((v) => applyToLenses("bevelWidth", v));
4236
- refractionFolder
4237
- .add(state, "magnify", 1, 5, 0.1)
4238
- .name("Magnify")
4239
- .onChange((v) => applyToLenses("magnify", v));
4240
-
4241
- const surfaceFolder = gui.addFolder("Surface");
4242
- surfaceFolder
4243
- .add(state, "frost", 0, 10, 0.1)
4244
- .name("Frost")
4245
- .onChange((v) => applyToLenses("frost", v));
4246
- surfaceFolder
4247
- .add(state, "specular")
4248
- .name("Specular")
4249
- .onChange((v) => applyToLenses("specular", v));
4250
- surfaceFolder
4251
- .add(state, "shadow")
4252
- .name("Shadow")
4253
- .onChange((v) => applyToLenses("shadow", v));
4254
-
4255
- const tiltFolder = gui.addFolder("Tilt");
4256
- tiltFolder
4257
- .add(state, "tilt")
4258
- .name("Tilt")
4259
- .onChange((v) => applyToLenses("tilt", v));
4260
- tiltFolder
4261
- .add(state, "tiltFactor", 0, 25, 0.1)
4262
- .name("Tilt Factor")
4263
- .onChange((v) => applyToLenses("tiltFactor", v));
4264
- tiltFolder
4265
- .add(state, "tiltEase", 0, 1000, 10)
4266
- .name("Tilt Ease")
4267
- .onChange((v) => applyToLenses("tiltEase", v));
4268
-
4269
- const initFolder = gui.addFolder("Initialisation");
4270
- const reinitState = {
4271
- reveal: options.reveal,
4272
- resolution: options.resolution,
4273
- };
4274
-
4275
- initFolder
4276
- .add(reinitState, "reveal", ["none", "fade"])
4277
- .name("Reveal")
4278
- .onFinishChange((value) => {
4279
- if (
4280
- confirm(
4281
- "Changing reveal applies on the next initialisation. Continue?",
4282
- )
4283
- ) {
4284
- applyToLenses("reveal", value);
4285
- } else {
4286
- reinitState.reveal = options.reveal;
4287
- gui.controllersRecursive().forEach((c) => c.updateDisplay());
4288
- }
4289
- });
4290
-
4291
- initFolder
4292
- .add(reinitState, "resolution", 0.5, 3, 0.25)
4293
- .name("Resolution")
4294
- .onFinishChange((value) => {
4295
- if (
4296
- confirm(
4297
- "Changing resolution re-captures the page snapshot. Continue?",
4298
- )
4299
- ) {
4300
- applyToLenses("resolution", value);
4301
- const renderer = window.__liquidGLRenderer__;
4302
- if (renderer) {
4303
- renderer._snapshotResolution = Math.max(0.1, Math.min(3.0, value));
4304
- renderer.captureSnapshot();
4305
- }
4306
- } else {
4307
- reinitState.resolution = options.resolution;
4308
- gui.controllersRecursive().forEach((c) => c.updateDisplay());
4309
- }
4310
- });
4311
-
4312
- const copyButton = {
4313
- copySettings: () => {
4314
- const code = generateInitCode(options);
4315
- const controller = gui.controllers.find(
4316
- (c) => c.property === "copySettings",
4317
- );
4318
-
4319
- if (!controller) return;
4320
-
4321
- const originalName = controller._name;
4322
- controller.disable();
4323
- controller.name("✓ Copied");
4324
-
4325
- const copySuccess = () => {
4326
- setTimeout(() => {
4327
- controller.name(originalName);
4328
- controller.enable();
4329
- }, 1500);
4330
- };
4331
-
4332
- if (navigator.clipboard && navigator.clipboard.writeText) {
4333
- navigator.clipboard
4334
- .writeText(code)
4335
- .then(() => {
4336
- copySuccess();
4337
- })
4338
- .catch((err) => {
4339
- console.error("Clipboard error:", err);
4340
- fallbackCopy(code);
4341
- });
4342
- } else {
4343
- fallbackCopy(code);
4344
- }
4345
-
4346
- function fallbackCopy(text) {
4347
- const textarea = document.createElement("textarea");
4348
- textarea.value = text;
4349
- textarea.style.position = "fixed";
4350
- textarea.style.opacity = "0";
4351
- document.body.appendChild(textarea);
4352
- textarea.select();
4353
- try {
4354
- document.execCommand("copy");
4355
- copySuccess();
4356
- } catch (err) {
4357
- console.error("Copy failed:", err);
4358
- controller.name(originalName);
4359
- controller.enable();
4360
- prompt("Copy this code manually:", text);
4361
- }
4362
- document.body.removeChild(textarea);
4363
- }
4364
- },
4365
- };
4366
- gui.add(copyButton, "copySettings").name("Copy settings");
4367
-
4368
- refractionFolder.close();
4369
- surfaceFolder.close();
4370
- tiltFolder.close();
4371
- initFolder.close();
4372
-
4373
- helperGUIs.push({ gui, lenses: lensList });
4374
- return gui;
4375
- }
4376
-
4377
- function generateInitCode(options) {
4378
- const lines = ["liquidGL({"];
4379
-
4380
- lines.push(` target: "${options.target}",`);
4381
- lines.push(` snapshot: "${options.snapshot}",`);
4382
- lines.push(` resolution: ${options.resolution},`);
4383
- lines.push(` refraction: ${options.refraction},`);
4384
- lines.push(` aberration: ${options.aberration},`);
4385
- lines.push(` bevelDepth: ${options.bevelDepth},`);
4386
- lines.push(` bevelWidth: ${options.bevelWidth},`);
4387
- lines.push(` frost: ${options.frost},`);
4388
- lines.push(` shadow: ${options.shadow},`);
4389
- lines.push(` specular: ${options.specular},`);
4390
- lines.push(` reveal: "${options.reveal}",`);
4391
- lines.push(` tilt: ${options.tilt},`);
4392
- lines.push(` tiltFactor: ${options.tiltFactor},`);
4393
- lines.push(` tiltEase: ${options.tiltEase},`);
4394
- lines.push(` magnify: ${options.magnify},`);
4395
- lines.push(` helper: false,`);
4396
- lines.push(`});`);
4397
-
4398
- return lines.join("\n");
4399
- }
4400
-
4401
4857
  /* --------------------------------------------------
4402
4858
  * Public API
4403
4859
  * ------------------------------------------------*/
@@ -4406,6 +4862,7 @@ const liquidGL = (() => {
4406
4862
  target: ".liquidGL",
4407
4863
  snapshot: "body",
4408
4864
  resolution: 2.0,
4865
+ engine: "auto",
4409
4866
  refraction: 0.01,
4410
4867
  aberration: 0,
4411
4868
  bevelDepth: 0.08,
@@ -4423,20 +4880,40 @@ const liquidGL = (() => {
4423
4880
  };
4424
4881
  const options = { ...defaults, ...userOptions };
4425
4882
 
4426
- if (typeof window.__liquidGLNoWebGL__ === "undefined") {
4427
- const testCanvas = document.createElement("canvas");
4428
- const testCtx =
4429
- testCanvas.getContext("webgl2") ||
4430
- testCanvas.getContext("webgl") ||
4431
- testCanvas.getContext("experimental-webgl");
4432
- 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";
4433
4894
  }
4434
4895
 
4435
- 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;
4436
4913
 
4437
- if (noWebGL) {
4914
+ if (window.__liquidGLNoWebGL__ === true || noGPU) {
4438
4915
  console.warn(
4439
- "liquidGL: WebGL not available – falling back to CSS backdrop-filter.",
4916
+ "liquidGL: WebGPU/WebGL not available – falling back to CSS backdrop-filter.",
4440
4917
  );
4441
4918
  const fallbackNodes = document.querySelectorAll(options.target);
4442
4919
  fallbackNodes.forEach((node) => {
@@ -4453,7 +4930,11 @@ const liquidGL = (() => {
4453
4930
 
4454
4931
  let renderer = window.__liquidGLRenderer__;
4455
4932
  if (!renderer) {
4456
- renderer = new liquidGLRenderer(options.snapshot, options.resolution);
4933
+ renderer = new liquidGLRenderer(
4934
+ options.snapshot,
4935
+ options.resolution,
4936
+ options.engine,
4937
+ );
4457
4938
  window.__liquidGLRenderer__ = renderer;
4458
4939
  }
4459
4940
 
@@ -4478,14 +4959,13 @@ const liquidGL = (() => {
4478
4959
  }
4479
4960
 
4480
4961
  if (options.helper) {
4481
- loadLilGui()
4482
- .then(() => {
4483
- const instanceIndex = helperGUIs.length;
4484
- createHelperGUI(instances, options, instanceIndex);
4485
- })
4486
- .catch((err) => {
4487
- console.error("liquidGL: Failed to load helper GUI:", err);
4488
- });
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
+ }
4489
4969
  }
4490
4970
 
4491
4971
  return instances.length === 1 ? instances[0] : instances;
@@ -4590,6 +5070,7 @@ const liquidGL = (() => {
4590
5070
 
4591
5071
  return { lenis, locomotiveScroll: loco };
4592
5072
  };
5073
+
4593
5074
  return window.liquidGL;
4594
5075
  })();
4595
5076