fancy-ui-svelte 0.2.1 → 0.4.1
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 +18 -4
- package/dist/fancy-ui/animated-beam/README.md +0 -21
- package/dist/fancy-ui/compare/Compare.svelte +1 -1
- package/dist/fancy-ui/confetti/Confetti.svelte +3 -3
- package/dist/fancy-ui/confetti/Confetti.svelte.d.ts +1 -1
- package/dist/fancy-ui/confetti/README.md +1 -1
- package/dist/fancy-ui/flip-words/FlipWords.svelte +45 -13
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +72 -5
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte.d.ts +4 -1
- package/dist/fancy-ui/fluid-cursor/FluidCursor.test.js +34 -1
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.svelte +1556 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.svelte.d.ts +33 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.test.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/FluidCursorAdvanced.test.js +86 -0
- package/dist/fancy-ui/fluid-cursor/README.md +25 -0
- package/dist/fancy-ui/fluid-cursor/index.d.ts +1 -0
- package/dist/fancy-ui/fluid-cursor/index.js +1 -0
- package/dist/fancy-ui/index.d.ts +4 -0
- package/dist/fancy-ui/index.js +4 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.svelte +127 -0
- package/dist/fancy-ui/matrix-rain/MatrixRain.svelte.d.ts +11 -0
- package/dist/fancy-ui/matrix-rain/index.d.ts +3 -0
- package/dist/fancy-ui/matrix-rain/index.js +2 -0
- package/dist/fancy-ui/number-ticker/NumberTicker.svelte +1 -4
- package/dist/fancy-ui/registry.js +24 -0
- package/dist/fancy-ui/tailwind.css +2 -2
- package/dist/fancy-ui/terminal-text/TerminalText.svelte +187 -0
- package/dist/fancy-ui/terminal-text/TerminalText.svelte.d.ts +13 -0
- package/dist/fancy-ui/terminal-text/index.d.ts +3 -0
- package/dist/fancy-ui/terminal-text/index.js +2 -0
- package/package.json +3 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface ColorRGB {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
simResolution?: number;
|
|
8
|
+
dyeResolution?: number;
|
|
9
|
+
captureResolution?: number;
|
|
10
|
+
densityDissipation?: number;
|
|
11
|
+
velocityDissipation?: number;
|
|
12
|
+
pressure?: number;
|
|
13
|
+
pressureIterations?: number;
|
|
14
|
+
curl?: number;
|
|
15
|
+
splatRadius?: number;
|
|
16
|
+
splatForce?: number;
|
|
17
|
+
shading?: boolean;
|
|
18
|
+
colorUpdateSpeed?: number;
|
|
19
|
+
backColor?: ColorRGB | string;
|
|
20
|
+
transparent?: boolean;
|
|
21
|
+
fluidColor?: string;
|
|
22
|
+
fluidColors?: string[];
|
|
23
|
+
colorIntensity?: number;
|
|
24
|
+
class?: string;
|
|
25
|
+
autoSplat?: boolean;
|
|
26
|
+
autoSplatInterval?: number;
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
pauseWhenHidden?: boolean;
|
|
29
|
+
splatOnMount?: boolean;
|
|
30
|
+
}
|
|
31
|
+
declare const FluidCursorAdvanced: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type FluidCursorAdvanced = ReturnType<typeof FluidCursorAdvanced>;
|
|
33
|
+
export default FluidCursorAdvanced;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, cleanup } from "@testing-library/svelte";
|
|
2
|
+
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
|
3
|
+
import FluidCursorAdvanced from "./FluidCursorAdvanced.svelte";
|
|
4
|
+
describe("FluidCursorAdvanced", () => {
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
it("renders a container div", () => {
|
|
7
|
+
const { container } = render(FluidCursorAdvanced);
|
|
8
|
+
const div = container.firstElementChild;
|
|
9
|
+
expect(div).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
it("renders a canvas element", () => {
|
|
12
|
+
const { container } = render(FluidCursorAdvanced);
|
|
13
|
+
const canvas = container.querySelector("canvas");
|
|
14
|
+
expect(canvas).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
it("canvas does not share the hard-coded id with FluidCursor", () => {
|
|
17
|
+
const { container } = render(FluidCursorAdvanced);
|
|
18
|
+
const canvas = container.querySelector("canvas");
|
|
19
|
+
expect(canvas?.id).not.toBe("fluid");
|
|
20
|
+
});
|
|
21
|
+
it("container has pointer-events-none class (contained mode)", () => {
|
|
22
|
+
const { container } = render(FluidCursorAdvanced);
|
|
23
|
+
const div = container.firstElementChild;
|
|
24
|
+
expect(div?.className).toContain("pointer-events-none");
|
|
25
|
+
});
|
|
26
|
+
it("container has absolute positioning in contained mode", () => {
|
|
27
|
+
const { container } = render(FluidCursorAdvanced);
|
|
28
|
+
const div = container.firstElementChild;
|
|
29
|
+
expect(div?.className).toContain("absolute");
|
|
30
|
+
expect(div?.className).toContain("inset-0");
|
|
31
|
+
});
|
|
32
|
+
it("canvas fills container in contained mode", () => {
|
|
33
|
+
const { container } = render(FluidCursorAdvanced);
|
|
34
|
+
const canvas = container.querySelector("canvas");
|
|
35
|
+
expect(canvas?.className).toContain("h-full");
|
|
36
|
+
expect(canvas?.className).toContain("w-full");
|
|
37
|
+
});
|
|
38
|
+
it("applies custom class names", () => {
|
|
39
|
+
const { container } = render(FluidCursorAdvanced, { props: { class: "my-advanced" } });
|
|
40
|
+
const div = container.firstElementChild;
|
|
41
|
+
expect(div?.className).toContain("my-advanced");
|
|
42
|
+
});
|
|
43
|
+
describe("interaction props", () => {
|
|
44
|
+
it("interactive={false} does not register mouse event listeners", () => {
|
|
45
|
+
const addSpy = vi.spyOn(window, "addEventListener");
|
|
46
|
+
render(FluidCursorAdvanced, { props: { interactive: false } });
|
|
47
|
+
const mousedownCalls = addSpy.mock.calls.filter(([event]) => event === "mousedown");
|
|
48
|
+
expect(mousedownCalls).toHaveLength(0);
|
|
49
|
+
const mousemoveCalls = addSpy.mock.calls.filter(([event]) => event === "mousemove");
|
|
50
|
+
expect(mousemoveCalls).toHaveLength(0);
|
|
51
|
+
addSpy.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
it("mounts without error with autoSplat={true} and splatOnMount={true}", () => {
|
|
54
|
+
const { container } = render(FluidCursorAdvanced, {
|
|
55
|
+
props: { autoSplat: true, splatOnMount: true },
|
|
56
|
+
});
|
|
57
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
it("pauseWhenHidden={false} does not instantiate IntersectionObserver", () => {
|
|
60
|
+
const observeSpy = vi.fn();
|
|
61
|
+
const mockObserver = vi.fn(() => ({ observe: observeSpy, disconnect: vi.fn() }));
|
|
62
|
+
vi.stubGlobal("IntersectionObserver", mockObserver);
|
|
63
|
+
render(FluidCursorAdvanced, { props: { pauseWhenHidden: false } });
|
|
64
|
+
expect(mockObserver).not.toHaveBeenCalled();
|
|
65
|
+
vi.unstubAllGlobals();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("color props", () => {
|
|
69
|
+
it("renders without error when fluidColor is provided", () => {
|
|
70
|
+
const { container } = render(FluidCursorAdvanced, { props: { fluidColor: "#6366f1" } });
|
|
71
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
it("renders without error when fluidColors palette is provided", () => {
|
|
74
|
+
const { container } = render(FluidCursorAdvanced, {
|
|
75
|
+
props: { fluidColors: ["#6366f1", "#ec4899"] },
|
|
76
|
+
});
|
|
77
|
+
expect(container.querySelector("canvas")).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
it("warns and falls back when an invalid hex color is passed", () => {
|
|
80
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
81
|
+
render(FluidCursorAdvanced, { props: { backColor: "not-a-color" } });
|
|
82
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[FluidCursorAdvanced] Invalid hex color"));
|
|
83
|
+
warn.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -9,6 +9,7 @@ A WebGL-based fluid simulation that follows the cursor, creating beautiful flowi
|
|
|
9
9
|
- Configurable simulation parameters
|
|
10
10
|
- Transparent background support
|
|
11
11
|
- Automatic canvas resizing
|
|
12
|
+
- Controllable fluid colors via hex strings (fixed color, palette cycling, or random)
|
|
12
13
|
|
|
13
14
|
## Implementation Notes
|
|
14
15
|
|
|
@@ -27,6 +28,30 @@ The component implements a full Navier-Stokes fluid simulation with:
|
|
|
27
28
|
- Advection for both velocity and dye
|
|
28
29
|
- Gradient subtraction for incompressible flow
|
|
29
30
|
|
|
31
|
+
## Color Props
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | Description |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `fluidColor` | `string` (hex) | — | Fixed fluid color. Disables random cycling. |
|
|
36
|
+
| `fluidColors` | `string[]` (hex) | — | Palette to cycle through on each splat. |
|
|
37
|
+
| `colorIntensity` | `number` | `0.15` | Intensity multiplier applied to fluid colors (0–1). |
|
|
38
|
+
| `backColor` | `{ r, g, b }` or `string` (hex) | `{ r: 0.5, g: 0, b: 0 }` | Background color — accepts RGB object or hex string. |
|
|
39
|
+
|
|
40
|
+
**Priority**: `fluidColor` > `fluidColors` > random HSV
|
|
41
|
+
|
|
42
|
+
### Examples
|
|
43
|
+
|
|
44
|
+
```svelte
|
|
45
|
+
<!-- Fixed teal fluid -->
|
|
46
|
+
<FluidCursor fluidColor="#00ffcc" />
|
|
47
|
+
|
|
48
|
+
<!-- Cycling palette -->
|
|
49
|
+
<FluidCursor fluidColors={["#ff0080", "#00ffcc", "#7700ff"]} colorIntensity={0.3} />
|
|
50
|
+
|
|
51
|
+
<!-- Hex background -->
|
|
52
|
+
<FluidCursor backColor="#1a1a2e" />
|
|
53
|
+
```
|
|
54
|
+
|
|
30
55
|
### Event Handling
|
|
31
56
|
|
|
32
57
|
- Window-level mouse/touch events for full-screen tracking
|
package/dist/fancy-ui/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export * from "./marquee/index.js";
|
|
|
13
13
|
export * from "./compare/index.js";
|
|
14
14
|
export * from "./image-trail-cursor/index.js";
|
|
15
15
|
export * from "./interactive-grid-pattern/index.js";
|
|
16
|
+
export * from "./interactive-hover-button/index.js";
|
|
17
|
+
export * from "./gradient-button/index.js";
|
|
16
18
|
export * from "./logo-cloud/index.js";
|
|
17
19
|
export * from "./direction-aware-hover/index.js";
|
|
18
20
|
export * from "./rainbow-button/index.js";
|
|
@@ -49,4 +51,6 @@ export * from "./text-generate-effect/index.js";
|
|
|
49
51
|
export * from "./line-shadow-text/index.js";
|
|
50
52
|
export * from "./tracing-beam/index.js";
|
|
51
53
|
export * from "./displacement-text/index.js";
|
|
54
|
+
export * from "./matrix-rain/index.js";
|
|
55
|
+
export * from "./terminal-text/index.js";
|
|
52
56
|
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
|
@@ -18,6 +18,8 @@ export * from "./marquee/index.js";
|
|
|
18
18
|
export * from "./compare/index.js";
|
|
19
19
|
export * from "./image-trail-cursor/index.js";
|
|
20
20
|
export * from "./interactive-grid-pattern/index.js";
|
|
21
|
+
export * from "./interactive-hover-button/index.js";
|
|
22
|
+
export * from "./gradient-button/index.js";
|
|
21
23
|
export * from "./logo-cloud/index.js";
|
|
22
24
|
export * from "./direction-aware-hover/index.js";
|
|
23
25
|
export * from "./rainbow-button/index.js";
|
|
@@ -54,6 +56,8 @@ export * from "./text-generate-effect/index.js";
|
|
|
54
56
|
export * from "./line-shadow-text/index.js";
|
|
55
57
|
export * from "./tracing-beam/index.js";
|
|
56
58
|
export * from "./displacement-text/index.js";
|
|
59
|
+
export * from "./matrix-rain/index.js";
|
|
60
|
+
export * from "./terminal-text/index.js";
|
|
57
61
|
// =============================================================================
|
|
58
62
|
// Registry
|
|
59
63
|
// =============================================================================
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface MatrixRainProps {
|
|
3
|
+
color?: string;
|
|
4
|
+
speed?: number;
|
|
5
|
+
density?: number;
|
|
6
|
+
glyphSize?: number;
|
|
7
|
+
fadeOpacity?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { cn } from "../../utils.js";
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
color = "#00ff41",
|
|
17
|
+
speed = 1.0,
|
|
18
|
+
density = 1.0,
|
|
19
|
+
glyphSize = 16,
|
|
20
|
+
fadeOpacity = 0.05,
|
|
21
|
+
class: className,
|
|
22
|
+
}: MatrixRainProps = $props();
|
|
23
|
+
|
|
24
|
+
const GLYPHS =
|
|
25
|
+
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&";
|
|
26
|
+
|
|
27
|
+
let canvas: HTMLCanvasElement;
|
|
28
|
+
let ctx: CanvasRenderingContext2D | null = null;
|
|
29
|
+
let rafId: number;
|
|
30
|
+
let columns: number[] = [];
|
|
31
|
+
let frameCount = 0;
|
|
32
|
+
let canvasW = 0;
|
|
33
|
+
let canvasH = 0;
|
|
34
|
+
|
|
35
|
+
function randomGlyph(): string {
|
|
36
|
+
return GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function init(w: number, h: number) {
|
|
40
|
+
const safeGlyphSize = Math.max(1, glyphSize);
|
|
41
|
+
const safeDensity = Math.max(0.1, density);
|
|
42
|
+
const colCount = Math.max(1, Math.floor(w / (safeGlyphSize * safeDensity)));
|
|
43
|
+
columns = Array.from({ length: colCount }, () =>
|
|
44
|
+
Math.floor(Math.random() * (h / safeGlyphSize))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function draw() {
|
|
49
|
+
if (!ctx) return;
|
|
50
|
+
|
|
51
|
+
const w = canvasW;
|
|
52
|
+
const h = canvasH;
|
|
53
|
+
|
|
54
|
+
// Fade trail
|
|
55
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${fadeOpacity})`;
|
|
56
|
+
ctx.fillRect(0, 0, w, h);
|
|
57
|
+
|
|
58
|
+
ctx.font = `${glyphSize}px monospace`;
|
|
59
|
+
ctx.shadowBlur = 8;
|
|
60
|
+
ctx.shadowColor = color;
|
|
61
|
+
|
|
62
|
+
const rowsPerFrame = Math.max(1, speed);
|
|
63
|
+
frameCount++;
|
|
64
|
+
|
|
65
|
+
// Only advance every N frames to control speed below 1x
|
|
66
|
+
const shouldAdvance = speed >= 1 || frameCount % Math.round(1 / speed) === 0;
|
|
67
|
+
|
|
68
|
+
if (shouldAdvance) {
|
|
69
|
+
for (let i = 0; i < columns.length; i++) {
|
|
70
|
+
const x = i * glyphSize * density;
|
|
71
|
+
const y = columns[i] * glyphSize;
|
|
72
|
+
|
|
73
|
+
// Head character (bright white)
|
|
74
|
+
ctx.fillStyle = "#ffffff";
|
|
75
|
+
ctx.fillText(randomGlyph(), x, y);
|
|
76
|
+
|
|
77
|
+
// Body glyph one step behind (dimmer)
|
|
78
|
+
if (columns[i] > 1) {
|
|
79
|
+
ctx.fillStyle = color;
|
|
80
|
+
ctx.fillText(randomGlyph(), x, y - glyphSize);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reset column when it exits bottom, with random delay
|
|
84
|
+
if (y > h && Math.random() > 0.975) {
|
|
85
|
+
columns[i] = 0;
|
|
86
|
+
} else {
|
|
87
|
+
columns[i] += rowsPerFrame;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.shadowBlur = 0;
|
|
93
|
+
rafId = requestAnimationFrame(draw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
ctx = canvas.getContext("2d");
|
|
98
|
+
if (!ctx) return;
|
|
99
|
+
|
|
100
|
+
// Set canvas size to container size with HiDPI scaling
|
|
101
|
+
const resize = () => {
|
|
102
|
+
const dpr = window.devicePixelRatio || 1;
|
|
103
|
+
canvasW = canvas.clientWidth;
|
|
104
|
+
canvasH = canvas.clientHeight;
|
|
105
|
+
canvas.width = Math.floor(canvasW * dpr);
|
|
106
|
+
canvas.height = Math.floor(canvasH * dpr);
|
|
107
|
+
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
108
|
+
ctx!.fillStyle = "black";
|
|
109
|
+
ctx!.fillRect(0, 0, canvasW, canvasH);
|
|
110
|
+
init(canvasW, canvasH);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
resize();
|
|
114
|
+
|
|
115
|
+
const observer = new ResizeObserver(resize);
|
|
116
|
+
observer.observe(canvas);
|
|
117
|
+
|
|
118
|
+
rafId = requestAnimationFrame(draw);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
cancelAnimationFrame(rafId);
|
|
122
|
+
observer.disconnect();
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<canvas bind:this={canvas} class={cn("block h-full w-full bg-black", className)}></canvas>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MatrixRainProps {
|
|
2
|
+
color?: string;
|
|
3
|
+
speed?: number;
|
|
4
|
+
density?: number;
|
|
5
|
+
glyphSize?: number;
|
|
6
|
+
fadeOpacity?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const MatrixRain: import("svelte").Component<MatrixRainProps, {}, "">;
|
|
10
|
+
type MatrixRain = ReturnType<typeof MatrixRain>;
|
|
11
|
+
export default MatrixRain;
|
|
@@ -117,10 +117,7 @@
|
|
|
117
117
|
|
|
118
118
|
<span
|
|
119
119
|
bind:this={spanRef}
|
|
120
|
-
class={cn(
|
|
121
|
-
"number-ticker inline-block tracking-wider text-black tabular-nums dark:text-white",
|
|
122
|
-
className
|
|
123
|
-
)}
|
|
120
|
+
class={cn("number-ticker inline-block text-black tabular-nums dark:text-white", className)}
|
|
124
121
|
>
|
|
125
122
|
{formattedValue}
|
|
126
123
|
</span>
|
|
@@ -243,6 +243,16 @@ export const registry = {
|
|
|
243
243
|
{ source: "Inspira UI", url: "https://inspira-ui.com/components/cursor/fluid-cursor" },
|
|
244
244
|
],
|
|
245
245
|
},
|
|
246
|
+
"fluid-cursor-advanced": {
|
|
247
|
+
name: "FluidCursorAdvanced",
|
|
248
|
+
slug: "fluid-cursor-advanced",
|
|
249
|
+
description: "WebGL fluid simulation confined to a parent container element",
|
|
250
|
+
category: "effects",
|
|
251
|
+
status: "done",
|
|
252
|
+
credits: [
|
|
253
|
+
{ source: "Inspira UI", url: "https://inspira-ui.com/components/cursor/fluid-cursor" },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
246
256
|
"glow-border": {
|
|
247
257
|
name: "GlowBorder",
|
|
248
258
|
slug: "glow-border",
|
|
@@ -553,6 +563,20 @@ export const registry = {
|
|
|
553
563
|
status: "done",
|
|
554
564
|
credits: [{ source: "VengenceUI", url: "https://vengenceui.com/docs/liquid-text" }],
|
|
555
565
|
},
|
|
566
|
+
"matrix-rain": {
|
|
567
|
+
name: "MatrixRain",
|
|
568
|
+
slug: "matrix-rain",
|
|
569
|
+
description: "Canvas-based Matrix-style falling glyph rain with configurable color, speed, and density",
|
|
570
|
+
category: "backgrounds",
|
|
571
|
+
status: "done",
|
|
572
|
+
},
|
|
573
|
+
"terminal-text": {
|
|
574
|
+
name: "TerminalText",
|
|
575
|
+
slug: "terminal-text",
|
|
576
|
+
description: "Terminal-style text streamer with per-character animation, blinking cursor, and glitch effect",
|
|
577
|
+
category: "text",
|
|
578
|
+
status: "done",
|
|
579
|
+
},
|
|
556
580
|
};
|
|
557
581
|
// =============================================================================
|
|
558
582
|
// Registry Helpers
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/* Import this file in your app's CSS to include fancy-ui
|
|
2
|
-
@import "fancy-ui
|
|
1
|
+
/* Import this file in your app's CSS to include fancy-ui Tailwind classes:
|
|
2
|
+
@import "fancy-ui/tailwind.css"; */
|
|
3
3
|
@source "..";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface TerminalTextProps {
|
|
3
|
+
lines: string[];
|
|
4
|
+
speed?: number;
|
|
5
|
+
delay?: number;
|
|
6
|
+
cursor?: boolean;
|
|
7
|
+
cursorChar?: string;
|
|
8
|
+
glitch?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
onComplete?: () => void;
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import { cn } from "../../utils.js";
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
lines,
|
|
19
|
+
speed = 40,
|
|
20
|
+
delay = 0,
|
|
21
|
+
cursor = true,
|
|
22
|
+
cursorChar = "█",
|
|
23
|
+
glitch = false,
|
|
24
|
+
class: className,
|
|
25
|
+
onComplete,
|
|
26
|
+
}: TerminalTextProps = $props();
|
|
27
|
+
|
|
28
|
+
const GLITCH_GLYPHS = "アイウエオ@#$%&!?░▒▓█▄▀■□▪▫";
|
|
29
|
+
|
|
30
|
+
let displayedLines = $state<string[]>([]);
|
|
31
|
+
let done = $state(false);
|
|
32
|
+
let glitchState = $state<{ lineIdx: number; charIdx: number; original: string } | null>(null);
|
|
33
|
+
|
|
34
|
+
let timeouts: ReturnType<typeof setTimeout>[] = [];
|
|
35
|
+
|
|
36
|
+
function scheduleTimeout(fn: () => void, ms: number) {
|
|
37
|
+
const id = setTimeout(fn, ms);
|
|
38
|
+
timeouts.push(id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearAllTimeouts() {
|
|
43
|
+
timeouts.forEach(clearTimeout);
|
|
44
|
+
timeouts = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function streamLines() {
|
|
48
|
+
clearAllTimeouts();
|
|
49
|
+
displayedLines = [];
|
|
50
|
+
done = false;
|
|
51
|
+
glitchState = null;
|
|
52
|
+
|
|
53
|
+
let totalDelay = delay;
|
|
54
|
+
|
|
55
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
56
|
+
const line = lines[lineIdx];
|
|
57
|
+
|
|
58
|
+
// Push empty line placeholder
|
|
59
|
+
const capturedIdx = lineIdx;
|
|
60
|
+
scheduleTimeout(() => {
|
|
61
|
+
displayedLines = [...displayedLines, ""];
|
|
62
|
+
}, totalDelay);
|
|
63
|
+
|
|
64
|
+
// Stream characters one by one
|
|
65
|
+
for (let charIdx = 0; charIdx < line.length; charIdx++) {
|
|
66
|
+
const capturedChar = charIdx;
|
|
67
|
+
const capturedLine = line;
|
|
68
|
+
totalDelay += speed;
|
|
69
|
+
scheduleTimeout(() => {
|
|
70
|
+
displayedLines = displayedLines.map((l, i) =>
|
|
71
|
+
i === capturedIdx ? capturedLine.slice(0, capturedChar + 1) : l
|
|
72
|
+
);
|
|
73
|
+
}, totalDelay);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Small pause between lines
|
|
77
|
+
totalDelay += speed * 3;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Mark done
|
|
81
|
+
scheduleTimeout(() => {
|
|
82
|
+
done = true;
|
|
83
|
+
onComplete?.();
|
|
84
|
+
}, totalDelay);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function startGlitchLoop() {
|
|
88
|
+
function glitchOnce() {
|
|
89
|
+
if (displayedLines.length === 0) return;
|
|
90
|
+
|
|
91
|
+
// Pick a random line with content
|
|
92
|
+
const linesWithContent = displayedLines
|
|
93
|
+
.map((l, i) => ({ l, i }))
|
|
94
|
+
.filter(({ l }) => l.length > 0);
|
|
95
|
+
if (linesWithContent.length === 0) return;
|
|
96
|
+
|
|
97
|
+
const { l, i } = linesWithContent[Math.floor(Math.random() * linesWithContent.length)];
|
|
98
|
+
const charIdx = Math.floor(Math.random() * l.length);
|
|
99
|
+
const original = l[charIdx];
|
|
100
|
+
const fakeGlyph = GLITCH_GLYPHS[Math.floor(Math.random() * GLITCH_GLYPHS.length)];
|
|
101
|
+
|
|
102
|
+
glitchState = { lineIdx: i, charIdx, original };
|
|
103
|
+
|
|
104
|
+
// Temporarily replace char in display via glitchState, restore after 100ms
|
|
105
|
+
displayedLines = displayedLines.map((line, idx) => {
|
|
106
|
+
if (idx !== i) return line;
|
|
107
|
+
return line.slice(0, charIdx) + fakeGlyph + line.slice(charIdx + 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
scheduleTimeout(() => {
|
|
111
|
+
if (glitchState && glitchState.lineIdx === i && glitchState.charIdx === charIdx) {
|
|
112
|
+
displayedLines = displayedLines.map((line, idx) => {
|
|
113
|
+
if (idx !== i) return line;
|
|
114
|
+
return line.slice(0, charIdx) + original + line.slice(charIdx + 1);
|
|
115
|
+
});
|
|
116
|
+
glitchState = null;
|
|
117
|
+
}
|
|
118
|
+
}, 100);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scheduleGlitch() {
|
|
122
|
+
// Random interval between 2s and 4s
|
|
123
|
+
const interval = 2000 + Math.random() * 2000;
|
|
124
|
+
scheduleTimeout(() => {
|
|
125
|
+
if (glitch) glitchOnce();
|
|
126
|
+
scheduleGlitch();
|
|
127
|
+
}, interval);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
scheduleGlitch();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$effect(() => {
|
|
134
|
+
// React only to lines/speed/delay — changing glitch won't restart the stream
|
|
135
|
+
void lines;
|
|
136
|
+
void speed;
|
|
137
|
+
void delay;
|
|
138
|
+
|
|
139
|
+
streamLines();
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
clearAllTimeouts();
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
$effect(() => {
|
|
147
|
+
// React to glitch changes without restarting the stream
|
|
148
|
+
void glitch;
|
|
149
|
+
|
|
150
|
+
if (glitch) {
|
|
151
|
+
startGlitchLoop();
|
|
152
|
+
} else {
|
|
153
|
+
glitchState = null;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<div class={cn("font-mono text-sm leading-relaxed", className)}>
|
|
159
|
+
{#each displayedLines as line, i}
|
|
160
|
+
<div class="min-h-[1.4em]">
|
|
161
|
+
<span>{line}</span>{#if cursor && i === displayedLines.length - 1 && !done}<span
|
|
162
|
+
class="cursor-blink">{cursorChar}</span
|
|
163
|
+
>{/if}
|
|
164
|
+
</div>
|
|
165
|
+
{/each}
|
|
166
|
+
{#if cursor && done}
|
|
167
|
+
<div class="min-h-[1.4em]">
|
|
168
|
+
<span class="cursor-blink">{cursorChar}</span>
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<style>
|
|
174
|
+
.cursor-blink {
|
|
175
|
+
animation: blink 1s step-end infinite;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@keyframes blink {
|
|
179
|
+
0%,
|
|
180
|
+
100% {
|
|
181
|
+
opacity: 1;
|
|
182
|
+
}
|
|
183
|
+
50% {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TerminalTextProps {
|
|
2
|
+
lines: string[];
|
|
3
|
+
speed?: number;
|
|
4
|
+
delay?: number;
|
|
5
|
+
cursor?: boolean;
|
|
6
|
+
cursorChar?: string;
|
|
7
|
+
glitch?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
onComplete?: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const TerminalText: import("svelte").Component<TerminalTextProps, {}, "">;
|
|
12
|
+
type TerminalText = ReturnType<typeof TerminalText>;
|
|
13
|
+
export default TerminalText;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fancy-ui-svelte",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"tailwindcss": "^4.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@vercel/analytics": "^2.0.1",
|
|
49
50
|
"canvas-confetti": "^1.9.4",
|
|
50
51
|
"clsx": "^2.1.1",
|
|
51
52
|
"gsap": "^3.14.2",
|
|
@@ -71,6 +72,7 @@
|
|
|
71
72
|
"@types/three": "^0.183.1",
|
|
72
73
|
"arctic": "^3.7.0",
|
|
73
74
|
"bits-ui": "^2.15.4",
|
|
75
|
+
"isomorphic-dompurify": "^3.7.1",
|
|
74
76
|
"jsdom": "^28.0.0",
|
|
75
77
|
"marked": "^17.0.3",
|
|
76
78
|
"nanoid": "^5.1.6",
|