liqvued 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -60,6 +60,8 @@ import { Liqvued } from 'liqvued'
60
60
  | bezel | number | 22 | Width of the displacement bezel in pixels |
61
61
  | thickness | number | 42 | Maximum displacement magnitude in pixels |
62
62
  | refraction | number | 1 | Refraction index multiplier |
63
+ | magnification | number | 0 | Center magnification amount. Positive values enlarge, negative values shrink |
64
+ | magnificationFocus | number | 0.82 | Focus radius of the magnification area. Lower values concentrate the effect near center |
63
65
  | blur | number | 0.4 | CSS backdrop-filter blur amount |
64
66
  | surface | string | `'convex'` | Glass surface profile shape |
65
67
  | specularOpacity | number | 0.45 | Opacity of the specular highlight |
@@ -74,6 +76,12 @@ import { Liqvued } from 'liqvued'
74
76
  - `convex` — squircle convex profile — soft pill-like edges
75
77
  - `concave` — inverted convex — inward-curving dish-like refraction
76
78
  - `lip` — smooth transition from convex at center to concave at edge
79
+ - `bowl` — deeper concave profile with a more even inward pull
80
+ - `bevel` — linear edge ramp for harder, cleaner refraction
81
+ - `saddle` — mirrored S-curve that shifts direction through the middle
82
+ - `ripple` — small wave modulation across the edge band
83
+ - `noise` — irregular organic edge distortion
84
+ - `asymmetric` — biased profile with more weight toward one side of the edge band
77
85
 
78
86
  ## Dynamic Element Rendering
79
87
 
@@ -98,7 +106,7 @@ The fallback applies `backdrop-filter: blur(12px)` with a subtle border and shad
98
106
 
99
107
  ## How It Works
100
108
 
