libraries-dev-svelte 0.1.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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/bot-avatars/BotAvatar.svelte +368 -0
- package/dist/bot-avatars/BotAvatar.svelte.d.ts +4 -0
- package/dist/bot-avatars/color.d.ts +14 -0
- package/dist/bot-avatars/color.js +89 -0
- package/dist/bot-avatars/draw.d.ts +55 -0
- package/dist/bot-avatars/draw.js +495 -0
- package/dist/bot-avatars/engine.d.ts +138 -0
- package/dist/bot-avatars/engine.js +690 -0
- package/dist/bot-avatars/path-cache.d.ts +2 -0
- package/dist/bot-avatars/path-cache.js +10 -0
- package/dist/bot-avatars/plastic.d.ts +80 -0
- package/dist/bot-avatars/plastic.js +839 -0
- package/dist/bot-avatars/presets.d.ts +12 -0
- package/dist/bot-avatars/presets.js +34 -0
- package/dist/bot-avatars/shapes.d.ts +4 -0
- package/dist/bot-avatars/shapes.js +27 -0
- package/dist/bot-avatars/ticker.d.ts +8 -0
- package/dist/bot-avatars/ticker.js +52 -0
- package/dist/bot-avatars/types.d.ts +163 -0
- package/dist/bot-avatars/types.js +1 -0
- package/dist/gooey-radial-menu/GooeyRadialMenu.svelte +747 -0
- package/dist/gooey-radial-menu/GooeyRadialMenu.svelte.d.ts +7 -0
- package/dist/gooey-radial-menu/motion.d.ts +2 -0
- package/dist/gooey-radial-menu/motion.js +34 -0
- package/dist/gooey-radial-menu/shadow.d.ts +10 -0
- package/dist/gooey-radial-menu/shadow.js +54 -0
- package/dist/gooey-radial-menu/types.d.ts +28 -0
- package/dist/gooey-radial-menu/types.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +12 -0
- package/package.json +46 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, onMount } from 'svelte';
|
|
3
|
+
import { easingFunction } from './motion';
|
|
4
|
+
import { parseShadow, type ShadowLayer } from './shadow';
|
|
5
|
+
import type { GooeyMenuSettings, GooeyRadialMenuProps } from './types';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
blur = 6,
|
|
9
|
+
contrast = 18,
|
|
10
|
+
fill = 'var(--modal-bg, #fff)',
|
|
11
|
+
shadow = '',
|
|
12
|
+
pro = false,
|
|
13
|
+
bare = false,
|
|
14
|
+
}: GooeyRadialMenuProps & { fill?: string } = $props();
|
|
15
|
+
|
|
16
|
+
const instanceId = $props.id();
|
|
17
|
+
const filterId = `gooey-${instanceId.replace(/[^a-zA-Z0-9_-]/g, '')}`;
|
|
18
|
+
const satellites = [
|
|
19
|
+
{ label: 'New file', x: -54, y: -34, icon: 'file' },
|
|
20
|
+
{ label: 'Add image', x: 0, y: -64, icon: 'image' },
|
|
21
|
+
{ label: 'New folder', x: 54, y: -34, icon: 'folder' },
|
|
22
|
+
] as const;
|
|
23
|
+
const defaults: GooeyMenuSettings = {
|
|
24
|
+
openDur: 550,
|
|
25
|
+
openEase: 'Bouncy',
|
|
26
|
+
openStagger: 40,
|
|
27
|
+
closeDur: 250,
|
|
28
|
+
closeEase: 'Snappy',
|
|
29
|
+
closeStagger: 0,
|
|
30
|
+
spread: 1,
|
|
31
|
+
iconDur: 180,
|
|
32
|
+
iconDelay: 120,
|
|
33
|
+
anticipDist: 5,
|
|
34
|
+
anticipDur: 700,
|
|
35
|
+
};
|
|
36
|
+
const eases: Record<string, string> = {
|
|
37
|
+
Bouncy: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
38
|
+
Smooth: 'cubic-bezier(0.3, 1.05, 0.4, 1)',
|
|
39
|
+
Snappy: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
40
|
+
'Ease in-out': 'ease-in-out',
|
|
41
|
+
Linear: 'linear',
|
|
42
|
+
};
|
|
43
|
+
const easeNames = Object.keys(eases);
|
|
44
|
+
const allItems = [...satellites, { label: 'Menu', x: 0, y: 0, icon: 'main' as const }];
|
|
45
|
+
const initialBoxes: Array<BlobBox | null> = allItems.map(() => null);
|
|
46
|
+
|
|
47
|
+
interface BlobBox {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
w: number;
|
|
51
|
+
h: number;
|
|
52
|
+
r: [number, number, number, number];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const binarize = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 60 -29.5';
|
|
56
|
+
let menuOpen = $state(false);
|
|
57
|
+
let anticipating = $state(false);
|
|
58
|
+
let menuSettings = $state<GooeyMenuSettings>({ ...defaults });
|
|
59
|
+
let copied = $state(false);
|
|
60
|
+
let reducedMotion = $state(false);
|
|
61
|
+
let mounted = $state(false);
|
|
62
|
+
let groupElement: HTMLDivElement;
|
|
63
|
+
let itemWrappers: Array<HTMLDivElement | undefined> = [];
|
|
64
|
+
let blobElements: Array<SVGRectElement | undefined> = [];
|
|
65
|
+
let itemBoxes = $state<Array<BlobBox | null>>(initialBoxes);
|
|
66
|
+
let groupSize = $state({ w: 0, h: 0 });
|
|
67
|
+
|
|
68
|
+
const shadows = $derived(parseShadow(shadow));
|
|
69
|
+
const shadowEntries = $derived(shadows.map((layer, index) => ({ layer, index })));
|
|
70
|
+
const svgShadows = $derived(
|
|
71
|
+
shadowEntries.filter(({ layer }) => layer.inset || layer.spread !== 0),
|
|
72
|
+
);
|
|
73
|
+
const cssShadowFilter = $derived(
|
|
74
|
+
shadowEntries
|
|
75
|
+
.filter(({ layer }) => !layer.inset && layer.spread === 0)
|
|
76
|
+
.map(({ layer }) => `drop-shadow(${layer.x}px ${layer.y}px ${layer.blur}px ${layer.color})`)
|
|
77
|
+
.join(' '),
|
|
78
|
+
);
|
|
79
|
+
const shadowExtent = $derived(
|
|
80
|
+
svgShadows.reduce(
|
|
81
|
+
(max, { layer }) =>
|
|
82
|
+
Math.max(
|
|
83
|
+
max,
|
|
84
|
+
Math.max(Math.abs(layer.x), Math.abs(layer.y)) +
|
|
85
|
+
layer.blur * 1.5 +
|
|
86
|
+
Math.max(0, layer.spread),
|
|
87
|
+
),
|
|
88
|
+
0,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
const filterPad = $derived(Math.ceil(blur * 3 + shadowExtent + 24));
|
|
92
|
+
const intercept = $derived(Math.round((0.5 - contrast * (5 / 12)) * 100) / 100);
|
|
93
|
+
const phase = $derived(
|
|
94
|
+
menuOpen
|
|
95
|
+
? {
|
|
96
|
+
duration: menuSettings.openDur,
|
|
97
|
+
ease: eases[menuSettings.openEase],
|
|
98
|
+
stagger: menuSettings.openStagger,
|
|
99
|
+
}
|
|
100
|
+
: {
|
|
101
|
+
duration: menuSettings.closeDur,
|
|
102
|
+
ease: eases[menuSettings.closeEase],
|
|
103
|
+
stagger: menuSettings.closeStagger,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
let positions = allItems.map(() => ({ x: 0, y: 0 }));
|
|
108
|
+
let frameIds = allItems.map(() => 0);
|
|
109
|
+
let anticipationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
110
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
|
|
112
|
+
function parseCorner(value: string, size: number): number {
|
|
113
|
+
const first = value.split(' ')[0];
|
|
114
|
+
return first.endsWith('%')
|
|
115
|
+
? ((parseFloat(first) || 0) / 100) * size
|
|
116
|
+
: parseFloat(first) || 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function offsetTo(element: HTMLElement, ancestor: HTMLElement) {
|
|
120
|
+
let x = 0;
|
|
121
|
+
let y = 0;
|
|
122
|
+
let node: HTMLElement | null = element;
|
|
123
|
+
while (node && node !== ancestor && ancestor.contains(node)) {
|
|
124
|
+
x += node.offsetLeft;
|
|
125
|
+
y += node.offsetTop;
|
|
126
|
+
node = node.offsetParent as HTMLElement | null;
|
|
127
|
+
}
|
|
128
|
+
return { x, y };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sameBox(left: BlobBox | null, right: BlobBox) {
|
|
132
|
+
return !!left && left.x === right.x && left.y === right.y && left.w === right.w &&
|
|
133
|
+
left.h === right.h && left.r.every((radius, index) => radius === right.r[index]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function measure() {
|
|
137
|
+
if (!groupElement) return;
|
|
138
|
+
const w = groupElement.offsetWidth;
|
|
139
|
+
const h = groupElement.offsetHeight;
|
|
140
|
+
if (groupSize.w !== w || groupSize.h !== h) groupSize = { w, h };
|
|
141
|
+
|
|
142
|
+
const measured = itemWrappers.map((wrapper, index) => {
|
|
143
|
+
if (!wrapper) return itemBoxes[index];
|
|
144
|
+
const offset = offsetTo(wrapper, groupElement);
|
|
145
|
+
const boxWidth = wrapper.offsetWidth;
|
|
146
|
+
const boxHeight = wrapper.offsetHeight;
|
|
147
|
+
const target = (wrapper.firstElementChild as HTMLElement | null) ?? wrapper;
|
|
148
|
+
const computed = getComputedStyle(target);
|
|
149
|
+
const size = Math.min(boxWidth, boxHeight);
|
|
150
|
+
return {
|
|
151
|
+
x: offset.x,
|
|
152
|
+
y: offset.y,
|
|
153
|
+
w: boxWidth,
|
|
154
|
+
h: boxHeight,
|
|
155
|
+
r: [
|
|
156
|
+
parseCorner(computed.borderTopLeftRadius, size),
|
|
157
|
+
parseCorner(computed.borderTopRightRadius, size),
|
|
158
|
+
parseCorner(computed.borderBottomRightRadius, size),
|
|
159
|
+
parseCorner(computed.borderBottomLeftRadius, size),
|
|
160
|
+
] as [number, number, number, number],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
if (measured.some((box, index) => !box || !sameBox(itemBoxes[index], box))) {
|
|
164
|
+
itemBoxes = measured;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeTransform(index: number, x: number, y: number) {
|
|
169
|
+
const transform = `translate(${x}px, ${y}px)`;
|
|
170
|
+
if (itemWrappers[index]) itemWrappers[index]!.style.transform = transform;
|
|
171
|
+
if (blobElements[index]) blobElements[index]!.style.transform = transform;
|
|
172
|
+
positions[index] = { x, y };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function animateItem(index: number, x: number, y: number, duration: number, ease: string, delay: number) {
|
|
176
|
+
if (frameIds[index]) cancelAnimationFrame(frameIds[index]);
|
|
177
|
+
const from = positions[index];
|
|
178
|
+
if (duration <= 0 || (from.x === x && from.y === y)) {
|
|
179
|
+
writeTransform(index, x, y);
|
|
180
|
+
frameIds[index] = 0;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const startPosition = { ...from };
|
|
185
|
+
const evaluate = easingFunction(ease);
|
|
186
|
+
const start = performance.now() + delay;
|
|
187
|
+
const tick = (now: number) => {
|
|
188
|
+
const progress = Math.min(1, Math.max(0, (now - start) / duration));
|
|
189
|
+
const eased = evaluate(progress);
|
|
190
|
+
writeTransform(
|
|
191
|
+
index,
|
|
192
|
+
startPosition.x + (x - startPosition.x) * eased,
|
|
193
|
+
startPosition.y + (y - startPosition.y) * eased,
|
|
194
|
+
);
|
|
195
|
+
if (progress < 1) frameIds[index] = requestAnimationFrame(tick);
|
|
196
|
+
else frameIds[index] = 0;
|
|
197
|
+
};
|
|
198
|
+
frameIds[index] = requestAnimationFrame(tick);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$effect(() => {
|
|
202
|
+
const open = menuOpen;
|
|
203
|
+
const duration = reducedMotion ? 0 : phase.duration;
|
|
204
|
+
const ease = phase.ease;
|
|
205
|
+
const stagger = phase.stagger;
|
|
206
|
+
const spread = menuSettings.spread;
|
|
207
|
+
if (!mounted) return;
|
|
208
|
+
satellites.forEach((satellite, index) => {
|
|
209
|
+
animateItem(
|
|
210
|
+
index,
|
|
211
|
+
open ? satellite.x * spread : 0,
|
|
212
|
+
open ? satellite.y * spread : 0,
|
|
213
|
+
duration,
|
|
214
|
+
ease,
|
|
215
|
+
index * stagger,
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
animateItem(allItems.length - 1, 0, 0, duration, ease, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
function toggle() {
|
|
222
|
+
if (menuOpen && menuSettings.anticipDist > 0) {
|
|
223
|
+
if (anticipationTimer) clearTimeout(anticipationTimer);
|
|
224
|
+
anticipating = false;
|
|
225
|
+
requestAnimationFrame(() => (anticipating = true));
|
|
226
|
+
anticipationTimer = setTimeout(() => (anticipating = false), menuSettings.anticipDur);
|
|
227
|
+
}
|
|
228
|
+
menuOpen = !menuOpen;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function updateSetting<K extends keyof GooeyMenuSettings>(key: K, value: GooeyMenuSettings[K]) {
|
|
232
|
+
menuSettings[key] = value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function menuSnippet(settings: GooeyMenuSettings): string {
|
|
236
|
+
return [
|
|
237
|
+
'// liquid-gooey — Menu values',
|
|
238
|
+
`open: { duration: ${settings.openDur}, easing: '${eases[settings.openEase]}', stagger: ${settings.openStagger} },`,
|
|
239
|
+
`close: { duration: ${settings.closeDur}, easing: '${eases[settings.closeEase]}', stagger: ${settings.closeStagger} },`,
|
|
240
|
+
`anticipation: { distance: ${settings.anticipDist}, duration: ${settings.anticipDur} },`,
|
|
241
|
+
`spread: ${settings.spread}, iconFade: ${settings.iconDur}, iconDelay: ${settings.iconDelay},`,
|
|
242
|
+
].join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function copyValues() {
|
|
246
|
+
navigator.clipboard.writeText(menuSnippet(menuSettings)).then(() => {
|
|
247
|
+
copied = true;
|
|
248
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
249
|
+
copyTimer = setTimeout(() => (copied = false), 1400);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onMount(() => {
|
|
254
|
+
measure();
|
|
255
|
+
const observer = new ResizeObserver(measure);
|
|
256
|
+
observer.observe(groupElement);
|
|
257
|
+
itemWrappers.forEach((wrapper) => wrapper && observer.observe(wrapper));
|
|
258
|
+
|
|
259
|
+
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
260
|
+
reducedMotion = motionQuery.matches;
|
|
261
|
+
const updateMotionPreference = (event: MediaQueryListEvent) => (reducedMotion = event.matches);
|
|
262
|
+
motionQuery.addEventListener('change', updateMotionPreference);
|
|
263
|
+
mounted = true;
|
|
264
|
+
|
|
265
|
+
return () => {
|
|
266
|
+
observer.disconnect();
|
|
267
|
+
motionQuery.removeEventListener('change', updateMotionPreference);
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
onDestroy(() => {
|
|
272
|
+
if (anticipationTimer) clearTimeout(anticipationTimer);
|
|
273
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
274
|
+
frameIds.forEach((frame) => frame && cancelAnimationFrame(frame));
|
|
275
|
+
});
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
{#snippet menuStage()}
|
|
279
|
+
<div
|
|
280
|
+
class={`pm${menuOpen ? ' pm-open' : ''}${anticipating ? ' pm-anticipating' : ''}`}
|
|
281
|
+
bind:this={groupElement}
|
|
282
|
+
style={`--pm-anticip:${menuSettings.anticipDist}px;--pm-anticip-dur:${menuSettings.anticipDur}ms;--pm-icon-dur:${menuSettings.iconDur}ms;position:relative;isolation:isolate;`}
|
|
283
|
+
>
|
|
284
|
+
<svg
|
|
285
|
+
aria-hidden="true"
|
|
286
|
+
focusable="false"
|
|
287
|
+
data-gooey-svg=""
|
|
288
|
+
style={`position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:-1;will-change:filter,transform;${cssShadowFilter ? `filter:${cssShadowFilter};` : ''}`}
|
|
289
|
+
>
|
|
290
|
+
<defs>
|
|
291
|
+
<filter
|
|
292
|
+
id={filterId}
|
|
293
|
+
filterUnits="userSpaceOnUse"
|
|
294
|
+
x={-filterPad}
|
|
295
|
+
y={-filterPad}
|
|
296
|
+
width={groupSize.w + filterPad * 2}
|
|
297
|
+
height={groupSize.h + filterPad * 2}
|
|
298
|
+
color-interpolation-filters="sRGB"
|
|
299
|
+
>
|
|
300
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
|
|
301
|
+
<feColorMatrix
|
|
302
|
+
in="blur"
|
|
303
|
+
type="matrix"
|
|
304
|
+
values={`1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${contrast} ${intercept}`}
|
|
305
|
+
result="goo"
|
|
306
|
+
/>
|
|
307
|
+
<feComposite in="SourceGraphic" in2="goo" operator="atop" result="shape" />
|
|
308
|
+
{#if svgShadows.some(({ layer }) => layer.inset || layer.spread !== 0)}
|
|
309
|
+
<feColorMatrix in="shape" type="matrix" values={binarize} result="bin" />
|
|
310
|
+
{/if}
|
|
311
|
+
{#each svgShadows as entry (entry.index)}
|
|
312
|
+
{@const layer = entry.layer}
|
|
313
|
+
{@const suffix = `s${entry.index}`}
|
|
314
|
+
{#if layer.inset}
|
|
315
|
+
{@const eroded = layer.spread !== 0}
|
|
316
|
+
{@const shifted = layer.x !== 0 || layer.y !== 0}
|
|
317
|
+
{@const softened = layer.blur > 0}
|
|
318
|
+
{#if eroded}
|
|
319
|
+
<feMorphology
|
|
320
|
+
in="bin"
|
|
321
|
+
operator={layer.spread > 0 ? 'erode' : 'dilate'}
|
|
322
|
+
radius={Math.abs(layer.spread)}
|
|
323
|
+
result={`${suffix}-er`}
|
|
324
|
+
/>
|
|
325
|
+
{/if}
|
|
326
|
+
{#if shifted}
|
|
327
|
+
<feOffset
|
|
328
|
+
in={softened ? eroded ? `${suffix}-er` : 'bin' : eroded ? `${suffix}-er` : 'bin'}
|
|
329
|
+
dx={layer.x}
|
|
330
|
+
dy={layer.y}
|
|
331
|
+
result={`${suffix}-o`}
|
|
332
|
+
/>
|
|
333
|
+
{/if}
|
|
334
|
+
{#if softened}
|
|
335
|
+
<feGaussianBlur
|
|
336
|
+
in={shifted ? `${suffix}-o` : eroded ? `${suffix}-er` : 'bin'}
|
|
337
|
+
stdDeviation={layer.blur / 2}
|
|
338
|
+
result={`${suffix}-b`}
|
|
339
|
+
/>
|
|
340
|
+
{/if}
|
|
341
|
+
{@const shapeSource = softened
|
|
342
|
+
? `${suffix}-b`
|
|
343
|
+
: shifted
|
|
344
|
+
? `${suffix}-o`
|
|
345
|
+
: eroded
|
|
346
|
+
? `${suffix}-er`
|
|
347
|
+
: 'bin'}
|
|
348
|
+
<feComposite in="bin" in2={shapeSource} operator="out" result={`${suffix}-band`} />
|
|
349
|
+
<feFlood flood-color={layer.color} result={`${suffix}-c`} />
|
|
350
|
+
<feComposite
|
|
351
|
+
in={`${suffix}-c`}
|
|
352
|
+
in2={`${suffix}-band`}
|
|
353
|
+
operator="in"
|
|
354
|
+
result={suffix}
|
|
355
|
+
/>
|
|
356
|
+
{:else}
|
|
357
|
+
{@const spread = layer.spread !== 0}
|
|
358
|
+
{@const softened = layer.blur > 0}
|
|
359
|
+
{@const shifted = layer.x !== 0 || layer.y !== 0}
|
|
360
|
+
{#if spread}
|
|
361
|
+
<feMorphology
|
|
362
|
+
in="bin"
|
|
363
|
+
operator={layer.spread > 0 ? 'dilate' : 'erode'}
|
|
364
|
+
radius={Math.abs(layer.spread)}
|
|
365
|
+
result={`${suffix}-sp`}
|
|
366
|
+
/>
|
|
367
|
+
{/if}
|
|
368
|
+
{#if softened}
|
|
369
|
+
<feGaussianBlur
|
|
370
|
+
in={spread ? `${suffix}-sp` : 'shape'}
|
|
371
|
+
stdDeviation={layer.blur / 2}
|
|
372
|
+
result={`${suffix}-b`}
|
|
373
|
+
/>
|
|
374
|
+
{/if}
|
|
375
|
+
{#if shifted}
|
|
376
|
+
<feOffset
|
|
377
|
+
in={softened ? `${suffix}-b` : spread ? `${suffix}-sp` : 'shape'}
|
|
378
|
+
dx={layer.x}
|
|
379
|
+
dy={layer.y}
|
|
380
|
+
result={`${suffix}-o`}
|
|
381
|
+
/>
|
|
382
|
+
{/if}
|
|
383
|
+
{@const shadowSource = shifted
|
|
384
|
+
? `${suffix}-o`
|
|
385
|
+
: softened
|
|
386
|
+
? `${suffix}-b`
|
|
387
|
+
: spread
|
|
388
|
+
? `${suffix}-sp`
|
|
389
|
+
: 'shape'}
|
|
390
|
+
<feFlood flood-color={layer.color} result={`${suffix}-c`} />
|
|
391
|
+
<feComposite
|
|
392
|
+
in={`${suffix}-c`}
|
|
393
|
+
in2={shadowSource}
|
|
394
|
+
operator="in"
|
|
395
|
+
result={suffix}
|
|
396
|
+
/>
|
|
397
|
+
{/if}
|
|
398
|
+
{/each}
|
|
399
|
+
{#if svgShadows.length > 0}
|
|
400
|
+
<feMerge>
|
|
401
|
+
{#each svgShadows.filter(({ layer }) => !layer.inset).slice().reverse() as entry (entry.index)}
|
|
402
|
+
<feMergeNode in={`s${entry.index}`} />
|
|
403
|
+
{/each}
|
|
404
|
+
<feMergeNode in="shape" />
|
|
405
|
+
{#each svgShadows.filter(({ layer }) => layer.inset) as entry (entry.index)}
|
|
406
|
+
<feMergeNode in={`s${entry.index}`} />
|
|
407
|
+
{/each}
|
|
408
|
+
</feMerge>
|
|
409
|
+
{/if}
|
|
410
|
+
</filter>
|
|
411
|
+
</defs>
|
|
412
|
+
<g id={`${filterId}-sil`} filter={`url(#${filterId})`} style={`fill:${fill};`}>
|
|
413
|
+
{#each itemBoxes as box, index (index)}
|
|
414
|
+
{#if box}
|
|
415
|
+
<rect
|
|
416
|
+
bind:this={blobElements[index]}
|
|
417
|
+
x={box.x}
|
|
418
|
+
y={box.y}
|
|
419
|
+
width={box.w}
|
|
420
|
+
height={box.h}
|
|
421
|
+
rx={box.r[0] === box.r[1] && box.r[1] === box.r[2] && box.r[2] === box.r[3]
|
|
422
|
+
? Math.max(0, Math.min(box.r[0], Math.min(box.w, box.h) / 2))
|
|
423
|
+
: undefined}
|
|
424
|
+
style={`transform:translate(${positions[index].x}px,${positions[index].y}px);transform-box:fill-box;transform-origin:center;will-change:transform;`}
|
|
425
|
+
/>
|
|
426
|
+
{/if}
|
|
427
|
+
{/each}
|
|
428
|
+
</g>
|
|
429
|
+
</svg>
|
|
430
|
+
|
|
431
|
+
{#each satellites as satellite, index (satellite.label)}
|
|
432
|
+
<div bind:this={itemWrappers[index]} class="pm-slot" style="display:inline-block;will-change:transform;">
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
class="pm-btn pm-sat"
|
|
436
|
+
aria-label={satellite.label}
|
|
437
|
+
tabindex={menuOpen ? 0 : -1}
|
|
438
|
+
onclick={toggle}
|
|
439
|
+
>
|
|
440
|
+
<span
|
|
441
|
+
class="pm-sat-icon"
|
|
442
|
+
style:transition-delay={menuOpen
|
|
443
|
+
? `${menuSettings.iconDelay + index * phase.stagger}ms`
|
|
444
|
+
: '0ms'}
|
|
445
|
+
>
|
|
446
|
+
{#if satellite.icon === 'file'}
|
|
447
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
448
|
+
<path d="M9 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V6z" />
|
|
449
|
+
<path d="M9 1.5V6h4.5" />
|
|
450
|
+
</svg>
|
|
451
|
+
{:else if satellite.icon === 'image'}
|
|
452
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
453
|
+
<rect x="1.5" y="1.5" width="13" height="13" rx="2" />
|
|
454
|
+
<circle cx="5.5" cy="5.5" r="1.25" />
|
|
455
|
+
<path d="M14.5 10.5L11 7l-7.5 7.5" />
|
|
456
|
+
</svg>
|
|
457
|
+
{:else}
|
|
458
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
459
|
+
<path d="M14.5 12.5A1.5 1.5 0 0 1 13 14H3a1.5 1.5 0 0 1-1.5-1.5V3A1.5 1.5 0 0 1 3 1.5h3L7.5 4H13a1.5 1.5 0 0 1 1.5 1.5z" />
|
|
460
|
+
</svg>
|
|
461
|
+
{/if}
|
|
462
|
+
</span>
|
|
463
|
+
</button>
|
|
464
|
+
</div>
|
|
465
|
+
{/each}
|
|
466
|
+
|
|
467
|
+
<div bind:this={itemWrappers[satellites.length]} class="pm-slot" style="display:inline-block;will-change:transform;">
|
|
468
|
+
<button
|
|
469
|
+
type="button"
|
|
470
|
+
class="pm-btn pm-main"
|
|
471
|
+
aria-expanded={menuOpen}
|
|
472
|
+
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
|
473
|
+
onclick={toggle}
|
|
474
|
+
>
|
|
475
|
+
<span class="pm-plus">
|
|
476
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round">
|
|
477
|
+
<path d="M10 4V16M4 10H16" />
|
|
478
|
+
</svg>
|
|
479
|
+
</span>
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
{/snippet}
|
|
484
|
+
|
|
485
|
+
{#if bare}
|
|
486
|
+
{@render menuStage()}
|
|
487
|
+
{:else}
|
|
488
|
+
<div class="pm-wrap">
|
|
489
|
+
<div class="stage">{@render menuStage()}</div>
|
|
490
|
+
|
|
491
|
+
{#if !pro}
|
|
492
|
+
<div class="cp-panel">
|
|
493
|
+
<div class="cp-section">
|
|
494
|
+
<div class="cp-section-head">
|
|
495
|
+
<span class="cp-section-title">Morph</span>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="cp-row">
|
|
498
|
+
<span class="cp-label">Duration (ms)</span>
|
|
499
|
+
<span class="cp-slider">
|
|
500
|
+
<input type="range" min="80" max="1200" step="10" value={menuSettings.openDur} oninput={(event) => updateSetting('openDur', Number(event.currentTarget.value))} />
|
|
501
|
+
<span class="cp-val">{menuSettings.openDur}</span>
|
|
502
|
+
</span>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="cp-row">
|
|
505
|
+
<span class="cp-label">Stagger (ms)</span>
|
|
506
|
+
<span class="cp-slider">
|
|
507
|
+
<input type="range" min="0" max="200" step="5" value={menuSettings.openStagger} oninput={(event) => updateSetting('openStagger', Number(event.currentTarget.value))} />
|
|
508
|
+
<span class="cp-val">{menuSettings.openStagger}</span>
|
|
509
|
+
</span>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
{:else}
|
|
514
|
+
<div class="cp-panel">
|
|
515
|
+
<div class="cp-section">
|
|
516
|
+
<div class="cp-section-head">
|
|
517
|
+
<span class="cp-section-title">Menu controls</span>
|
|
518
|
+
<span class="cp-actions">
|
|
519
|
+
<button type="button" class={`cp-export${copied ? ' is-copied' : ''}`} onclick={copyValues}>{copied ? 'Copied' : 'Copy values'}</button>
|
|
520
|
+
<button type="button" class="cp-export" onclick={() => (menuSettings = { ...defaults })}>Reset</button>
|
|
521
|
+
</span>
|
|
522
|
+
</div>
|
|
523
|
+
<div class="cp-subhead">Open</div>
|
|
524
|
+
<div class="cp-row">
|
|
525
|
+
<span class="cp-label">Duration (ms)</span>
|
|
526
|
+
<span class="cp-slider"><input type="range" min="80" max="1200" step="10" value={menuSettings.openDur} oninput={(event) => updateSetting('openDur', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.openDur}</span></span>
|
|
527
|
+
</div>
|
|
528
|
+
<div class="cp-row">
|
|
529
|
+
<span class="cp-label">Easing</span>
|
|
530
|
+
<select class="cp-ease" value={menuSettings.openEase} onchange={(event) => updateSetting('openEase', event.currentTarget.value)}>
|
|
531
|
+
{#each easeNames as name}<option>{name}</option>{/each}
|
|
532
|
+
</select>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="cp-row">
|
|
535
|
+
<span class="cp-label">Stagger (ms)</span>
|
|
536
|
+
<span class="cp-slider"><input type="range" min="0" max="200" step="5" value={menuSettings.openStagger} oninput={(event) => updateSetting('openStagger', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.openStagger}</span></span>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="cp-subhead">Close</div>
|
|
539
|
+
<div class="cp-row">
|
|
540
|
+
<span class="cp-label">Duration (ms)</span>
|
|
541
|
+
<span class="cp-slider"><input type="range" min="80" max="1200" step="10" value={menuSettings.closeDur} oninput={(event) => updateSetting('closeDur', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.closeDur}</span></span>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="cp-row">
|
|
544
|
+
<span class="cp-label">Easing</span>
|
|
545
|
+
<select class="cp-ease" value={menuSettings.closeEase} onchange={(event) => updateSetting('closeEase', event.currentTarget.value)}>
|
|
546
|
+
{#each easeNames as name}<option>{name}</option>{/each}
|
|
547
|
+
</select>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="cp-row">
|
|
550
|
+
<span class="cp-label">Stagger (ms)</span>
|
|
551
|
+
<span class="cp-slider"><input type="range" min="0" max="200" step="5" value={menuSettings.closeStagger} oninput={(event) => updateSetting('closeStagger', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.closeStagger}</span></span>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="cp-subhead">Anticipation (on close)</div>
|
|
554
|
+
<div class="cp-row">
|
|
555
|
+
<span class="cp-label">Distance (px)</span>
|
|
556
|
+
<span class="cp-slider"><input type="range" min="0" max="24" step="1" value={menuSettings.anticipDist} oninput={(event) => updateSetting('anticipDist', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.anticipDist}</span></span>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="cp-row">
|
|
559
|
+
<span class="cp-label">Duration (ms)</span>
|
|
560
|
+
<span class="cp-slider"><input type="range" min="120" max="1400" step="20" value={menuSettings.anticipDur} oninput={(event) => updateSetting('anticipDur', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.anticipDur}</span></span>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="cp-subhead">Layout & icons</div>
|
|
563
|
+
<div class="cp-row">
|
|
564
|
+
<span class="cp-label">Spread</span>
|
|
565
|
+
<span class="cp-slider"><input type="range" min="0.4" max="2" step="0.05" value={menuSettings.spread} oninput={(event) => updateSetting('spread', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.spread}</span></span>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="cp-row">
|
|
568
|
+
<span class="cp-label">Icon fade (ms)</span>
|
|
569
|
+
<span class="cp-slider"><input type="range" min="0" max="600" step="10" value={menuSettings.iconDur} oninput={(event) => updateSetting('iconDur', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.iconDur}</span></span>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="cp-row">
|
|
572
|
+
<span class="cp-label">Icon delay (ms)</span>
|
|
573
|
+
<span class="cp-slider"><input type="range" min="0" max="600" step="10" value={menuSettings.iconDelay} oninput={(event) => updateSetting('iconDelay', Number(event.currentTarget.value))} /><span class="cp-val">{menuSettings.iconDelay}</span></span>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
{/if}
|
|
578
|
+
</div>
|
|
579
|
+
{/if}
|
|
580
|
+
|
|
581
|
+
<style>
|
|
582
|
+
.stage {
|
|
583
|
+
display: grid;
|
|
584
|
+
place-items: center;
|
|
585
|
+
height: 280px;
|
|
586
|
+
background: var(--stage-bg, rgba(225, 225, 225, 0.3));
|
|
587
|
+
border-radius: 10px;
|
|
588
|
+
overflow: hidden;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.pm {
|
|
592
|
+
width: 200px;
|
|
593
|
+
height: 140px;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.pm-slot {
|
|
597
|
+
position: absolute;
|
|
598
|
+
left: 80px;
|
|
599
|
+
top: 80px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.pm-btn {
|
|
603
|
+
width: 40px;
|
|
604
|
+
height: 40px;
|
|
605
|
+
border: 0;
|
|
606
|
+
padding: 0;
|
|
607
|
+
border-radius: 50%;
|
|
608
|
+
background: transparent;
|
|
609
|
+
display: grid;
|
|
610
|
+
place-items: center;
|
|
611
|
+
cursor: pointer;
|
|
612
|
+
color: var(--btn-text, #17181c);
|
|
613
|
+
-webkit-tap-highlight-color: transparent;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.pm-btn:focus-visible {
|
|
617
|
+
outline: 2px solid var(--accent, #17181c);
|
|
618
|
+
outline-offset: 2px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.pm-sat { pointer-events: none; }
|
|
622
|
+
.pm-open .pm-sat { pointer-events: auto; }
|
|
623
|
+
.pm-open .pm-sat:hover { background: var(--hover-veil, rgba(0, 0, 0, 0.04)); }
|
|
624
|
+
|
|
625
|
+
.pm-sat-icon {
|
|
626
|
+
display: grid;
|
|
627
|
+
place-items: center;
|
|
628
|
+
opacity: 0;
|
|
629
|
+
filter: blur(2px);
|
|
630
|
+
transition: opacity 120ms ease, filter 120ms ease;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.pm-open .pm-sat-icon {
|
|
634
|
+
opacity: 1;
|
|
635
|
+
filter: blur(0);
|
|
636
|
+
transition-duration: var(--pm-icon-dur, 180ms);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@keyframes pm-anticipate {
|
|
640
|
+
0% { transform: translateY(0); }
|
|
641
|
+
30% { transform: translateY(var(--pm-anticip, 5px)); }
|
|
642
|
+
100% { transform: translateY(0); }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.pm-anticipating [data-gooey-svg],
|
|
646
|
+
.pm-anticipating .pm-main {
|
|
647
|
+
animation-name: pm-anticipate;
|
|
648
|
+
animation-duration: var(--pm-anticip-dur, 700ms);
|
|
649
|
+
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
650
|
+
animation-fill-mode: both;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.pm-wrap { display: flex; flex-direction: column; }
|
|
654
|
+
|
|
655
|
+
.pm-plus {
|
|
656
|
+
display: grid;
|
|
657
|
+
place-items: center;
|
|
658
|
+
transform: rotate(0deg);
|
|
659
|
+
transition: transform 250ms ease-in-out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.pm-open .pm-plus { transform: rotate(45deg); }
|
|
663
|
+
|
|
664
|
+
.cp-panel {
|
|
665
|
+
margin-top: 14px;
|
|
666
|
+
background: var(--panel-bg);
|
|
667
|
+
border: 1px solid var(--panel-border);
|
|
668
|
+
border-radius: 12px;
|
|
669
|
+
-webkit-font-smoothing: antialiased;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.cp-section { padding: 14px 16px 16px; }
|
|
673
|
+
.cp-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
674
|
+
.cp-section-title {
|
|
675
|
+
font-family: 'Roboto Mono', ui-monospace, monospace;
|
|
676
|
+
font-size: 11px;
|
|
677
|
+
font-weight: 500;
|
|
678
|
+
color: var(--text);
|
|
679
|
+
text-transform: uppercase;
|
|
680
|
+
letter-spacing: 0.06em;
|
|
681
|
+
opacity: 0.72;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.cp-export {
|
|
685
|
+
appearance: none;
|
|
686
|
+
border: 0;
|
|
687
|
+
background: var(--accent);
|
|
688
|
+
color: var(--on-accent);
|
|
689
|
+
font-family: inherit;
|
|
690
|
+
font-size: 11px;
|
|
691
|
+
font-weight: 500;
|
|
692
|
+
height: 22px;
|
|
693
|
+
padding: 0 10px;
|
|
694
|
+
border-radius: 5px;
|
|
695
|
+
cursor: pointer;
|
|
696
|
+
white-space: nowrap;
|
|
697
|
+
flex: none;
|
|
698
|
+
transition: background-color 120ms ease;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.cp-export:hover { background: var(--accent); opacity: 0.82; }
|
|
702
|
+
.cp-export.is-copied { background: #178a4a; }
|
|
703
|
+
.cp-actions { display: flex; align-items: center; gap: 6px; }
|
|
704
|
+
.cp-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px; padding: 3px 0; }
|
|
705
|
+
.cp-subhead { margin: 14px 0 2px; font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--label); }
|
|
706
|
+
.cp-subhead:first-of-type { margin-top: 4px; }
|
|
707
|
+
.cp-label { color: var(--muted); font-size: 11.5px; line-height: 1.3; font-weight: 400; user-select: none; }
|
|
708
|
+
|
|
709
|
+
.cp-row input,
|
|
710
|
+
.cp-row select {
|
|
711
|
+
width: 86px;
|
|
712
|
+
height: 24px;
|
|
713
|
+
padding: 0 6px;
|
|
714
|
+
border: 1px solid var(--ctl-border);
|
|
715
|
+
border-radius: 4px;
|
|
716
|
+
font-family: 'Roboto Mono', ui-monospace, monospace;
|
|
717
|
+
font-size: 11px;
|
|
718
|
+
color: var(--text);
|
|
719
|
+
background: var(--field-bg);
|
|
720
|
+
text-align: right;
|
|
721
|
+
-moz-appearance: textfield;
|
|
722
|
+
appearance: textfield;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.cp-row input:focus,
|
|
726
|
+
.cp-row select:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
|
|
727
|
+
.cp-slider { display: inline-flex; align-items: center; gap: 8px; }
|
|
728
|
+
.cp-slider input[type='range'] { width: 110px; height: 24px; margin: 0; padding: 0; border: 0; background: transparent; accent-color: var(--accent); }
|
|
729
|
+
.cp-slider .cp-val { font-family: 'Roboto Mono', ui-monospace, monospace; font-size: 11px; color: var(--text); min-width: 34px; text-align: right; }
|
|
730
|
+
.cp-row select.cp-ease {
|
|
731
|
+
width: 128px;
|
|
732
|
+
text-align: left;
|
|
733
|
+
padding-right: 4px;
|
|
734
|
+
cursor: pointer;
|
|
735
|
+
appearance: none;
|
|
736
|
+
-webkit-appearance: none;
|
|
737
|
+
background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
|
|
738
|
+
background-position: calc(100% - 10px) 10px, calc(100% - 6px) 10px;
|
|
739
|
+
background-size: 4px 4px, 4px 4px;
|
|
740
|
+
background-repeat: no-repeat;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
@media (prefers-reduced-motion: reduce) {
|
|
744
|
+
.pm-plus, .pm-sat-icon { transition: none !important; }
|
|
745
|
+
.pm-anticipating [data-gooey-svg], .pm-anticipating .pm-main { animation: none !important; }
|
|
746
|
+
}
|
|
747
|
+
</style>
|