liqvued 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -16
- package/dist/liqvued.css +1 -1
- package/dist/liqvued.mjs +199 -153
- package/dist/liqvued.umd.js +1 -1
- package/package.json +1 -1
- package/src/Liqvued.vue +133 -81
- package/src/shape.test.ts +54 -0
- package/src/shape.ts +42 -0
- package/src/surfaces.ts +13 -0
- package/src/types.ts +10 -1
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ Inspired by [Liquid Glass CSS/SVG](https://kube.io/blog/liquid-glass-css-svg/)
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
17
|
- **Real-time SVG filter generation** — displacement maps computed via Canvas API
|
|
18
|
-
- **Multiple surface types** — convex, concave, lip
|
|
19
|
-
- **Configurable optics** — bezel width, corner radius, displacement thickness, refraction
|
|
18
|
+
- **Multiple surface types** — convex, concave, lip, bowl, bevel, saddle, ripple, noise, asymmetric
|
|
19
|
+
- **Configurable optics** — bezel width, corner radius, displacement thickness, refraction, magnification, focus, glare angle
|
|
20
20
|
- **Built-in glassmorphism styling** — gradient backgrounds, inset shadows, highlight overlays
|
|
21
21
|
- **TypeScript** — full type definitions
|
|
22
22
|
- **Lightweight** — minimal runtime dependencies beyond Vue
|
|
@@ -53,25 +53,35 @@ import { Liqvued } from 'liqvued'
|
|
|
53
53
|
|
|
54
54
|
## Props
|
|
55
55
|
|
|
56
|
-
| Prop | Type | Default | Description
|
|
57
|
-
|
|
58
|
-
| radius | number | 32 | Corner radius in pixels
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|-----------------|---------------------|------------|----------------------------------------------------------------------------------------|
|
|
58
|
+
| radius | number | 32 | Corner radius in pixels |
|
|
59
|
+
| borderRadius | string | — | Per-corner border-radius CSS value (e.g. `"10px 20px 10px 20px"`); overrides `radius` |
|
|
60
|
+
| bezel | number | 22 | Width of the displacement bezel in pixels |
|
|
61
|
+
| thickness | number | 42 | Maximum displacement magnitude in pixels |
|
|
62
|
+
| refraction | number | 1 | Refraction index multiplier |
|
|
63
|
+
| magnification | number | 0 | Center magnification amount. Positive values enlarge, negative values shrink |
|
|
64
|
+
| magnificationFocus | number | 0.82 | Focus radius of the magnification area. Lower values concentrate the effect near center |
|
|
65
|
+
| blur | number | 0.4 | CSS backdrop-filter blur amount |
|
|
66
|
+
| surface | string | `'convex'` | Glass surface profile shape |
|
|
67
|
+
| specularOpacity | number | 0.45 | Opacity of the specular highlight |
|
|
68
|
+
| glareAngle | number | -60 | Light source angle in degrees for specular highlight |
|
|
69
|
+
| glassBackground | string | — | Glass panel background color (auto-derived from `asProps.color` when available) |
|
|
70
|
+
| fallbackOnly | boolean | `false` | When `true`, disables the SVG displacement effect and uses only CSS blur |
|
|
71
|
+
| as | string \| Component | `'div'` | HTML tag or Vue component to render as (rendered as a child of the glass root) |
|
|
72
|
+
| asProps | object | `{}` | Props passed to the rendered element/component |
|
|
69
73
|
|
|
70
74
|
### Surface Types
|
|
71
75
|
|
|
72
76
|
- `convex` — squircle convex profile — soft pill-like edges
|
|
73
77
|
- `concave` — inverted convex — inward-curving dish-like refraction
|
|
74
78
|
- `lip` — smooth transition from convex at center to concave at edge
|
|
79
|
+
- `bowl` — deeper concave profile with a more even inward pull
|
|
80
|
+
- `bevel` — linear edge ramp for harder, cleaner refraction
|
|
81
|
+
- `saddle` — mirrored S-curve that shifts direction through the middle
|
|
82
|
+
- `ripple` — small wave modulation across the edge band
|
|
83
|
+
- `noise` — irregular organic edge distortion
|
|
84
|
+
- `asymmetric` — biased profile with more weight toward one side of the edge band
|
|
75
85
|
|
|
76
86
|
## Dynamic Element Rendering
|
|
77
87
|
|
|
@@ -96,7 +106,7 @@ The fallback applies `backdrop-filter: blur(12px)` with a subtle border and shad
|
|
|
96
106
|
|
|
97
107
|
## How It Works
|
|
98
108
|
|
|
99
|
-
The component generates a per-pixel displacement map as an RGBA PNG (red/green channels encode X/Y displacement vectors), injects it into an SVG `<feDisplacementMap>` filter, and applies it via `backdrop-filter` combined with CSS blur. The surface profile functions
|
|
109
|
+
The component generates a per-pixel displacement map as an RGBA PNG (red/green channels encode X/Y displacement vectors), injects it into an SVG `<feDisplacementMap>` filter, and applies it via `backdrop-filter` combined with CSS blur. The surface profile functions define the edge refraction slope along the bezel, while `magnification` and `magnificationFocus` control an optional center lens effect. A separate specular highlight map is generated and composited via `feBlend` to simulate surface lighting.
|
|
100
110
|
|
|
101
111
|
## License
|
|
102
112
|
|
package/dist/liqvued.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.liquid-glass[data-v-
|
|
1
|
+
.liquid-glass[data-v-56a7fa97]{isolation:isolate;position:relative;overflow:hidden}.liquid-glass__svg[data-v-56a7fa97]{pointer-events:none;width:100%;height:100%;position:absolute;inset:0}@supports not ((-webkit-backdrop-filter:url("#x")) or (backdrop-filter:url("#x"))){.liquid-glass[data-v-56a7fa97]{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/liqvued.mjs
CHANGED
|
@@ -1,35 +1,56 @@
|
|
|
1
|
-
import { computed as
|
|
2
|
-
|
|
1
|
+
import { computed as C, createBlock as he, createCommentVNode as fe, createElementBlock as Z, createElementVNode as k, defineComponent as pe, guardReactiveProps as ve, inject as ge, nextTick as ee, normalizeProps as me, normalizeStyle as Me, onBeforeUnmount as be, onMounted as ye, openBlock as W, provide as xe, ref as w, renderSlot as _e, resolveDynamicComponent as ke, unref as we, watch as Ie, withCtx as Re } from "vue";
|
|
2
|
+
function q(r, a, s, o, l) {
|
|
3
|
+
const b = Math.min(l, Math.min(s, o) / 2), y = s / 2 - b, x = o / 2 - b, c = Math.abs(r - s / 2) - y, S = Math.abs(a - o / 2) - x;
|
|
4
|
+
return -(Math.min(Math.max(c, S), 0) + Math.hypot(Math.max(c, 0), Math.max(S, 0)) - b);
|
|
5
|
+
}
|
|
6
|
+
function Se(r, a, s, o, l) {
|
|
7
|
+
const y = q(r - 1.5, a, s, o, l) - q(r + 1.5, a, s, o, l), x = q(r, a - 1.5, s, o, l) - q(r, a + 1.5, s, o, l), c = Math.hypot(y, x);
|
|
8
|
+
return c < 1e-10 ? {
|
|
9
|
+
x: 0,
|
|
10
|
+
y: -1
|
|
11
|
+
} : {
|
|
12
|
+
x: y / c,
|
|
13
|
+
y: x / c
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
var Be = {
|
|
3
17
|
key: 0,
|
|
4
18
|
class: "liquid-glass__svg",
|
|
5
19
|
"aria-hidden": "true"
|
|
6
|
-
},
|
|
20
|
+
}, $e = [
|
|
7
21
|
"href",
|
|
8
22
|
"width",
|
|
9
23
|
"height"
|
|
10
|
-
],
|
|
24
|
+
], Ce = ["scale"], qe = [
|
|
11
25
|
"href",
|
|
12
26
|
"width",
|
|
13
27
|
"height"
|
|
14
|
-
],
|
|
28
|
+
], Pe = /* @__PURE__ */ pe({
|
|
15
29
|
__name: "Liqvued",
|
|
16
30
|
props: {
|
|
17
31
|
as: { default: "div" },
|
|
18
32
|
asProps: { default: () => ({}) },
|
|
19
33
|
radius: { default: 32 },
|
|
34
|
+
borderRadius: {},
|
|
20
35
|
bezel: { default: 22 },
|
|
21
36
|
thickness: { default: 42 },
|
|
22
37
|
refraction: { default: 1 },
|
|
38
|
+
magnification: { default: 0 },
|
|
39
|
+
magnificationFocus: { default: 0.82 },
|
|
23
40
|
blur: { default: 0.4 },
|
|
24
41
|
surface: { default: "convex" },
|
|
25
42
|
specularOpacity: { default: 0.45 },
|
|
26
43
|
glareAngle: { default: -60 },
|
|
27
|
-
glassBackground: { default: void 0 }
|
|
44
|
+
glassBackground: { default: void 0 },
|
|
45
|
+
fallbackOnly: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: !1
|
|
48
|
+
}
|
|
28
49
|
},
|
|
29
|
-
setup(
|
|
30
|
-
const a =
|
|
31
|
-
|
|
32
|
-
const
|
|
50
|
+
setup(r) {
|
|
51
|
+
const a = r, s = w(null), o = w(1), l = w(1), b = w(""), y = w(""), x = w(1), c = typeof CSS < "u" && typeof navigator < "u" && CSS.supports?.("backdrop-filter", "url(#x)") && /Chrome/.test(navigator.userAgent), S = `${`lg-${Math.random().toString(36).slice(2)}`}-filter`, N = ge("_liqvued", !1);
|
|
52
|
+
xe("_liqvued", !0);
|
|
53
|
+
const I = w(!1), V = {
|
|
33
54
|
0: 0,
|
|
34
55
|
sm: 4,
|
|
35
56
|
md: 8,
|
|
@@ -39,91 +60,107 @@ var ye = {
|
|
|
39
60
|
circle: 9999,
|
|
40
61
|
shaped: 16
|
|
41
62
|
};
|
|
42
|
-
function
|
|
43
|
-
return
|
|
63
|
+
function j() {
|
|
64
|
+
return s.value;
|
|
44
65
|
}
|
|
45
|
-
function
|
|
66
|
+
function i(e, t = 0, n = 1) {
|
|
46
67
|
return Math.max(t, Math.min(n, e));
|
|
47
68
|
}
|
|
48
|
-
function
|
|
49
|
-
return e =
|
|
50
|
-
}
|
|
51
|
-
function I(e) {
|
|
52
|
-
return Math.pow(1 - Math.pow(1 - f(e), 4), 1 / 4);
|
|
53
|
-
}
|
|
54
|
-
function G(e) {
|
|
55
|
-
if (a.surface === "concave") return 1 - I(e);
|
|
56
|
-
if (a.surface === "lip") {
|
|
57
|
-
const t = X(e);
|
|
58
|
-
return I(e) * (1 - t) + (1 - I(e)) * t;
|
|
59
|
-
}
|
|
60
|
-
return I(e);
|
|
69
|
+
function B(e) {
|
|
70
|
+
return e = i(e), e * e * e * (e * (e * 6 - 15) + 10);
|
|
61
71
|
}
|
|
62
|
-
function
|
|
63
|
-
return (
|
|
72
|
+
function P(e) {
|
|
73
|
+
return Math.pow(1 - Math.pow(1 - i(e), 4), 1 / 4);
|
|
64
74
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
return Math.tan(s - g);
|
|
75
|
+
function te(e, t, n) {
|
|
76
|
+
return i(e + Math.sin(i(e) * Math.PI * t) * n);
|
|
68
77
|
}
|
|
69
|
-
function
|
|
70
|
-
const
|
|
71
|
-
return
|
|
78
|
+
function ae(e) {
|
|
79
|
+
const t = i(e);
|
|
80
|
+
return i(t + Math.sin(t * 39.1) * 0.06 + Math.sin(t * 91.7) * 0.035);
|
|
72
81
|
}
|
|
73
|
-
function
|
|
74
|
-
const g = f
|
|
75
|
-
let o = e - g, l = t - h;
|
|
76
|
-
if (o === 0 && l === 0) {
|
|
77
|
-
const m = e, i = n - e, d = t, b = s - t, u = Math.min(m, i, d, b);
|
|
78
|
-
u === m ? o = -1 : u === i ? o = 1 : u === d ? l = -1 : l = 1;
|
|
79
|
-
}
|
|
80
|
-
const c = Math.hypot(o, l) || 1;
|
|
82
|
+
function ne(e, t, n, f, m, R) {
|
|
83
|
+
const v = n / 2, g = f / 2, $ = Math.max(1, n / 2), M = Math.max(1, f / 2), z = (e - v) / $, D = (t - g) / M, u = Math.hypot(z, D), d = i(R, 0.2, 1.4), h = Math.pow(B(1 - i(u / d)), 1.6), p = m >= 0 ? 1 + m * h : 1 / (1 + Math.abs(m) * h);
|
|
81
84
|
return {
|
|
82
|
-
x:
|
|
83
|
-
y:
|
|
85
|
+
x: (e - v) / p - (e - v),
|
|
86
|
+
y: (t - g) / p - (t - g)
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
|
-
function
|
|
89
|
+
function H(e) {
|
|
90
|
+
const t = i(e);
|
|
91
|
+
switch (a.surface) {
|
|
92
|
+
case "concave":
|
|
93
|
+
case "bowl":
|
|
94
|
+
return 1 - P(t);
|
|
95
|
+
case "lip":
|
|
96
|
+
return P(t) * (1 - B(t)) + (1 - P(t)) * B(t);
|
|
97
|
+
case "bevel":
|
|
98
|
+
return t;
|
|
99
|
+
case "saddle":
|
|
100
|
+
return 0.5 + Math.sin((t - 0.5) * Math.PI) * 0.5;
|
|
101
|
+
case "ripple":
|
|
102
|
+
return te(t, 3, 0.12);
|
|
103
|
+
case "noise":
|
|
104
|
+
return ae(t);
|
|
105
|
+
case "asymmetric":
|
|
106
|
+
return i(Math.pow(t, 0.65) * 0.65 + B(t) * 0.35);
|
|
107
|
+
case "convex":
|
|
108
|
+
return P(t);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function re(e) {
|
|
112
|
+
return (H(e + 1e-3) - H(e - 1e-3)) / (2 * 1e-3);
|
|
113
|
+
}
|
|
114
|
+
function oe(e) {
|
|
115
|
+
const f = Math.atan(e), m = 1 / 1.5 * Math.sin(f), R = Math.asin(i(m, -1, 1));
|
|
116
|
+
return Math.tan(f - R);
|
|
117
|
+
}
|
|
118
|
+
function J(e) {
|
|
87
119
|
return e.toDataURL("image/png");
|
|
88
120
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
121
|
+
function T() {
|
|
122
|
+
if (a.fallbackOnly) return;
|
|
123
|
+
const e = Math.max(1, Math.round(o.value)), t = Math.max(1, Math.round(l.value)), n = document.createElement("canvas");
|
|
91
124
|
n.width = e, n.height = t;
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
let
|
|
96
|
-
const
|
|
125
|
+
const f = document.createElement("canvas");
|
|
126
|
+
f.width = e, f.height = t;
|
|
127
|
+
const m = n.getContext("2d"), R = f.getContext("2d"), v = m.createImageData(e, t), g = R.createImageData(e, t), $ = [];
|
|
128
|
+
let M = 1;
|
|
129
|
+
const z = {
|
|
97
130
|
x: Math.cos(a.glareAngle * Math.PI / 180),
|
|
98
131
|
y: Math.sin(a.glareAngle * Math.PI / 180)
|
|
99
|
-
};
|
|
100
|
-
for (let
|
|
101
|
-
const
|
|
102
|
-
let L = 0,
|
|
103
|
-
if (
|
|
104
|
-
const
|
|
105
|
-
L = -
|
|
106
|
-
const
|
|
107
|
-
|
|
132
|
+
}, D = Math.max(1, Math.min(a.bezel, Math.min(e, t) * 0.28));
|
|
133
|
+
for (let u = 0; u < t; u++) for (let d = 0; d < e; d++) {
|
|
134
|
+
const h = q(d, u, e, t, F.value), p = i(h / D);
|
|
135
|
+
let L = 0, E = 0, X = 0;
|
|
136
|
+
if (h >= 0 && h < D) {
|
|
137
|
+
const _ = Se(d, u, e, t, F.value), Y = oe(re(p)) * a.thickness * B(1 - p) * a.refraction;
|
|
138
|
+
L = -_.x * Y, E = -_.y * Y, M = Math.max(M, Math.abs(L), Math.abs(E));
|
|
139
|
+
const de = i(_.x * z.x + _.y * z.y, 0, 1);
|
|
140
|
+
X = Math.pow(de, 18) * a.specularOpacity;
|
|
108
141
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
if (a.magnification !== 0 && h >= 0) {
|
|
143
|
+
const _ = ne(d, u, e, t, a.magnification, a.magnificationFocus);
|
|
144
|
+
L += _.x, E += _.y;
|
|
145
|
+
}
|
|
146
|
+
$.push([L, E]);
|
|
147
|
+
const U = (u * e + d) * 4, ue = Math.round(X * 255);
|
|
148
|
+
g.data[U] = 255, g.data[U + 1] = 255, g.data[U + 2] = 255, g.data[U + 3] = ue;
|
|
112
149
|
}
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
}),
|
|
150
|
+
M = $.reduce((u, [d, h]) => Math.max(u, Math.abs(d), Math.abs(h)), 1), $.forEach(([u, d], h) => {
|
|
151
|
+
const p = h * 4;
|
|
152
|
+
v.data[p] = Math.round(128 + u / M * 127), v.data[p + 1] = Math.round(128 + d / M * 127), v.data[p + 2] = 128, v.data[p + 3] = 255;
|
|
153
|
+
}), m.putImageData(v, 0, 0), R.putImageData(g, 0, 0), b.value = J(n), y.value = J(f), x.value = M;
|
|
117
154
|
}
|
|
118
|
-
function
|
|
119
|
-
const e =
|
|
120
|
-
e && (
|
|
155
|
+
function K() {
|
|
156
|
+
const e = j();
|
|
157
|
+
e && (o.value = Math.max(1, e.offsetWidth), l.value = Math.max(1, e.offsetHeight));
|
|
121
158
|
}
|
|
122
|
-
const
|
|
159
|
+
const Q = c ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", F = C(() => {
|
|
123
160
|
if (a.radius !== 32) return a.radius;
|
|
124
161
|
const e = a.asProps;
|
|
125
|
-
return typeof e.rounded == "string" && e.rounded in
|
|
126
|
-
}),
|
|
162
|
+
return typeof e.rounded == "string" && e.rounded in V ? V[e.rounded] : a.radius;
|
|
163
|
+
}), se = C(() => a.borderRadius || `${F.value}px`), le = /* @__PURE__ */ new Set([
|
|
127
164
|
"primary",
|
|
128
165
|
"secondary",
|
|
129
166
|
"accent",
|
|
@@ -134,107 +171,116 @@ var ye = {
|
|
|
134
171
|
"surface",
|
|
135
172
|
"background"
|
|
136
173
|
]);
|
|
137
|
-
function
|
|
138
|
-
if (
|
|
174
|
+
function ce(e, t) {
|
|
175
|
+
if (le.has(e)) return `rgba(var(--v-theme-${e}), ${t})`;
|
|
139
176
|
if (e.startsWith("#")) {
|
|
140
177
|
const n = e.replace("#", "");
|
|
141
178
|
return `rgba(${parseInt(n.slice(0, 2), 16)}, ${parseInt(n.slice(2, 4), 16)}, ${parseInt(n.slice(4, 6), 16)}, ${t})`;
|
|
142
179
|
}
|
|
143
|
-
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) :
|
|
180
|
+
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) : Q;
|
|
144
181
|
}
|
|
145
|
-
const
|
|
182
|
+
const A = C(() => {
|
|
146
183
|
if (a.glassBackground !== void 0) return a.glassBackground;
|
|
147
184
|
const e = a.asProps;
|
|
148
|
-
return typeof e.color == "string" ?
|
|
149
|
-
}),
|
|
150
|
-
borderRadius: `${
|
|
151
|
-
...
|
|
152
|
-
...
|
|
153
|
-
...
|
|
154
|
-
backdropFilter:
|
|
155
|
-
WebkitBackdropFilter:
|
|
185
|
+
return typeof e.color == "string" ? ce(e.color, 0.6) : Q;
|
|
186
|
+
}), O = C(() => a.fallbackOnly || !c ? "blur(12px)" : `url(#${S}) blur(${a.blur}px)`), ie = C(() => ({
|
|
187
|
+
borderRadius: `${se.value}`,
|
|
188
|
+
...A.value !== void 0 ? { backgroundColor: A.value } : {},
|
|
189
|
+
...A.value !== void 0 ? { boxShadow: ["inset 0 0 0 1px rgba(255, 255, 255, 0.28)", "0 18px 48px rgba(0, 0, 0, 0.18)"].join(", ") } : {},
|
|
190
|
+
...I.value && !N ? {
|
|
191
|
+
backdropFilter: O.value,
|
|
192
|
+
WebkitBackdropFilter: O.value
|
|
156
193
|
} : {},
|
|
157
|
-
...
|
|
158
|
-
backdropFilter:
|
|
159
|
-
WebkitBackdropFilter:
|
|
194
|
+
...I.value && N && !c ? {
|
|
195
|
+
backdropFilter: O.value,
|
|
196
|
+
WebkitBackdropFilter: O.value
|
|
160
197
|
} : {}
|
|
161
198
|
}));
|
|
162
|
-
let
|
|
163
|
-
return
|
|
164
|
-
if (await
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
let G = null;
|
|
200
|
+
return ye(async () => {
|
|
201
|
+
if (await ee(), c) {
|
|
202
|
+
K(), T(), await ee(), I.value = !0, G = new ResizeObserver(() => {
|
|
203
|
+
K(), T(), I.value = !0;
|
|
167
204
|
});
|
|
168
|
-
const e =
|
|
169
|
-
e &&
|
|
170
|
-
} else
|
|
171
|
-
}),
|
|
172
|
-
|
|
205
|
+
const e = j();
|
|
206
|
+
e && G.observe(e);
|
|
207
|
+
} else I.value = !0;
|
|
208
|
+
}), Ie(() => [
|
|
209
|
+
F.value,
|
|
173
210
|
a.bezel,
|
|
174
211
|
a.thickness,
|
|
175
212
|
a.refraction,
|
|
213
|
+
a.magnification,
|
|
214
|
+
a.magnificationFocus,
|
|
176
215
|
a.surface,
|
|
177
216
|
a.specularOpacity,
|
|
178
|
-
a.glareAngle
|
|
217
|
+
a.glareAngle,
|
|
218
|
+
a.fallbackOnly
|
|
179
219
|
], () => {
|
|
180
|
-
|
|
181
|
-
}),
|
|
182
|
-
|
|
183
|
-
}), (e, t) => (
|
|
220
|
+
c && T(), I.value = !0;
|
|
221
|
+
}), be(() => {
|
|
222
|
+
G?.disconnect();
|
|
223
|
+
}), (e, t) => (W(), Z("div", {
|
|
184
224
|
ref_key: "root",
|
|
185
|
-
ref:
|
|
186
|
-
class: "liquid-glass"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
225
|
+
ref: s,
|
|
226
|
+
class: "liquid-glass",
|
|
227
|
+
style: Me(ie.value)
|
|
228
|
+
}, [we(c) ? (W(), Z("svg", Be, [k("defs", null, [k("filter", {
|
|
229
|
+
id: S,
|
|
230
|
+
"color-interpolation-filters": "sRGB",
|
|
231
|
+
filterUnits: "userSpaceOnUse",
|
|
232
|
+
x: "0",
|
|
233
|
+
y: "0",
|
|
234
|
+
width: "10000",
|
|
235
|
+
height: "10000"
|
|
236
|
+
}, [
|
|
237
|
+
k("feImage", {
|
|
238
|
+
href: b.value,
|
|
239
|
+
x: "0",
|
|
240
|
+
y: "0",
|
|
241
|
+
width: o.value,
|
|
242
|
+
height: l.value,
|
|
243
|
+
result: "displacement_map"
|
|
244
|
+
}, null, 8, $e),
|
|
245
|
+
t[0] || (t[0] = k("feGaussianBlur", {
|
|
246
|
+
in: "displacement_map",
|
|
247
|
+
stdDeviation: "2",
|
|
248
|
+
result: "smooth_disp"
|
|
249
|
+
}, null, -1)),
|
|
250
|
+
k("feDisplacementMap", {
|
|
251
|
+
in: "SourceGraphic",
|
|
252
|
+
in2: "smooth_disp",
|
|
253
|
+
scale: x.value,
|
|
254
|
+
xChannelSelector: "R",
|
|
255
|
+
yChannelSelector: "G",
|
|
256
|
+
result: "refracted"
|
|
257
|
+
}, null, 8, Ce),
|
|
258
|
+
k("feImage", {
|
|
259
|
+
href: y.value,
|
|
192
260
|
x: "0",
|
|
193
261
|
y: "0",
|
|
194
|
-
width:
|
|
195
|
-
height:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
w("feDisplacementMap", {
|
|
206
|
-
in: "SourceGraphic",
|
|
207
|
-
in2: "displacement_map",
|
|
208
|
-
scale: z.value,
|
|
209
|
-
xChannelSelector: "R",
|
|
210
|
-
yChannelSelector: "G",
|
|
211
|
-
result: "refracted"
|
|
212
|
-
}, null, 8, ke),
|
|
213
|
-
w("feImage", {
|
|
214
|
-
href: U.value,
|
|
215
|
-
x: "0",
|
|
216
|
-
y: "0",
|
|
217
|
-
width: _.value,
|
|
218
|
-
height: y.value,
|
|
219
|
-
result: "specular"
|
|
220
|
-
}, null, 8, we),
|
|
221
|
-
t[0] || (t[0] = w("feBlend", {
|
|
222
|
-
in: "refracted",
|
|
223
|
-
in2: "specular",
|
|
224
|
-
mode: "screen"
|
|
225
|
-
}, null, -1))
|
|
226
|
-
])])])) : ue("", !0), ge(e.$slots, "default", {}, void 0, !0)]),
|
|
262
|
+
width: o.value,
|
|
263
|
+
height: l.value,
|
|
264
|
+
result: "specular"
|
|
265
|
+
}, null, 8, qe),
|
|
266
|
+
t[1] || (t[1] = k("feBlend", {
|
|
267
|
+
in: "refracted",
|
|
268
|
+
in2: "specular",
|
|
269
|
+
mode: "screen"
|
|
270
|
+
}, null, -1))
|
|
271
|
+
])])])) : fe("", !0), (W(), he(ke(r.as), me(ve(r.asProps)), {
|
|
272
|
+
default: Re(() => [_e(e.$slots, "default", {}, void 0, !0)]),
|
|
227
273
|
_: 3
|
|
228
|
-
}, 16,
|
|
274
|
+
}, 16))], 4));
|
|
229
275
|
}
|
|
230
|
-
}),
|
|
231
|
-
const
|
|
232
|
-
for (const [
|
|
233
|
-
return
|
|
234
|
-
},
|
|
235
|
-
|
|
276
|
+
}), Fe = (r, a) => {
|
|
277
|
+
const s = r.__vccOpts || r;
|
|
278
|
+
for (const [o, l] of a) s[o] = l;
|
|
279
|
+
return s;
|
|
280
|
+
}, Oe = /* @__PURE__ */ Fe(Pe, [["__scopeId", "data-v-56a7fa97"]]), De = { install(r) {
|
|
281
|
+
r.component("Liqvued", Oe);
|
|
236
282
|
} };
|
|
237
283
|
export {
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
Oe as Liqvued,
|
|
285
|
+
De as default
|
|
240
286
|
};
|
package/dist/liqvued.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(v,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(v=typeof globalThis<"u"?globalThis:v||self,t(v.Liqvued={},v.Vue))})(this,function(v,t){Object.defineProperties(v,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});function I(o,n,c,s,l){const x=Math.min(l,Math.min(c,s)/2),_=c/2-x,k=s/2-x,i=Math.abs(o-c/2)-_,R=Math.abs(n-s/2)-k;return-(Math.min(Math.max(i,R),0)+Math.hypot(Math.max(i,0),Math.max(R,0))-x)}function Y(o,n,c,s,l){const _=I(o-1.5,n,c,s,l)-I(o+1.5,n,c,s,l),k=I(o,n-1.5,c,s,l)-I(o,n+1.5,c,s,l),i=Math.hypot(_,k);return i<1e-10?{x:0,y:-1}:{x:_/i,y:k/i}}var Z={key:0,class:"liquid-glass__svg","aria-hidden":"true"},ee=["href","width","height"],te=["scale"],ae=["href","width","height"],ne=(0,t.defineComponent)({__name:"Liqvued",props:{as:{default:"div"},asProps:{default:()=>({})},radius:{default:32},borderRadius:{},bezel:{default:22},thickness:{default:42},refraction:{default:1},magnification:{default:0},magnificationFocus:{default:.82},blur:{default:.4},surface:{default:"convex"},specularOpacity:{default:.45},glareAngle:{default:-60},glassBackground:{default:void 0},fallbackOnly:{type:Boolean,default:!1}},setup(o){const n=o,c=(0,t.ref)(null),s=(0,t.ref)(1),l=(0,t.ref)(1),x=(0,t.ref)(""),_=(0,t.ref)(""),k=(0,t.ref)(1),i=typeof CSS<"u"&&typeof navigator<"u"&&CSS.supports?.("backdrop-filter","url(#x)")&&/Chrome/.test(navigator.userAgent),R=`${`lg-${Math.random().toString(36).slice(2)}`}-filter`,A=(0,t.inject)("_liqvued",!1);(0,t.provide)("_liqvued",!0);const B=(0,t.ref)(!1),G={0:0,sm:4,md:8,lg:12,xl:16,pill:9999,circle:9999,shaped:16};function W(){return c.value}function u(e,a=0,r=1){return Math.max(a,Math.min(r,e))}function E(e){return e=u(e),e*e*e*(e*(e*6-15)+10)}function q(e){return Math.pow(1-Math.pow(1-u(e),4),1/4)}function se(e,a,r){return u(e+Math.sin(u(e)*Math.PI*a)*r)}function ce(e){const a=u(e);return u(a+Math.sin(a*39.1)*.06+Math.sin(a*91.7)*.035)}function le(e,a,r,p,b,S){const g=r/2,M=p/2,$=Math.max(1,r/2),y=Math.max(1,p/2),O=(e-g)/$,V=(a-M)/y,d=Math.hypot(O,V),f=u(S,.2,1.4),h=Math.pow(E(1-u(d/f)),1.6),m=b>=0?1+b*h:1/(1+Math.abs(b)*h);return{x:(e-g)/m-(e-g),y:(a-M)/m-(a-M)}}function j(e){const a=u(e);switch(n.surface){case"concave":case"bowl":return 1-q(a);case"lip":return q(a)*(1-E(a))+(1-q(a))*E(a);case"bevel":return a;case"saddle":return .5+Math.sin((a-.5)*Math.PI)*.5;case"ripple":return se(a,3,.12);case"noise":return ce(a);case"asymmetric":return u(Math.pow(a,.65)*.65+E(a)*.35);case"convex":return q(a)}}function ie(e){return(j(e+.001)-j(e-.001))/(2*.001)}function ue(e){const p=Math.atan(e),b=1/1.5*Math.sin(p),S=Math.asin(u(b,-1,1));return Math.tan(p-S)}function H(e){return e.toDataURL("image/png")}function L(){if(n.fallbackOnly)return;const e=Math.max(1,Math.round(s.value)),a=Math.max(1,Math.round(l.value)),r=document.createElement("canvas");r.width=e,r.height=a;const p=document.createElement("canvas");p.width=e,p.height=a;const b=r.getContext("2d"),S=p.getContext("2d"),g=b.createImageData(e,a),M=S.createImageData(e,a),$=[];let y=1;const O={x:Math.cos(n.glareAngle*Math.PI/180),y:Math.sin(n.glareAngle*Math.PI/180)},V=Math.max(1,Math.min(n.bezel,Math.min(e,a)*.28));for(let d=0;d<a;d++)for(let f=0;f<e;f++){const h=I(f,d,e,a,C.value),m=u(h/V);let F=0,N=0,Q=0;if(h>=0&&h<V){const w=Y(f,d,e,a,C.value),X=ue(ie(m))*n.thickness*E(1-m)*n.refraction;F=-w.x*X,N=-w.y*X,y=Math.max(y,Math.abs(F),Math.abs(N));const ge=u(w.x*O.x+w.y*O.y,0,1);Q=Math.pow(ge,18)*n.specularOpacity}if(n.magnification!==0&&h>=0){const w=le(f,d,e,a,n.magnification,n.magnificationFocus);F+=w.x,N+=w.y}$.push([F,N]);const T=(d*e+f)*4,me=Math.round(Q*255);M.data[T]=255,M.data[T+1]=255,M.data[T+2]=255,M.data[T+3]=me}y=$.reduce((d,[f,h])=>Math.max(d,Math.abs(f),Math.abs(h)),1),$.forEach(([d,f],h)=>{const m=h*4;g.data[m]=Math.round(128+d/y*127),g.data[m+1]=Math.round(128+f/y*127),g.data[m+2]=128,g.data[m+3]=255}),b.putImageData(g,0,0),S.putImageData(M,0,0),x.value=H(r),_.value=H(p),k.value=y}function J(){const e=W();e&&(s.value=Math.max(1,e.offsetWidth),l.value=Math.max(1,e.offsetHeight))}const K=i?"rgba(255, 255, 255, 0.08)":"rgba(255, 255, 255, 0.22)",C=(0,t.computed)(()=>{if(n.radius!==32)return n.radius;const e=n.asProps;return typeof e.rounded=="string"&&e.rounded in G?G[e.rounded]:n.radius}),de=(0,t.computed)(()=>n.borderRadius||`${C.value}px`),fe=new Set(["primary","secondary","accent","info","warning","error","success","surface","background"]);function he(e,a){if(fe.has(e))return`rgba(var(--v-theme-${e}), ${a})`;if(e.startsWith("#")){const r=e.replace("#","");return`rgba(${parseInt(r.slice(0,2),16)}, ${parseInt(r.slice(2,4),16)}, ${parseInt(r.slice(4,6),16)}, ${a})`}return e.startsWith("rgb")?e.replace("rgb(","rgba(").replace(")",`, ${a})`):K}const z=(0,t.computed)(()=>{if(n.glassBackground!==void 0)return n.glassBackground;const e=n.asProps;return typeof e.color=="string"?he(e.color,.6):K}),P=(0,t.computed)(()=>n.fallbackOnly||!i?"blur(12px)":`url(#${R}) blur(${n.blur}px)`),pe=(0,t.computed)(()=>({borderRadius:`${de.value}`,...z.value!==void 0?{backgroundColor:z.value}:{},...z.value!==void 0?{boxShadow:["inset 0 0 0 1px rgba(255, 255, 255, 0.28)","0 18px 48px rgba(0, 0, 0, 0.18)"].join(", ")}:{},...B.value&&!A?{backdropFilter:P.value,WebkitBackdropFilter:P.value}:{},...B.value&&A&&!i?{backdropFilter:P.value,WebkitBackdropFilter:P.value}:{}}));let D=null;return(0,t.onMounted)(async()=>{if(await(0,t.nextTick)(),i){J(),L(),await(0,t.nextTick)(),B.value=!0,D=new ResizeObserver(()=>{J(),L(),B.value=!0});const e=W();e&&D.observe(e)}else B.value=!0}),(0,t.watch)(()=>[C.value,n.bezel,n.thickness,n.refraction,n.magnification,n.magnificationFocus,n.surface,n.specularOpacity,n.glareAngle,n.fallbackOnly],()=>{i&&L(),B.value=!0}),(0,t.onBeforeUnmount)(()=>{D?.disconnect()}),(e,a)=>((0,t.openBlock)(),(0,t.createElementBlock)("div",{ref_key:"root",ref:c,class:"liquid-glass",style:(0,t.normalizeStyle)(pe.value)},[(0,t.unref)(i)?((0,t.openBlock)(),(0,t.createElementBlock)("svg",Z,[(0,t.createElementVNode)("defs",null,[(0,t.createElementVNode)("filter",{id:R,"color-interpolation-filters":"sRGB",filterUnits:"userSpaceOnUse",x:"0",y:"0",width:"10000",height:"10000"},[(0,t.createElementVNode)("feImage",{href:x.value,x:"0",y:"0",width:s.value,height:l.value,result:"displacement_map"},null,8,ee),a[0]||(a[0]=(0,t.createElementVNode)("feGaussianBlur",{in:"displacement_map",stdDeviation:"2",result:"smooth_disp"},null,-1)),(0,t.createElementVNode)("feDisplacementMap",{in:"SourceGraphic",in2:"smooth_disp",scale:k.value,xChannelSelector:"R",yChannelSelector:"G",result:"refracted"},null,8,te),(0,t.createElementVNode)("feImage",{href:_.value,x:"0",y:"0",width:s.value,height:l.value,result:"specular"},null,8,ae),a[1]||(a[1]=(0,t.createElementVNode)("feBlend",{in:"refracted",in2:"specular",mode:"screen"},null,-1))])])])):(0,t.createCommentVNode)("",!0),((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(o.as),(0,t.normalizeProps)((0,t.guardReactiveProps)(o.asProps)),{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,"default",{},void 0,!0)]),_:3},16))],4))}}),re=(o,n)=>{const c=o.__vccOpts||o;for(const[s,l]of n)c[s]=l;return c},U=re(ne,[["__scopeId","data-v-56a7fa97"]]),oe={install(o){o.component("Liqvued",U)}};v.Liqvued=U,v.default=oe});
|
package/package.json
CHANGED
package/src/Liqvued.vue
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
type Component,
|
|
4
|
-
type ComponentPublicInstance,
|
|
5
4
|
computed,
|
|
6
5
|
inject,
|
|
7
6
|
nextTick,
|
|
@@ -11,27 +10,35 @@ import {
|
|
|
11
10
|
ref,
|
|
12
11
|
watch,
|
|
13
12
|
} from 'vue'
|
|
13
|
+
import { distanceToRoundedRectEdge, normalFromRoundedRect } from './shape'
|
|
14
14
|
import type { Surface } from './types'
|
|
15
15
|
|
|
16
16
|
const props = withDefaults(defineProps<{
|
|
17
17
|
as?: string | Component
|
|
18
18
|
asProps?: object
|
|
19
19
|
radius?: number
|
|
20
|
+
borderRadius?: string
|
|
20
21
|
bezel?: number
|
|
21
22
|
thickness?: number
|
|
22
23
|
refraction?: number
|
|
24
|
+
magnification?: number
|
|
25
|
+
magnificationFocus?: number
|
|
23
26
|
blur?: number
|
|
24
27
|
surface?: Surface
|
|
25
28
|
specularOpacity?: number
|
|
26
29
|
glareAngle?: number
|
|
27
30
|
glassBackground?: string
|
|
31
|
+
fallbackOnly?: boolean
|
|
28
32
|
}>(), {
|
|
29
33
|
as: 'div',
|
|
30
34
|
asProps: () => ({}),
|
|
35
|
+
fallbackOnly: false,
|
|
31
36
|
radius: 32,
|
|
32
37
|
bezel: 22,
|
|
33
38
|
thickness: 42,
|
|
34
39
|
refraction: 1,
|
|
40
|
+
magnification: 0,
|
|
41
|
+
magnificationFocus: 0.82,
|
|
35
42
|
blur: 0.4,
|
|
36
43
|
surface: 'convex',
|
|
37
44
|
specularOpacity: 0.45,
|
|
@@ -39,7 +46,7 @@ const props = withDefaults(defineProps<{
|
|
|
39
46
|
glassBackground: undefined,
|
|
40
47
|
})
|
|
41
48
|
|
|
42
|
-
const root = ref<HTMLElement |
|
|
49
|
+
const root = ref<HTMLElement | null>(null)
|
|
43
50
|
|
|
44
51
|
const width = ref(1)
|
|
45
52
|
const height = ref(1)
|
|
@@ -47,6 +54,8 @@ const mapUrl = ref('')
|
|
|
47
54
|
const specularUrl = ref('')
|
|
48
55
|
const scale = ref(1)
|
|
49
56
|
const supportsLiquidGlass =
|
|
57
|
+
typeof CSS !== 'undefined' &&
|
|
58
|
+
typeof navigator !== 'undefined' &&
|
|
50
59
|
CSS.supports?.('backdrop-filter', 'url(#x)') &&
|
|
51
60
|
/Chrome/.test(navigator.userAgent)
|
|
52
61
|
|
|
@@ -54,6 +63,11 @@ const id = `lg-${Math.random().toString(36).slice(2)}`
|
|
|
54
63
|
const filterId = `${id}-filter`
|
|
55
64
|
const isNested = inject('_liqvued', false)
|
|
56
65
|
provide('_liqvued', true)
|
|
66
|
+
|
|
67
|
+
if (import.meta.env.DEV && typeof props.as !== 'string') {
|
|
68
|
+
console.warn('[Liqvued] `as` prop should be a string (HTML tag name). For Vue components, use Liqvued as a wrapper instead of `as`.')
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
const glassReady = ref(false)
|
|
58
72
|
|
|
59
73
|
const roundedMap: Record<string, number> = {
|
|
@@ -68,9 +82,7 @@ const roundedMap: Record<string, number> = {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
function getRootEl(): HTMLElement | null {
|
|
71
|
-
|
|
72
|
-
if (root.value instanceof Element) return root.value as HTMLElement
|
|
73
|
-
return (root.value as ComponentPublicInstance).$el ?? null
|
|
85
|
+
return root.value
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
function clamp(v: number, min = 0, max = 1) {
|
|
@@ -86,13 +98,67 @@ function convex(x: number) {
|
|
|
86
98
|
return Math.pow(1 - Math.pow(1 - clamp(x), 4), 1 / 4)
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
function wave(x: number, frequency: number, amplitude: number) {
|
|
102
|
+
return clamp(x + Math.sin(clamp(x) * Math.PI * frequency) * amplitude)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function deterministicNoise(x: number) {
|
|
106
|
+
const t = clamp(x)
|
|
107
|
+
return clamp(t + Math.sin(t * 39.1) * 0.06 + Math.sin(t * 91.7) * 0.035)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function magnificationVector(
|
|
111
|
+
x: number,
|
|
112
|
+
y: number,
|
|
113
|
+
w: number,
|
|
114
|
+
h: number,
|
|
115
|
+
magnification: number,
|
|
116
|
+
magnificationFocus: number,
|
|
117
|
+
) {
|
|
118
|
+
const cx = w / 2
|
|
119
|
+
const cy = h / 2
|
|
120
|
+
const rx = Math.max(1, w / 2)
|
|
121
|
+
const ry = Math.max(1, h / 2)
|
|
122
|
+
const dx = (x - cx) / rx
|
|
123
|
+
const dy = (y - cy) / ry
|
|
124
|
+
const radius = Math.hypot(dx, dy)
|
|
125
|
+
const focusRadius = clamp(magnificationFocus, 0.2, 1.4)
|
|
126
|
+
const focus = Math.pow(
|
|
127
|
+
smootherStep(1 - clamp(radius / focusRadius)),
|
|
128
|
+
1.6,
|
|
129
|
+
)
|
|
130
|
+
const zoom = magnification >= 0
|
|
131
|
+
? 1 + magnification * focus
|
|
132
|
+
: 1 / (1 + Math.abs(magnification) * focus)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
x: (x - cx) / zoom - (x - cx),
|
|
136
|
+
y: (y - cy) / zoom - (y - cy),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
89
140
|
function surfaceFn(x: number) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
const t = clamp(x)
|
|
142
|
+
|
|
143
|
+
switch (props.surface) {
|
|
144
|
+
case 'concave':
|
|
145
|
+
case 'bowl':
|
|
146
|
+
return 1 - convex(t)
|
|
147
|
+
case 'lip':
|
|
148
|
+
return convex(t) * (1 - smootherStep(t)) + (1 - convex(t)) * smootherStep(t)
|
|
149
|
+
case 'bevel':
|
|
150
|
+
return t
|
|
151
|
+
case 'saddle':
|
|
152
|
+
return 0.5 + Math.sin((t - 0.5) * Math.PI) * 0.5
|
|
153
|
+
case 'ripple':
|
|
154
|
+
return wave(t, 3, 0.12)
|
|
155
|
+
case 'noise':
|
|
156
|
+
return deterministicNoise(t)
|
|
157
|
+
case 'asymmetric':
|
|
158
|
+
return clamp(Math.pow(t, 0.65) * 0.65 + smootherStep(t) * 0.35)
|
|
159
|
+
case 'convex':
|
|
160
|
+
return convex(t)
|
|
94
161
|
}
|
|
95
|
-
return convex(x)
|
|
96
162
|
}
|
|
97
163
|
|
|
98
164
|
function derivative(x: number) {
|
|
@@ -113,68 +179,15 @@ function refractSlope(slope: number) {
|
|
|
113
179
|
return Math.tan(theta1 - theta2)
|
|
114
180
|
}
|
|
115
181
|
|
|
116
|
-
function distanceToRoundedRectEdge(
|
|
117
|
-
x: number,
|
|
118
|
-
y: number,
|
|
119
|
-
w: number,
|
|
120
|
-
h: number,
|
|
121
|
-
r: number,
|
|
122
|
-
) {
|
|
123
|
-
const cx = clamp(x, r, w - r)
|
|
124
|
-
const cy = clamp(y, r, h - r)
|
|
125
|
-
|
|
126
|
-
const dx = x - cx
|
|
127
|
-
const dy = y - cy
|
|
128
|
-
|
|
129
|
-
const cornerDistance = Math.hypot(dx, dy)
|
|
130
|
-
const edgeDistance = Math.min(x, y, w - x, h - y)
|
|
131
|
-
|
|
132
|
-
if (dx !== 0 && dy !== 0) {
|
|
133
|
-
return r - cornerDistance
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return edgeDistance
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function normalFromRoundedRect(
|
|
140
|
-
x: number,
|
|
141
|
-
y: number,
|
|
142
|
-
w: number,
|
|
143
|
-
h: number,
|
|
144
|
-
r: number,
|
|
145
|
-
) {
|
|
146
|
-
const cx = clamp(x, r, w - r)
|
|
147
|
-
const cy = clamp(y, r, h - r)
|
|
148
|
-
|
|
149
|
-
let nx = x - cx
|
|
150
|
-
let ny = y - cy
|
|
151
|
-
|
|
152
|
-
if (nx === 0 && ny === 0) {
|
|
153
|
-
const left = x
|
|
154
|
-
const right = w - x
|
|
155
|
-
const top = y
|
|
156
|
-
const bottom = h - y
|
|
157
|
-
const m = Math.min(left, right, top, bottom)
|
|
158
|
-
|
|
159
|
-
if (m === left) nx = -1
|
|
160
|
-
else if (m === right) nx = 1
|
|
161
|
-
else if (m === top) ny = -1
|
|
162
|
-
else ny = 1
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const len = Math.hypot(nx, ny) || 1
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
x: nx / len,
|
|
169
|
-
y: ny / len,
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
182
|
function canvasToUrl(canvas: HTMLCanvasElement) {
|
|
174
183
|
return canvas.toDataURL('image/png')
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
function buildMaps() {
|
|
187
|
+
if (props.fallbackOnly) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
const w = Math.max(1, Math.round(width.value))
|
|
179
192
|
const h = Math.max(1, Math.round(height.value))
|
|
180
193
|
|
|
@@ -199,23 +212,30 @@ function buildMaps() {
|
|
|
199
212
|
x: Math.cos((props.glareAngle * Math.PI) / 180),
|
|
200
213
|
y: Math.sin((props.glareAngle * Math.PI) / 180),
|
|
201
214
|
}
|
|
215
|
+
const edgeBand = Math.max(
|
|
216
|
+
1,
|
|
217
|
+
Math.min(props.bezel, Math.min(w, h) * 0.28),
|
|
218
|
+
)
|
|
202
219
|
|
|
203
220
|
for (let y = 0; y < h; y++) {
|
|
204
221
|
for (let x = 0; x < w; x++) {
|
|
205
222
|
const dist = distanceToRoundedRectEdge(x, y, w, h, resolvedRadius.value)
|
|
206
|
-
const t = clamp(dist /
|
|
223
|
+
const t = clamp(dist / edgeBand)
|
|
207
224
|
|
|
208
225
|
let vx = 0
|
|
209
226
|
let vy = 0
|
|
210
227
|
let spec = 0
|
|
211
228
|
|
|
212
|
-
if (dist >= 0 && dist <
|
|
229
|
+
if (dist >= 0 && dist < edgeBand) {
|
|
213
230
|
const n = normalFromRoundedRect(x, y, w, h, resolvedRadius.value)
|
|
214
231
|
const slope = derivative(t)
|
|
215
232
|
const bend = refractSlope(slope)
|
|
216
233
|
|
|
217
234
|
const amount =
|
|
218
|
-
bend *
|
|
235
|
+
bend *
|
|
236
|
+
props.thickness *
|
|
237
|
+
smootherStep(1 - t) *
|
|
238
|
+
props.refraction
|
|
219
239
|
|
|
220
240
|
vx = -n.x * amount
|
|
221
241
|
vy = -n.y * amount
|
|
@@ -226,6 +246,19 @@ function buildMaps() {
|
|
|
226
246
|
spec = Math.pow(facing, 18) * props.specularOpacity
|
|
227
247
|
}
|
|
228
248
|
|
|
249
|
+
if (props.magnification !== 0 && dist >= 0) {
|
|
250
|
+
const lens = magnificationVector(
|
|
251
|
+
x,
|
|
252
|
+
y,
|
|
253
|
+
w,
|
|
254
|
+
h,
|
|
255
|
+
props.magnification,
|
|
256
|
+
props.magnificationFocus,
|
|
257
|
+
)
|
|
258
|
+
vx += lens.x
|
|
259
|
+
vy += lens.y
|
|
260
|
+
}
|
|
261
|
+
|
|
229
262
|
vectors.push([vx, vy])
|
|
230
263
|
|
|
231
264
|
const hi = (y * w + x) * 4
|
|
@@ -238,6 +271,10 @@ function buildMaps() {
|
|
|
238
271
|
}
|
|
239
272
|
}
|
|
240
273
|
|
|
274
|
+
max = vectors.reduce((currentMax, [vx, vy]) => {
|
|
275
|
+
return Math.max(currentMax, Math.abs(vx), Math.abs(vy))
|
|
276
|
+
}, 1)
|
|
277
|
+
|
|
241
278
|
vectors.forEach(([vx, vy], index) => {
|
|
242
279
|
const i = index * 4
|
|
243
280
|
|
|
@@ -276,6 +313,10 @@ const resolvedRadius = computed(() => {
|
|
|
276
313
|
return props.radius
|
|
277
314
|
})
|
|
278
315
|
|
|
316
|
+
const borderRadios = computed(() => {
|
|
317
|
+
return props.borderRadius || `${resolvedRadius.value}px`
|
|
318
|
+
})
|
|
319
|
+
|
|
279
320
|
const vuetifyThemeColors = new Set([
|
|
280
321
|
'primary', 'secondary', 'accent', 'info', 'warning', 'error', 'success',
|
|
281
322
|
'surface', 'background',
|
|
@@ -308,14 +349,14 @@ const glassBgValue = computed(() => {
|
|
|
308
349
|
})
|
|
309
350
|
|
|
310
351
|
const backdropFilter = computed(() => {
|
|
311
|
-
if (supportsLiquidGlass) {
|
|
312
|
-
return
|
|
352
|
+
if (props.fallbackOnly || !supportsLiquidGlass) {
|
|
353
|
+
return 'blur(12px)'
|
|
313
354
|
}
|
|
314
|
-
return
|
|
355
|
+
return `url(#${filterId}) blur(${props.blur}px)`
|
|
315
356
|
})
|
|
316
357
|
|
|
317
358
|
const rootStyle = computed(() => ({
|
|
318
|
-
borderRadius: `${
|
|
359
|
+
borderRadius: `${borderRadios.value}`,
|
|
319
360
|
...(glassBgValue.value !== undefined
|
|
320
361
|
? { backgroundColor: glassBgValue.value }
|
|
321
362
|
: {}),
|
|
@@ -371,9 +412,12 @@ watch(
|
|
|
371
412
|
props.bezel,
|
|
372
413
|
props.thickness,
|
|
373
414
|
props.refraction,
|
|
415
|
+
props.magnification,
|
|
416
|
+
props.magnificationFocus,
|
|
374
417
|
props.surface,
|
|
375
418
|
props.specularOpacity,
|
|
376
419
|
props.glareAngle,
|
|
420
|
+
props.fallbackOnly,
|
|
377
421
|
],
|
|
378
422
|
() => {
|
|
379
423
|
if (supportsLiquidGlass) {
|
|
@@ -386,15 +430,12 @@ watch(
|
|
|
386
430
|
onBeforeUnmount(() => {
|
|
387
431
|
ro?.disconnect()
|
|
388
432
|
})
|
|
389
|
-
|
|
390
433
|
</script>
|
|
391
434
|
|
|
392
435
|
<template>
|
|
393
|
-
<
|
|
394
|
-
:is="as"
|
|
436
|
+
<div
|
|
395
437
|
ref="root"
|
|
396
438
|
class="liquid-glass"
|
|
397
|
-
v-bind="asProps"
|
|
398
439
|
:style="rootStyle"
|
|
399
440
|
>
|
|
400
441
|
<svg
|
|
@@ -421,9 +462,15 @@ onBeforeUnmount(() => {
|
|
|
421
462
|
result="displacement_map"
|
|
422
463
|
/>
|
|
423
464
|
|
|
465
|
+
<feGaussianBlur
|
|
466
|
+
in="displacement_map"
|
|
467
|
+
stdDeviation="2"
|
|
468
|
+
result="smooth_disp"
|
|
469
|
+
/>
|
|
470
|
+
|
|
424
471
|
<feDisplacementMap
|
|
425
472
|
in="SourceGraphic"
|
|
426
|
-
in2="
|
|
473
|
+
in2="smooth_disp"
|
|
427
474
|
:scale="scale"
|
|
428
475
|
xChannelSelector="R"
|
|
429
476
|
yChannelSelector="G"
|
|
@@ -444,8 +491,13 @@ onBeforeUnmount(() => {
|
|
|
444
491
|
</defs>
|
|
445
492
|
</svg>
|
|
446
493
|
|
|
447
|
-
<
|
|
448
|
-
|
|
494
|
+
<component
|
|
495
|
+
:is="as"
|
|
496
|
+
v-bind="asProps"
|
|
497
|
+
>
|
|
498
|
+
<slot />
|
|
499
|
+
</component>
|
|
500
|
+
</div>
|
|
449
501
|
</template>
|
|
450
502
|
|
|
451
503
|
<style scoped>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert'
|
|
2
|
+
import { normalFromRoundedRect } from './shape'
|
|
3
|
+
|
|
4
|
+
function closeTo(actual: number, expected: number, tolerance = 0.001) {
|
|
5
|
+
assert.ok(
|
|
6
|
+
Math.abs(actual - expected) <= tolerance,
|
|
7
|
+
`Expected ${actual} to be within ${tolerance} of ${expected}`,
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertUnitNormal(point: [number, number]) {
|
|
12
|
+
const normal = normalFromRoundedRect(point[0], point[1], 200, 120, 40)
|
|
13
|
+
const length = Math.hypot(normal.x, normal.y)
|
|
14
|
+
|
|
15
|
+
closeTo(length, 1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function dot(a: [number, number], b: [number, number]) {
|
|
19
|
+
return a[0] * b[0] + a[1] * b[1]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalAt(point: [number, number]): [number, number] {
|
|
23
|
+
const normal = normalFromRoundedRect(point[0], point[1], 200, 120, 40)
|
|
24
|
+
return [normal.x, normal.y]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assertUnitNormal([20, 30])
|
|
28
|
+
assertUnitNormal([180, 30])
|
|
29
|
+
assertUnitNormal([20, 90])
|
|
30
|
+
assertUnitNormal([180, 90])
|
|
31
|
+
|
|
32
|
+
const topLeft = normalAt([20, 30])
|
|
33
|
+
const topRight = normalAt([180, 30])
|
|
34
|
+
const bottomLeft = normalAt([20, 90])
|
|
35
|
+
const bottomRight = normalAt([180, 90])
|
|
36
|
+
|
|
37
|
+
closeTo(topLeft[0], -topRight[0])
|
|
38
|
+
closeTo(topLeft[1], topRight[1])
|
|
39
|
+
closeTo(topLeft[0], bottomLeft[0])
|
|
40
|
+
closeTo(topLeft[1], -bottomLeft[1])
|
|
41
|
+
closeTo(topLeft[0], -bottomRight[0])
|
|
42
|
+
closeTo(topLeft[1], -bottomRight[1])
|
|
43
|
+
|
|
44
|
+
assert.ok(
|
|
45
|
+
dot(normalAt([39, 10]), normalAt([40, 10])) > 0.99,
|
|
46
|
+
'Expected top edge to top-right corner transition to be smooth',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
assert.ok(
|
|
50
|
+
dot(normalAt([39, 10]), normalAt([41, 10])) > 0.98,
|
|
51
|
+
'Expected rounded corner seam to remain visually continuous',
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
console.log('shape geometry tests passed')
|
package/src/shape.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Point {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function distanceToRoundedRectEdge(
|
|
7
|
+
x: number,
|
|
8
|
+
y: number,
|
|
9
|
+
w: number,
|
|
10
|
+
h: number,
|
|
11
|
+
r: number,
|
|
12
|
+
) {
|
|
13
|
+
const hr = Math.min(r, Math.min(w, h) / 2)
|
|
14
|
+
const hw = w / 2 - hr
|
|
15
|
+
const hh = h / 2 - hr
|
|
16
|
+
const ax = Math.abs(x - w / 2) - hw
|
|
17
|
+
const ay = Math.abs(y - h / 2) - hh
|
|
18
|
+
|
|
19
|
+
const outsidePart = Math.hypot(Math.max(ax, 0), Math.max(ay, 0))
|
|
20
|
+
const insidePart = Math.min(Math.max(ax, ay), 0)
|
|
21
|
+
|
|
22
|
+
return -(insidePart + outsidePart - hr)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalFromRoundedRect(
|
|
26
|
+
x: number,
|
|
27
|
+
y: number,
|
|
28
|
+
w: number,
|
|
29
|
+
h: number,
|
|
30
|
+
r: number,
|
|
31
|
+
): Point {
|
|
32
|
+
const delta = 1.5
|
|
33
|
+
const nx = distanceToRoundedRectEdge(x - delta, y, w, h, r)
|
|
34
|
+
- distanceToRoundedRectEdge(x + delta, y, w, h, r)
|
|
35
|
+
const ny = distanceToRoundedRectEdge(x, y - delta, w, h, r)
|
|
36
|
+
- distanceToRoundedRectEdge(x, y + delta, w, h, r)
|
|
37
|
+
|
|
38
|
+
const len = Math.hypot(nx, ny)
|
|
39
|
+
if (len < 1e-10) return { x: 0, y: -1 }
|
|
40
|
+
|
|
41
|
+
return { x: nx / len, y: ny / len }
|
|
42
|
+
}
|
package/src/surfaces.ts
ADDED