big upgrade

This commit is contained in:
2026-05-10 12:19:58 +02:00
parent 5bd2019fc6
commit f7dd547f8d
38 changed files with 2363 additions and 329 deletions

View File

@@ -0,0 +1,156 @@
.border-glow-card {
--edge-proximity: 0;
--cursor-angle: 45deg;
--edge-sensitivity: 30;
--color-sensitivity: calc(var(--edge-sensitivity) + 20);
--border-radius: 28px;
--glow-padding: 40px;
--cone-spread: 25;
position: relative;
border-radius: var(--border-radius);
isolation: isolate;
transform: translate3d(0, 0, 0.01px);
display: grid;
border: 1px solid rgb(255 255 255 / 8%);
background: var(--card-bg, #000);
overflow: hidden;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
box-shadow:
rgba(0, 0, 0, 0.1) 0px 1px 2px,
rgba(0, 0, 0, 0.1) 0px 2px 4px,
rgba(0, 0, 0, 0.1) 0px 4px 8px,
rgba(0, 0, 0, 0.1) 0px 8px 16px,
rgba(0, 0, 0, 0.1) 0px 16px 32px,
rgba(0, 0, 0, 0.1) 0px 32px 64px;
}
.border-glow-card::before,
.border-glow-card::after,
.border-glow-card > .edge-light {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
transition: opacity 0.25s ease-out;
z-index: -1;
pointer-events: none;
}
.border-glow-card:not(:hover):not(.sweep-active)::before,
.border-glow-card:not(:hover):not(.sweep-active)::after,
.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {
opacity: 0 !important;
visibility: hidden;
transition: opacity 0.75s ease-in-out, visibility 0.75s ease-in-out;
}
/* colored mesh-gradient border */
.border-glow-card::before {
border: 1px solid transparent;
background:
linear-gradient(var(--card-bg, #120F17) 0 100%) padding-box,
linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,
var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box,
var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box,
var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box,
var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box,
var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box,
var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box,
var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box,
var(--gradient-base, linear-gradient(#c299ff 0 100%)) border-box;
opacity: calc(
(var(--edge-proximity) - var(--color-sensitivity)) /
(100 - var(--color-sensitivity))
);
mask-image: conic-gradient(
from var(--cursor-angle) at center,
black calc(var(--cone-spread) * 1%),
transparent calc((var(--cone-spread) + 15) * 1%),
transparent calc((100 - var(--cone-spread) - 15) * 1%),
black calc((100 - var(--cone-spread)) * 1%)
);
}
/* colored mesh-gradient background fill near edges */
.border-glow-card::after {
border: 1px solid transparent;
background:
var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) padding-box,
var(--gradient-base, linear-gradient(#c299ff 0 100%)) padding-box;
mask-image:
linear-gradient(to bottom, black, black),
radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
conic-gradient(
from var(--cursor-angle) at center,
transparent 5%,
black 15%,
black 85%,
transparent 95%
);
mask-composite: subtract, add, add, add, add, add;
opacity: calc(
var(--fill-opacity, 0.5) *
(var(--edge-proximity) - var(--color-sensitivity)) /
(100 - var(--color-sensitivity))
);
mix-blend-mode: soft-light;
}
/* outer glow layer */
.border-glow-card > .edge-light {
inset: 0;
pointer-events: none;
z-index: 1;
mask-image: conic-gradient(
from var(--cursor-angle) at center,
black 2.5%,
transparent 10%,
transparent 90%,
black 97.5%
);
opacity: calc(
(var(--edge-proximity) - var(--edge-sensitivity)) /
(100 - var(--edge-sensitivity))
);
mix-blend-mode: plus-lighter;
}
.border-glow-card > .edge-light::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow:
inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),
inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),
0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));
}
.border-glow-inner {
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
z-index: 1;
}

View File

@@ -0,0 +1,184 @@
import { useRef, useCallback, useEffect, type ReactNode } from 'react';
import './BorderGlow.css';
function parseHSL(hslStr: string) {
const match = hslStr.match(/([\d.]+)\s*([\d.]+)%?\s*([\d.]+)%?/);
if (!match) return { h: 40, s: 80, l: 80 };
return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };
}
function buildGlowVars(glowColor: string, intensity: number) {
const { h, s, l } = parseHSL(glowColor);
const base = `${h}deg ${s}% ${l}%`;
const opacities = [100, 60, 50, 40, 30, 20, 10];
const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10'];
const vars: Record<string, string> = {};
for (let i = 0; i < opacities.length; i++) {
vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`;
}
return vars;
}
const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];
const GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven'];
const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];
function buildGradientVars(colors: string[]) {
const vars: Record<string, string> = {};
for (let i = 0; i < 7; i++) {
const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`;
}
vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`;
return vars;
}
function easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }
function easeInCubic(x: number) { return x * x * x; }
function animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: {
start?: number; end?: number; duration?: number; delay?: number;
ease?: (x: number) => number; onUpdate: (v: number) => void; onEnd?: () => void;
}) {
const t0 = performance.now() + delay;
function tick() {
const elapsed = performance.now() - t0;
const t = Math.min(elapsed / duration, 1);
onUpdate(start + (end - start) * ease(t));
if (t < 1) requestAnimationFrame(tick);
else if (onEnd) onEnd();
}
setTimeout(() => requestAnimationFrame(tick), delay);
}
interface BorderGlowProps {
children: ReactNode;
className?: string;
edgeSensitivity?: number;
glowColor?: string;
backgroundColor?: string;
borderRadius?: number;
glowRadius?: number;
glowIntensity?: number;
coneSpread?: number;
animated?: boolean;
colors?: string[];
fillOpacity?: number;
}
const BorderGlow = ({
children,
className = '',
edgeSensitivity = 30,
glowColor = '40 80 80',
backgroundColor = '#120F17',
borderRadius = 28,
glowRadius = 40,
glowIntensity = 1.0,
coneSpread = 25,
animated = false,
colors = ['#c084fc', '#f472b6', '#38bdf8'],
fillOpacity = 0.5,
}: BorderGlowProps) => {
const cardRef = useRef<HTMLDivElement>(null);
const getCenterOfElement = useCallback((el: HTMLElement) => {
const { width, height } = el.getBoundingClientRect();
return [width / 2, height / 2];
}, []);
const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
let kx = Infinity;
let ky = Infinity;
if (dx !== 0) kx = cx / Math.abs(dx);
if (dy !== 0) ky = cy / Math.abs(dy);
return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
}, [getCenterOfElement]);
const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
if (dx === 0 && dy === 0) return 0;
const radians = Math.atan2(dy, dx);
let degrees = radians * (180 / Math.PI) + 90;
if (degrees < 0) degrees += 360;
return degrees;
}, [getCenterOfElement]);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const edge = getEdgeProximity(card, x, y);
const angle = getCursorAngle(card, x, y);
card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`);
card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`);
}, [getEdgeProximity, getCursorAngle]);
useEffect(() => {
if (!animated || !cardRef.current) return;
const card = cardRef.current;
const angleStart = 110;
const angleEnd = 465;
let cancelled = false;
function runSweep() {
if (cancelled || !card) return;
card.classList.add('sweep-active');
card.style.setProperty('--cursor-angle', `${angleStart}deg`);
animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', String(v)) });
animateValue({
ease: easeInCubic, duration: 1500, end: 50,
onUpdate: v => { card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); }
});
animateValue({
ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100,
onUpdate: v => { card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`); }
});
animateValue({
ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,
onUpdate: v => card.style.setProperty('--edge-proximity', String(v)),
onEnd: () => {
card.classList.remove('sweep-active');
if (!cancelled) setTimeout(runSweep, 800);
},
});
}
runSweep();
return () => { cancelled = true; };
}, [animated]);
const glowVars = buildGlowVars(glowColor, glowIntensity);
return (
<div
ref={cardRef}
onPointerMove={animated ? undefined : handlePointerMove}
className={`border-glow-card ${className}`}
style={{
'--card-bg': backgroundColor,
'--edge-sensitivity': edgeSensitivity,
'--border-radius': `${borderRadius}px`,
'--glow-padding': `${glowRadius}px`,
'--cone-spread': coneSpread,
'--fill-opacity': fillOpacity,
...glowVars,
...buildGradientVars(colors),
} as React.CSSProperties}
>
<span className="edge-light" />
<div className="border-glow-inner">
{children}
</div>
</div>
);
};
export default BorderGlow;

