fancy-ui-svelte 0.4.1 → 0.6.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 +40 -7
- package/dist/fancy-ui/dock/index.d.ts +1 -1
- package/dist/fancy-ui/dock/index.js +0 -1
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +181 -0
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte.d.ts +26 -0
- package/dist/fancy-ui/editorial-engine/README.md +56 -0
- package/dist/fancy-ui/editorial-engine/engine.d.ts +7 -0
- package/dist/fancy-ui/editorial-engine/engine.js +581 -0
- package/dist/fancy-ui/editorial-engine/index.d.ts +2 -0
- package/dist/fancy-ui/editorial-engine/index.js +1 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +168 -32
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +7 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +36 -9
- package/dist/fancy-ui/fluid-cursor/index.d.ts +4 -1
- package/dist/fancy-ui/fluid-cursor/index.js +4 -1
- package/dist/fancy-ui/index.d.ts +4 -0
- package/dist/fancy-ui/index.js +4 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte +168 -0
- package/dist/fancy-ui/line-reveal/LineReveal.svelte.d.ts +33 -0
- package/dist/fancy-ui/line-reveal/README.md +42 -0
- package/dist/fancy-ui/line-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/line-reveal/index.js +1 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte +323 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.svelte.d.ts +19 -0
- package/dist/fancy-ui/noise-reveal/NoiseReveal.test.js +123 -0
- package/dist/fancy-ui/noise-reveal/index.d.ts +2 -0
- package/dist/fancy-ui/noise-reveal/index.js +1 -0
- package/dist/fancy-ui/registry.js +1589 -9
- package/dist/fancy-ui/shimmer-button/ShimmerButton.svelte +1 -1
- package/dist/fancy-ui/tailwind.css +2 -2
- package/dist/types.d.ts +37 -0
- package/package.json +10 -2
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.svelte +0 -1556
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.svelte.d.ts +0 -33
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.test.js +0 -86
- /package/dist/fancy-ui/{fluid-cursor/FluidCursorAdvanced.test.d.ts → noise-reveal/NoiseReveal.test.d.ts} +0 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { cn } from "../../utils.js";
|
|
3
3
|
import { onMount } from "svelte";
|
|
4
4
|
|
|
5
|
+
// Module-level singleton registry
|
|
6
|
+
let activeInstance: (() => void) | null = null;
|
|
7
|
+
|
|
5
8
|
interface ColorRGB {
|
|
6
9
|
r: number;
|
|
7
10
|
g: number;
|
|
@@ -27,6 +30,13 @@
|
|
|
27
30
|
fluidColors?: string[];
|
|
28
31
|
colorIntensity?: number;
|
|
29
32
|
class?: string;
|
|
33
|
+
autoSplat?: boolean;
|
|
34
|
+
autoSplatInterval?: number;
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
pauseWhenHidden?: boolean;
|
|
37
|
+
splatOnMount?: boolean;
|
|
38
|
+
allowMultiple?: boolean;
|
|
39
|
+
contained?: boolean;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
let {
|
|
@@ -48,6 +58,13 @@
|
|
|
48
58
|
fluidColors,
|
|
49
59
|
colorIntensity = 0.15,
|
|
50
60
|
class: className = "",
|
|
61
|
+
autoSplat = false,
|
|
62
|
+
autoSplatInterval = 1500,
|
|
63
|
+
interactive = true,
|
|
64
|
+
pauseWhenHidden = true,
|
|
65
|
+
splatOnMount = false,
|
|
66
|
+
allowMultiple = false,
|
|
67
|
+
contained = true,
|
|
51
68
|
}: Props = $props();
|
|
52
69
|
|
|
53
70
|
function hexToRgb(hex: string): ColorRGB {
|
|
@@ -956,6 +973,28 @@
|
|
|
956
973
|
return Math.floor(input * pixelRatio);
|
|
957
974
|
}
|
|
958
975
|
|
|
976
|
+
let canvasRectCache: DOMRect | null = null;
|
|
977
|
+
function updateCanvasRectCache() {
|
|
978
|
+
if (contained && canvas) canvasRectCache = canvas.getBoundingClientRect();
|
|
979
|
+
}
|
|
980
|
+
if (contained) {
|
|
981
|
+
window.addEventListener("resize", updateCanvasRectCache, { passive: true });
|
|
982
|
+
window.addEventListener("scroll", updateCanvasRectCache, { passive: true });
|
|
983
|
+
updateCanvasRectCache();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function getCanvasPos(clientX: number, clientY: number): { x: number; y: number } {
|
|
987
|
+
if (contained) {
|
|
988
|
+
if (!canvasRectCache) updateCanvasRectCache();
|
|
989
|
+
const rect = canvasRectCache!;
|
|
990
|
+
return {
|
|
991
|
+
x: scaleByPixelRatio(clientX - rect.left),
|
|
992
|
+
y: scaleByPixelRatio(clientY - rect.top),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return { x: scaleByPixelRatio(clientX), y: scaleByPixelRatio(clientY) };
|
|
996
|
+
}
|
|
997
|
+
|
|
959
998
|
// Simulation Setup
|
|
960
999
|
updateKeywords();
|
|
961
1000
|
initFramebuffers();
|
|
@@ -996,7 +1035,10 @@
|
|
|
996
1035
|
return cachedFluidColorsScaled;
|
|
997
1036
|
}
|
|
998
1037
|
|
|
1038
|
+
let isVisible = true;
|
|
1039
|
+
|
|
999
1040
|
function updateFrame() {
|
|
1041
|
+
if (!isVisible) return;
|
|
1000
1042
|
const dt = calcDeltaTime();
|
|
1001
1043
|
if (resizeCanvas()) initFramebuffers();
|
|
1002
1044
|
updateColors(dt);
|
|
@@ -1247,6 +1289,36 @@
|
|
|
1247
1289
|
dye.swap();
|
|
1248
1290
|
}
|
|
1249
1291
|
|
|
1292
|
+
function multipleSplats(steps: number) {
|
|
1293
|
+
// Simulate a cursor sweep along a random arc — each step on its own frame
|
|
1294
|
+
// so the velocity field has time to evolve between injections.
|
|
1295
|
+
const angle = Math.random() * Math.PI * 2;
|
|
1296
|
+
const cx = 0.25 + Math.random() * 0.5;
|
|
1297
|
+
const cy = 0.25 + Math.random() * 0.5;
|
|
1298
|
+
const arcSpan = Math.PI * (0.8 + Math.random() * 0.8);
|
|
1299
|
+
let i = 0;
|
|
1300
|
+
|
|
1301
|
+
function step() {
|
|
1302
|
+
if (i >= steps) return;
|
|
1303
|
+
const t = i / Math.max(1, steps - 1);
|
|
1304
|
+
const a = angle + t * arcSpan;
|
|
1305
|
+
const r = 0.15 + 0.05 * Math.sin(t * Math.PI);
|
|
1306
|
+
const x = cx + Math.cos(a) * r;
|
|
1307
|
+
const y = cy + Math.sin(a) * r;
|
|
1308
|
+
const color = generateColor();
|
|
1309
|
+
splat(
|
|
1310
|
+
x,
|
|
1311
|
+
y,
|
|
1312
|
+
-Math.sin(a) * config.SPLAT_FORCE * 0.5,
|
|
1313
|
+
Math.cos(a) * config.SPLAT_FORCE * 0.5,
|
|
1314
|
+
color
|
|
1315
|
+
);
|
|
1316
|
+
i++;
|
|
1317
|
+
if (i < steps) requestAnimationFrame(step);
|
|
1318
|
+
}
|
|
1319
|
+
requestAnimationFrame(step);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1250
1322
|
function correctRadius(radius: number) {
|
|
1251
1323
|
const aspectRatio = canvas.width / canvas.height;
|
|
1252
1324
|
if (aspectRatio > 1) radius *= aspectRatio;
|
|
@@ -1360,37 +1432,30 @@
|
|
|
1360
1432
|
// Event Listeners
|
|
1361
1433
|
function handleMouseDown(e: MouseEvent) {
|
|
1362
1434
|
const pointer = pointers[0];
|
|
1363
|
-
const posX =
|
|
1364
|
-
const posY = scaleByPixelRatio(e.clientY);
|
|
1435
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1365
1436
|
updatePointerDownData(pointer, -1, posX, posY);
|
|
1366
1437
|
clickSplat(pointer);
|
|
1367
1438
|
}
|
|
1368
1439
|
|
|
1369
1440
|
function handleFirstMouseMove(e: MouseEvent) {
|
|
1370
1441
|
const pointer = pointers[0];
|
|
1371
|
-
const posX =
|
|
1372
|
-
const posY = scaleByPixelRatio(e.clientY);
|
|
1442
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1373
1443
|
const color = generateColor();
|
|
1374
|
-
updateFrame();
|
|
1375
1444
|
updatePointerMoveData(pointer, posX, posY, color);
|
|
1376
1445
|
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1377
1446
|
}
|
|
1378
1447
|
|
|
1379
1448
|
function handleMouseMove(e: MouseEvent) {
|
|
1380
1449
|
const pointer = pointers[0];
|
|
1381
|
-
const posX =
|
|
1382
|
-
|
|
1383
|
-
const color = pointer.color;
|
|
1384
|
-
updatePointerMoveData(pointer, posX, posY, color);
|
|
1450
|
+
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
1451
|
+
updatePointerMoveData(pointer, posX, posY, pointer.color);
|
|
1385
1452
|
}
|
|
1386
1453
|
|
|
1387
1454
|
function handleFirstTouchStart(e: TouchEvent) {
|
|
1388
1455
|
const touches = e.targetTouches;
|
|
1389
1456
|
const pointer = pointers[0];
|
|
1390
1457
|
for (let i = 0; i < touches.length; i++) {
|
|
1391
|
-
const posX =
|
|
1392
|
-
const posY = scaleByPixelRatio(touches[i].clientY);
|
|
1393
|
-
updateFrame();
|
|
1458
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1394
1459
|
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
|
|
1395
1460
|
}
|
|
1396
1461
|
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
@@ -1400,8 +1465,7 @@
|
|
|
1400
1465
|
const touches = e.targetTouches;
|
|
1401
1466
|
const pointer = pointers[0];
|
|
1402
1467
|
for (let i = 0; i < touches.length; i++) {
|
|
1403
|
-
const posX =
|
|
1404
|
-
const posY = scaleByPixelRatio(touches[i].clientY);
|
|
1468
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1405
1469
|
updatePointerDownData(pointer, touches[i].identifier, posX, posY);
|
|
1406
1470
|
}
|
|
1407
1471
|
}
|
|
@@ -1410,36 +1474,108 @@
|
|
|
1410
1474
|
const touches = e.targetTouches;
|
|
1411
1475
|
const pointer = pointers[0];
|
|
1412
1476
|
for (let i = 0; i < touches.length; i++) {
|
|
1413
|
-
const posX =
|
|
1414
|
-
const posY = scaleByPixelRatio(touches[i].clientY);
|
|
1477
|
+
const { x: posX, y: posY } = getCanvasPos(touches[i].clientX, touches[i].clientY);
|
|
1415
1478
|
updatePointerMoveData(pointer, posX, posY, pointer.color);
|
|
1416
1479
|
}
|
|
1417
1480
|
}
|
|
1418
1481
|
|
|
1419
1482
|
// Add event listeners
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1483
|
+
if (interactive) {
|
|
1484
|
+
window.addEventListener("mousedown", handleMouseDown);
|
|
1485
|
+
document.body.addEventListener("mousemove", handleFirstMouseMove);
|
|
1486
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
1487
|
+
document.body.addEventListener("touchstart", handleFirstTouchStart);
|
|
1488
|
+
window.addEventListener("touchstart", handleTouchStart, false);
|
|
1489
|
+
window.addEventListener("touchmove", handleTouchMove, false);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Pause animation when scrolled out of view
|
|
1493
|
+
let observer: IntersectionObserver | null = null;
|
|
1494
|
+
if (pauseWhenHidden) {
|
|
1495
|
+
observer = new IntersectionObserver(
|
|
1496
|
+
([entry]) => {
|
|
1497
|
+
const wasVisible = isVisible;
|
|
1498
|
+
isVisible = entry.isIntersecting;
|
|
1499
|
+
if (isVisible && !wasVisible) {
|
|
1500
|
+
lastUpdateTime = Date.now();
|
|
1501
|
+
animationFrameId = requestAnimationFrame(updateFrame);
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
{ threshold: 0 }
|
|
1505
|
+
);
|
|
1506
|
+
observer.observe(canvas);
|
|
1507
|
+
}
|
|
1426
1508
|
|
|
1427
1509
|
// Start animation
|
|
1428
1510
|
updateFrame();
|
|
1511
|
+
if (splatOnMount) multipleSplats(Math.floor(Math.random() * 6) + 10);
|
|
1512
|
+
|
|
1513
|
+
// Auto-splat timer
|
|
1514
|
+
let autoSplatTimer: ReturnType<typeof setInterval> | null = null;
|
|
1515
|
+
if (autoSplat) {
|
|
1516
|
+
autoSplatTimer = setInterval(() => {
|
|
1517
|
+
const color = generateColor();
|
|
1518
|
+
splat(
|
|
1519
|
+
Math.random(),
|
|
1520
|
+
Math.random(),
|
|
1521
|
+
(Math.random() - 0.5) * 200,
|
|
1522
|
+
(Math.random() - 0.5) * 200,
|
|
1523
|
+
color
|
|
1524
|
+
);
|
|
1525
|
+
}, autoSplatInterval);
|
|
1526
|
+
}
|
|
1429
1527
|
|
|
1430
1528
|
// Cleanup
|
|
1431
|
-
|
|
1529
|
+
function cleanup() {
|
|
1432
1530
|
cancelAnimationFrame(animationFrameId);
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1531
|
+
if (observer) observer.disconnect();
|
|
1532
|
+
if (autoSplatTimer) clearInterval(autoSplatTimer);
|
|
1533
|
+
if (interactive) {
|
|
1534
|
+
window.removeEventListener("mousedown", handleMouseDown);
|
|
1535
|
+
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1536
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
1537
|
+
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
1538
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
1539
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
1540
|
+
}
|
|
1541
|
+
if (contained) {
|
|
1542
|
+
window.removeEventListener("resize", updateCanvasRectCache);
|
|
1543
|
+
window.removeEventListener("scroll", updateCanvasRectCache);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (!allowMultiple) {
|
|
1548
|
+
if (activeInstance) {
|
|
1549
|
+
if (import.meta.env.DEV) {
|
|
1550
|
+
console.warn(
|
|
1551
|
+
"[FluidCursor] Destroying previous instance. Only one instance is allowed by default. Use `allowMultiple={true}` to opt out of singleton behavior."
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
activeInstance();
|
|
1555
|
+
}
|
|
1556
|
+
activeInstance = cleanup;
|
|
1557
|
+
return () => {
|
|
1558
|
+
cleanup();
|
|
1559
|
+
if (activeInstance === cleanup) {
|
|
1560
|
+
activeInstance = null;
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return cleanup;
|
|
1440
1566
|
});
|
|
1441
1567
|
</script>
|
|
1442
1568
|
|
|
1443
|
-
<div
|
|
1444
|
-
|
|
1569
|
+
<div
|
|
1570
|
+
class={cn(
|
|
1571
|
+
contained
|
|
1572
|
+
? "pointer-events-none absolute inset-0 h-full w-full"
|
|
1573
|
+
: "pointer-events-none fixed top-0 left-0 z-50 size-full",
|
|
1574
|
+
className
|
|
1575
|
+
)}
|
|
1576
|
+
>
|
|
1577
|
+
<canvas
|
|
1578
|
+
bind:this={canvasRef}
|
|
1579
|
+
class={contained ? "block h-full w-full" : "block h-screen w-screen"}
|
|
1580
|
+
></canvas>
|
|
1445
1581
|
</div>
|
|
@@ -22,6 +22,13 @@ interface Props {
|
|
|
22
22
|
fluidColors?: string[];
|
|
23
23
|
colorIntensity?: number;
|
|
24
24
|
class?: string;
|
|
25
|
+
autoSplat?: boolean;
|
|
26
|
+
autoSplatInterval?: number;
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
pauseWhenHidden?: boolean;
|
|
29
|
+
splatOnMount?: boolean;
|
|
30
|
+
allowMultiple?: boolean;
|
|
31
|
+
contained?: boolean;
|
|
25
32
|
}
|
|
26
33
|
declare const FluidCursor: import("svelte").Component<Props, {}, "">;
|
|
27
34
|
type FluidCursor = ReturnType<typeof FluidCursor>;
|
|
@@ -13,24 +13,27 @@ describe("FluidCursor", () => {
|
|
|
13
13
|
const canvas = container.querySelector("canvas");
|
|
14
14
|
expect(canvas).toBeInTheDocument();
|
|
15
15
|
});
|
|
16
|
-
it('canvas has id="fluid"', () => {
|
|
17
|
-
const { container } = render(FluidCursor);
|
|
18
|
-
const canvas = container.querySelector("canvas#fluid");
|
|
19
|
-
expect(canvas).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
16
|
it("container has pointer-events-none class", () => {
|
|
22
17
|
const { container } = render(FluidCursor);
|
|
23
18
|
const div = container.firstElementChild;
|
|
24
19
|
expect(div?.className).toContain("pointer-events-none");
|
|
25
20
|
});
|
|
26
|
-
it("
|
|
21
|
+
it("defaults to contained mode (absolute positioning)", () => {
|
|
27
22
|
const { container } = render(FluidCursor);
|
|
28
23
|
const div = container.firstElementChild;
|
|
29
|
-
expect(div?.className).toContain("
|
|
24
|
+
expect(div?.className).toContain("absolute");
|
|
25
|
+
expect(div?.className).toContain("inset-0");
|
|
30
26
|
});
|
|
31
|
-
it("container
|
|
27
|
+
it("canvas fills container in contained mode", () => {
|
|
32
28
|
const { container } = render(FluidCursor);
|
|
29
|
+
const canvas = container.querySelector("canvas");
|
|
30
|
+
expect(canvas?.className).toContain("h-full");
|
|
31
|
+
expect(canvas?.className).toContain("w-full");
|
|
32
|
+
});
|
|
33
|
+
it("uses fixed positioning when contained={false}", () => {
|
|
34
|
+
const { container } = render(FluidCursor, { props: { contained: false } });
|
|
33
35
|
const div = container.firstElementChild;
|
|
36
|
+
expect(div?.className).toContain("fixed");
|
|
34
37
|
expect(div?.className).toContain("z-50");
|
|
35
38
|
});
|
|
36
39
|
it("applies custom class names", () => {
|
|
@@ -38,6 +41,31 @@ describe("FluidCursor", () => {
|
|
|
38
41
|
const div = container.firstElementChild;
|
|
39
42
|
expect(div?.className).toContain("my-fluid");
|
|
40
43
|
});
|
|
44
|
+
describe("interaction props", () => {
|
|
45
|
+
it("interactive={false} does not register mouse event listeners", () => {
|
|
46
|
+
const addSpy = vi.spyOn(window, "addEventListener");
|
|
47
|
+
render(FluidCursor, { props: { interactive: false } });
|
|
48
|
+
const mousedownCalls = addSpy.mock.calls.filter(([event]) => event === "mousedown");
|
|
49
|
+
expect(mousedownCalls).toHaveLength(0);
|
|
50
|
+
const mousemoveCalls = addSpy.mock.calls.filter(([event]) => event === "mousemove");
|
|
51
|
+
expect(mousemoveCalls).toHaveLength(0);
|
|
52
|
+
addSpy.mockRestore();
|
|
53
|
+
});
|
|
54
|
+
it("mounts without error with autoSplat={true} and splatOnMount={true}", () => {
|
|
55
|
+
const { container } = render(FluidCursor, {
|
|
56
|
+
props: { autoSplat: true, splatOnMount: true },
|
|
57
|
+
});
|
|
58
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
it("pauseWhenHidden={false} does not instantiate IntersectionObserver", () => {
|
|
61
|
+
const observeSpy = vi.fn();
|
|
62
|
+
const mockObserver = vi.fn(() => ({ observe: observeSpy, disconnect: vi.fn() }));
|
|
63
|
+
vi.stubGlobal("IntersectionObserver", mockObserver);
|
|
64
|
+
render(FluidCursor, { props: { pauseWhenHidden: false } });
|
|
65
|
+
expect(mockObserver).not.toHaveBeenCalled();
|
|
66
|
+
vi.unstubAllGlobals();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
41
69
|
describe("color props", () => {
|
|
42
70
|
it("renders without error when fluidColor is provided", () => {
|
|
43
71
|
const { container } = render(FluidCursor, { props: { fluidColor: "#00ffcc" } });
|
|
@@ -50,7 +78,6 @@ describe("FluidCursor", () => {
|
|
|
50
78
|
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
51
79
|
});
|
|
52
80
|
it("renders without error when fluidColor and fluidColors are both provided (fluidColor takes priority)", () => {
|
|
53
|
-
// Both props accepted without throw — fluidColor priority is exercised at runtime
|
|
54
81
|
const { container } = render(FluidCursor, {
|
|
55
82
|
props: { fluidColor: "#ff0000", fluidColors: ["#00ffcc", "#7700ff"] },
|
|
56
83
|
});
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { default as FluidCursor } from "./FluidCursor.svelte";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Use `FluidCursor` instead. `FluidCursorAdvanced` has been merged into `FluidCursor`.
|
|
4
|
+
*/
|
|
5
|
+
export { default as FluidCursorAdvanced } from "./FluidCursor.svelte";
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { default as FluidCursor } from "./FluidCursor.svelte";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Use `FluidCursor` instead. `FluidCursorAdvanced` has been merged into `FluidCursor`.
|
|
4
|
+
*/
|
|
5
|
+
export { default as FluidCursorAdvanced } from "./FluidCursor.svelte";
|
package/dist/fancy-ui/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./border-beam/index.js";
|
|
|
11
11
|
export * from "./glow-border/index.js";
|
|
12
12
|
export * from "./marquee/index.js";
|
|
13
13
|
export * from "./compare/index.js";
|
|
14
|
+
export * from "./dock/index.js";
|
|
14
15
|
export * from "./image-trail-cursor/index.js";
|
|
15
16
|
export * from "./interactive-grid-pattern/index.js";
|
|
16
17
|
export * from "./interactive-hover-button/index.js";
|
|
@@ -53,4 +54,7 @@ export * from "./tracing-beam/index.js";
|
|
|
53
54
|
export * from "./displacement-text/index.js";
|
|
54
55
|
export * from "./matrix-rain/index.js";
|
|
55
56
|
export * from "./terminal-text/index.js";
|
|
57
|
+
export * from "./noise-reveal/index.js";
|
|
58
|
+
export * from "./line-reveal/index.js";
|
|
59
|
+
export * from "./editorial-engine/index.js";
|
|
56
60
|
export { registry, categories, categoryLabels, categoryDescriptions, getAllComponents, getComponent, getComponentsByStatus, getComponentsByCategory, getComponentsGroupedByCategory, getComponentsGroupedByStatus, searchComponents, getStats, hasComponent, getComponentCategory, } from "./registry.js";
|
package/dist/fancy-ui/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./border-beam/index.js";
|
|
|
16
16
|
export * from "./glow-border/index.js";
|
|
17
17
|
export * from "./marquee/index.js";
|
|
18
18
|
export * from "./compare/index.js";
|
|
19
|
+
export * from "./dock/index.js";
|
|
19
20
|
export * from "./image-trail-cursor/index.js";
|
|
20
21
|
export * from "./interactive-grid-pattern/index.js";
|
|
21
22
|
export * from "./interactive-hover-button/index.js";
|
|
@@ -58,6 +59,9 @@ export * from "./tracing-beam/index.js";
|
|
|
58
59
|
export * from "./displacement-text/index.js";
|
|
59
60
|
export * from "./matrix-rain/index.js";
|
|
60
61
|
export * from "./terminal-text/index.js";
|
|
62
|
+
export * from "./noise-reveal/index.js";
|
|
63
|
+
export * from "./line-reveal/index.js";
|
|
64
|
+
export * from "./editorial-engine/index.js";
|
|
61
65
|
// =============================================================================
|
|
62
66
|
// Registry
|
|
63
67
|
// =============================================================================
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Props for LineReveal
|
|
4
|
+
*
|
|
5
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
6
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
7
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
8
|
+
*/
|
|
9
|
+
export interface LineRevealProps {
|
|
10
|
+
/** Text to reveal */
|
|
11
|
+
text: string;
|
|
12
|
+
/**
|
|
13
|
+
* CSS font shorthand used for both measurement and rendering
|
|
14
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
15
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
16
|
+
* canvas measurement and DOM rendering.
|
|
17
|
+
*/
|
|
18
|
+
font?: string;
|
|
19
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
20
|
+
lineHeight?: number;
|
|
21
|
+
/** Delay between lines in seconds */
|
|
22
|
+
stagger?: number;
|
|
23
|
+
/** Animation duration per line in seconds */
|
|
24
|
+
duration?: number;
|
|
25
|
+
/** Initial delay before the first line in seconds */
|
|
26
|
+
delay?: number;
|
|
27
|
+
/** Animate only the first time the component enters the viewport */
|
|
28
|
+
once?: boolean;
|
|
29
|
+
/** Additional CSS classes */
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { cn } from "../../utils.js";
|
|
36
|
+
import { onMount } from "svelte";
|
|
37
|
+
import {
|
|
38
|
+
prepareWithSegments,
|
|
39
|
+
layoutWithLines,
|
|
40
|
+
type PreparedTextWithSegments,
|
|
41
|
+
} from "@chenglou/pretext";
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
text,
|
|
45
|
+
// Named families only: canvas and CSS can resolve `system-ui` to
|
|
46
|
+
// different fonts (notably on macOS), which skews measured widths.
|
|
47
|
+
font = '600 32px "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
48
|
+
lineHeight,
|
|
49
|
+
stagger = 0.08,
|
|
50
|
+
duration = 0.7,
|
|
51
|
+
delay = 0,
|
|
52
|
+
once = true,
|
|
53
|
+
class: className,
|
|
54
|
+
}: LineRevealProps = $props();
|
|
55
|
+
|
|
56
|
+
const fontSize = $derived(Number(/(\d+(?:\.\d+)?)px/.exec(font)?.[1] ?? 32));
|
|
57
|
+
const resolvedLineHeight = $derived(lineHeight ?? Math.round(fontSize * 1.2));
|
|
58
|
+
|
|
59
|
+
let containerRef: HTMLDivElement;
|
|
60
|
+
let containerWidth = $state(0);
|
|
61
|
+
let revealed = $state(false);
|
|
62
|
+
let prepared = $state<PreparedTextWithSegments | null>(null);
|
|
63
|
+
|
|
64
|
+
// Measurement requires a canvas context, so this only runs in the browser
|
|
65
|
+
// ($effect never runs during SSR). Web fonts must be loaded first or the
|
|
66
|
+
// canvas silently measures with the fallback font.
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const currentText = text;
|
|
69
|
+
const currentFont = font;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
|
|
72
|
+
document.fonts
|
|
73
|
+
.load(currentFont)
|
|
74
|
+
.catch(() => {})
|
|
75
|
+
.then(() => {
|
|
76
|
+
if (!cancelled) {
|
|
77
|
+
prepared = prepareWithSegments(currentText, currentFont);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Pure arithmetic on cached measurements — cheap to re-run on every resize
|
|
87
|
+
// (containerWidth is tracked by Svelte's bind:clientWidth ResizeObserver).
|
|
88
|
+
const layoutResult = $derived(
|
|
89
|
+
prepared && containerWidth > 0
|
|
90
|
+
? layoutWithLines(prepared, containerWidth, resolvedLineHeight)
|
|
91
|
+
: null
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
onMount(() => {
|
|
95
|
+
const observer = new IntersectionObserver(
|
|
96
|
+
(entries) => {
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.isIntersecting) {
|
|
99
|
+
revealed = true;
|
|
100
|
+
if (once) observer.disconnect();
|
|
101
|
+
} else if (!once) {
|
|
102
|
+
revealed = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{ threshold: 0.1 }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
observer.observe(containerRef);
|
|
110
|
+
|
|
111
|
+
return () => observer.disconnect();
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<div
|
|
116
|
+
bind:this={containerRef}
|
|
117
|
+
bind:clientWidth={containerWidth}
|
|
118
|
+
class={cn("relative w-full", className)}
|
|
119
|
+
style:font
|
|
120
|
+
style:line-height="{resolvedLineHeight}px"
|
|
121
|
+
style:height={layoutResult ? `${layoutResult.height}px` : undefined}
|
|
122
|
+
>
|
|
123
|
+
<span class="sr-only">{text}</span>
|
|
124
|
+
{#if layoutResult}
|
|
125
|
+
{#each layoutResult.lines as line, i (i)}
|
|
126
|
+
<div class="line-mask" style:height="{resolvedLineHeight}px" aria-hidden="true">
|
|
127
|
+
<div
|
|
128
|
+
class="line"
|
|
129
|
+
class:revealed
|
|
130
|
+
style:transition-duration="{duration}s"
|
|
131
|
+
style:transition-delay="{delay + i * stagger}s"
|
|
132
|
+
>
|
|
133
|
+
{line.text}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{/each}
|
|
137
|
+
{:else}
|
|
138
|
+
<!-- SSR / pre-measure fallback: keeps text in the HTML and reserves space -->
|
|
139
|
+
<div style:visibility="hidden" aria-hidden="true">{text}</div>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<style>
|
|
144
|
+
.line-mask {
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
/* preserve exact spacing so rendered width matches pretext measurement */
|
|
147
|
+
white-space: pre;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.line {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
transform: translateY(105%);
|
|
153
|
+
transition-property: transform, opacity;
|
|
154
|
+
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.line.revealed {
|
|
158
|
+
opacity: 1;
|
|
159
|
+
transform: translateY(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (prefers-reduced-motion: reduce) {
|
|
163
|
+
.line {
|
|
164
|
+
transition-duration: 0.1s !important;
|
|
165
|
+
transition-delay: 0s !important;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props for LineReveal
|
|
3
|
+
*
|
|
4
|
+
* Reveals text line by line with a staggered slide-up animation.
|
|
5
|
+
* Line breaks are computed with @chenglou/pretext (canvas text
|
|
6
|
+
* measurement), so no DOM splitting or reflow measurement is needed.
|
|
7
|
+
*/
|
|
8
|
+
export interface LineRevealProps {
|
|
9
|
+
/** Text to reveal */
|
|
10
|
+
text: string;
|
|
11
|
+
/**
|
|
12
|
+
* CSS font shorthand used for both measurement and rendering
|
|
13
|
+
* (canvas `ctx.font` format, e.g. "600 32px Inter, sans-serif").
|
|
14
|
+
* Prefer named families: `system-ui` can resolve differently between
|
|
15
|
+
* canvas measurement and DOM rendering.
|
|
16
|
+
*/
|
|
17
|
+
font?: string;
|
|
18
|
+
/** Line height in pixels (defaults to 1.2 × font size) */
|
|
19
|
+
lineHeight?: number;
|
|
20
|
+
/** Delay between lines in seconds */
|
|
21
|
+
stagger?: number;
|
|
22
|
+
/** Animation duration per line in seconds */
|
|
23
|
+
duration?: number;
|
|
24
|
+
/** Initial delay before the first line in seconds */
|
|
25
|
+
delay?: number;
|
|
26
|
+
/** Animate only the first time the component enters the viewport */
|
|
27
|
+
once?: boolean;
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
declare const LineReveal: import("svelte").Component<LineRevealProps, {}, "">;
|
|
32
|
+
type LineReveal = ReturnType<typeof LineReveal>;
|
|
33
|
+
export default LineReveal;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LineReveal
|
|
2
|
+
|
|
3
|
+
Reveals text line by line with a staggered slide-up animation.
|
|
4
|
+
|
|
5
|
+
Unlike DOM-splitting approaches, line breaks are computed with
|
|
6
|
+
[@chenglou/pretext](https://github.com/chenglou/pretext) — canvas-accurate text
|
|
7
|
+
measurement in pure JS. Each computed line is rendered as its own masked
|
|
8
|
+
element, so the per-line stagger works without measuring or mutating the DOM,
|
|
9
|
+
and lines re-wrap instantly on container resize.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { LineReveal } from "$lib/fancy-ui/line-reveal";
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<LineReveal
|
|
19
|
+
text="Text measurement and layout without touching the DOM."
|
|
20
|
+
font="700 40px Helvetica, Arial, sans-serif"
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
| ------------ | --------- | -------------------------------- | ------------------------------------------------------------ |
|
|
28
|
+
| `text` | `string` | — (required) | Text to reveal |
|
|
29
|
+
| `font` | `string` | `"600 32px \"Helvetica Neue\", Helvetica, Arial, sans-serif"` | CSS font shorthand, used for both measurement and rendering |
|
|
30
|
+
| `lineHeight` | `number` | `1.2 × font size` | Line height in pixels |
|
|
31
|
+
| `stagger` | `number` | `0.08` | Delay between lines in seconds |
|
|
32
|
+
| `duration` | `number` | `0.7` | Animation duration per line in seconds |
|
|
33
|
+
| `delay` | `number` | `0` | Initial delay in seconds |
|
|
34
|
+
| `once` | `boolean` | `true` | Animate only on first viewport entry |
|
|
35
|
+
| `class` | `string` | — | Additional CSS classes |
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- `font` must be a valid canvas `ctx.font` shorthand (weight, size in px, family).
|
|
40
|
+
- Measurement happens client-side only (pretext needs a canvas); during SSR the
|
|
41
|
+
raw text is rendered hidden so content stays in the HTML.
|
|
42
|
+
- Resizing the container re-wraps lines without replaying the animation.
|