@oalacea/chaosui 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/bin/cli.js +216 -0
- package/components/backgrounds/glow-orbs/glow-orbs.module.css +31 -0
- package/components/backgrounds/glow-orbs/index.tsx +87 -0
- package/components/backgrounds/light-beams/index.tsx +80 -0
- package/components/backgrounds/light-beams/light-beams.module.css +27 -0
- package/components/backgrounds/noise-canvas/index.tsx +113 -0
- package/components/backgrounds/noise-canvas/noise-canvas.module.css +8 -0
- package/components/backgrounds/particle-field/index.tsx +81 -0
- package/components/backgrounds/particle-field/particle-field.module.css +31 -0
- package/components/buttons/chaos-button/chaos-button.module.css +173 -0
- package/components/buttons/chaos-button/index.tsx +60 -0
- package/components/buttons/glitch-button/glitch-button.module.css +197 -0
- package/components/buttons/glitch-button/index.tsx +53 -0
- package/components/effects/cursor-follower/cursor-follower.module.css +50 -0
- package/components/effects/cursor-follower/index.tsx +83 -0
- package/components/effects/screen-distortion/index.tsx +54 -0
- package/components/effects/screen-distortion/screen-distortion.module.css +127 -0
- package/components/effects/warning-tape/index.tsx +64 -0
- package/components/effects/warning-tape/warning-tape.module.css +29 -0
- package/components/glow-orbs/glow-orbs.module.css +31 -0
- package/components/glow-orbs/index.tsx +87 -0
- package/components/light-beams/index.tsx +80 -0
- package/components/light-beams/light-beams.module.css +27 -0
- package/components/noise-canvas/index.tsx +113 -0
- package/components/noise-canvas/noise-canvas.module.css +8 -0
- package/components/overlays/noise-overlay/index.tsx +56 -0
- package/components/overlays/noise-overlay/noise-overlay.module.css +20 -0
- package/components/overlays/scanlines/index.tsx +61 -0
- package/components/overlays/scanlines/scanlines.module.css +16 -0
- package/components/overlays/static-flicker/index.tsx +51 -0
- package/components/overlays/static-flicker/static-flicker.module.css +27 -0
- package/components/overlays/vignette/index.tsx +58 -0
- package/components/overlays/vignette/vignette.module.css +7 -0
- package/components/package.json +13 -0
- package/components/particle-field/index.tsx +81 -0
- package/components/particle-field/particle-field.module.css +31 -0
- package/components/text/distortion-text/distortion-text.module.css +100 -0
- package/components/text/distortion-text/index.tsx +53 -0
- package/components/text/falling-text/falling-text.module.css +57 -0
- package/components/text/falling-text/index.tsx +61 -0
- package/components/text/flicker-text/flicker-text.module.css +91 -0
- package/components/text/flicker-text/index.tsx +48 -0
- package/components/text/glitch-text/glitch-text.module.css +142 -0
- package/components/text/glitch-text/index.tsx +53 -0
- package/package.json +38 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './scanlines.module.css';
|
|
5
|
+
|
|
6
|
+
export interface ScanlinesProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Line opacity (0-1) */
|
|
8
|
+
opacity?: number;
|
|
9
|
+
/** Line thickness in pixels */
|
|
10
|
+
lineWidth?: number;
|
|
11
|
+
/** Gap between lines in pixels */
|
|
12
|
+
gap?: number;
|
|
13
|
+
/** Line color */
|
|
14
|
+
color?: string;
|
|
15
|
+
/** Enable flicker animation */
|
|
16
|
+
flicker?: boolean;
|
|
17
|
+
/** Fixed position (covers viewport) or absolute (covers parent) */
|
|
18
|
+
position?: 'fixed' | 'absolute';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Scanlines = forwardRef<HTMLDivElement, ScanlinesProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
opacity = 0.1,
|
|
25
|
+
lineWidth = 1,
|
|
26
|
+
gap = 2,
|
|
27
|
+
color = '#000000',
|
|
28
|
+
flicker = false,
|
|
29
|
+
position = 'fixed',
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={`${styles.scanlines} ${flicker ? styles.flicker : ''} ${className || ''}`}
|
|
40
|
+
style={{
|
|
41
|
+
position,
|
|
42
|
+
opacity,
|
|
43
|
+
background: `repeating-linear-gradient(
|
|
44
|
+
0deg,
|
|
45
|
+
${color},
|
|
46
|
+
${color} ${lineWidth}px,
|
|
47
|
+
transparent ${lineWidth}px,
|
|
48
|
+
transparent ${lineWidth + gap}px
|
|
49
|
+
)`,
|
|
50
|
+
...style,
|
|
51
|
+
}}
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Scanlines.displayName = 'Scanlines';
|
|
60
|
+
|
|
61
|
+
export default Scanlines;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.scanlines {
|
|
2
|
+
inset: 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
z-index: 9998;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.flicker {
|
|
10
|
+
animation: scanline-flicker 0.1s infinite;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@keyframes scanline-flicker {
|
|
14
|
+
0%, 100% { opacity: 0.1; }
|
|
15
|
+
50% { opacity: 0.15; }
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './static-flicker.module.css';
|
|
5
|
+
|
|
6
|
+
export interface StaticFlickerProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Noise opacity (0-1) */
|
|
8
|
+
opacity?: number;
|
|
9
|
+
/** Flicker speed: slow, normal, fast */
|
|
10
|
+
speed?: 'slow' | 'normal' | 'fast';
|
|
11
|
+
/** Noise frequency */
|
|
12
|
+
frequency?: number;
|
|
13
|
+
/** Fixed or absolute positioning */
|
|
14
|
+
position?: 'fixed' | 'absolute';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StaticFlicker = forwardRef<HTMLDivElement, StaticFlickerProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
opacity = 0.03,
|
|
21
|
+
speed = 'normal',
|
|
22
|
+
frequency = 0.9,
|
|
23
|
+
position = 'fixed',
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) => {
|
|
30
|
+
const noiseSvg = `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='${frequency}' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={`${styles.static} ${styles[speed]} ${className || ''}`}
|
|
36
|
+
style={{
|
|
37
|
+
position,
|
|
38
|
+
opacity,
|
|
39
|
+
backgroundImage: noiseSvg,
|
|
40
|
+
...style,
|
|
41
|
+
}}
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
StaticFlicker.displayName = 'StaticFlicker';
|
|
50
|
+
|
|
51
|
+
export default StaticFlicker;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.static {
|
|
2
|
+
inset: 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
z-index: 9999;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.slow {
|
|
10
|
+
animation: flicker 0.2s steps(8) infinite;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.normal {
|
|
14
|
+
animation: flicker 0.1s steps(5) infinite;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.fast {
|
|
18
|
+
animation: flicker 0.05s steps(3) infinite;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes flicker {
|
|
22
|
+
0%, 100% { opacity: 0.02; }
|
|
23
|
+
20% { opacity: 0.04; }
|
|
24
|
+
40% { opacity: 0.02; }
|
|
25
|
+
60% { opacity: 0.03; }
|
|
26
|
+
80% { opacity: 0.025; }
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './vignette.module.css';
|
|
5
|
+
|
|
6
|
+
export interface VignetteProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Vignette intensity (0-1) */
|
|
8
|
+
intensity?: number;
|
|
9
|
+
/** Vignette color */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Vignette spread (0-1, how far the effect extends) */
|
|
12
|
+
spread?: number;
|
|
13
|
+
/** Fixed position (covers viewport) or absolute (covers parent) */
|
|
14
|
+
position?: 'fixed' | 'absolute';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Vignette = forwardRef<HTMLDivElement, VignetteProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
intensity = 0.8,
|
|
21
|
+
color = '#000000',
|
|
22
|
+
spread = 0.5,
|
|
23
|
+
position = 'fixed',
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) => {
|
|
30
|
+
// Convert hex to rgba for gradient
|
|
31
|
+
const hexToRgba = (hex: string, alpha: number) => {
|
|
32
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
33
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
34
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
35
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const innerStop = Math.max(0, (1 - spread) * 100);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={`${styles.vignette} ${className || ''}`}
|
|
44
|
+
style={{
|
|
45
|
+
position,
|
|
46
|
+
background: `radial-gradient(ellipse at center, transparent ${innerStop}%, ${hexToRgba(color, intensity)} 100%)`,
|
|
47
|
+
...style,
|
|
48
|
+
}}
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
Vignette.displayName = 'Vignette';
|
|
57
|
+
|
|
58
|
+
export default Vignette;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chaos/components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Glitch, noise, and distortion UI components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./text/*": "./text/*/index.tsx",
|
|
8
|
+
"./overlays/*": "./overlays/*/index.tsx",
|
|
9
|
+
"./buttons/*": "./buttons/*/index.tsx",
|
|
10
|
+
"./effects/*": "./effects/*/index.tsx",
|
|
11
|
+
"./backgrounds/*": "./backgrounds/*/index.tsx"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes, useMemo } from 'react';
|
|
4
|
+
import styles from './particle-field.module.css';
|
|
5
|
+
|
|
6
|
+
export interface ParticleFieldProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Number of particles */
|
|
8
|
+
count?: number;
|
|
9
|
+
/** Particle color */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Particle size range [min, max] in pixels */
|
|
12
|
+
sizeRange?: [number, number];
|
|
13
|
+
/** Animation duration range [min, max] in seconds */
|
|
14
|
+
durationRange?: [number, number];
|
|
15
|
+
/** Particle opacity */
|
|
16
|
+
opacity?: number;
|
|
17
|
+
/** Fixed or absolute positioning */
|
|
18
|
+
position?: 'fixed' | 'absolute';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ParticleField = forwardRef<HTMLDivElement, ParticleFieldProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
count = 50,
|
|
25
|
+
color = '#ffffff',
|
|
26
|
+
sizeRange = [1, 3],
|
|
27
|
+
durationRange = [10, 20],
|
|
28
|
+
opacity = 0.5,
|
|
29
|
+
position = 'fixed',
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
const particles = useMemo(() => {
|
|
37
|
+
return Array.from({ length: count }, (_, i) => {
|
|
38
|
+
const size = sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]);
|
|
39
|
+
const duration = durationRange[0] + Math.random() * (durationRange[1] - durationRange[0]);
|
|
40
|
+
const delay = Math.random() * -duration;
|
|
41
|
+
const startX = Math.random() * 100;
|
|
42
|
+
const startY = Math.random() * 100;
|
|
43
|
+
const drift = (Math.random() - 0.5) * 100;
|
|
44
|
+
|
|
45
|
+
return { id: i, size, duration, delay, startX, startY, drift };
|
|
46
|
+
});
|
|
47
|
+
}, [count, sizeRange, durationRange]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={`${styles.container} ${className || ''}`}
|
|
53
|
+
style={{ position, ...style }}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{particles.map((p) => (
|
|
58
|
+
<div
|
|
59
|
+
key={p.id}
|
|
60
|
+
className={styles.particle}
|
|
61
|
+
style={{
|
|
62
|
+
width: p.size,
|
|
63
|
+
height: p.size,
|
|
64
|
+
backgroundColor: color,
|
|
65
|
+
opacity,
|
|
66
|
+
left: `${p.startX}%`,
|
|
67
|
+
top: `${p.startY}%`,
|
|
68
|
+
animationDuration: `${p.duration}s`,
|
|
69
|
+
animationDelay: `${p.delay}s`,
|
|
70
|
+
'--drift': `${p.drift}px`,
|
|
71
|
+
} as React.CSSProperties}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
ParticleField.displayName = 'ParticleField';
|
|
80
|
+
|
|
81
|
+
export default ParticleField;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
inset: 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
z-index: 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.particle {
|
|
11
|
+
position: absolute;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
animation: drift linear infinite;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes drift {
|
|
17
|
+
0% {
|
|
18
|
+
transform: translate(0, 0);
|
|
19
|
+
opacity: 0;
|
|
20
|
+
}
|
|
21
|
+
10% {
|
|
22
|
+
opacity: var(--opacity, 0.5);
|
|
23
|
+
}
|
|
24
|
+
90% {
|
|
25
|
+
opacity: var(--opacity, 0.5);
|
|
26
|
+
}
|
|
27
|
+
100% {
|
|
28
|
+
transform: translate(var(--drift, 50px), -100vh);
|
|
29
|
+
opacity: 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
.distortion {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/* WAVE */
|
|
6
|
+
.wave {
|
|
7
|
+
animation: wave var(--duration, 2s) ease-in-out infinite;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.wave.subtle { --wave-amount: 2px; }
|
|
11
|
+
.wave.medium { --wave-amount: 5px; }
|
|
12
|
+
.wave.intense { --wave-amount: 10px; }
|
|
13
|
+
|
|
14
|
+
@keyframes wave {
|
|
15
|
+
0%, 100% {
|
|
16
|
+
transform: translateY(0) skewX(0);
|
|
17
|
+
}
|
|
18
|
+
25% {
|
|
19
|
+
transform: translateY(calc(var(--wave-amount) * -1)) skewX(-2deg);
|
|
20
|
+
}
|
|
21
|
+
50% {
|
|
22
|
+
transform: translateY(0) skewX(0);
|
|
23
|
+
}
|
|
24
|
+
75% {
|
|
25
|
+
transform: translateY(var(--wave-amount)) skewX(2deg);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* SHAKE */
|
|
30
|
+
.shake {
|
|
31
|
+
animation: shake var(--duration, 0.5s) linear infinite;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.shake.subtle { --shake-amount: 1px; }
|
|
35
|
+
.shake.medium { --shake-amount: 3px; }
|
|
36
|
+
.shake.intense { --shake-amount: 6px; }
|
|
37
|
+
|
|
38
|
+
@keyframes shake {
|
|
39
|
+
0%, 100% { transform: translate(0); }
|
|
40
|
+
10% { transform: translate(calc(var(--shake-amount) * -1), var(--shake-amount)); }
|
|
41
|
+
20% { transform: translate(var(--shake-amount), calc(var(--shake-amount) * -1)); }
|
|
42
|
+
30% { transform: translate(calc(var(--shake-amount) * -1), calc(var(--shake-amount) * -1)); }
|
|
43
|
+
40% { transform: translate(var(--shake-amount), var(--shake-amount)); }
|
|
44
|
+
50% { transform: translate(calc(var(--shake-amount) * -1), 0); }
|
|
45
|
+
60% { transform: translate(var(--shake-amount), 0); }
|
|
46
|
+
70% { transform: translate(0, var(--shake-amount)); }
|
|
47
|
+
80% { transform: translate(0, calc(var(--shake-amount) * -1)); }
|
|
48
|
+
90% { transform: translate(var(--shake-amount), var(--shake-amount)); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* SKEW */
|
|
52
|
+
.skew {
|
|
53
|
+
animation: skew var(--duration, 3s) ease-in-out infinite;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.skew.subtle { --skew-amount: 3deg; }
|
|
57
|
+
.skew.medium { --skew-amount: 8deg; }
|
|
58
|
+
.skew.intense { --skew-amount: 15deg; }
|
|
59
|
+
|
|
60
|
+
@keyframes skew {
|
|
61
|
+
0%, 100% { transform: skewX(0) skewY(0); }
|
|
62
|
+
25% { transform: skewX(var(--skew-amount)) skewY(calc(var(--skew-amount) * 0.5)); }
|
|
63
|
+
50% { transform: skewX(0) skewY(0); }
|
|
64
|
+
75% { transform: skewX(calc(var(--skew-amount) * -1)) skewY(calc(var(--skew-amount) * -0.5)); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* BLUR */
|
|
68
|
+
.blur {
|
|
69
|
+
animation: blur var(--duration, 2s) ease-in-out infinite;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.blur.subtle { --blur-amount: 1px; }
|
|
73
|
+
.blur.medium { --blur-amount: 3px; }
|
|
74
|
+
.blur.intense { --blur-amount: 6px; }
|
|
75
|
+
|
|
76
|
+
@keyframes blur {
|
|
77
|
+
0%, 100% { filter: blur(0); }
|
|
78
|
+
50% { filter: blur(var(--blur-amount)); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* SPEEDS */
|
|
82
|
+
.slow { --duration: 4s; }
|
|
83
|
+
.normal { --duration: 2s; }
|
|
84
|
+
.fast { --duration: 0.8s; }
|
|
85
|
+
|
|
86
|
+
.shake.slow { --duration: 1s; }
|
|
87
|
+
.shake.normal { --duration: 0.5s; }
|
|
88
|
+
.shake.fast { --duration: 0.2s; }
|
|
89
|
+
|
|
90
|
+
/* HOVER ONLY */
|
|
91
|
+
.hoverOnly {
|
|
92
|
+
animation: none;
|
|
93
|
+
transform: none;
|
|
94
|
+
filter: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.hoverOnly.wave:hover { animation: wave var(--duration, 2s) ease-in-out infinite; }
|
|
98
|
+
.hoverOnly.shake:hover { animation: shake var(--duration, 0.5s) linear infinite; }
|
|
99
|
+
.hoverOnly.skew:hover { animation: skew var(--duration, 3s) ease-in-out infinite; }
|
|
100
|
+
.hoverOnly.blur:hover { animation: blur var(--duration, 2s) ease-in-out infinite; }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './distortion-text.module.css';
|
|
5
|
+
|
|
6
|
+
export interface DistortionTextProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
/** The text to display */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Distortion type */
|
|
10
|
+
type?: 'wave' | 'shake' | 'skew' | 'blur';
|
|
11
|
+
/** Animation speed: slow, normal, fast */
|
|
12
|
+
speed?: 'slow' | 'normal' | 'fast';
|
|
13
|
+
/** Distortion intensity */
|
|
14
|
+
intensity?: 'subtle' | 'medium' | 'intense';
|
|
15
|
+
/** Only animate on hover */
|
|
16
|
+
hoverOnly?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DistortionText = forwardRef<HTMLSpanElement, DistortionTextProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
children,
|
|
23
|
+
type = 'wave',
|
|
24
|
+
speed = 'normal',
|
|
25
|
+
intensity = 'medium',
|
|
26
|
+
hoverOnly = false,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
},
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={`
|
|
36
|
+
${styles.distortion}
|
|
37
|
+
${styles[type]}
|
|
38
|
+
${styles[speed]}
|
|
39
|
+
${styles[intensity]}
|
|
40
|
+
${hoverOnly ? styles.hoverOnly : ''}
|
|
41
|
+
${className || ''}
|
|
42
|
+
`}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
DistortionText.displayName = 'DistortionText';
|
|
52
|
+
|
|
53
|
+
export default DistortionText;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.letter {
|
|
7
|
+
display: inline-block;
|
|
8
|
+
opacity: 0;
|
|
9
|
+
animation-fill-mode: forwards;
|
|
10
|
+
animation-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.down {
|
|
14
|
+
animation-name: fall-down;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.up {
|
|
18
|
+
animation-name: fall-up;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.loop {
|
|
22
|
+
animation-iteration-count: infinite;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes fall-down {
|
|
26
|
+
0% {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
transform: translateY(-100%) rotate(-10deg);
|
|
29
|
+
}
|
|
30
|
+
20% {
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
80% {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
100% {
|
|
37
|
+
opacity: 0;
|
|
38
|
+
transform: translateY(100%) rotate(10deg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@keyframes fall-up {
|
|
43
|
+
0% {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
transform: translateY(100%) rotate(10deg);
|
|
46
|
+
}
|
|
47
|
+
20% {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
80% {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
}
|
|
53
|
+
100% {
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transform: translateY(-100%) rotate(-10deg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './falling-text.module.css';
|
|
5
|
+
|
|
6
|
+
export interface FallingTextProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Text to display falling */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Fall duration in seconds */
|
|
10
|
+
duration?: number;
|
|
11
|
+
/** Stagger delay between letters in ms */
|
|
12
|
+
stagger?: number;
|
|
13
|
+
/** Loop the animation */
|
|
14
|
+
loop?: boolean;
|
|
15
|
+
/** Fall direction */
|
|
16
|
+
direction?: 'down' | 'up';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const FallingText = forwardRef<HTMLDivElement, FallingTextProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
children,
|
|
23
|
+
duration = 2,
|
|
24
|
+
stagger = 100,
|
|
25
|
+
loop = true,
|
|
26
|
+
direction = 'down',
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref
|
|
32
|
+
) => {
|
|
33
|
+
const letters = children.split('');
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={`${styles.container} ${className || ''}`}
|
|
39
|
+
style={style}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{letters.map((letter, i) => (
|
|
43
|
+
<span
|
|
44
|
+
key={i}
|
|
45
|
+
className={`${styles.letter} ${styles[direction]} ${loop ? styles.loop : ''}`}
|
|
46
|
+
style={{
|
|
47
|
+
animationDuration: `${duration}s`,
|
|
48
|
+
animationDelay: `${i * stagger}ms`,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{letter === ' ' ? '\u00A0' : letter}
|
|
52
|
+
</span>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
FallingText.displayName = 'FallingText';
|
|
60
|
+
|
|
61
|
+
export default FallingText;
|