This commit is contained in:
2026-01-29 23:27:24 +01:00
parent eba4905706
commit 98d3ee2198
2 changed files with 176 additions and 105 deletions

View File

@@ -26,7 +26,7 @@ export default function LandingPage() {
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-16 items-center"> <div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-16 items-center">
{/* Left Column: Brand & Number */} {/* Left Column: Brand & Number */}
<div className="md:col-span-5 relative z-10"> <div className="md:col-span-5 relative z-10 bg-white/50 backdrop-blur-[2px]">
<Reveal> <Reveal>
<div className="space-y-8"> <div className="space-y-8">
<div className="flex items-center gap-3 text-slate-400 font-mono text-[10px] uppercase tracking-[0.3em]"> <div className="flex items-center gap-3 text-slate-400 font-mono text-[10px] uppercase tracking-[0.3em]">
@@ -59,7 +59,7 @@ export default function LandingPage() {
</div> </div>
<Reveal delay={0.2}> <Reveal delay={0.2}>
<div className="relative bg-white/80 backdrop-blur-sm p-8 md:p-12 border border-slate-100 rounded-2xl shadow-2xl shadow-slate-100/50 max-w-md mx-auto"> <div className="relative bg-white/90 backdrop-blur-md p-8 md:p-12 border border-slate-100 rounded-2xl shadow-2xl shadow-slate-100/50 max-w-md mx-auto z-10">
<div className="absolute -top-6 -left-6 w-12 h-12 bg-slate-900 text-white flex items-center justify-center rounded-full font-bold text-xl"> <div className="absolute -top-6 -left-6 w-12 h-12 bg-slate-900 text-white flex items-center justify-center rounded-full font-bold text-xl">
01 01
</div> </div>
@@ -95,7 +95,7 @@ export default function LandingPage() {
</h3> </h3>
</Reveal> </Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> <div className="grid grid-cols-1 md:grid-cols-2 gap-12 relative z-10">
<Reveal delay={0.1}> <Reveal delay={0.1}>
<div className="space-y-8"> <div className="space-y-8">
<div className="flex items-center gap-4 text-slate-900 font-bold text-lg"> <div className="flex items-center gap-4 text-slate-900 font-bold text-lg">
@@ -157,7 +157,7 @@ export default function LandingPage() {
</p> </p>
</Reveal> </Reveal>
<div className="grid grid-cols-1 gap-6 relative z-10"> <div className="grid grid-cols-1 gap-6 relative z-20">
<ComparisonRow <ComparisonRow
negativeLabel="Agentur" negativeLabel="Agentur"
negativeText="Konzeptcalls, Meetings, Slides, Warten auf das Angebot." negativeText="Konzeptcalls, Meetings, Slides, Warten auf das Angebot."
@@ -190,7 +190,7 @@ export default function LandingPage() {
<div className="absolute left-0 top-0 w-full h-full -z-10 opacity-30 pointer-events-none"> <div className="absolute left-0 top-0 w-full h-full -z-10 opacity-30 pointer-events-none">
<CirclePattern /> <CirclePattern />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
<Reveal> <Reveal>
<div className="p-10 bg-slate-900 text-white rounded-3xl h-full flex flex-col justify-between group hover:scale-[1.02] transition-transform duration-500 shadow-2xl shadow-slate-900/20"> <div className="p-10 bg-slate-900 text-white rounded-3xl h-full flex flex-col justify-between group hover:scale-[1.02] transition-transform duration-500 shadow-2xl shadow-slate-900/20">
<div className="space-y-6"> <div className="space-y-6">
@@ -235,7 +235,7 @@ export default function LandingPage() {
<ServicesFlow /> <ServicesFlow />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative z-10"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative z-20">
<Reveal delay={0.1}> <Reveal delay={0.1}>
<div className="bg-white p-8 rounded-2xl border border-slate-100 shadow-lg hover:shadow-xl transition-all duration-300 group h-full"> <div className="bg-white p-8 rounded-2xl border border-slate-100 shadow-lg hover:shadow-xl transition-all duration-300 group h-full">
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-500"> <div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-500">
@@ -291,7 +291,7 @@ export default function LandingPage() {
<span className="text-slate-300">starten.</span> <span className="text-slate-300">starten.</span>
</h2> </h2>
<div className="flex flex-col md:flex-row gap-12 items-start"> <div className="flex flex-col md:flex-row gap-12 items-start relative z-10">
<div className="space-y-6 flex-1"> <div className="space-y-6 flex-1">
<p className="text-xl text-slate-600 font-serif italic"> <p className="text-xl text-slate-600 font-serif italic">
Schreiben Sie mir kurz, worum es geht. Ich melde mich innerhalb von 24 Stunden mit einer ersten Einschätzung. Schreiben Sie mir kurz, worum es geht. Ich melde mich innerhalb von 24 Stunden mit einer ersten Einschätzung.

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import { useEffect, useRef, useState, useCallback } from 'react';
import { motion, useScroll, useTransform, useSpring, useAnimationFrame } from 'framer-motion'; import { motion, useScroll, useTransform, useSpring, useAnimationFrame } from 'framer-motion';
interface FlowingPathProps { interface FlowingPathProps {
@@ -36,26 +36,42 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
scrollYProgress, scrollYProgress,
(latest) => { (latest) => {
if (dimensions.height === 0 || vh === 0) return latest; if (dimensions.height === 0 || vh === 0) return latest;
// Total scrollable distance
const scrollRange = dimensions.height - vh; const scrollRange = dimensions.height - vh;
const currentScrollY = latest * scrollRange; const currentScrollY = latest * scrollRange;
const targetY = currentScrollY + vh / 2;
// 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)); return Math.max(0, Math.min(1, targetY / dimensions.height));
} }
); );
// Smooth spring animation for the path drawing // Smooth spring animation for the path drawing
const smoothProgress = useSpring(centeredProgress, { const smoothProgress = useSpring(centeredProgress, {
stiffness: 40, stiffness: 100, // Much higher stiffness for immediate response
damping: 25, damping: 40, // Higher damping to prevent any overshoot/backward motion
restDelta: 0.0001, restDelta: 0.0001,
mass: 0.5, // Lower mass for faster acceleration
}); });
// Transform scroll progress to path drawing // Transform scroll progress to path drawing
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]); const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
// Accent path transform
const accentPathDrawn = useTransform(smoothProgress, [0, 1], [0, 0.98]);
// Dot transforms // Dot transforms
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']); const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '98%']); const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '98%']);
@@ -105,8 +121,9 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
const curr = nodes[i]; const curr = nodes[i];
if (curr.type === 'curve') { if (curr.type === 'curve') {
const cp1y = prev.y + (curr.y - prev.y) * 0.5; const dy = curr.y - prev.y;
const cp2y = prev.y + (curr.y - prev.y) * 0.5; 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}`; path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
} else { } else {
path += ` L ${curr.x} ${curr.y}`; path += ` L ${curr.x} ${curr.y}`;
@@ -116,30 +133,45 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
return path; return path;
}, []); }, []);
// Generate secondary decorative paths // Generate secondary decorative paths that follow the main path with some divergence
const generateSecondaryPath = useCallback((width: number, height: number, offset: number): string => { const generateSecondaryPath = useCallback((width: number, height: number, seed: number): string => {
const margin = width < 768 ? 10 : 40 + offset; const margin = width < 768 ? 20 : 80;
const contentWidth = Math.min(width * 0.85, 1000); const contentWidth = Math.min(width * 0.9, 1200);
const leftEdge = (width - contentWidth) / 2 - margin; const leftEdge = (width - contentWidth) / 2 - margin;
const rightEdge = (width + contentWidth) / 2 + margin; const rightEdge = (width + contentWidth) / 2 + margin;
const centerX = width / 2;
const sectionHeight = height / 8; const sectionHeight = height / 8;
let path = `M ${width / 2 + offset} ${-100}`; const random = (i: number) => {
const x = Math.sin(seed + i) * 10000;
const points = [ return x - Math.floor(x);
{ x: leftEdge + offset, y: sectionHeight * 1.2 }, };
{ x: rightEdge - offset, y: sectionHeight * 2.5 },
{ x: leftEdge - offset, y: sectionHeight * 4.0 }, // Base nodes similar to generatePath but with randomized offsets
{ x: rightEdge + offset, y: sectionHeight * 5.5 }, const nodes = [
{ x: width / 2 + offset, y: height + 100 }, { 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 }
]; ];
for (let i = 0; i < points.length; i++) { let path = `M ${nodes[0].x} ${nodes[0].y}`;
const curr = points[i]; for (let i = 1; i < nodes.length; i++) {
const prev = i > 0 ? points[i - 1] : { x: width / 2 + offset, y: -100 }; const prev = nodes[i - 1];
const cp1y = prev.y + (curr.y - prev.y) * 0.5; const curr = nodes[i];
const cp2y = prev.y + (curr.y - prev.y) * 0.5; 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}`; path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
} }
@@ -196,13 +228,13 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
<div <div
ref={containerRef} ref={containerRef}
className={`absolute inset-0 pointer-events-none overflow-visible ${className}`} className={`absolute inset-0 pointer-events-none overflow-visible ${className}`}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: '100%',
height: dimensions.height, height: dimensions.height,
zIndex: 5, zIndex: 1,
}} }}
aria-hidden="true" aria-hidden="true"
> >
@@ -219,35 +251,36 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
<feGaussianBlur stdDeviation="4" result="blur" /> <feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" /> <feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter> </filter>
<filter id="dotGlow" x="-100%" y="-100%" width="300%" height="300%"> <filter id="dotGlow" x="-200%" y="-200%" width="500%" height="500%">
<feGaussianBlur stdDeviation="3" result="blur" /> <feGaussianBlur stdDeviation="5" result="blur" />
<feColorMatrix type="matrix" values="0 0 0 0 0.58 0 0 0 0 0.64 0 0 0 0 0.72 0 0 0 1 0" /> <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> <feMerge>
<feMergeNode in="blur" />
<feMergeNode in="blur" /> <feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" /> <feMergeNode in="SourceGraphic" />
</feMerge> </feMerge>
</filter> </filter>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="rgba(148, 163, 184, 0.05)" /> <stop offset="0%" stopColor="rgba(0, 0, 0, 0.05)" />
<stop offset="20%" stopColor="rgba(148, 163, 184, 0.3)" /> <stop offset="20%" stopColor="rgba(0, 0, 0, 0.2)" />
<stop offset="50%" stopColor="rgba(148, 163, 184, 0.6)" /> <stop offset="50%" stopColor="rgba(0, 0, 0, 0.4)" />
<stop offset="80%" stopColor="rgba(148, 163, 184, 0.3)" /> <stop offset="80%" stopColor="rgba(0, 0, 0, 0.2)" />
<stop offset="100%" stopColor="rgba(148, 163, 184, 0.05)" /> <stop offset="100%" stopColor="rgba(0, 0, 0, 0.05)" />
</linearGradient> </linearGradient>
</defs> </defs>
{/* Background decorative paths - static, very subtle */} {/* Background decorative paths - static, very subtle */}
<path <path
d={generateSecondaryPath(dimensions.width, dimensions.height, 40)} d={generateSecondaryPath(dimensions.width, dimensions.height, 10)}
stroke="rgba(148, 163, 184, 0.15)" stroke="rgba(0, 0, 0, 0.1)"
strokeWidth="1" strokeWidth="1"
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
strokeDasharray="5,10" strokeDasharray="5,10"
/> />
<path <path
d={generateSecondaryPath(dimensions.width, dimensions.height, -30)} d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
stroke="rgba(148, 163, 184, 0.1)" stroke="rgba(0, 0, 0, 0.08)"
strokeWidth="0.5" strokeWidth="0.5"
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
@@ -258,7 +291,7 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
ref={pathRef} ref={pathRef}
d={pathData} d={pathData}
stroke="url(#lineGradient)" stroke="url(#lineGradient)"
strokeWidth="1.5" strokeWidth="2"
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -268,45 +301,97 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
}} }}
/> />
{/* Accent path that follows slightly behind */} {/* Accent path - fully filled, static */}
<motion.path <path
d={pathData} d={pathData}
stroke="rgba(203, 213, 225, 0.2)" stroke="rgba(0, 0, 0, 0.05)"
strokeWidth="3" strokeWidth="4"
fill="none" fill="none"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
style={{
pathLength: accentPathDrawn,
}}
/> />
{/* Particle System */} {/* Secondary accent path - fully filled, static */}
<ParticleSystem <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} pathRef={pathRef}
progress={smoothProgress} progress={smoothProgress}
pathLength={pathLength} pathLength={pathLength}
checkpoints={[0.12, 0.28, 0.45, 0.62, 0.82]}
/> />
{/* Animated dot that travels along the path */} {/* Animated dot that travels along the path */}
<motion.circle <motion.circle
r="4" r="6"
fill="rgba(30, 41, 59, 1)" fill="white"
stroke="black"
strokeWidth="1.5"
filter="url(#dotGlow)" filter="url(#dotGlow)"
style={{ style={{
offsetPath: `path("${pathData}")`, offsetPath: `path("${pathData}")`,
offsetDistance: dot1Distance, offsetDistance: dot1Distance,
}} }}
animate={{
scale: [1, 1.2, 1],
opacity: [0.8, 1, 0.8],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/> />
{/* Secondary traveling dot */} {/* Secondary traveling dot */}
<motion.circle <motion.circle
r="2" r="3"
fill="rgba(148, 163, 184, 0.6)" fill="rgba(0, 0, 0, 0.4)"
style={{ style={{
offsetPath: `path("${pathData}")`, offsetPath: `path("${pathData}")`,
offsetDistance: dot2Distance, 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 */} {/* Node markers at key positions */}
@@ -385,76 +470,62 @@ const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
); );
}; };
interface Particle { interface Pulse {
id: number; id: number;
x: number; x: number;
y: number; y: number;
vx: number;
vy: number;
life: number; life: number;
size: number;
} }
const ParticleSystem: React.FC<{ const PulseSystem: React.FC<{
pathRef: React.RefObject<SVGPathElement>; pathRef: React.RefObject<SVGPathElement>;
progress: any; progress: any;
pathLength: number; pathLength: number;
}> = ({ pathRef, progress, pathLength }) => { checkpoints: number[];
const [particles, setParticles] = useState<Particle[]>([]); }> = ({ pathRef, progress, pathLength, checkpoints }) => {
const lastPos = useRef({ x: 0, y: 0 }); const [pulses, setPulses] = useState<Pulse[]>([]);
const particleId = useRef(0); const lastProgress = useRef(0);
const pulseId = useRef(0);
useAnimationFrame((time, delta) => { useAnimationFrame((time, delta) => {
if (!pathRef.current || pathLength === 0) return; if (!pathRef.current || pathLength === 0) return;
// Get current position of the dot
const currentProgress = progress.get(); const currentProgress = progress.get();
const point = pathRef.current.getPointAtLength(currentProgress * pathLength); const point = pathRef.current.getPointAtLength(currentProgress * pathLength);
// Update and emit particles in one go // Check for checkpoint pulses
setParticles(prev => { checkpoints.forEach(cp => {
const updated = prev const crossed = (lastProgress.current < cp && currentProgress >= cp) ||
.map(p => ({ (lastProgress.current > cp && currentProgress <= cp);
...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; 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 ( return (
<g> <g>
{particles.map(p => ( {pulses.map(p => (
<circle <circle
key={p.id} key={p.id}
cx={p.x} cx={p.x}
cy={p.y} cy={p.y}
r={p.size} r={(1 - p.life) * 150}
fill={`rgba(148, 163, 184, ${p.life * 0.5})`} fill="none"
stroke="rgba(0, 0, 0, 0.15)"
strokeWidth={p.life * 3}
style={{ opacity: p.life }}
/> />
))} ))}
</g> </g>