@mks2508/mks-ui 0.5.2 → 0.5.7
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/dist/react-ui/index.js +8 -3
- package/dist/react-ui/primitives/index.js +5 -0
- package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts +120 -0
- package/dist/react-ui/primitives/waapi/Gooey/Gooey.types.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts +10 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyCanvas.js +190 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.d.ts +7 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/GooeyFilter.js +78 -0
- package/dist/react-ui/primitives/waapi/Gooey/MorphPath.d.ts +7 -0
- package/dist/react-ui/primitives/waapi/Gooey/MorphPath.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/MorphPath.js +51 -0
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts +94 -0
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/gooey-utils.js +182 -0
- package/dist/react-ui/primitives/waapi/Gooey/index.d.ts +28 -0
- package/dist/react-ui/primitives/waapi/Gooey/index.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/index.js +5 -0
- package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.d.ts +7 -0
- package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.d.ts.map +1 -0
- package/dist/react-ui/primitives/waapi/Gooey/useMorphPath.js +47 -0
- package/dist/react-ui/primitives/waapi/index.d.ts +2 -0
- package/dist/react-ui/primitives/waapi/index.d.ts.map +1 -1
- package/dist/react-ui/primitives/waapi/index.js +6 -0
- package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts +26 -16
- package/dist/react-ui/ui/DataCard/DataCard.styles.d.ts.map +1 -1
- package/dist/react-ui/ui/DataCard/DataCard.styles.js +36 -74
- package/dist/react-ui/ui/DataCard/DataCard.types.d.ts +50 -70
- package/dist/react-ui/ui/DataCard/DataCard.types.d.ts.map +1 -1
- package/dist/react-ui/ui/DataCard/index.d.ts +24 -93
- package/dist/react-ui/ui/DataCard/index.d.ts.map +1 -1
- package/dist/react-ui/ui/DataCard/index.js +76 -118
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle-DOR3Ld-k.css +376 -0
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.css +376 -0
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.js +0 -0
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.d.ts +20 -8
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.d.ts.map +1 -1
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.styles.js +55 -27
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts +69 -14
- package/dist/react-ui/ui/DynamicToggle/DynamicToggle.types.d.ts.map +1 -1
- package/dist/react-ui/ui/DynamicToggle/index.d.ts +22 -20
- package/dist/react-ui/ui/DynamicToggle/index.d.ts.map +1 -1
- package/dist/react-ui/ui/DynamicToggle/index.js +133 -96
- package/dist/react-ui/ui/Switch/index.js +1 -1
- package/dist/react-ui/ui/index.js +2 -2
- package/package.json +2 -2
- package/src/css.d.ts +1 -0
- package/src/react-ui/primitives/waapi/Gooey/Gooey.types.ts +141 -0
- package/src/react-ui/primitives/waapi/Gooey/GooeyCanvas.tsx +217 -0
- package/src/react-ui/primitives/waapi/Gooey/GooeyFilter.tsx +77 -0
- package/src/react-ui/primitives/waapi/Gooey/MorphPath.tsx +58 -0
- package/src/react-ui/primitives/waapi/Gooey/gooey-utils.ts +253 -0
- package/src/react-ui/primitives/waapi/Gooey/index.ts +50 -0
- package/src/react-ui/primitives/waapi/Gooey/useMorphPath.ts +48 -0
- package/src/react-ui/primitives/waapi/index.ts +23 -0
- package/src/react-ui/ui/DataCard/DataCard.styles.ts +45 -101
- package/src/react-ui/ui/DataCard/DataCard.types.ts +52 -73
- package/src/react-ui/ui/DataCard/index.tsx +118 -184
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.css +320 -94
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.styles.ts +60 -40
- package/src/react-ui/ui/DynamicToggle/DynamicToggle.types.ts +101 -14
- package/src/react-ui/ui/DynamicToggle/index.tsx +172 -96
- package/src/react-ui/ui/DynamicToggle/prototype-v7-ios.html +413 -0
- package/src/react-ui/ui/DynamicToggle/prototype-v7.html +615 -0
- package/src/react-ui/ui/DynamicToggle/prototype-v8-gooey-safari.html +560 -0
- package/src/react-ui/ui/DynamicToggle/prototype-v8b-react-structure.html +227 -0
- package/src/react-ui/ui/DynamicToggle/prototype.html +419 -0
- package/src/react-ui/ui/Switch/index.tsx +1 -1
- /package/dist/react-ui/blocks/Terminal/panel/{terminal-filter-dropdown.module-DAcl_XQZ.css → terminal-filter-dropdown.module-C6oDcFBS.css} +0 -0
- /package/dist/react-ui/blocks/Terminal/panel/{terminal-session-tabs.module-DNAop5e3.css → terminal-session-tabs.module-D_-sgyza.css} +0 -0
- /package/dist/react-ui/components/MorphingPopover/{morphing-popover.module-BJrjXisF.css → morphing-popover.module-B1ftlaYj.css} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GooeyCanvas — SVG-based gooey filter with animated pill + bubble rects.
|
|
5
|
+
*
|
|
6
|
+
* Uses SVG `<rect>` elements inside a filtered container. The bubble rect
|
|
7
|
+
* animates via WAAPI for smooth cross-browser rendering.
|
|
8
|
+
*
|
|
9
|
+
* Key patterns (derived from Sileo):
|
|
10
|
+
* - Filter applied to canvas div, SVG content inside (not HTML children)
|
|
11
|
+
* - `transform: translateZ(0)` + `contain: layout style` on canvas
|
|
12
|
+
* - No DOM mutation during/after animation — `fill: 'forwards'` only
|
|
13
|
+
* - Readiness gate: 1-frame rAF delay before first animation (Sileo pattern)
|
|
14
|
+
*
|
|
15
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from 'react';
|
|
19
|
+
import { cn } from '@/react-ui/lib/utils';
|
|
20
|
+
import { EASINGS, getResponsiveDuration } from '@/react-ui/primitives/waapi/core/animationConstants';
|
|
21
|
+
import { computeBlur, GOOEY_TIMING } from './gooey-utils';
|
|
22
|
+
import type { IGooeyCanvasProps } from './Gooey.types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GooeyCanvas — SVG rects + gooey filter, WAAPI-animated bubble.
|
|
26
|
+
*/
|
|
27
|
+
function GooeyCanvas({
|
|
28
|
+
blur,
|
|
29
|
+
height = 32,
|
|
30
|
+
width = 260,
|
|
31
|
+
radius = 9999,
|
|
32
|
+
fillColor = 'var(--card)',
|
|
33
|
+
bubbleHeight: bubbleHeightProp,
|
|
34
|
+
bubbleInset = 0.2,
|
|
35
|
+
expanded = false,
|
|
36
|
+
outlineBlur = 0.5,
|
|
37
|
+
outlineColor = 'var(--border)',
|
|
38
|
+
outlineLayers = 2,
|
|
39
|
+
alphaGain,
|
|
40
|
+
alphaOffset,
|
|
41
|
+
expandDuration,
|
|
42
|
+
collapseDuration,
|
|
43
|
+
expandEasing,
|
|
44
|
+
collapseEasing,
|
|
45
|
+
className,
|
|
46
|
+
}: IGooeyCanvasProps) {
|
|
47
|
+
const filterId = React.useId().replace(/:/g, '') + '-goo';
|
|
48
|
+
const computedBlur = blur ?? computeBlur(height);
|
|
49
|
+
const bubbleRectRef = React.useRef<SVGRectElement>(null);
|
|
50
|
+
const animRef = React.useRef<Animation | null>(null);
|
|
51
|
+
const [ready, setReady] = React.useState(false);
|
|
52
|
+
const lastValues = React.useRef({ y: -1, h: -1 });
|
|
53
|
+
|
|
54
|
+
// Computed geometry
|
|
55
|
+
const pad = 2;
|
|
56
|
+
const effectiveR = Math.min(radius, height / 2);
|
|
57
|
+
const bubbleH = bubbleHeightProp ?? Math.round(height * 0.4);
|
|
58
|
+
const insetPx = width * bubbleInset;
|
|
59
|
+
const bubbleW = width - 2 * insetPx;
|
|
60
|
+
const bubbleR = Math.min(effectiveR * 0.6, bubbleH * 0.45, 12);
|
|
61
|
+
const totalH = height + bubbleH;
|
|
62
|
+
|
|
63
|
+
// Static filter string — never changes after mount (critical for Safari)
|
|
64
|
+
const filterUrl = React.useMemo(() => `url(#${filterId})`, [filterId]);
|
|
65
|
+
|
|
66
|
+
// Drop-shadow outline — separate from SVG filter for cleaner compositing
|
|
67
|
+
const outlineShadow = React.useMemo(() => {
|
|
68
|
+
const shadow = `drop-shadow(0 0 ${outlineBlur}px ${outlineColor})`;
|
|
69
|
+
return Array(outlineLayers).fill(shadow).join(' ');
|
|
70
|
+
}, [outlineBlur, outlineColor, outlineLayers]);
|
|
71
|
+
|
|
72
|
+
// Canvas style — Sileo pattern: translateZ(0) + contain + static filter
|
|
73
|
+
const canvasStyle = React.useMemo(
|
|
74
|
+
() => ({
|
|
75
|
+
filter: `${filterUrl} ${outlineShadow}`,
|
|
76
|
+
transform: 'translateZ(0)',
|
|
77
|
+
contain: 'layout style' as const,
|
|
78
|
+
}),
|
|
79
|
+
[filterUrl, outlineShadow],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Readiness gate — Sileo pattern: defer first animation by 1 rAF frame.
|
|
83
|
+
// Ensures the initial DOM state is painted before any animation starts.
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
const rect = bubbleRectRef.current;
|
|
86
|
+
if (!rect) return;
|
|
87
|
+
|
|
88
|
+
// Set initial visual state
|
|
89
|
+
const initY = expanded ? 0 : bubbleH;
|
|
90
|
+
const initH = expanded ? bubbleH : 0;
|
|
91
|
+
lastValues.current = { y: initY, h: initH };
|
|
92
|
+
if (expanded) {
|
|
93
|
+
rect.animate([{ y: '0px', height: `${bubbleH}px` }], { duration: 0, fill: 'forwards' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// After 1 painted frame, mark as ready — triggers re-render so animation
|
|
97
|
+
// effect can pick up any expanded changes that arrived during the wait.
|
|
98
|
+
const raf = requestAnimationFrame(() => setReady(true));
|
|
99
|
+
return () => cancelAnimationFrame(raf);
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// WAAPI animation — tracks values in ref for smooth mid-animation reversals.
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
const rect = bubbleRectRef.current;
|
|
106
|
+
if (!rect || !ready) return;
|
|
107
|
+
|
|
108
|
+
const isExpanding = expanded;
|
|
109
|
+
|
|
110
|
+
// Direction-aware timing
|
|
111
|
+
const baseDuration = isExpanding
|
|
112
|
+
? (expandDuration ?? GOOEY_TIMING.EXPAND_DURATION)
|
|
113
|
+
: (collapseDuration ?? GOOEY_TIMING.COLLAPSE_DURATION);
|
|
114
|
+
const duration = getResponsiveDuration(baseDuration);
|
|
115
|
+
const easing = isExpanding
|
|
116
|
+
? (expandEasing ?? EASINGS.SPRING_GENTLE)
|
|
117
|
+
: (collapseEasing ?? EASINGS.EASE_OUT_CUBIC);
|
|
118
|
+
|
|
119
|
+
// Target state
|
|
120
|
+
const toY = isExpanding ? 0 : bubbleH;
|
|
121
|
+
const toH = isExpanding ? bubbleH : 0;
|
|
122
|
+
|
|
123
|
+
// From state: use tracked values (survives animation cancel)
|
|
124
|
+
const fromY = lastValues.current.y;
|
|
125
|
+
const fromH = lastValues.current.h;
|
|
126
|
+
|
|
127
|
+
// Update tracked values to target
|
|
128
|
+
lastValues.current = { y: toY, h: toH };
|
|
129
|
+
|
|
130
|
+
// Reduced motion: instant
|
|
131
|
+
if (duration === 0) {
|
|
132
|
+
if (animRef.current) animRef.current.cancel();
|
|
133
|
+
rect.animate([{ y: `${toY}px`, height: `${toH}px` }], { duration: 0, fill: 'forwards' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Cancel previous animation (visual state maintained by lastValues tracking)
|
|
138
|
+
if (animRef.current) animRef.current.cancel();
|
|
139
|
+
|
|
140
|
+
// 2-keyframe animation — fill:forwards, no DOM mutation.
|
|
141
|
+
// CRITICAL: Chrome WAAPI requires "px" units for SVG geometry props.
|
|
142
|
+
animRef.current = rect.animate(
|
|
143
|
+
[
|
|
144
|
+
{ y: `${fromY}px`, height: `${fromH}px` },
|
|
145
|
+
{ y: `${toY}px`, height: `${toH}px` },
|
|
146
|
+
],
|
|
147
|
+
{ duration, easing, fill: 'forwards' },
|
|
148
|
+
);
|
|
149
|
+
}, [ready, expanded, bubbleH, expandDuration, collapseDuration, expandEasing, collapseEasing]);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
data-slot="gooey-canvas"
|
|
154
|
+
style={canvasStyle}
|
|
155
|
+
className={cn(
|
|
156
|
+
'absolute inset-0 rounded-[inherit] pointer-events-none z-0 overflow-visible',
|
|
157
|
+
className,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
160
|
+
<svg
|
|
161
|
+
data-slot="gooey-svg"
|
|
162
|
+
width={width}
|
|
163
|
+
height={totalH}
|
|
164
|
+
viewBox={`0 0 ${width} ${totalH}`}
|
|
165
|
+
style={{ position: 'absolute', top: -bubbleH, left: 0, overflow: 'visible' }}
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
>
|
|
168
|
+
<defs>
|
|
169
|
+
<filter
|
|
170
|
+
id={filterId}
|
|
171
|
+
x="-20%"
|
|
172
|
+
y="-20%"
|
|
173
|
+
width="140%"
|
|
174
|
+
height="140%"
|
|
175
|
+
colorInterpolationFilters="sRGB"
|
|
176
|
+
>
|
|
177
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation={computedBlur} result="blur" />
|
|
178
|
+
<feColorMatrix
|
|
179
|
+
in="blur"
|
|
180
|
+
mode="matrix"
|
|
181
|
+
values={`1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${alphaGain ?? 20} ${alphaOffset ?? -10}`}
|
|
182
|
+
result="goo"
|
|
183
|
+
/>
|
|
184
|
+
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
|
|
185
|
+
</filter>
|
|
186
|
+
</defs>
|
|
187
|
+
|
|
188
|
+
{/* Pill rect — static background */}
|
|
189
|
+
<rect
|
|
190
|
+
x={pad}
|
|
191
|
+
y={bubbleH}
|
|
192
|
+
width={width - pad * 2}
|
|
193
|
+
height={height - pad * 2}
|
|
194
|
+
rx={effectiveR}
|
|
195
|
+
ry={effectiveR}
|
|
196
|
+
fill={fillColor}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
{/* Bubble rect — always collapsed in DOM, WAAPI controls visual state */}
|
|
200
|
+
<rect
|
|
201
|
+
ref={bubbleRectRef}
|
|
202
|
+
x={insetPx}
|
|
203
|
+
y={bubbleH}
|
|
204
|
+
width={bubbleW}
|
|
205
|
+
height={0}
|
|
206
|
+
rx={bubbleR}
|
|
207
|
+
ry={bubbleR}
|
|
208
|
+
fill={fillColor}
|
|
209
|
+
/>
|
|
210
|
+
</svg>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
GooeyCanvas.displayName = 'GooeyCanvas';
|
|
216
|
+
|
|
217
|
+
export { GooeyCanvas };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GooeyFilter — SVG filter definition for gooey morphing effects.
|
|
5
|
+
*
|
|
6
|
+
* Renders a hidden `<svg>` with a `<filter>` that uses `feGaussianBlur` +
|
|
7
|
+
* `feColorMatrix` + `feComposite` to merge same-colored shapes organically.
|
|
8
|
+
*
|
|
9
|
+
* Apply via `filter: url(#id)` on a container. All same-colored children merge.
|
|
10
|
+
* `feComposite operator="atop"` preserves sharp text/content on top.
|
|
11
|
+
*
|
|
12
|
+
* @param id - Unique filter ID
|
|
13
|
+
* @param blur - Gaussian blur stdDeviation (default: 8)
|
|
14
|
+
* @param alphaGain - Alpha multiplier for merge threshold (default: 20)
|
|
15
|
+
* @param alphaOffset - Alpha offset for edge sharpness (default: -10)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <GooeyFilter id="my-goo" blur={6} />
|
|
20
|
+
* <div style={{ filter: 'url(#my-goo)' }}>
|
|
21
|
+
* <div className="bg-card" /> // these merge
|
|
22
|
+
* <div className="bg-card" /> // organically
|
|
23
|
+
* </div>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { memo } from 'react';
|
|
30
|
+
import { GOOEY_DEFAULTS, buildColorMatrixValues } from './gooey-utils';
|
|
31
|
+
import type { IGooeyFilterProps } from './Gooey.types';
|
|
32
|
+
|
|
33
|
+
const HIDDEN_SVG_STYLE = {
|
|
34
|
+
position: 'absolute' as const,
|
|
35
|
+
width: 0,
|
|
36
|
+
height: 0,
|
|
37
|
+
overflow: 'hidden' as const,
|
|
38
|
+
pointerEvents: 'none' as const,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GooeyFilter — renders an SVG filter definition for gooey morphing.
|
|
43
|
+
*/
|
|
44
|
+
const GooeyFilter = memo(function GooeyFilter({
|
|
45
|
+
id,
|
|
46
|
+
blur = 8,
|
|
47
|
+
alphaGain = GOOEY_DEFAULTS.ALPHA_GAIN,
|
|
48
|
+
alphaOffset = GOOEY_DEFAULTS.ALPHA_OFFSET,
|
|
49
|
+
}: IGooeyFilterProps) {
|
|
50
|
+
return (
|
|
51
|
+
<svg aria-hidden="true" style={HIDDEN_SVG_STYLE}>
|
|
52
|
+
<defs>
|
|
53
|
+
<filter
|
|
54
|
+
id={id}
|
|
55
|
+
x="-20%"
|
|
56
|
+
y="-20%"
|
|
57
|
+
width="140%"
|
|
58
|
+
height="140%"
|
|
59
|
+
colorInterpolationFilters="sRGB"
|
|
60
|
+
>
|
|
61
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
|
|
62
|
+
<feColorMatrix
|
|
63
|
+
in="blur"
|
|
64
|
+
mode="matrix"
|
|
65
|
+
values={buildColorMatrixValues(alphaGain, alphaOffset)}
|
|
66
|
+
result="goo"
|
|
67
|
+
/>
|
|
68
|
+
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
|
|
69
|
+
</filter>
|
|
70
|
+
</defs>
|
|
71
|
+
</svg>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
GooeyFilter.displayName = 'GooeyFilter';
|
|
76
|
+
|
|
77
|
+
export { GooeyFilter };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MorphPath — SVG `<path>` component with parametric pill-to-blob morphing.
|
|
5
|
+
*
|
|
6
|
+
* Generates a `<path d="...">` that transitions from a pill shape to an
|
|
7
|
+
* expanded blob based on the `progress` value (0→1). Uses quadratic Bezier
|
|
8
|
+
* curves at the junction for organic concave connections.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <svg width={300} height={80} style={{ position: 'absolute' }}>
|
|
13
|
+
* <MorphPath
|
|
14
|
+
* pillWidth={260}
|
|
15
|
+
* bodyWidth={180}
|
|
16
|
+
* totalHeight={80}
|
|
17
|
+
* progress={expanded ? 1 : 0}
|
|
18
|
+
* direction="up"
|
|
19
|
+
* fill="var(--card)"
|
|
20
|
+
* />
|
|
21
|
+
* </svg>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { memo } from 'react';
|
|
28
|
+
import { useMorphPath } from './useMorphPath';
|
|
29
|
+
import type { IMorphPathProps } from './Gooey.types';
|
|
30
|
+
import { GOOEY_DEFAULTS } from './gooey-utils';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MorphPath — parametric SVG path for pill→blob morphing.
|
|
34
|
+
*/
|
|
35
|
+
const MorphPath = memo(function MorphPath({
|
|
36
|
+
pillWidth,
|
|
37
|
+
bodyWidth,
|
|
38
|
+
totalHeight,
|
|
39
|
+
progress,
|
|
40
|
+
pillHeight = GOOEY_DEFAULTS.PILL_HEIGHT,
|
|
41
|
+
direction = 'down',
|
|
42
|
+
...pathProps
|
|
43
|
+
}: IMorphPathProps) {
|
|
44
|
+
const d = useMorphPath({
|
|
45
|
+
pillWidth,
|
|
46
|
+
bodyWidth,
|
|
47
|
+
totalHeight,
|
|
48
|
+
progress,
|
|
49
|
+
pillHeight,
|
|
50
|
+
direction,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return <path d={d} {...pathProps} />;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
MorphPath.displayName = 'MorphPath';
|
|
57
|
+
|
|
58
|
+
export { MorphPath };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gooey morphing utilities — pure functions for SVG filter and path generation.
|
|
3
|
+
*
|
|
4
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Constants
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** Gooey animation timing constants. */
|
|
12
|
+
export const GOOEY_TIMING = {
|
|
13
|
+
/** Expand duration in ms — spring-like with room for overshoot */
|
|
14
|
+
EXPAND_DURATION: 550,
|
|
15
|
+
/** Collapse duration in ms — snappier settle, no overshoot */
|
|
16
|
+
COLLAPSE_DURATION: 400,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** Default gooey filter parameters. */
|
|
20
|
+
export const GOOEY_DEFAULTS = {
|
|
21
|
+
/** Blur = height * BLUR_RATIO */
|
|
22
|
+
BLUR_RATIO: 0.15,
|
|
23
|
+
/** feColorMatrix alpha channel multiplier */
|
|
24
|
+
ALPHA_GAIN: 20,
|
|
25
|
+
/** feColorMatrix alpha channel offset */
|
|
26
|
+
ALPHA_OFFSET: -10,
|
|
27
|
+
/** Default pill height (px) */
|
|
28
|
+
PILL_HEIGHT: 34,
|
|
29
|
+
/** Quadratic bezier curve factor at full expansion */
|
|
30
|
+
CURVE_FACTOR: 14,
|
|
31
|
+
/** Max body corner radius (px) */
|
|
32
|
+
MAX_CORNER_RADIUS: 16,
|
|
33
|
+
/** Corner radius to body-height ratio */
|
|
34
|
+
CORNER_RATIO: 0.45,
|
|
35
|
+
/** Minimum body height delta to show rounded corners */
|
|
36
|
+
MIN_BODY_DELTA: 8,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Blur computation
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute the optimal Gaussian blur for a gooey filter based on element height.
|
|
45
|
+
*
|
|
46
|
+
* @param height - Element height in pixels
|
|
47
|
+
* @returns Blur stdDeviation value
|
|
48
|
+
*/
|
|
49
|
+
export function computeBlur(height: number): number {
|
|
50
|
+
return Math.round(height * GOOEY_DEFAULTS.BLUR_RATIO);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the feColorMatrix `values` string.
|
|
55
|
+
*
|
|
56
|
+
* @param gain - Alpha multiplier (default: 20)
|
|
57
|
+
* @param offset - Alpha offset (default: -10)
|
|
58
|
+
* @returns Matrix values string for feColorMatrix
|
|
59
|
+
*/
|
|
60
|
+
export function buildColorMatrixValues(
|
|
61
|
+
gain: number = GOOEY_DEFAULTS.ALPHA_GAIN,
|
|
62
|
+
offset: number = GOOEY_DEFAULTS.ALPHA_OFFSET,
|
|
63
|
+
): string {
|
|
64
|
+
return `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${gain} ${offset}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the CSS filter string with gooey + optional drop-shadow outline.
|
|
69
|
+
*
|
|
70
|
+
* @param filterId - SVG filter ID to reference
|
|
71
|
+
* @param outlineBlur - Drop-shadow blur (px)
|
|
72
|
+
* @param outlineColor - Drop-shadow color
|
|
73
|
+
* @param outlineLayers - Number of stacked drop-shadows for thickness
|
|
74
|
+
* @returns CSS filter property value
|
|
75
|
+
*/
|
|
76
|
+
export function buildFilterString(
|
|
77
|
+
filterId: string,
|
|
78
|
+
outlineBlur: number = 0.5,
|
|
79
|
+
outlineColor: string = 'var(--border)',
|
|
80
|
+
outlineLayers: number = 2,
|
|
81
|
+
): string {
|
|
82
|
+
const goo = `url(#${filterId})`;
|
|
83
|
+
const shadow = `drop-shadow(0 0 ${outlineBlur}px ${outlineColor})`;
|
|
84
|
+
const shadows = Array(outlineLayers).fill(shadow).join(' ');
|
|
85
|
+
return `${goo} ${shadows}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Path memoization
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
type PathFn = (pw: number, bw: number, th: number, t: number, ph: number) => string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Memoize a path generation function — caches last result to avoid
|
|
96
|
+
* recomputing identical paths on consecutive animation frames.
|
|
97
|
+
*
|
|
98
|
+
* @param fn - Path generation function
|
|
99
|
+
* @returns Memoized function
|
|
100
|
+
*/
|
|
101
|
+
export function memoizePath(fn: PathFn): PathFn {
|
|
102
|
+
let lastArgs: [number, number, number, number, number] | null = null;
|
|
103
|
+
let lastResult = '';
|
|
104
|
+
return (pw, bw, th, t, ph) => {
|
|
105
|
+
if (lastArgs && lastArgs[0] === pw && lastArgs[1] === bw && lastArgs[2] === th && lastArgs[3] === t && lastArgs[4] === ph) {
|
|
106
|
+
return lastResult;
|
|
107
|
+
}
|
|
108
|
+
lastResult = fn(pw, bw, th, t, ph);
|
|
109
|
+
lastArgs = [pw, bw, th, t, ph];
|
|
110
|
+
return lastResult;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Parametric SVG path generation
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generate an SVG path for a pill that morphs downward into a body blob.
|
|
120
|
+
* Adapted from goey-toast's morphPathRaw.
|
|
121
|
+
*
|
|
122
|
+
* @param pw - Pill width
|
|
123
|
+
* @param bw - Body width (expanded)
|
|
124
|
+
* @param th - Total height (pill + body)
|
|
125
|
+
* @param t - Transition progress 0 (pill only) → 1 (full blob)
|
|
126
|
+
* @param ph - Pill height (default: PILL_HEIGHT)
|
|
127
|
+
* @returns SVG path d-string
|
|
128
|
+
*/
|
|
129
|
+
export function morphPathDown(
|
|
130
|
+
pw: number,
|
|
131
|
+
bw: number,
|
|
132
|
+
th: number,
|
|
133
|
+
t: number,
|
|
134
|
+
ph: number = GOOEY_DEFAULTS.PILL_HEIGHT,
|
|
135
|
+
): string {
|
|
136
|
+
const pr = ph / 2;
|
|
137
|
+
const pillW = Math.min(pw, bw);
|
|
138
|
+
const bodyH = ph + (th - ph) * t;
|
|
139
|
+
|
|
140
|
+
// Pure pill when t ≤ 0 or body too small
|
|
141
|
+
if (t <= 0 || bodyH - ph < GOOEY_DEFAULTS.MIN_BODY_DELTA) {
|
|
142
|
+
return [
|
|
143
|
+
`M 0,${pr}`,
|
|
144
|
+
`A ${pr},${pr} 0 0 1 ${pr},0`,
|
|
145
|
+
`H ${pillW - pr}`,
|
|
146
|
+
`A ${pr},${pr} 0 0 1 ${pillW},${pr}`,
|
|
147
|
+
`A ${pr},${pr} 0 0 1 ${pillW - pr},${ph}`,
|
|
148
|
+
`H ${pr}`,
|
|
149
|
+
`A ${pr},${pr} 0 0 1 0,${pr}`,
|
|
150
|
+
`Z`,
|
|
151
|
+
].join(' ');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const curve = GOOEY_DEFAULTS.CURVE_FACTOR * t;
|
|
155
|
+
const cr = Math.min(GOOEY_DEFAULTS.MAX_CORNER_RADIUS, (bodyH - ph) * GOOEY_DEFAULTS.CORNER_RATIO);
|
|
156
|
+
const bodyW = pillW + (bw - pillW) * t;
|
|
157
|
+
const bodyTop = ph - curve;
|
|
158
|
+
const qEndX = Math.min(pillW + curve, bodyW - cr);
|
|
159
|
+
|
|
160
|
+
return [
|
|
161
|
+
`M 0,${pr}`,
|
|
162
|
+
`A ${pr},${pr} 0 0 1 ${pr},0`,
|
|
163
|
+
`H ${pillW - pr}`,
|
|
164
|
+
`A ${pr},${pr} 0 0 1 ${pillW},${pr}`,
|
|
165
|
+
`L ${pillW},${bodyTop}`,
|
|
166
|
+
`Q ${pillW},${bodyTop + curve} ${qEndX},${bodyTop + curve}`,
|
|
167
|
+
`H ${bodyW - cr}`,
|
|
168
|
+
`A ${cr},${cr} 0 0 1 ${bodyW},${bodyTop + curve + cr}`,
|
|
169
|
+
`L ${bodyW},${bodyH - cr}`,
|
|
170
|
+
`A ${cr},${cr} 0 0 1 ${bodyW - cr},${bodyH}`,
|
|
171
|
+
`H ${cr}`,
|
|
172
|
+
`A ${cr},${cr} 0 0 1 0,${bodyH - cr}`,
|
|
173
|
+
`Z`,
|
|
174
|
+
].join(' ');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate an SVG path for a pill that morphs UPWARD into a body blob.
|
|
179
|
+
* Used for DynamicToggle's bubble-above-pill layout.
|
|
180
|
+
*
|
|
181
|
+
* @param pw - Pill width
|
|
182
|
+
* @param bw - Body (bubble) width
|
|
183
|
+
* @param th - Total height (bubble + pill)
|
|
184
|
+
* @param t - Transition progress 0 (pill only) → 1 (full blob with bubble)
|
|
185
|
+
* @param ph - Pill height
|
|
186
|
+
* @returns SVG path d-string
|
|
187
|
+
*/
|
|
188
|
+
export function morphPathUp(
|
|
189
|
+
pw: number,
|
|
190
|
+
bw: number,
|
|
191
|
+
th: number,
|
|
192
|
+
t: number,
|
|
193
|
+
ph: number = GOOEY_DEFAULTS.PILL_HEIGHT,
|
|
194
|
+
): string {
|
|
195
|
+
const pr = ph / 2;
|
|
196
|
+
const pillW = Math.min(pw, bw);
|
|
197
|
+
const bubbleH = (th - ph) * t;
|
|
198
|
+
|
|
199
|
+
// Pure pill when t ≤ 0 or bubble too small
|
|
200
|
+
if (t <= 0 || bubbleH < GOOEY_DEFAULTS.MIN_BODY_DELTA) {
|
|
201
|
+
const y0 = th - ph;
|
|
202
|
+
return [
|
|
203
|
+
`M 0,${y0 + pr}`,
|
|
204
|
+
`A ${pr},${pr} 0 0 1 ${pr},${y0}`,
|
|
205
|
+
`H ${pillW - pr}`,
|
|
206
|
+
`A ${pr},${pr} 0 0 1 ${pillW},${y0 + pr}`,
|
|
207
|
+
`A ${pr},${pr} 0 0 1 ${pillW - pr},${y0 + ph}`,
|
|
208
|
+
`H ${pr}`,
|
|
209
|
+
`A ${pr},${pr} 0 0 1 0,${y0 + pr}`,
|
|
210
|
+
`Z`,
|
|
211
|
+
].join(' ');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const curve = GOOEY_DEFAULTS.CURVE_FACTOR * t;
|
|
215
|
+
const cr = Math.min(GOOEY_DEFAULTS.MAX_CORNER_RADIUS, bubbleH * GOOEY_DEFAULTS.CORNER_RATIO);
|
|
216
|
+
const bubbleW = pillW + (bw - pillW) * t;
|
|
217
|
+
const pillTop = th - ph;
|
|
218
|
+
const bubbleBottom = pillTop + curve;
|
|
219
|
+
const bubbleTop = pillTop - bubbleH + curve;
|
|
220
|
+
const qEndX = Math.min(pillW + curve, bubbleW - cr);
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
// Pill bottom-left
|
|
224
|
+
`M 0,${pillTop + pr}`,
|
|
225
|
+
`A ${pr},${pr} 0 0 1 0,${pillTop + ph - pr}`,
|
|
226
|
+
// Pill bottom
|
|
227
|
+
`A ${pr},${pr} 0 0 1 ${pr},${pillTop + ph}`,
|
|
228
|
+
`H ${pillW - pr}`,
|
|
229
|
+
`A ${pr},${pr} 0 0 1 ${pillW},${pillTop + ph - pr}`,
|
|
230
|
+
// Pill right up to junction
|
|
231
|
+
`L ${pillW},${bubbleBottom}`,
|
|
232
|
+
// Right organic curve: pill to bubble
|
|
233
|
+
`Q ${pillW},${bubbleBottom - curve} ${qEndX},${bubbleBottom - curve}`,
|
|
234
|
+
// Bubble right side
|
|
235
|
+
`H ${bubbleW - cr}`,
|
|
236
|
+
`A ${cr},${cr} 0 0 0 ${bubbleW},${bubbleBottom - curve - cr}`,
|
|
237
|
+
// Bubble right edge up
|
|
238
|
+
`L ${bubbleW},${bubbleTop + cr}`,
|
|
239
|
+
// Bubble top-right corner
|
|
240
|
+
`A ${cr},${cr} 0 0 0 ${bubbleW - cr},${bubbleTop}`,
|
|
241
|
+
// Bubble top
|
|
242
|
+
`H ${cr}`,
|
|
243
|
+
// Bubble top-left corner
|
|
244
|
+
`A ${cr},${cr} 0 0 0 0,${bubbleTop + cr}`,
|
|
245
|
+
// Left edge down to pill
|
|
246
|
+
`Z`,
|
|
247
|
+
].join(' ');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Memoized versions for animation performance. */
|
|
251
|
+
export const morphPathDownMemo = memoizePath(morphPathDown);
|
|
252
|
+
export const morphPathUpMemo = memoizePath(morphPathUp);
|
|
253
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gooey — SVG-based morphing primitives for organic shape merging.
|
|
3
|
+
*
|
|
4
|
+
* Two techniques:
|
|
5
|
+
* - **Filter**: `GooeyCanvas` / `GooeyFilter` — SVG filter merges same-colored shapes
|
|
6
|
+
* - **Path**: `MorphPath` / `useMorphPath` — parametric SVG path with Bezier junctions
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // Filter approach — automatic merge
|
|
11
|
+
* <GooeyCanvas height={40}>
|
|
12
|
+
* <div className="bg-card rounded-full h-10 w-64" />
|
|
13
|
+
* <div className="bg-card absolute bottom-full rounded-lg h-6 w-32" />
|
|
14
|
+
* </GooeyCanvas>
|
|
15
|
+
*
|
|
16
|
+
* // Path approach — precise control
|
|
17
|
+
* <svg><MorphPath pillWidth={260} bodyWidth={180} totalHeight={60} progress={0.5} fill="var(--card)" /></svg>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Components
|
|
24
|
+
export { GooeyFilter } from './GooeyFilter';
|
|
25
|
+
export { GooeyCanvas } from './GooeyCanvas';
|
|
26
|
+
export { MorphPath } from './MorphPath';
|
|
27
|
+
|
|
28
|
+
// Hooks
|
|
29
|
+
export { useMorphPath } from './useMorphPath';
|
|
30
|
+
|
|
31
|
+
// Utilities
|
|
32
|
+
export {
|
|
33
|
+
GOOEY_DEFAULTS,
|
|
34
|
+
computeBlur,
|
|
35
|
+
buildColorMatrixValues,
|
|
36
|
+
buildFilterString,
|
|
37
|
+
memoizePath,
|
|
38
|
+
morphPathDown,
|
|
39
|
+
morphPathUp,
|
|
40
|
+
morphPathDownMemo,
|
|
41
|
+
morphPathUpMemo,
|
|
42
|
+
} from './gooey-utils';
|
|
43
|
+
|
|
44
|
+
// Types
|
|
45
|
+
export type {
|
|
46
|
+
IGooeyFilterProps,
|
|
47
|
+
IGooeyCanvasProps,
|
|
48
|
+
IMorphPathProps,
|
|
49
|
+
IUseMorphPathOptions,
|
|
50
|
+
} from './Gooey.types';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useMorphPath — hook that generates a parametric SVG path d-string.
|
|
5
|
+
*
|
|
6
|
+
* Returns a memoized path for a pill shape morphing into a blob.
|
|
7
|
+
* Use with `<path d={path} />` in an SVG element.
|
|
8
|
+
*
|
|
9
|
+
* @param opts - Dimensions and progress
|
|
10
|
+
* @returns SVG path d-string
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const path = useMorphPath({
|
|
15
|
+
* pillWidth: 260,
|
|
16
|
+
* bodyWidth: 180,
|
|
17
|
+
* totalHeight: 60,
|
|
18
|
+
* progress: expanded ? 1 : 0,
|
|
19
|
+
* direction: 'up',
|
|
20
|
+
* });
|
|
21
|
+
* return <svg><path d={path} fill="var(--card)" /></svg>;
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module @mks2508/mks-ui/react/primitives/waapi/Gooey
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useMemo } from 'react';
|
|
28
|
+
import { morphPathDown, morphPathUp, GOOEY_DEFAULTS } from './gooey-utils';
|
|
29
|
+
import type { IUseMorphPathOptions } from './Gooey.types';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a parametric SVG morph path from dimensions and progress.
|
|
33
|
+
*/
|
|
34
|
+
function useMorphPath({
|
|
35
|
+
pillWidth,
|
|
36
|
+
bodyWidth,
|
|
37
|
+
totalHeight,
|
|
38
|
+
progress,
|
|
39
|
+
pillHeight = GOOEY_DEFAULTS.PILL_HEIGHT,
|
|
40
|
+
direction = 'down',
|
|
41
|
+
}: IUseMorphPathOptions): string {
|
|
42
|
+
return useMemo(() => {
|
|
43
|
+
const fn = direction === 'up' ? morphPathUp : morphPathDown;
|
|
44
|
+
return fn(pillWidth, bodyWidth, totalHeight, progress, pillHeight);
|
|
45
|
+
}, [pillWidth, bodyWidth, totalHeight, progress, pillHeight, direction]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { useMorphPath };
|