View File

@@ -1,50 +1,62 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowRight, Mail, Phone } from "lucide-react";
import BorderGlow from "@/components/BorderGlow";
const Contact = () => {
return (
<section id="contact" className="py-24 md:py-32 bg-background relative">
<div className="container mx-auto px-6">
<div className="max-w-4xl">
{/* Section Header */}
<div className="mb-12">
<div className="label-tag mb-4">Kontakt</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-6">
Bereit für Ihr<br />
nächstes Projekt?
</h2>
<p className="text-muted-foreground text-lg max-w-xl">
Lassen Sie uns gemeinsam Ihre digitale Vision verwirklichen.
Buchen Sie jetzt ein kostenloses Erstgespräch.
</p>
</div>
<div className="max-w-6xl mx-auto">
<BorderGlow
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="#120F17"
borderRadius={28}
glowRadius={40}
glowIntensity={1}
coneSpread={25}
animated
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="p-8 md:p-12 lg:p-16">
<div className="mb-12">
<div className="label-tag mb-4">Kontakt</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-6">
Bereit für Ihr<br />
nächstes Projekt?
</h2>
<p className="text-muted-foreground text-lg max-w-xl">
Lassen Sie uns gemeinsam Ihre digitale Vision verwirklichen.
Buchen Sie jetzt ein kostenloses Erstgespräch.
</p>
</div>
{/* CTA */}
<div className="flex flex-col sm:flex-row gap-4 mb-16">
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Termin buchen
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
<div className="flex flex-col sm:flex-row gap-4 mb-16">
<Link to="/kontakt">
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Termin buchen
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
{/* Contact Info */}
<div className="divider mb-12" />
<div className="flex flex-col sm:flex-row gap-8 text-muted-foreground">
<a href="mailto:support@webklar.com" className="flex items-center gap-3 hover:text-foreground transition-colors group">
<Mail className="w-5 h-5" />
<span>support@webklar.com</span>
</a>
<a href="tel:+491704969375" className="flex items-center gap-3 hover:text-foreground transition-colors group">
<Phone className="w-5 h-5" />
<span>0170 4969375</span>
</a>
</div>
<div className="divider mb-12" />
<div className="flex flex-col sm:flex-row gap-8 text-muted-foreground">
<a href="mailto:support@webklar.com" className="flex items-center gap-3 hover:text-foreground transition-colors group">
<Mail className="w-5 h-5" />
<span>support@webklar.com</span>
</a>
<a href="tel:+491704969375" className="flex items-center gap-3 hover:text-foreground transition-colors group">
<Phone className="w-5 h-5" />
<span>0170 4969375</span>
</a>
</div>
</div>
</BorderGlow>
</div>
</div>
</section>

View File

@@ -14,10 +14,11 @@ import {
MobileNavMenu,
} from "@/components/ui/resizable-navbar";
import Logo from "@/components/Logo";
import { ThemeToggle } from "@/components/ThemeToggle";
const Header = () => {
const navItems = [
{ name: "Über uns", link: "#about" },
{ name: "Über uns", link: "/ueber-uns" },
{ name: "Leistungen", link: "#services" },
{ name: "Projekte", link: "#projects" },
{ name: "Ablauf", link: "#process" },
@@ -38,6 +39,7 @@ const Header = () => {
</NavbarLogo>
<NavItems items={navItems} />
<div className="navbar-actions flex items-center gap-4">
<ThemeToggle />
<Link to="/kontakt">
<NavbarButton
as="span"
@@ -58,28 +60,44 @@ const Header = () => {
Webklar
</span>
</NavbarLogo>
<MobileNavToggle
isOpen={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>
<div className="flex items-center gap-2">
<ThemeToggle />
<MobileNavToggle
isOpen={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
/>
</div>
</MobileNavHeader>
<MobileNavMenu
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
>
{navItems.map((item, idx) => (
<a
key={`mobile-link-${idx}`}
href={item.link}
onClick={() => setIsMobileMenuOpen(false)}
className="relative text-neutral-600 dark:text-neutral-300"
>
<span className="block font-medium uppercase tracking-wider">
{item.name}
</span>
</a>
))}
{navItems.map((item, idx) =>
item.link.startsWith("/") ? (
<Link
key={`mobile-link-${idx}`}
to={item.link}
onClick={() => setIsMobileMenuOpen(false)}
className="relative text-neutral-600 dark:text-neutral-300"
>
<span className="block font-medium uppercase tracking-wider">
{item.name}
</span>
</Link>
) : (
<a
key={`mobile-link-${idx}`}
href={item.link}
onClick={() => setIsMobileMenuOpen(false)}
className="relative text-neutral-600 dark:text-neutral-300"
>
<span className="block font-medium uppercase tracking-wider">
{item.name}
</span>
</a>
)
)}
<div className="flex w-full flex-col gap-4">
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
<NavbarButton

View File

@@ -1,6 +1,7 @@
import { useNavigate } from "react-router-dom";
import { ArrowRight } from "lucide-react";
import React, { useState, useEffect, useRef } from "react";
import { useTheme } from "next-themes";
import Silk from "@/components/Silk";
import CountUp from "@/components/CountUp";
@@ -48,6 +49,7 @@ const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
const Hero = () => {
const navigate = useNavigate();
const { resolvedTheme } = useTheme();
const [companyAge, setCompanyAge] = useState("");
const secondBtnRef = useRef<HTMLButtonElement>(null);
@@ -96,9 +98,19 @@ const Hero = () => {
calculateAge();
const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
return () => clearInterval(interval);
}, []);
// ── Silk-Hintergrund Farben ──
const isDark = resolvedTheme === "dark";
// Silk: Hauptfarbe (Wellenspitzen)
const silkColor = isDark ? "#6a6a6a" : "#ffffff";
// Silk: Zweite Farbe (Wellentäler)
const silkColor2 = isDark ? "#000000" : "#c0c0c0";
// Silk: Rausch-Intensität
const silkNoise = isDark ? 4 : 1.5;
return (
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
{/* Silk animated background */}
@@ -106,9 +118,9 @@ const Hero = () => {
<Silk
speed={3}
scale={0.5}
color="#6a6a6a"
noiseIntensity={4
}
color={silkColor}
color2={silkColor2}
noiseIntensity={silkNoise}
rotation={0}
/>
</div>

View File

@@ -1,33 +1,76 @@
const tools = ["Cursor", "Kiro", "N8N", "Mistral", "Hetzner", "Porkbun", "Appwrite", "Traefik"];
const technologies = [
{ name: "C#", logo: "/svg/csharp.svg" },
{ name: "PHP", logo: "/svg/php.svg" },
{ name: "HTML5", logo: "/svg/html5.svg" },
{ name: "React", logo: "/svg/react.svg" },
{ name: "Next.js", logo: "/svg/nextjs.svg" },
{ name: "Vite", logo: "/svg/vite.svg" },
{ name: "SQL", logo: "/svg/sql.svg" },
];
const Partners = () => {
return <section className="py-16 md:py-20 bg-background border-y border-border overflow-hidden">
<div className="container mx-auto px-6 mb-8">
<div className="label-tag text-center">UNSERE TOOLS MIT DEN WIR ARBEITEN</div>
return (
<section className="py-8 md:py-10 bg-background border-y border-border overflow-hidden">
<div className="container mx-auto px-6 mb-4">
<div className="label-tag text-center">
PROGRAMMIERSPRACHEN
</div>
</div>
<div className="relative">
<div className="relative overflow-x-hidden py-2">
{/* Fade edges */}
<div className="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-background to-transparent z-10" />
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-background to-transparent z-10" />
<div className="pointer-events-none absolute inset-y-0 left-0 w-32 bg-gradient-to-r from-background to-transparent z-20" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-background to-transparent z-20" />
{/* Marquee */}
<div className="flex overflow-hidden">
<div className="marquee">
{[...tools, ...tools].map((tool, index) => <div key={`${tool}-${index}`} className="flex items-center justify-center min-w-[160px] px-8">
<span className="text-xl md:text-2xl font-display font-medium text-muted-foreground/50 hover:text-foreground transition-colors duration-300 uppercase tracking-wider">
{tool}
{[...technologies, ...technologies].map((tech, index) => (
<div
key={`${tech.name}-${index}`}
className="flex items-center gap-3 pr-12 md:pr-20 group transition-all duration-300"
aria-hidden={index >= technologies.length}
>
<img
src={tech.logo}
alt={tech.name}
className="h-10 w-auto object-contain transition-transform group-hover:scale-110 opacity-100 dark:opacity-60"
width={40}
height={40}
loading={index < technologies.length ? "eager" : "lazy"}
decoding="async"
/>
<span className="text-xl md:text-2xl font-display font-medium text-foreground group-hover:text-muted-foreground dark:text-muted-foreground/50 dark:group-hover:text-foreground transition-colors duration-300 whitespace-nowrap">
{tech.name}
</span>
</div>)}
</div>
))}
</div>
<div className="marquee" aria-hidden="true">
{[...tools, ...tools].map((tool, index) => <div key={`${tool}-dup-${index}`} className="flex items-center justify-center min-w-[160px] px-8">
<span className="text-xl md:text-2xl font-display font-medium text-muted-foreground/50 hover:text-foreground transition-colors duration-300 uppercase tracking-wider">
{tool}
{[...technologies, ...technologies].map((tech, index) => (
<div
key={`${tech.name}-dup-${index}`}
className="flex items-center gap-3 pr-12 md:pr-20 group transition-all duration-300"
>
<img
src={tech.logo}
alt={tech.name}
className="h-10 w-auto object-contain transition-transform group-hover:scale-110 opacity-100 dark:opacity-60"
width={40}
height={40}
loading="lazy"
decoding="async"
/>
<span className="text-xl md:text-2xl font-display font-medium text-foreground group-hover:text-muted-foreground dark:text-muted-foreground/50 dark:group-hover:text-foreground transition-colors duration-300 whitespace-nowrap">
{tech.name}
</span>
</div>)}
</div>
))}
</div>
</div>
</div>
</section>;
</section>
);
};
export default Partners;
export default Partners;

View File

@@ -0,0 +1,6 @@
.pixel-blast-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}

View File

@@ -0,0 +1,525 @@
/* eslint-disable react/no-unknown-property */
import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import './PixelBlast.css';
const createTouchTexture = () => {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D context not available');
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
const trail: Array<{ x: number; y: number; age: number; force: number; vx: number; vy: number }> = [];
let last: { x: number; y: number } | null = null;
const maxAge = 64;
let radius = 0.1 * size;
const speed = 1 / maxAge;
const clear = () => {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const drawPoint = (p: { x: number; y: number; age: number; force: number; vx: number; vy: number }) => {
const pos = { x: p.x * size, y: (1 - p.y) * size };
let intensity = 1;
const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2);
const easeOutQuad = (t: number) => -t * (t - 2);
if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));
else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;
intensity *= p.force;
const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;
const offset = size * 5;
ctx.shadowOffsetX = offset;
ctx.shadowOffsetY = offset;
ctx.shadowBlur = radius;
ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,1)';
ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);
ctx.fill();
};
const addTouch = (norm: { x: number; y: number }) => {
let force = 0;
let vx = 0;
let vy = 0;
if (last) {
const dx = norm.x - last.x;
const dy = norm.y - last.y;
if (dx === 0 && dy === 0) return;
const dd = dx * dx + dy * dy;
const d = Math.sqrt(dd);
vx = dx / (d || 1);
vy = dy / (d || 1);
force = Math.min(dd * 10000, 1);
}
last = { x: norm.x, y: norm.y };
trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });
};
const update = () => {
clear();
for (let i = trail.length - 1; i >= 0; i--) {
const point = trail[i];
const f = point.force * speed * (1 - point.age / maxAge);
point.x += point.vx * f;
point.y += point.vy * f;
point.age++;
if (point.age > maxAge) trail.splice(i, 1);
}
for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);
texture.needsUpdate = true;
};
return {
canvas,
texture,
addTouch,
update,
set radiusScale(v: number) { radius = 0.1 * size * v; },
get radiusScale() { return radius / (0.1 * size); },
size,
};
};
const createLiquidEffect = (texture: THREE.Texture, opts?: { strength?: number; freq?: number }) => {
const fragment = `
uniform sampler2D uTexture;
uniform float uStrength;
uniform float uTime;
uniform float uFreq;
void mainUv(inout vec2 uv) {
vec4 tex = texture2D(uTexture, uv);
float vx = tex.r * 2.0 - 1.0;
float vy = tex.g * 2.0 - 1.0;
float intensity = tex.b;
float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);
float amt = uStrength * intensity * wave;
uv += vec2(vx, vy) * amt;
}
`;
return new Effect('LiquidEffect', fragment, {
uniforms: new Map([
['uTexture', new THREE.Uniform(texture)],
['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
['uTime', new THREE.Uniform(0)],
['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)],
]),
});
};
const SHAPE_MAP: Record<string, number> = { square: 0, circle: 1, triangle: 2, diamond: 3 };
const VERTEX_SRC = `void main() { gl_Position = vec4(position, 1.0); }`;
const FRAGMENT_SRC = `precision highp float;
uniform vec3 uColor;
uniform vec2 uResolution;
uniform float uTime;
uniform float uPixelSize;
uniform float uScale;
uniform float uDensity;
uniform float uPixelJitter;
uniform int uEnableRipples;
uniform float uRippleSpeed;
uniform float uRippleThickness;
uniform float uRippleIntensity;
uniform float uEdgeFade;
uniform int uShapeType;
const int SHAPE_SQUARE = 0;
const int SHAPE_CIRCLE = 1;
const int SHAPE_TRIANGLE = 2;
const int SHAPE_DIAMOND = 3;
const int MAX_CLICKS = 10;
uniform vec2 uClickPos [MAX_CLICKS];
uniform float uClickTimes[MAX_CLICKS];
out vec4 fragColor;
float Bayer2(vec2 a) { a = floor(a); return fract(a.x / 2. + a.y * a.y * .75); }
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))
#define FBM_OCTAVES 5
#define FBM_LACUNARITY 1.25
#define FBM_GAIN 1.0
float hash11(float n){ return fract(sin(n)*43758.5453); }
float vnoise(vec3 p){
vec3 ip = floor(p); vec3 fp = fract(p);
float n000 = hash11(dot(ip + vec3(0,0,0), vec3(1,57,113)));
float n100 = hash11(dot(ip + vec3(1,0,0), vec3(1,57,113)));
float n010 = hash11(dot(ip + vec3(0,1,0), vec3(1,57,113)));
float n110 = hash11(dot(ip + vec3(1,1,0), vec3(1,57,113)));
float n001 = hash11(dot(ip + vec3(0,0,1), vec3(1,57,113)));
float n101 = hash11(dot(ip + vec3(1,0,1), vec3(1,57,113)));
float n011 = hash11(dot(ip + vec3(0,1,1), vec3(1,57,113)));
float n111 = hash11(dot(ip + vec3(1,1,1), vec3(1,57,113)));
vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
float x00 = mix(n000, n100, w.x); float x10 = mix(n010, n110, w.x);
float x01 = mix(n001, n101, w.x); float x11 = mix(n011, n111, w.x);
float y0 = mix(x00, x10, w.y); float y1 = mix(x01, x11, w.y);
return mix(y0, y1, w.z) * 2.0 - 1.0;
}
float fbm2(vec2 uv, float t){
vec3 p = vec3(uv * uScale, t);
float amp = 1.0; float freq = 1.0; float sum = 1.0;
for (int i = 0; i < FBM_OCTAVES; ++i){ sum += amp * vnoise(p * freq); freq *= FBM_LACUNARITY; amp *= FBM_GAIN; }
return sum * 0.5 + 0.5;
}
float maskCircle(vec2 p, float cov){ float r = sqrt(cov) * .25; float d = length(p - 0.5) - r; float aa = 0.5 * fwidth(d); return cov * (1.0 - smoothstep(-aa, aa, d * 2.0)); }
float maskTriangle(vec2 p, vec2 id, float cov){ bool flip = mod(id.x + id.y, 2.0) > 0.5; if (flip) p.x = 1.0 - p.x; float r = sqrt(cov); float d = p.y - r*(1.0 - p.x); float aa = fwidth(d); return cov * clamp(0.5 - d/aa, 0.0, 1.0); }
float maskDiamond(vec2 p, float cov){ float r = sqrt(cov) * 0.564; return step(abs(p.x - 0.49) + abs(p.y - 0.49), r); }
void main(){
float pixelSize = uPixelSize;
vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
float aspectRatio = uResolution.x / uResolution.y;
vec2 pixelId = floor(fragCoord / pixelSize);
vec2 pixelUV = fract(fragCoord / pixelSize);
float cellPixelSize = 8.0 * pixelSize;
vec2 cellId = floor(fragCoord / cellPixelSize);
vec2 cellCoord = cellId * cellPixelSize;
vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
float base = fbm2(uv, uTime * 0.05);
base = base * 0.5 - 0.65;
float feed = base + (uDensity - 0.5) * 0.3;
float speed = uRippleSpeed; float thickness = uRippleThickness;
const float dampT = 1.0; const float dampR = 10.0;
if (uEnableRipples == 1) {
for (int i = 0; i < MAX_CLICKS; ++i){
vec2 pos = uClickPos[i]; if (pos.x < 0.0) continue;
float cellPixelSize2 = 8.0 * pixelSize;
vec2 cuv = (((pos - uResolution * .5 - cellPixelSize2 * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
float t = max(uTime - uClickTimes[i], 0.0);
float r = distance(uv, cuv);
float waveR = speed * t;
float ring = exp(-pow((r - waveR) / thickness, 2.0));
float atten = exp(-dampT * t) * exp(-dampR * r);
feed = max(feed, ring * atten * uRippleIntensity);
}
}
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
float bw = step(0.5, feed + bayer);
float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
float coverage = bw * jitterScale;
float M;
if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);
else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);
else M = coverage;
if (uEdgeFade > 0.0) {
vec2 norm = gl_FragCoord.xy / uResolution;
float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
float fade = smoothstep(0.0, uEdgeFade, edge);
M *= fade;
}
vec3 color = uColor;
vec3 srgbColor = mix(color * 12.92, 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, color));
fragColor = vec4(srgbColor, M);
}`;
const MAX_CLICKS = 10;
interface PixelBlastProps {
variant?: 'square' | 'circle' | 'triangle' | 'diamond';
pixelSize?: number;
color?: string;
className?: string;
style?: React.CSSProperties;
antialias?: boolean;
patternScale?: number;
patternDensity?: number;
liquid?: boolean;
liquidStrength?: number;
liquidRadius?: number;
pixelSizeJitter?: number;
enableRipples?: boolean;
rippleIntensityScale?: number;
rippleThickness?: number;
rippleSpeed?: number;
liquidWobbleSpeed?: number;
autoPauseOffscreen?: boolean;
speed?: number;
transparent?: boolean;
edgeFade?: number;
noiseAmount?: number;
}
const PixelBlast = ({
variant = 'square',
pixelSize = 3,
color = '#B497CF',
className,
style,
antialias = true,
patternScale = 2,
patternDensity = 1,
liquid = false,
liquidStrength = 0.1,
liquidRadius = 1,
pixelSizeJitter = 0,
enableRipples = true,
rippleIntensityScale = 1,
rippleThickness = 0.1,
rippleSpeed = 0.3,
liquidWobbleSpeed = 4.5,
autoPauseOffscreen = true,
speed = 0.5,
transparent = true,
edgeFade = 0.5,
noiseAmount = 0,
}: PixelBlastProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const visibilityRef = useRef({ visible: true });
const speedRef = useRef(speed);
const threeRef = useRef<any>(null);
const prevConfigRef = useRef<any>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
speedRef.current = speed;
const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];
const cfg: Record<string, any> = { antialias, liquid, noiseAmount };
let mustReinit = false;
if (!threeRef.current) mustReinit = true;
else if (prevConfigRef.current) {
for (const k of needsReinitKeys)
if (prevConfigRef.current[k] !== cfg[k]) { mustReinit = true; break; }
}
if (mustReinit) {
if (threeRef.current) {
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
t.renderer.forceContextLoss();
if (t.renderer.domElement.parentElement === container)
container.removeChild(t.renderer.domElement);
threeRef.current = null;
}
const canvas = document.createElement('canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias, alpha: true, powerPreference: 'high-performance' });
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
container.appendChild(renderer.domElement);
if (transparent) renderer.setClearAlpha(0);
else renderer.setClearColor(0x000000, 1);
const uniforms: Record<string, any> = {
uResolution: { value: new THREE.Vector2(0, 0) },
uTime: { value: 0 },
uColor: { value: new THREE.Color(color) },
uClickPos: { value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1)) },
uClickTimes: { value: new Float32Array(MAX_CLICKS) },
uShapeType: { value: SHAPE_MAP[variant] ?? 0 },
uPixelSize: { value: pixelSize * renderer.getPixelRatio() },
uScale: { value: patternScale },
uDensity: { value: patternDensity },
uPixelJitter: { value: pixelSizeJitter },
uEnableRipples: { value: enableRipples ? 1 : 0 },
uRippleSpeed: { value: rippleSpeed },
uRippleThickness: { value: rippleThickness },
uRippleIntensity: { value: rippleIntensityScale },
uEdgeFade: { value: edgeFade },
};
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const material = new THREE.ShaderMaterial({
vertexShader: VERTEX_SRC,
fragmentShader: FRAGMENT_SRC,
uniforms,
transparent: true,
depthTest: false,
depthWrite: false,
glslVersion: THREE.GLSL3,
});
const quadGeom = new THREE.PlaneGeometry(2, 2);
const quad = new THREE.Mesh(quadGeom, material);
scene.add(quad);
const clock = new THREE.Clock();
const setSize = () => {
const w = container.clientWidth || 1;
const h = container.clientHeight || 1;
renderer.setSize(w, h, false);
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);
if (threeRef.current?.composer)
threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height);
uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio();
};
setSize();
const ro = new ResizeObserver(setSize);
ro.observe(container);
const randomFloat = () => {
if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
const u32 = new Uint32Array(1);
window.crypto.getRandomValues(u32);
return u32[0] / 0xffffffff;
}
return Math.random();
};
const timeOffset = randomFloat() * 1000;
let composer: EffectComposer | undefined;
let touch: ReturnType<typeof createTouchTexture> | undefined;
let liquidEffect: Effect | undefined;
if (liquid) {
touch = createTouchTexture();
touch.radiusScale = liquidRadius;
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
liquidEffect = createLiquidEffect(touch.texture, { strength: liquidStrength, freq: liquidWobbleSpeed });
const effectPass = new EffectPass(camera, liquidEffect);
effectPass.renderToScreen = true;
composer.addPass(renderPass);
composer.addPass(effectPass);
}
if (noiseAmount > 0) {
if (!composer) {
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
}
const noiseEffect = new Effect('NoiseEffect',
`uniform float uTime; uniform float uAmount;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453); }
void mainUv(inout vec2 uv){}
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor){
float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0));
float g=(n-0.5)*uAmount;
outputColor=inputColor+vec4(vec3(g),0.0);
}`,
{ uniforms: new Map([['uTime', new THREE.Uniform(0)], ['uAmount', new THREE.Uniform(noiseAmount)]]) }
);
const noisePass = new EffectPass(camera, noiseEffect);
noisePass.renderToScreen = true;
if (composer && composer.passes.length > 0) composer.passes.forEach((p: any) => (p.renderToScreen = false));
composer.addPass(noisePass);
}
if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);
const mapToPixels = (e: PointerEvent) => {
const rect = renderer.domElement.getBoundingClientRect();
const scaleX = renderer.domElement.width / rect.width;
const scaleY = renderer.domElement.height / rect.height;
const fx = (e.clientX - rect.left) * scaleX;
const fy = (rect.height - (e.clientY - rect.top)) * scaleY;
return { fx, fy, w: renderer.domElement.width, h: renderer.domElement.height };
};
const onPointerDown = (e: PointerEvent) => {
const { fx, fy } = mapToPixels(e);
const ix = threeRef.current?.clickIx ?? 0;
uniforms.uClickPos.value[ix].set(fx, fy);
uniforms.uClickTimes.value[ix] = uniforms.uTime.value;
if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS;
};
const onPointerMove = (e: PointerEvent) => {
if (!touch) return;
const { fx, fy, w, h } = mapToPixels(e);
touch.addTouch({ x: fx / w, y: fy / h });
};
renderer.domElement.addEventListener('pointerdown', onPointerDown, { passive: true });
renderer.domElement.addEventListener('pointermove', onPointerMove, { passive: true });
let raf = 0;
const animate = () => {
if (autoPauseOffscreen && !visibilityRef.current.visible) {
raf = requestAnimationFrame(animate);
return;
}
uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current;
if (liquidEffect) liquidEffect.uniforms.get('uTime')!.value = uniforms.uTime.value;
if (composer) {
if (touch) touch.update();
composer.passes.forEach((p: any) => {
const effs = p.effects;
if (effs) effs.forEach((eff: any) => { const u = eff.uniforms?.get('uTime'); if (u) u.value = uniforms.uTime.value; });
});
composer.render();
} else renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
};
raf = requestAnimationFrame(animate);
threeRef.current = { renderer, scene, camera, material, clock, clickIx: 0, uniforms, resizeObserver: ro, raf, quad, timeOffset, composer, touch, liquidEffect };
} else {
const t = threeRef.current;
t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0;
t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio();
t.uniforms.uColor.value.set(color);
t.uniforms.uScale.value = patternScale;
t.uniforms.uDensity.value = patternDensity;
t.uniforms.uPixelJitter.value = pixelSizeJitter;
t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0;
t.uniforms.uRippleIntensity.value = rippleIntensityScale;
t.uniforms.uRippleThickness.value = rippleThickness;
t.uniforms.uRippleSpeed.value = rippleSpeed;
t.uniforms.uEdgeFade.value = edgeFade;
if (transparent) t.renderer.setClearAlpha(0);
else t.renderer.setClearColor(0x000000, 1);
if (t.liquidEffect) {
const uFreq = t.liquidEffect.uniforms.get('uFreq');
if (uFreq) uFreq.value = liquidWobbleSpeed;
}
if (t.touch) t.touch.radiusScale = liquidRadius;
}
prevConfigRef.current = cfg;
return () => {
if (threeRef.current && mustReinit) return;
if (!threeRef.current) return;
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
t.renderer.forceContextLoss();
if (t.renderer.domElement.parentElement === container)
container.removeChild(t.renderer.domElement);
threeRef.current = null;
};
}, [
antialias, liquid, noiseAmount, pixelSize, patternScale, patternDensity,
enableRipples, rippleIntensityScale, rippleThickness, rippleSpeed,
pixelSizeJitter, edgeFade, transparent, liquidStrength, liquidRadius,
liquidWobbleSpeed, autoPauseOffscreen, variant, color, speed,
]);
return (
<div
ref={containerRef}
className={`pixel-blast-container ${className ?? ''}`}
style={style}
aria-label="PixelBlast interactive background"
/>
);
};
export default PixelBlast;

