@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
package/bin/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const COMPONENTS_DIR = path.join(__dirname, '..', 'components');
|
|
13
|
+
|
|
14
|
+
// Component registry
|
|
15
|
+
const COMPONENTS = {
|
|
16
|
+
// Overlays
|
|
17
|
+
'noise-overlay': { category: 'overlays', description: 'SVG fractal noise texture overlay', status: 'ready' },
|
|
18
|
+
'scanlines': { category: 'overlays', description: 'CRT-style horizontal scanlines', status: 'ready' },
|
|
19
|
+
'vignette': { category: 'overlays', description: 'Dark edges radial gradient', status: 'ready' },
|
|
20
|
+
'static-flicker': { category: 'overlays', description: 'Animated noise with flicker effect', status: 'ready' },
|
|
21
|
+
|
|
22
|
+
// Text effects
|
|
23
|
+
'glitch-text': { category: 'text', description: 'RGB split glitch text effect', status: 'ready' },
|
|
24
|
+
'flicker-text': { category: 'text', description: 'Text that flickers randomly', status: 'ready' },
|
|
25
|
+
'distortion-text': { category: 'text', description: 'Wave/shake/skew/blur text effects', status: 'ready' },
|
|
26
|
+
'falling-text': { category: 'text', description: 'Letters falling in cascade', status: 'ready' },
|
|
27
|
+
|
|
28
|
+
// Buttons
|
|
29
|
+
'glitch-button': { category: 'buttons', description: 'Button with glitch hover effect', status: 'ready' },
|
|
30
|
+
'chaos-button': { category: 'buttons', description: 'Chaotic animated button with debris', status: 'ready' },
|
|
31
|
+
|
|
32
|
+
// Backgrounds
|
|
33
|
+
'noise-canvas': { category: 'backgrounds', description: 'Animated noise canvas background', status: 'ready' },
|
|
34
|
+
'light-beams': { category: 'backgrounds', description: 'Vertical colored light beams', status: 'ready' },
|
|
35
|
+
'glow-orbs': { category: 'backgrounds', description: 'Floating blurred orbs', status: 'ready' },
|
|
36
|
+
'particle-field': { category: 'backgrounds', description: 'Drifting particle background', status: 'ready' },
|
|
37
|
+
|
|
38
|
+
// Effects
|
|
39
|
+
'warning-tape': { category: 'effects', description: 'Scrolling warning tape banner', status: 'ready' },
|
|
40
|
+
'cursor-follower': { category: 'effects', description: 'Custom cursor with trail', status: 'ready' },
|
|
41
|
+
'screen-distortion': { category: 'effects', description: 'Full screen distortion effect', status: 'ready' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const program = new Command();
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.name('chaos-ui')
|
|
48
|
+
.description('Add glitch & noise components to your project')
|
|
49
|
+
.version('0.1.0');
|
|
50
|
+
|
|
51
|
+
// LIST command
|
|
52
|
+
program
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('List all available components')
|
|
55
|
+
.action(() => {
|
|
56
|
+
console.log(pc.bold('\n🗡️ Chaos UI Components\n'));
|
|
57
|
+
|
|
58
|
+
const categories = {};
|
|
59
|
+
for (const [name, info] of Object.entries(COMPONENTS)) {
|
|
60
|
+
if (!categories[info.category]) categories[info.category] = [];
|
|
61
|
+
categories[info.category].push({ name, ...info });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [category, components] of Object.entries(categories)) {
|
|
65
|
+
console.log(pc.cyan(` ${category}/`));
|
|
66
|
+
for (const comp of components) {
|
|
67
|
+
console.log(` ${pc.white(comp.name.padEnd(20))} ${pc.dim(comp.description)}`);
|
|
68
|
+
}
|
|
69
|
+
console.log();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ADD command
|
|
74
|
+
program
|
|
75
|
+
.command('add [component]')
|
|
76
|
+
.description('Add a component to your project')
|
|
77
|
+
.option('-d, --dir <path>', 'Target directory', './components/chaos')
|
|
78
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
79
|
+
.action(async (componentName, options) => {
|
|
80
|
+
// If no component specified, show interactive picker
|
|
81
|
+
if (!componentName) {
|
|
82
|
+
const choices = Object.entries(COMPONENTS).map(([name, info]) => ({
|
|
83
|
+
title: name,
|
|
84
|
+
description: info.description,
|
|
85
|
+
value: name,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const response = await prompts({
|
|
89
|
+
type: 'autocomplete',
|
|
90
|
+
name: 'component',
|
|
91
|
+
message: 'Which component?',
|
|
92
|
+
choices,
|
|
93
|
+
suggest: (input, choices) =>
|
|
94
|
+
choices.filter(c => c.title.includes(input) || c.description.toLowerCase().includes(input.toLowerCase()))
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!response.component) {
|
|
98
|
+
console.log(pc.dim('Cancelled.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
componentName = response.component;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate component exists
|
|
105
|
+
if (!COMPONENTS[componentName]) {
|
|
106
|
+
console.log(pc.red(`\n✗ Component "${componentName}" not found.`));
|
|
107
|
+
console.log(pc.dim(` Run ${pc.white('chaos-ui list')} to see available components.\n`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const info = COMPONENTS[componentName];
|
|
112
|
+
const sourceDir = path.join(COMPONENTS_DIR, info.category, componentName);
|
|
113
|
+
const targetDir = path.resolve(options.dir, info.category);
|
|
114
|
+
|
|
115
|
+
// Check source exists
|
|
116
|
+
if (!fs.existsSync(sourceDir)) {
|
|
117
|
+
console.log(pc.yellow(`\n⚠ Component "${componentName}" is not yet implemented.`));
|
|
118
|
+
console.log(pc.dim(' Coming soon!\n'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Confirm
|
|
123
|
+
if (!options.yes) {
|
|
124
|
+
const confirm = await prompts({
|
|
125
|
+
type: 'confirm',
|
|
126
|
+
name: 'value',
|
|
127
|
+
message: `Add ${pc.cyan(componentName)} to ${pc.dim(targetDir)}?`,
|
|
128
|
+
initial: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!confirm.value) {
|
|
132
|
+
console.log(pc.dim('Cancelled.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copy files
|
|
138
|
+
try {
|
|
139
|
+
await fs.ensureDir(targetDir);
|
|
140
|
+
await fs.copy(sourceDir, path.join(targetDir, componentName));
|
|
141
|
+
|
|
142
|
+
console.log(pc.green(`\n✓ Added ${componentName}`));
|
|
143
|
+
console.log(pc.dim(` → ${path.join(targetDir, componentName)}\n`));
|
|
144
|
+
|
|
145
|
+
// Show usage hint
|
|
146
|
+
const pascalName = componentName
|
|
147
|
+
.split('-')
|
|
148
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
149
|
+
.join('');
|
|
150
|
+
|
|
151
|
+
console.log(pc.dim(' Import:'));
|
|
152
|
+
console.log(pc.white(` import { ${pascalName} } from './components/chaos/${info.category}/${componentName}';\n`));
|
|
153
|
+
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(pc.red(`\n✗ Failed to add component: ${err.message}\n`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// INIT command
|
|
160
|
+
program
|
|
161
|
+
.command('init')
|
|
162
|
+
.description('Initialize chaos-ui in your project')
|
|
163
|
+
.action(async () => {
|
|
164
|
+
console.log(pc.bold('\n🗡️ Initializing Chaos UI...\n'));
|
|
165
|
+
|
|
166
|
+
const response = await prompts([
|
|
167
|
+
{
|
|
168
|
+
type: 'select',
|
|
169
|
+
name: 'framework',
|
|
170
|
+
message: 'Framework?',
|
|
171
|
+
choices: [
|
|
172
|
+
{ title: 'React', value: 'react' },
|
|
173
|
+
{ title: 'Vue', value: 'vue' },
|
|
174
|
+
{ title: 'Vanilla (HTML/CSS/JS)', value: 'vanilla' },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: 'select',
|
|
179
|
+
name: 'styling',
|
|
180
|
+
message: 'Styling?',
|
|
181
|
+
choices: [
|
|
182
|
+
{ title: 'CSS Modules', value: 'css-modules' },
|
|
183
|
+
{ title: 'Plain CSS', value: 'css' },
|
|
184
|
+
{ title: 'Tailwind (with CSS vars)', value: 'tailwind' },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
name: 'dir',
|
|
190
|
+
message: 'Components directory?',
|
|
191
|
+
initial: './components/chaos',
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
if (!response.framework) {
|
|
196
|
+
console.log(pc.dim('Cancelled.'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Create config file
|
|
201
|
+
const config = {
|
|
202
|
+
framework: response.framework,
|
|
203
|
+
styling: response.styling,
|
|
204
|
+
componentsDir: response.dir,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
await fs.writeJson('./chaos-ui.json', config, { spaces: 2 });
|
|
208
|
+
await fs.ensureDir(response.dir);
|
|
209
|
+
|
|
210
|
+
console.log(pc.green('\n✓ Initialized!'));
|
|
211
|
+
console.log(pc.dim(` Config: ${pc.white('./chaos-ui.json')}`));
|
|
212
|
+
console.log(pc.dim(` Components: ${pc.white(response.dir)}\n`));
|
|
213
|
+
console.log(pc.dim(` Run ${pc.white('chaos-ui add <component>')} to add components.\n`));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
program.parse();
|
|
@@ -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,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
|
+
}
|