@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,127 @@
|
|
|
1
|
+
.distortion {
|
|
2
|
+
inset: 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
z-index: 9990;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* WAVE */
|
|
10
|
+
.wave {
|
|
11
|
+
background: transparent;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.wave::before {
|
|
15
|
+
content: '';
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: 0;
|
|
18
|
+
background: inherit;
|
|
19
|
+
animation: wave-distort var(--duration, 3s) ease-in-out infinite;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes wave-distort {
|
|
23
|
+
0%, 100% { transform: scaleY(1) skewX(0); }
|
|
24
|
+
25% { transform: scaleY(1.002) skewX(0.5deg); }
|
|
25
|
+
50% { transform: scaleY(0.998) skewX(0); }
|
|
26
|
+
75% { transform: scaleY(1.001) skewX(-0.5deg); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* GLITCH */
|
|
30
|
+
.glitch::before,
|
|
31
|
+
.glitch::after {
|
|
32
|
+
content: '';
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset: 0;
|
|
35
|
+
background: inherit;
|
|
36
|
+
opacity: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.glitch::before {
|
|
40
|
+
animation: glitch-distort-1 var(--duration, 4s) infinite;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.glitch::after {
|
|
44
|
+
animation: glitch-distort-2 var(--duration, 4s) infinite;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes glitch-distort-1 {
|
|
48
|
+
0%, 85%, 100% { opacity: 0; clip-path: inset(0 0 100% 0); }
|
|
49
|
+
86% { opacity: 1; clip-path: inset(20% 0 60% 0); transform: translateX(var(--shift, -5px)); background: rgba(255, 0, 64, 0.1); }
|
|
50
|
+
88% { opacity: 1; clip-path: inset(50% 0 30% 0); transform: translateX(var(--shift, 5px)); }
|
|
51
|
+
90% { opacity: 0; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@keyframes glitch-distort-2 {
|
|
55
|
+
0%, 85%, 100% { opacity: 0; clip-path: inset(0 0 100% 0); }
|
|
56
|
+
87% { opacity: 1; clip-path: inset(40% 0 40% 0); transform: translateX(var(--shift, 3px)); background: rgba(0, 255, 255, 0.1); }
|
|
57
|
+
89% { opacity: 1; clip-path: inset(70% 0 10% 0); transform: translateX(var(--shift, -3px)); }
|
|
58
|
+
91% { opacity: 0; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* CHROMATIC */
|
|
62
|
+
.chromatic::before,
|
|
63
|
+
.chromatic::after {
|
|
64
|
+
content: '';
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset: 0;
|
|
67
|
+
mix-blend-mode: screen;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.chromatic::before {
|
|
71
|
+
background: rgba(255, 0, 0, var(--opacity, 0.02));
|
|
72
|
+
animation: chromatic-r var(--duration, 5s) ease-in-out infinite;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.chromatic::after {
|
|
76
|
+
background: rgba(0, 255, 255, var(--opacity, 0.02));
|
|
77
|
+
animation: chromatic-c var(--duration, 5s) ease-in-out infinite reverse;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@keyframes chromatic-r {
|
|
81
|
+
0%, 100% { transform: translate(0); }
|
|
82
|
+
50% { transform: translate(var(--shift, 2px), var(--shift, -1px)); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes chromatic-c {
|
|
86
|
+
0%, 100% { transform: translate(0); }
|
|
87
|
+
50% { transform: translate(calc(var(--shift, 2px) * -1), var(--shift, 1px)); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* NOISE */
|
|
91
|
+
.noise {
|
|
92
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
93
|
+
opacity: var(--opacity, 0.02);
|
|
94
|
+
animation: noise-animate var(--duration, 0.2s) steps(5) infinite;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@keyframes noise-animate {
|
|
98
|
+
0% { transform: translate(0, 0); }
|
|
99
|
+
25% { transform: translate(-1px, 1px); }
|
|
100
|
+
50% { transform: translate(1px, -1px); }
|
|
101
|
+
75% { transform: translate(-1px, -1px); }
|
|
102
|
+
100% { transform: translate(0, 0); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* INTENSITY */
|
|
106
|
+
.subtle { --shift: 2px; --opacity: 0.01; }
|
|
107
|
+
.medium { --shift: 5px; --opacity: 0.02; }
|
|
108
|
+
.intense { --shift: 10px; --opacity: 0.04; }
|
|
109
|
+
|
|
110
|
+
/* SPEED */
|
|
111
|
+
.slow { --duration: 6s; }
|
|
112
|
+
.normal { --duration: 4s; }
|
|
113
|
+
.fast { --duration: 2s; }
|
|
114
|
+
|
|
115
|
+
.noise.slow { --duration: 0.3s; }
|
|
116
|
+
.noise.normal { --duration: 0.2s; }
|
|
117
|
+
.noise.fast { --duration: 0.1s; }
|
|
118
|
+
|
|
119
|
+
/* HOVER ONLY */
|
|
120
|
+
.hoverOnly {
|
|
121
|
+
opacity: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.hoverOnly:hover,
|
|
125
|
+
*:hover > .hoverOnly {
|
|
126
|
+
opacity: 1;
|
|
127
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './warning-tape.module.css';
|
|
5
|
+
|
|
6
|
+
export interface WarningTapeProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** The text to scroll */
|
|
8
|
+
children: string;
|
|
9
|
+
/** Background color */
|
|
10
|
+
bgColor?: string;
|
|
11
|
+
/** Text color */
|
|
12
|
+
textColor?: string;
|
|
13
|
+
/** Scroll speed in seconds */
|
|
14
|
+
duration?: number;
|
|
15
|
+
/** Rotation angle in degrees */
|
|
16
|
+
rotation?: number;
|
|
17
|
+
/** Reverse scroll direction */
|
|
18
|
+
reverse?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const WarningTape = forwardRef<HTMLDivElement, WarningTapeProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
children,
|
|
25
|
+
bgColor = '#ff0040',
|
|
26
|
+
textColor = '#000000',
|
|
27
|
+
duration = 20,
|
|
28
|
+
rotation = -1,
|
|
29
|
+
reverse = false,
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
// Repeat text for seamless scroll
|
|
37
|
+
const repeatedText = `${children} • `.repeat(20);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={`${styles.tape} ${className || ''}`}
|
|
43
|
+
style={{
|
|
44
|
+
backgroundColor: bgColor,
|
|
45
|
+
color: textColor,
|
|
46
|
+
transform: `rotate(${rotation}deg) scale(1.1)`,
|
|
47
|
+
...style,
|
|
48
|
+
}}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
className={`${styles.scroll} ${reverse ? styles.reverse : ''}`}
|
|
53
|
+
style={{ animationDuration: `${duration}s` }}
|
|
54
|
+
>
|
|
55
|
+
{repeatedText}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
WarningTape.displayName = 'WarningTape';
|
|
63
|
+
|
|
64
|
+
export default WarningTape;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.tape {
|
|
2
|
+
width: 100%;
|
|
3
|
+
padding: 0.75rem 0;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
white-space: nowrap;
|
|
6
|
+
font-weight: 700;
|
|
7
|
+
font-size: 0.75rem;
|
|
8
|
+
letter-spacing: 0.1em;
|
|
9
|
+
text-transform: uppercase;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.scroll {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
animation: scroll-left 20s linear infinite;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.reverse {
|
|
18
|
+
animation-name: scroll-right;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes scroll-left {
|
|
22
|
+
from { transform: translateX(0); }
|
|
23
|
+
to { transform: translateX(-50%); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes scroll-right {
|
|
27
|
+
from { transform: translateX(-50%); }
|
|
28
|
+
to { transform: translateX(0); }
|
|
29
|
+
}
|
|
@@ -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
|
+
.orb {
|
|
11
|
+
position: absolute;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
opacity: 0.3;
|
|
14
|
+
transform: translate(-50%, -50%);
|
|
15
|
+
animation: float 20s ease-in-out infinite;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@keyframes float {
|
|
19
|
+
0%, 100% {
|
|
20
|
+
transform: translate(-50%, -50%) translate(0, 0);
|
|
21
|
+
}
|
|
22
|
+
25% {
|
|
23
|
+
transform: translate(-50%, -50%) translate(30px, -40px);
|
|
24
|
+
}
|
|
25
|
+
50% {
|
|
26
|
+
transform: translate(-50%, -50%) translate(-20px, 20px);
|
|
27
|
+
}
|
|
28
|
+
75% {
|
|
29
|
+
transform: translate(-50%, -50%) translate(40px, 30px);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './glow-orbs.module.css';
|
|
5
|
+
|
|
6
|
+
export interface GlowOrbsProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Array of orb colors (will cycle through) */
|
|
8
|
+
colors?: string[];
|
|
9
|
+
/** Number of orbs */
|
|
10
|
+
count?: number;
|
|
11
|
+
/** Orb size range [min, max] in pixels */
|
|
12
|
+
sizeRange?: [number, number];
|
|
13
|
+
/** Blur amount in pixels */
|
|
14
|
+
blur?: number;
|
|
15
|
+
/** Animation duration range [min, max] in seconds */
|
|
16
|
+
durationRange?: [number, number];
|
|
17
|
+
/** Fixed or absolute positioning */
|
|
18
|
+
position?: 'fixed' | 'absolute';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const GlowOrbs = forwardRef<HTMLDivElement, GlowOrbsProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
colors = ['#7c3aed', '#06b6d4', '#ec4899', '#10b981'],
|
|
25
|
+
count = 5,
|
|
26
|
+
sizeRange = [100, 300],
|
|
27
|
+
blur = 80,
|
|
28
|
+
durationRange = [15, 25],
|
|
29
|
+
position = 'fixed',
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
// Generate random orbs
|
|
37
|
+
const orbs = 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 color = colors[i % colors.length];
|
|
42
|
+
const startX = Math.random() * 100;
|
|
43
|
+
const startY = Math.random() * 100;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: i,
|
|
47
|
+
size,
|
|
48
|
+
duration,
|
|
49
|
+
delay,
|
|
50
|
+
color,
|
|
51
|
+
startX,
|
|
52
|
+
startY,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={`${styles.container} ${className || ''}`}
|
|
60
|
+
style={{ position, ...style }}
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
{orbs.map((orb) => (
|
|
65
|
+
<div
|
|
66
|
+
key={orb.id}
|
|
67
|
+
className={styles.orb}
|
|
68
|
+
style={{
|
|
69
|
+
width: orb.size,
|
|
70
|
+
height: orb.size,
|
|
71
|
+
background: orb.color,
|
|
72
|
+
filter: `blur(${blur}px)`,
|
|
73
|
+
left: `${orb.startX}%`,
|
|
74
|
+
top: `${orb.startY}%`,
|
|
75
|
+
animationDuration: `${orb.duration}s`,
|
|
76
|
+
animationDelay: `${orb.delay}s`,
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
GlowOrbs.displayName = 'GlowOrbs';
|
|
86
|
+
|
|
87
|
+
export default GlowOrbs;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './light-beams.module.css';
|
|
5
|
+
|
|
6
|
+
export interface LightBeamsProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Array of beam colors */
|
|
8
|
+
colors?: string[];
|
|
9
|
+
/** Number of beams */
|
|
10
|
+
count?: number;
|
|
11
|
+
/** Beam width in pixels */
|
|
12
|
+
beamWidth?: number;
|
|
13
|
+
/** Beam opacity (0-1) */
|
|
14
|
+
opacity?: number;
|
|
15
|
+
/** Animation duration range [min, max] in seconds */
|
|
16
|
+
durationRange?: [number, number];
|
|
17
|
+
/** Fixed or absolute positioning */
|
|
18
|
+
position?: 'fixed' | 'absolute';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const LightBeams = forwardRef<HTMLDivElement, LightBeamsProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
colors = ['#7c3aed', '#06b6d4', '#ec4899', '#10b981', '#f59e0b'],
|
|
25
|
+
count = 5,
|
|
26
|
+
beamWidth = 2,
|
|
27
|
+
opacity = 0.15,
|
|
28
|
+
durationRange = [15, 25],
|
|
29
|
+
position = 'fixed',
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
},
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
const beams = Array.from({ length: count }, (_, i) => {
|
|
37
|
+
const duration = durationRange[0] + Math.random() * (durationRange[1] - durationRange[0]);
|
|
38
|
+
const delay = Math.random() * -duration;
|
|
39
|
+
const color = colors[i % colors.length];
|
|
40
|
+
const positionX = ((i + 1) / (count + 1)) * 100;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: i,
|
|
44
|
+
duration,
|
|
45
|
+
delay,
|
|
46
|
+
color,
|
|
47
|
+
positionX,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={`${styles.container} ${className || ''}`}
|
|
55
|
+
style={{ position, ...style }}
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{beams.map((beam) => (
|
|
60
|
+
<div
|
|
61
|
+
key={beam.id}
|
|
62
|
+
className={styles.beam}
|
|
63
|
+
style={{
|
|
64
|
+
left: `${beam.positionX}%`,
|
|
65
|
+
width: beamWidth,
|
|
66
|
+
background: `linear-gradient(180deg, transparent, ${beam.color}, transparent)`,
|
|
67
|
+
opacity,
|
|
68
|
+
animationDuration: `${beam.duration}s`,
|
|
69
|
+
animationDelay: `${beam.delay}s`,
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
LightBeams.displayName = 'LightBeams';
|
|
79
|
+
|
|
80
|
+
export default LightBeams;
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
.beam {
|
|
11
|
+
position: absolute;
|
|
12
|
+
top: 0;
|
|
13
|
+
height: 100%;
|
|
14
|
+
filter: blur(1px);
|
|
15
|
+
animation: beam-move 20s ease-in-out infinite;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@keyframes beam-move {
|
|
19
|
+
0%, 100% {
|
|
20
|
+
transform: translateX(-50px) skewX(-5deg);
|
|
21
|
+
opacity: 0.1;
|
|
22
|
+
}
|
|
23
|
+
50% {
|
|
24
|
+
transform: translateX(50px) skewX(5deg);
|
|
25
|
+
opacity: 0.2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './noise-canvas.module.css';
|
|
5
|
+
|
|
6
|
+
export interface NoiseCanvasProps extends HTMLAttributes<HTMLCanvasElement> {
|
|
7
|
+
/** Noise opacity (0-1) */
|
|
8
|
+
opacity?: number;
|
|
9
|
+
/** Animation speed (fps) */
|
|
10
|
+
fps?: number;
|
|
11
|
+
/** Noise intensity (0-255) */
|
|
12
|
+
intensity?: number;
|
|
13
|
+
/** Monochrome or colored noise */
|
|
14
|
+
monochrome?: boolean;
|
|
15
|
+
/** Fixed or absolute positioning */
|
|
16
|
+
position?: 'fixed' | 'absolute';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const NoiseCanvas = forwardRef<HTMLCanvasElement, NoiseCanvasProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
opacity = 0.05,
|
|
23
|
+
fps = 24,
|
|
24
|
+
intensity = 50,
|
|
25
|
+
monochrome = true,
|
|
26
|
+
position = 'fixed',
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref
|
|
32
|
+
) => {
|
|
33
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
34
|
+
const animationRef = useRef<number>();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const canvas = canvasRef.current;
|
|
38
|
+
if (!canvas) return;
|
|
39
|
+
|
|
40
|
+
const ctx = canvas.getContext('2d');
|
|
41
|
+
if (!ctx) return;
|
|
42
|
+
|
|
43
|
+
const resize = () => {
|
|
44
|
+
canvas.width = window.innerWidth;
|
|
45
|
+
canvas.height = window.innerHeight;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
resize();
|
|
49
|
+
window.addEventListener('resize', resize);
|
|
50
|
+
|
|
51
|
+
let lastFrame = 0;
|
|
52
|
+
const frameInterval = 1000 / fps;
|
|
53
|
+
|
|
54
|
+
const render = (timestamp: number) => {
|
|
55
|
+
if (timestamp - lastFrame >= frameInterval) {
|
|
56
|
+
const imageData = ctx.createImageData(canvas.width, canvas.height);
|
|
57
|
+
const data = imageData.data;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
60
|
+
const value = Math.random() * intensity;
|
|
61
|
+
|
|
62
|
+
if (monochrome) {
|
|
63
|
+
data[i] = value;
|
|
64
|
+
data[i + 1] = value;
|
|
65
|
+
data[i + 2] = value;
|
|
66
|
+
} else {
|
|
67
|
+
data[i] = Math.random() * intensity;
|
|
68
|
+
data[i + 1] = Math.random() * intensity;
|
|
69
|
+
data[i + 2] = Math.random() * intensity;
|
|
70
|
+
}
|
|
71
|
+
data[i + 3] = 255;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ctx.putImageData(imageData, 0, 0);
|
|
75
|
+
lastFrame = timestamp;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
animationRef.current = requestAnimationFrame(render);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
animationRef.current = requestAnimationFrame(render);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
window.removeEventListener('resize', resize);
|
|
85
|
+
if (animationRef.current) {
|
|
86
|
+
cancelAnimationFrame(animationRef.current);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}, [fps, intensity, monochrome]);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<canvas
|
|
93
|
+
ref={(node) => {
|
|
94
|
+
(canvasRef as React.MutableRefObject<HTMLCanvasElement | null>).current = node;
|
|
95
|
+
if (typeof ref === 'function') ref(node);
|
|
96
|
+
else if (ref) ref.current = node;
|
|
97
|
+
}}
|
|
98
|
+
className={`${styles.canvas} ${className || ''}`}
|
|
99
|
+
style={{
|
|
100
|
+
position,
|
|
101
|
+
opacity,
|
|
102
|
+
...style,
|
|
103
|
+
}}
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
NoiseCanvas.displayName = 'NoiseCanvas';
|
|
112
|
+
|
|
113
|
+
export default NoiseCanvas;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, HTMLAttributes } from 'react';
|
|
4
|
+
import styles from './noise-overlay.module.css';
|
|
5
|
+
|
|
6
|
+
export interface NoiseOverlayProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Noise opacity (0-1) */
|
|
8
|
+
opacity?: number;
|
|
9
|
+
/** Noise frequency (higher = finer grain) */
|
|
10
|
+
frequency?: number;
|
|
11
|
+
/** Enable animation */
|
|
12
|
+
animated?: boolean;
|
|
13
|
+
/** Blend mode */
|
|
14
|
+
blendMode?: 'overlay' | 'multiply' | 'screen' | 'soft-light' | 'normal';
|
|
15
|
+
/** Fixed position (covers viewport) or absolute (covers parent) */
|
|
16
|
+
position?: 'fixed' | 'absolute';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const NoiseOverlay = forwardRef<HTMLDivElement, NoiseOverlayProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
opacity = 0.05,
|
|
23
|
+
frequency = 0.8,
|
|
24
|
+
animated = false,
|
|
25
|
+
blendMode = 'overlay',
|
|
26
|
+
position = 'fixed',
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref
|
|
32
|
+
) => {
|
|
33
|
+
// Generate SVG noise inline
|
|
34
|
+
const noiseSvg = `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='${frequency}' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={`${styles.noise} ${animated ? styles.animated : ''} ${className || ''}`}
|
|
40
|
+
style={{
|
|
41
|
+
position,
|
|
42
|
+
opacity,
|
|
43
|
+
mixBlendMode: blendMode,
|
|
44
|
+
backgroundImage: noiseSvg,
|
|
45
|
+
...style,
|
|
46
|
+
}}
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
NoiseOverlay.displayName = 'NoiseOverlay';
|
|
55
|
+
|
|
56
|
+
export default NoiseOverlay;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.noise {
|
|
2
|
+
inset: 0;
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
z-index: 9999;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.animated {
|
|
10
|
+
animation: noise-shift 0.2s steps(5) infinite;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@keyframes noise-shift {
|
|
14
|
+
0% { transform: translate(0, 0); }
|
|
15
|
+
20% { transform: translate(-2px, 1px); }
|
|
16
|
+
40% { transform: translate(1px, -1px); }
|
|
17
|
+
60% { transform: translate(2px, 2px); }
|
|
18
|
+
80% { transform: translate(-1px, -2px); }
|
|
19
|
+
100% { transform: translate(0, 0); }
|
|
20
|
+
}
|