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">
{/* 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>
<div className="space-y-8">
<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>
<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">
01
</div>
@@ -95,7 +95,7 @@ export default function LandingPage() {
</h3>
</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}>
<div className="space-y-8">
<div className="flex items-center gap-4 text-slate-900 font-bold text-lg">
@@ -157,7 +157,7 @@ export default function LandingPage() {
</p>
</Reveal>
<div className="grid grid-cols-1 gap-6 relative z-10">
<div className="grid grid-cols-1 gap-6 relative z-20">
<ComparisonRow
negativeLabel="Agentur"
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">
<CirclePattern />
</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>
<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">
@@ -235,7 +235,7 @@ export default function LandingPage() {
<ServicesFlow />
</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}>
<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">
@@ -291,7 +291,7 @@ export default function LandingPage() {
<span className="text-slate-300">starten.</span>
</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">
<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.

View File

@@ -1,7 +1,7 @@
'use client';
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';
interface FlowingPathProps {
@@ -36,26 +36,42 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
scrollYProgress,
(latest) => {
if (dimensions.height === 0 || vh === 0) return latest;
// Total scrollable distance
const scrollRange = dimensions.height - vh;
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));
}
);
// Smooth spring animation for the path drawing
const smoothProgress = useSpring(centeredProgress, {
stiffness: 40,
damping: 25,
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]);
// Accent path transform
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%', '98%']);
@@ -105,8 +121,9 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
const curr = nodes[i];
if (curr.type === 'curve') {
const cp1y = prev.y + (curr.y - prev.y) * 0.5;
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}`;
} else {
path += ` L ${curr.x} ${curr.y}`;
@@ -116,30 +133,45 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
return path;
}, []);
// Generate secondary decorative paths
const generateSecondaryPath = useCallback((width: number, height: number, offset: number): string => {
const margin = width < 768 ? 10 : 40 + offset;
const contentWidth = Math.min(width * 0.85, 1000);
// 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;
let path = `M ${width / 2 + offset} ${-100}`;
const points = [
{ 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 },
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 }
];
for (let i = 0; i < points.length; i++) {
const curr = points[i];
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;
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}`;
}
@@ -196,13 +228,13 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
<div
ref={containerRef}
className={`absolute inset-0 pointer-events-none overflow-visible ${className}`}
style={{
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: dimensions.height,
zIndex: 5,
zIndex: 1,
}}
aria-hidden="true"
>
@@ -219,35 +251,36 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="dotGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="3" 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" />
<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(148, 163, 184, 0.05)" />
<stop offset="20%" stopColor="rgba(148, 163, 184, 0.3)" />
<stop offset="50%" stopColor="rgba(148, 163, 184, 0.6)" />
<stop offset="80%" stopColor="rgba(148, 163, 184, 0.3)" />
<stop offset="100%" stopColor="rgba(148, 163, 184, 0.05)" />
<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, 40)}
stroke="rgba(148, 163, 184, 0.15)"
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, -30)}
stroke="rgba(148, 163, 184, 0.1)"
d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
stroke="rgba(0, 0, 0, 0.08)"
strokeWidth="0.5"
fill="none"
strokeLinecap="round"
@@ -258,7 +291,7 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
ref={pathRef}
d={pathData}
stroke="url(#lineGradient)"
strokeWidth="1.5"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
@@ -268,45 +301,97 @@ export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
}}
/>
{/* Accent path that follows slightly behind */}
<motion.path
{/* Accent path - fully filled, static */}
<path
d={pathData}
stroke="rgba(203, 213, 225, 0.2)"
strokeWidth="3"
stroke="rgba(0, 0, 0, 0.05)"
strokeWidth="4"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{
pathLength: accentPathDrawn,
}}
/>
{/* Particle System */}
<ParticleSystem
{/* 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="4"
fill="rgba(30, 41, 59, 1)"
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="2"
fill="rgba(148, 163, 184, 0.6)"
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 */}
@@ -385,76 +470,62 @@ const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
);
};
interface Particle {
interface Pulse {
id: number;
x: number;
y: number;
vx: number;
vy: number;
life: number;
size: number;
}
const ParticleSystem: React.FC<{
const PulseSystem: React.FC<{
pathRef: React.RefObject<SVGPathElement>;
progress: any;
pathLength: number;
}> = ({ pathRef, progress, pathLength }) => {
const [particles, setParticles] = useState<Particle[]>([]);
const lastPos = useRef({ x: 0, y: 0 });
const particleId = useRef(0);
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;
// 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);
}
// Check for checkpoint pulses
checkpoints.forEach(cp => {
const crossed = (lastProgress.current < cp && currentProgress >= cp) ||
(lastProgress.current > cp && currentProgress <= cp);
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 (
<g>
{particles.map(p => (
{pulses.map(p => (
<circle
key={p.id}
cx={p.x}
cy={p.y}
r={p.size}
fill={`rgba(148, 163, 184, ${p.life * 0.5})`}
r={(1 - p.life) * 150}
fill="none"
stroke="rgba(0, 0, 0, 0.15)"
strokeWidth={p.life * 3}
style={{ opacity: p.life }}
/>
))}
</g>