101
- The component generates a per-pixel displacement map as an RGBA PNG (red/green channels encode X/Y displacement vectors), injects it into an SVG `<feDisplacementMap>` filter, and applies it via `backdrop-filter` combined with CSS blur. The surface profile functions (`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.
102
110
 
103
111
  ## License
104
112
 
package/dist/liqvued.css CHANGED
@@ -1,2 +1,2 @@
1
- .liquid-glass[data-v-01d40041]{isolation:isolate;position:relative;overflow:hidden}.liquid-glass__svg[data-v-01d40041]{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-01d40041]{-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,17 +1,31 @@
1
- import { computed as R, createBlock as ie, createCommentVNode as ce, createElementBlock as Q, createElementVNode as v, defineComponent as de, guardReactiveProps as he, inject as fe, nextTick as X, normalizeProps as pe, normalizeStyle as ve, onBeforeUnmount as ge, onMounted as me, openBlock as L, provide as Me, ref as g, renderSlot as be, resolveDynamicComponent as ye, unref as _e, watch as xe, withCtx as ke } from "vue";
2
- var we = {
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
- }, Re = [
20
+ }, $e = [
7
21
  "href",
8
22
  "width",
9
23
  "height"
10
- ], Be = ["scale"], Ie = [
24
+ ], Ce = ["scale"], qe = [
11
25
  "href",
12
26
  "width",
13
27
  "height"
14
- ], Se = /* @__PURE__ */ de({
28
+ ], Pe = /* @__PURE__ */ pe({
15
29
  __name: "Liqvued",
16
30
  props: {
17
31
  as: { default: "div" },
@@ -21,6 +35,8 @@ var we = {
21
35
  bezel: { default: 22 },
22
36
  thickness: { default: 42 },
23
37
  refraction: { default: 1 },
38
+ magnification: { default: 0 },
39
+ magnificationFocus: { default: 0.82 },
24
40
  blur: { default: 0.4 },
25
41
  surface: { default: "convex" },
26
42
  specularOpacity: { default: 0.45 },
@@ -31,10 +47,10 @@ var we = {
31
47
  default: !1
32
48
  }
33
49
  },
34
- setup(d) {
35
- const a = d, x = g(null), m = g(1), M = g(1), E = g(""), F = g(""), U = g(1), b = CSS.supports?.("backdrop-filter", "url(#x)") && /Chrome/.test(navigator.userAgent), T = `${`lg-${Math.random().toString(36).slice(2)}`}-filter`, A = fe("_liqvued", !1);
36
- Me("_liqvued", !0);
37
- const y = g(!1), G = {
50
+ setup(r) {
51
+ const a = r, s = w(null), o = w(1), l = w(1), b = w(""), y = w(""), x = w(1), c = typeof CSS < "u" && typeof navigator < "u" && CSS.supports?.("backdrop-filter", "url(#x)") && /Chrome/.test(navigator.userAgent), S = `${`lg-${Math.random().toString(36).slice(2)}`}-filter`, N = ge("_liqvued", !1);
52
+ xe("_liqvued", !0);
53
+ const I = w(!1), V = {
38
54
  0: 0,
39
55
  sm: 4,
40
56
  md: 8,
@@ -44,92 +60,107 @@ var we = {
44
60
  circle: 9999,
45
61
  shaped: 16
46
62
  };
47
- function W() {
48
- return x.value;
63
+ function j() {
64
+ return s.value;
49
65
  }
50
- function k(e, t = 0, n = 1) {
66
+ function i(e, t = 0, n = 1) {
51
67
  return Math.max(t, Math.min(n, e));
52
68
  }
53
- function Y(e) {
54
- return e = k(e), e * e * e * (e * (e * 6 - 15) + 10);
55
- }
56
69
  function B(e) {
57
- return Math.pow(1 - Math.pow(1 - k(e), 4), 1 / 4);
58
- }
59
- function N(e) {
60
- if (a.surface === "concave") return 1 - B(e);
61
- if (a.surface === "lip") {
62
- const t = Y(e);
63
- return B(e) * (1 - t) + (1 - B(e)) * t;
64
- }
65
- return B(e);
70
+ return e = i(e), e * e * e * (e * (e * 6 - 15) + 10);
66
71
  }
67
- function Z(e) {
68
- return (N(e + 1e-3) - N(e - 1e-3)) / (2 * 1e-3);
72
+ function P(e) {
73
+ return Math.pow(1 - Math.pow(1 - i(e), 4), 1 / 4);
69
74
  }
70
- function ee(e) {
71
- const r = Math.atan(e), c = 1 / 1.5 * Math.sin(r), l = Math.asin(k(c, -1, 1));
72
- return Math.tan(r - l);
75
+ function te(e, t, n) {
76
+ return i(e + Math.sin(i(e) * Math.PI * t) * n);
73
77
  }
74
- function te(e, t, n, r, c) {
75
- const l = Math.min(c, Math.min(n, r) / 2), s = n / 2 - l, u = r / 2 - l, i = Math.abs(e - n / 2) - s, o = Math.abs(t - r / 2) - u;
76
- return -(Math.min(Math.max(i, o), 0) + Math.hypot(Math.max(i, 0), Math.max(o, 0)) - l);
78
+ function ae(e) {
79
+ const t = i(e);
80
+ return i(t + Math.sin(t * 39.1) * 0.06 + Math.sin(t * 91.7) * 0.035);
77
81
  }
78
- function ae(e, t, n, r) {
79
- const c = Math.min(e, n - 1 - e), l = Math.min(t, r - 1 - t), s = 4, u = Math.pow(c, s) + Math.pow(l, s);
80
- let i, o;
81
- u > 1e-10 ? (i = (e > n / 2 ? 1 : -1) * (Math.pow(l, s) / u), o = (t > r / 2 ? 1 : -1) * (Math.pow(c, s) / u)) : (i = e > n / 2 ? 1 : -1, o = t > r / 2 ? 1 : -1);
82
- const _ = Math.hypot(i, o);
83
- return _ < 1e-10 ? {
84
- x: 0,
85
- y: -1
86
- } : {
87
- x: i / _,
88
- y: o / _
82
+ function ne(e, t, n, f, m, R) {
83
+ const v = n / 2, g = f / 2, $ = Math.max(1, n / 2), M = Math.max(1, f / 2), z = (e - v) / $, D = (t - g) / M, u = Math.hypot(z, D), d = i(R, 0.2, 1.4), h = Math.pow(B(1 - i(u / d)), 1.6), p = m >= 0 ? 1 + m * h : 1 / (1 + Math.abs(m) * h);
84
+ return {
85
+ x: (e - v) / p - (e - v),
86
+ y: (t - g) / p - (t - g)
89
87
  };
90
88
  }
91
- function V(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) {
92
119
  return e.toDataURL("image/png");
93
120
  }
94
- function C() {
121
+ function T() {
95
122
  if (a.fallbackOnly) return;
96
- const e = Math.max(1, Math.round(m.value)), t = Math.max(1, Math.round(M.value)), n = document.createElement("canvas");
123
+ const e = Math.max(1, Math.round(o.value)), t = Math.max(1, Math.round(l.value)), n = document.createElement("canvas");
97
124
  n.width = e, n.height = t;
98
- const r = document.createElement("canvas");
99
- r.width = e, r.height = t;
100
- const c = n.getContext("2d"), l = r.getContext("2d"), s = c.createImageData(e, t), u = l.createImageData(e, t), i = [];
101
- let o = 1;
102
- const _ = {
125
+ const f = document.createElement("canvas");
126
+ f.width = e, f.height = t;
127
+ const m = n.getContext("2d"), R = f.getContext("2d"), v = m.createImageData(e, t), g = R.createImageData(e, t), $ = [];
128
+ let M = 1;
129
+ const z = {
103
130
  x: Math.cos(a.glareAngle * Math.PI / 180),
104
131
  y: Math.sin(a.glareAngle * Math.PI / 180)
105
- };
106
- for (let h = 0; h < t; h++) for (let f = 0; f < e; f++) {
107
- const w = te(f, h, e, t, q.value), p = k(w / a.bezel);
108
- let z = 0, D = 0, J = 0;
109
- if (w >= 0 && w < a.bezel) {
110
- const $ = ae(f, h, e, t), K = ee(Z(p)) * a.thickness * (1 - p) * a.refraction;
111
- z = -$.x * K, D = -$.y * K, o = Math.max(o, Math.abs(z), Math.abs(D));
112
- const ue = k($.x * _.x + $.y * _.y, 0, 1);
113
- J = Math.pow(ue, 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;
114
141
  }
115
- i.push([z, D]);
116
- const S = (h * e + f) * 4, le = Math.round(J * 255);
117
- u.data[S] = 255, u.data[S + 1] = 255, u.data[S + 2] = 255, u.data[S + 3] = le;
142
+ if (a.magnification !== 0 && h >= 0) {
143
+ const _ = ne(d, u, e, t, a.magnification, a.magnificationFocus);
144
+ L += _.x, E += _.y;
145
+ }
146
+ $.push([L, E]);
147
+ const U = (u * e + d) * 4, ue = Math.round(X * 255);
148
+ g.data[U] = 255, g.data[U + 1] = 255, g.data[U + 2] = 255, g.data[U + 3] = ue;
118
149
  }
119
- i.forEach(([h, f], w) => {
120
- const p = w * 4;
121
- s.data[p] = Math.round(128 + h / o * 127), s.data[p + 1] = Math.round(128 + f / o * 127), s.data[p + 2] = 128, s.data[p + 3] = 255;
122
- }), c.putImageData(s, 0, 0), l.putImageData(u, 0, 0), E.value = V(n), F.value = V(r), U.value = o;
150
+ M = $.reduce((u, [d, h]) => Math.max(u, Math.abs(d), Math.abs(h)), 1), $.forEach(([u, d], h) => {
151
+ const p = h * 4;
152
+ v.data[p] = Math.round(128 + u / M * 127), v.data[p + 1] = Math.round(128 + d / M * 127), v.data[p + 2] = 128, v.data[p + 3] = 255;
153
+ }), m.putImageData(v, 0, 0), R.putImageData(g, 0, 0), b.value = J(n), y.value = J(f), x.value = M;
123
154
  }
124
- function j() {
125
- const e = W();
126
- e && (m.value = Math.max(1, e.offsetWidth), M.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));
127
158
  }
128
- const H = b ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", q = R(() => {
159
+ const Q = c ? "rgba(255, 255, 255, 0.08)" : "rgba(255, 255, 255, 0.22)", F = C(() => {
129
160
  if (a.radius !== 32) return a.radius;
130
161
  const e = a.asProps;
131
- return typeof e.rounded == "string" && e.rounded in G ? G[e.rounded] : a.radius;
132
- }), ne = R(() => a.borderRadius || `${q.value}px`), re = /* @__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([
133
164
  "primary",
134
165
  "secondary",
135
166
  "accent",
@@ -140,60 +171,62 @@ var we = {
140
171
  "surface",
141
172
  "background"
142
173
  ]);
143
- function oe(e, t) {
144
- if (re.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})`;
145
176
  if (e.startsWith("#")) {
146
177
  const n = e.replace("#", "");
147
178
  return `rgba(${parseInt(n.slice(0, 2), 16)}, ${parseInt(n.slice(2, 4), 16)}, ${parseInt(n.slice(4, 6), 16)}, ${t})`;
148
179
  }
149
- return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) : H;
180
+ return e.startsWith("rgb") ? e.replace("rgb(", "rgba(").replace(")", `, ${t})`) : Q;
150
181
  }
