From eba4905706ed22da2d1f3fc8cc0b36a678db8318 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 29 Jan 2026 23:10:46 +0100 Subject: [PATCH] design --- src/components/Landing/FlowingPath.tsx | 303 ++++++++++++++++++------- 1 file changed, 217 insertions(+), 86 deletions(-) diff --git a/src/components/Landing/FlowingPath.tsx b/src/components/Landing/FlowingPath.tsx index 8189ff1..51080e0 100644 --- a/src/components/Landing/FlowingPath.tsx +++ b/src/components/Landing/FlowingPath.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; -import { useEffect, useRef, useState, useCallback } from 'react'; -import { motion, useScroll, useTransform, useSpring } from 'framer-motion'; +import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { motion, useScroll, useTransform, useSpring, useAnimationFrame } from 'framer-motion'; interface FlowingPathProps { className?: string; @@ -22,66 +22,82 @@ export const FlowingPath: React.FC = ({ className = '' }) => { const pathRef = useRef(null); const { scrollYProgress } = useScroll(); + const [vh, setVh] = useState(0); + + useEffect(() => { + setVh(window.innerHeight); + const handleResize = () => setVh(window.innerHeight); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // Calculate the progress needed to keep the dot at the center of the viewport + const centeredProgress = useTransform( + scrollYProgress, + (latest) => { + if (dimensions.height === 0 || vh === 0) return latest; + const scrollRange = dimensions.height - vh; + const currentScrollY = latest * scrollRange; + const targetY = currentScrollY + vh / 2; + return Math.max(0, Math.min(1, targetY / dimensions.height)); + } + ); // Smooth spring animation for the path drawing - const smoothProgress = useSpring(scrollYProgress, { - stiffness: 50, - damping: 20, - restDelta: 0.001, + const smoothProgress = useSpring(centeredProgress, { + stiffness: 40, + damping: 25, + restDelta: 0.0001, }); // 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]); + const accentPathDrawn = useTransform(smoothProgress, [0, 1], [0, 0.98]); // Dot transforms const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']); - const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '95%']); + const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '98%']); // 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 margin = width < 768 ? 20 : 80; + const contentWidth = Math.min(width * 0.9, 1200); 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 sectionHeight = height / 8; const nodes: PathNode[] = [ - // Start from top left, flowing down - { x: leftEdge, y: 0, type: 'line' }, - { x: leftEdge, y: sectionHeight * 0.3, type: 'curve' }, + { x: centerX - 100, y: 0, type: 'line' }, + { x: centerX - 100, y: sectionHeight * 0.2, type: 'curve' }, - // Curve around hero section (right side) - { x: rightEdge, y: sectionHeight * 0.6, type: 'curve' }, - { x: rightEdge, y: sectionHeight * 1.2, type: 'curve' }, + // Hero section - wide sweep + { x: rightEdge, y: sectionHeight * 0.8, type: 'curve' }, + { x: rightEdge - 50, y: sectionHeight * 1.5, type: 'curve' }, - // Flow to left for section 02 - { x: leftEdge, y: sectionHeight * 1.5, type: 'curve' }, - { x: leftEdge, y: sectionHeight * 2, type: 'curve' }, + // Section 2 - tight curve to left + { x: leftEdge + 50, y: sectionHeight * 2.2, type: 'curve' }, + { x: leftEdge, y: sectionHeight * 3.0, type: 'curve' }, - // Cross to right for section 03 - { x: rightEdge, y: sectionHeight * 2.3, type: 'curve' }, - { x: rightEdge, y: sectionHeight * 2.8, type: 'curve' }, + // Section 3 - middle flow + { x: centerX + 150, y: sectionHeight * 3.8, type: 'curve' }, + { x: centerX - 150, y: sectionHeight * 4.6, type: 'curve' }, - // Back to left for section 04 - { x: leftEdge, y: sectionHeight * 3.2, type: 'curve' }, - { x: leftEdge, y: sectionHeight * 3.8, type: 'curve' }, + // Section 4 - right side + { x: rightEdge - 20, y: sectionHeight * 5.4, type: 'curve' }, + { x: rightEdge - 100, y: sectionHeight * 6.2, type: 'curve' }, - // Right side for section 05 - { x: rightEdge, y: sectionHeight * 4.2, type: 'curve' }, - { x: rightEdge, y: sectionHeight * 4.8, type: 'curve' }, + // Section 5 - back to left + { x: leftEdge + 100, y: sectionHeight * 7.0, type: 'curve' }, - // Final section - converge to center - { x: centerX, y: sectionHeight * 5.5, type: 'curve' }, + // Final - center + { x: centerX, y: sectionHeight * 7.6, 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++) { @@ -89,14 +105,9 @@ export const FlowingPath: React.FC = ({ className = '' }) => { 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}`; + const cp1y = prev.y + (curr.y - prev.y) * 0.5; + const cp2y = prev.y + (curr.y - prev.y) * 0.5; + path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`; } else { path += ` L ${curr.x} ${curr.y}`; } @@ -107,31 +118,29 @@ export const FlowingPath: React.FC = ({ className = '' }) => { // 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 margin = width < 768 ? 10 : 40 + offset; + const contentWidth = Math.min(width * 0.85, 1000); const leftEdge = (width - contentWidth) / 2 - margin; const rightEdge = (width + contentWidth) / 2 + margin; - const sectionHeight = height / 6; + const sectionHeight = height / 8; - let path = `M ${leftEdge + offset * 2} ${-50}`; + let path = `M ${width / 2 + offset} ${-100}`; - // 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 }, + { x: leftEdge + offset, y: sectionHeight * 1.2 }, + { x: rightEdge - offset, y: sectionHeight * 2.5 }, + { x: leftEdge - offset, y: sectionHeight * 4.0 }, + { x: rightEdge + offset, y: sectionHeight * 5.5 }, + { x: width / 2 + offset, y: height + 100 }, ]; 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}`; + const prev = i > 0 ? points[i - 1] : { x: width / 2 + offset, y: -100 }; + const cp1y = prev.y + (curr.y - prev.y) * 0.5; + const cp2y = prev.y + (curr.y - prev.y) * 0.5; + path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`; } return path; @@ -206,28 +215,39 @@ export const FlowingPath: React.FC = ({ className = '' }) => { style={{ position: 'absolute', top: 0, left: 0 }} > - - + + + + + + + + + + - - - + + + + + {/* Background decorative paths - static, very subtle */} = ({ className = '' }) => { }} /> + {/* Particle System */} + + {/* Animated dot that travels along the path */} = ({ className = '' }) => { {/* Secondary traveling dot */} = ({ className = '' }) => { /> {/* Node markers at key positions */} - {[0.15, 0.33, 0.5, 0.67, 0.85].map((pos, i) => ( + {[0.12, 0.28, 0.45, 0.62, 0.82].map((pos, i) => ( = ({ pos, pathData, progress }) => { - const opacity = useTransform( + const isActive = useTransform( progress, - [pos - 0.1, pos, pos + 0.05], + [pos - 0.05, pos, pos + 0.05], [0, 1, 1] ); const scale = useTransform( progress, - [pos - 0.05, pos, pos + 0.1], - [0.5, 1.2, 1] + [pos - 0.05, pos, pos + 0.05], + [0.8, 1.4, 1] + ); + + const glowOpacity = useTransform( + progress, + [pos - 0.02, pos, pos + 0.02], + [0, 0.6, 0] ); return ( - + + + + + + ); +}; + +interface Particle { + id: number; + x: number; + y: number; + vx: number; + vy: number; + life: number; + size: number; +} + +const ParticleSystem: React.FC<{ + pathRef: React.RefObject; + progress: any; + pathLength: number; +}> = ({ pathRef, progress, pathLength }) => { + const [particles, setParticles] = useState([]); + const lastPos = useRef({ x: 0, y: 0 }); + const particleId = useRef(0); + + useAnimationFrame((time, delta) => { + if (!pathRef.current || pathLength === 0) return; + + // Get current position of the dot + const currentProgress = progress.get(); + const point = pathRef.current.getPointAtLength(currentProgress * pathLength); + + // Update and emit particles in one go + setParticles(prev => { + const updated = prev + .map(p => ({ + ...p, + x: p.x + p.vx, + y: p.y + p.vy, + life: p.life - 0.02, + })) + .filter(p => p.life > 0); + + const dist = Math.hypot(point.x - lastPos.current.x, point.y - lastPos.current.y); + if (dist > 1) { + const newParticles: Particle[] = []; + const count = Math.min(3, Math.floor(dist / 2)); + + for (let i = 0; i < count; i++) { + newParticles.push({ + id: particleId.current++, + x: point.x, + y: point.y, + vx: (Math.random() - 0.5) * 2, + vy: (Math.random() - 0.5) * 2, + life: 1.0, + size: Math.random() * 3 + 1, + }); + } + lastPos.current = { x: point.x, y: point.y }; + return [...updated, ...newParticles].slice(-60); + } + + return updated; + }); + }); + + return ( + + {particles.map(p => ( + + ))} + ); };