@mizuui/liquid-glass-core 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 +49 -0
- package/index.d.ts +38 -0
- package/package.json +29 -0
- package/src/displacement.js +92 -0
- package/src/filter.js +136 -0
- package/src/glass.js +137 -0
- package/src/index.js +3 -0
- package/src/styles.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# mizuui-liquid-glass-core
|
|
2
|
+
|
|
3
|
+
The rendering engine behind the liquid glass effect. No framework, no build step — plain ESM.
|
|
4
|
+
|
|
5
|
+
It generates a displacement map for a rounded rectangle and feeds it to an SVG
|
|
6
|
+
`feDisplacementMap` that runs inside `backdrop-filter`, so the content behind the
|
|
7
|
+
element is genuinely refracted through a bevel. There is no blur by default: this
|
|
8
|
+
is glass, not frost.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install mizuui-liquid-glass-core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { createLiquidGlass } from "mizuui-liquid-glass-core";
|
|
16
|
+
|
|
17
|
+
const glass = createLiquidGlass(document.querySelector("#panel"), {
|
|
18
|
+
radius: 28,
|
|
19
|
+
depth: 12,
|
|
20
|
+
strength: 70,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
glass.update({ strength: 100 });
|
|
24
|
+
glass.destroy();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
| Option | Default | Description |
|
|
30
|
+
| --------------------- | ------- | ------------------------------------------------------- |
|
|
31
|
+
| `radius` | `24` | Corner radius in px. |
|
|
32
|
+
| `depth` | `10` | Thickness of the refracting bevel in px. |
|
|
33
|
+
| `strength` | `60` | Maximum backdrop displacement in px. |
|
|
34
|
+
| `falloff` | `2` | Bevel curve. Higher pushes the bend towards the edge. |
|
|
35
|
+
| `chromaticAberration` | `3` | Per-channel offset in px. |
|
|
36
|
+
| `blur` | `0` | Backdrop blur. Anything above 0 starts looking frosted. |
|
|
37
|
+
| `saturation` | `1.6` | Backdrop saturation multiplier. |
|
|
38
|
+
| `brightness` | `1.04` | Backdrop brightness multiplier. |
|
|
39
|
+
| `specular` | `1` | Specular rim intensity, `0` disables it. |
|
|
40
|
+
| `resolution` | `512` | Max pixel size of the generated displacement map. |
|
|
41
|
+
| `fallbackBlur` | `8` | Blur used where SVG backdrop filters are unsupported. |
|
|
42
|
+
|
|
43
|
+
The instance resizes itself via `ResizeObserver`; call `refresh()` if you move it
|
|
44
|
+
in a way that observer cannot see.
|
|
45
|
+
|
|
46
|
+
## Browser support
|
|
47
|
+
|
|
48
|
+
`backdrop-filter: url(#filter)` currently works in Chromium. Safari and Firefox
|
|
49
|
+
fall back to `fallbackBlur` plus the specular rim.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface LiquidGlassOptions {
|
|
2
|
+
radius?: number
|
|
3
|
+
depth?: number
|
|
4
|
+
strength?: number
|
|
5
|
+
falloff?: number
|
|
6
|
+
chromaticAberration?: number
|
|
7
|
+
blur?: number
|
|
8
|
+
saturation?: number
|
|
9
|
+
brightness?: number
|
|
10
|
+
specular?: number
|
|
11
|
+
resolution?: number
|
|
12
|
+
fallbackBlur?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LiquidGlassInstance {
|
|
16
|
+
readonly options: Required<LiquidGlassOptions>
|
|
17
|
+
update(options: LiquidGlassOptions): void
|
|
18
|
+
refresh(): void
|
|
19
|
+
destroy(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare const defaultOptions: Required<LiquidGlassOptions>
|
|
23
|
+
|
|
24
|
+
export declare function createLiquidGlass(
|
|
25
|
+
element: HTMLElement,
|
|
26
|
+
options?: LiquidGlassOptions,
|
|
27
|
+
): LiquidGlassInstance
|
|
28
|
+
|
|
29
|
+
export declare function createDisplacementMap(options: {
|
|
30
|
+
width: number
|
|
31
|
+
height: number
|
|
32
|
+
radius?: number
|
|
33
|
+
depth?: number
|
|
34
|
+
falloff?: number
|
|
35
|
+
resolution?: number
|
|
36
|
+
}): { href: string; width: number; height: number }
|
|
37
|
+
|
|
38
|
+
export declare function injectStyles(): void
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mizuui/liquid-glass-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic liquid glass (real backdrop refraction, not frosted blur).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"module": "./src/index.js",
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"import": "./src/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"liquid-glass",
|
|
24
|
+
"glassmorphism",
|
|
25
|
+
"refraction",
|
|
26
|
+
"backdrop-filter",
|
|
27
|
+
"svg-filter"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const clamp = (value, min, max) => (value < min ? min : value > max ? max : value)
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Signed distance (negative = inside) to a rounded rectangle centred on the
|
|
5
|
+
* origin, plus the outward-pointing unit normal at that point.
|
|
6
|
+
*/
|
|
7
|
+
function roundedRectSDF(px, py, halfWidth, halfHeight, radius) {
|
|
8
|
+
const qx = Math.abs(px) - (halfWidth - radius)
|
|
9
|
+
const qy = Math.abs(py) - (halfHeight - radius)
|
|
10
|
+
const mx = Math.max(qx, 0)
|
|
11
|
+
const my = Math.max(qy, 0)
|
|
12
|
+
const len = Math.hypot(mx, my)
|
|
13
|
+
const distance = Math.min(Math.max(qx, qy), 0) + len - radius
|
|
14
|
+
|
|
15
|
+
const sx = px < 0 ? -1 : 1
|
|
16
|
+
const sy = py < 0 ? -1 : 1
|
|
17
|
+
|
|
18
|
+
let nx
|
|
19
|
+
let ny
|
|
20
|
+
if (len > 0) {
|
|
21
|
+
nx = (sx * mx) / len
|
|
22
|
+
ny = (sy * my) / len
|
|
23
|
+
} else if (qx > qy) {
|
|
24
|
+
nx = sx
|
|
25
|
+
ny = 0
|
|
26
|
+
} else {
|
|
27
|
+
nx = 0
|
|
28
|
+
ny = sy
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { distance, nx, ny }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Renders the refraction map consumed by `feDisplacementMap`.
|
|
36
|
+
*
|
|
37
|
+
* Red encodes the horizontal offset, green the vertical one, both biased around
|
|
38
|
+
* 128 (= no displacement). The offset ramps up towards the edges of the rounded
|
|
39
|
+
* rectangle so the backdrop is bent like it would be by a thick glass bevel.
|
|
40
|
+
*
|
|
41
|
+
* @returns {{ href: string, width: number, height: number }}
|
|
42
|
+
*/
|
|
43
|
+
export function createDisplacementMap({
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
radius = 24,
|
|
47
|
+
depth = 8,
|
|
48
|
+
falloff = 2,
|
|
49
|
+
resolution = 512,
|
|
50
|
+
}) {
|
|
51
|
+
const aspectScale = Math.min(1, resolution / Math.max(width, height, 1))
|
|
52
|
+
const w = Math.max(1, Math.round(width * aspectScale))
|
|
53
|
+
const h = Math.max(1, Math.round(height * aspectScale))
|
|
54
|
+
|
|
55
|
+
const canvas = document.createElement('canvas')
|
|
56
|
+
canvas.width = w
|
|
57
|
+
canvas.height = h
|
|
58
|
+
|
|
59
|
+
const ctx = canvas.getContext('2d')
|
|
60
|
+
const image = ctx.createImageData(w, h)
|
|
61
|
+
const data = image.data
|
|
62
|
+
|
|
63
|
+
const halfW = w / 2
|
|
64
|
+
const halfH = h / 2
|
|
65
|
+
const r = clamp(radius * aspectScale, 0, Math.min(halfW, halfH))
|
|
66
|
+
const edge = clamp(depth * aspectScale, 0.5, Math.min(halfW, halfH))
|
|
67
|
+
const curve = Math.max(0.1, falloff)
|
|
68
|
+
|
|
69
|
+
for (let y = 0; y < h; y++) {
|
|
70
|
+
for (let x = 0; x < w; x++) {
|
|
71
|
+
const px = x + 0.5 - halfW
|
|
72
|
+
const py = y + 0.5 - halfH
|
|
73
|
+
const { distance, nx, ny } = roundedRectSDF(px, py, halfW, halfH, r)
|
|
74
|
+
|
|
75
|
+
let amount = 0
|
|
76
|
+
if (distance < 0) {
|
|
77
|
+
const inset = -distance
|
|
78
|
+
amount = Math.pow(1 - clamp(inset / edge, 0, 1), curve)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const i = (y * w + x) * 4
|
|
82
|
+
data[i] = clamp(Math.round(128 + nx * amount * 127), 0, 255)
|
|
83
|
+
data[i + 1] = clamp(Math.round(128 + ny * amount * 127), 0, 255)
|
|
84
|
+
data[i + 2] = 128
|
|
85
|
+
data[i + 3] = 255
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ctx.putImageData(image, 0, 0)
|
|
90
|
+
|
|
91
|
+
return { href: canvas.toDataURL(), width: w, height: h }
|
|
92
|
+
}
|
package/src/filter.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
2
|
+
const XLINK_NS = 'http://www.w3.org/1999/xlink'
|
|
3
|
+
|
|
4
|
+
let defsHost = null
|
|
5
|
+
|
|
6
|
+
function getHost() {
|
|
7
|
+
if (defsHost && defsHost.isConnected) return defsHost
|
|
8
|
+
|
|
9
|
+
defsHost = document.createElementNS(SVG_NS, 'svg')
|
|
10
|
+
defsHost.setAttribute('aria-hidden', 'true')
|
|
11
|
+
defsHost.setAttribute('focusable', 'false')
|
|
12
|
+
defsHost.dataset.liquidGlass = 'filters'
|
|
13
|
+
defsHost.style.cssText =
|
|
14
|
+
'position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;'
|
|
15
|
+
document.body.appendChild(defsHost)
|
|
16
|
+
|
|
17
|
+
return defsHost
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function el(name, attrs) {
|
|
21
|
+
const node = document.createElementNS(SVG_NS, name)
|
|
22
|
+
for (const key in attrs) node.setAttribute(key, attrs[key])
|
|
23
|
+
return node
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Keeps only one colour channel, optionally discarding alpha so passes can be summed. */
|
|
27
|
+
function channelMatrix(channel, keepAlpha) {
|
|
28
|
+
const rows = {
|
|
29
|
+
r: '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0',
|
|
30
|
+
g: '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0',
|
|
31
|
+
b: '0 0 0 0 0 0 0 0 0 0 0 0 1 0 0',
|
|
32
|
+
}
|
|
33
|
+
return `${rows[channel]} 0 0 0 ${keepAlpha ? 1 : 0} 0`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates an SVG filter that refracts whatever is behind the element.
|
|
38
|
+
* Returns a handle used by the glass controller.
|
|
39
|
+
*/
|
|
40
|
+
export function createGlassFilter(id) {
|
|
41
|
+
const filter = el('filter', {
|
|
42
|
+
id,
|
|
43
|
+
'color-interpolation-filters': 'sRGB',
|
|
44
|
+
x: '0',
|
|
45
|
+
y: '0',
|
|
46
|
+
width: '100%',
|
|
47
|
+
height: '100%',
|
|
48
|
+
})
|
|
49
|
+
getHost().appendChild(filter)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
url: `url(#${id})`,
|
|
53
|
+
|
|
54
|
+
update({ map, scale, aberration, blur }) {
|
|
55
|
+
filter.replaceChildren()
|
|
56
|
+
|
|
57
|
+
const image = el('feImage', {
|
|
58
|
+
result: 'map',
|
|
59
|
+
preserveAspectRatio: 'none',
|
|
60
|
+
x: '0',
|
|
61
|
+
y: '0',
|
|
62
|
+
width: String(map.displayWidth),
|
|
63
|
+
height: String(map.displayHeight),
|
|
64
|
+
})
|
|
65
|
+
image.setAttribute('href', map.href)
|
|
66
|
+
image.setAttributeNS(XLINK_NS, 'xlink:href', map.href)
|
|
67
|
+
filter.appendChild(image)
|
|
68
|
+
|
|
69
|
+
const displace = (scaleValue, result) =>
|
|
70
|
+
el('feDisplacementMap', {
|
|
71
|
+
in: 'SourceGraphic',
|
|
72
|
+
in2: 'map',
|
|
73
|
+
scale: String(scaleValue),
|
|
74
|
+
xChannelSelector: 'R',
|
|
75
|
+
yChannelSelector: 'G',
|
|
76
|
+
result,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
let last = 'refracted'
|
|
80
|
+
|
|
81
|
+
if (aberration > 0) {
|
|
82
|
+
const channels = [
|
|
83
|
+
['r', scale + aberration],
|
|
84
|
+
['g', scale],
|
|
85
|
+
['b', scale - aberration],
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
channels.forEach(([channel, channelScale], index) => {
|
|
89
|
+
filter.appendChild(displace(channelScale, `pass-${channel}`))
|
|
90
|
+
filter.appendChild(
|
|
91
|
+
el('feColorMatrix', {
|
|
92
|
+
in: `pass-${channel}`,
|
|
93
|
+
type: 'matrix',
|
|
94
|
+
values: channelMatrix(channel, index === 0),
|
|
95
|
+
result: `channel-${channel}`,
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
filter.appendChild(
|
|
101
|
+
el('feComposite', {
|
|
102
|
+
in: 'channel-r',
|
|
103
|
+
in2: 'channel-g',
|
|
104
|
+
operator: 'arithmetic',
|
|
105
|
+
k2: '1',
|
|
106
|
+
k3: '1',
|
|
107
|
+
result: 'rg',
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
filter.appendChild(
|
|
111
|
+
el('feComposite', {
|
|
112
|
+
in: 'rg',
|
|
113
|
+
in2: 'channel-b',
|
|
114
|
+
operator: 'arithmetic',
|
|
115
|
+
k2: '1',
|
|
116
|
+
k3: '1',
|
|
117
|
+
result: 'refracted',
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
} else {
|
|
121
|
+
filter.appendChild(displace(scale, 'refracted'))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (blur > 0) {
|
|
125
|
+
filter.appendChild(
|
|
126
|
+
el('feGaussianBlur', { in: last, stdDeviation: String(blur), result: 'blurred' }),
|
|
127
|
+
)
|
|
128
|
+
last = 'blurred'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
destroy() {
|
|
133
|
+
filter.remove()
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/glass.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createDisplacementMap } from './displacement.js'
|
|
2
|
+
import { createGlassFilter } from './filter.js'
|
|
3
|
+
import { injectStyles } from './styles.js'
|
|
4
|
+
|
|
5
|
+
export const defaultOptions = {
|
|
6
|
+
/** Corner radius in px. Keep in sync with any border-radius you set yourself. */
|
|
7
|
+
radius: 30,
|
|
8
|
+
/** Thickness of the refracting bevel in px. */
|
|
9
|
+
depth: 35,
|
|
10
|
+
/** Maximum backdrop displacement in px. This is the strength of the lensing. */
|
|
11
|
+
strength: 120,
|
|
12
|
+
/** Falloff curve of the bevel. Higher = the bend is pushed closer to the edge. */
|
|
13
|
+
falloff: 2,
|
|
14
|
+
/** Per-channel offset in px. Gives the prism fringe real glass has. */
|
|
15
|
+
chromaticAberration: 0,
|
|
16
|
+
/** Backdrop blur in px. Leave at 0 — blur is frosted glass, not liquid glass. */
|
|
17
|
+
blur: 15,
|
|
18
|
+
/** Backdrop saturation multiplier. */
|
|
19
|
+
saturation: 3,
|
|
20
|
+
/** Backdrop brightness multiplier. */
|
|
21
|
+
brightness: 1.04,
|
|
22
|
+
/** Specular rim intensity, 0 disables it. */
|
|
23
|
+
specular: 1,
|
|
24
|
+
/** Max pixel size of the generated displacement map. */
|
|
25
|
+
resolution: 512,
|
|
26
|
+
/** Blur used when the browser cannot run SVG filters in a backdrop-filter. */
|
|
27
|
+
fallbackBlur: 8,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let uid = 0
|
|
31
|
+
|
|
32
|
+
function supportsBackdropFilterUrl() {
|
|
33
|
+
if (typeof CSS === 'undefined' || !CSS.supports) return false
|
|
34
|
+
// Firefox parses backdrop-filter but silently drops url() references.
|
|
35
|
+
return (
|
|
36
|
+
CSS.supports('backdrop-filter', 'url(#x)') &&
|
|
37
|
+
!/\bGecko\/\b/.test(navigator.userAgent ?? '')
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Turns an element into a pane of liquid glass: everything behind it is
|
|
43
|
+
* refracted through a generated bevel instead of being blurred.
|
|
44
|
+
*
|
|
45
|
+
* @param {HTMLElement} element
|
|
46
|
+
* @param {Partial<typeof defaultOptions>} [options]
|
|
47
|
+
*/
|
|
48
|
+
export function createLiquidGlass(element, options = {}) {
|
|
49
|
+
injectStyles()
|
|
50
|
+
|
|
51
|
+
let config = { ...defaultOptions, ...options }
|
|
52
|
+
let destroyed = false
|
|
53
|
+
let frame = 0
|
|
54
|
+
|
|
55
|
+
const useFilter = supportsBackdropFilterUrl()
|
|
56
|
+
const filter = useFilter ? createGlassFilter(`liquid-glass-${++uid}`) : null
|
|
57
|
+
|
|
58
|
+
element.classList.add('liquid-glass')
|
|
59
|
+
|
|
60
|
+
function applyBackdrop() {
|
|
61
|
+
const layers = []
|
|
62
|
+
if (config.brightness !== 1) layers.push(`brightness(${config.brightness})`)
|
|
63
|
+
if (config.saturation !== 1) layers.push(`saturate(${config.saturation})`)
|
|
64
|
+
|
|
65
|
+
if (filter) {
|
|
66
|
+
if (config.blur > 0) layers.push(`blur(${config.blur}px)`)
|
|
67
|
+
layers.push(filter.url)
|
|
68
|
+
} else if (config.fallbackBlur > 0) {
|
|
69
|
+
layers.push(`blur(${config.fallbackBlur}px)`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
element.style.setProperty('--lg-backdrop', layers.join(' ') || 'none')
|
|
73
|
+
element.style.setProperty('--lg-radius', `${config.radius}px`)
|
|
74
|
+
element.style.setProperty('--lg-specular', String(config.specular))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function render() {
|
|
78
|
+
frame = 0
|
|
79
|
+
if (destroyed || !filter) return
|
|
80
|
+
|
|
81
|
+
const { width, height } = element.getBoundingClientRect()
|
|
82
|
+
if (width < 1 || height < 1) return
|
|
83
|
+
|
|
84
|
+
const map = createDisplacementMap({
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
radius: config.radius,
|
|
88
|
+
depth: config.depth,
|
|
89
|
+
falloff: config.falloff,
|
|
90
|
+
resolution: config.resolution,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
filter.update({
|
|
94
|
+
map: { ...map, displayWidth: width, displayHeight: height },
|
|
95
|
+
scale: config.strength,
|
|
96
|
+
aberration: config.chromaticAberration,
|
|
97
|
+
blur: 0,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function schedule() {
|
|
102
|
+
if (destroyed || frame) return
|
|
103
|
+
frame = requestAnimationFrame(render)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null
|
|
107
|
+
observer?.observe(element)
|
|
108
|
+
|
|
109
|
+
applyBackdrop()
|
|
110
|
+
schedule()
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
get options() {
|
|
114
|
+
return { ...config }
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
update(next = {}) {
|
|
118
|
+
config = { ...config, ...next }
|
|
119
|
+
applyBackdrop()
|
|
120
|
+
schedule()
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
refresh: schedule,
|
|
124
|
+
|
|
125
|
+
destroy() {
|
|
126
|
+
if (destroyed) return
|
|
127
|
+
destroyed = true
|
|
128
|
+
if (frame) cancelAnimationFrame(frame)
|
|
129
|
+
observer?.disconnect()
|
|
130
|
+
filter?.destroy()
|
|
131
|
+
element.classList.remove('liquid-glass')
|
|
132
|
+
element.style.removeProperty('--lg-backdrop')
|
|
133
|
+
element.style.removeProperty('--lg-radius')
|
|
134
|
+
element.style.removeProperty('--lg-specular')
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/index.js
ADDED
package/src/styles.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const STYLE_ID = 'mizuui-liquid-glass-core-styles'
|
|
2
|
+
|
|
3
|
+
const CSS = `
|
|
4
|
+
.liquid-glass {
|
|
5
|
+
position: relative;
|
|
6
|
+
isolation: isolate;
|
|
7
|
+
border-radius: var(--lg-radius, 24px);
|
|
8
|
+
-webkit-backdrop-filter: var(--lg-backdrop, none);
|
|
9
|
+
backdrop-filter: var(--lg-backdrop, none);
|
|
10
|
+
transform: translateZ(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.liquid-glass::after {
|
|
14
|
+
content: '';
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
z-index: 1;
|
|
18
|
+
border-radius: inherit;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
box-shadow:
|
|
21
|
+
inset 2px 2px 1px -2px rgba(255, 255, 255, calc(0.9 * var(--lg-specular, 1))),
|
|
22
|
+
inset -1.5px -1.5px 1px -1.5px rgba(255, 255, 255, calc(0.6 * var(--lg-specular, 1))),
|
|
23
|
+
inset 0 0 6px 1px rgba(255, 255, 255, calc(0.25 * var(--lg-specular, 1)));
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
export function injectStyles() {
|
|
28
|
+
if (typeof document === 'undefined') return
|
|
29
|
+
if (document.getElementById(STYLE_ID)) return
|
|
30
|
+
|
|
31
|
+
const style = document.createElement('style')
|
|
32
|
+
style.id = STYLE_ID
|
|
33
|
+
style.textContent = CSS
|
|
34
|
+
document.head.appendChild(style)
|
|
35
|
+
}
|