151
- const O = R(() => {
182
+ const A = C(() => {
152
183
  if (a.glassBackground !== void 0) return a.glassBackground;
153
184
  const e = a.asProps;
154
- return typeof e.color == "string" ? oe(e.color, 0.6) : H;
155
- }), I = R(() => a.fallbackOnly || !b ? "blur(12px)" : `url(#${T}) blur(${a.blur}px)`), se = R(() => ({
156
- borderRadius: `${ne.value}`,
157
- ...O.value !== void 0 ? { backgroundColor: O.value } : {},
158
- ...O.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(", ") } : {},
159
- ...y.value && !A ? {
160
- backdropFilter: I.value,
161
- WebkitBackdropFilter: I.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
162
193
  } : {},
163
- ...y.value && A && !b ? {
164
- backdropFilter: I.value,
165
- WebkitBackdropFilter: I.value
194
+ ...I.value && N && !c ? {
195
+ backdropFilter: O.value,
196
+ WebkitBackdropFilter: O.value
166
197
  } : {}
167
198
  }));
168
- let P = null;
169
- return me(async () => {
170
- if (await X(), b) {
171
- j(), C(), await X(), y.value = !0, P = new ResizeObserver(() => {
172
- j(), C(), y.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;
173
204
  });
174
- const e = W();
175
- e && P.observe(e);
176
- } else y.value = !0;
177
- }), xe(() => [
178
- q.value,
205
+ const e = j();
206
+ e && G.observe(e);
207
+ } else I.value = !0;
208
+ }), Ie(() => [
209
+ F.value,
179
210
  a.bezel,
180
211
  a.thickness,
181
212
  a.refraction,
213
+ a.magnification,
214
+ a.magnificationFocus,
182
215
  a.surface,
183
216
  a.specularOpacity,
184
217
  a.glareAngle,
185
218
  a.fallbackOnly
186
219
  ], () => {
187
- b && C(), y.value = !0;
188
- }), ge(() => {
189
- P?.disconnect();
190
- }), (e, t) => (L(), Q("div", {
220
+ c && T(), I.value = !0;
221
+ }), be(() => {
222
+ G?.disconnect();
223
+ }), (e, t) => (W(), Z("div", {
191
224
  ref_key: "root",
192
- ref: x,
225
+ ref: s,
193
226
  class: "liquid-glass",
194
- style: ve(se.value)
195
- }, [_e(b) ? (L(), Q("svg", we, [v("defs", null, [v("filter", {
196
- id: T,
227
+ style: Me(ie.value)
228
+ }, [we(c) ? (W(), Z("svg", Be, [k("defs", null, [k("filter", {
229
+ id: S,
197
230
  "color-interpolation-filters": "sRGB",
198
231
  filterUnits: "userSpaceOnUse",
199
232
  x: "0",
@@ -201,53 +234,53 @@ var we = {
201
234
  width: "10000",
202
235
  height: "10000"
203
236
  }, [
204
- v("feImage", {
205
- href: E.value,
237
+ k("feImage", {
238
+ href: b.value,
206
239
  x: "0",
207
240
  y: "0",
208
- width: m.value,
209
- height: M.value,
241
+ width: o.value,
242
+ height: l.value,
210
243
  result: "displacement_map"
211
- }, null, 8, Re),
212
- t[0] || (t[0] = v("feGaussianBlur", {
244
+ }, null, 8, $e),
245
+ t[0] || (t[0] = k("feGaussianBlur", {
213
246
  in: "displacement_map",
214
247
  stdDeviation: "2",
215
248
  result: "smooth_disp"
216
249
  }, null, -1)),
217
- v("feDisplacementMap", {
250
+ k("feDisplacementMap", {
218
251
  in: "SourceGraphic",
219
252
  in2: "smooth_disp",
220
- scale: U.value,
253
+ scale: x.value,
221
254
  xChannelSelector: "R",
222
255
  yChannelSelector: "G",
223
256
  result: "refracted"
224
- }, null, 8, Be),
225
- v("feImage", {
226
- href: F.value,
257
+ }, null, 8, Ce),
258
+ k("feImage", {
259
+ href: y.value,
227
260
  x: "0",
228
261
  y: "0",
229
- width: m.value,
230
- height: M.value,
262
+ width: o.value,
263
+ height: l.value,
231
264
  result: "specular"
232
- }, null, 8, Ie),
233
- t[1] || (t[1] = v("feBlend", {
265
+ }, null, 8, qe),
266
+ t[1] || (t[1] = k("feBlend", {
234
267
  in: "refracted",
235
268
  in2: "specular",
236
269
  mode: "screen"
237
270
  }, null, -1))
238
- ])])])) : ce("", !0), (L(), ie(ye(d.as), pe(he(d.asProps)), {
239
- default: ke(() => [be(e.$slots, "default", {}, void 0, !0)]),
271
+ ])])])) : fe("", !0), (W(), he(ke(r.as), me(ve(r.asProps)), {
272
+ default: Re(() => [_e(e.$slots, "default", {}, void 0, !0)]),
240
273
  _: 3
241
274
  }, 16))], 4));
242
275
  }
243
- }), $e = (d, a) => {
244
- const x = d.__vccOpts || d;
245
- for (const [m, M] of a) x[m] = M;
246
- return x;
247
- }, Ce = /* @__PURE__ */ $e(Se, [["__scopeId", "data-v-01d40041"]]), Oe = { install(d) {
248
- d.component("Liqvued", Ce);
276
+ }), Fe = (r, a) => {
277
+ const s = r.__vccOpts || r;
278
+ for (const [o, l] of a) s[o] = l;
279
+ return s;
280
+ }, Oe = /* @__PURE__ */ Fe(Pe, [["__scopeId", "data-v-56a7fa97"]]), De = { install(r) {
281
+ r.component("Liqvued", Oe);
249
282
  } };
250
283
  export {
251
- Ce as Liqvued,
252
- Oe as default
284
+ Oe as Liqvued,
285
+ De as default
253
286
  };
@@ -1 +1 @@
1
- (function(f,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(f=typeof globalThis<"u"?globalThis:f||self,t(f.Liqvued={},f.Vue))})(this,function(f,t){Object.defineProperties(f,{__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},borderRadius:{},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},fallbackOnly:{type:Boolean,default:!1}},setup(h){const n=h,k=(0,t.ref)(null),v=(0,t.ref)(1),M=(0,t.ref)(1),V=(0,t.ref)(""),L=(0,t.ref)(""),N=(0,t.ref)(1),b=CSS.supports?.("backdrop-filter","url(#x)")&&/Chrome/.test(navigator.userAgent),z=`${`lg-${Math.random().toString(36).slice(2)}`}-filter`,D=(0,t.inject)("_liqvued",!1);(0,t.provide)("_liqvued",!0);const y=(0,t.ref)(!1),F={0:0,sm:4,md:8,lg:12,xl:16,pill:9999,circle:9999,shaped:16};function U(){return k.value}function x(e,a=0,r=1){return Math.max(a,Math.min(r,e))}function ae(e){return e=x(e),e*e*e*(e*(e*6-15)+10)}function B(e){return Math.pow(1-Math.pow(1-x(e),4),1/4)}function A(e){if(n.surface==="concave")return 1-B(e);if(n.surface==="lip"){const a=ae(e);return B(e)*(1-a)+(1-B(e))*a}return B(e)}function ne(e){return(A(e+.001)-A(e-.001))/(2*.001)}function re(e){const o=Math.atan(e),d=1/1.5*Math.sin(o),i=Math.asin(x(d,-1,1));return Math.tan(o-i)}function oe(e,a,r,o,d){const i=Math.min(d,Math.min(r,o)/2),s=r/2-i,c=o/2-i,u=Math.abs(e-r/2)-s,l=Math.abs(a-o/2)-c;return-(Math.min(Math.max(u,l),0)+Math.hypot(Math.max(u,0),Math.max(l,0))-i)}function le(e,a,r,o){const d=Math.min(e,r-1-e),i=Math.min(a,o-1-a),s=4,c=Math.pow(d,s)+Math.pow(i,s);let u,l;c>1e-10?(u=(e>r/2?1:-1)*(Math.pow(i,s)/c),l=(a>o/2?1:-1)*(Math.pow(d,s)/c)):(u=e>r/2?1:-1,l=a>o/2?1:-1);const _=Math.hypot(u,l);return _<1e-10?{x:0,y:-1}:{x:u/_,y:l/_}}function G(e){return e.toDataURL("image/png")}function E(){if(n.fallbackOnly)return;const e=Math.max(1,Math.round(v.value)),a=Math.max(1,Math.round(M.value)),r=document.createElement("canvas");r.width=e,r.height=a;const o=document.createElement("canvas");o.width=e,o.height=a;const d=r.getContext("2d"),i=o.getContext("2d"),s=d.createImageData(e,a),c=i.createImageData(e,a),u=[];let l=1;const _={x:Math.cos(n.glareAngle*Math.PI/180),y:Math.sin(n.glareAngle*Math.PI/180)};for(let p=0;p<a;p++)for(let m=0;m<e;m++){const w=oe(m,p,e,a,$.value),g=x(w/n.bezel);let O=0,P=0,H=0;if(w>=0&&w<n.bezel){const I=le(m,p,e,a),J=re(ne(g))*n.thickness*(1-g)*n.refraction;O=-I.x*J,P=-I.y*J,l=Math.max(l,Math.abs(O),Math.abs(P));const fe=x(I.x*_.x+I.y*_.y,0,1);H=Math.pow(fe,18)*n.specularOpacity}u.push([O,P]);const R=(p*e+m)*4,de=Math.round(H*255);c.data[R]=255,c.data[R+1]=255,c.data[R+2]=255,c.data[R+3]=de}u.forEach(([p,m],w)=>{const g=w*4;s.data[g]=Math.round(128+p/l*127),s.data[g+1]=Math.round(128+m/l*127),s.data[g+2]=128,s.data[g+3]=255}),d.putImageData(s,0,0),i.putImageData(c,0,0),V.value=G(r),L.value=G(o),N.value=l}function W(){const e=U();e&&(v.value=Math.max(1,e.offsetWidth),M.value=Math.max(1,e.offsetHeight))}const j=b?"rgba(255, 255, 255, 0.08)":"rgba(255, 255, 255, 0.22)",$=(0,t.computed)(()=>{if(n.radius!==32)return n.radius;const e=n.asProps;return typeof e.rounded=="string"&&e.rounded in F?F[e.rounded]:n.radius}),se=(0,t.computed)(()=>n.borderRadius||`${$.value}px`),ie=new Set(["primary","secondary","accent","info","warning","error","success","surface","background"]);function ce(e,a){if(ie.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})`):j}const q=(0,t.computed)(()=>{if(n.glassBackground!==void 0)return n.glassBackground;const e=n.asProps;return typeof e.color=="string"?ce(e.color,.6):j}),S=(0,t.computed)(()=>n.fallbackOnly||!b?"blur(12px)":`url(#${z}) blur(${n.blur}px)`),ue=(0,t.computed)(()=>({borderRadius:`${se.value}`,...q.value!==void 0?{backgroundColor:q.value}:{},...q.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(", ")}:{},...y.value&&!D?{backdropFilter:S.value,WebkitBackdropFilter:S.value}:{},...y.value&&D&&!b?{backdropFilter:S.value,WebkitBackdropFilter:S.value}:{}}));let C=null;return(0,t.onMounted)(async()=>{if(await(0,t.nextTick)(),b){W(),E(),await(0,t.nextTick)(),y.value=!0,C=new ResizeObserver(()=>{W(),E(),y.value=!0});const e=U();e&&C.observe(e)}else y.value=!0}),(0,t.watch)(()=>[$.value,n.bezel,n.thickness,n.refraction,n.surface,n.specularOpacity,n.glareAngle,n.fallbackOnly],()=>{b&&E(),y.value=!0}),(0,t.onBeforeUnmount)(()=>{C?.disconnect()}),(e,a)=>((0,t.openBlock)(),(0,t.createElementBlock)("div",{ref_key:"root",ref:k,class:"liquid-glass",style:(0,t.normalizeStyle)(ue.value)},[(0,t.unref)(b)?((0,t.openBlock)(),(0,t.createElementBlock)("svg",K,[(0,t.createElementVNode)("defs",null,[(0,t.createElementVNode)("filter",{id:z,"color-interpolation-filters":"sRGB",filterUnits:"userSpaceOnUse",x:"0",y:"0",width:"10000",height:"10000"},[(0,t.createElementVNode)("feImage",{href:V.value,x:"0",y:"0",width:v.value,height:M.value,result:"displacement_map"},null,8,Q),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:N.value,xChannelSelector:"R",yChannelSelector:"G",result:"refracted"},null,8,X),(0,t.createElementVNode)("feImage",{href:L.value,x:"0",y:"0",width:v.value,height:M.value,result:"specular"},null,8,Y),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)(h.as),(0,t.normalizeProps)((0,t.guardReactiveProps)(h.asProps)),{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,"default",{},void 0,!0)]),_:3},16))],4))}}),ee=(h,n)=>{const k=h.__vccOpts||h;for(const[v,M]of n)k[v]=M;return k},T=ee(Z,[["__scopeId","data-v-01d40041"]]),te={install(h){h.component("Liqvued",T)}};f.Liqvued=T,f.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.2",
4
+ "version": "0.2.0",
5
5
  "description": "Liquid-glass for Vue",
6
6
  "exports": {
7
7
  ".": {
package/src/Liqvued.vue CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  ref,
11
11
  watch,
12
12
  } from 'vue'
13
+ import { distanceToRoundedRectEdge, normalFromRoundedRect } from './shape'
13
14
  import type { Surface } from './types'
14
15
 
15
16
  const props = withDefaults(defineProps<{
@@ -20,6 +21,8 @@ const props = withDefaults(defineProps<{
20
21
  bezel?: number
21
22
  thickness?: number
22
23
  refraction?: number
24
+ magnification?: number
25
+ magnificationFocus?: number
23
26
  blur?: number
24
27
  surface?: Surface
25
28
  specularOpacity?: number
@@ -34,6 +37,8 @@ const props = withDefaults(defineProps<{
34
37
  bezel: 22,
35
38
  thickness: 42,
36
39
  refraction: 1,
40
+ magnification: 0,
41
+ magnificationFocus: 0.82,
37
42
  blur: 0.4,
38
43
  surface: 'convex',
39
44
  specularOpacity: 0.45,
@@ -49,6 +54,8 @@ const mapUrl = ref('')
49
54
  const specularUrl = ref('')
50
55
  const scale = ref(1)
51
56
  const supportsLiquidGlass =
57
+ typeof CSS !== 'undefined' &&
58
+ typeof navigator !== 'undefined' &&
52
59
  CSS.supports?.('backdrop-filter', 'url(#x)') &&
53
60
  /Chrome/.test(navigator.userAgent)
54
61
 
@@ -91,13 +98,67 @@ function convex(x: number) {
91
98
  return Math.pow(1 - Math.pow(1 - clamp(x), 4), 1 / 4)
92
99
  }
93
100
 
101
+ function wave(x: number, frequency: number, amplitude: number) {
102
+ return clamp(x + Math.sin(clamp(x) * Math.PI * frequency) * amplitude)
103
+ }
104
+
105
+ function deterministicNoise(x: number) {
106
+ const t = clamp(x)
107
+ return clamp(t + Math.sin(t * 39.1) * 0.06 + Math.sin(t * 91.7) * 0.035)
108
+ }
109
+
110
+ function magnificationVector(
111
+ x: number,
112
+ y: number,
113
+ w: number,
114
+ h: number,
115
+ magnification: number,
116
+ magnificationFocus: number,
117
+ ) {
118
+ const cx = w / 2
119
+ const cy = h / 2
120
+ const rx = Math.max(1, w / 2)
121
+ const ry = Math.max(1, h / 2)
122
+ const dx = (x - cx) / rx
123
+ const dy = (y - cy) / ry
124
+ const radius = Math.hypot(dx, dy)
125
+ const focusRadius = clamp(magnificationFocus, 0.2, 1.4)
126
+ const focus = Math.pow(
127
+ smootherStep(1 - clamp(radius / focusRadius)),
128
+ 1.6,
129
+ )
130
+ const zoom = magnification >= 0
131
+ ? 1 + magnification * focus
132
+ : 1 / (1 + Math.abs(magnification) * focus)
133
+
134
+ return {
135
+ x: (x - cx) / zoom - (x - cx),
136
+ y: (y - cy) / zoom - (y - cy),
137
+ }
138
+ }
139
+
94
140
  function surfaceFn(x: number) {
95
- if (props.surface === 'concave') return 1 - convex(x)
96
- if (props.surface === 'lip') {
97
- const t = smootherStep(x)
98
- 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)
99
161
  }
100
- return convex(x)
101
162
  }
102
163
 
103
164
  function derivative(x: number) {
@@ -118,54 +179,6 @@ function refractSlope(slope: number) {
118
179
  return Math.tan(theta1 - theta2)
119
180
  }
120
181
 
121
- function distanceToRoundedRectEdge(
122
- x: number,
123
- y: number,
124
- w: number,
125
- h: number,
126
- r: number,
127
- ) {
128
- const hr = Math.min(r, Math.min(w, h) / 2)
129
- const hw = w / 2 - hr
130
- const hh = h / 2 - hr
131
- const ax = Math.abs(x - w / 2) - hw
132
- const ay = Math.abs(y - h / 2) - hh
133
-
134
- const outsidePart = Math.hypot(Math.max(ax, 0), Math.max(ay, 0))
135
- const insidePart = Math.min(Math.max(ax, ay), 0)
136
-
137
- return -(insidePart + outsidePart - hr)
138
- }
139
-
140
- function normalFromRoundedRect(
141
- x: number,
142
- y: number,
143
- w: number,
144
- h: number,
145
- ) {
146
- const dx = Math.min(x, w - 1 - x)
147
- const dy = Math.min(y, h - 1 - y)
148
-
149
- const p = 4
150
- const wsum = Math.pow(dx, p) + Math.pow(dy, p)
151
-
152
- let nx: number
153
- let ny: number
154
-
155
- if (wsum > 1e-10) {
156
- nx = (x > w / 2 ? 1 : -1) * (Math.pow(dy, p) / wsum)
157
- ny = (y > h / 2 ? 1 : -1) * (Math.pow(dx, p) / wsum)
158
- } else {
159
- nx = x > w / 2 ? 1 : -1
160
- ny = y > h / 2 ? 1 : -1
161
- }
162
-
163
- const len = Math.hypot(nx, ny)
164
- if (len < 1e-10) return { x: 0, y: -1 }
165
-
166
- return { x: nx / len, y: ny / len }
167
- }
168
-
169
182
  function canvasToUrl(canvas: HTMLCanvasElement) {
170
183
  return canvas.toDataURL('image/png')
171
184
  }
@@ -199,23 +212,30 @@ function buildMaps() {
199
212
  x: Math.cos((props.glareAngle * Math.PI) / 180),
200
213
  y: Math.sin((props.glareAngle * Math.PI) / 180),
201
214
  }
215
+ const edgeBand = Math.max(
216
+ 1,
217
+ Math.min(props.bezel, Math.min(w, h) * 0.28),
218
+ )
202
219
 
203
220
  for (let y = 0; y < h; y++) {
204
221
  for (let x = 0; x < w; x++) {
205
222
  const dist = distanceToRoundedRectEdge(x, y, w, h, resolvedRadius.value)
206
- const t = clamp(dist / 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) {
213
- const n = normalFromRoundedRect(x, y, w, h)
229
+ if (dist >= 0 && dist < edgeBand) {
230
+ const n = normalFromRoundedRect(x, y, w, h, resolvedRadius.value)
214
231
  const slope = derivative(t)
215
232
  const bend = refractSlope(slope)
216
233
 
217
234
  const amount =
218
- bend * 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
 
@@ -375,6 +412,8 @@ watch(
375
412
  props.bezel,
376
413
  props.thickness,
377
414
  props.refraction,
415
+ props.magnification,
416
+ props.magnificationFocus,
378
417
  props.surface,
379
418
  props.specularOpacity,
380
419
  props.glareAngle,
@@ -0,0 +1,54 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { normalFromRoundedRect } from './shape'
3
+
4
+ function closeTo(actual: number, expected: number, tolerance = 0.001) {
5
+ assert.ok(
6
+ Math.abs(actual - expected) <= tolerance,
7
+ `Expected ${actual} to be within ${tolerance} of ${expected}`,
8
+ )
9
+ }
10
+
11
+ function assertUnitNormal(point: [number, number]) {
12
+ const normal = normalFromRoundedRect(point[0], point[1], 200, 120, 40)
13
+ const length = Math.hypot(normal.x, normal.y)
14
+
15
+ closeTo(length, 1)
16
+ }
17
+
18
+ function dot(a: [number, number], b: [number, number]) {
19
+ return a[0] * b[0] + a[1] * b[1]
20
+ }
21
+
22
+ function normalAt(point: [number, number]): [number, number] {
23
+ const normal = normalFromRoundedRect(point[0], point[1], 200, 120, 40)
24
+ return [normal.x, normal.y]
25
+ }
26
+
27
+ assertUnitNormal([20, 30])
28
+ assertUnitNormal([180, 30])
29
+ assertUnitNormal([20, 90])
30
+ assertUnitNormal([180, 90])
31
+
32
+ const topLeft = normalAt([20, 30])
33
+ const topRight = normalAt([180, 30])
34
+ const bottomLeft = normalAt([20, 90])
35
+ const bottomRight = normalAt([180, 90])
36
+
37
+ closeTo(topLeft[0], -topRight[0])
38
+ closeTo(topLeft[1], topRight[1])
39
+ closeTo(topLeft[0], bottomLeft[0])
40
+ closeTo(topLeft[1], -bottomLeft[1])
41
+ closeTo(topLeft[0], -bottomRight[0])
42
+ closeTo(topLeft[1], -bottomRight[1])
43
+
44
+ assert.ok(
45
+ dot(normalAt([39, 10]), normalAt([40, 10])) > 0.99,
46
+ 'Expected top edge to top-right corner transition to be smooth',
47
+ )
48
+
49
+ assert.ok(
50
+ dot(normalAt([39, 10]), normalAt([41, 10])) > 0.98,
51
+ 'Expected rounded corner seam to remain visually continuous',
52
+ )
53
+
54
+ console.log('shape geometry tests passed')
package/src/shape.ts ADDED
@@ -0,0 +1,42 @@
1
+ export interface Point {
2
+ x: number
3
+ y: number
4
+ }
5
+
6
+ export function distanceToRoundedRectEdge(
7
+ x: number,
8
+ y: number,
9
+ w: number,
10
+ h: number,
11
+ r: number,
12
+ ) {
13
+ const hr = Math.min(r, Math.min(w, h) / 2)
14
+ const hw = w / 2 - hr
15
+ const hh = h / 2 - hr
16
+ const ax = Math.abs(x - w / 2) - hw
17
+ const ay = Math.abs(y - h / 2) - hh
18
+
19
+ const outsidePart = Math.hypot(Math.max(ax, 0), Math.max(ay, 0))
20
+ const insidePart = Math.min(Math.max(ax, ay), 0)
21
+
22
+ return -(insidePart + outsidePart - hr)
23
+ }
24
+
25
+ export function normalFromRoundedRect(
26
+ x: number,
27
+ y: number,
28
+ w: number,
29
+ h: number,
30
+ r: number,
31
+ ): Point {
32
+ const delta = 1.5
33
+ const nx = distanceToRoundedRectEdge(x - delta, y, w, h, r)
34
+ - distanceToRoundedRectEdge(x + delta, y, w, h, r)
35
+ const ny = distanceToRoundedRectEdge(x, y - delta, w, h, r)
36
+ - distanceToRoundedRectEdge(x, y + delta, w, h, r)
37
+
38
+ const len = Math.hypot(nx, ny)
39
+ if (len < 1e-10) return { x: 0, y: -1 }
40
+
41
+ return { x: nx / len, y: ny / len }
42
+ }
@@ -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'