design
This commit is contained in:
15
app/page.tsx
15
app/page.tsx
@@ -1,16 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Zap, Shield, MousePointer2, Sparkles, ArrowRight, Check, X, Layout, Database, Workflow } from 'lucide-react';
|
||||
import { Zap, Shield, ArrowRight, Check, X, Layout, Database, Workflow } from 'lucide-react';
|
||||
import { Reveal } from '../src/components/Reveal';
|
||||
import { Section } from '../src/components/Section';
|
||||
import { HeroItem, FeatureCard, ComparisonRow, ServiceCard, HeroLines, GridLines, FlowLines, CirclePattern, ServicesFlow, ComparisonLines, ConnectorStart, ConnectorBranch, ConnectorSplit, ConnectorEnd } from '../src/components/Landing';
|
||||
import { HeroItem, FeatureCard, ComparisonRow, ServiceCard, HeroLines, GridLines, FlowLines, CirclePattern, ServicesFlow, ComparisonLines, ConnectorStart, ConnectorBranch, ConnectorSplit, ConnectorEnd, FlowingPath } from '../src/components/Landing';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden">
|
||||
{/* Global Background Pattern */}
|
||||
<div className="fixed inset-0 pointer-events-none -z-20 opacity-[0.03]" style={{
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
{/* Flowing Path Animation - wraps around content */}
|
||||
<FlowingPath />
|
||||
|
||||
{/* Subtle Grid Pattern Overlay */}
|
||||
<div className="fixed inset-0 pointer-events-none -z-20 opacity-[0.015]" style={{
|
||||
backgroundImage: 'linear-gradient(#0f172a 1px, transparent 1px), linear-gradient(90deg, #0f172a 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px'
|
||||
backgroundSize: '80px 80px'
|
||||
}} />
|
||||
|
||||
{/* Hero Section - Split Layout */}
|
||||
|
||||
333
src/components/Landing/FlowingPath.tsx
Normal file
333
src/components/Landing/FlowingPath.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
|
||||
|
||||
interface FlowingPathProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface PathNode {
|
||||
x: number;
|
||||
y: number;
|
||||
type: 'curve' | 'line';
|
||||
}
|
||||
|
||||
export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [pathData, setPathData] = useState<string>('');
|
||||
const [pathLength, setPathLength] = useState<number>(0);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const pathRef = useRef<SVGPathElement>(null);
|
||||
|
||||
const { scrollYProgress } = useScroll();
|
||||
|
||||
// Smooth spring animation for the path drawing
|
||||
const smoothProgress = useSpring(scrollYProgress, {
|
||||
stiffness: 50,
|
||||
damping: 20,
|
||||
restDelta: 0.001,
|
||||
});
|
||||
|
||||
// Transform scroll progress to path drawing
|
||||
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
|
||||
|
||||
// Accent path transform
|
||||
const accentPathDrawn = useTransform(smoothProgress, [0, 1], [0, 0.95]);
|
||||
|
||||
// Dot transforms
|
||||
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
|
||||
const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '95%']);
|
||||
|
||||
// Generate a flowing path that wraps around content sections
|
||||
const generatePath = useCallback((width: number, height: number): string => {
|
||||
const margin = 60;
|
||||
const contentWidth = Math.min(width * 0.85, 900);
|
||||
const leftEdge = (width - contentWidth) / 2 - margin;
|
||||
const rightEdge = (width + contentWidth) / 2 + margin;
|
||||
const centerX = width / 2;
|
||||
|
||||
// Define key vertical positions (approximate section boundaries)
|
||||
const sectionHeight = height / 6;
|
||||
|
||||
const nodes: PathNode[] = [
|
||||
// Start from top left, flowing down
|
||||
{ x: leftEdge, y: 0, type: 'line' },
|
||||
{ x: leftEdge, y: sectionHeight * 0.3, type: 'curve' },
|
||||
|
||||
// Curve around hero section (right side)
|
||||
{ x: rightEdge, y: sectionHeight * 0.6, type: 'curve' },
|
||||
{ x: rightEdge, y: sectionHeight * 1.2, type: 'curve' },
|
||||
|
||||
// Flow to left for section 02
|
||||
{ x: leftEdge, y: sectionHeight * 1.5, type: 'curve' },
|
||||
{ x: leftEdge, y: sectionHeight * 2, type: 'curve' },
|
||||
|
||||
// Cross to right for section 03
|
||||
{ x: rightEdge, y: sectionHeight * 2.3, type: 'curve' },
|
||||
{ x: rightEdge, y: sectionHeight * 2.8, type: 'curve' },
|
||||
|
||||
// Back to left for section 04
|
||||
{ x: leftEdge, y: sectionHeight * 3.2, type: 'curve' },
|
||||
{ x: leftEdge, y: sectionHeight * 3.8, type: 'curve' },
|
||||
|
||||
// Right side for section 05
|
||||
{ x: rightEdge, y: sectionHeight * 4.2, type: 'curve' },
|
||||
{ x: rightEdge, y: sectionHeight * 4.8, type: 'curve' },
|
||||
|
||||
// Final section - converge to center
|
||||
{ x: centerX, y: sectionHeight * 5.5, type: 'curve' },
|
||||
{ x: centerX, y: height, type: 'line' },
|
||||
];
|
||||
|
||||
// Build SVG path with smooth bezier curves
|
||||
let path = `M ${nodes[0].x} ${nodes[0].y}`;
|
||||
|
||||
for (let i = 1; i < nodes.length; i++) {
|
||||
const prev = nodes[i - 1];
|
||||
const curr = nodes[i];
|
||||
|
||||
if (curr.type === 'curve') {
|
||||
// Calculate control points for smooth S-curves
|
||||
const midY = (prev.y + curr.y) / 2;
|
||||
const controlX1 = prev.x;
|
||||
const controlY1 = midY;
|
||||
const controlX2 = curr.x;
|
||||
const controlY2 = midY;
|
||||
|
||||
path += ` C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${curr.x} ${curr.y}`;
|
||||
} else {
|
||||
path += ` L ${curr.x} ${curr.y}`;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}, []);
|
||||
|
||||
// Generate secondary decorative paths
|
||||
const generateSecondaryPath = useCallback((width: number, height: number, offset: number): string => {
|
||||
const margin = 40 + offset;
|
||||
const contentWidth = Math.min(width * 0.85, 900);
|
||||
const leftEdge = (width - contentWidth) / 2 - margin;
|
||||
const rightEdge = (width + contentWidth) / 2 + margin;
|
||||
|
||||
const sectionHeight = height / 6;
|
||||
|
||||
let path = `M ${leftEdge + offset * 2} ${-50}`;
|
||||
|
||||
// Simpler parallel path
|
||||
const points = [
|
||||
{ x: leftEdge, y: sectionHeight * 0.5 },
|
||||
{ x: rightEdge, y: sectionHeight * 1.0 },
|
||||
{ x: leftEdge, y: sectionHeight * 1.8 },
|
||||
{ x: rightEdge, y: sectionHeight * 2.5 },
|
||||
{ x: leftEdge, y: sectionHeight * 3.5 },
|
||||
{ x: rightEdge, y: sectionHeight * 4.5 },
|
||||
{ x: width / 2, y: height + 50 },
|
||||
];
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const curr = points[i];
|
||||
const prev = i > 0 ? points[i - 1] : { x: leftEdge + offset * 2, y: -50 };
|
||||
const midY = (prev.y + curr.y) / 2;
|
||||
path += ` C ${prev.x} ${midY}, ${curr.x} ${midY}, ${curr.x} ${curr.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}, []);
|
||||
|
||||
// Update dimensions and path on resize
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const width = window.innerWidth;
|
||||
const height = document.documentElement.scrollHeight;
|
||||
setDimensions({ width, height });
|
||||
setPathData(generatePath(width, height));
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
|
||||
// Debounced resize handler
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(updateDimensions, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Also update when content might have changed
|
||||
const observer = new ResizeObserver(handleResize);
|
||||
if (document.body) {
|
||||
observer.observe(document.body);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
observer.disconnect();
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [generatePath]);
|
||||
|
||||
// Get path length after path is rendered
|
||||
useEffect(() => {
|
||||
if (pathRef.current) {
|
||||
setPathLength(pathRef.current.getTotalLength());
|
||||
}
|
||||
}, [pathData]);
|
||||
|
||||
if (dimensions.width === 0 || dimensions.height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`absolute inset-0 pointer-events-none overflow-visible ${className}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: dimensions.height,
|
||||
zIndex: 5,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="rgba(148, 163, 184, 0.1)" />
|
||||
<stop offset="50%" stopColor="rgba(148, 163, 184, 0.5)" />
|
||||
<stop offset="100%" stopColor="rgba(148, 163, 184, 0.1)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Background decorative paths - static, very subtle */}
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
|
||||
stroke="rgba(226, 232, 240, 0.4)"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, -15)}
|
||||
stroke="rgba(226, 232, 240, 0.3)"
|
||||
strokeWidth="0.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* Main flowing path - draws as you scroll */}
|
||||
<motion.path
|
||||
ref={pathRef}
|
||||
d={pathData}
|
||||
stroke="url(#lineGradient)"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
filter="url(#glow)"
|
||||
style={{
|
||||
pathLength: pathDrawn,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Accent path that follows slightly behind */}
|
||||
<motion.path
|
||||
d={pathData}
|
||||
stroke="rgba(203, 213, 225, 0.2)"
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
pathLength: accentPathDrawn,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated dot that travels along the path */}
|
||||
<motion.circle
|
||||
r="3"
|
||||
fill="rgba(15, 23, 42, 0.8)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: dot1Distance,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary traveling dot */}
|
||||
<motion.circle
|
||||
r="1.5"
|
||||
fill="rgba(148, 163, 184, 0.4)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: dot2Distance,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Node markers at key positions */}
|
||||
{[0.15, 0.33, 0.5, 0.67, 0.85].map((pos, i) => (
|
||||
<NodeMarker
|
||||
key={i}
|
||||
pos={pos}
|
||||
pathData={pathData}
|
||||
progress={smoothProgress}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface NodeMarkerProps {
|
||||
pos: number;
|
||||
pathData: string;
|
||||
progress: any;
|
||||
}
|
||||
|
||||
const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
|
||||
const opacity = useTransform(
|
||||
progress,
|
||||
[pos - 0.1, pos, pos + 0.05],
|
||||
[0, 1, 1]
|
||||
);
|
||||
|
||||
const scale = useTransform(
|
||||
progress,
|
||||
[pos - 0.05, pos, pos + 0.1],
|
||||
[0.5, 1.2, 1]
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.circle
|
||||
r="6"
|
||||
fill="white"
|
||||
stroke="rgba(148, 163, 184, 0.5)"
|
||||
strokeWidth="2"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: `${pos * 100}%`,
|
||||
opacity,
|
||||
scale,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowingPath;
|
||||
259
src/components/Landing/ParticleNetwork.tsx
Normal file
259
src/components/Landing/ParticleNetwork.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
baseX: number; // The "home" x position (in the gutters)
|
||||
baseY: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
targetAlpha: number;
|
||||
}
|
||||
|
||||
interface ParticleNetworkProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ParticleNetwork: React.FC<ParticleNetworkProps> = ({ className = '' }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const particlesRef = useRef<Particle[]>([]);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const scrollRef = useRef<number>(0);
|
||||
const mouseRef = useRef<{ x: number; y: number }>({ x: -1000, y: -1000 });
|
||||
const dimensionsRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
particleCount: 60, // Reduced count for cleaner look
|
||||
connectionDistance: 180,
|
||||
mouseRadius: 250,
|
||||
baseSpeed: 0.2,
|
||||
gutterWidth: 0.25, // 25% of width on each side
|
||||
colors: {
|
||||
particle: 'rgba(148, 163, 184, 0.8)', // slate-400
|
||||
line: 'rgba(203, 213, 225, 0.4)', // slate-300
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize particles
|
||||
const initParticles = useCallback((width: number, height: number) => {
|
||||
const particles: Particle[] = [];
|
||||
const count = config.particleCount;
|
||||
|
||||
// Create particles primarily in the side gutters
|
||||
for (let i = 0; i < count; i++) {
|
||||
const isLeft = Math.random() > 0.5;
|
||||
// Random position within the gutter (0-25% or 75-100%)
|
||||
const gutterX = isLeft
|
||||
? Math.random() * (width * config.gutterWidth)
|
||||
: width - (Math.random() * (width * config.gutterWidth));
|
||||
|
||||
// Add some occasional strays near the content but not IN it
|
||||
const x = gutterX;
|
||||
const y = Math.random() * height;
|
||||
|
||||
particles.push({
|
||||
x,
|
||||
y,
|
||||
baseX: x,
|
||||
baseY: y,
|
||||
vx: (Math.random() - 0.5) * 0.1, // Very slight horizontal drift
|
||||
vy: 0.2 + Math.random() * 0.3, // Consistent downward flow
|
||||
size: Math.random() * 1.5 + 0.5,
|
||||
alpha: 0,
|
||||
targetAlpha: Math.random() * 0.6 + 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
particlesRef.current = particles;
|
||||
}, [config.particleCount, config.gutterWidth]);
|
||||
|
||||
// Animation loop
|
||||
const animate = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const { width, height } = dimensionsRef.current;
|
||||
const scroll = scrollRef.current;
|
||||
const mouse = mouseRef.current;
|
||||
const particles = particlesRef.current;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// Update and draw particles
|
||||
particles.forEach((p, i) => {
|
||||
// 1. Movement
|
||||
// Apply downward flow
|
||||
p.y += p.vy;
|
||||
p.x += p.vx;
|
||||
|
||||
// Wrap around vertical
|
||||
if (p.y > height) {
|
||||
p.y = -10;
|
||||
p.x = p.baseX; // Reset X to base to keep structure
|
||||
}
|
||||
|
||||
// 2. Mouse Interaction (Repulsion)
|
||||
const dx = p.x - mouse.x;
|
||||
const dy = p.y - mouse.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < config.mouseRadius) {
|
||||
const force = (config.mouseRadius - dist) / config.mouseRadius;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const pushX = Math.cos(angle) * force * 2;
|
||||
const pushY = Math.sin(angle) * force * 2;
|
||||
|
||||
p.x += pushX;
|
||||
p.y += pushY;
|
||||
}
|
||||
|
||||
// 3. Return to "Lane" (Elasticity)
|
||||
// Gently pull x back towards baseX if not influenced by mouse
|
||||
if (dist >= config.mouseRadius) {
|
||||
p.x += (p.baseX - p.x) * 0.02;
|
||||
}
|
||||
|
||||
// 4. Scroll Effect (Parallax/Speed)
|
||||
// Scroll adds a temporary velocity boost or shift
|
||||
// We use a simple factor here, but could be more complex
|
||||
const scrollFactor = Math.max(0, Math.min(1, scroll * 0.001));
|
||||
p.y += scrollFactor * 0.5; // Move faster when scrolled down?
|
||||
// Actually, let's make them react to scroll speed if we tracked delta,
|
||||
// but for now just position shift based on scroll is handled by the container being fixed.
|
||||
// Let's add a subtle wave based on scroll position
|
||||
p.x += Math.sin(scroll * 0.002 + p.y * 0.01) * 0.2;
|
||||
|
||||
// Fade in
|
||||
if (p.alpha < p.targetAlpha) p.alpha += 0.01;
|
||||
|
||||
// Draw Particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(148, 163, 184, ${p.alpha})`;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Draw Connections
|
||||
// We only connect particles that are close enough
|
||||
ctx.lineWidth = 0.5;
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const p1 = particles[i];
|
||||
// Optimization: only check particles after this one
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const p2 = particles[j];
|
||||
|
||||
// Quick check for distance
|
||||
const dx = p1.x - p2.x;
|
||||
const dy = p1.y - p2.y;
|
||||
|
||||
// Optimization: skip sqrt if obviously too far
|
||||
if (Math.abs(dx) > config.connectionDistance || Math.abs(dy) > config.connectionDistance) continue;
|
||||
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < config.connectionDistance) {
|
||||
// Calculate opacity based on distance
|
||||
const opacity = (1 - dist / config.connectionDistance) * 0.3;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = `rgba(203, 213, 225, ${opacity})`;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}, [config]);
|
||||
|
||||
// Handle resize
|
||||
const handleResize = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!container || !canvas) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
dimensionsRef.current = { width: rect.width, height: rect.height };
|
||||
initParticles(rect.width, rect.height);
|
||||
}, [initParticles]);
|
||||
|
||||
// Handle scroll
|
||||
const handleScroll = useCallback(() => {
|
||||
scrollRef.current = window.scrollY;
|
||||
}, []);
|
||||
|
||||
// Handle mouse move
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
mouseRef.current = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top, // Fixed position, so no scrollY needed for mouse relative to canvas
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle mouse leave
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
mouseRef.current = { x: -1000, y: -1000 };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleResize();
|
||||
handleScroll();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
window.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseleave', handleMouseLeave);
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
};
|
||||
}, [handleResize, handleScroll, handleMouseMove, handleMouseLeave, animate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`fixed inset-0 pointer-events-none -z-10 ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticleNetwork;
|
||||
@@ -3,3 +3,5 @@ export * from './FeatureCard';
|
||||
export * from './ComparisonRow';
|
||||
export * from './ServiceCard';
|
||||
export * from './AbstractLines';
|
||||
export * from './ParticleNetwork';
|
||||
export * from './FlowingPath';
|
||||
|
||||
Reference in New Issue
Block a user