liqvued 0.1.2 → 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 +11 -3
- package/dist/liqvued.css +1 -1
- package/dist/liqvued.mjs +164 -131
- package/dist/liqvued.umd.js +1 -1
- package/package.json +1 -1
- package/src/Liqvued.vue +96 -57
- 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
|
|
@@ -60,6 +60,8 @@ import { Liqvued } from 'liqvued'
|
|
|
60
60
|
| bezel | number | 22 | Width of the displacement bezel in pixels |
|
|
61
61
|
| thickness | number | 42 | Maximum displacement magnitude in pixels |
|
|
62
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 |
|
|
63
65
|
| blur | number | 0.4 | CSS backdrop-filter blur amount |
|
|
64
66
|
| surface | string | `'convex'` | Glass surface profile shape |
|
|
65
67
|
| specularOpacity | number | 0.45 | Opacity of the specular highlight |
|
|
@@ -74,6 +76,12 @@ import { Liqvued } from 'liqvued'
|
|
|
74
76
|
- `convex` — squircle convex profile — soft pill-like edges
|
|
75
77
|
- `concave` — inverted convex — inward-curving dish-like refraction
|
|
76
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
|
|
77
85
|
|
|
78
86
|
## Dynamic Element Rendering
|
|
79
87
|
|
|
@@ -98,7 +106,7 @@ The fallback applies `backdrop-filter: blur(12px)` with a subtle border and shad
|
|
|
98
106
|
|
|
99
107
|
## How It Works
|
|
100
108
|
|
|
101
|
-
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.
|
|
102
110
|
|
|
103
111
|
## License
|
|
104
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,17 +1,31 @@
|
|
|
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" },
|
|
@@ -21,6 +35,8 @@ var we = {
|
|
|
21
35
|
bezel: { default: 22 },
|
|
22
36
|
thickness: { default: 42 },
|
|
23
37
|
refraction: { default: 1 },
|
|
38
|
+
magnification: { default: 0 },
|
|
39
|
+
magnificationFocus: { default: 0.82 },
|
|
24
40
|
blur: { default: 0.4 },
|
|
25
41
|
surface: { default: "convex" },
|
|
26
42
|
specularOpacity: { default: 0.45 },
|
|
@@ -31,10 +47,10 @@ var we = {
|
|
|
31
47
|
default: !1
|
|
32
48
|
}
|
|
33
49
|
},
|
|
34
|
-
setup(
|
|
35
|
-
const a =
|
|
36
|
-
|
|
37
|
-
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 = {
|
|
38
54
|
0: 0,
|
|
39
55
|
sm: 4,
|
|
40
56
|
md: 8,
|
|
@@ -44,92 +60,107 @@ var we = {
|
|
|
44
60
|
circle: 9999,
|
|
45
61
|
shaped: 16
|
|
46
62
|
};
|
|
47
|
-
function
|
|
48
|
-
return
|
|
63
|
+
function j() {
|
|
64
|
+
return s.value;
|
|
49
65
|
}
|
|
50
|
-
function
|
|
66
|
+
function i(e, t = 0, n = 1) {
|
|
51
67
|
return Math.max(t, Math.min(n, e));
|
|
52
68
|
}
|
|
53
|
-
function Y(e) {
|
|
54
|
-
return e = k(e), e * e * e * (e * (e * 6 - 15) + 10);
|
|
55
|
-
}
|
|
56
69
|
function B(e) {
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
function N(e) {
|
|
60
|
-
if (a.surface === "concave") return 1 - B(e);
|
|
61
|
-
if (a.surface === "lip") {
|
|
62
|
-
const t = Y(e);
|
|
63
|
-
return B(e) * (1 - t) + (1 - B(e)) * t;
|
|
64
|
-
}
|
|
65
|
-
return B(e);
|
|
70
|
+
return e = i(e), e * e * e * (e * (e * 6 - 15) + 10);
|
|
66
71
|
}
|
|
67
|
-
function
|
|
68
|
-
return (
|
|
72
|
+
function P(e) {
|
|
73
|
+
return Math.pow(1 - Math.pow(1 - i(e), 4), 1 / 4);
|
|
69
74
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
return Math.tan(r - l);
|
|
75
|
+
function te(e, t, n) {
|
|
76
|
+
return i(e + Math.sin(i(e) * Math.PI * t) * n);
|
|
73
77
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
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);
|
|
77
81
|
}
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return _ < 1e-10 ? {
|
|
84
|
-
x: 0,
|
|
85
|
-
y: -1
|
|
86
|
-
} : {
|
|
87
|
-
x: i / _,
|
|
88
|
-
y: o / _
|
|
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);
|
|
84
|
+
return {
|
|
85
|
+
x: (e - v) / p - (e - v),
|
|
86
|
+
y: (t - g) / p - (t - g)
|
|
89
87
|
};
|
|
90
88
|
}
|
|
91
|
-
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) {
|
|
92
119
|
return e.toDataURL("image/png");
|
|
93
120
|
}
|
|
94
|
-
function
|
|
121
|
+
function T() {
|
|
95
122
|
if (a.fallbackOnly) return;
|
|
96
|
-
const e = Math.max(1, Math.round(
|
|
123
|
+
const e = Math.max(1, Math.round(o.value)), t = Math.max(1, Math.round(l.value)), n = document.createElement("canvas");
|
|
97
124
|
n.width = e, n.height = t;
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
let
|
|
102
|
-
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 = {
|
|
103
130
|
x: Math.cos(a.glareAngle * Math.PI / 180),
|
|
104
131
|
y: Math.sin(a.glareAngle * Math.PI / 180)
|
|
105
|
-
};
|
|
106
|
-
for (let
|
|
107
|
-
const
|
|
108
|
-
let
|
|
109
|
-
if (
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
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;
|
|
114
141
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
118
149
|
}
|
|
119
|
-
|
|
120
|
-
const p =
|
|
121
|
-
|
|
122
|
-
}),
|
|
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;
|
|
123
154
|
}
|
|
124
|
-
function
|
|
125
|
-
const e =
|
|
126
|
-
e && (
|
|
155
|
+
function K() {
|
|
156
|
+
const e = j();
|
|
157
|
+
e && (o.value = Math.max(1, e.offsetWidth), l.value = Math.max(1, e.offsetHeight));
|
|
127
158
|
}
|
|
128
|
-
const
|
|
159
|
+
const Q = c ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", F = C(() => {
|
|
129
160
|
if (a.radius !== 32) return a.radius;
|
|
130
161
|
const e = a.asProps;
|
|
131
|
-
return typeof e.rounded == "string" && e.rounded in
|
|
132
|
-
}),
|
|
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([
|
|
133
164
|
"primary",
|
|
134
165
|
"secondary",
|
|
135
166
|
"accent",
|
|
@@ -140,60 +171,62 @@ var we = {
|
|
|
140
171
|
"surface",
|
|
141
172
|
"background"
|
|
142
173
|
]);
|
|
143
|
-
function
|
|
144
|
-
if (
|
|
174
|
+
function ce(e, t) {
|
|
175
|
+
if (le.has(e)) return `rgba(var(--v-theme-${e}), ${t})`;
|
|
145
176
|
if (e.startsWith("#")) {
|
|
146
177
|
const n = e.replace("#", "");
|
|
147
178
|
return `rgba(${parseInt(n.slice(0, 2), 16)}, ${parseInt(n.slice(2, 4), 16)}, ${parseInt(n.slice(4, 6), 16)}, ${t})`;
|
|
148
179
|
}
|
|
149
|
-
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) :
|
|
180
|
+
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) : Q;
|
|
150
181
|
}
|
|
151
|
-
const
|
|
182
|
+
const A = C(() => {
|
|
152
183
|
if (a.glassBackground !== void 0) return a.glassBackground;
|
|
153
184
|
const e = a.asProps;
|
|
154
|
-
return typeof e.color == "string" ?
|
|
155
|
-
}),
|
|
156
|
-
borderRadius: `${
|
|
157
|
-
...
|
|
158
|
-
...
|
|
159
|
-
...
|
|
160
|
-
backdropFilter:
|
|
161
|
-
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
|
|
162
193
|
} : {},
|
|
163
|
-
...
|
|
164
|
-
backdropFilter:
|
|
165
|
-
WebkitBackdropFilter:
|
|
194
|
+
...I.value && N && !c ? {
|
|
195
|
+
backdropFilter: O.value,
|
|
196
|
+
WebkitBackdropFilter: O.value
|
|
166
197
|
} : {}
|
|
167
198
|
}));
|
|
168
|
-
let
|
|
169
|
-
return
|
|
170
|
-
if (await
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
204
|
});
|
|
174
|
-
const e =
|
|
175
|
-
e &&
|
|
176
|
-
} else
|
|
177
|
-
}),
|
|
178
|
-
|
|
205
|
+
const e = j();
|
|
206
|
+
e && G.observe(e);
|
|
207
|
+
} else I.value = !0;
|
|
208
|
+
}), Ie(() => [
|
|
209
|
+
F.value,
|
|
179
210
|
a.bezel,
|
|
180
211
|
a.thickness,
|
|
181
212
|
a.refraction,
|
|
213
|
+
a.magnification,
|
|
214
|
+
a.magnificationFocus,
|
|
182
215
|
a.surface,
|
|
183
216
|
a.specularOpacity,
|
|
184
217
|
a.glareAngle,
|
|
185
218
|
a.fallbackOnly
|
|
186
219
|
], () => {
|
|
187
|
-
|
|
188
|
-
}),
|
|
189
|
-
|
|
190
|
-
}), (e, t) => (
|
|
220
|
+
c && T(), I.value = !0;
|
|
221
|
+
}), be(() => {
|
|
222
|
+
G?.disconnect();
|
|
223
|
+
}), (e, t) => (W(), Z("div", {
|
|
191
224
|
ref_key: "root",
|
|
192
|
-
ref:
|
|
225
|
+
ref: s,
|
|
193
226
|
class: "liquid-glass",
|
|
194
|
-
style:
|
|
195
|
-
}, [
|
|
196
|
-
id:
|
|
227
|
+
style: Me(ie.value)
|
|
228
|
+
}, [we(c) ? (W(), Z("svg", Be, [k("defs", null, [k("filter", {
|
|
229
|
+
id: S,
|
|
197
230
|
"color-interpolation-filters": "sRGB",
|
|
198
231
|
filterUnits: "userSpaceOnUse",
|
|
199
232
|
x: "0",
|
|
@@ -201,53 +234,53 @@ var we = {
|
|
|
201
234
|
width: "10000",
|
|
202
235
|
height: "10000"
|
|
203
236
|
}, [
|
|
204
|
-
|
|
205
|
-
href:
|
|
237
|
+
k("feImage", {
|
|
238
|
+
href: b.value,
|
|
206
239
|
x: "0",
|
|
207
240
|
y: "0",
|
|
208
|
-
width:
|
|
209
|
-
height:
|
|
241
|
+
width: o.value,
|
|
242
|
+
height: l.value,
|
|
210
243
|
result: "displacement_map"
|
|
211
|
-
}, null, 8,
|
|
212
|
-
t[0] || (t[0] =
|
|
244
|
+
}, null, 8, $e),
|
|
245
|
+
t[0] || (t[0] = k("feGaussianBlur", {
|
|
213
246
|
in: "displacement_map",
|
|
214
247
|
stdDeviation: "2",
|
|
215
248
|
result: "smooth_disp"
|
|
216
249
|
}, null, -1)),
|
|
217
|
-
|
|
250
|
+
k("feDisplacementMap", {
|
|
218
251
|
in: "SourceGraphic",
|
|
219
252
|
in2: "smooth_disp",
|
|
220
|
-
scale:
|
|
253
|
+
scale: x.value,
|
|
221
254
|
xChannelSelector: "R",
|
|
222
255
|
yChannelSelector: "G",
|
|
223
256
|
result: "refracted"
|
|
224
|
-
}, null, 8,
|
|
225
|
-
|
|
226
|
-
href:
|
|
257
|
+
}, null, 8, Ce),
|
|
258
|
+
k("feImage", {
|
|
259
|
+
href: y.value,
|
|
227
260
|
x: "0",
|
|
228
261
|
y: "0",
|
|
229
|
-
width:
|
|
230
|
-
height:
|
|
262
|
+
width: o.value,
|
|
263
|
+
height: l.value,
|
|
231
264
|
result: "specular"
|
|
232
|
-
}, null, 8,
|
|
233
|
-
t[1] || (t[1] =
|
|
265
|
+
}, null, 8, qe),
|
|
266
|
+
t[1] || (t[1] = k("feBlend", {
|
|
234
267
|
in: "refracted",
|
|
235
268
|
in2: "specular",
|
|
236
269
|
mode: "screen"
|
|
237
270
|
}, null, -1))
|
|
238
|
-
])])])) :
|
|
239
|
-
default:
|
|
271
|
+
])])])) : fe("", !0), (W(), he(ke(r.as), me(ve(r.asProps)), {
|
|
272
|
+
default: Re(() => [_e(e.$slots, "default", {}, void 0, !0)]),
|
|
240
273
|
_: 3
|
|
241
274
|
}, 16))], 4));
|
|
242
275
|
}
|
|
243
|
-
}),
|
|
244
|
-
const
|
|
245
|
-
for (const [
|
|
246
|
-
return
|
|
247
|
-
},
|
|
248
|
-
|
|
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);
|
|
249
282
|
} };
|
|
250
283
|
export {
|
|
251
|
-
|
|
252
|
-
|
|
284
|
+
Oe as Liqvued,
|
|
285
|
+
De as default
|
|
253
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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ref,
|
|
11
11
|
watch,
|
|
12
12
|
} from 'vue'
|
|
13
|
+
import { distanceToRoundedRectEdge, normalFromRoundedRect } from './shape'
|
|
13
14
|
import type { Surface } from './types'
|
|
14
15
|
|
|
15
16
|
const props = withDefaults(defineProps<{
|
|
@@ -20,6 +21,8 @@ const props = withDefaults(defineProps<{
|
|
|
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
|
|
@@ -34,6 +37,8 @@ const props = withDefaults(defineProps<{
|
|
|
34
37
|
bezel: 22,
|
|
35
38
|
thickness: 42,
|
|
36
39
|
refraction: 1,
|
|
40
|
+
magnification: 0,
|
|
41
|
+
magnificationFocus: 0.82,
|
|
37
42
|
blur: 0.4,
|
|
38
43
|
surface: 'convex',
|
|
39
44
|
specularOpacity: 0.45,
|
|
@@ -49,6 +54,8 @@ const mapUrl = ref('')
|
|
|
49
54
|
const specularUrl = ref('')
|
|
50
55
|
const scale = ref(1)
|
|
51
56
|
const supportsLiquidGlass =
|
|
57
|
+
typeof CSS !== 'undefined' &&
|
|
58
|
+
typeof navigator !== 'undefined' &&
|
|
52
59
|
CSS.supports?.('backdrop-filter', 'url(#x)') &&
|
|
53
60
|
/Chrome/.test(navigator.userAgent)
|
|
54
61
|
|
|
@@ -91,13 +98,67 @@ function convex(x: number) {
|
|
|
91
98
|
return Math.pow(1 - Math.pow(1 - clamp(x), 4), 1 / 4)
|
|
92
99
|
}
|
|
93
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
|
+
|
|
94
140
|
function surfaceFn(x: number) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)
|
|
99
161
|
}
|
|
100
|
-
return convex(x)
|
|
101
162
|
}
|
|
102
163
|
|
|
103
164
|
function derivative(x: number) {
|
|
@@ -118,54 +179,6 @@ function refractSlope(slope: number) {
|
|
|
118
179
|
return Math.tan(theta1 - theta2)
|
|
119
180
|
}
|
|
120
181
|
|
|
121
|
-
function distanceToRoundedRectEdge(
|
|
122
|
-
x: number,
|
|
123
|
-
y: number,
|
|
124
|
-
w: number,
|
|
125
|
-
h: number,
|
|
126
|
-
r: number,
|
|
127
|
-
) {
|
|
128
|
-
const hr = Math.min(r, Math.min(w, h) / 2)
|
|
129
|
-
const hw = w / 2 - hr
|
|
130
|
-
const hh = h / 2 - hr
|
|
131
|
-
const ax = Math.abs(x - w / 2) - hw
|
|
132
|
-
const ay = Math.abs(y - h / 2) - hh
|
|
133
|
-
|
|
134
|
-
const outsidePart = Math.hypot(Math.max(ax, 0), Math.max(ay, 0))
|
|
135
|
-
const insidePart = Math.min(Math.max(ax, ay), 0)
|
|
136
|
-
|
|
137
|
-
return -(insidePart + outsidePart - hr)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function normalFromRoundedRect(
|
|
141
|
-
x: number,
|
|
142
|
-
y: number,
|
|
143
|
-
w: number,
|
|
144
|
-
h: number,
|
|
145
|
-
) {
|
|
146
|
-
const dx = Math.min(x, w - 1 - x)
|
|
147
|
-
const dy = Math.min(y, h - 1 - y)
|
|
148
|
-
|
|
149
|
-
const p = 4
|
|
150
|
-
const wsum = Math.pow(dx, p) + Math.pow(dy, p)
|
|
151
|
-
|
|
152
|
-
let nx: number
|
|
153
|
-
let ny: number
|
|
154
|
-
|
|
155
|
-
if (wsum > 1e-10) {
|
|
156
|
-
nx = (x > w / 2 ? 1 : -1) * (Math.pow(dy, p) / wsum)
|
|
157
|
-
ny = (y > h / 2 ? 1 : -1) * (Math.pow(dx, p) / wsum)
|
|
158
|
-
} else {
|
|
159
|
-
nx = x > w / 2 ? 1 : -1
|
|
160
|
-
ny = y > h / 2 ? 1 : -1
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const len = Math.hypot(nx, ny)
|
|
164
|
-
if (len < 1e-10) return { x: 0, y: -1 }
|
|
165
|
-
|
|
166
|
-
return { x: nx / len, y: ny / len }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
182
|
function canvasToUrl(canvas: HTMLCanvasElement) {
|
|
170
183
|
return canvas.toDataURL('image/png')
|
|
171
184
|
}
|
|
@@ -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 <
|
|
213
|
-
const n = normalFromRoundedRect(x, y, w, h)
|
|
229
|
+
if (dist >= 0 && dist < edgeBand) {
|
|
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
|
|
|
@@ -375,6 +412,8 @@ watch(
|
|
|
375
412
|
props.bezel,
|
|
376
413
|
props.thickness,
|
|
377
414
|
props.refraction,
|
|
415
|
+
props.magnification,
|
|
416
|
+
props.magnificationFocus,
|
|
378
417
|
props.surface,
|
|
379
418
|
props.specularOpacity,
|
|
380
419
|
props.glareAngle,
|
|
@@ -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