big upgrade
This commit is contained in:
156
src/components/BorderGlow.css
Normal file
156
src/components/BorderGlow.css
Normal 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;
|
||||
}
|
||||
184
src/components/BorderGlow.tsx
Normal file
184
src/components/BorderGlow.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
src/components/PixelBlast.css
Normal file
6
src/components/PixelBlast.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.pixel-blast-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
525
src/components/PixelBlast.tsx
Normal file
525
src/components/PixelBlast.tsx
Normal 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;
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
20
src/components/ThemeProvider.tsx
Normal file
20
src/components/ThemeProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
71
src/components/ThemeToggle.tsx
Normal file
71
src/components/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
120
src/components/ui/text-hover-effect.tsx
Normal file
120
src/components/ui/text-hover-effect.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user