diff --git a/app/page.tsx b/app/page.tsx index 531ed1e..5d9147d 100644 --- a/app/page.tsx +++ b/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 ( -
- {/* Global Background Pattern */} -
+ {/* Flowing Path Animation - wraps around content */} + + + {/* Subtle Grid Pattern Overlay */} +
{/* Hero Section - Split Layout */} diff --git a/src/components/Landing/FlowingPath.tsx b/src/components/Landing/FlowingPath.tsx new file mode 100644 index 0000000..8189ff1 --- /dev/null +++ b/src/components/Landing/FlowingPath.tsx @@ -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 = ({ className = '' }) => { + const containerRef = useRef(null); + const [pathData, setPathData] = useState(''); + const [pathLength, setPathLength] = useState(0); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const pathRef = useRef(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 ( + + ); +}; + +interface NodeMarkerProps { + pos: number; + pathData: string; + progress: any; +} + +const NodeMarker: React.FC = ({ 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 ( + + ); +}; + +export default FlowingPath; diff --git a/src/components/Landing/ParticleNetwork.tsx b/src/components/Landing/ParticleNetwork.tsx new file mode 100644 index 0000000..64c6e98 --- /dev/null +++ b/src/components/Landing/ParticleNetwork.tsx @@ -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 = ({ className = '' }) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const particlesRef = useRef([]); + const animationFrameRef = useRef(0); + const scrollRef = useRef(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 ( + + ); +}; + +export default ParticleNetwork; diff --git a/src/components/Landing/index.ts b/src/components/Landing/index.ts index d4e5737..0161484 100644 --- a/src/components/Landing/index.ts +++ b/src/components/Landing/index.ts @@ -3,3 +3,5 @@ export * from './FeatureCard'; export * from './ComparisonRow'; export * from './ServiceCard'; export * from './AbstractLines'; +export * from './ParticleNetwork'; +export * from './FlowingPath';