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 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 index, glare angle
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
- | bezel | number | 22 | Width of the displacement bezel in pixels |
60
- | thickness | number | 42 | Maximum displacement magnitude in pixels |
61
- | refraction | number | 1 | Refraction index multiplier |
62
- | blur | number | 0.4 | CSS backdrop-filter blur amount |
63
- | surface | string | `'convex'` | Glass surface profile shape |
64
- | specularOpacity | number | 0.45 | Opacity of the specular highlight |
65
- | glareAngle | number | -60 | Light source angle in degrees for specular highlight |
66
- | glassBackground | string | | Glass panel background color (auto-derived from `asProps.color` when available) |
67
- | as | string \| Component | `'div'` | HTML tag or Vue component to render as |
68
- | asProps | object | `{}` | Props passed to the rendered element/component |
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 (`convex`, `concave`, `lip`) define the slope at each point along the bezel, which determines the displacement magnitude. A separate specular highlight map is generated and composited via `feBlend` to simulate surface lighting.
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-2c1a4b58]{isolation:isolate;position:relative;overflow:hidden}.liquid-glass__svg[data-v-2c1a4b58]{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-2c1a4b58]{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}}
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 R, createBlock as le, createCommentVNode as ue, createElementBlock as ce, createElementVNode as w, defineComponent as ie, inject as de, mergeProps as fe, nextTick as K, onBeforeUnmount as he, onMounted as pe, openBlock as Q, provide as ve, ref as M, renderSlot as ge, resolveDynamicComponent as me, unref as be, watch as Me, withCtx as _e } from "vue";
2
- var ye = {
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
- }, xe = [
20
+ }, $e = [
7
21
  "href",
8
22
  "width",
9
23
  "height"
10
- ], ke = ["scale"], we = [
24
+ ], Ce = ["scale"], qe = [
11
25
  "href",
12
26
  "width",
13
27
  "height"
14
- ], Ie = /* @__PURE__ */ ie({
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(p) {
30
- const a = p, v = M(null), _ = M(1), y = M(1), F = M(""), U = M(""), z = M(1), x = CSS.supports?.("backdrop-filter", "url(#x)") && /Chrome/.test(navigator.userAgent), O = `${`lg-${Math.random().toString(36).slice(2)}`}-filter`, T = de("_liqvued", !1);
31
- ve("_liqvued", !0);
32
- const k = M(!1), A = {
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 W() {
43
- return v.value ? v.value instanceof Element ? v.value : v.value.$el ?? null : null;
63
+ function j() {
64
+ return s.value;
44
65
  }
45
- function f(e, t = 0, n = 1) {
66
+ function i(e, t = 0, n = 1) {
46
67
  return Math.max(t, Math.min(n, e));
47
68
  }
48
- function X(e) {
49
- return e = f(e), e * e * e * (e * (e * 6 - 15) + 10);
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 Y(e) {
63
- return (G(e + 1e-3) - G(e - 1e-3)) / (2 * 1e-3);
72
+ function P(e) {
73
+ return Math.pow(1 - Math.pow(1 - i(e), 4), 1 / 4);
64
74
  }
65
- function Z(e) {
66
- const s = Math.atan(e), r = 1 / 1.5 * Math.sin(s), g = Math.asin(f(r, -1, 1));
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 ee(e, t, n, s, r) {
70
- const g = f(e, r, n - r), h = f(t, r, s - r), o = e - g, l = t - h, c = Math.hypot(o, l), m = Math.min(e, t, n - e, s - t);
71
- return o !== 0 && l !== 0 ? r - c : m;
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 te(e, t, n, s, r) {
74
- const g = f(e, r, n - r), h = f(t, r, s - r);
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: o / c,
83
- y: l / c
85
+ x: (e - v) / p - (e - v),
86
+ y: (t - g) / p - (t - g)
84
87
  };
85
88
  }
86
- function N(e) {
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 q() {
90
- const e = Math.max(1, Math.round(_.value)), t = Math.max(1, Math.round(y.value)), n = document.createElement("canvas");
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 s = document.createElement("canvas");
93
- s.width = e, s.height = t;
94
- const r = n.getContext("2d"), g = s.getContext("2d"), h = r.createImageData(e, t), o = g.createImageData(e, t), l = [];
95
- let c = 1;
96
- const m = {
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 i = 0; i < t; i++) for (let d = 0; d < e; d++) {
101
- const b = ee(d, i, e, t, S.value), u = f(b / a.bezel);
102
- let L = 0, P = 0, H = 0;
103
- if (b >= 0 && b < a.bezel) {
104
- const C = te(d, i, e, t, S.value), J = Z(Y(u)) * a.thickness * (1 - u) * a.refraction;
105
- L = -C.x * J, P = -C.y * J, c = Math.max(c, Math.abs(L), Math.abs(P));
106
- const se = f(C.x * m.x + C.y * m.y, 0, 1);
107
- H = Math.pow(se, 18) * a.specularOpacity;
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
- l.push([L, P]);
110
- const B = (i * e + d) * 4, oe = Math.round(H * 255);
111
- o.data[B] = 255, o.data[B + 1] = 255, o.data[B + 2] = 255, o.data[B + 3] = oe;
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
- l.forEach(([i, d], b) => {
114
- const u = b * 4;
115
- h.data[u] = Math.round(128 + i / c * 127), h.data[u + 1] = Math.round(128 + d / c * 127), h.data[u + 2] = 128, h.data[u + 3] = 255;
116
- }), r.putImageData(h, 0, 0), g.putImageData(o, 0, 0), F.value = N(n), U.value = N(s), z.value = c;
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 V() {
119
- const e = W();
120
- e && (_.value = Math.max(1, e.offsetWidth), y.value = Math.max(1, e.offsetHeight));
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 j = x ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", S = R(() => {
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 A ? A[e.rounded] : a.radius;
126
- }), ae = /* @__PURE__ */ new Set([
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 ne(e, t) {
138
- if (ae.has(e)) return `rgba(var(--v-theme-${e}), ${t})`;
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})`) : j;
180
+ return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) : Q;
144
181
  }
145
- const D = R(() => {
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" ? ne(e.color, 0.6) : j;
149
- }), $ = R(() => x ? `url(#${O}) blur(${a.blur}px)` : "blur(12px)"), re = R(() => ({
150
- borderRadius: `${S.value}px`,
151
- ...D.value !== void 0 ? { backgroundColor: D.value } : {},
152
- ...D.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(", ") } : {},
153
- ...k.value && !T ? {
154
- backdropFilter: $.value,
155
- WebkitBackdropFilter: $.value
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
- ...k.value && T && !x ? {
158
- backdropFilter: $.value,
159
- WebkitBackdropFilter: $.value
194
+ ...I.value && N && !c ? {
195
+ backdropFilter: O.value,
196
+ WebkitBackdropFilter: O.value
160
197
  } : {}
161
198
  }));
162
- let E = null;
163
- return pe(async () => {
164
- if (await K(), x) {
165
- V(), q(), await K(), k.value = !0, E = new ResizeObserver(() => {
166
- V(), q(), k.value = !0;
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 = W();
169
- e && E.observe(e);
170
- } else k.value = !0;
171
- }), Me(() => [
172
- S.value,
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
- x && q(), k.value = !0;
181
- }), he(() => {
182
- E?.disconnect();
183
- }), (e, t) => (Q(), le(me(p.as), fe({
220
+ c && T(), I.value = !0;
221
+ }), be(() => {
222
+ G?.disconnect();
223
+ }), (e, t) => (W(), Z("div", {
184
224
  ref_key: "root",
185
- ref: v,
186
- class: "liquid-glass"
187
- }, p.asProps, { style: re.value }), {
188
- default: _e(() => [be(x) ? (Q(), ce("svg", ye, [w("defs", null, [w("filter", {
189
- id: O,
190
- "color-interpolation-filters": "sRGB",
191
- filterUnits: "userSpaceOnUse",
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: "10000",
195
- height: "10000"
196
- }, [
197
- w("feImage", {
198
- href: F.value,
199
- x: "0",
200
- y: "0",
201
- width: _.value,
202
- height: y.value,
203
- result: "displacement_map"
204
- }, null, 8, xe),
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, ["style"]));
274
+ }, 16))], 4));
229
275
  }
230
- }), Se = (p, a) => {
231
- const v = p.__vccOpts || p;
232
- for (const [_, y] of a) v[_] = y;
233
- return v;
234
- }, $e = /* @__PURE__ */ Se(Ie, [["__scopeId", "data-v-2c1a4b58"]]), Ce = { install(p) {
235
- p.component("Liqvued", $e);
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
- $e as Liqvued,
239
- Ce as default
284
+ Oe as Liqvued,
285
+ De as default
240
286
  };
@@ -1 +1 @@
1
- (function(g,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(g=typeof globalThis<"u"?globalThis:g||self,t(g.Liqvued={},g.Vue))})(this,function(g,t){Object.defineProperties(g,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var K={key:0,class:"liquid-glass__svg","aria-hidden":"true"},Q=["href","width","height"],X=["scale"],Y=["href","width","height"],Z=(0,t.defineComponent)({__name:"Liqvued",props:{as:{default:"div"},asProps:{default:()=>({})},radius:{default:32},bezel:{default:22},thickness:{default:42},refraction:{default:1},blur:{default:.4},surface:{default:"convex"},specularOpacity:{default:.45},glareAngle:{default:-60},glassBackground:{default:void 0}},setup(m){const n=m,v=(0,t.ref)(null),y=(0,t.ref)(1),k=(0,t.ref)(1),P=(0,t.ref)(""),V=(0,t.ref)(""),N=(0,t.ref)(1),x=CSS.supports?.("backdrop-filter","url(#x)")&&/Chrome/.test(navigator.userAgent),F=`${`lg-${Math.random().toString(36).slice(2)}`}-filter`,O=(0,t.inject)("_liqvued",!1);(0,t.provide)("_liqvued",!0);const w=(0,t.ref)(!1),U={0:0,sm:4,md:8,lg:12,xl:16,pill:9999,circle:9999,shaped:16};function z(){return v.value?v.value instanceof Element?v.value:v.value.$el??null:null}function h(e,a=0,r=1){return Math.max(a,Math.min(r,e))}function ae(e){return e=h(e),e*e*e*(e*(e*6-15)+10)}function S(e){return Math.pow(1-Math.pow(1-h(e),4),1/4)}function A(e){if(n.surface==="concave")return 1-S(e);if(n.surface==="lip"){const a=ae(e);return S(e)*(1-a)+(1-S(e))*a}return S(e)}function ne(e){return(A(e+.001)-A(e-.001))/(2*.001)}function re(e){const s=Math.atan(e),o=1/1.5*Math.sin(s),b=Math.asin(h(o,-1,1));return Math.tan(s-b)}function oe(e,a,r,s,o){const b=h(e,o,r-o),p=h(a,o,s-o),l=e-b,c=a-p,u=Math.hypot(l,c),M=Math.min(e,a,r-e,s-a);return l!==0&&c!==0?o-u:M}function le(e,a,r,s,o){const b=h(e,o,r-o),p=h(a,o,s-o);let l=e-b,c=a-p;if(l===0&&c===0){const M=e,d=r-e,f=a,_=s-a,i=Math.min(M,d,f,_);i===M?l=-1:i===d?l=1:i===f?c=-1:c=1}const u=Math.hypot(l,c)||1;return{x:l/u,y:c/u}}function W(e){return e.toDataURL("image/png")}function E(){const e=Math.max(1,Math.round(y.value)),a=Math.max(1,Math.round(k.value)),r=document.createElement("canvas");r.width=e,r.height=a;const s=document.createElement("canvas");s.width=e,s.height=a;const o=r.getContext("2d"),b=s.getContext("2d"),p=o.createImageData(e,a),l=b.createImageData(e,a),c=[];let u=1;const M={x:Math.cos(n.glareAngle*Math.PI/180),y:Math.sin(n.glareAngle*Math.PI/180)};for(let d=0;d<a;d++)for(let f=0;f<e;f++){const _=oe(f,d,e,a,I.value),i=h(_/n.bezel);let T=0,D=0,H=0;if(_>=0&&_<n.bezel){const q=le(f,d,e,a,I.value),J=re(ne(i))*n.thickness*(1-i)*n.refraction;T=-q.x*J,D=-q.y*J,u=Math.max(u,Math.abs(T),Math.abs(D));const de=h(q.x*M.x+q.y*M.y,0,1);H=Math.pow(de,18)*n.specularOpacity}c.push([T,D]);const $=(d*e+f)*4,ue=Math.round(H*255);l.data[$]=255,l.data[$+1]=255,l.data[$+2]=255,l.data[$+3]=ue}c.forEach(([d,f],_)=>{const i=_*4;p.data[i]=Math.round(128+d/u*127),p.data[i+1]=Math.round(128+f/u*127),p.data[i+2]=128,p.data[i+3]=255}),o.putImageData(p,0,0),b.putImageData(l,0,0),P.value=W(r),V.value=W(s),N.value=u}function j(){const e=z();e&&(y.value=Math.max(1,e.offsetWidth),k.value=Math.max(1,e.offsetHeight))}const G=x?"rgba(255, 255, 255, 0.08)":"rgba(255, 255, 255, 0.22)",I=(0,t.computed)(()=>{if(n.radius!==32)return n.radius;const e=n.asProps;return typeof e.rounded=="string"&&e.rounded in U?U[e.rounded]:n.radius}),se=new Set(["primary","secondary","accent","info","warning","error","success","surface","background"]);function ce(e,a){if(se.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})`):G}const C=(0,t.computed)(()=>{if(n.glassBackground!==void 0)return n.glassBackground;const e=n.asProps;return typeof e.color=="string"?ce(e.color,.6):G}),B=(0,t.computed)(()=>x?`url(#${F}) blur(${n.blur}px)`:"blur(12px)"),ie=(0,t.computed)(()=>({borderRadius:`${I.value}px`,...C.value!==void 0?{backgroundColor:C.value}:{},...C.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&&!O?{backdropFilter:B.value,WebkitBackdropFilter:B.value}:{},...w.value&&O&&!x?{backdropFilter:B.value,WebkitBackdropFilter:B.value}:{}}));let R=null;return(0,t.onMounted)(async()=>{if(await(0,t.nextTick)(),x){j(),E(),await(0,t.nextTick)(),w.value=!0,R=new ResizeObserver(()=>{j(),E(),w.value=!0});const e=z();e&&R.observe(e)}else w.value=!0}),(0,t.watch)(()=>[I.value,n.bezel,n.thickness,n.refraction,n.surface,n.specularOpacity,n.glareAngle],()=>{x&&E(),w.value=!0}),(0,t.onBeforeUnmount)(()=>{R?.disconnect()}),(e,a)=>((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(m.as),(0,t.mergeProps)({ref_key:"root",ref:v,class:"liquid-glass"},m.asProps,{style:ie.value}),{default:(0,t.withCtx)(()=>[(0,t.unref)(x)?((0,t.openBlock)(),(0,t.createElementBlock)("svg",K,[(0,t.createElementVNode)("defs",null,[(0,t.createElementVNode)("filter",{id:F,"color-interpolation-filters":"sRGB",filterUnits:"userSpaceOnUse",x:"0",y:"0",width:"10000",height:"10000"},[(0,t.createElementVNode)("feImage",{href:P.value,x:"0",y:"0",width:y.value,height:k.value,result:"displacement_map"},null,8,Q),(0,t.createElementVNode)("feDisplacementMap",{in:"SourceGraphic",in2:"displacement_map",scale:N.value,xChannelSelector:"R",yChannelSelector:"G",result:"refracted"},null,8,X),(0,t.createElementVNode)("feImage",{href:V.value,x:"0",y:"0",width:y.value,height:k.value,result:"specular"},null,8,Y),a[0]||(a[0]=(0,t.createElementVNode)("feBlend",{in:"refracted",in2:"specular",mode:"screen"},null,-1))])])])):(0,t.createCommentVNode)("",!0),(0,t.renderSlot)(e.$slots,"default",{},void 0,!0)]),_:3},16,["style"]))}}),ee=(m,n)=>{const v=m.__vccOpts||m;for(const[y,k]of n)v[y]=k;return v},L=ee(Z,[["__scopeId","data-v-2c1a4b58"]]),te={install(m){m.component("Liqvued",L)}};g.Liqvued=L,g.default=te});
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "liqvued",
3
3
  "types": "./index.d.ts",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "description": "Liquid-glass for Vue",
6
6
  "exports": {
7
7
  ".": {
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 | ComponentPublicInstance | null>(null)
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
- if (!root.value) return null
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
- if (props.surface === 'concave') return 1 - convex(x)
91
- if (props.surface === 'lip') {
92
- const t = smootherStep(x)
93
- return convex(x) * (1 - t) + (1 - convex(x)) * t
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 / props.bezel)
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 < props.bezel) {
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 * props.thickness * (1 - t) * props.refraction
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 `url(#${filterId}) blur(${props.blur}px)`
352
+ if (props.fallbackOnly || !supportsLiquidGlass) {
353
+ return 'blur(12px)'
313
354
  }
314
- return 'blur(12px)'
355
+ return `url(#${filterId}) blur(${props.blur}px)`
315
356
  })
316
357
 
317
358
  const rootStyle = computed(() => ({
318
- borderRadius: `${resolvedRadius.value}px`,
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
- <component
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="displacement_map"
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
- <slot />
448
- </component>
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
+ }
@@ -0,0 +1,13 @@
1
+ import type { Surface } from './types'
2
+
3
+ export const surfaceOptions = [
4
+ 'convex',
5
+ 'concave',
6
+ 'lip',
7
+ 'bowl',
8
+ 'bevel',
9
+ 'saddle',
10
+ 'ripple',
11
+ 'noise',
12
+ 'asymmetric',
13
+ ] as const satisfies readonly Surface[]
package/src/types.ts CHANGED
@@ -1 +1,10 @@
1
- export type Surface = 'convex' | 'concave' | 'lip'
1
+ export type Surface =
2
+ | 'convex'
3
+ | 'concave'
4
+ | 'lip'
5
+ | 'bowl'
6
+ | 'bevel'
7
+ | 'saddle'
8
+ | 'ripple'
9
+ | 'noise'
10
+ | 'asymmetric'