liquid-glass-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # Liquid Glass Canvas
2
+
3
+ A high-performance, WebGL-native liquid glass refraction library for HTML5 Canvas and WebGL sources.
4
+
5
+ > [!IMPORTANT]
6
+ > **This library is designed specifically to refract Canvas (2D/WebGL) content.** It does not refract arbitrary HTML DOM elements (such as text, images, or standard divs) that are outside the canvas source.
7
+
8
+ ---
9
+
10
+ ## Why Liquid Glass Canvas?
11
+
12
+ Traditional glassmorphism effects on the web rely on CSS `backdrop-filter: blur()`, SVG `<feDisplacementMap>`, or DOM-cloning techniques. While these work well for static or standard HTML content, they fail when dealing with **dynamic canvas animations** (e.g., interactive particle systems, Three.js scenes, PixiJS renderers, or canvas game loops):
13
+ 1. **SVG Displacement Maps** are extremely slow and CPU-bound in many browsers.
14
+ 2. **Canvas `toDataURL` or `getImageData`** approaches require copying pixel buffers back to the CPU, destroying performance and causing frame drops.
15
+ 3. **`backdrop-filter`** does not provide custom lens-based optical refraction (e.g., normal-based displacement, chromatic aberration, or custom edge curves).
16
+
17
+ **Liquid Glass Canvas** solves this by performing native WebGL-based refraction directly in the GPU. It captures a source canvas as a WebGL texture, calculates precise rounded-rectangle Signed Distance Field (SDF) lenses, and renders high-performance refraction overlays or quads at 60 FPS.
18
+
19
+ ---
20
+
21
+ ## Features
22
+
23
+ - **Rounded Rectangle SDF Lenses:** Clean, mathematical shapes with adjustable corner radii.
24
+ - **Edge Normal Displacement:** Realistic lens refraction based on edge normals and customizable falloff curves.
25
+ - **Chromatic Aberration:** Simulated glass dispersion by sampling color channels with slight offsets.
26
+ - **Layered Aesthetics:** Combined refraction core, tint overlay (supporting RGBA/hex/CSS color strings), and dynamic specular glint.
27
+ - **Dual Modes:**
28
+ - **Overlay Mode:** Ideal for standard web apps. It places a transparent WebGL canvas over your background canvas and automatically aligns glass lenses with floating DOM elements (like cards or menus).
29
+ - **Pass Mode:** Ideal for custom graphics pipelines. Render the refraction step directly inside an existing WebGL context or render loop.
30
+ - **Performance Optimized:** No expensive blur shaders, smart DPR handling, and low-quality fallbacks for mobile/lower-end devices.
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install liquid-glass-canvas
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ### 1. Overlay Mode (Aligning with DOM Elements)
45
+
46
+ In Overlay Mode, the library places an overlay WebGL canvas on top of a source canvas and automatically tracks DOM elements to apply refraction.
47
+
48
+ ```html
49
+ <div id="container" style="position: relative; width: 100vw; height: 100vh;">
50
+ <!-- Your dynamic background (e.g., Three.js, 2D Canvas particles) -->
51
+ <canvas id="bg-canvas" style="width: 100%; height: 100%;"></canvas>
52
+
53
+ <!-- A standard DOM element you want to turn into a glass lens -->
54
+ <div id="glass-card" style="position: absolute; width: 300px; height: 200px; border-radius: 24px;">
55
+ <h2>Refracted Card</h2>
56
+ </div>
57
+ </div>
58
+ ```
59
+
60
+ ```typescript
61
+ import { createCanvasLiquidGlass } from 'liquid-glass-canvas';
62
+
63
+ // Initialize the overlay
64
+ const glass = createCanvasLiquidGlass({
65
+ source: document.getElementById('bg-canvas') as HTMLCanvasElement,
66
+ container: document.getElementById('container') as HTMLElement,
67
+ dpr: 'auto', // Matches device pixel ratio automatically
68
+ });
69
+
70
+ // Register the DOM element as a lens
71
+ glass.registerLens(document.getElementById('glass-card')!, {
72
+ radius: 24, // Match CSS border-radius
73
+ depth: 80, // Strength of refraction displacement
74
+ feather: 16, // Feather distance in pixels at the lens edge
75
+ curve: 2.0, // Falloff curve exponent (higher = sharper edge)
76
+ chroma: 0.05, // Chromatic aberration intensity
77
+ tint: 'rgba(255, 255, 255, 0.06)', // Glass tint color
78
+ glint: 0.4 // Specular highlight brightness on top-left edge
79
+ });
80
+
81
+ // Start the requestAnimationFrame rendering loop
82
+ glass.start();
83
+ ```
84
+
85
+ ### 2. Pass Mode (Custom WebGL Pipeline Integration)
86
+
87
+ For projects with their own WebGL contexts (like custom Three.js/PixiJS post-processing passes), Pass Mode renders the liquid glass quads directly into your active framebuffer.
88
+
89
+ ```typescript
90
+ import { createLiquidGlassPass } from 'liquid-glass-canvas';
91
+
92
+ // 1. Initialize the pass with a WebGL context
93
+ const pass = createLiquidGlassPass(gl, { maxLenses: 16 });
94
+
95
+ // 2. In your render loop:
96
+ function render() {
97
+ // ... Draw background scene to a texture (sourceTexture) ...
98
+
99
+ // Render refraction quads
100
+ pass.render({
101
+ sourceTexture: mySceneTexture,
102
+ resolution: [viewportWidth, viewportHeight],
103
+ lenses: [
104
+ {
105
+ x: 100,
106
+ y: 150,
107
+ width: 300,
108
+ height: 200,
109
+ radius: 24,
110
+ depth: 80,
111
+ feather: 16,
112
+ curve: 2.0,
113
+ chroma: 0.05,
114
+ tint: [1.0, 1.0, 1.0, 0.06], // Normalized RGBA [r, g, b, a]
115
+ glint: 0.4
116
+ }
117
+ ]
118
+ });
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## API Reference
125
+
126
+ ### `createCanvasLiquidGlass(options)`
127
+
128
+ Initializes an Overlay Mode instance.
129
+
130
+ - **Options:**
131
+ - `source`: `HTMLCanvasElement` (The underlying canvas containing background graphics)
132
+ - `container`: `HTMLElement` (The common parent containing the source canvas and the DOM overlay lenses)
133
+ - `dpr`: `number | 'auto'` (Default: `'auto'`. Output resolution scaling factor)
134
+ - `quality`: `'auto' | 'high' | 'low'` (Default: `'auto'`. High quality enables chromatic aberration; low quality disables it for better performance)
135
+
136
+ #### Instance Methods:
137
+
138
+ - **`glass.registerLens(target, options)`**
139
+ Tracks a DOM element as a lens. Matches layout positions relative to the container.
140
+ - **`glass.registerRectLens(rect, options)`**
141
+ Creates a static lens defined by manual coordinates `{ x, y, width, height }`. Returns a unique `symbol` ID.
142
+ - **`glass.unregisterLens(target)`**
143
+ Removes a registered element or static rect lens (using its `symbol` ID).
144
+ - **`glass.updateLens(target, options)`**
145
+ Updates settings dynamically for a registered element or rect lens.
146
+ - **`glass.start()`**
147
+ Starts the automatic requestAnimationFrame loop to render overlays and track layouts.
148
+ - **`glass.tick()`**
149
+ Manually triggers a single frame render. Useful if you want to drive the rendering using your own animation loop.
150
+ - **`glass.stop()`**
151
+ Pauses the automatic rendering loop.
152
+ - **`glass.destroy()`**
153
+ Stops rendering, detaches resize listeners, deletes internal WebGL resources, and removes the overlay canvas from the DOM.
154
+
155
+ ---
156
+
157
+ ## Performance & Technical Tradeoffs
158
+
159
+ ### 1. No Blur Effects
160
+ Typical "frosted glass" effects require Gaussian Blur or Box Blur, which involve multiple texture lookups and render passes. Doing this in real-time as an overlay synchronized with complex background canvases is highly resource-intensive (especially on mobile). By prioritizing crisp optical refraction, normal-based displacement, tinting, and glints, Liquid Glass Canvas delivers a premium glass aesthetic at a fraction of the performance cost.
161
+
162
+ ### 2. CORS and Canvas Tainting
163
+ Because this library uploads the source canvas to WebGL using `texImage2D`, the source canvas must not be **tainted**. If your source canvas draws images or videos from a different origin, ensure those resources are served with appropriate CORS headers (`Access-Control-Allow-Origin`) and loaded with `crossOrigin = "anonymous"`.
164
+
165
+ ---
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,2 @@
1
+ import { CanvasLiquidGlassOptions, LiquidGlassInstance } from '../types';
2
+ export declare function createCanvasLiquidGlass(options: CanvasLiquidGlassOptions): LiquidGlassInstance;
@@ -0,0 +1,2 @@
1
+ import { PassOptions, LiquidGlassPassInstance } from '../types';
2
+ export declare function createLiquidGlassPass(gl: WebGLRenderingContext, _options?: PassOptions): LiquidGlassPassInstance;
@@ -0,0 +1,15 @@
1
+ import { LensOptions, RectLensDef } from '../types';
2
+ export declare class LensRegistry {
3
+ private elementLenses;
4
+ private rectLenses;
5
+ registerElement(element: HTMLElement, options?: LensOptions): void;
6
+ registerRect(rect: {
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ } & LensOptions): symbol;
12
+ unregister(target: HTMLElement | symbol): void;
13
+ update(target: HTMLElement | symbol, options: Partial<LensOptions>): void;
14
+ getActiveLenses(container: HTMLElement): RectLensDef[];
15
+ }
@@ -0,0 +1,2 @@
1
+ import { LensOptions } from '../types';
2
+ export declare const DEFAULT_LENS_OPTIONS: Required<LensOptions>;
@@ -0,0 +1,8 @@
1
+ export interface Rect {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export declare function measureElement(element: HTMLElement, container: HTMLElement): Rect;
8
+ export declare function parseColor(color: string | [number, number, number, number]): [number, number, number, number];
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export { createCanvasLiquidGlass } from './adapters/overlay';
3
+ export { createLiquidGlassPass } from './adapters/pass';
@@ -0,0 +1,2 @@
1
+ .liquid-glass-overlay{pointer-events:none;z-index:5;transform-origin:0 0;position:absolute;top:0;left:0}
2
+ /*$vite$:1*/
@@ -0,0 +1,332 @@
1
+ //#region src/core/defaults.ts
2
+ var e = {
3
+ radius: 16,
4
+ depth: 50,
5
+ feather: 16,
6
+ curve: 2,
7
+ chroma: 0,
8
+ tint: [
9
+ 1,
10
+ 1,
11
+ 1,
12
+ .05
13
+ ],
14
+ glint: .2
15
+ };
16
+ //#endregion
17
+ //#region src/core/measure.ts
18
+ function t(e, t) {
19
+ let n = e.getBoundingClientRect(), r = t.getBoundingClientRect();
20
+ return {
21
+ x: n.left - r.left,
22
+ y: n.top - r.top,
23
+ width: n.width,
24
+ height: n.height
25
+ };
26
+ }
27
+ var n = /* @__PURE__ */ new Map();
28
+ function r(e) {
29
+ if (Array.isArray(e)) return e;
30
+ let t = n.get(e);
31
+ if (t) return t;
32
+ let r = document.createElement("div");
33
+ r.style.color = e, r.style.display = "none", document.body.appendChild(r);
34
+ let i = getComputedStyle(r).color;
35
+ document.body.removeChild(r);
36
+ let a = i.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/);
37
+ if (a) {
38
+ let t = [
39
+ parseInt(a[1], 10) / 255,
40
+ parseInt(a[2], 10) / 255,
41
+ parseInt(a[3], 10) / 255,
42
+ a[4] === void 0 ? 1 : parseFloat(a[4])
43
+ ];
44
+ return n.set(e, t), t;
45
+ }
46
+ let o = [
47
+ 1,
48
+ 1,
49
+ 1,
50
+ 1
51
+ ];
52
+ return n.set(e, o), o;
53
+ }
54
+ //#endregion
55
+ //#region src/core/LensRegistry.ts
56
+ var i = class {
57
+ elementLenses = /* @__PURE__ */ new Map();
58
+ rectLenses = /* @__PURE__ */ new Map();
59
+ registerElement(t, n) {
60
+ this.elementLenses.set(t, {
61
+ ...e,
62
+ ...n
63
+ });
64
+ }
65
+ registerRect(t) {
66
+ let n = Symbol(), i = {
67
+ ...e,
68
+ ...t
69
+ };
70
+ return this.rectLenses.set(n, {
71
+ x: t.x,
72
+ y: t.y,
73
+ width: t.width,
74
+ height: t.height,
75
+ radius: i.radius,
76
+ depth: i.depth,
77
+ feather: i.feather,
78
+ curve: i.curve,
79
+ chroma: i.chroma,
80
+ tint: r(i.tint),
81
+ glint: i.glint
82
+ }), n;
83
+ }
84
+ unregister(e) {
85
+ typeof e == "symbol" ? this.rectLenses.delete(e) : this.elementLenses.delete(e);
86
+ }
87
+ update(e, t) {
88
+ if (typeof e == "symbol") {
89
+ let n = this.rectLenses.get(e);
90
+ if (n) {
91
+ let i = t.tint ? r(t.tint) : n.tint;
92
+ this.rectLenses.set(e, {
93
+ ...n,
94
+ ...t,
95
+ tint: i
96
+ });
97
+ }
98
+ } else {
99
+ let n = this.elementLenses.get(e);
100
+ n && this.elementLenses.set(e, {
101
+ ...n,
102
+ ...t
103
+ });
104
+ }
105
+ }
106
+ getActiveLenses(e) {
107
+ let n = [];
108
+ for (let [i, a] of this.elementLenses.entries()) {
109
+ let o = t(i, e);
110
+ n.push({
111
+ x: o.x,
112
+ y: o.y,
113
+ width: o.width,
114
+ height: o.height,
115
+ radius: a.radius,
116
+ depth: a.depth,
117
+ feather: a.feather,
118
+ curve: a.curve,
119
+ chroma: a.chroma,
120
+ tint: r(a.tint),
121
+ glint: a.glint
122
+ });
123
+ }
124
+ for (let e of this.rectLenses.values()) n.push(e);
125
+ return n;
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region src/webgl/createContext.ts
130
+ function a(e) {
131
+ let t = e.getContext("webgl", {
132
+ alpha: !0,
133
+ depth: !1,
134
+ stencil: !1,
135
+ antialias: !1,
136
+ premultipliedAlpha: !0,
137
+ preserveDrawingBuffer: !1
138
+ }) || e.getContext("experimental-webgl");
139
+ if (!t) throw Error("WebGL not supported");
140
+ return t;
141
+ }
142
+ //#endregion
143
+ //#region src/webgl/TextureSource.ts
144
+ var o = class {
145
+ gl;
146
+ texture;
147
+ constructor(e) {
148
+ this.gl = e;
149
+ let t = e.createTexture();
150
+ if (!t) throw Error("Could not create texture");
151
+ this.texture = t, e.bindTexture(e.TEXTURE_2D, this.texture), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.LINEAR), e.bindTexture(e.TEXTURE_2D, null);
152
+ }
153
+ update(e) {
154
+ let t = this.gl;
155
+ t.bindTexture(t.TEXTURE_2D, this.texture), t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL, !0), t.texImage2D(t.TEXTURE_2D, 0, t.RGBA, t.RGBA, t.UNSIGNED_BYTE, e), t.bindTexture(t.TEXTURE_2D, null);
156
+ }
157
+ getTexture() {
158
+ return this.texture;
159
+ }
160
+ destroy() {
161
+ this.gl.deleteTexture(this.texture);
162
+ }
163
+ };
164
+ //#endregion
165
+ //#region src/webgl/createProgram.ts
166
+ function s(e, t, n) {
167
+ let r = e.createShader(t);
168
+ if (!r) throw Error("Could not create shader");
169
+ if (e.shaderSource(r, n), e.compileShader(r), !e.getShaderParameter(r, e.COMPILE_STATUS)) {
170
+ let t = e.getShaderInfoLog(r);
171
+ throw e.deleteShader(r), Error(`Shader compilation failed: ${t}`);
172
+ }
173
+ return r;
174
+ }
175
+ function c(e, t, n) {
176
+ let r = s(e, e.VERTEX_SHADER, t), i = s(e, e.FRAGMENT_SHADER, n), a = e.createProgram();
177
+ if (!a) throw Error("Could not create program");
178
+ if (e.attachShader(a, r), e.attachShader(a, i), e.linkProgram(a), !e.getProgramParameter(a, e.LINK_STATUS)) {
179
+ let t = e.getProgramInfoLog(a);
180
+ throw e.deleteProgram(a), Error(`Program linking failed: ${t}`);
181
+ }
182
+ return e.deleteShader(r), e.deleteShader(i), a;
183
+ }
184
+ //#endregion
185
+ //#region src/webgl/createQuad.ts
186
+ function l(e, t) {
187
+ let n = new Float32Array([
188
+ -1,
189
+ -1,
190
+ 1,
191
+ -1,
192
+ -1,
193
+ 1,
194
+ -1,
195
+ 1,
196
+ 1,
197
+ -1,
198
+ 1,
199
+ 1
200
+ ]), r = e.createBuffer();
201
+ if (!r) throw Error("Could not create buffer");
202
+ e.bindBuffer(e.ARRAY_BUFFER, r), e.bufferData(e.ARRAY_BUFFER, n, e.STATIC_DRAW);
203
+ let i = e.getExtension("OES_vertex_array_object"), a = null, o = e.getAttribLocation(t, "a_position");
204
+ return i && (a = i.createVertexArrayOES(), i.bindVertexArrayOES(a), e.bindBuffer(e.ARRAY_BUFFER, r), e.enableVertexAttribArray(o), e.vertexAttribPointer(o, 2, e.FLOAT, !1, 0, 0), i.bindVertexArrayOES(null)), {
205
+ buffer: r,
206
+ vao: a,
207
+ draw: () => {
208
+ i && a ? (i.bindVertexArrayOES(a), e.drawArrays(e.TRIANGLES, 0, 6), i.bindVertexArrayOES(null)) : (e.bindBuffer(e.ARRAY_BUFFER, r), e.enableVertexAttribArray(o), e.vertexAttribPointer(o, 2, e.FLOAT, !1, 0, 0), e.drawArrays(e.TRIANGLES, 0, 6));
209
+ },
210
+ destroy: () => {
211
+ e.deleteBuffer(r), i && a && i.deleteVertexArrayOES(a);
212
+ }
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/shaders/liquidGlass.vert.ts
217
+ var u = "\nattribute vec2 a_position;\nvarying vec2 v_uv;\n\nvoid main() {\n v_uv = a_position * 0.5 + 0.5;\n // flip Y since WebGL textures have origin at bottom-left, but DOM uses top-left\n // Actually, we'll handle the flip when we upload the texture via gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)\n gl_Position = vec4(a_position, 0.0, 1.0);\n}\n", d = "\nprecision mediump float;\n\nvarying vec2 v_uv;\n\nuniform sampler2D u_source;\nuniform vec2 u_resolution;\nuniform vec4 u_lensRect; // x, y, width, height (in pixels)\nuniform vec4 u_lensParams1; // radius, depth, feather, curve\nuniform vec4 u_lensParams2; // chroma, glint, unused, unused\nuniform vec4 u_lensTint; // r, g, b, a\n\n// Rounded rectangle SDF\nfloat sdRoundRect(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b + vec2(r);\n return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;\n}\n\n// Get outward normal from rounded rectangle\nvec2 getNormal(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b + vec2(r);\n if (d.x <= 0.0 && d.y <= 0.0) return vec2(0.0);\n return sign(p) * normalize(max(d, 0.0));\n}\n\nvoid main() {\n vec2 fragCoord = v_uv * u_resolution;\n // flip Y back since fragCoord Y is 0 at bottom, but our DOM coordinates are 0 at top.\n // Wait, if we flip Y during texture upload and the canvas is fullscreen, \n // v_uv is bottom-up (0,0 bottom-left).\n // u_lensRect uses top-left as origin (DOM coords).\n // So we must convert DOM Y to GL Y:\n float glY = u_resolution.y - fragCoord.y;\n vec2 pxCoords = vec2(fragCoord.x, glY);\n\n // Center of the lens\n vec2 lensCenter = u_lensRect.xy + u_lensRect.zw * 0.5;\n vec2 p = pxCoords - lensCenter;\n \n // Half extents\n vec2 b = u_lensRect.zw * 0.5;\n \n float radius = u_lensParams1.x;\n float depth = u_lensParams1.y;\n float feather = u_lensParams1.z;\n float curve = u_lensParams1.w;\n \n float chroma = u_lensParams2.x;\n float glint = u_lensParams2.y;\n\n // Compute SDF\n float dist = sdRoundRect(p, b, radius);\n \n // Discard fragments outside the rounded rect\n if (dist > 0.0) {\n discard;\n }\n\n // Calculate edge effect amount\n // dist goes from -b to 0 at the edge. \n // We want the effect to happen within 'feather' pixels from the edge.\n // So when dist is -feather, edge=0. When dist is 0, edge=1.\n float edge = clamp((dist + feather) / feather, 0.0, 1.0);\n \n // Apply curve. \n float amount = pow(edge, curve);\n\n // Normal for displacement\n vec2 normal = getNormal(p, b, radius);\n \n // Notice we must map the pixel normal back to UV space for offset.\n // We negate the Y normal because UV Y goes up, but DOM Y goes down.\n vec2 uvOffset = vec2(normal.x, -normal.y) * amount * (depth / u_resolution);\n vec2 sampleUv = v_uv - uvOffset;\n\n vec4 color;\n if (chroma > 0.0) {\n float cOffset = chroma * amount;\n // slightly different offsets for RGB\n vec2 offsetR = vec2(normal.x, -normal.y) * amount * ((depth + cOffset) / u_resolution);\n vec2 offsetG = uvOffset;\n vec2 offsetB = vec2(normal.x, -normal.y) * amount * ((depth - cOffset) / u_resolution);\n \n float rColor = texture2D(u_source, v_uv - offsetR).r;\n float gColor = texture2D(u_source, v_uv - offsetG).g;\n float bColor = texture2D(u_source, v_uv - offsetB).b;\n float aColor = texture2D(u_source, sampleUv).a;\n color = vec4(rColor, gColor, bColor, aColor);\n } else {\n color = texture2D(u_source, sampleUv);\n }\n\n // Apply tint\n color.rgb = mix(color.rgb, u_lensTint.rgb, u_lensTint.a);\n\n // Apply glint (simple specular-like highlight on the top-left edge)\n // We can use the normal and dot product with a light vector\n vec2 lightDir = normalize(vec2(-1.0, -1.0)); // coming from top-left in DOM space\n float specular = max(dot(normal, lightDir), 0.0);\n // sharpen specular\n specular = pow(specular, 4.0) * amount;\n \n color.rgb += vec3(specular * glint);\n\n gl_FragColor = color;\n}\n", f = class {
218
+ gl;
219
+ program;
220
+ quad;
221
+ locations;
222
+ constructor(e) {
223
+ this.gl = e, this.program = c(e, u, d), this.quad = l(e, this.program), this.locations = {
224
+ u_source: e.getUniformLocation(this.program, "u_source"),
225
+ u_resolution: e.getUniformLocation(this.program, "u_resolution"),
226
+ u_lensRect: e.getUniformLocation(this.program, "u_lensRect"),
227
+ u_lensParams1: e.getUniformLocation(this.program, "u_lensParams1"),
228
+ u_lensParams2: e.getUniformLocation(this.program, "u_lensParams2"),
229
+ u_lensTint: e.getUniformLocation(this.program, "u_lensTint")
230
+ };
231
+ }
232
+ render(e) {
233
+ let t = this.gl;
234
+ t.useProgram(this.program), t.viewport(0, 0, e.resolution[0], e.resolution[1]), t.activeTexture(t.TEXTURE0), t.bindTexture(t.TEXTURE_2D, e.sourceTexture), this.locations.u_source && t.uniform1i(this.locations.u_source, 0), this.locations.u_resolution && t.uniform2f(this.locations.u_resolution, e.resolution[0], e.resolution[1]), t.enable(t.BLEND), t.blendFunc(t.ONE, t.ONE_MINUS_SRC_ALPHA);
235
+ for (let n of e.lenses) this.locations.u_lensRect && t.uniform4f(this.locations.u_lensRect, n.x, n.y, n.width, n.height), this.locations.u_lensParams1 && t.uniform4f(this.locations.u_lensParams1, n.radius, n.depth, n.feather, n.curve), this.locations.u_lensParams2 && t.uniform4f(this.locations.u_lensParams2, n.chroma, n.glint, 0, 0), this.locations.u_lensTint && t.uniform4f(this.locations.u_lensTint, n.tint[0], n.tint[1], n.tint[2], n.tint[3]), this.quad.draw();
236
+ t.disable(t.BLEND);
237
+ }
238
+ destroy() {
239
+ this.quad.destroy(), this.gl.deleteProgram(this.program);
240
+ }
241
+ }, p = class {
242
+ overlay;
243
+ gl;
244
+ textureSource;
245
+ pass;
246
+ registry = new i();
247
+ source;
248
+ container;
249
+ dpr;
250
+ quality;
251
+ rafId = 0;
252
+ isRunning = !1;
253
+ constructor(e) {
254
+ this.source = e.source, this.container = e.container, this.dpr = e.dpr || "auto", this.quality = e.quality || "auto", this.overlay = document.createElement("canvas"), this.overlay.className = "liquid-glass-overlay", this.overlay.style.position = "absolute", this.overlay.style.top = "0", this.overlay.style.left = "0", this.overlay.style.pointerEvents = "none", getComputedStyle(this.container).position === "static" && (this.container.style.position = "relative"), this.container.appendChild(this.overlay), this.gl = a(this.overlay), this.textureSource = new o(this.gl), this.pass = new f(this.gl), this.resize = this.resize.bind(this), window.addEventListener("resize", this.resize), this.resize();
255
+ }
256
+ getActiveQuality() {
257
+ let e = this.quality;
258
+ return e === "auto" && (e = typeof navigator < "u" && /Mobi|Android|iPhone/i.test(navigator.userAgent) ? "low" : "high"), e;
259
+ }
260
+ resize() {
261
+ let e = this.container.getBoundingClientRect(), t = this.getActiveQuality(), n = this.dpr === "auto" ? window.devicePixelRatio || 1 : this.dpr;
262
+ n = t === "low" ? Math.min(n, 1) : Math.min(n, 2), this.overlay.width = e.width * n, this.overlay.height = e.height * n, this.overlay.style.width = `${e.width}px`, this.overlay.style.height = `${e.height}px`;
263
+ }
264
+ registerLens(e, t) {
265
+ this.registry.registerElement(e, t);
266
+ }
267
+ registerRectLens(e, t) {
268
+ return this.registry.registerRect({
269
+ ...e,
270
+ ...t
271
+ });
272
+ }
273
+ unregisterLens(e) {
274
+ this.registry.unregister(e);
275
+ }
276
+ updateLens(e, t) {
277
+ this.registry.update(e, t);
278
+ }
279
+ start() {
280
+ if (this.isRunning) return;
281
+ this.isRunning = !0;
282
+ let e = () => {
283
+ this.tick(), this.isRunning && (this.rafId = requestAnimationFrame(e));
284
+ };
285
+ e();
286
+ }
287
+ stop() {
288
+ this.isRunning = !1, cancelAnimationFrame(this.rafId);
289
+ }
290
+ tick() {
291
+ let e = this.registry.getActiveLenses(this.container);
292
+ if (e.length === 0) {
293
+ this.gl.clearColor(0, 0, 0, 0), this.gl.clear(this.gl.COLOR_BUFFER_BIT);
294
+ return;
295
+ }
296
+ let t = this.getActiveQuality(), n = t === "low" ? 4 : 16;
297
+ e.length > n && (e = e.slice(0, n)), this.textureSource.update(this.source);
298
+ let r = this.dpr === "auto" ? window.devicePixelRatio || 1 : this.dpr;
299
+ r = t === "low" ? Math.min(r, 1) : Math.min(r, 2);
300
+ let i = e.map((e) => ({
301
+ ...e,
302
+ x: e.x * r,
303
+ y: e.y * r,
304
+ width: e.width * r,
305
+ height: e.height * r,
306
+ radius: e.radius * r,
307
+ feather: e.feather * r,
308
+ depth: e.depth * r,
309
+ chroma: t === "low" ? 0 : e.chroma
310
+ }));
311
+ this.gl.clearColor(0, 0, 0, 0), this.gl.clear(this.gl.COLOR_BUFFER_BIT), this.pass.render({
312
+ sourceTexture: this.textureSource.getTexture(),
313
+ resolution: [this.overlay.width, this.overlay.height],
314
+ lenses: i
315
+ });
316
+ }
317
+ destroy() {
318
+ this.stop(), window.removeEventListener("resize", this.resize), this.pass.destroy(), this.textureSource.destroy(), this.overlay.parentNode && this.overlay.parentNode.removeChild(this.overlay);
319
+ }
320
+ };
321
+ //#endregion
322
+ //#region src/adapters/overlay.ts
323
+ function m(e) {
324
+ return new p(e);
325
+ }
326
+ //#endregion
327
+ //#region src/adapters/pass.ts
328
+ function h(e, t) {
329
+ return new f(e);
330
+ }
331
+ //#endregion
332
+ export { m as createCanvasLiquidGlass, h as createLiquidGlassPass };
@@ -0,0 +1,117 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.LiquidGlassCanvas={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t={radius:16,depth:50,feather:16,curve:2,chroma:0,tint:[1,1,1,.05],glint:.2};function n(e,t){let n=e.getBoundingClientRect(),r=t.getBoundingClientRect();return{x:n.left-r.left,y:n.top-r.top,width:n.width,height:n.height}}var r=new Map;function i(e){if(Array.isArray(e))return e;let t=r.get(e);if(t)return t;let n=document.createElement(`div`);n.style.color=e,n.style.display=`none`,document.body.appendChild(n);let i=getComputedStyle(n).color;document.body.removeChild(n);let a=i.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/);if(a){let t=[parseInt(a[1],10)/255,parseInt(a[2],10)/255,parseInt(a[3],10)/255,a[4]===void 0?1:parseFloat(a[4])];return r.set(e,t),t}let o=[1,1,1,1];return r.set(e,o),o}var a=class{elementLenses=new Map;rectLenses=new Map;registerElement(e,n){this.elementLenses.set(e,{...t,...n})}registerRect(e){let n=Symbol(),r={...t,...e};return this.rectLenses.set(n,{x:e.x,y:e.y,width:e.width,height:e.height,radius:r.radius,depth:r.depth,feather:r.feather,curve:r.curve,chroma:r.chroma,tint:i(r.tint),glint:r.glint}),n}unregister(e){typeof e==`symbol`?this.rectLenses.delete(e):this.elementLenses.delete(e)}update(e,t){if(typeof e==`symbol`){let n=this.rectLenses.get(e);if(n){let r=t.tint?i(t.tint):n.tint;this.rectLenses.set(e,{...n,...t,tint:r})}}else{let n=this.elementLenses.get(e);n&&this.elementLenses.set(e,{...n,...t})}}getActiveLenses(e){let t=[];for(let[r,a]of this.elementLenses.entries()){let o=n(r,e);t.push({x:o.x,y:o.y,width:o.width,height:o.height,radius:a.radius,depth:a.depth,feather:a.feather,curve:a.curve,chroma:a.chroma,tint:i(a.tint),glint:a.glint})}for(let e of this.rectLenses.values())t.push(e);return t}};function o(e){let t=e.getContext(`webgl`,{alpha:!0,depth:!1,stencil:!1,antialias:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!1})||e.getContext(`experimental-webgl`);if(!t)throw Error(`WebGL not supported`);return t}var s=class{gl;texture;constructor(e){this.gl=e;let t=e.createTexture();if(!t)throw Error(`Could not create texture`);this.texture=t,e.bindTexture(e.TEXTURE_2D,this.texture),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.bindTexture(e.TEXTURE_2D,null)}update(e){let t=this.gl;t.bindTexture(t.TEXTURE_2D,this.texture),t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,!0),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,e),t.bindTexture(t.TEXTURE_2D,null)}getTexture(){return this.texture}destroy(){this.gl.deleteTexture(this.texture)}};function c(e,t,n){let r=e.createShader(t);if(!r)throw Error(`Could not create shader`);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){let t=e.getShaderInfoLog(r);throw e.deleteShader(r),Error(`Shader compilation failed: ${t}`)}return r}function l(e,t,n){let r=c(e,e.VERTEX_SHADER,t),i=c(e,e.FRAGMENT_SHADER,n),a=e.createProgram();if(!a)throw Error(`Could not create program`);if(e.attachShader(a,r),e.attachShader(a,i),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS)){let t=e.getProgramInfoLog(a);throw e.deleteProgram(a),Error(`Program linking failed: ${t}`)}return e.deleteShader(r),e.deleteShader(i),a}function u(e,t){let n=new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),r=e.createBuffer();if(!r)throw Error(`Could not create buffer`);e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,n,e.STATIC_DRAW);let i=e.getExtension(`OES_vertex_array_object`),a=null,o=e.getAttribLocation(t,`a_position`);return i&&(a=i.createVertexArrayOES(),i.bindVertexArrayOES(a),e.bindBuffer(e.ARRAY_BUFFER,r),e.enableVertexAttribArray(o),e.vertexAttribPointer(o,2,e.FLOAT,!1,0,0),i.bindVertexArrayOES(null)),{buffer:r,vao:a,draw:()=>{i&&a?(i.bindVertexArrayOES(a),e.drawArrays(e.TRIANGLES,0,6),i.bindVertexArrayOES(null)):(e.bindBuffer(e.ARRAY_BUFFER,r),e.enableVertexAttribArray(o),e.vertexAttribPointer(o,2,e.FLOAT,!1,0,0),e.drawArrays(e.TRIANGLES,0,6))},destroy:()=>{e.deleteBuffer(r),i&&a&&i.deleteVertexArrayOES(a)}}}var d=`
2
+ attribute vec2 a_position;
3
+ varying vec2 v_uv;
4
+
5
+ void main() {
6
+ v_uv = a_position * 0.5 + 0.5;
7
+ // flip Y since WebGL textures have origin at bottom-left, but DOM uses top-left
8
+ // Actually, we'll handle the flip when we upload the texture via gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
9
+ gl_Position = vec4(a_position, 0.0, 1.0);
10
+ }
11
+ `,f=`
12
+ precision mediump float;
13
+
14
+ varying vec2 v_uv;
15
+
16
+ uniform sampler2D u_source;
17
+ uniform vec2 u_resolution;
18
+ uniform vec4 u_lensRect; // x, y, width, height (in pixels)
19
+ uniform vec4 u_lensParams1; // radius, depth, feather, curve
20
+ uniform vec4 u_lensParams2; // chroma, glint, unused, unused
21
+ uniform vec4 u_lensTint; // r, g, b, a
22
+
23
+ // Rounded rectangle SDF
24
+ float sdRoundRect(vec2 p, vec2 b, float r) {
25
+ vec2 d = abs(p) - b + vec2(r);
26
+ return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
27
+ }
28
+
29
+ // Get outward normal from rounded rectangle
30
+ vec2 getNormal(vec2 p, vec2 b, float r) {
31
+ vec2 d = abs(p) - b + vec2(r);
32
+ if (d.x <= 0.0 && d.y <= 0.0) return vec2(0.0);
33
+ return sign(p) * normalize(max(d, 0.0));
34
+ }
35
+
36
+ void main() {
37
+ vec2 fragCoord = v_uv * u_resolution;
38
+ // flip Y back since fragCoord Y is 0 at bottom, but our DOM coordinates are 0 at top.
39
+ // Wait, if we flip Y during texture upload and the canvas is fullscreen,
40
+ // v_uv is bottom-up (0,0 bottom-left).
41
+ // u_lensRect uses top-left as origin (DOM coords).
42
+ // So we must convert DOM Y to GL Y:
43
+ float glY = u_resolution.y - fragCoord.y;
44
+ vec2 pxCoords = vec2(fragCoord.x, glY);
45
+
46
+ // Center of the lens
47
+ vec2 lensCenter = u_lensRect.xy + u_lensRect.zw * 0.5;
48
+ vec2 p = pxCoords - lensCenter;
49
+
50
+ // Half extents
51
+ vec2 b = u_lensRect.zw * 0.5;
52
+
53
+ float radius = u_lensParams1.x;
54
+ float depth = u_lensParams1.y;
55
+ float feather = u_lensParams1.z;
56
+ float curve = u_lensParams1.w;
57
+
58
+ float chroma = u_lensParams2.x;
59
+ float glint = u_lensParams2.y;
60
+
61
+ // Compute SDF
62
+ float dist = sdRoundRect(p, b, radius);
63
+
64
+ // Discard fragments outside the rounded rect
65
+ if (dist > 0.0) {
66
+ discard;
67
+ }
68
+
69
+ // Calculate edge effect amount
70
+ // dist goes from -b to 0 at the edge.
71
+ // We want the effect to happen within 'feather' pixels from the edge.
72
+ // So when dist is -feather, edge=0. When dist is 0, edge=1.
73
+ float edge = clamp((dist + feather) / feather, 0.0, 1.0);
74
+
75
+ // Apply curve.
76
+ float amount = pow(edge, curve);
77
+
78
+ // Normal for displacement
79
+ vec2 normal = getNormal(p, b, radius);
80
+
81
+ // Notice we must map the pixel normal back to UV space for offset.
82
+ // We negate the Y normal because UV Y goes up, but DOM Y goes down.
83
+ vec2 uvOffset = vec2(normal.x, -normal.y) * amount * (depth / u_resolution);
84
+ vec2 sampleUv = v_uv - uvOffset;
85
+
86
+ vec4 color;
87
+ if (chroma > 0.0) {
88
+ float cOffset = chroma * amount;
89
+ // slightly different offsets for RGB
90
+ vec2 offsetR = vec2(normal.x, -normal.y) * amount * ((depth + cOffset) / u_resolution);
91
+ vec2 offsetG = uvOffset;
92
+ vec2 offsetB = vec2(normal.x, -normal.y) * amount * ((depth - cOffset) / u_resolution);
93
+
94
+ float rColor = texture2D(u_source, v_uv - offsetR).r;
95
+ float gColor = texture2D(u_source, v_uv - offsetG).g;
96
+ float bColor = texture2D(u_source, v_uv - offsetB).b;
97
+ float aColor = texture2D(u_source, sampleUv).a;
98
+ color = vec4(rColor, gColor, bColor, aColor);
99
+ } else {
100
+ color = texture2D(u_source, sampleUv);
101
+ }
102
+
103
+ // Apply tint
104
+ color.rgb = mix(color.rgb, u_lensTint.rgb, u_lensTint.a);
105
+
106
+ // Apply glint (simple specular-like highlight on the top-left edge)
107
+ // We can use the normal and dot product with a light vector
108
+ vec2 lightDir = normalize(vec2(-1.0, -1.0)); // coming from top-left in DOM space
109
+ float specular = max(dot(normal, lightDir), 0.0);
110
+ // sharpen specular
111
+ specular = pow(specular, 4.0) * amount;
112
+
113
+ color.rgb += vec3(specular * glint);
114
+
115
+ gl_FragColor = color;
116
+ }
117
+ `,p=class{gl;program;quad;locations;constructor(e){this.gl=e,this.program=l(e,d,f),this.quad=u(e,this.program),this.locations={u_source:e.getUniformLocation(this.program,`u_source`),u_resolution:e.getUniformLocation(this.program,`u_resolution`),u_lensRect:e.getUniformLocation(this.program,`u_lensRect`),u_lensParams1:e.getUniformLocation(this.program,`u_lensParams1`),u_lensParams2:e.getUniformLocation(this.program,`u_lensParams2`),u_lensTint:e.getUniformLocation(this.program,`u_lensTint`)}}render(e){let t=this.gl;t.useProgram(this.program),t.viewport(0,0,e.resolution[0],e.resolution[1]),t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,e.sourceTexture),this.locations.u_source&&t.uniform1i(this.locations.u_source,0),this.locations.u_resolution&&t.uniform2f(this.locations.u_resolution,e.resolution[0],e.resolution[1]),t.enable(t.BLEND),t.blendFunc(t.ONE,t.ONE_MINUS_SRC_ALPHA);for(let n of e.lenses)this.locations.u_lensRect&&t.uniform4f(this.locations.u_lensRect,n.x,n.y,n.width,n.height),this.locations.u_lensParams1&&t.uniform4f(this.locations.u_lensParams1,n.radius,n.depth,n.feather,n.curve),this.locations.u_lensParams2&&t.uniform4f(this.locations.u_lensParams2,n.chroma,n.glint,0,0),this.locations.u_lensTint&&t.uniform4f(this.locations.u_lensTint,n.tint[0],n.tint[1],n.tint[2],n.tint[3]),this.quad.draw();t.disable(t.BLEND)}destroy(){this.quad.destroy(),this.gl.deleteProgram(this.program)}},m=class{overlay;gl;textureSource;pass;registry=new a;source;container;dpr;quality;rafId=0;isRunning=!1;constructor(e){this.source=e.source,this.container=e.container,this.dpr=e.dpr||`auto`,this.quality=e.quality||`auto`,this.overlay=document.createElement(`canvas`),this.overlay.className=`liquid-glass-overlay`,this.overlay.style.position=`absolute`,this.overlay.style.top=`0`,this.overlay.style.left=`0`,this.overlay.style.pointerEvents=`none`,getComputedStyle(this.container).position===`static`&&(this.container.style.position=`relative`),this.container.appendChild(this.overlay),this.gl=o(this.overlay),this.textureSource=new s(this.gl),this.pass=new p(this.gl),this.resize=this.resize.bind(this),window.addEventListener(`resize`,this.resize),this.resize()}getActiveQuality(){let e=this.quality;return e===`auto`&&(e=typeof navigator<`u`&&/Mobi|Android|iPhone/i.test(navigator.userAgent)?`low`:`high`),e}resize(){let e=this.container.getBoundingClientRect(),t=this.getActiveQuality(),n=this.dpr===`auto`?window.devicePixelRatio||1:this.dpr;n=t===`low`?Math.min(n,1):Math.min(n,2),this.overlay.width=e.width*n,this.overlay.height=e.height*n,this.overlay.style.width=`${e.width}px`,this.overlay.style.height=`${e.height}px`}registerLens(e,t){this.registry.registerElement(e,t)}registerRectLens(e,t){return this.registry.registerRect({...e,...t})}unregisterLens(e){this.registry.unregister(e)}updateLens(e,t){this.registry.update(e,t)}start(){if(this.isRunning)return;this.isRunning=!0;let e=()=>{this.tick(),this.isRunning&&(this.rafId=requestAnimationFrame(e))};e()}stop(){this.isRunning=!1,cancelAnimationFrame(this.rafId)}tick(){let e=this.registry.getActiveLenses(this.container);if(e.length===0){this.gl.clearColor(0,0,0,0),this.gl.clear(this.gl.COLOR_BUFFER_BIT);return}let t=this.getActiveQuality(),n=t===`low`?4:16;e.length>n&&(e=e.slice(0,n)),this.textureSource.update(this.source);let r=this.dpr===`auto`?window.devicePixelRatio||1:this.dpr;r=t===`low`?Math.min(r,1):Math.min(r,2);let i=e.map(e=>({...e,x:e.x*r,y:e.y*r,width:e.width*r,height:e.height*r,radius:e.radius*r,feather:e.feather*r,depth:e.depth*r,chroma:t===`low`?0:e.chroma}));this.gl.clearColor(0,0,0,0),this.gl.clear(this.gl.COLOR_BUFFER_BIT),this.pass.render({sourceTexture:this.textureSource.getTexture(),resolution:[this.overlay.width,this.overlay.height],lenses:i})}destroy(){this.stop(),window.removeEventListener(`resize`,this.resize),this.pass.destroy(),this.textureSource.destroy(),this.overlay.parentNode&&this.overlay.parentNode.removeChild(this.overlay)}};function h(e){return new m(e)}function g(e,t){return new p(e)}e.createCanvasLiquidGlass=h,e.createLiquidGlassPass=g});
@@ -0,0 +1 @@
1
+ export declare const fragmentShader = "\nprecision mediump float;\n\nvarying vec2 v_uv;\n\nuniform sampler2D u_source;\nuniform vec2 u_resolution;\nuniform vec4 u_lensRect; // x, y, width, height (in pixels)\nuniform vec4 u_lensParams1; // radius, depth, feather, curve\nuniform vec4 u_lensParams2; // chroma, glint, unused, unused\nuniform vec4 u_lensTint; // r, g, b, a\n\n// Rounded rectangle SDF\nfloat sdRoundRect(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b + vec2(r);\n return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;\n}\n\n// Get outward normal from rounded rectangle\nvec2 getNormal(vec2 p, vec2 b, float r) {\n vec2 d = abs(p) - b + vec2(r);\n if (d.x <= 0.0 && d.y <= 0.0) return vec2(0.0);\n return sign(p) * normalize(max(d, 0.0));\n}\n\nvoid main() {\n vec2 fragCoord = v_uv * u_resolution;\n // flip Y back since fragCoord Y is 0 at bottom, but our DOM coordinates are 0 at top.\n // Wait, if we flip Y during texture upload and the canvas is fullscreen, \n // v_uv is bottom-up (0,0 bottom-left).\n // u_lensRect uses top-left as origin (DOM coords).\n // So we must convert DOM Y to GL Y:\n float glY = u_resolution.y - fragCoord.y;\n vec2 pxCoords = vec2(fragCoord.x, glY);\n\n // Center of the lens\n vec2 lensCenter = u_lensRect.xy + u_lensRect.zw * 0.5;\n vec2 p = pxCoords - lensCenter;\n \n // Half extents\n vec2 b = u_lensRect.zw * 0.5;\n \n float radius = u_lensParams1.x;\n float depth = u_lensParams1.y;\n float feather = u_lensParams1.z;\n float curve = u_lensParams1.w;\n \n float chroma = u_lensParams2.x;\n float glint = u_lensParams2.y;\n\n // Compute SDF\n float dist = sdRoundRect(p, b, radius);\n \n // Discard fragments outside the rounded rect\n if (dist > 0.0) {\n discard;\n }\n\n // Calculate edge effect amount\n // dist goes from -b to 0 at the edge. \n // We want the effect to happen within 'feather' pixels from the edge.\n // So when dist is -feather, edge=0. When dist is 0, edge=1.\n float edge = clamp((dist + feather) / feather, 0.0, 1.0);\n \n // Apply curve. \n float amount = pow(edge, curve);\n\n // Normal for displacement\n vec2 normal = getNormal(p, b, radius);\n \n // Notice we must map the pixel normal back to UV space for offset.\n // We negate the Y normal because UV Y goes up, but DOM Y goes down.\n vec2 uvOffset = vec2(normal.x, -normal.y) * amount * (depth / u_resolution);\n vec2 sampleUv = v_uv - uvOffset;\n\n vec4 color;\n if (chroma > 0.0) {\n float cOffset = chroma * amount;\n // slightly different offsets for RGB\n vec2 offsetR = vec2(normal.x, -normal.y) * amount * ((depth + cOffset) / u_resolution);\n vec2 offsetG = uvOffset;\n vec2 offsetB = vec2(normal.x, -normal.y) * amount * ((depth - cOffset) / u_resolution);\n \n float rColor = texture2D(u_source, v_uv - offsetR).r;\n float gColor = texture2D(u_source, v_uv - offsetG).g;\n float bColor = texture2D(u_source, v_uv - offsetB).b;\n float aColor = texture2D(u_source, sampleUv).a;\n color = vec4(rColor, gColor, bColor, aColor);\n } else {\n color = texture2D(u_source, sampleUv);\n }\n\n // Apply tint\n color.rgb = mix(color.rgb, u_lensTint.rgb, u_lensTint.a);\n\n // Apply glint (simple specular-like highlight on the top-left edge)\n // We can use the normal and dot product with a light vector\n vec2 lightDir = normalize(vec2(-1.0, -1.0)); // coming from top-left in DOM space\n float specular = max(dot(normal, lightDir), 0.0);\n // sharpen specular\n specular = pow(specular, 4.0) * amount;\n \n color.rgb += vec3(specular * glint);\n\n gl_FragColor = color;\n}\n";
@@ -0,0 +1 @@
1
+ export declare const vertexShader = "\nattribute vec2 a_position;\nvarying vec2 v_uv;\n\nvoid main() {\n v_uv = a_position * 0.5 + 0.5;\n // flip Y since WebGL textures have origin at bottom-left, but DOM uses top-left\n // Actually, we'll handle the flip when we upload the texture via gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)\n gl_Position = vec4(a_position, 0.0, 1.0);\n}\n";
@@ -0,0 +1,55 @@
1
+ export interface LensOptions {
2
+ radius?: number;
3
+ depth?: number;
4
+ feather?: number;
5
+ curve?: number;
6
+ chroma?: number;
7
+ tint?: string | [number, number, number, number];
8
+ glint?: number;
9
+ }
10
+ export interface RectLensDef {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ radius: number;
16
+ depth: number;
17
+ feather: number;
18
+ curve: number;
19
+ chroma: number;
20
+ tint: [number, number, number, number];
21
+ glint: number;
22
+ }
23
+ export interface CanvasLiquidGlassOptions {
24
+ source: HTMLCanvasElement;
25
+ container: HTMLElement;
26
+ dpr?: number | 'auto';
27
+ quality?: 'auto' | 'high' | 'low';
28
+ }
29
+ export interface PassOptions {
30
+ maxLenses?: number;
31
+ }
32
+ export interface RenderPassOptions {
33
+ sourceTexture: WebGLTexture;
34
+ resolution: [number, number];
35
+ lenses: RectLensDef[];
36
+ }
37
+ export interface LiquidGlassInstance {
38
+ registerLens(target: HTMLElement, options?: LensOptions): void;
39
+ registerRectLens(rect: {
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ }, options?: LensOptions): symbol;
45
+ unregisterLens(target: HTMLElement | symbol): void;
46
+ updateLens(target: HTMLElement | symbol, options: Partial<LensOptions>): void;
47
+ start(): void;
48
+ tick(): void;
49
+ stop(): void;
50
+ destroy(): void;
51
+ }
52
+ export interface LiquidGlassPassInstance {
53
+ render(options: RenderPassOptions): void;
54
+ destroy(): void;
55
+ }
@@ -0,0 +1,10 @@
1
+ import { RenderPassOptions } from '../types';
2
+ export declare class LiquidGlassPass {
3
+ private gl;
4
+ private program;
5
+ private quad;
6
+ private locations;
7
+ constructor(gl: WebGLRenderingContext);
8
+ render(options: RenderPassOptions): void;
9
+ destroy(): void;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { CanvasLiquidGlassOptions, LensOptions } from '../types';
2
+ export declare class LiquidGlassRenderer {
3
+ private overlay;
4
+ private gl;
5
+ private textureSource;
6
+ private pass;
7
+ private registry;
8
+ private source;
9
+ private container;
10
+ private dpr;
11
+ private quality;
12
+ private rafId;
13
+ private isRunning;
14
+ constructor(options: CanvasLiquidGlassOptions);
15
+ private getActiveQuality;
16
+ private resize;
17
+ registerLens(target: HTMLElement, options?: LensOptions): void;
18
+ registerRectLens(rect: {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ }, options?: LensOptions): symbol;
24
+ unregisterLens(target: HTMLElement | symbol): void;
25
+ updateLens(target: HTMLElement | symbol, options: Partial<LensOptions>): void;
26
+ start(): void;
27
+ stop(): void;
28
+ tick(): void;
29
+ destroy(): void;
30
+ }
@@ -0,0 +1,8 @@
1
+ export declare class TextureSource {
2
+ private gl;
3
+ private texture;
4
+ constructor(gl: WebGLRenderingContext);
5
+ update(source: HTMLCanvasElement | HTMLImageElement | HTMLVideoElement): void;
6
+ getTexture(): WebGLTexture;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function createContext(canvas: HTMLCanvasElement): WebGLRenderingContext;
@@ -0,0 +1,2 @@
1
+ export declare function compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader;
2
+ export declare function createProgram(gl: WebGLRenderingContext, vertSource: string, fragSource: string): WebGLProgram;
@@ -0,0 +1,7 @@
1
+ export interface Quad {
2
+ buffer: WebGLBuffer;
3
+ vao: WebGLVertexArrayObjectOES | null;
4
+ draw: () => void;
5
+ destroy: () => void;
6
+ }
7
+ export declare function createQuad(gl: WebGLRenderingContext, program: WebGLProgram): Quad;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "liquid-glass-canvas",
3
+ "version": "0.1.0",
4
+ "description": "High-performance WebGL-native liquid glass refraction effects for HTML5 Canvas and WebGL sources.",
5
+ "main": "./dist/liquid-glass-canvas.umd.js",
6
+ "module": "./dist/liquid-glass-canvas.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/liquid-glass-canvas.mjs",
12
+ "require": "./dist/liquid-glass-canvas.umd.js"
13
+ },
14
+ "./css": "./dist/liquid-glass-canvas.css"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "dev": "vite -c vite.config.demo.ts",
21
+ "build": "npm run build:lib && npm run build:demo",
22
+ "build:lib": "vite build -c vite.config.ts",
23
+ "build:demo": "vite build -c vite.config.demo.ts",
24
+ "preview": "vite preview -c vite.config.demo.ts",
25
+ "test": "echo \"Error: no test specified\" && exit 1",
26
+ "prepack": "npm run build:lib",
27
+ "deploy": "npm run build:demo && wrangler deploy"
28
+ },
29
+ "homepage": "https://liquid-glass-canvas.carsonye.com",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/Whynotmetoo/liquid-glass-canvas.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/Whynotmetoo/liquid-glass-canvas/issues"
36
+ },
37
+ "keywords": [
38
+ "webgl",
39
+ "canvas",
40
+ "liquid-glass",
41
+ "glassmorphism",
42
+ "refraction",
43
+ "threejs",
44
+ "particles"
45
+ ],
46
+ "author": "",
47
+ "license": "MIT",
48
+ "type": "commonjs",
49
+ "devDependencies": {
50
+ "@microsoft/api-extractor": "^7.58.9",
51
+ "@types/three": "^0.184.1",
52
+ "typescript": "^6.0.3",
53
+ "vite": "^8.1.0",
54
+ "vite-plugin-dts": "^5.0.3",
55
+ "wrangler": "^3.109.0"
56
+ },
57
+ "dependencies": {
58
+ "three": "^0.184.0"
59
+ }
60
+ }