liqvued 0.1.2 → 0.2.1
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 +19 -4
- package/dist/liqvued.css +1 -1
- package/dist/liqvued.mjs +210 -151
- package/dist/liqvued.umd.js +1 -1
- package/index.d.ts +43 -0
- package/package.json +17 -17
- package/src/Liqvued.vue +114 -65
- package/src/refraction.test.ts +36 -0
- package/src/refraction.ts +50 -0
- package/src/shape.test.ts +54 -0
- package/src/shape.ts +42 -0
- package/src/surfaces.ts +13 -0
- package/src/types.ts +12 -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,10 +60,13 @@ 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
|
+
| refractionMode | string | `'edge'` | Refraction direction mode: `edge`, `center`, or `split` |
|
|
64
|
+
| magnification | number | 0 | Center magnification amount. Positive values enlarge, negative values shrink |
|
|
65
|
+
| magnificationFocus | number | 0.82 | Focus radius of the magnification area. Lower values concentrate the effect near center |
|
|
63
66
|
| blur | number | 0.4 | CSS backdrop-filter blur amount |
|
|
64
67
|
| surface | string | `'convex'` | Glass surface profile shape |
|
|
65
68
|
| specularOpacity | number | 0.45 | Opacity of the specular highlight |
|
|
66
|
-
| glareAngle | number
|
|
69
|
+
| glareAngle | number \| false | -60 | Light source angle in degrees for specular highlight. Set `false` to disable |
|
|
67
70
|
| glassBackground | string | — | Glass panel background color (auto-derived from `asProps.color` when available) |
|
|
68
71
|
| fallbackOnly | boolean | `false` | When `true`, disables the SVG displacement effect and uses only CSS blur |
|
|
69
72
|
| as | string \| Component | `'div'` | HTML tag or Vue component to render as (rendered as a child of the glass root) |
|
|
@@ -74,6 +77,18 @@ import { Liqvued } from 'liqvued'
|
|
|
74
77
|
- `convex` — squircle convex profile — soft pill-like edges
|
|
75
78
|
- `concave` — inverted convex — inward-curving dish-like refraction
|
|
76
79
|
- `lip` — smooth transition from convex at center to concave at edge
|
|
80
|
+
- `bowl` — deeper concave profile with a more even inward pull
|
|
81
|
+
- `bevel` — linear edge ramp for harder, cleaner refraction
|
|
82
|
+
- `saddle` — mirrored S-curve that shifts direction through the middle
|
|
83
|
+
- `ripple` — small wave modulation across the edge band
|
|
84
|
+
- `noise` — irregular organic edge distortion
|
|
85
|
+
- `asymmetric` — biased profile with more weight toward one side of the edge band
|
|
86
|
+
|
|
87
|
+
### Refraction Modes
|
|
88
|
+
|
|
89
|
+
- `edge` — original behavior; displacement follows the rounded edge normal
|
|
90
|
+
- `center` — displacement points inward from each edge toward the shape centerline
|
|
91
|
+
- `split` — outer and inner bezel halves bend in opposite directions with a neutral midpoint
|
|
77
92
|
|
|
78
93
|
## Dynamic Element Rendering
|
|
79
94
|
|
|
@@ -98,7 +113,7 @@ The fallback applies `backdrop-filter: blur(12px)` with a subtle border and shad
|
|
|
98
113
|
|
|
99
114
|
## How It Works
|
|
100
115
|
|
|
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
|
|
116
|
+
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 strength along the bezel, while `refractionMode` controls the displacement direction. `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
117
|
|
|
103
118
|
## License
|
|
104
119
|
|
package/dist/liqvued.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.liquid-glass[data-v-
|
|
1
|
+
.liquid-glass[data-v-5607a45c]{isolation:isolate;position:relative;overflow:hidden}.liquid-glass__svg[data-v-5607a45c]{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-5607a45c]{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}}
|
|
2
2
|
/*$vite$:1*/
|
package/dist/liqvued.mjs
CHANGED
|
@@ -1,40 +1,78 @@
|
|
|
1
|
-
import { computed as
|
|
2
|
-
|
|
1
|
+
import { computed as C, createBlock as he, createCommentVNode as pe, createElementBlock as ee, createElementVNode as _, defineComponent as ve, guardReactiveProps as ge, inject as me, nextTick as te, normalizeProps as Me, normalizeStyle as ye, onBeforeUnmount as be, onMounted as xe, openBlock as N, provide as _e, ref as k, renderSlot as ke, resolveDynamicComponent as we, unref as Se, watch as Be, withCtx as Ie } from "vue";
|
|
2
|
+
function Re(n, t, i, r, c, y, g) {
|
|
3
|
+
if (n === "center") return t;
|
|
4
|
+
const h = {
|
|
5
|
+
x: -t.x,
|
|
6
|
+
y: -t.y
|
|
7
|
+
};
|
|
8
|
+
if (n === "split") {
|
|
9
|
+
const o = g < 0.5 ? -1 : 1;
|
|
10
|
+
return {
|
|
11
|
+
x: h.x * o,
|
|
12
|
+
y: h.y * o
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return h;
|
|
16
|
+
}
|
|
17
|
+
function $e(n, t) {
|
|
18
|
+
return n !== "split" ? 1 : Math.min(1, Math.abs(t - 0.5) * 2);
|
|
19
|
+
}
|
|
20
|
+
function q(n, t, i, r, c) {
|
|
21
|
+
const y = Math.min(c, Math.min(i, r) / 2), g = i / 2 - y, h = r / 2 - y, o = Math.abs(n - i / 2) - g, I = Math.abs(t - r / 2) - h;
|
|
22
|
+
return -(Math.min(Math.max(o, I), 0) + Math.hypot(Math.max(o, 0), Math.max(I, 0)) - y);
|
|
23
|
+
}
|
|
24
|
+
function Ce(n, t, i, r, c) {
|
|
25
|
+
const g = q(n - 1.5, t, i, r, c) - q(n + 1.5, t, i, r, c), h = q(n, t - 1.5, i, r, c) - q(n, t + 1.5, i, r, c), o = Math.hypot(g, h);
|
|
26
|
+
return o < 1e-10 ? {
|
|
27
|
+
x: 0,
|
|
28
|
+
y: -1
|
|
29
|
+
} : {
|
|
30
|
+
x: g / o,
|
|
31
|
+
y: h / o
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
var qe = {
|
|
3
35
|
key: 0,
|
|
4
36
|
class: "liquid-glass__svg",
|
|
5
37
|
"aria-hidden": "true"
|
|
6
|
-
},
|
|
38
|
+
}, Pe = [
|
|
7
39
|
"href",
|
|
8
40
|
"width",
|
|
9
41
|
"height"
|
|
10
|
-
],
|
|
42
|
+
], Fe = ["scale"], Oe = [
|
|
11
43
|
"href",
|
|
12
44
|
"width",
|
|
13
45
|
"height"
|
|
14
|
-
],
|
|
46
|
+
], De = /* @__PURE__ */ ve({
|
|
15
47
|
__name: "Liqvued",
|
|
16
48
|
props: {
|
|
17
49
|
as: { default: "div" },
|
|
18
50
|
asProps: { default: () => ({}) },
|
|
19
51
|
radius: { default: 32 },
|
|
20
|
-
borderRadius: {},
|
|
52
|
+
borderRadius: { default: void 0 },
|
|
21
53
|
bezel: { default: 22 },
|
|
22
54
|
thickness: { default: 42 },
|
|
23
55
|
refraction: { default: 1 },
|
|
56
|
+
refractionMode: { default: "edge" },
|
|
57
|
+
magnification: { default: 0 },
|
|
58
|
+
magnificationFocus: { default: 0.82 },
|
|
24
59
|
blur: { default: 0.4 },
|
|
25
60
|
surface: { default: "convex" },
|
|
26
61
|
specularOpacity: { default: 0.45 },
|
|
27
|
-
glareAngle: {
|
|
62
|
+
glareAngle: {
|
|
63
|
+
type: [Number, Boolean],
|
|
64
|
+
default: -60
|
|
65
|
+
},
|
|
28
66
|
glassBackground: { default: void 0 },
|
|
29
67
|
fallbackOnly: {
|
|
30
68
|
type: Boolean,
|
|
31
69
|
default: !1
|
|
32
70
|
}
|
|
33
71
|
},
|
|
34
|
-
setup(
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const
|
|
72
|
+
setup(n) {
|
|
73
|
+
const t = n, i = k(null), r = k(1), c = k(1), y = k(""), g = k(""), h = k(1), o = typeof CSS < "u" && typeof navigator < "u" && CSS.supports?.("backdrop-filter", "url(#x)") && /Chrome/.test(navigator.userAgent), I = `${`lg-${Math.random().toString(36).slice(2)}`}-filter`, W = me("_liqvued", !1);
|
|
74
|
+
_e("_liqvued", !0);
|
|
75
|
+
const w = k(!1), V = {
|
|
38
76
|
0: 0,
|
|
39
77
|
sm: 4,
|
|
40
78
|
md: 8,
|
|
@@ -44,92 +82,110 @@ var we = {
|
|
|
44
82
|
circle: 9999,
|
|
45
83
|
shaped: 16
|
|
46
84
|
};
|
|
47
|
-
function
|
|
48
|
-
return
|
|
85
|
+
function j() {
|
|
86
|
+
return i.value;
|
|
49
87
|
}
|
|
50
|
-
function
|
|
51
|
-
return Math.max(
|
|
88
|
+
function u(e, a = 0, s = 1) {
|
|
89
|
+
return Math.max(a, Math.min(s, e));
|
|
52
90
|
}
|
|
53
|
-
function
|
|
54
|
-
return e =
|
|
91
|
+
function R(e) {
|
|
92
|
+
return e = u(e), e * e * e * (e * (e * 6 - 15) + 10);
|
|
55
93
|
}
|
|
56
|
-
function
|
|
57
|
-
return Math.pow(1 - Math.pow(1 -
|
|
94
|
+
function P(e) {
|
|
95
|
+
return Math.pow(1 - Math.pow(1 - u(e), 4), 1 / 4);
|
|
58
96
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
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);
|
|
97
|
+
function ae(e, a, s) {
|
|
98
|
+
return u(e + Math.sin(u(e) * Math.PI * a) * s);
|
|
66
99
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
100
|
+
function ne(e) {
|
|
101
|
+
const a = u(e);
|
|
102
|
+
return u(a + Math.sin(a * 39.1) * 0.06 + Math.sin(a * 91.7) * 0.035);
|
|
69
103
|
}
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
return
|
|
104
|
+
function re(e, a, s, v, b, S) {
|
|
105
|
+
const m = s / 2, M = v / 2, $ = Math.max(1, s / 2), x = Math.max(1, v / 2), D = (e - m) / $, z = (a - M) / x, l = Math.hypot(D, z), d = u(S, 0.2, 1.4), p = Math.pow(R(1 - u(l / d)), 1.6), f = b >= 0 ? 1 + b * p : 1 / (1 + Math.abs(b) * p);
|
|
106
|
+
return {
|
|
107
|
+
x: (e - m) / f - (e - m),
|
|
108
|
+
y: (a - M) / f - (a - M)
|
|
109
|
+
};
|
|
73
110
|
}
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
|
|
111
|
+
function H(e) {
|
|
112
|
+
const a = u(e);
|
|
113
|
+
switch (t.surface) {
|
|
114
|
+
case "concave":
|
|
115
|
+
case "bowl":
|
|
116
|
+
return 1 - P(a);
|
|
117
|
+
case "lip":
|
|
118
|
+
return P(a) * (1 - R(a)) + (1 - P(a)) * R(a);
|
|
119
|
+
case "bevel":
|
|
120
|
+
return a;
|
|
121
|
+
case "saddle":
|
|
122
|
+
return 0.5 + Math.sin((a - 0.5) * Math.PI) * 0.5;
|
|
123
|
+
case "ripple":
|
|
124
|
+
return ae(a, 3, 0.12);
|
|
125
|
+
case "noise":
|
|
126
|
+
return ne(a);
|
|
127
|
+
case "asymmetric":
|
|
128
|
+
return u(Math.pow(a, 0.65) * 0.65 + R(a) * 0.35);
|
|
129
|
+
case "convex":
|
|
130
|
+
return P(a);
|
|
131
|
+
}
|
|
77
132
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
return
|
|
84
|
-
x: 0,
|
|
85
|
-
y: -1
|
|
86
|
-
} : {
|
|
87
|
-
x: i / _,
|
|
88
|
-
y: o / _
|
|
89
|
-
};
|
|
133
|
+
function oe(e) {
|
|
134
|
+
return (H(e + 1e-3) - H(e - 1e-3)) / (2 * 1e-3);
|
|
135
|
+
}
|
|
136
|
+
function se(e) {
|
|
137
|
+
const v = Math.atan(e), b = 1 / 1.5 * Math.sin(v), S = Math.asin(u(b, -1, 1));
|
|
138
|
+
return Math.tan(v - S);
|
|
90
139
|
}
|
|
91
|
-
function
|
|
140
|
+
function J(e) {
|
|
92
141
|
return e.toDataURL("image/png");
|
|
93
142
|
}
|
|
94
|
-
function
|
|
95
|
-
if (
|
|
96
|
-
const e = Math.max(1, Math.round(
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
let
|
|
102
|
-
const
|
|
103
|
-
x: Math.cos(
|
|
104
|
-
y: Math.sin(
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
function A() {
|
|
144
|
+
if (t.fallbackOnly) return;
|
|
145
|
+
const e = Math.max(1, Math.round(r.value)), a = Math.max(1, Math.round(c.value)), s = document.createElement("canvas");
|
|
146
|
+
s.width = e, s.height = a;
|
|
147
|
+
const v = document.createElement("canvas");
|
|
148
|
+
v.width = e, v.height = a;
|
|
149
|
+
const b = s.getContext("2d"), S = v.getContext("2d"), m = b.createImageData(e, a), M = S.createImageData(e, a), $ = [];
|
|
150
|
+
let x = 1;
|
|
151
|
+
const D = t.glareAngle !== !1 ? {
|
|
152
|
+
x: Math.cos(t.glareAngle * Math.PI / 180),
|
|
153
|
+
y: Math.sin(t.glareAngle * Math.PI / 180)
|
|
154
|
+
} : {
|
|
155
|
+
x: 0,
|
|
156
|
+
y: 0
|
|
157
|
+
}, z = Math.max(1, Math.min(t.bezel, Math.min(e, a) * 0.28));
|
|
158
|
+
for (let l = 0; l < a; l++) for (let d = 0; d < e; d++) {
|
|
159
|
+
const p = q(d, l, e, a, F.value), f = u(p / z);
|
|
160
|
+
let L = 0, E = 0, X = 0;
|
|
161
|
+
if (p >= 0 && p < z) {
|
|
162
|
+
const B = Ce(d, l, e, a, F.value), Y = se(oe(f)) * t.thickness * R(1 - f) * $e(t.refractionMode, f) * t.refraction, Z = Re(t.refractionMode, B, d, l, e, a, f);
|
|
163
|
+
L = Z.x * Y, E = Z.y * Y, x = Math.max(x, Math.abs(L), Math.abs(E));
|
|
164
|
+
const fe = u(B.x * D.x + B.y * D.y, 0, 1);
|
|
165
|
+
X = Math.pow(fe, 18) * t.specularOpacity;
|
|
166
|
+
}
|
|
167
|
+
if (t.magnification !== 0 && p >= 0) {
|
|
168
|
+
const B = re(d, l, e, a, t.magnification, t.magnificationFocus);
|
|
169
|
+
L += B.x, E += B.y;
|
|
114
170
|
}
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
171
|
+
$.push([L, E]);
|
|
172
|
+
const U = (l * e + d) * 4, de = Math.round(X * 255);
|
|
173
|
+
M.data[U] = 255, M.data[U + 1] = 255, M.data[U + 2] = 255, M.data[U + 3] = de;
|
|
118
174
|
}
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
}),
|
|
175
|
+
x = $.reduce((l, [d, p]) => Math.max(l, Math.abs(d), Math.abs(p)), 1), $.forEach(([l, d], p) => {
|
|
176
|
+
const f = p * 4;
|
|
177
|
+
m.data[f] = Math.round(128 + l / x * 127), m.data[f + 1] = Math.round(128 + d / x * 127), m.data[f + 2] = 128, m.data[f + 3] = 255;
|
|
178
|
+
}), b.putImageData(m, 0, 0), S.putImageData(M, 0, 0), y.value = J(s), g.value = J(v), h.value = x;
|
|
123
179
|
}
|
|
124
|
-
function
|
|
125
|
-
const e =
|
|
126
|
-
e && (
|
|
180
|
+
function K() {
|
|
181
|
+
const e = j();
|
|
182
|
+
e && (r.value = Math.max(1, e.offsetWidth), c.value = Math.max(1, e.offsetHeight));
|
|
127
183
|
}
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
const e =
|
|
131
|
-
return typeof e.rounded == "string" && e.rounded in
|
|
132
|
-
}),
|
|
184
|
+
const Q = o ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", F = C(() => {
|
|
185
|
+
if (t.radius !== 32) return t.radius;
|
|
186
|
+
const e = t.asProps;
|
|
187
|
+
return typeof e.rounded == "string" && e.rounded in V ? V[e.rounded] : t.radius;
|
|
188
|
+
}), ie = C(() => t.borderRadius || `${F.value}px`), ce = /* @__PURE__ */ new Set([
|
|
133
189
|
"primary",
|
|
134
190
|
"secondary",
|
|
135
191
|
"accent",
|
|
@@ -140,60 +196,63 @@ var we = {
|
|
|
140
196
|
"surface",
|
|
141
197
|
"background"
|
|
142
198
|
]);
|
|
143
|
-
function
|
|
144
|
-
if (
|
|
199
|
+
function ue(e, a) {
|
|
200
|
+
if (ce.has(e)) return `rgba(var(--v-theme-${e}), ${a})`;
|
|
145
201
|
if (e.startsWith("#")) {
|
|
146
|
-
const
|
|
147
|
-
return `rgba(${parseInt(
|
|
202
|
+
const s = e.replace("#", "");
|
|
203
|
+
return `rgba(${parseInt(s.slice(0, 2), 16)}, ${parseInt(s.slice(2, 4), 16)}, ${parseInt(s.slice(4, 6), 16)}, ${a})`;
|
|
148
204
|
}
|
|
149
|
-
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${
|
|
205
|
+
return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${a})`) : Q;
|
|
150
206
|
}
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
const e =
|
|
154
|
-
return typeof e.color == "string" ?
|
|
155
|
-
}),
|
|
156
|
-
borderRadius: `${
|
|
157
|
-
...
|
|
158
|
-
...
|
|
159
|
-
...
|
|
160
|
-
backdropFilter:
|
|
161
|
-
WebkitBackdropFilter:
|
|
207
|
+
const T = C(() => {
|
|
208
|
+
if (t.glassBackground !== void 0) return t.glassBackground;
|
|
209
|
+
const e = t.asProps;
|
|
210
|
+
return typeof e.color == "string" ? ue(e.color, 0.6) : Q;
|
|
211
|
+
}), O = C(() => t.fallbackOnly || !o ? "blur(12px)" : `url(#${I}) blur(${t.blur}px)`), le = C(() => ({
|
|
212
|
+
borderRadius: `${ie.value}`,
|
|
213
|
+
...T.value !== void 0 ? { backgroundColor: T.value } : {},
|
|
214
|
+
...T.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(", ") } : {},
|
|
215
|
+
...w.value && !W ? {
|
|
216
|
+
backdropFilter: O.value,
|
|
217
|
+
WebkitBackdropFilter: O.value
|
|
162
218
|
} : {},
|
|
163
|
-
...
|
|
164
|
-
backdropFilter:
|
|
165
|
-
WebkitBackdropFilter:
|
|
219
|
+
...w.value && W && !o ? {
|
|
220
|
+
backdropFilter: O.value,
|
|
221
|
+
WebkitBackdropFilter: O.value
|
|
166
222
|
} : {}
|
|
167
223
|
}));
|
|
168
|
-
let
|
|
169
|
-
return
|
|
170
|
-
if (await
|
|
171
|
-
|
|
172
|
-
|
|
224
|
+
let G = null;
|
|
225
|
+
return xe(async () => {
|
|
226
|
+
if (await te(), o) {
|
|
227
|
+
K(), A(), await te(), w.value = !0, G = new ResizeObserver(() => {
|
|
228
|
+
K(), A(), w.value = !0;
|
|
173
229
|
});
|
|
174
|
-
const e =
|
|
175
|
-
e &&
|
|
176
|
-
} else
|
|
177
|
-
}),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
230
|
+
const e = j();
|
|
231
|
+
e && G.observe(e);
|
|
232
|
+
} else w.value = !0;
|
|
233
|
+
}), Be(() => [
|
|
234
|
+
F.value,
|
|
235
|
+
t.bezel,
|
|
236
|
+
t.thickness,
|
|
237
|
+
t.refraction,
|
|
238
|
+
t.refractionMode,
|
|
239
|
+
t.magnification,
|
|
240
|
+
t.magnificationFocus,
|
|
241
|
+
t.surface,
|
|
242
|
+
t.specularOpacity,
|
|
243
|
+
t.glareAngle,
|
|
244
|
+
t.fallbackOnly
|
|
186
245
|
], () => {
|
|
187
|
-
|
|
188
|
-
}),
|
|
189
|
-
|
|
190
|
-
}), (e,
|
|
246
|
+
o && A(), w.value = !0;
|
|
247
|
+
}), be(() => {
|
|
248
|
+
G?.disconnect();
|
|
249
|
+
}), (e, a) => (N(), ee("div", {
|
|
191
250
|
ref_key: "root",
|
|
192
|
-
ref:
|
|
251
|
+
ref: i,
|
|
193
252
|
class: "liquid-glass",
|
|
194
|
-
style:
|
|
195
|
-
}, [
|
|
196
|
-
id:
|
|
253
|
+
style: ye(le.value)
|
|
254
|
+
}, [Se(o) ? (N(), ee("svg", qe, [_("defs", null, [_("filter", {
|
|
255
|
+
id: I,
|
|
197
256
|
"color-interpolation-filters": "sRGB",
|
|
198
257
|
filterUnits: "userSpaceOnUse",
|
|
199
258
|
x: "0",
|
|
@@ -201,53 +260,53 @@ var we = {
|
|
|
201
260
|
width: "10000",
|
|
202
261
|
height: "10000"
|
|
203
262
|
}, [
|
|
204
|
-
|
|
205
|
-
href:
|
|
263
|
+
_("feImage", {
|
|
264
|
+
href: y.value,
|
|
206
265
|
x: "0",
|
|
207
266
|
y: "0",
|
|
208
|
-
width:
|
|
209
|
-
height:
|
|
267
|
+
width: r.value,
|
|
268
|
+
height: c.value,
|
|
210
269
|
result: "displacement_map"
|
|
211
|
-
}, null, 8,
|
|
212
|
-
|
|
270
|
+
}, null, 8, Pe),
|
|
271
|
+
a[0] || (a[0] = _("feGaussianBlur", {
|
|
213
272
|
in: "displacement_map",
|
|
214
273
|
stdDeviation: "2",
|
|
215
274
|
result: "smooth_disp"
|
|
216
275
|
}, null, -1)),
|
|
217
|
-
|
|
276
|
+
_("feDisplacementMap", {
|
|
218
277
|
in: "SourceGraphic",
|
|
219
278
|
in2: "smooth_disp",
|
|
220
|
-
scale:
|
|
279
|
+
scale: h.value,
|
|
221
280
|
xChannelSelector: "R",
|
|
222
281
|
yChannelSelector: "G",
|
|
223
282
|
result: "refracted"
|
|
224
|
-
}, null, 8,
|
|
225
|
-
|
|
226
|
-
href:
|
|
283
|
+
}, null, 8, Fe),
|
|
284
|
+
_("feImage", {
|
|
285
|
+
href: g.value,
|
|
227
286
|
x: "0",
|
|
228
287
|
y: "0",
|
|
229
|
-
width:
|
|
230
|
-
height:
|
|
288
|
+
width: r.value,
|
|
289
|
+
height: c.value,
|
|
231
290
|
result: "specular"
|
|
232
|
-
}, null, 8,
|
|
233
|
-
|
|
291
|
+
}, null, 8, Oe),
|
|
292
|
+
a[1] || (a[1] = _("feBlend", {
|
|
234
293
|
in: "refracted",
|
|
235
294
|
in2: "specular",
|
|
236
295
|
mode: "screen"
|
|
237
296
|
}, null, -1))
|
|
238
|
-
])])])) :
|
|
239
|
-
default:
|
|
297
|
+
])])])) : pe("", !0), (N(), he(we(n.as), Me(ge(n.asProps)), {
|
|
298
|
+
default: Ie(() => [ke(e.$slots, "default", {}, void 0, !0)]),
|
|
240
299
|
_: 3
|
|
241
300
|
}, 16))], 4));
|
|
242
301
|
}
|
|
243
|
-
}),
|
|
244
|
-
const
|
|
245
|
-
for (const [
|
|
246
|
-
return
|
|
247
|
-
},
|
|
248
|
-
|
|
302
|
+
}), ze = (n, t) => {
|
|
303
|
+
const i = n.__vccOpts || n;
|
|
304
|
+
for (const [r, c] of t) i[r] = c;
|
|
305
|
+
return i;
|
|
306
|
+
}, Le = /* @__PURE__ */ ze(De, [["__scopeId", "data-v-5607a45c"]]), Ue = { install(n) {
|
|
307
|
+
n.component("Liqvued", Le);
|
|
249
308
|
} };
|
|
250
309
|
export {
|
|
251
|
-
|
|
252
|
-
|
|
310
|
+
Le as Liqvued,
|
|
311
|
+
Ue as default
|
|
253
312
|
};
|
package/dist/liqvued.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(b,n){typeof exports=="object"&&typeof module<"u"?n(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],n):(b=typeof globalThis<"u"?globalThis:b||self,n(b.Liqvued={},b.Vue))})(this,function(b,n){Object.defineProperties(b,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});function Z(r,t,c,o,l,x,M){if(r==="center")return t;const p={x:-t.x,y:-t.y};if(r==="split"){const s=M<.5?-1:1;return{x:p.x*s,y:p.y*s}}return p}function ee(r,t){return r!=="split"?1:Math.min(1,Math.abs(t-.5)*2)}function I(r,t,c,o,l){const x=Math.min(l,Math.min(c,o)/2),M=c/2-x,p=o/2-x,s=Math.abs(r-c/2)-M,R=Math.abs(t-o/2)-p;return-(Math.min(Math.max(s,R),0)+Math.hypot(Math.max(s,0),Math.max(R,0))-x)}function te(r,t,c,o,l){const M=I(r-1.5,t,c,o,l)-I(r+1.5,t,c,o,l),p=I(r,t-1.5,c,o,l)-I(r,t+1.5,c,o,l),s=Math.hypot(M,p);return s<1e-10?{x:0,y:-1}:{x:M/s,y:p/s}}var ae={key:0,class:"liquid-glass__svg","aria-hidden":"true"},ne=["href","width","height"],re=["scale"],oe=["href","width","height"],se=(0,n.defineComponent)({__name:"Liqvued",props:{as:{default:"div"},asProps:{default:()=>({})},radius:{default:32},borderRadius:{default:void 0},bezel:{default:22},thickness:{default:42},refraction:{default:1},refractionMode:{default:"edge"},magnification:{default:0},magnificationFocus:{default:.82},blur:{default:.4},surface:{default:"convex"},specularOpacity:{default:.45},glareAngle:{type:[Number,Boolean],default:-60},glassBackground:{default:void 0},fallbackOnly:{type:Boolean,default:!1}},setup(r){const t=r,c=(0,n.ref)(null),o=(0,n.ref)(1),l=(0,n.ref)(1),x=(0,n.ref)(""),M=(0,n.ref)(""),p=(0,n.ref)(1),s=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,n.inject)("_liqvued",!1);(0,n.provide)("_liqvued",!0);const w=(0,n.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,i=1){return Math.max(a,Math.min(i,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 le(e,a,i){return u(e+Math.sin(u(e)*Math.PI*a)*i)}function ue(e){const a=u(e);return u(a+Math.sin(a*39.1)*.06+Math.sin(a*91.7)*.035)}function de(e,a,i,g,_,B){const v=i/2,y=g/2,$=Math.max(1,i/2),k=Math.max(1,g/2),N=(e-v)/$,O=(a-y)/k,d=Math.hypot(N,O),f=u(B,.2,1.4),m=Math.pow(E(1-u(d/f)),1.6),h=_>=0?1+_*m:1/(1+Math.abs(_)*m);return{x:(e-v)/h-(e-v),y:(a-y)/h-(a-y)}}function j(e){const a=u(e);switch(t.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 le(a,3,.12);case"noise":return ue(a);case"asymmetric":return u(Math.pow(a,.65)*.65+E(a)*.35);case"convex":return q(a)}}function fe(e){return(j(e+.001)-j(e-.001))/(2*.001)}function he(e){const g=Math.atan(e),_=1/1.5*Math.sin(g),B=Math.asin(u(_,-1,1));return Math.tan(g-B)}function H(e){return e.toDataURL("image/png")}function D(){if(t.fallbackOnly)return;const e=Math.max(1,Math.round(o.value)),a=Math.max(1,Math.round(l.value)),i=document.createElement("canvas");i.width=e,i.height=a;const g=document.createElement("canvas");g.width=e,g.height=a;const _=i.getContext("2d"),B=g.getContext("2d"),v=_.createImageData(e,a),y=B.createImageData(e,a),$=[];let k=1;const N=t.glareAngle!==!1?{x:Math.cos(t.glareAngle*Math.PI/180),y:Math.sin(t.glareAngle*Math.PI/180)}:{x:0,y:0},O=Math.max(1,Math.min(t.bezel,Math.min(e,a)*.28));for(let d=0;d<a;d++)for(let f=0;f<e;f++){const m=I(f,d,e,a,C.value),h=u(m/O);let V=0,F=0,Q=0;if(m>=0&&m<O){const S=te(f,d,e,a,C.value),X=he(fe(h))*t.thickness*E(1-h)*ee(t.refractionMode,h)*t.refraction,Y=Z(t.refractionMode,S,f,d,e,a,h);V=Y.x*X,F=Y.y*X,k=Math.max(k,Math.abs(V),Math.abs(F));const ye=u(S.x*N.x+S.y*N.y,0,1);Q=Math.pow(ye,18)*t.specularOpacity}if(t.magnification!==0&&m>=0){const S=de(f,d,e,a,t.magnification,t.magnificationFocus);V+=S.x,F+=S.y}$.push([V,F]);const T=(d*e+f)*4,ve=Math.round(Q*255);y.data[T]=255,y.data[T+1]=255,y.data[T+2]=255,y.data[T+3]=ve}k=$.reduce((d,[f,m])=>Math.max(d,Math.abs(f),Math.abs(m)),1),$.forEach(([d,f],m)=>{const h=m*4;v.data[h]=Math.round(128+d/k*127),v.data[h+1]=Math.round(128+f/k*127),v.data[h+2]=128,v.data[h+3]=255}),_.putImageData(v,0,0),B.putImageData(y,0,0),x.value=H(i),M.value=H(g),p.value=k}function J(){const e=W();e&&(o.value=Math.max(1,e.offsetWidth),l.value=Math.max(1,e.offsetHeight))}const K=s?"rgba(255, 255, 255, 0.08)":"rgba(255, 255, 255, 0.22)",C=(0,n.computed)(()=>{if(t.radius!==32)return t.radius;const e=t.asProps;return typeof e.rounded=="string"&&e.rounded in G?G[e.rounded]:t.radius}),pe=(0,n.computed)(()=>t.borderRadius||`${C.value}px`),me=new Set(["primary","secondary","accent","info","warning","error","success","surface","background"]);function ge(e,a){if(me.has(e))return`rgba(var(--v-theme-${e}), ${a})`;if(e.startsWith("#")){const i=e.replace("#","");return`rgba(${parseInt(i.slice(0,2),16)}, ${parseInt(i.slice(2,4),16)}, ${parseInt(i.slice(4,6),16)}, ${a})`}return e.startsWith("rgb")?e.replace("rgb(","rgba(").replace(")",`, ${a})`):K}const L=(0,n.computed)(()=>{if(t.glassBackground!==void 0)return t.glassBackground;const e=t.asProps;return typeof e.color=="string"?ge(e.color,.6):K}),P=(0,n.computed)(()=>t.fallbackOnly||!s?"blur(12px)":`url(#${R}) blur(${t.blur}px)`),Me=(0,n.computed)(()=>({borderRadius:`${pe.value}`,...L.value!==void 0?{backgroundColor:L.value}:{},...L.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(", ")}:{},...w.value&&!A?{backdropFilter:P.value,WebkitBackdropFilter:P.value}:{},...w.value&&A&&!s?{backdropFilter:P.value,WebkitBackdropFilter:P.value}:{}}));let z=null;return(0,n.onMounted)(async()=>{if(await(0,n.nextTick)(),s){J(),D(),await(0,n.nextTick)(),w.value=!0,z=new ResizeObserver(()=>{J(),D(),w.value=!0});const e=W();e&&z.observe(e)}else w.value=!0}),(0,n.watch)(()=>[C.value,t.bezel,t.thickness,t.refraction,t.refractionMode,t.magnification,t.magnificationFocus,t.surface,t.specularOpacity,t.glareAngle,t.fallbackOnly],()=>{s&&D(),w.value=!0}),(0,n.onBeforeUnmount)(()=>{z?.disconnect()}),(e,a)=>((0,n.openBlock)(),(0,n.createElementBlock)("div",{ref_key:"root",ref:c,class:"liquid-glass",style:(0,n.normalizeStyle)(Me.value)},[(0,n.unref)(s)?((0,n.openBlock)(),(0,n.createElementBlock)("svg",ae,[(0,n.createElementVNode)("defs",null,[(0,n.createElementVNode)("filter",{id:R,"color-interpolation-filters":"sRGB",filterUnits:"userSpaceOnUse",x:"0",y:"0",width:"10000",height:"10000"},[(0,n.createElementVNode)("feImage",{href:x.value,x:"0",y:"0",width:o.value,height:l.value,result:"displacement_map"},null,8,ne),a[0]||(a[0]=(0,n.createElementVNode)("feGaussianBlur",{in:"displacement_map",stdDeviation:"2",result:"smooth_disp"},null,-1)),(0,n.createElementVNode)("feDisplacementMap",{in:"SourceGraphic",in2:"smooth_disp",scale:p.value,xChannelSelector:"R",yChannelSelector:"G",result:"refracted"},null,8,re),(0,n.createElementVNode)("feImage",{href:M.value,x:"0",y:"0",width:o.value,height:l.value,result:"specular"},null,8,oe),a[1]||(a[1]=(0,n.createElementVNode)("feBlend",{in:"refracted",in2:"specular",mode:"screen"},null,-1))])])])):(0,n.createCommentVNode)("",!0),((0,n.openBlock)(),(0,n.createBlock)((0,n.resolveDynamicComponent)(r.as),(0,n.normalizeProps)((0,n.guardReactiveProps)(r.asProps)),{default:(0,n.withCtx)(()=>[(0,n.renderSlot)(e.$slots,"default",{},void 0,!0)]),_:3},16))],4))}}),ie=(r,t)=>{const c=r.__vccOpts||r;for(const[o,l]of t)c[o]=l;return c},U=ie(se,[["__scopeId","data-v-5607a45c"]]),ce={install(r){r.component("Liqvued",U)}};b.Liqvued=U,b.default=ce});
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type Surface =
|
|
4
|
+
| 'convex'
|
|
5
|
+
| 'concave'
|
|
6
|
+
| 'lip'
|
|
7
|
+
| 'bowl'
|
|
8
|
+
| 'bevel'
|
|
9
|
+
| 'saddle'
|
|
10
|
+
| 'ripple'
|
|
11
|
+
| 'noise'
|
|
12
|
+
| 'asymmetric'
|
|
13
|
+
|
|
14
|
+
export type RefractionMode = 'edge' | 'center' | 'split'
|
|
15
|
+
|
|
16
|
+
export interface LiqvuedProps {
|
|
17
|
+
as?: string | Component
|
|
18
|
+
asProps?: Record<string, unknown>
|
|
19
|
+
radius?: number
|
|
20
|
+
borderRadius?: string
|
|
21
|
+
bezel?: number
|
|
22
|
+
thickness?: number
|
|
23
|
+
refraction?: number
|
|
24
|
+
refractionMode?: RefractionMode
|
|
25
|
+
magnification?: number
|
|
26
|
+
magnificationFocus?: number
|
|
27
|
+
blur?: number
|
|
28
|
+
surface?: Surface
|
|
29
|
+
specularOpacity?: number
|
|
30
|
+
glareAngle?: number | false
|
|
31
|
+
glassBackground?: string
|
|
32
|
+
fallbackOnly?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare const Liqvued: new() => {
|
|
36
|
+
$props: LiqvuedProps
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare const _default: {
|
|
40
|
+
install(app: unknown): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default _default
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "liqvued",
|
|
3
3
|
"types": "./index.d.ts",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Liquid-glass for Vue",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -49,32 +49,32 @@
|
|
|
49
49
|
"author": "Igor Voytovich",
|
|
50
50
|
"license": "MIT",
|
|
51
51
|
"repository": "https://github.com/IVoyt/liqvued",
|
|
52
|
-
"packageManager": "pnpm@
|
|
52
|
+
"packageManager": "pnpm@11.17.0",
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"vue": "^3.5.14"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@intlify/unplugin-vue-i18n": "^11.
|
|
57
|
+
"@intlify/unplugin-vue-i18n": "^11.2.4",
|
|
58
58
|
"@rollup/plugin-alias": "^5.1.1",
|
|
59
59
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
60
|
-
"@types/node": "^
|
|
61
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
62
|
-
"@vitejs/plugin-vue-jsx": "^5.1.
|
|
63
|
-
"eslint": "^10.
|
|
60
|
+
"@types/node": "^26.1.1",
|
|
61
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
62
|
+
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
63
|
+
"eslint": "^10.8.0",
|
|
64
64
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
65
65
|
"eslint-plugin-case-police": "^2.2.1",
|
|
66
66
|
"eslint-plugin-import": "^2.32.0",
|
|
67
|
-
"eslint-plugin-promise": "^7.
|
|
68
|
-
"eslint-plugin-sonarjs": "^4.0
|
|
69
|
-
"eslint-plugin-unicorn": "^
|
|
67
|
+
"eslint-plugin-promise": "^7.3.0",
|
|
68
|
+
"eslint-plugin-sonarjs": "^4.2.0",
|
|
69
|
+
"eslint-plugin-unicorn": "^72.0.0",
|
|
70
70
|
"eslint-plugin-vue": "^10.9.0",
|
|
71
|
-
"sass-embedded": "^1.
|
|
72
|
-
"tsx": "^4.
|
|
71
|
+
"sass-embedded": "^1.100.0",
|
|
72
|
+
"tsx": "^4.23.1",
|
|
73
73
|
"typescript": "^5.9.3",
|
|
74
|
-
"typescript-eslint": "^8.
|
|
75
|
-
"unplugin-vue-components": "^
|
|
76
|
-
"vite": "^8.
|
|
74
|
+
"typescript-eslint": "^8.65.0",
|
|
75
|
+
"unplugin-vue-components": "^32.1.0",
|
|
76
|
+
"vite": "^8.1.5",
|
|
77
77
|
"vue-eslint-parser": "^10.4.0",
|
|
78
|
-
"vue-tsc": "^3.
|
|
78
|
+
"vue-tsc": "^3.3.8"
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|
package/src/Liqvued.vue
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
ref,
|
|
11
11
|
watch,
|
|
12
12
|
} from 'vue'
|
|
13
|
-
import
|
|
13
|
+
import { refractionDirection, refractionModeStrength } from './refraction'
|
|
14
|
+
import { distanceToRoundedRectEdge, normalFromRoundedRect } from './shape'
|
|
15
|
+
import type { RefractionMode, Surface } from './types'
|
|
14
16
|
|
|
15
17
|
const props = withDefaults(defineProps<{
|
|
16
18
|
as?: string | Component
|
|
@@ -20,10 +22,13 @@ const props = withDefaults(defineProps<{
|
|
|
20
22
|
bezel?: number
|
|
21
23
|
thickness?: number
|
|
22
24
|
refraction?: number
|
|
25
|
+
refractionMode?: RefractionMode
|
|
26
|
+
magnification?: number
|
|
27
|
+
magnificationFocus?: number
|
|
23
28
|
blur?: number
|
|
24
29
|
surface?: Surface
|
|
25
30
|
specularOpacity?: number
|
|
26
|
-
glareAngle?: number
|
|
31
|
+
glareAngle?: number | false
|
|
27
32
|
glassBackground?: string
|
|
28
33
|
fallbackOnly?: boolean
|
|
29
34
|
}>(), {
|
|
@@ -31,9 +36,13 @@ const props = withDefaults(defineProps<{
|
|
|
31
36
|
asProps: () => ({}),
|
|
32
37
|
fallbackOnly: false,
|
|
33
38
|
radius: 32,
|
|
39
|
+
borderRadius: undefined,
|
|
34
40
|
bezel: 22,
|
|
35
41
|
thickness: 42,
|
|
36
42
|
refraction: 1,
|
|
43
|
+
refractionMode: 'edge',
|
|
44
|
+
magnification: 0,
|
|
45
|
+
magnificationFocus: 0.82,
|
|
37
46
|
blur: 0.4,
|
|
38
47
|
surface: 'convex',
|
|
39
48
|
specularOpacity: 0.45,
|
|
@@ -49,6 +58,8 @@ const mapUrl = ref('')
|
|
|
49
58
|
const specularUrl = ref('')
|
|
50
59
|
const scale = ref(1)
|
|
51
60
|
const supportsLiquidGlass =
|
|
61
|
+
typeof CSS !== 'undefined' &&
|
|
62
|
+
typeof navigator !== 'undefined' &&
|
|
52
63
|
CSS.supports?.('backdrop-filter', 'url(#x)') &&
|
|
53
64
|
/Chrome/.test(navigator.userAgent)
|
|
54
65
|
|
|
@@ -91,13 +102,67 @@ function convex(x: number) {
|
|
|
91
102
|
return Math.pow(1 - Math.pow(1 - clamp(x), 4), 1 / 4)
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
function wave(x: number, frequency: number, amplitude: number) {
|
|
106
|
+
return clamp(x + Math.sin(clamp(x) * Math.PI * frequency) * amplitude)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deterministicNoise(x: number) {
|
|
110
|
+
const t = clamp(x)
|
|
111
|
+
return clamp(t + Math.sin(t * 39.1) * 0.06 + Math.sin(t * 91.7) * 0.035)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function magnificationVector(
|
|
115
|
+
x: number,
|
|
116
|
+
y: number,
|
|
117
|
+
w: number,
|
|
118
|
+
h: number,
|
|
119
|
+
magnification: number,
|
|
120
|
+
magnificationFocus: number,
|
|
121
|
+
) {
|
|
122
|
+
const cx = w / 2
|
|
123
|
+
const cy = h / 2
|
|
124
|
+
const rx = Math.max(1, w / 2)
|
|
125
|
+
const ry = Math.max(1, h / 2)
|
|
126
|
+
const dx = (x - cx) / rx
|
|
127
|
+
const dy = (y - cy) / ry
|
|
128
|
+
const radius = Math.hypot(dx, dy)
|
|
129
|
+
const focusRadius = clamp(magnificationFocus, 0.2, 1.4)
|
|
130
|
+
const focus = Math.pow(
|
|
131
|
+
smootherStep(1 - clamp(radius / focusRadius)),
|
|
132
|
+
1.6,
|
|
133
|
+
)
|
|
134
|
+
const zoom = magnification >= 0
|
|
135
|
+
? 1 + magnification * focus
|
|
136
|
+
: 1 / (1 + Math.abs(magnification) * focus)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
x: (x - cx) / zoom - (x - cx),
|
|
140
|
+
y: (y - cy) / zoom - (y - cy),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
94
144
|
function surfaceFn(x: number) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
const t = clamp(x)
|
|
146
|
+
|
|
147
|
+
switch (props.surface) {
|
|
148
|
+
case 'concave':
|
|
149
|
+
case 'bowl':
|
|
150
|
+
return 1 - convex(t)
|
|
151
|
+
case 'lip':
|
|
152
|
+
return convex(t) * (1 - smootherStep(t)) + (1 - convex(t)) * smootherStep(t)
|
|
153
|
+
case 'bevel':
|
|
154
|
+
return t
|
|
155
|
+
case 'saddle':
|
|
156
|
+
return 0.5 + Math.sin((t - 0.5) * Math.PI) * 0.5
|
|
157
|
+
case 'ripple':
|
|
158
|
+
return wave(t, 3, 0.12)
|
|
159
|
+
case 'noise':
|
|
160
|
+
return deterministicNoise(t)
|
|
161
|
+
case 'asymmetric':
|
|
162
|
+
return clamp(Math.pow(t, 0.65) * 0.65 + smootherStep(t) * 0.35)
|
|
163
|
+
case 'convex':
|
|
164
|
+
return convex(t)
|
|
99
165
|
}
|
|
100
|
-
return convex(x)
|
|
101
166
|
}
|
|
102
167
|
|
|
103
168
|
function derivative(x: number) {
|
|
@@ -118,54 +183,6 @@ function refractSlope(slope: number) {
|
|
|
118
183
|
return Math.tan(theta1 - theta2)
|
|
119
184
|
}
|
|
120
185
|
|
|
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
186
|
function canvasToUrl(canvas: HTMLCanvasElement) {
|
|
170
187
|
return canvas.toDataURL('image/png')
|
|
171
188
|
}
|
|
@@ -195,30 +212,42 @@ function buildMaps() {
|
|
|
195
212
|
const vectors: Array<[number, number]> = []
|
|
196
213
|
let max = 1
|
|
197
214
|
|
|
198
|
-
const light =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
const light = props.glareAngle !== false
|
|
216
|
+
? {
|
|
217
|
+
x: Math.cos((props.glareAngle * Math.PI) / 180),
|
|
218
|
+
y: Math.sin((props.glareAngle * Math.PI) / 180),
|
|
219
|
+
}
|
|
220
|
+
: { x: 0, y: 0 }
|
|
221
|
+
const edgeBand = Math.max(
|
|
222
|
+
1,
|
|
223
|
+
Math.min(props.bezel, Math.min(w, h) * 0.28),
|
|
224
|
+
)
|
|
202
225
|
|
|
203
226
|
for (let y = 0; y < h; y++) {
|
|
204
227
|
for (let x = 0; x < w; x++) {
|
|
205
228
|
const dist = distanceToRoundedRectEdge(x, y, w, h, resolvedRadius.value)
|
|
206
|
-
const t = clamp(dist /
|
|
229
|
+
const t = clamp(dist / edgeBand)
|
|
207
230
|
|
|
208
231
|
let vx = 0
|
|
209
232
|
let vy = 0
|
|
210
233
|
let spec = 0
|
|
211
234
|
|
|
212
|
-
if (dist >= 0 && dist <
|
|
213
|
-
const n = normalFromRoundedRect(x, y, w, h)
|
|
235
|
+
if (dist >= 0 && dist < edgeBand) {
|
|
236
|
+
const n = normalFromRoundedRect(x, y, w, h, resolvedRadius.value)
|
|
214
237
|
const slope = derivative(t)
|
|
215
238
|
const bend = refractSlope(slope)
|
|
216
239
|
|
|
217
240
|
const amount =
|
|
218
|
-
bend *
|
|
241
|
+
bend *
|
|
242
|
+
props.thickness *
|
|
243
|
+
smootherStep(1 - t) *
|
|
244
|
+
refractionModeStrength(props.refractionMode, t) *
|
|
245
|
+
props.refraction
|
|
246
|
+
|
|
247
|
+
const direction = refractionDirection(props.refractionMode, n, x, y, w, h, t)
|
|
219
248
|
|
|
220
|
-
vx =
|
|
221
|
-
vy =
|
|
249
|
+
vx = direction.x * amount
|
|
250
|
+
vy = direction.y * amount
|
|
222
251
|
|
|
223
252
|
max = Math.max(max, Math.abs(vx), Math.abs(vy))
|
|
224
253
|
|
|
@@ -226,6 +255,19 @@ function buildMaps() {
|
|
|
226
255
|
spec = Math.pow(facing, 18) * props.specularOpacity
|
|
227
256
|
}
|
|
228
257
|
|
|
258
|
+
if (props.magnification !== 0 && dist >= 0) {
|
|
259
|
+
const lens = magnificationVector(
|
|
260
|
+
x,
|
|
261
|
+
y,
|
|
262
|
+
w,
|
|
263
|
+
h,
|
|
264
|
+
props.magnification,
|
|
265
|
+
props.magnificationFocus,
|
|
266
|
+
)
|
|
267
|
+
vx += lens.x
|
|
268
|
+
vy += lens.y
|
|
269
|
+
}
|
|
270
|
+
|
|
229
271
|
vectors.push([vx, vy])
|
|
230
272
|
|
|
231
273
|
const hi = (y * w + x) * 4
|
|
@@ -238,6 +280,10 @@ function buildMaps() {
|
|
|
238
280
|
}
|
|
239
281
|
}
|
|
240
282
|
|
|
283
|
+
max = vectors.reduce((currentMax, [vx, vy]) => {
|
|
284
|
+
return Math.max(currentMax, Math.abs(vx), Math.abs(vy))
|
|
285
|
+
}, 1)
|
|
286
|
+
|
|
241
287
|
vectors.forEach(([vx, vy], index) => {
|
|
242
288
|
const i = index * 4
|
|
243
289
|
|
|
@@ -375,6 +421,9 @@ watch(
|
|
|
375
421
|
props.bezel,
|
|
376
422
|
props.thickness,
|
|
377
423
|
props.refraction,
|
|
424
|
+
props.refractionMode,
|
|
425
|
+
props.magnification,
|
|
426
|
+
props.magnificationFocus,
|
|
378
427
|
props.surface,
|
|
379
428
|
props.specularOpacity,
|
|
380
429
|
props.glareAngle,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { deepStrictEqual, ok } from 'node:assert'
|
|
2
|
+
import { refractionDirection, refractionModeStrength } from './refraction'
|
|
3
|
+
|
|
4
|
+
const normal = { x: 1, y: 0 }
|
|
5
|
+
|
|
6
|
+
deepStrictEqual(
|
|
7
|
+
refractionDirection('edge', normal, 20, 50, 100, 100, 0.25),
|
|
8
|
+
{ x: -1, y: -0 },
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
deepStrictEqual(
|
|
12
|
+
refractionDirection('center', normal, 20, 50, 100, 100, 0.25),
|
|
13
|
+
{ x: 1, y: 0 },
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
deepStrictEqual(
|
|
17
|
+
refractionDirection('center', { x: 0, y: 1 }, 50, 20, 100, 100, 0.25),
|
|
18
|
+
{ x: 0, y: 1 },
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
deepStrictEqual(
|
|
22
|
+
refractionDirection('center', { x: 0, y: 1 }, 20, 20, 100, 100, 0.25),
|
|
23
|
+
{ x: 0, y: 1 },
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const outerSplit = refractionDirection('split', normal, 20, 50, 100, 100, 0.25)
|
|
27
|
+
const innerSplit = refractionDirection('split', normal, 20, 50, 100, 100, 0.75)
|
|
28
|
+
|
|
29
|
+
ok(outerSplit.x * innerSplit.x < 0)
|
|
30
|
+
deepStrictEqual(outerSplit, { x: 1, y: 0 })
|
|
31
|
+
deepStrictEqual(innerSplit, { x: -1, y: -0 })
|
|
32
|
+
|
|
33
|
+
deepStrictEqual(refractionModeStrength('center', 0.5), 1)
|
|
34
|
+
deepStrictEqual(refractionModeStrength('split', 0.5), 0)
|
|
35
|
+
ok(refractionModeStrength('split', 0.1) > refractionModeStrength('split', 0.4))
|
|
36
|
+
ok(refractionModeStrength('split', 0.9) > refractionModeStrength('split', 0.6))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RefractionMode } from './types'
|
|
2
|
+
|
|
3
|
+
export const refractionModeOptions = [
|
|
4
|
+
'edge',
|
|
5
|
+
'center',
|
|
6
|
+
'split',
|
|
7
|
+
] as const satisfies readonly RefractionMode[]
|
|
8
|
+
|
|
9
|
+
interface Vector {
|
|
10
|
+
x: number
|
|
11
|
+
y: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function refractionDirection(
|
|
15
|
+
mode: RefractionMode,
|
|
16
|
+
normal: Vector,
|
|
17
|
+
_x: number,
|
|
18
|
+
_y: number,
|
|
19
|
+
_width: number,
|
|
20
|
+
_height: number,
|
|
21
|
+
t: number,
|
|
22
|
+
): Vector {
|
|
23
|
+
if (mode === 'center') {
|
|
24
|
+
return normal
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const edge = {
|
|
28
|
+
x: -normal.x,
|
|
29
|
+
y: -normal.y,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (mode === 'split') {
|
|
33
|
+
const side = t < 0.5 ? -1 : 1
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
x: edge.x * side,
|
|
37
|
+
y: edge.y * side,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return edge
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function refractionModeStrength(mode: RefractionMode, t: number): number {
|
|
45
|
+
if (mode !== 'split') {
|
|
46
|
+
return 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Math.min(1, Math.abs(t - 0.5) * 2)
|
|
50
|
+
}
|
|
@@ -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
package/src/types.ts
CHANGED