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 +169 -0
- package/dist/adapters/overlay.d.ts +2 -0
- package/dist/adapters/pass.d.ts +2 -0
- package/dist/core/LensRegistry.d.ts +15 -0
- package/dist/core/defaults.d.ts +2 -0
- package/dist/core/measure.d.ts +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/liquid-glass-canvas.css +2 -0
- package/dist/liquid-glass-canvas.mjs +332 -0
- package/dist/liquid-glass-canvas.umd.js +117 -0
- package/dist/shaders/liquidGlass.frag.d.ts +1 -0
- package/dist/shaders/liquidGlass.vert.d.ts +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/webgl/LiquidGlassPass.d.ts +10 -0
- package/dist/webgl/LiquidGlassRenderer.d.ts +30 -0
- package/dist/webgl/TextureSource.d.ts +8 -0
- package/dist/webgl/createContext.d.ts +1 -0
- package/dist/webgl/createProgram.d.ts +2 -0
- package/dist/webgl/createQuad.d.ts +7 -0
- package/package.json +60 -0
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,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,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];
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
export declare function createContext(canvas: HTMLCanvasElement): WebGLRenderingContext;
|
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
|
+
}
|