create-neon-flux 1.0.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/.env.example +5 -0
- package/.eslintrc.json +3 -0
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/app/globals.css +170 -0
- package/app/layout.tsx +29 -0
- package/app/page.tsx +289 -0
- package/assets/banner.svg +76 -0
- package/bin/create-app.js +99 -0
- package/components/3d/CyberCity.tsx +260 -0
- package/components/animations/CyberGrid.tsx +13 -0
- package/components/animations/GlitchText.tsx +17 -0
- package/components/animations/Hero.tsx +88 -0
- package/components/effects/Particles.tsx +180 -0
- package/components/effects/ScanLines.tsx +5 -0
- package/components/hooks/useAudioReactive.ts +68 -0
- package/components/hooks/useGlitch.ts +26 -0
- package/components/hooks/useNeonGlow.ts +31 -0
- package/components/hooks/useParallax.ts +23 -0
- package/components/ui/CyberNav.tsx +129 -0
- package/components/ui/HologramCard.tsx +107 -0
- package/components/ui/NeonButton.tsx +27 -0
- package/components/ui/Terminal.tsx +56 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +16 -0
- package/package.json +61 -0
- package/postcss.config.mjs +9 -0
- package/styles/animations.css +204 -0
- package/styles/glitch.css +273 -0
- package/styles/neon.css +130 -0
- package/tailwind.config.ts +112 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function CyberCity() {
|
|
6
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// Check for reduced motion preference
|
|
10
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
11
|
+
|
|
12
|
+
const canvas = canvasRef.current;
|
|
13
|
+
if (!canvas) return;
|
|
14
|
+
|
|
15
|
+
const ctx = canvas.getContext('2d');
|
|
16
|
+
if (!ctx) return;
|
|
17
|
+
|
|
18
|
+
// Set canvas size
|
|
19
|
+
let width = window.innerWidth;
|
|
20
|
+
let height = window.innerHeight;
|
|
21
|
+
canvas.width = width;
|
|
22
|
+
canvas.height = height;
|
|
23
|
+
|
|
24
|
+
// Create initial gradient
|
|
25
|
+
let bgGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
26
|
+
bgGradient.addColorStop(0, '#0D0221');
|
|
27
|
+
bgGradient.addColorStop(1, '#0A0A0A');
|
|
28
|
+
|
|
29
|
+
const setCanvasSize = () => {
|
|
30
|
+
width = window.innerWidth;
|
|
31
|
+
height = window.innerHeight;
|
|
32
|
+
canvas.width = width;
|
|
33
|
+
canvas.height = height;
|
|
34
|
+
// Re-cache gradient on resize
|
|
35
|
+
bgGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
36
|
+
bgGradient.addColorStop(0, '#0D0221');
|
|
37
|
+
bgGradient.addColorStop(1, '#0A0A0A');
|
|
38
|
+
};
|
|
39
|
+
window.addEventListener('resize', setCanvasSize);
|
|
40
|
+
|
|
41
|
+
// Building data with antenna flag pre-determined
|
|
42
|
+
interface Building {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
color: string;
|
|
48
|
+
windows: Array<{ x: number; y: number; lit: boolean }>;
|
|
49
|
+
hasAntenna: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const buildings: Building[] = [];
|
|
53
|
+
const colors = ['#FF10F0', '#00FFF0', '#B026FF', '#00D4FF'];
|
|
54
|
+
const buildingCount = 25; // Reduced for performance
|
|
55
|
+
|
|
56
|
+
// Generate buildings
|
|
57
|
+
for (let i = 0; i < buildingCount; i++) {
|
|
58
|
+
const bWidth = 40 + Math.random() * 60;
|
|
59
|
+
const bHeight = 100 + Math.random() * 300;
|
|
60
|
+
const x = (width / buildingCount) * i;
|
|
61
|
+
const y = height - bHeight;
|
|
62
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
63
|
+
|
|
64
|
+
// Generate windows
|
|
65
|
+
const windows = [];
|
|
66
|
+
const windowRows = Math.floor(bHeight / 20);
|
|
67
|
+
const windowCols = Math.floor(bWidth / 15);
|
|
68
|
+
|
|
69
|
+
for (let row = 0; row < windowRows; row++) {
|
|
70
|
+
for (let col = 0; col < windowCols; col++) {
|
|
71
|
+
windows.push({
|
|
72
|
+
x: x + col * 15 + 5,
|
|
73
|
+
y: y + row * 20 + 5,
|
|
74
|
+
lit: Math.random() > 0.3,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
buildings.push({
|
|
80
|
+
x, y, width: bWidth, height: bHeight, color, windows,
|
|
81
|
+
hasAntenna: Math.random() > 0.7 // Pre-determine antenna
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Animation variables
|
|
86
|
+
let animationFrame: number;
|
|
87
|
+
let time = 0;
|
|
88
|
+
|
|
89
|
+
// Stars - reduced count
|
|
90
|
+
const stars: Array<{ x: number; y: number; size: number; phaseOffset: number }> = [];
|
|
91
|
+
for (let i = 0; i < 60; i++) { // Reduced from 100
|
|
92
|
+
stars.push({
|
|
93
|
+
x: Math.random() * width,
|
|
94
|
+
y: Math.random() * height * 0.6,
|
|
95
|
+
size: Math.random() * 2,
|
|
96
|
+
phaseOffset: Math.random() * Math.PI * 2,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Grid
|
|
101
|
+
const gridSpacing = 50;
|
|
102
|
+
|
|
103
|
+
// If reduced motion, draw static scene once
|
|
104
|
+
if (prefersReducedMotion) {
|
|
105
|
+
ctx.fillStyle = bgGradient;
|
|
106
|
+
ctx.fillRect(0, 0, width, height);
|
|
107
|
+
|
|
108
|
+
// Static stars
|
|
109
|
+
stars.forEach((star) => {
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
|
112
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
113
|
+
ctx.fill();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Static buildings
|
|
117
|
+
buildings.forEach((building) => {
|
|
118
|
+
ctx.fillStyle = 'rgba(10, 10, 10, 0.8)';
|
|
119
|
+
ctx.fillRect(building.x, building.y, building.width, building.height);
|
|
120
|
+
ctx.strokeStyle = building.color;
|
|
121
|
+
ctx.lineWidth = 2;
|
|
122
|
+
ctx.strokeRect(building.x, building.y, building.width, building.height);
|
|
123
|
+
|
|
124
|
+
building.windows.forEach((win) => {
|
|
125
|
+
if (win.lit) {
|
|
126
|
+
ctx.fillStyle = 'rgba(0, 255, 240, 0.6)';
|
|
127
|
+
ctx.fillRect(win.x, win.y, 8, 8);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Animation loop
|
|
135
|
+
const animate = () => {
|
|
136
|
+
time += 0.01;
|
|
137
|
+
|
|
138
|
+
// Clear with cached gradient
|
|
139
|
+
ctx.fillStyle = bgGradient;
|
|
140
|
+
ctx.fillRect(0, 0, width, height);
|
|
141
|
+
|
|
142
|
+
// Draw stars with pre-calculated phase
|
|
143
|
+
for (let i = 0; i < stars.length; i++) {
|
|
144
|
+
const star = stars[i];
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
|
147
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${0.5 + Math.sin(time * 2 + star.phaseOffset) * 0.5})`;
|
|
148
|
+
ctx.fill();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Draw perspective grid
|
|
152
|
+
ctx.lineWidth = 1;
|
|
153
|
+
const horizon = height * 0.6;
|
|
154
|
+
|
|
155
|
+
// Horizontal lines
|
|
156
|
+
for (let i = 0; i < 10; i++) {
|
|
157
|
+
const y = horizon + i * 30;
|
|
158
|
+
const perspective = (y - horizon) / (height - horizon);
|
|
159
|
+
ctx.beginPath();
|
|
160
|
+
ctx.moveTo(0, y);
|
|
161
|
+
ctx.lineTo(width, y);
|
|
162
|
+
ctx.strokeStyle = `rgba(0, 255, 240, ${0.05 + perspective * 0.1})`;
|
|
163
|
+
ctx.stroke();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Vertical lines
|
|
167
|
+
const vanishingPointX = width / 2;
|
|
168
|
+
for (let i = -10; i <= 10; i++) {
|
|
169
|
+
const x = width / 2 + i * gridSpacing;
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
ctx.moveTo(x, height);
|
|
172
|
+
ctx.lineTo(vanishingPointX + (x - vanishingPointX) * 0.3, horizon);
|
|
173
|
+
ctx.strokeStyle = 'rgba(0, 255, 240, 0.05)';
|
|
174
|
+
ctx.stroke();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Draw buildings - batch similar operations
|
|
178
|
+
ctx.fillStyle = 'rgba(10, 10, 10, 0.8)';
|
|
179
|
+
for (let i = 0; i < buildings.length; i++) {
|
|
180
|
+
const building = buildings[i];
|
|
181
|
+
ctx.fillRect(building.x, building.y, building.width, building.height);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Building borders and details
|
|
185
|
+
for (let i = 0; i < buildings.length; i++) {
|
|
186
|
+
const building = buildings[i];
|
|
187
|
+
|
|
188
|
+
// Neon border
|
|
189
|
+
ctx.strokeStyle = building.color;
|
|
190
|
+
ctx.lineWidth = 2;
|
|
191
|
+
ctx.shadowColor = building.color;
|
|
192
|
+
ctx.shadowBlur = 10;
|
|
193
|
+
ctx.strokeRect(building.x, building.y, building.width, building.height);
|
|
194
|
+
ctx.shadowBlur = 0;
|
|
195
|
+
|
|
196
|
+
// Windows - batch by lit state
|
|
197
|
+
ctx.fillStyle = 'rgba(0, 255, 240, 0.6)';
|
|
198
|
+
for (let j = 0; j < building.windows.length; j++) {
|
|
199
|
+
const win = building.windows[j];
|
|
200
|
+
if (win.lit) {
|
|
201
|
+
ctx.fillRect(win.x, win.y, 8, 8);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Antenna (pre-determined)
|
|
206
|
+
if (building.hasAntenna) {
|
|
207
|
+
const antennaX = building.x + building.width / 2;
|
|
208
|
+
const antennaHeight = 20;
|
|
209
|
+
|
|
210
|
+
ctx.strokeStyle = building.color;
|
|
211
|
+
ctx.lineWidth = 2;
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.moveTo(antennaX, building.y);
|
|
214
|
+
ctx.lineTo(antennaX, building.y - antennaHeight);
|
|
215
|
+
ctx.stroke();
|
|
216
|
+
|
|
217
|
+
// Blinking light
|
|
218
|
+
if (Math.sin(time * 5) > 0.5) {
|
|
219
|
+
ctx.fillStyle = '#FF10F0';
|
|
220
|
+
ctx.shadowColor = '#FF10F0';
|
|
221
|
+
ctx.shadowBlur = 10;
|
|
222
|
+
ctx.beginPath();
|
|
223
|
+
ctx.arc(antennaX, building.y - antennaHeight, 3, 0, Math.PI * 2);
|
|
224
|
+
ctx.fill();
|
|
225
|
+
ctx.shadowBlur = 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Scanline effect
|
|
231
|
+
ctx.fillStyle = 'rgba(0, 255, 240, 0.02)';
|
|
232
|
+
const scanlineY = (time * 100) % height;
|
|
233
|
+
ctx.fillRect(0, scanlineY, width, 2);
|
|
234
|
+
|
|
235
|
+
animationFrame = requestAnimationFrame(animate);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
animate();
|
|
239
|
+
|
|
240
|
+
return () => {
|
|
241
|
+
cancelAnimationFrame(animationFrame);
|
|
242
|
+
window.removeEventListener('resize', setCanvasSize);
|
|
243
|
+
};
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<canvas
|
|
248
|
+
ref={canvasRef}
|
|
249
|
+
className="canvas-container"
|
|
250
|
+
style={{
|
|
251
|
+
position: 'fixed',
|
|
252
|
+
top: 0,
|
|
253
|
+
left: 0,
|
|
254
|
+
width: '100%',
|
|
255
|
+
height: '100%',
|
|
256
|
+
zIndex: -1,
|
|
257
|
+
}}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface GlitchTextProps {
|
|
4
|
+
text: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function GlitchText({ text, className = '' }: GlitchTextProps) {
|
|
9
|
+
return (
|
|
10
|
+
<h1
|
|
11
|
+
className={`glitch neon-text-pink ${className}`}
|
|
12
|
+
data-text={text}
|
|
13
|
+
>
|
|
14
|
+
{text}
|
|
15
|
+
</h1>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import GlitchText from './GlitchText';
|
|
5
|
+
import NeonButton from '../ui/NeonButton';
|
|
6
|
+
import Particles from '../effects/Particles';
|
|
7
|
+
|
|
8
|
+
export default function Hero() {
|
|
9
|
+
const [mounted, setMounted] = useState(false);
|
|
10
|
+
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setMounted(true);
|
|
14
|
+
|
|
15
|
+
// Hide scroll indicator after user scrolls
|
|
16
|
+
const handleScroll = () => {
|
|
17
|
+
if (window.scrollY > 100) {
|
|
18
|
+
setShowScrollIndicator(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
22
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const scrollToSection = (id: string) => {
|
|
26
|
+
const element = document.getElementById(id);
|
|
27
|
+
element?.scrollIntoView({ behavior: 'smooth' });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<section
|
|
32
|
+
id="home"
|
|
33
|
+
className="relative z-10 min-h-screen flex items-center justify-center pt-20"
|
|
34
|
+
>
|
|
35
|
+
{/* Particles Background */}
|
|
36
|
+
{mounted && <Particles />}
|
|
37
|
+
|
|
38
|
+
<div className="container mx-auto px-4 text-center">
|
|
39
|
+
<div className="space-y-8 fade-in">
|
|
40
|
+
{/* Main Title with Glitch Effect */}
|
|
41
|
+
<GlitchText
|
|
42
|
+
text="NEON FLUX"
|
|
43
|
+
className="text-5xl md:text-7xl lg:text-9xl font-orbitron font-bold"
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
{/* Subtitle */}
|
|
47
|
+
<h2 className="text-2xl md:text-4xl font-rajdhani text-neon-cyan text-shadow-neon-cyan">
|
|
48
|
+
Cyberpunk Portfolio Template
|
|
49
|
+
</h2>
|
|
50
|
+
|
|
51
|
+
{/* Description */}
|
|
52
|
+
<p className="text-lg md:text-xl text-neon-cyan/80 max-w-2xl mx-auto font-space-mono">
|
|
53
|
+
Showcase your projects with stunning graphics, smooth animations,
|
|
54
|
+
and cyberpunk aesthetics. Built with Next.js 15 & TypeScript.
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
{/* CTA Buttons */}
|
|
58
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
|
|
59
|
+
<NeonButton
|
|
60
|
+
variant="primary"
|
|
61
|
+
onClick={() => scrollToSection('portfolio')}
|
|
62
|
+
>
|
|
63
|
+
View Portfolio
|
|
64
|
+
</NeonButton>
|
|
65
|
+
<NeonButton
|
|
66
|
+
variant="secondary"
|
|
67
|
+
onClick={() => scrollToSection('blog')}
|
|
68
|
+
>
|
|
69
|
+
Read Blog
|
|
70
|
+
</NeonButton>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Scroll Indicator - hidden after scroll */}
|
|
74
|
+
{showScrollIndicator && (
|
|
75
|
+
<div
|
|
76
|
+
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce cursor-pointer transition-opacity duration-300"
|
|
77
|
+
onClick={() => scrollToSection('portfolio')}
|
|
78
|
+
>
|
|
79
|
+
<div className="w-6 h-10 border-2 border-neon-cyan rounded-full flex justify-center">
|
|
80
|
+
<div className="w-1 h-3 bg-neon-cyan rounded-full mt-2 animate-pulse" />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function ParticlesBackground() {
|
|
6
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
7
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
8
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
9
|
+
|
|
10
|
+
// Intersection Observer to pause when off-screen
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const container = containerRef.current;
|
|
13
|
+
if (!container) return;
|
|
14
|
+
|
|
15
|
+
const observer = new IntersectionObserver(
|
|
16
|
+
([entry]) => setIsVisible(entry.isIntersecting),
|
|
17
|
+
{ threshold: 0 }
|
|
18
|
+
);
|
|
19
|
+
observer.observe(container);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Check for reduced motion preference
|
|
25
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
26
|
+
if (prefersReducedMotion) return;
|
|
27
|
+
|
|
28
|
+
const canvas = canvasRef.current;
|
|
29
|
+
if (!canvas) return;
|
|
30
|
+
|
|
31
|
+
const ctx = canvas.getContext('2d');
|
|
32
|
+
if (!ctx) return;
|
|
33
|
+
|
|
34
|
+
let animationFrame: number;
|
|
35
|
+
let isRunning = true;
|
|
36
|
+
|
|
37
|
+
// Set canvas size
|
|
38
|
+
const setCanvasSize = () => {
|
|
39
|
+
canvas.width = window.innerWidth;
|
|
40
|
+
canvas.height = window.innerHeight;
|
|
41
|
+
};
|
|
42
|
+
setCanvasSize();
|
|
43
|
+
window.addEventListener('resize', setCanvasSize);
|
|
44
|
+
|
|
45
|
+
// Particle system - reduced count for better performance
|
|
46
|
+
const particles: Array<{
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
vx: number;
|
|
50
|
+
vy: number;
|
|
51
|
+
color: string;
|
|
52
|
+
size: number;
|
|
53
|
+
}> = [];
|
|
54
|
+
|
|
55
|
+
const colors = ['#FF10F0', '#00FFF0', '#FFFF00'];
|
|
56
|
+
const particleCount = 50; // Reduced from 80
|
|
57
|
+
|
|
58
|
+
// Initialize particles
|
|
59
|
+
for (let i = 0; i < particleCount; i++) {
|
|
60
|
+
particles.push({
|
|
61
|
+
x: Math.random() * canvas.width,
|
|
62
|
+
y: Math.random() * canvas.height,
|
|
63
|
+
vx: (Math.random() - 0.5) * 0.5,
|
|
64
|
+
vy: (Math.random() - 0.5) * 0.5,
|
|
65
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
66
|
+
size: Math.random() * 2 + 1,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Mouse position
|
|
71
|
+
let mouseX = 0;
|
|
72
|
+
let mouseY = 0;
|
|
73
|
+
|
|
74
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
75
|
+
mouseX = e.clientX;
|
|
76
|
+
mouseY = e.clientY;
|
|
77
|
+
};
|
|
78
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
79
|
+
|
|
80
|
+
// Spatial grid for O(n) neighbor lookup
|
|
81
|
+
const CELL_SIZE = 150;
|
|
82
|
+
const getGridKey = (x: number, y: number) =>
|
|
83
|
+
`${Math.floor(x / CELL_SIZE)},${Math.floor(y / CELL_SIZE)}`;
|
|
84
|
+
|
|
85
|
+
// Animation loop
|
|
86
|
+
const animate = () => {
|
|
87
|
+
ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
|
|
88
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
89
|
+
|
|
90
|
+
// Build spatial grid
|
|
91
|
+
const grid: Map<string, number[]> = new Map();
|
|
92
|
+
particles.forEach((particle, i) => {
|
|
93
|
+
const key = getGridKey(particle.x, particle.y);
|
|
94
|
+
if (!grid.has(key)) grid.set(key, []);
|
|
95
|
+
grid.get(key)!.push(i);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
particles.forEach((particle, i) => {
|
|
99
|
+
// Update position
|
|
100
|
+
particle.x += particle.vx;
|
|
101
|
+
particle.y += particle.vy;
|
|
102
|
+
|
|
103
|
+
// Bounce off edges
|
|
104
|
+
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
|
105
|
+
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
|
106
|
+
|
|
107
|
+
// Mouse repulsion
|
|
108
|
+
const dx = mouseX - particle.x;
|
|
109
|
+
const dy = mouseY - particle.y;
|
|
110
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
111
|
+
|
|
112
|
+
if (distance < 100) {
|
|
113
|
+
particle.x -= (dx / distance) * 2;
|
|
114
|
+
particle.y -= (dy / distance) * 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Draw particle
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
|
120
|
+
ctx.fillStyle = particle.color;
|
|
121
|
+
ctx.fill();
|
|
122
|
+
|
|
123
|
+
// Draw connections - only check neighboring cells
|
|
124
|
+
const cellX = Math.floor(particle.x / CELL_SIZE);
|
|
125
|
+
const cellY = Math.floor(particle.y / CELL_SIZE);
|
|
126
|
+
|
|
127
|
+
for (let ox = -1; ox <= 1; ox++) {
|
|
128
|
+
for (let oy = -1; oy <= 1; oy++) {
|
|
129
|
+
const neighborKey = `${cellX + ox},${cellY + oy}`;
|
|
130
|
+
const neighbors = grid.get(neighborKey);
|
|
131
|
+
if (!neighbors) continue;
|
|
132
|
+
|
|
133
|
+
for (const j of neighbors) {
|
|
134
|
+
if (i >= j) continue; // Avoid duplicate lines
|
|
135
|
+
|
|
136
|
+
const other = particles[j];
|
|
137
|
+
const dx = particle.x - other.x;
|
|
138
|
+
const dy = particle.y - other.y;
|
|
139
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
140
|
+
|
|
141
|
+
if (dist < 150) {
|
|
142
|
+
ctx.beginPath();
|
|
143
|
+
ctx.moveTo(particle.x, particle.y);
|
|
144
|
+
ctx.lineTo(other.x, other.y);
|
|
145
|
+
ctx.strokeStyle = `rgba(0, 255, 240, ${0.3 * (1 - dist / 150)})`;
|
|
146
|
+
ctx.lineWidth = 1;
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (isRunning) {
|
|
155
|
+
animationFrame = requestAnimationFrame(animate);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
animate();
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
isRunning = false;
|
|
163
|
+
cancelAnimationFrame(animationFrame);
|
|
164
|
+
window.removeEventListener('resize', setCanvasSize);
|
|
165
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
166
|
+
};
|
|
167
|
+
}, [isVisible]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div ref={containerRef} className="absolute inset-0 z-0">
|
|
171
|
+
<canvas
|
|
172
|
+
ref={canvasRef}
|
|
173
|
+
className="w-full h-full"
|
|
174
|
+
style={{
|
|
175
|
+
pointerEvents: 'none',
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
interface AudioReactiveData {
|
|
6
|
+
volume: number;
|
|
7
|
+
frequency: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useAudioReactive(enabled: boolean = false) {
|
|
11
|
+
const [audioData, setAudioData] = useState<AudioReactiveData>({
|
|
12
|
+
volume: 0,
|
|
13
|
+
frequency: 0,
|
|
14
|
+
});
|
|
15
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
16
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
17
|
+
const dataArrayRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!enabled || typeof window === 'undefined') return;
|
|
21
|
+
|
|
22
|
+
const setupAudio = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
25
|
+
const audioContext = new AudioContext();
|
|
26
|
+
const source = audioContext.createMediaStreamSource(stream);
|
|
27
|
+
const analyser = audioContext.createAnalyser();
|
|
28
|
+
|
|
29
|
+
analyser.fftSize = 256;
|
|
30
|
+
source.connect(analyser);
|
|
31
|
+
|
|
32
|
+
audioContextRef.current = audioContext;
|
|
33
|
+
analyserRef.current = analyser;
|
|
34
|
+
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
|
|
35
|
+
|
|
36
|
+
const updateAudioData = () => {
|
|
37
|
+
if (!analyserRef.current || !dataArrayRef.current) return;
|
|
38
|
+
|
|
39
|
+
analyserRef.current.getByteFrequencyData(dataArrayRef.current);
|
|
40
|
+
|
|
41
|
+
const volume =
|
|
42
|
+
dataArrayRef.current.reduce((sum, value) => sum + value, 0) /
|
|
43
|
+
dataArrayRef.current.length;
|
|
44
|
+
|
|
45
|
+
const frequency = dataArrayRef.current[0];
|
|
46
|
+
|
|
47
|
+
setAudioData({ volume: volume / 255, frequency: frequency / 255 });
|
|
48
|
+
|
|
49
|
+
requestAnimationFrame(updateAudioData);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
updateAudioData();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Error setting up audio:', error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
setupAudio();
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
if (audioContextRef.current) {
|
|
62
|
+
audioContextRef.current.close();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}, [enabled]);
|
|
66
|
+
|
|
67
|
+
return audioData;
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface UseGlitchOptions {
|
|
6
|
+
interval?: number;
|
|
7
|
+
duration?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useGlitch(options: UseGlitchOptions = {}) {
|
|
11
|
+
const { interval = 3000, duration = 300 } = options;
|
|
12
|
+
const [isGlitching, setIsGlitching] = useState(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const glitchInterval = setInterval(() => {
|
|
16
|
+
setIsGlitching(true);
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
setIsGlitching(false);
|
|
19
|
+
}, duration);
|
|
20
|
+
}, interval);
|
|
21
|
+
|
|
22
|
+
return () => clearInterval(glitchInterval);
|
|
23
|
+
}, [interval, duration]);
|
|
24
|
+
|
|
25
|
+
return isGlitching;
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
export function useNeonGlow() {
|
|
6
|
+
const [isGlowing, setIsGlowing] = useState(false);
|
|
7
|
+
|
|
8
|
+
const startGlow = useCallback(() => {
|
|
9
|
+
setIsGlowing(true);
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
const stopGlow = useCallback(() => {
|
|
13
|
+
setIsGlowing(false);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
const toggleGlow = useCallback(() => {
|
|
17
|
+
setIsGlowing((prev) => !prev);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const glowClasses = isGlowing
|
|
21
|
+
? 'shadow-neon-cyan brightness-125 scale-105'
|
|
22
|
+
: '';
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
isGlowing,
|
|
26
|
+
startGlow,
|
|
27
|
+
stopGlow,
|
|
28
|
+
toggleGlow,
|
|
29
|
+
glowClasses,
|
|
30
|
+
};
|
|
31
|
+
}
|