View File

@@ -1,8 +1,21 @@
import { Calendar, MessageSquareOff, TrendingDown, Folders } from "lucide-react";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { LampTop } from "@/components/ui/lamp";
import LightRays from "@/components/LightRays";
import PixelBlast from "@/components/PixelBlast";
const ProblemSection = () => {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const isDark = mounted && resolvedTheme === "dark";
const isLight = mounted && resolvedTheme === "light";
const problems = [
{
icon: Calendar,
@@ -24,6 +37,23 @@ const ProblemSection = () => {
return (
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
{/* PixelBlast animated background - nur im Light Mode */}
{isLight && (
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#f43f5e"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
)}
{/* Hintergrundbild: nur Blitze rechts, auf Handy maximale Breite */}
<div
className="problem-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] z-0"
@@ -32,22 +62,25 @@ const ProblemSection = () => {
}}
aria-hidden
/>
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
raysColor="#ef4444"
raysSpeed={1}
lightSpread={0.5}
rayLength={3}
followMouse={false}
mouseInfluence={0}
noiseAmount={0}
distortion={0}
pulsating
fadeDistance={2}
saturation={2}
/>
</div>
{/* LightRays - nur im Dark Mode */}
{isDark && (
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
raysColor="#ef4444"
raysSpeed={1}
lightSpread={0.5}
rayLength={3}
followMouse={false}
mouseInfluence={0}
noiseAmount={0}
distortion={0}
pulsating
fadeDistance={2}
saturation={2}
/>
</div>
)}
<LampTop />
<div className="container mx-auto px-6 relative z-10">
{/* Section Header */}
@@ -66,7 +99,7 @@ const ProblemSection = () => {
{problems.map((problem, index) => (
<div
key={index}
className="problem-section-tint flex items-start gap-4 p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
className="problem-section-tint flex items-start gap-4 p-6 rounded-2xl bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.12)] hover:bg-white/[0.06] hover:border-white/[0.12] transition-all duration-300"
>
<div className="w-10 h-10 rounded-full border border-destructive/30 bg-destructive/10 flex items-center justify-center flex-shrink-0">
<problem.icon className="w-5 h-5 text-destructive" />

View File

@@ -1,4 +1,6 @@
import CountUp from "@/components/CountUp";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
import PixelBlast from "@/components/PixelBlast";
const Process = () => {
const steps = [
@@ -25,14 +27,37 @@ const Process = () => {
];
return (
<section id="process" className="py-24 md:py-32 bg-secondary/20 relative">
<div className="container mx-auto px-6">
<section id="process" className="pt-8 pb-24 md:pt-12 md:pb-32 bg-secondary/20 relative">
{/* PixelBlast animated background */}
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#5b5b5b"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
rippleSpeed={0.4}
rippleThickness={0.12}
rippleIntensityScale={1.5}
liquid={false}
liquidStrength={0.12}
liquidRadius={1.2}
liquidWobbleSpeed={5}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Ablauf" />
</div>
<div className="container mx-auto px-6 relative z-10">
{/* Section Header */}
<div className="mb-16 md:mb-24">
<div className="mb-4">
<div className="label-tag mb-4">So arbeiten wir</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Ablauf
</h2>
</div>
<div className="max-w-4xl">

View File

@@ -1,4 +1,7 @@
import { ArrowUpRight } from "lucide-react";
import { useTheme } from "next-themes";
import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
type Project = {
title: string;
@@ -41,49 +44,60 @@ const projects: Project[] = [
];
const ProjectShowcase = () => {
const { resolvedTheme } = useTheme();
const cardBg = resolvedTheme === "dark" ? "hsl(0 0% 6%)" : "hsl(0 0% 96%)";
return (
<section id="projects" className="py-24 md:py-32 bg-background relative">
<section id="projects" className="pt-8 pb-24 md:pt-12 md:pb-32 bg-background relative">
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Projekte" />
</div>
<div className="container mx-auto px-6">
{/* Section Header */}
<div className="mb-16 md:mb-24">
<div className="label-tag mb-4">Ausgewählte Arbeiten</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Projekte
</h2>
</div>
{/* Projects Grid */}
<div className="space-y-2">
{projects.map((project, index) => (
<a
<BorderGlow
key={project.title}
href={project.url}
target={project.url.startsWith("http") ? "_blank" : undefined}
rel={project.url.startsWith("http") ? "noopener noreferrer" : undefined}
className="group block project-card rounded-lg p-6 md:p-8"
style={{ animationDelay: `${index * 0.1}s` }}
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor={cardBg}
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground mb-2 group-hover:text-muted-foreground transition-colors uppercase tracking-tight">
{project.title}
</h3>
<p className="text-muted-foreground text-sm uppercase tracking-wider">
{project.description}
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-24 h-16 md:w-32 md:h-20 rounded overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<img
src={project.image}
alt={project.title}
className="w-full h-full object-cover"
/>
<a
href={project.url}
target={project.url.startsWith("http") ? "_blank" : undefined}
rel={project.url.startsWith("http") ? "noopener noreferrer" : undefined}
className="group block p-6 md:p-8"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground mb-2 group-hover:text-muted-foreground transition-colors uppercase tracking-tight">
{project.title}
</h3>
<p className="text-muted-foreground text-sm uppercase tracking-wider">
{project.description}
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-24 h-16 md:w-32 md:h-20 rounded overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<img
src={project.image}
alt={project.title}
className="w-full h-full object-cover"
/>
</div>
<ArrowUpRight className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
<ArrowUpRight className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
</div>
</a>
</a>
</BorderGlow>
))}
</div>
</div>

View File

@@ -29,14 +29,11 @@ const Services = () => {
];
return (
<section id="services" className="py-24 md:py-32 bg-background relative">
<section id="services" className="pt-8 pb-8 md:pt-12 md:pb-12 bg-background relative">
<div className="container mx-auto px-6">
{/* Section Header */}
<div className="mb-16 md:mb-24">
<div className="mb-4">
<div className="label-tag mb-4">Was wir tun</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase">
Leistungen
</h2>
</div>
<div className="grid md:grid-cols-2 gap-px bg-border rounded-lg overflow-hidden">

View File

@@ -1,4 +1,4 @@
/* eslint-disable react/no-unknown-property */
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
import { Color, type Mesh, type ShaderMaterial } from "three";
@@ -33,6 +33,7 @@ uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
uniform vec3 uColor2;
const float e = 2.71828182845904523536;
@@ -63,7 +64,8 @@ void main() {
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
vec3 mixed = mix(uColor2, uColor, pattern);
vec4 col = vec4(mixed, 1.0) - rnd / 15.0 * uNoiseIntensity;
col.a = 1.0;
gl_FragColor = col;
}
@@ -74,6 +76,7 @@ type SilkPlaneProps = {
uSpeed: { value: number };
uScale: { value: number };
uNoiseIntensity: { value: number };
uColor2: { value: Color };
uColor: { value: Color };
uRotation: { value: number };
uTime: { value: number };
@@ -118,6 +121,7 @@ type SilkProps = {
speed?: number;
scale?: number;
color?: string;
color2?: string;
noiseIntensity?: number;
rotation?: number;
};
@@ -126,6 +130,7 @@ const Silk = ({
speed = 5,
scale = 1,
color = "#7B7481",
color2 = "#000000",
noiseIntensity = 1.5,
rotation = 0,
}: SilkProps) => {
@@ -136,11 +141,12 @@ const Silk = ({
uSpeed: { value: speed },
uScale: { value: scale },
uNoiseIntensity: { value: noiseIntensity },
uColor2: { value: new Color(...hexToNormalizedRGB(color2)) },
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
uRotation: { value: rotation },
uTime: { value: 0 },
}),
[speed, scale, noiseIntensity, color, rotation]
[speed, scale, noiseIntensity, color, color2, rotation]
);
return (

View File

@@ -1,10 +1,23 @@
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import { ArrowRight, CheckCircle2 } from "lucide-react";
import { LampTop } from "@/components/ui/lamp";
import LightRays from "@/components/LightRays";
import PixelBlast from "@/components/PixelBlast";
const SolutionSection = () => {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const isDark = mounted && resolvedTheme === "dark";
const isLight = mounted && resolvedTheme === "light";
const benefits = [
"Alle Prozesse greifen ineinander.",
"Informationen fließen automatisch.",
@@ -13,6 +26,23 @@ const SolutionSection = () => {
return (
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
{/* PixelBlast animated background - nur im Light Mode */}
{isLight && (
<div className="absolute inset-0 z-0 w-full h-full">
<PixelBlast
variant="circle"
pixelSize={3}
color="#06b6d4"
patternScale={7.75}
patternDensity={0.7}
pixelSizeJitter={2}
enableRipples={false}
speed={0.3}
edgeFade={0.5}
transparent
/>
</div>
)}
{/* Hintergrundbild mittig, auf Handy maximale Breite */}
<div
className="solution-section-bg absolute inset-0 bg-center bg-no-repeat opacity-[0.3] z-0"
@@ -21,22 +51,25 @@ const SolutionSection = () => {
}}
aria-hidden
/>
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
raysColor="#22d3ee"
raysSpeed={1}
lightSpread={0.5}
rayLength={3}
followMouse={false}
mouseInfluence={0}
noiseAmount={0}
distortion={0}
pulsating
fadeDistance={2}
saturation={2}
/>
</div>
{/* LightRays - nur im Dark Mode */}
{isDark && (
<div className="absolute inset-0 w-full overflow-hidden z-0">
<LightRays
raysOrigin="top-center"
raysColor="#22d3ee"
raysSpeed={1}
lightSpread={0.5}
rayLength={3}
followMouse={false}
mouseInfluence={0}
noiseAmount={0}
distortion={0}
pulsating
fadeDistance={2}
saturation={2}
/>
</div>
)}
<LampTop lineClassName="bg-cyan-400" />
<div className="container mx-auto px-6 relative z-10">
<div className="grid lg:grid-cols-2 gap-16 items-center">
@@ -74,7 +107,7 @@ const SolutionSection = () => {
{/* Right Content - Visual Element */}
<div className="relative">
<div className="solution-section-tint aspect-square bg-secondary/50 rounded-2xl border border-border p-8 md:p-12 flex flex-col justify-center">
<div className="solution-section-tint aspect-square rounded-2xl bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.12)] p-8 md:p-12 flex flex-col justify-center">
<div className="space-y-6">
<div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div>
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight">

View File

@@ -0,0 +1,20 @@
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps as NextThemeProviderProps } from "next-themes";
export interface ThemeProviderProps extends Omit<NextThemeProviderProps, "children"> {
children: React.ReactNode;
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem={true}
storageKey="theme"
{...props}
>
{children}
</NextThemesProvider>
);
}

View File

@@ -0,0 +1,71 @@
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon, Monitor } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface ThemeToggleProps {
className?: string;
}
const themeLabels: Record<string, string> = {
light: "Hell",
dark: "Dunkel",
system: "System",
};
export function ThemeToggle({ className }: ThemeToggleProps) {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const currentLabel = themeLabels[theme ?? "system"] ?? "System";
const icon = !mounted ? (
<Monitor className="h-4 w-4" />
) : theme === "system" ? (
<Monitor className="h-4 w-4" />
) : resolvedTheme === "dark" ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={className}
aria-label={`Theme wechseln, aktuell: ${currentLabel}`}
>
{icon}
<span className="sr-only">Theme wechseln</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Sun className="mr-2 h-4 w-4" />
Hell
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Moon className="mr-2 h-4 w-4" />
Dunkel
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Monitor className="mr-2 h-4 w-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,4 +1,6 @@
import { Users, Cog, MessageSquare, Target, BarChart3, Layers } from "lucide-react";
import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
const Values = () => {
const features = [
@@ -35,10 +37,14 @@ const Values = () => {
];
return (
<section id="features" className="py-24 md:py-32 bg-background relative overflow-hidden">
<section id="features" className="pt-8 pb-0 md:pt-12 md:pb-0 bg-background relative overflow-hidden">
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Leistungen" />
</div>
{/* Hintergrundbild: auf Handy maximale Breite */}
<div
className="values-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3]"
className="values-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] pointer-events-none"
style={{
backgroundImage: "url(/backgroud_effect.png)",
}}
@@ -55,21 +61,32 @@ const Values = () => {
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div
<BorderGlow
key={feature.title}
className="group p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
style={{ animationDelay: `${index * 0.1}s` }}
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor="hsl(0 0% 6%)"
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<div className="w-12 h-12 rounded-full border border-border flex items-center justify-center mb-6 group-hover:border-foreground/30 transition-colors">
<feature.icon className="w-5 h-5 text-muted-foreground group-hover:text-foreground transition-colors" />
<div
className="group p-6"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="w-12 h-12 rounded-full border border-border flex items-center justify-center mb-6 group-hover:border-foreground/30 transition-colors">
<feature.icon className="w-5 h-5 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
<h3 className="text-xl font-display font-medium text-foreground mb-3 uppercase tracking-tight">
{feature.title}
</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
{feature.description}
</p>
</div>
<h3 className="text-xl font-display font-medium text-foreground mb-3 uppercase tracking-tight">
{feature.title}
</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
{feature.description}
</p>
</div>
</BorderGlow>
))}
</div>
</div>

View File

@@ -18,7 +18,7 @@ export const LampTop = ({
return (
<div
className={cn(
"absolute top-0 left-0 right-0 w-full min-h-0 pointer-events-none z-50 flex items-start justify-center",
"absolute top-0 left-0 right-0 w-full min-h-0 pointer-events-none z-10 flex items-start justify-center",
className
)}
>

View File

@@ -9,6 +9,7 @@ import {
useMotionValueEvent,
} from "motion/react";
import React, { useRef, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
interface NavbarProps {
children: React.ReactNode;
@@ -72,8 +73,8 @@ export const NavBody = ({ children, className, visible }: NavBodyProps) => {
}}
className={cn(
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "bg-black/90",
"text-black dark:text-white [&_a]:text-black dark:[&_a]:text-white [&_a:hover]:text-black/70 dark:[&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
visible && "!text-white dark:!text-white [&_a]:!text-white dark:[&_a]:!text-white [&_a:hover]:!text-white/90 bg-black/90",
className
)}
>
@@ -108,23 +109,31 @@ export const NavItems = ({
className
)}
>
{items.map((item, idx) => (
<a
onMouseEnter={() => setHovered(idx)}
onClick={onItemClick}
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
key={`link-${idx}`}
href={item.link}
>
{hovered === idx && (
<motion.div
layoutId="hovered"
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
/>
)}
<span className="relative z-20">{item.name}</span>
</a>
))}
{items.map((item, idx) => {
const isRoute = item.link.startsWith("/");
const commonProps = {
onMouseEnter: () => setHovered(idx),
onClick: onItemClick,
className: "relative px-4 py-2 text-neutral-900 dark:text-neutral-300",
key: `link-${idx}`,
};
const inner = (
<>
{hovered === idx && (
<motion.div
layoutId="hovered"
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
/>
)}
<span className="relative z-20">{item.name}</span>
</>
);
return isRoute ? (
<RouterLink to={item.link} {...commonProps}>{inner}</RouterLink>
) : (
<a href={item.link} {...commonProps}>{inner}</a>
);
})}
</motion.div>
);
};
@@ -160,8 +169,8 @@ export const MobileNav = ({
}}
className={cn(
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
visible && "bg-black/90",
"[&>div:first-child]:text-black dark:[&>div:first-child]:text-white [&>div:first-child_a]:text-black dark:[&>div:first-child_a]:text-white [&>div:first-child_svg]:text-black dark:[&>div:first-child_svg]:text-white",
visible && "[&>div:first-child]:!text-white dark:[&>div:first-child]:!text-white [&>div:first-child_a]:!text-white [&>div:first-child_svg]:!text-white bg-black/90",
className
)}
>

View File

@@ -0,0 +1,120 @@
"use client";
import { useRef, useState, useCallback, useId } from "react";
import { motion } from "motion/react";
export const TextHoverEffect = ({
text,
}: {
text: string;
duration?: number;
}) => {
const svgRef = useRef<SVGSVGElement>(null);
const [hovered, setHovered] = useState(false);
const [cursorPos, setCursorPos] = useState({ x: 150, y: 30 });
const id = useId();
const gradientId = `textGradient-${id}`;
const revealMaskId = `revealMask-${id}`;
const textMaskId = `textMask-${id}`;
const handleMouseMove = useCallback(
(e: React.MouseEvent<SVGSVGElement>) => {
if (!svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
// Map screen coords to viewBox coords (0 0 300 60)
const x = ((e.clientX - rect.left) / rect.width) * 300;
const y = ((e.clientY - rect.top) / rect.height) * 60;
setCursorPos({ x, y });
},
[]
);
return (
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 300 60"
xmlns="http://www.w3.org/2000/svg"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseMove={handleMouseMove}
className="select-none"
>
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#eab308" />
<stop offset="25%" stopColor="#ef4444" />
<stop offset="50%" stopColor="#3b82f6" />
<stop offset="75%" stopColor="#06b6d4" />
<stop offset="100%" stopColor="#8b5cf6" />
</linearGradient>
<radialGradient
id={revealMaskId}
gradientUnits="userSpaceOnUse"
cx={cursorPos.x}
cy={cursorPos.y}
r="80"
>
<stop offset="0%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</radialGradient>
<mask id={textMaskId}>
<rect
x="0"
y="0"
width="300"
height="60"
fill={hovered ? `url(#${revealMaskId})` : "black"}
/>
</mask>
</defs>
{/* Faint outline always visible */}
<text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
style={{ opacity: 0.15, fontSize: "2rem" }}
>
{text}
</text>
{/* Animated stroke draw */}
<motion.text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
strokeWidth="0.3"
className="font-display font-bold fill-transparent stroke-neutral-200 dark:stroke-neutral-800"
style={{ fontSize: "2rem" }}
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
animate={{ strokeDashoffset: 0, strokeDasharray: 1000 }}
transition={{ duration: 4, ease: "easeInOut" }}
>
{text}
</motion.text>
{/* Colored gradient revealed on hover */}
<text
x="150"
y="30"
textAnchor="middle"
dominantBaseline="middle"
stroke={`url(#${gradientId})`}
strokeWidth="0.3"
mask={`url(#${textMaskId})`}
className="font-display font-bold fill-transparent"
style={{ fontSize: "2rem" }}
>
{text}
</text>
</svg>
);
};