cleanup
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter, Newsreader } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Analytics } from '../src/components/Analytics';
|
||||
import { Footer } from '../src/components/Footer';
|
||||
import { Header } from '../src/components/Header';
|
||||
import { CTA } from '../src/components/CTA';
|
||||
import { InteractiveElements } from '../src/components/InteractiveElements';
|
||||
import { Analytics } from '../src/components/Analytics';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
const newsreader = Newsreader({
|
||||
@@ -40,7 +39,6 @@ export default function RootLayout({
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<CTA />
|
||||
<Footer />
|
||||
<InteractiveElements />
|
||||
<Analytics />
|
||||
|
||||
12
app/page.tsx
12
app/page.tsx
@@ -1,14 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { Zap, Shield, ArrowRight, Check, X, Layout, Database, Workflow } from 'lucide-react';
|
||||
import { ArrowRight, Check, Database, Layout, Shield, Workflow, X, Zap } from 'lucide-react';
|
||||
import { CirclePattern, ComparisonRow, ConnectorBranch, ConnectorEnd, ConnectorSplit, ConnectorStart, FlowLines, GridLines, HeroLines, ServicesFlow } from '../src/components/Landing';
|
||||
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, FlowingPath } from '../src/components/Landing';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<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={{
|
||||
@@ -146,11 +143,6 @@ export default function LandingPage() {
|
||||
{/* Section 03: The Difference */}
|
||||
<Section number="03" title="Der Unterschied" variant="white" borderTop connector={<ConnectorStart className="h-full" />}>
|
||||
<div className="space-y-12 relative">
|
||||
{/* Animated Vertical Line */}
|
||||
<div className="absolute left-1/2 top-24 bottom-0 -translate-x-1/2 hidden md:block w-24 h-full pointer-events-none z-0">
|
||||
<ComparisonLines className="w-full h-full" />
|
||||
</div>
|
||||
|
||||
<Reveal>
|
||||
<p className="text-xl md:text-2xl font-serif italic text-slate-600 max-w-2xl relative z-10">
|
||||
Der klassische Agentur-Weg ist oft langsam und teuer. Mein Ansatz ist radikal anders: Ich baue zuerst, dann reden wir über Details.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { Reveal } from './Reveal';
|
||||
|
||||
export const CTA: React.FC = () => {
|
||||
return (
|
||||
<section className="container relative py-20 md:py-32">
|
||||
<Reveal width="100%">
|
||||
<div className="relative p-12 md:p-20 border border-slate-200 rounded-[3rem] bg-white">
|
||||
<div className="absolute top-0 left-12 w-px h-24 bg-slate-200 -translate-y-12"></div>
|
||||
<div className="absolute -right-12 -top-12 text-[15rem] font-bold text-slate-50 select-none -z-10 opacity-50">?</div>
|
||||
|
||||
<div className="space-y-16 relative z-10">
|
||||
<h2 className="text-6xl md:text-8xl font-bold tracking-tighter leading-[0.9] text-slate-900">
|
||||
Interesse? <br />
|
||||
<span className="text-slate-100">Fragen kostet nichts.</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
|
||||
<p className="text-2xl md:text-3xl text-slate-400 font-serif italic leading-tight">
|
||||
Schreiben Sie mir einfach, was Sie brauchen. Ich sage Ihnen ehrlich, ob ich es mache.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-start gap-8">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="group inline-flex items-center gap-4 bg-white border border-slate-200 rounded-full px-10 py-5 text-2xl font-bold text-slate-900 hover:border-slate-400 hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
Projekt anfragen
|
||||
<ArrowRight className="w-6 h-6 group-hover:translate-x-2 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,535 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { motion, useScroll, useTransform, useSpring, useAnimationFrame } 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();
|
||||
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;
|
||||
|
||||
// Total scrollable distance
|
||||
const scrollRange = dimensions.height - vh;
|
||||
const currentScrollY = latest * scrollRange;
|
||||
|
||||
// We want the dot to be at y = currentScrollY + vh/2
|
||||
// But at the very top (latest=0), we want it at y=0 (or above)
|
||||
// At the very bottom (latest=1), we want it at y=dimensions.height
|
||||
|
||||
let targetY;
|
||||
if (latest < 0.05) {
|
||||
// Transition from top of page to center of viewport
|
||||
targetY = (latest / 0.05) * (vh / 2);
|
||||
} else if (latest > 0.95) {
|
||||
// Transition from center of viewport to bottom of page
|
||||
const p = (latest - 0.95) / 0.05;
|
||||
targetY = (currentScrollY + vh / 2) * (1 - p) + dimensions.height * p;
|
||||
} else {
|
||||
targetY = currentScrollY + vh / 2;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(1, targetY / dimensions.height));
|
||||
}
|
||||
);
|
||||
|
||||
// Smooth spring animation for the path drawing
|
||||
const smoothProgress = useSpring(centeredProgress, {
|
||||
stiffness: 100, // Much higher stiffness for immediate response
|
||||
damping: 40, // Higher damping to prevent any overshoot/backward motion
|
||||
restDelta: 0.0001,
|
||||
mass: 0.5, // Lower mass for faster acceleration
|
||||
});
|
||||
|
||||
// Transform scroll progress to path drawing
|
||||
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
|
||||
|
||||
// Dot transforms
|
||||
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
|
||||
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 = 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;
|
||||
|
||||
const sectionHeight = height / 8;
|
||||
|
||||
const nodes: PathNode[] = [
|
||||
{ x: centerX - 100, y: 0, type: 'line' },
|
||||
{ x: centerX - 100, y: sectionHeight * 0.2, type: 'curve' },
|
||||
|
||||
// Hero section - wide sweep
|
||||
{ x: rightEdge, y: sectionHeight * 0.8, type: 'curve' },
|
||||
{ x: rightEdge - 50, y: sectionHeight * 1.5, 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' },
|
||||
|
||||
// Section 3 - middle flow
|
||||
{ x: centerX + 150, y: sectionHeight * 3.8, type: 'curve' },
|
||||
{ x: centerX - 150, y: sectionHeight * 4.6, type: 'curve' },
|
||||
|
||||
// Section 4 - right side
|
||||
{ x: rightEdge - 20, y: sectionHeight * 5.4, type: 'curve' },
|
||||
{ x: rightEdge - 100, y: sectionHeight * 6.2, type: 'curve' },
|
||||
|
||||
// Section 5 - back to left
|
||||
{ x: leftEdge + 100, y: sectionHeight * 7.0, type: 'curve' },
|
||||
|
||||
// Final - center
|
||||
{ x: centerX, y: sectionHeight * 7.6, type: 'curve' },
|
||||
{ x: centerX, y: height, type: 'line' },
|
||||
];
|
||||
|
||||
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') {
|
||||
const dy = curr.y - prev.y;
|
||||
const cp1y = prev.y + dy * 0.4;
|
||||
const cp2y = prev.y + dy * 0.6;
|
||||
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||
} else {
|
||||
path += ` L ${curr.x} ${curr.y}`;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}, []);
|
||||
|
||||
// Generate secondary decorative paths that follow the main path with some divergence
|
||||
const generateSecondaryPath = useCallback((width: number, height: number, seed: number): string => {
|
||||
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;
|
||||
|
||||
const sectionHeight = height / 8;
|
||||
|
||||
const random = (i: number) => {
|
||||
const x = Math.sin(seed + i) * 10000;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
|
||||
// Base nodes similar to generatePath but with randomized offsets
|
||||
const nodes = [
|
||||
{ x: centerX - 100 + (random(0) - 0.5) * 40, y: 0 },
|
||||
{ x: centerX - 100 + (random(1) - 0.5) * 40, y: sectionHeight * 0.2 },
|
||||
{ x: rightEdge + (random(2) - 0.5) * 60, y: sectionHeight * 0.8 },
|
||||
{ x: rightEdge - 50 + (random(3) - 0.5) * 60, y: sectionHeight * 1.5 },
|
||||
{ x: leftEdge + 50 + (random(4) - 0.5) * 60, y: sectionHeight * 2.2 },
|
||||
{ x: leftEdge + (random(5) - 0.5) * 60, y: sectionHeight * 3.0 },
|
||||
{ x: centerX + 150 + (random(6) - 0.5) * 80, y: sectionHeight * 3.8 },
|
||||
{ x: centerX - 150 + (random(7) - 0.5) * 80, y: sectionHeight * 4.6 },
|
||||
{ x: rightEdge - 20 + (random(8) - 0.5) * 60, y: sectionHeight * 5.4 },
|
||||
{ x: rightEdge - 100 + (random(9) - 0.5) * 60, y: sectionHeight * 6.2 },
|
||||
{ x: leftEdge + 100 + (random(10) - 0.5) * 60, y: sectionHeight * 7.0 },
|
||||
{ x: centerX + (random(11) - 0.5) * 40, y: sectionHeight * 7.6 },
|
||||
{ x: centerX + (random(12) - 0.5) * 40, y: height }
|
||||
];
|
||||
|
||||
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];
|
||||
const dy = curr.y - prev.y;
|
||||
const cp1y = prev.y + dy * 0.4;
|
||||
const cp2y = prev.y + dy * 0.6;
|
||||
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${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: 1,
|
||||
}}
|
||||
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="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
<filter id="dotGlow" x="-200%" y="-200%" width="500%" height="500%">
|
||||
<feGaussianBlur stdDeviation="5" result="blur" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 1 0" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor="rgba(0, 0, 0, 0.05)" />
|
||||
<stop offset="20%" stopColor="rgba(0, 0, 0, 0.2)" />
|
||||
<stop offset="50%" stopColor="rgba(0, 0, 0, 0.4)" />
|
||||
<stop offset="80%" stopColor="rgba(0, 0, 0, 0.2)" />
|
||||
<stop offset="100%" stopColor="rgba(0, 0, 0, 0.05)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Background decorative paths - static, very subtle */}
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 10)}
|
||||
stroke="rgba(0, 0, 0, 0.1)"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="5,10"
|
||||
/>
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
|
||||
stroke="rgba(0, 0, 0, 0.08)"
|
||||
strokeWidth="0.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* Main flowing path - draws as you scroll */}
|
||||
<motion.path
|
||||
ref={pathRef}
|
||||
d={pathData}
|
||||
stroke="url(#lineGradient)"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
filter="url(#glow)"
|
||||
style={{
|
||||
pathLength: pathDrawn,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Accent path - fully filled, static */}
|
||||
<path
|
||||
d={pathData}
|
||||
stroke="rgba(0, 0, 0, 0.05)"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Secondary accent path - fully filled, static */}
|
||||
<path
|
||||
d={pathData}
|
||||
stroke="rgba(0, 0, 0, 0.03)"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="translate(5, 5)"
|
||||
/>
|
||||
|
||||
{/* Extra static dashed lines with unique random paths */}
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 123)}
|
||||
stroke="rgba(0, 0, 0, 0.15)"
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeDasharray="10,20"
|
||||
/>
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 456)}
|
||||
stroke="rgba(0, 0, 0, 0.12)"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
strokeDasharray="5,15"
|
||||
/>
|
||||
<path
|
||||
d={generateSecondaryPath(dimensions.width, dimensions.height, 789)}
|
||||
stroke="rgba(0, 0, 0, 0.1)"
|
||||
strokeWidth="0.8"
|
||||
fill="none"
|
||||
strokeDasharray="2,10"
|
||||
/>
|
||||
|
||||
{/* Pulse System */}
|
||||
<PulseSystem
|
||||
pathRef={pathRef}
|
||||
progress={smoothProgress}
|
||||
pathLength={pathLength}
|
||||
checkpoints={[0.12, 0.28, 0.45, 0.62, 0.82]}
|
||||
/>
|
||||
|
||||
{/* Animated dot that travels along the path */}
|
||||
<motion.circle
|
||||
r="6"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
strokeWidth="1.5"
|
||||
filter="url(#dotGlow)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: dot1Distance,
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.8, 1, 0.8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary traveling dot */}
|
||||
<motion.circle
|
||||
r="3"
|
||||
fill="rgba(0, 0, 0, 0.4)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: dot2Distance,
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
translateX: [0, 5, -5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Node markers at key positions */}
|
||||
{[0.12, 0.28, 0.45, 0.62, 0.82].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 isActive = useTransform(
|
||||
progress,
|
||||
[pos - 0.05, pos, pos + 0.05],
|
||||
[0, 1, 1]
|
||||
);
|
||||
|
||||
const scale = useTransform(
|
||||
progress,
|
||||
[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 (
|
||||
<g>
|
||||
<motion.circle
|
||||
r="12"
|
||||
fill="rgba(148, 163, 184, 0.2)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: `${pos * 100}%`,
|
||||
opacity: glowOpacity,
|
||||
scale: 2,
|
||||
}}
|
||||
/>
|
||||
<motion.circle
|
||||
r="6"
|
||||
fill="white"
|
||||
stroke="rgba(148, 163, 184, 0.8)"
|
||||
strokeWidth="2"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: `${pos * 100}%`,
|
||||
opacity: isActive,
|
||||
scale,
|
||||
}}
|
||||
/>
|
||||
<motion.circle
|
||||
r="2"
|
||||
fill="rgba(148, 163, 184, 1)"
|
||||
style={{
|
||||
offsetPath: `path("${pathData}")`,
|
||||
offsetDistance: `${pos * 100}%`,
|
||||
opacity: isActive,
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
interface Pulse {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
life: number;
|
||||
}
|
||||
|
||||
const PulseSystem: React.FC<{
|
||||
pathRef: React.RefObject<SVGPathElement>;
|
||||
progress: any;
|
||||
pathLength: number;
|
||||
checkpoints: number[];
|
||||
}> = ({ pathRef, progress, pathLength, checkpoints }) => {
|
||||
const [pulses, setPulses] = useState<Pulse[]>([]);
|
||||
const lastProgress = useRef(0);
|
||||
const pulseId = useRef(0);
|
||||
|
||||
useAnimationFrame((time, delta) => {
|
||||
if (!pathRef.current || pathLength === 0) return;
|
||||
|
||||
const currentProgress = progress.get();
|
||||
const point = pathRef.current.getPointAtLength(currentProgress * pathLength);
|
||||
|
||||
// Check for checkpoint pulses
|
||||
checkpoints.forEach(cp => {
|
||||
const crossed = (lastProgress.current < cp && currentProgress >= cp) ||
|
||||
(lastProgress.current > cp && currentProgress <= cp);
|
||||
|
||||
if (crossed) {
|
||||
const checkpointPoint = pathRef.current!.getPointAtLength(cp * pathLength);
|
||||
setPulses(prev => [...prev, {
|
||||
id: pulseId.current++,
|
||||
x: checkpointPoint.x,
|
||||
y: checkpointPoint.y,
|
||||
life: 1.0,
|
||||
}].slice(-8));
|
||||
}
|
||||
});
|
||||
lastProgress.current = currentProgress;
|
||||
|
||||
// Update pulses
|
||||
setPulses(prev => prev.map(p => ({ ...p, life: p.life - 0.015 })).filter(p => p.life > 0));
|
||||
});
|
||||
|
||||
return (
|
||||
<g>
|
||||
{pulses.map(p => (
|
||||
<circle
|
||||
key={p.id}
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={(1 - p.life) * 150}
|
||||
fill="none"
|
||||
stroke="rgba(0, 0, 0, 0.15)"
|
||||
strokeWidth={p.life * 3}
|
||||
style={{ opacity: p.life }}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlowingPath;
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './HeroItem';
|
||||
export * from './FeatureCard';
|
||||
export * from './ComparisonRow';
|
||||
export * from './ServiceCard';
|
||||
export * from './AbstractLines';
|
||||
export * from './ComparisonRow';
|
||||
export * from './FeatureCard';
|
||||
export * from './HeroItem';
|
||||
export * from './ParticleNetwork';
|
||||
export * from './FlowingPath';
|
||||
export * from './ServiceCard';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user