glasskit-js 0.0.1 → 1.0.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/LICENSE +21 -0
- package/LiquidGlass.d.ts +13 -0
- package/LiquidGlass.mjs +51 -0
- package/README.md +143 -2
- package/liquid-glass.d.ts +78 -0
- package/liquid-glass.js +420 -0
- package/package.json +61 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aman Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LiquidGlass.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { LiquidGlassOptions } from "./liquid-glass";
|
|
3
|
+
|
|
4
|
+
export interface LiquidGlassProps
|
|
5
|
+
extends LiquidGlassOptions,
|
|
6
|
+
React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/** Element tag to render (e.g. "div", "button", "section"). @default "div" */
|
|
8
|
+
as?: keyof JSX.IntrinsicElements;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare const LiquidGlass: React.FC<LiquidGlassProps>;
|
|
13
|
+
export default LiquidGlass;
|
package/LiquidGlass.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/*
|
|
3
|
+
* <LiquidGlass> — React/Next.js wrapper around liquid-glass.js.
|
|
4
|
+
* Ships without JSX so it works in any bundler (no transform of node_modules needed).
|
|
5
|
+
*
|
|
6
|
+
* import LiquidGlass from "glasskit-js/react";
|
|
7
|
+
*
|
|
8
|
+
* <LiquidGlass as="button" mode="auto" refraction={90} dispersion={0.5}
|
|
9
|
+
* style={{ borderRadius: 999, padding: "14px 28px" }}>
|
|
10
|
+
* Get tickets
|
|
11
|
+
* </LiquidGlass>
|
|
12
|
+
*
|
|
13
|
+
* Renders a normal element; the effect follows its own size + border-radius.
|
|
14
|
+
* SSR-safe: the engine only runs in a client effect.
|
|
15
|
+
*/
|
|
16
|
+
import React, { useRef, useEffect } from "react";
|
|
17
|
+
import LG from "./liquid-glass.js";
|
|
18
|
+
|
|
19
|
+
const GLASS_KEYS = [
|
|
20
|
+
"mode", "frost", "refraction", "depth", "dispersion", "splay",
|
|
21
|
+
"lightAngle", "lightIntensity", "curvature", "convexity",
|
|
22
|
+
"tint", "tintOpacity", "sheen", "sheenColor", "saturate", "brightness", "radius", "background",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default function LiquidGlass({ as: Tag = "div", children, ...props }) {
|
|
26
|
+
const ref = useRef(null);
|
|
27
|
+
const instRef = useRef(null);
|
|
28
|
+
|
|
29
|
+
// split glass options from normal DOM props
|
|
30
|
+
const opts = {};
|
|
31
|
+
const rest = {};
|
|
32
|
+
for (const k in props) {
|
|
33
|
+
if (GLASS_KEYS.includes(k)) opts[k] = props[k];
|
|
34
|
+
else rest[k] = props[k];
|
|
35
|
+
}
|
|
36
|
+
const optsKey = JSON.stringify(opts);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!ref.current) return;
|
|
40
|
+
instRef.current = LG.apply(ref.current, JSON.parse(optsKey));
|
|
41
|
+
return () => { instRef.current?.destroy(); instRef.current = null; };
|
|
42
|
+
// re-create only when the engine `mode` changes; other params are hot-updated below
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, [JSON.parse(optsKey).mode]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
instRef.current?.update(JSON.parse(optsKey));
|
|
48
|
+
}, [optsKey]);
|
|
49
|
+
|
|
50
|
+
return React.createElement(Tag, { ref, ...rest }, children);
|
|
51
|
+
}
|
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Glasskit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Drop-in Apple / Figma "Liquid Glass" for the web — and the generator that ships the code.**
|
|
4
|
+
|
|
5
|
+
The only liquid-glass tool that lets you **switch the rendering engine** (pure CSS · SVG
|
|
6
|
+
displacement · cross-browser clone · WebGL), apply it to **any element/shape**, **copy the
|
|
7
|
+
code in any framework**, and **measure the real performance cost** before you ship.
|
|
8
|
+
|
|
9
|
+
> npm: **`glasskit-js`** · global: **`Glasskit`** · web component: **`<glass-kit>`**
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
liquid-glass.js ← the engine (zero deps, UMD + <glass-kit> web component)
|
|
13
|
+
liquid-glass.d.ts ← TypeScript types
|
|
14
|
+
LiquidGlass.mjs ← React/Next.js wrapper (default export, no JSX — works in any bundler)
|
|
15
|
+
package.json ← npm package "glasskit-js"
|
|
16
|
+
demo.html ← minimal standalone playground
|
|
17
|
+
site/ ← the generator website (static, deploy as-is)
|
|
18
|
+
index.html · app.js · benchmark.html · liquid-glass.js
|
|
19
|
+
bench/ ← automated cross-browser benchmark runner (Playwright)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## The engine
|
|
25
|
+
|
|
26
|
+
### Modes — the switch nobody else gives you
|
|
27
|
+
|
|
28
|
+
| Mode | Real refraction | Browsers | Refracts | Best for |
|
|
29
|
+
|------|:---:|---|---|---|
|
|
30
|
+
| `css` | ✗ (blur) | **all** | live backdrop | default product UI, mobile, Safari/FF |
|
|
31
|
+
| `svg` | ✓ | **Chromium** | live backdrop | the "wow" surface on Chrome/Edge |
|
|
32
|
+
| `svg-clone` | ✓ | **all** | a cloned DOM element | cross-browser refraction over DOM |
|
|
33
|
+
| `webgl` | ✓ | **all** | an img/canvas/video | hero over a fixed background/video |
|
|
34
|
+
| `auto` | — | — | — | Chromium→`svg`, else `svg-clone` if `background` set, else `css` |
|
|
35
|
+
|
|
36
|
+
`svg-clone` is the cross-browser trick: Safari/Firefox don't allow an SVG filter in
|
|
37
|
+
`backdrop-filter`, so it **clones the background element and filters the clone** instead.
|
|
38
|
+
It refracts DOM, not `<canvas>` pixels — use `webgl` for canvas/video backgrounds.
|
|
39
|
+
|
|
40
|
+
### Params ↔ Figma's Glass panel
|
|
41
|
+
|
|
42
|
+
| Figma slider | Option | | Optical extras | Option |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| Frost | `frost` | | curvature (sphere→squircle) | `curvature` |
|
|
45
|
+
| Refraction | `refraction` | | convex↔concave | `convexity` |
|
|
46
|
+
| Depth | `depth` | | tint | `tint`, `tintOpacity` |
|
|
47
|
+
| Dispersion | `dispersion` | | corner radius | `radius` |
|
|
48
|
+
| Splay | `splay` | | | |
|
|
49
|
+
| Light (angle / %) | `lightAngle` / `lightIntensity` | | | |
|
|
50
|
+
|
|
51
|
+
### Usage
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<!-- vanilla / CDN -->
|
|
55
|
+
<script src="https://unpkg.com/glasskit-js"></script>
|
|
56
|
+
<script>
|
|
57
|
+
const g = Glasskit.apply(document.querySelector('#card'), {
|
|
58
|
+
mode: 'auto', frost: 8, refraction: 90, dispersion: 0.5
|
|
59
|
+
});
|
|
60
|
+
g.update({ frost: 12 }); // hot-update · g.destroy();
|
|
61
|
+
</script>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<!-- web component (registered automatically) -->
|
|
66
|
+
<glass-kit mode="auto" refraction="90" dispersion="0.5"
|
|
67
|
+
style="width:340px;height:210px;border-radius:30px">Glass</glass-kit>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
// React
|
|
72
|
+
import Glass from 'glasskit-js/react';
|
|
73
|
+
<Glass as="button" mode="auto" refraction={90} dispersion={0.5}
|
|
74
|
+
style={{ borderRadius: 999, padding: '14px 28px' }}>Get tickets</Glass>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Shapes
|
|
78
|
+
Any **rounded rectangle (incl. pills & circles)** works with zero extra code — a
|
|
79
|
+
`ResizeObserver` regenerates the refraction map on resize. Arbitrary outlines (blobs,
|
|
80
|
+
SVG paths) keep the blur/tint/highlight via `clip-path`, but refraction edges assume a
|
|
81
|
+
rounded box; supply a custom displacement map for true custom outlines.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## The generator website (`site/`)
|
|
86
|
+
|
|
87
|
+
Static — no build step. Tune visually → switch engine → copy code for React, Vue,
|
|
88
|
+
vanilla, web component, CSS, or Tailwind. Plus a preset gallery and a benchmark page.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx serve site # or: python3 -m http.server --directory site
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Deploy to Cloudflare Pages:** create a Pages project, set the **build command empty**
|
|
95
|
+
and the **output/root directory to `site`**. Pure static files. (Vercel: framework
|
|
96
|
+
"Other", output dir `site`.) Then set your domain in the `og:`/`canonical` tags and add an
|
|
97
|
+
`og.png` (a generator script lives at `bench/og.mjs`).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## The benchmark
|
|
102
|
+
|
|
103
|
+
**In-browser** (`site/benchmark.html`): spawns batches of glass elements per engine,
|
|
104
|
+
animates them over a live backdrop, and records **avg/min FPS, avg & p95 frame time,
|
|
105
|
+
jank %, and JS heap**. Results are saved per browser in `localStorage`, so running it in
|
|
106
|
+
Chrome → Safari → Firefox builds a cross-browser comparison. Export JSON anytime.
|
|
107
|
+
|
|
108
|
+
**Automated** (`bench/run-playwright.mjs`): drives the page across every installed engine.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm i # installs playwright-core
|
|
112
|
+
npm run bench # uses your installed Chrome
|
|
113
|
+
node bench/run-playwright.mjs --dur 2.5 --counts 1,5,15,30,60 --modes css,svg,svg-clone,webgl
|
|
114
|
+
npx playwright install webkit firefox # to also bench Safari & Firefox engines
|
|
115
|
+
```
|
|
116
|
+
Writes `bench/results.json` and `bench/results.csv`.
|
|
117
|
+
|
|
118
|
+
> Note: headless reports vsync-capped 60fps; real differences show under load and on
|
|
119
|
+
> retina (`dpr 2`). `svg-clone` is the heaviest (one clone per element); `webgl` is
|
|
120
|
+
> capped by the browser's ~8–16 live GL contexts — both are surfaced as findings.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Releasing (CI)
|
|
125
|
+
|
|
126
|
+
Two GitHub Actions handle shipping:
|
|
127
|
+
|
|
128
|
+
- **`.github/workflows/pages.yml`** — deploys `site/` to GitHub Pages on every push to `main` that touches `site/`.
|
|
129
|
+
- **`.github/workflows/release.yml`** — publishes to npm when a `v*` tag is pushed, via **npm Trusted Publishing (OIDC)** — no `NPM_TOKEN`, provenance generated automatically.
|
|
130
|
+
|
|
131
|
+
**One-time bootstrap** (npm requires the package to exist before a trusted publisher can be configured):
|
|
132
|
+
|
|
133
|
+
1. `npm publish --access public` once from a logged-in machine to create `glasskit-js`.
|
|
134
|
+
2. npmjs.com → the package → **Settings → Trusted Publisher** → GitHub Actions, repo `amanblog/glasskit`, workflow `release.yml`.
|
|
135
|
+
|
|
136
|
+
After that, cutting a release is tokenless:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm version patch # bumps package.json + creates the vX.Y.Z tag & commit
|
|
140
|
+
git push --follow-tags # CI verifies tag == version, then publishes with provenance
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** liquid-glass.js — TypeScript definitions */
|
|
2
|
+
|
|
3
|
+
export type LiquidGlassMode = "auto" | "css" | "svg" | "svg-clone" | "webgl";
|
|
4
|
+
|
|
5
|
+
export interface LiquidGlassOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Rendering engine.
|
|
8
|
+
* - `css` blur + tint + highlight. Every browser. No real refraction.
|
|
9
|
+
* - `svg` real refraction on the live backdrop. Chromium only.
|
|
10
|
+
* - `svg-clone` real refraction in Chrome/Safari/Firefox (clones `background`). DOM only.
|
|
11
|
+
* - `webgl` real refraction of a supplied `background` (img/canvas/video).
|
|
12
|
+
* - `auto` Chromium→svg; else `background` set→svg-clone; else css.
|
|
13
|
+
* @default "auto"
|
|
14
|
+
*/
|
|
15
|
+
mode?: LiquidGlassMode;
|
|
16
|
+
/** Figma "Frost" — backdrop blur in px. @default 6 */
|
|
17
|
+
frost?: number;
|
|
18
|
+
/** Figma "Refraction" — edge displacement strength in px. @default 90 */
|
|
19
|
+
refraction?: number;
|
|
20
|
+
/** Figma "Depth" — bezel width in px. @default 22 */
|
|
21
|
+
depth?: number;
|
|
22
|
+
/** Figma "Dispersion" — chromatic aberration, 0..1. @default 0.4 */
|
|
23
|
+
dispersion?: number;
|
|
24
|
+
/** Figma "Splay" — softens/widens the bezel falloff, 0..1. @default 0 */
|
|
25
|
+
splay?: number;
|
|
26
|
+
/** Figma "Light" angle in degrees. @default -45 */
|
|
27
|
+
lightAngle?: number;
|
|
28
|
+
/** Figma "Light" % — specular highlight, 0..1. @default 0.8 */
|
|
29
|
+
lightIntensity?: number;
|
|
30
|
+
/** Profile exponent: ~2 spherical, ~4 squircle. @default 2.2 */
|
|
31
|
+
curvature?: number;
|
|
32
|
+
/** +1 convex (magnify) .. 0 flat .. -1 concave (shrink). @default 1 */
|
|
33
|
+
convexity?: number;
|
|
34
|
+
/** Glass tint as "r,g,b". @default "255,255,255" */
|
|
35
|
+
tint?: string;
|
|
36
|
+
/** Tint opacity 0..1. @default 0.08 */
|
|
37
|
+
tintOpacity?: number;
|
|
38
|
+
/** Diagonal gloss over the card face, 0..1 (0 removes it; the light border stays). @default 0.7 */
|
|
39
|
+
sheen?: number;
|
|
40
|
+
/** Gloss color as "r,g,b". @default "255,255,255" */
|
|
41
|
+
sheenColor?: string;
|
|
42
|
+
/** Backdrop saturation (css mode). @default 1.4 */
|
|
43
|
+
saturate?: number;
|
|
44
|
+
/** Backdrop brightness (css mode). @default 1.04 */
|
|
45
|
+
brightness?: number;
|
|
46
|
+
/** Override corner radius (px). null = read element's border-radius. @default null */
|
|
47
|
+
radius?: number | null;
|
|
48
|
+
/** Element or selector to refract. Required for `svg-clone` and `webgl`. */
|
|
49
|
+
background?: Element | string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LiquidGlassInstance {
|
|
53
|
+
readonly el: HTMLElement;
|
|
54
|
+
/** The resolved mode actually in use (after `auto` / fallbacks). */
|
|
55
|
+
readonly mode: LiquidGlassMode;
|
|
56
|
+
/** Hot-update any subset of options. */
|
|
57
|
+
update(patch: Partial<LiquidGlassOptions>): LiquidGlassInstance;
|
|
58
|
+
/** Re-read the background (call after the refracted content changes). */
|
|
59
|
+
refresh(): LiquidGlassInstance;
|
|
60
|
+
/** Remove all DOM, filters and listeners. */
|
|
61
|
+
destroy(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LiquidGlassStatic {
|
|
65
|
+
apply(el: HTMLElement, options?: LiquidGlassOptions): LiquidGlassInstance;
|
|
66
|
+
/** Register the <liquid-glass> custom element (auto-called on load). */
|
|
67
|
+
defineElement(): void;
|
|
68
|
+
isChromium(): boolean;
|
|
69
|
+
pickMode(requested: LiquidGlassMode, hasBackground: boolean): LiquidGlassMode;
|
|
70
|
+
DEFAULTS: Required<Omit<LiquidGlassOptions, "background" | "radius">> & {
|
|
71
|
+
radius: number | null;
|
|
72
|
+
background: Element | string | null;
|
|
73
|
+
};
|
|
74
|
+
version: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare const LiquidGlass: LiquidGlassStatic;
|
|
78
|
+
export default LiquidGlass;
|
package/liquid-glass.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* liquid-glass.js — drop-in "Figma Glass" / Apple Liquid Glass for any element.
|
|
3
|
+
* Zero dependencies. UMD (window.LiquidGlass + CommonJS) + <glass-kit> web component.
|
|
4
|
+
*
|
|
5
|
+
* const g = LiquidGlass.apply(el, { mode: 'auto', refraction: 90, dispersion: 0.5 });
|
|
6
|
+
* g.update({ frost: 12 }); g.destroy();
|
|
7
|
+
*
|
|
8
|
+
* MODES (the switch nobody else gives you):
|
|
9
|
+
* 'css' blur + tint + highlight. Every browser. No real refraction.
|
|
10
|
+
* 'svg' real refraction on the LIVE backdrop via backdrop-filter. CHROMIUM ONLY.
|
|
11
|
+
* 'svg-clone' real refraction in Chrome / Safari / Firefox by cloning a `background`
|
|
12
|
+
* element and filtering the clone. Cross-browser. Refracts DOM (not <canvas>).
|
|
13
|
+
* 'webgl' real refraction in a shader of a supplied `background` (img/canvas/video).
|
|
14
|
+
* 'auto' Chromium -> 'svg'; else `background` set -> 'svg-clone'; else 'css'.
|
|
15
|
+
*
|
|
16
|
+
* Params map onto Figma's Glass panel: frost, refraction, depth, dispersion, splay,
|
|
17
|
+
* lightAngle, lightIntensity (+ optical extras: curvature, convexity, tint).
|
|
18
|
+
*/
|
|
19
|
+
(function (root, factory) {
|
|
20
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
|
|
21
|
+
else { root.Glasskit = root.LiquidGlass = factory(); }
|
|
22
|
+
})(typeof window !== 'undefined' ? window : this, function () {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
var UID = 0;
|
|
26
|
+
var XLINK = 'http://www.w3.org/1999/xlink';
|
|
27
|
+
|
|
28
|
+
var DEFAULTS = {
|
|
29
|
+
mode: 'auto',
|
|
30
|
+
frost: 6, // Figma "Frost" — backdrop blur (px)
|
|
31
|
+
refraction: 90, // Figma "Refraction" — edge displacement strength (px)
|
|
32
|
+
depth: 22, // Figma "Depth" — bezel width (px)
|
|
33
|
+
dispersion: 0.4, // Figma "Dispersion" — chromatic aberration (0..1)
|
|
34
|
+
splay: 0, // Figma "Splay" — softens/widens bezel falloff (0..1)
|
|
35
|
+
lightAngle: -45, // Figma "Light" angle (deg)
|
|
36
|
+
lightIntensity: 0.8, // Figma "Light" % — specular highlight (0..1)
|
|
37
|
+
curvature: 2.2, // profile exponent: ~2 spherical, ~4 squircle
|
|
38
|
+
convexity: 1, // +1 convex (magnify) .. 0 flat .. -1 concave (shrink)
|
|
39
|
+
tint: '255,255,255', // "r,g,b"
|
|
40
|
+
tintOpacity: 0.08,
|
|
41
|
+
sheen: 0.7, // diagonal gloss over the card FACE (0 = remove; border stays)
|
|
42
|
+
sheenColor: '255,255,255', // "r,g,b" — recolor the gloss
|
|
43
|
+
saturate: 1.4,
|
|
44
|
+
brightness: 1.04,
|
|
45
|
+
radius: null, // null = read element border-radius
|
|
46
|
+
background: null // Element|selector — required for 'svg-clone' & 'webgl'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/* ---------------------------- helpers ---------------------------- */
|
|
50
|
+
function isChromium() {
|
|
51
|
+
if (typeof navigator === 'undefined' || typeof window === 'undefined') return false;
|
|
52
|
+
var ua = navigator.userAgent;
|
|
53
|
+
var chromium = /(Chrome|Chromium|Edg|OPR)\//.test(ua) && !/\bEdge\//.test(ua);
|
|
54
|
+
var ok = !!(window.CSS && CSS.supports &&
|
|
55
|
+
(CSS.supports('backdrop-filter', 'blur(1px)') || CSS.supports('-webkit-backdrop-filter', 'blur(1px)')));
|
|
56
|
+
return chromium && ok;
|
|
57
|
+
}
|
|
58
|
+
function clamp8(v) { return v < 0 ? 0 : v > 255 ? 255 : Math.round(v); }
|
|
59
|
+
// only ever emit a clean "r,g,b" triplet into inline CSS — blocks url()/expression smuggling
|
|
60
|
+
function safeRGB(v) { return (typeof v === 'string' && /^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$/.test(v)) ? v.replace(/\s+/g, '') : '255,255,255'; }
|
|
61
|
+
function num(v, d) { var n = parseFloat(v); return isFinite(n) ? n : d; }
|
|
62
|
+
function resolveEl(x) { return typeof x === 'string' ? document.querySelector(x) : x; }
|
|
63
|
+
function readRadius(el) { return parseFloat(getComputedStyle(el).borderRadius) || 0; }
|
|
64
|
+
function ns(tag) { return document.createElementNS('http://www.w3.org/2000/svg', tag); }
|
|
65
|
+
|
|
66
|
+
function pickMode(requested, hasBackground) {
|
|
67
|
+
if (requested && requested !== 'auto') return requested;
|
|
68
|
+
if (isChromium()) return 'svg';
|
|
69
|
+
return hasBackground ? 'svg-clone' : 'css';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* rounded-box signed distance field (negative inside) */
|
|
73
|
+
function sdf(x, y, hw, hh, r) {
|
|
74
|
+
var qx = Math.abs(x - hw) - (hw - r);
|
|
75
|
+
var qy = Math.abs(y - hh) - (hh - r);
|
|
76
|
+
return Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) + Math.min(Math.max(qx, qy), 0) - r;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* displacement map: R = x-shift, G = y-shift, 128 = neutral */
|
|
80
|
+
function buildMap(w, h, radius, bezel, splay, curvature, convexity) {
|
|
81
|
+
w = Math.max(1, Math.round(w)); h = Math.max(1, Math.round(h));
|
|
82
|
+
var cvs = document.createElement('canvas'); cvs.width = w; cvs.height = h;
|
|
83
|
+
var ctx = cvs.getContext('2d');
|
|
84
|
+
var img = ctx.createImageData(w, h), data = img.data;
|
|
85
|
+
var hw = w / 2, hh = h / 2, r = Math.min(radius, hw, hh), b = Math.max(1, bezel);
|
|
86
|
+
var exp = Math.max(0.3, curvature * (1 - 0.5 * splay));
|
|
87
|
+
for (var y = 0; y < h; y++) {
|
|
88
|
+
for (var x = 0; x < w; x++) {
|
|
89
|
+
var d = sdf(x + 0.5, y + 0.5, hw, hh, r);
|
|
90
|
+
var nx = sdf(x + 1.5, y + 0.5, hw, hh, r) - sdf(x - 0.5, y + 0.5, hw, hh, r);
|
|
91
|
+
var nyv = sdf(x + 0.5, y + 1.5, hw, hh, r) - sdf(x + 0.5, y - 0.5, hw, hh, r);
|
|
92
|
+
var nl = Math.hypot(nx, nyv) || 1, m = 0;
|
|
93
|
+
if (d < 0 && d > -b) { var t = -d / b; m = Math.pow(1 - t, exp) * convexity; }
|
|
94
|
+
var i = (y * w + x) * 4;
|
|
95
|
+
data[i] = clamp8(128 - (nx / nl) * m * 127);
|
|
96
|
+
data[i + 1] = clamp8(128 - (nyv / nl) * m * 127);
|
|
97
|
+
data[i + 2] = 128; data[i + 3] = 255;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ctx.putImageData(img, 0, 0);
|
|
101
|
+
return cvs.toDataURL();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ============================ instance ============================ */
|
|
105
|
+
function apply(el, options) {
|
|
106
|
+
var o = Object.assign({}, DEFAULTS, options || {});
|
|
107
|
+
var bg = resolveEl(o.background);
|
|
108
|
+
var id = 'lg' + (++UID);
|
|
109
|
+
var mode = pickMode(o.mode, !!bg);
|
|
110
|
+
// graceful fallback if a refraction mode lacks its background source
|
|
111
|
+
if ((mode === 'svg-clone' || mode === 'webgl') && !bg) mode = isChromium() ? 'svg' : 'css';
|
|
112
|
+
|
|
113
|
+
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
114
|
+
var prevBg = el.style.backgroundColor;
|
|
115
|
+
|
|
116
|
+
var overlay = document.createElement('div');
|
|
117
|
+
overlay.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:2;';
|
|
118
|
+
el.appendChild(overlay);
|
|
119
|
+
|
|
120
|
+
var svg, filter, feImage, feR, feG, feB, feBlur;
|
|
121
|
+
var lensWrap, clone; // svg-clone
|
|
122
|
+
var glcanvas, gl, glState; // webgl
|
|
123
|
+
var rafId = 0;
|
|
124
|
+
var lastMapKey = '';
|
|
125
|
+
|
|
126
|
+
if (mode === 'svg' || mode === 'svg-clone') buildSvgFilter();
|
|
127
|
+
if (mode === 'svg-clone') buildClone();
|
|
128
|
+
if (mode === 'webgl') buildGL();
|
|
129
|
+
|
|
130
|
+
/* ---- SVG filter (shared by backdrop + clone modes) ---- */
|
|
131
|
+
function buildSvgFilter() {
|
|
132
|
+
svg = ns('svg');
|
|
133
|
+
svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
|
|
134
|
+
svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;';
|
|
135
|
+
var defs = ns('defs');
|
|
136
|
+
filter = ns('filter');
|
|
137
|
+
filter.setAttribute('id', id);
|
|
138
|
+
filter.setAttribute('filterUnits', 'userSpaceOnUse');
|
|
139
|
+
filter.setAttribute('color-interpolation-filters', 'sRGB');
|
|
140
|
+
feImage = ns('feImage');
|
|
141
|
+
feImage.setAttribute('result', 'map');
|
|
142
|
+
feImage.setAttribute('preserveAspectRatio', 'none');
|
|
143
|
+
feR = disp('pR'); var mR = chan('mr', 'pR', 'r');
|
|
144
|
+
feG = disp('pG'); var mG = chan('mg', 'pG', 'g');
|
|
145
|
+
feB = disp('pB'); var mB = chan('mb', 'pB', 'b');
|
|
146
|
+
var b1 = blend('mr', 'mg', 'rg'), b2 = blend('rg', 'mb', 'rgb');
|
|
147
|
+
feBlur = ns('feGaussianBlur'); feBlur.setAttribute('in', 'rgb');
|
|
148
|
+
[feImage, feR, mR, feG, mG, feB, mB, b1, b2, feBlur].forEach(function (n) { filter.appendChild(n); });
|
|
149
|
+
defs.appendChild(filter); svg.appendChild(defs); document.body.appendChild(svg);
|
|
150
|
+
|
|
151
|
+
function disp(res) {
|
|
152
|
+
var f = ns('feDisplacementMap');
|
|
153
|
+
f.setAttribute('in', 'SourceGraphic'); f.setAttribute('in2', 'map');
|
|
154
|
+
f.setAttribute('xChannelSelector', 'R'); f.setAttribute('yChannelSelector', 'G');
|
|
155
|
+
f.setAttribute('result', res); return f;
|
|
156
|
+
}
|
|
157
|
+
function chan(res, inp, w) {
|
|
158
|
+
var f = ns('feColorMatrix'); f.setAttribute('in', inp); f.setAttribute('result', res);
|
|
159
|
+
f.setAttribute('type', 'matrix');
|
|
160
|
+
f.setAttribute('values',
|
|
161
|
+
w === 'r' ? '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0' :
|
|
162
|
+
w === 'g' ? '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0' :
|
|
163
|
+
'0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0'); return f;
|
|
164
|
+
}
|
|
165
|
+
function blend(a, b, res) {
|
|
166
|
+
var f = ns('feBlend'); f.setAttribute('in', a); f.setAttribute('in2', b);
|
|
167
|
+
f.setAttribute('mode', 'screen'); f.setAttribute('result', res); return f;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function refreshMap() {
|
|
172
|
+
if (mode !== 'svg' && mode !== 'svg-clone') return;
|
|
173
|
+
var r = el.getBoundingClientRect();
|
|
174
|
+
var rad = o.radius == null ? readRadius(el) : o.radius;
|
|
175
|
+
var key = [Math.round(r.width), Math.round(r.height), rad, o.depth, o.splay, o.curvature, o.convexity].join(',');
|
|
176
|
+
if (key === lastMapKey) return;
|
|
177
|
+
lastMapKey = key;
|
|
178
|
+
var url = buildMap(r.width, r.height, rad, o.depth, o.splay, o.curvature, o.convexity);
|
|
179
|
+
feImage.setAttribute('href', url);
|
|
180
|
+
feImage.setAttributeNS(XLINK, 'xlink:href', url);
|
|
181
|
+
feImage.setAttribute('width', r.width); feImage.setAttribute('height', r.height);
|
|
182
|
+
if (mode === 'svg') {
|
|
183
|
+
// backdrop-filter applies in the element's own space → region is 0,0,w,h
|
|
184
|
+
feImage.setAttribute('x', 0); feImage.setAttribute('y', 0);
|
|
185
|
+
filter.setAttribute('x', 0); filter.setAttribute('y', 0);
|
|
186
|
+
filter.setAttribute('width', r.width); filter.setAttribute('height', r.height);
|
|
187
|
+
}
|
|
188
|
+
// clone mode: the map + filter region track the visible window every frame (syncClone)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ---- clone layer (cross-browser refraction) ---- */
|
|
192
|
+
function buildClone() {
|
|
193
|
+
lensWrap = document.createElement('div');
|
|
194
|
+
lensWrap.style.cssText = 'position:absolute;inset:0;border-radius:inherit;overflow:hidden;z-index:0;pointer-events:none;';
|
|
195
|
+
clone = bg.cloneNode(true);
|
|
196
|
+
clone.removeAttribute('id');
|
|
197
|
+
clone.style.position = 'absolute';
|
|
198
|
+
clone.style.margin = '0';
|
|
199
|
+
clone.style.filter = 'url(#' + id + ')';
|
|
200
|
+
clone.style.webkitFilter = 'url(#' + id + ')';
|
|
201
|
+
lensWrap.appendChild(clone);
|
|
202
|
+
el.insertBefore(lensWrap, overlay);
|
|
203
|
+
el.style.backgroundColor = 'transparent';
|
|
204
|
+
syncClone();
|
|
205
|
+
}
|
|
206
|
+
function syncClone() {
|
|
207
|
+
if (!clone) return;
|
|
208
|
+
var er = el.getBoundingClientRect(), br = bg.getBoundingClientRect();
|
|
209
|
+
clone.style.left = (br.left - er.left) + 'px';
|
|
210
|
+
clone.style.top = (br.top - er.top) + 'px';
|
|
211
|
+
clone.style.width = br.width + 'px';
|
|
212
|
+
clone.style.height = br.height + 'px';
|
|
213
|
+
// The filter runs in the clone's user space (origin = background top-left). Move the
|
|
214
|
+
// displacement map + filter region to sit exactly over the glass window, wherever it is.
|
|
215
|
+
var ox = er.left - br.left, oy = er.top - br.top, pad = Math.ceil(o.frost * 2 + 12);
|
|
216
|
+
feImage.setAttribute('x', ox); feImage.setAttribute('y', oy);
|
|
217
|
+
filter.setAttribute('x', ox - pad); filter.setAttribute('y', oy - pad);
|
|
218
|
+
filter.setAttribute('width', er.width + pad * 2); filter.setAttribute('height', er.height + pad * 2);
|
|
219
|
+
}
|
|
220
|
+
function reclone() {
|
|
221
|
+
if (!lensWrap) return;
|
|
222
|
+
var nc = bg.cloneNode(true);
|
|
223
|
+
nc.removeAttribute('id'); nc.style.position = 'absolute'; nc.style.margin = '0';
|
|
224
|
+
nc.style.filter = 'url(#' + id + ')'; nc.style.webkitFilter = 'url(#' + id + ')';
|
|
225
|
+
lensWrap.replaceChild(nc, clone); clone = nc; syncClone();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ---- apply backdrop/filter scales + frost ---- */
|
|
229
|
+
function applyFilterParams() {
|
|
230
|
+
var k = o.dispersion * 0.4;
|
|
231
|
+
if (feR) {
|
|
232
|
+
feR.setAttribute('scale', o.refraction * (1 - k));
|
|
233
|
+
feG.setAttribute('scale', o.refraction);
|
|
234
|
+
feB.setAttribute('scale', o.refraction * (1 + k));
|
|
235
|
+
feBlur.setAttribute('stdDeviation', o.frost);
|
|
236
|
+
}
|
|
237
|
+
if (mode === 'svg') {
|
|
238
|
+
var f = 'url(#' + id + ')'; el.style.backdropFilter = f; el.style.webkitBackdropFilter = f;
|
|
239
|
+
} else if (mode === 'css') {
|
|
240
|
+
var c = 'blur(' + num(o.frost, 6) + 'px) saturate(' + num(o.saturate, 1.4) + ') brightness(' + num(o.brightness, 1.04) + ')';
|
|
241
|
+
el.style.backdropFilter = c; el.style.webkitBackdropFilter = c;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ---- tint + specular overlay (all modes) ---- */
|
|
246
|
+
function applyOverlay() {
|
|
247
|
+
var li = num(o.lightIntensity, 0.8), a = (num(o.lightAngle, -45) + 90) * Math.PI / 180;
|
|
248
|
+
if (mode === 'css' || mode === 'svg') el.style.backgroundColor = 'rgba(' + safeRGB(o.tint) + ',' + num(o.tintOpacity, 0.08) + ')';
|
|
249
|
+
// FACE gloss — controlled by `sheen`/`sheenColor`, independent of the border light below
|
|
250
|
+
var sc = safeRGB(o.sheenColor), sh = num(o.sheen, 0.7);
|
|
251
|
+
overlay.style.background = sh <= 0 ? 'none' :
|
|
252
|
+
'linear-gradient(' + (num(o.lightAngle, -45) + 90) + 'deg,rgba(' + sc + ',' + (0.6 * sh).toFixed(3) +
|
|
253
|
+
') 0%,rgba(' + sc + ',0) 30%,rgba(' + sc + ',0) 70%,rgba(' + sc + ',' + (0.14 * sh).toFixed(3) + ') 100%)';
|
|
254
|
+
overlay.style.boxShadow =
|
|
255
|
+
'inset 0 0 0 1px rgba(255,255,255,' + (0.30 * li) + '),' +
|
|
256
|
+
'inset ' + (Math.cos(a) * 2).toFixed(1) + 'px ' + (Math.sin(a) * 2).toFixed(1) + 'px 2px rgba(255,255,255,' + (0.5 * li) + '),' +
|
|
257
|
+
'inset ' + (-Math.cos(a) * 2).toFixed(1) + 'px ' + (-Math.sin(a) * 2).toFixed(1) + 'px 6px rgba(0,0,0,.15),' +
|
|
258
|
+
'0 8px 30px rgba(0,0,0,.18)';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ------------------------------ WebGL ------------------------------ */
|
|
262
|
+
function buildGL() {
|
|
263
|
+
glcanvas = document.createElement('canvas');
|
|
264
|
+
glcanvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border-radius:inherit;pointer-events:none;z-index:0;';
|
|
265
|
+
el.insertBefore(glcanvas, overlay);
|
|
266
|
+
el.style.backgroundColor = 'transparent';
|
|
267
|
+
gl = glcanvas.getContext('webgl', { premultipliedAlpha: false, alpha: true });
|
|
268
|
+
if (!gl) { mode = 'css'; applyFilterParams(); return; }
|
|
269
|
+
var vs = 'attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}';
|
|
270
|
+
var fs = [
|
|
271
|
+
'precision highp float;',
|
|
272
|
+
'uniform sampler2D u_bg;uniform vec2 u_win,u_origin,u_size,u_light;',
|
|
273
|
+
'uniform float u_radius,u_bezel,u_refraction,u_dispersion,u_frost,u_lightI,u_curv,u_convex;',
|
|
274
|
+
'float sd(vec2 p,vec2 b,float r){vec2 q=abs(p)-b+r;return min(max(q.x,q.y),0.)+length(max(q,0.))-r;}',
|
|
275
|
+
'vec4 bl(vec2 uv,float rad){if(rad<0.5)return texture2D(u_bg,uv);vec2 px=rad/u_win;vec4 c=vec4(0.);for(int i=-2;i<=2;i++){for(int j=-2;j<=2;j++){c+=texture2D(u_bg,uv+vec2(float(i),float(j))*px*0.5);}}return c/25.;}',
|
|
276
|
+
'void main(){',
|
|
277
|
+
' vec2 fc=vec2(gl_FragCoord.x,u_size.y-gl_FragCoord.y);',
|
|
278
|
+
' vec2 c=fc-u_size*0.5;vec2 b=u_size*0.5;float r=min(u_radius,min(b.x,b.y));',
|
|
279
|
+
' float d=sd(c,b,r);float e=1.0;',
|
|
280
|
+
' float dx=sd(c+vec2(e,0.),b,r)-sd(c-vec2(e,0.),b,r);',
|
|
281
|
+
' float dy=sd(c+vec2(0.,e),b,r)-sd(c-vec2(0.,e),b,r);',
|
|
282
|
+
' vec2 n=normalize(vec2(dx,dy)+1e-6);',
|
|
283
|
+
' float m=0.;if(d<0.&&d>-u_bezel){float t=-d/u_bezel;m=pow(1.-t,u_curv)*u_convex;}',
|
|
284
|
+
' vec2 disp=-n*m*u_refraction;vec2 base=u_origin+fc;float k=u_dispersion*0.4;',
|
|
285
|
+
' vec4 cr=bl((base+disp*(1.-k))/u_win,u_frost);',
|
|
286
|
+
' vec4 cg=bl((base+disp)/u_win,u_frost);',
|
|
287
|
+
' vec4 cb=bl((base+disp*(1.+k))/u_win,u_frost);',
|
|
288
|
+
' vec3 col=vec3(cr.r,cg.g,cb.b);',
|
|
289
|
+
' float spec=pow(max(dot(n,u_light),0.),4.)*abs(m)*u_lightI;col+=spec+0.04*u_lightI*abs(m);',
|
|
290
|
+
' float al=smoothstep(0.75,-0.75,d);gl_FragColor=vec4(col,al);}'
|
|
291
|
+
].join('\n');
|
|
292
|
+
var prog = linkProg(vs, fs); gl.useProgram(prog);
|
|
293
|
+
var buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
294
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
295
|
+
var loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc);
|
|
296
|
+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
297
|
+
var tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
298
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); // uv.y is top-origin to match the element math
|
|
299
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
300
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
301
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
302
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
303
|
+
gl.enable(gl.BLEND); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
304
|
+
glState = { prog: prog, tex: tex, src: null };
|
|
305
|
+
loadGLBg(bg);
|
|
306
|
+
|
|
307
|
+
function linkProg(v, f) {
|
|
308
|
+
function sh(t, s) { var x = gl.createShader(t); gl.shaderSource(x, s); gl.compileShader(x);
|
|
309
|
+
if (!gl.getShaderParameter(x, gl.COMPILE_STATUS)) console.warn('LG shader:', gl.getShaderInfoLog(x)); return x; }
|
|
310
|
+
var p = gl.createProgram();
|
|
311
|
+
gl.attachShader(p, sh(gl.VERTEX_SHADER, v)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, f));
|
|
312
|
+
gl.linkProgram(p); return p;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function loadGLBg(src) {
|
|
316
|
+
if (!gl || !src) return;
|
|
317
|
+
if (typeof src === 'string') { var im = new Image(); im.crossOrigin = 'anonymous'; im.onload = function () { glState.src = im; uploadGL(im); }; im.src = src; }
|
|
318
|
+
else { glState.src = src; uploadGL(src); }
|
|
319
|
+
}
|
|
320
|
+
function uploadGL(src) {
|
|
321
|
+
if (!gl || !src) return;
|
|
322
|
+
gl.bindTexture(gl.TEXTURE_2D, glState.tex);
|
|
323
|
+
try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src); } catch (e) {}
|
|
324
|
+
}
|
|
325
|
+
function renderGL() {
|
|
326
|
+
if (!gl || !glState || !glState.src) return;
|
|
327
|
+
// render the drawing buffer at device-pixel resolution so high-frequency
|
|
328
|
+
// detail (text edges) doesn't alias when the browser upscales on HiDPI screens.
|
|
329
|
+
var dpr = window.devicePixelRatio || 1;
|
|
330
|
+
var r = el.getBoundingClientRect();
|
|
331
|
+
var w = Math.max(1, Math.round(r.width * dpr)), h = Math.max(1, Math.round(r.height * dpr));
|
|
332
|
+
if (glcanvas.width !== w || glcanvas.height !== h) { glcanvas.width = w; glcanvas.height = h; }
|
|
333
|
+
gl.viewport(0, 0, w, h);
|
|
334
|
+
var tag = glState.src.tagName;
|
|
335
|
+
if (tag === 'CANVAS' || tag === 'VIDEO') uploadGL(glState.src);
|
|
336
|
+
var p = glState.prog, U = function (n) { return gl.getUniformLocation(p, n); };
|
|
337
|
+
var rad = o.radius == null ? readRadius(el) : o.radius, a = o.lightAngle * Math.PI / 180;
|
|
338
|
+
// every pixel-space uniform scales by dpr together, keeping the shader internally consistent
|
|
339
|
+
// sample UVs are relative to the BACKGROUND's on-screen rect, not the window
|
|
340
|
+
var br = bg && bg.getBoundingClientRect ? bg.getBoundingClientRect() : null;
|
|
341
|
+
var bw = br && br.width ? br.width : innerWidth, bh = br && br.height ? br.height : innerHeight;
|
|
342
|
+
gl.uniform2f(U('u_win'), bw * dpr, bh * dpr);
|
|
343
|
+
gl.uniform2f(U('u_origin'), (br && br.width ? r.left - br.left : r.left) * dpr, (br && br.height ? r.top - br.top : r.top) * dpr);
|
|
344
|
+
gl.uniform2f(U('u_size'), w, h);
|
|
345
|
+
gl.uniform1f(U('u_radius'), rad * dpr);
|
|
346
|
+
gl.uniform1f(U('u_bezel'), o.depth * dpr);
|
|
347
|
+
gl.uniform1f(U('u_refraction'), o.refraction * dpr);
|
|
348
|
+
gl.uniform1f(U('u_dispersion'), o.dispersion);
|
|
349
|
+
gl.uniform1f(U('u_frost'), o.frost * dpr);
|
|
350
|
+
gl.uniform1f(U('u_curv'), o.curvature);
|
|
351
|
+
gl.uniform1f(U('u_convex'), o.convexity);
|
|
352
|
+
gl.uniform1f(U('u_lightI'), o.lightIntensity);
|
|
353
|
+
gl.uniform2f(U('u_light'), Math.cos(a), Math.sin(a));
|
|
354
|
+
gl.uniform1i(U('u_bg'), 0);
|
|
355
|
+
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
|
|
356
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ---- per-frame loop only for modes that need positional sync ---- */
|
|
360
|
+
function tick() { if (mode === 'svg-clone') syncClone(); else if (mode === 'webgl') renderGL(); rafId = requestAnimationFrame(tick); }
|
|
361
|
+
if (mode === 'svg-clone' || mode === 'webgl') rafId = requestAnimationFrame(tick);
|
|
362
|
+
|
|
363
|
+
var ro = new ResizeObserver(function () { refreshMap(); if (mode === 'svg-clone') syncClone(); });
|
|
364
|
+
ro.observe(el);
|
|
365
|
+
|
|
366
|
+
refreshMap(); applyFilterParams(); applyOverlay();
|
|
367
|
+
|
|
368
|
+
var instance = {
|
|
369
|
+
el: el, mode: mode,
|
|
370
|
+
update: function (patch) {
|
|
371
|
+
Object.assign(o, patch || {});
|
|
372
|
+
if (patch && patch.background != null && mode === 'webgl') loadGLBg(resolveEl(o.background));
|
|
373
|
+
refreshMap(); applyFilterParams(); applyOverlay();
|
|
374
|
+
return instance;
|
|
375
|
+
},
|
|
376
|
+
refresh: function () { if (mode === 'svg-clone') reclone(); else if (mode === 'webgl') loadGLBg(resolveEl(o.background)); return instance; },
|
|
377
|
+
destroy: function () {
|
|
378
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
379
|
+
ro.disconnect();
|
|
380
|
+
// free the WebGL context explicitly — browsers cap ~16 per page
|
|
381
|
+
if (gl) { var lc = gl.getExtension('WEBGL_lose_context'); if (lc) lc.loseContext(); gl = null; }
|
|
382
|
+
[svg, lensWrap, glcanvas, overlay].forEach(function (n) { if (n && n.parentNode) n.parentNode.removeChild(n); });
|
|
383
|
+
el.style.backdropFilter = ''; el.style.webkitBackdropFilter = ''; el.style.backgroundColor = prevBg;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
return instance;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ===================== <glass-kit> web component ===================== */
|
|
390
|
+
var ATTRS = ['mode', 'frost', 'refraction', 'depth', 'dispersion', 'splay', 'light-angle',
|
|
391
|
+
'light-intensity', 'curvature', 'convexity', 'tint', 'tint-opacity', 'sheen', 'sheen-color',
|
|
392
|
+
'radius', 'background'];
|
|
393
|
+
function camel(s) { return s.replace(/-([a-z])/g, function (_, c) { return c.toUpperCase(); }); }
|
|
394
|
+
function defineElement() {
|
|
395
|
+
if (typeof customElements === 'undefined' || customElements.get('glass-kit')) return;
|
|
396
|
+
var Base = (typeof HTMLElement !== 'undefined') ? HTMLElement : function () {};
|
|
397
|
+
function readOpts(node) {
|
|
398
|
+
var opts = {};
|
|
399
|
+
ATTRS.forEach(function (a) {
|
|
400
|
+
if (!node.hasAttribute(a)) return;
|
|
401
|
+
var v = node.getAttribute(a), key = camel(a);
|
|
402
|
+
opts[key] = (a === 'mode' || a === 'tint' || a === 'sheen-color' || a === 'background') ? v : parseFloat(v);
|
|
403
|
+
});
|
|
404
|
+
return opts;
|
|
405
|
+
}
|
|
406
|
+
var C = function () { return Reflect.construct(Base, [], C); };
|
|
407
|
+
C.prototype = Object.create(Base.prototype);
|
|
408
|
+
C.observedAttributes = ATTRS;
|
|
409
|
+
C.prototype.connectedCallback = function () {
|
|
410
|
+
var self = this; if (this.style.display === '') this.style.display = 'block';
|
|
411
|
+
requestAnimationFrame(function () { self._lg = apply(self, readOpts(self)); });
|
|
412
|
+
};
|
|
413
|
+
C.prototype.attributeChangedCallback = function () { if (this._lg) this._lg.update(readOpts(this)); };
|
|
414
|
+
C.prototype.disconnectedCallback = function () { if (this._lg) { this._lg.destroy(); this._lg = null; } };
|
|
415
|
+
try { customElements.define('glass-kit', C); } catch (e) {}
|
|
416
|
+
}
|
|
417
|
+
if (typeof window !== 'undefined') { if (document.readyState !== 'loading') defineElement(); else document.addEventListener('DOMContentLoaded', defineElement); }
|
|
418
|
+
|
|
419
|
+
return { apply: apply, defineElement: defineElement, isChromium: isChromium, pickMode: pickMode, DEFAULTS: DEFAULTS, version: '1.0.0' };
|
|
420
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glasskit-js",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Drop-in Apple/Figma 'Liquid Glass' for any element — switch between pure CSS, SVG displacement (live backdrop), cross-browser clone-mode, or WebGL. Real refraction, chromatic aberration, specular highlight. Zero dependencies. Class + <liquid-glass> web component.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"liquid-glass",
|
|
7
|
+
"glassmorphism",
|
|
8
|
+
"glass",
|
|
9
|
+
"backdrop-filter",
|
|
10
|
+
"refraction",
|
|
11
|
+
"displacement-map",
|
|
12
|
+
"feDisplacementMap",
|
|
13
|
+
"chromatic-aberration",
|
|
14
|
+
"webgl",
|
|
15
|
+
"apple",
|
|
16
|
+
"ios26",
|
|
17
|
+
"figma",
|
|
18
|
+
"ui",
|
|
19
|
+
"web-component",
|
|
20
|
+
"frosted-glass"
|
|
21
|
+
],
|
|
22
|
+
"main": "./liquid-glass.js",
|
|
23
|
+
"module": "./liquid-glass.js",
|
|
24
|
+
"unpkg": "./liquid-glass.js",
|
|
25
|
+
"jsdelivr": "./liquid-glass.js",
|
|
26
|
+
"types": "./liquid-glass.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./liquid-glass.d.ts",
|
|
30
|
+
"import": "./liquid-glass.js",
|
|
31
|
+
"require": "./liquid-glass.js",
|
|
32
|
+
"default": "./liquid-glass.js"
|
|
33
|
+
},
|
|
34
|
+
"./react": {
|
|
35
|
+
"types": "./LiquidGlass.d.ts",
|
|
36
|
+
"default": "./LiquidGlass.mjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"liquid-glass.js",
|
|
41
|
+
"liquid-glass.d.ts",
|
|
42
|
+
"LiquidGlass.mjs",
|
|
43
|
+
"LiquidGlass.d.ts",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"sideEffects": [
|
|
47
|
+
"./liquid-glass.js"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"bench": "node bench/run-playwright.mjs"
|
|
51
|
+
},
|
|
5
52
|
"license": "MIT",
|
|
6
|
-
"
|
|
53
|
+
"author": "amanblog",
|
|
54
|
+
"homepage": "https://amanblog.github.io/glasskit/",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/amanblog/glasskit.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/amanblog/glasskit/issues"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"playwright-core": "^1.61.1"
|
|
64
|
+
}
|
|
7
65
|
}
|