feat: replace technical drawings with premium animated design elements (DrillOrbit + StratumLines)
Former-commit-id: f56a9d40b6b4068691b1d8851da455da6514fa3c
This commit is contained in:
@@ -6,6 +6,7 @@ import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
||||
|
||||
interface CompetenceBentoGridProps {
|
||||
badge?: string;
|
||||
@@ -167,6 +168,9 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Decorative: abstract earth strata / excavation layers */}
|
||||
<StratumLines placement="bottom-left" invert />
|
||||
<StratumLines placement="bottom-right" invert />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from 'next/image';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import { Badge, Container, Heading } from '@/components/ui';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||
|
||||
export interface HeroSectionProps {
|
||||
title: string;
|
||||
@@ -70,6 +71,8 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
{/* Decorative: abstract HDD bore trajectory */}
|
||||
<DrillOrbit side="right" />
|
||||
</section>
|
||||
</Reveal>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||
|
||||
export interface Reference {
|
||||
id: string;
|
||||
@@ -151,6 +152,8 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Decorative: abstract HDD bore trajectory */}
|
||||
<DrillOrbit side="left" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||
|
||||
interface SubCompanyTilesProps {
|
||||
badge?: string;
|
||||
@@ -65,7 +66,7 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
})) || defaultCompanies;
|
||||
|
||||
return (
|
||||
<section id="unternehmen" className="py-24 bg-neutral-dark border-b border-neutral-800">
|
||||
<section id="unternehmen" className="py-24 bg-neutral-dark border-b border-neutral-800 relative overflow-hidden">
|
||||
<div className="container relative z-10 px-4">
|
||||
|
||||
{(badge || title) && (
|
||||
@@ -197,6 +198,8 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
{/* Decorative: abstract HDD bore trajectory */}
|
||||
<DrillOrbit side="right" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from 'next/image';
|
||||
import { useLocale } from 'next-intl';
|
||||
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
||||
|
||||
interface CompanyInfo {
|
||||
contactEmail: string;
|
||||
@@ -150,6 +151,8 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<div className="flex items-center gap-2"><Zap className="w-4 h-4 text-white/40" /><span>High Performance</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Decorative: abstract earth strata */}
|
||||
<StratumLines placement="bottom-right" />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,6 +166,38 @@ export function PageTransitionShutter() {
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Abstract HDD bore arc — decorative element during page transitions */}
|
||||
<svg
|
||||
viewBox="0 0 700 220"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="absolute bottom-0 left-0 w-full h-32 sm:h-44 opacity-[0.07] pointer-events-none"
|
||||
>
|
||||
<motion.path
|
||||
d="M -50 60 C 120 60 180 180 350 180 C 520 180 580 60 750 60"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.6, ease: [0.4, 0, 0.2, 1], delay: 0.2 }}
|
||||
/>
|
||||
<motion.line
|
||||
x1="0" y1="60" x2="700" y2="60"
|
||||
stroke="white" strokeWidth="0.8" strokeDasharray="8 12"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.2, ease: 'easeOut' }}
|
||||
/>
|
||||
<motion.circle
|
||||
r={7}
|
||||
fill="#0e7a5c"
|
||||
style={{ offsetPath: "path('M -50 60 C 120 60 180 180 350 180 C 520 180 580 60 750 60')", offsetDistance: '0%' } as React.CSSProperties}
|
||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
||||
transition={{ duration: 1.8, ease: 'easeInOut', delay: 0.4, repeat: Infinity, repeatDelay: 0.6 }}
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
131
components/ui/decorations/DrillOrbit.tsx
Normal file
131
components/ui/decorations/DrillOrbit.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
/**
|
||||
* DrillOrbit
|
||||
*
|
||||
* A glowing point continuously looping along a parabolic arc —
|
||||
* evoking the trajectory of a horizontal directional drill bit.
|
||||
*
|
||||
* The arc is drawn once on scroll-entry. The dot then loops forever.
|
||||
* Place inside any `relative overflow-hidden` section. Zero layout impact.
|
||||
*/
|
||||
|
||||
type Side = 'left' | 'right';
|
||||
|
||||
interface DrillOrbitProps {
|
||||
/** Which side of the section to anchor to */
|
||||
side?: Side;
|
||||
/** Invert colors for light backgrounds */
|
||||
invert?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// The parabolic bore path (left-to-right across a 800×200 canvas)
|
||||
const PATH = 'M -20 80 C 150 80 200 340 400 340 C 600 340 650 80 820 80';
|
||||
|
||||
export function DrillOrbit({
|
||||
side = 'right',
|
||||
invert = false,
|
||||
className = '',
|
||||
}: DrillOrbitProps) {
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '0px 0px -10% 0px' });
|
||||
|
||||
const color = invert ? '#084c39' : '#0e7a5c';
|
||||
const glowColor = invert ? 'rgba(8,76,57,0.35)' : 'rgba(14,122,92,0.45)';
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
viewBox="0 0 800 420"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`absolute pointer-events-none select-none ${
|
||||
side === 'right' ? 'right-0 bottom-0' : 'left-0 bottom-0'
|
||||
} ${className}`}
|
||||
style={{
|
||||
width: 'clamp(360px, 55vw, 800px)',
|
||||
height: 'clamp(180px, 27vw, 420px)',
|
||||
transform: side === 'left' ? 'scaleX(-1)' : undefined,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
{/* Glow filter for the drill-head */}
|
||||
<filter id="drill-glow" x="-80%" y="-80%" width="260%" height="260%">
|
||||
<feGaussianBlur stdDeviation="10" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
{/* Fading trail gradient along path */}
|
||||
<linearGradient id="trail-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity="0" />
|
||||
<stop offset="60%" stopColor={color} stopOpacity="0.6" />
|
||||
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* ── Bore arc (draws in once, stays) ── */}
|
||||
<motion.path
|
||||
d={PATH}
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
opacity={0.18}
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={isInView ? { pathLength: 1 } : { pathLength: 0 }}
|
||||
transition={{ duration: 2.5, ease: [0.4, 0, 0.2, 1] }}
|
||||
/>
|
||||
|
||||
{/* ── Dashed ground-level reference ── */}
|
||||
<motion.line
|
||||
x1="0" y1="80" x2="800" y2="80"
|
||||
stroke={color}
|
||||
strokeWidth="0.8"
|
||||
strokeDasharray="6 10"
|
||||
opacity={0.12}
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={isInView ? { pathLength: 1 } : { pathLength: 0 }}
|
||||
transition={{ duration: 1.6, ease: 'easeOut' }}
|
||||
/>
|
||||
|
||||
{/* ── Continuously looping drill-head ── */}
|
||||
{isInView && (
|
||||
<>
|
||||
{/* Outer glow bloom */}
|
||||
<motion.circle
|
||||
r={22}
|
||||
fill={glowColor}
|
||||
filter="url(#drill-glow)"
|
||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
||||
/>
|
||||
{/* Core dot */}
|
||||
<motion.circle
|
||||
r={5}
|
||||
fill={color}
|
||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
||||
/>
|
||||
{/* Inner white highlight */}
|
||||
<motion.circle
|
||||
r={2}
|
||||
fill="white"
|
||||
opacity={0.8}
|
||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
130
components/ui/decorations/StratumLines.tsx
Normal file
130
components/ui/decorations/StratumLines.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
|
||||
/**
|
||||
* StratumLines
|
||||
*
|
||||
* Abstract horizontal "earth stratum" lines that slide in from opposite sides
|
||||
* and form a downward-tapered open shape — evoking excavated earth layers.
|
||||
*
|
||||
* - On scroll entry: lines slide in with staggered delay, longest at top
|
||||
* - After entry: each line gently breathes (oscillates horizontally) forever
|
||||
*
|
||||
* Place inside any `relative overflow-hidden` section. Zero layout impact.
|
||||
*/
|
||||
|
||||
interface StratumLinesProps {
|
||||
/** Which corner to anchor to */
|
||||
placement?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
||||
/** Invert colors for light backgrounds */
|
||||
invert?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type StratumLine = {
|
||||
width: number; // % of container width
|
||||
fromRight: boolean; // which side it slides in from
|
||||
delay: number;
|
||||
breatheAmp: number; // px oscillation
|
||||
breatheDur: number;
|
||||
};
|
||||
|
||||
// The stratum lines — longest at top, tapering like an open excavation
|
||||
const LINES: StratumLine[] = [
|
||||
{ width: 100, fromRight: false, delay: 0.0, breatheAmp: 4, breatheDur: 5.2 },
|
||||
{ width: 88, fromRight: true, delay: 0.1, breatheAmp: 5, breatheDur: 4.7 },
|
||||
{ width: 74, fromRight: false, delay: 0.2, breatheAmp: 6, breatheDur: 5.8 },
|
||||
{ width: 60, fromRight: true, delay: 0.3, breatheAmp: 5, breatheDur: 4.3 },
|
||||
{ width: 46, fromRight: false, delay: 0.4, breatheAmp: 4, breatheDur: 6.1 },
|
||||
{ width: 32, fromRight: true, delay: 0.5, breatheAmp: 3, breatheDur: 5.5 },
|
||||
{ width: 18, fromRight: false, delay: 0.6, breatheAmp: 2, breatheDur: 4.9 },
|
||||
];
|
||||
|
||||
const PLACEMENT_CLASSES: Record<NonNullable<StratumLinesProps['placement']>, string> = {
|
||||
'bottom-left': 'bottom-0 left-0',
|
||||
'bottom-right': 'bottom-0 right-0',
|
||||
'top-left': 'top-0 left-0',
|
||||
'top-right': 'top-0 right-0',
|
||||
};
|
||||
|
||||
export function StratumLines({
|
||||
placement = 'bottom-left',
|
||||
invert = false,
|
||||
className = '',
|
||||
}: StratumLinesProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '0px 0px -8% 0px' });
|
||||
|
||||
const color = invert ? '#084c39' : '#0e7a5c';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`absolute pointer-events-none select-none ${PLACEMENT_CLASSES[placement]} ${className}`}
|
||||
style={{
|
||||
width: 'clamp(180px, 24vw, 380px)',
|
||||
height: 'clamp(160px, 20vw, 320px)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="relative w-full h-full flex flex-col justify-end gap-[10%] pb-[8%]">
|
||||
{LINES.map((line, i) => {
|
||||
const isFromRight = placement.includes('right') ? !line.fromRight : line.fromRight;
|
||||
return (
|
||||
<motion.div
|
||||
key={i}
|
||||
style={{
|
||||
width: `${line.width}%`,
|
||||
height: '1.5px',
|
||||
background: color,
|
||||
opacity: 0.12 + (i / LINES.length) * 0.28,
|
||||
alignSelf: isFromRight ? 'flex-end' : 'flex-start',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
// Slide in from edge on first view
|
||||
initial={{
|
||||
x: isFromRight ? 60 : -60,
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={
|
||||
isInView
|
||||
? {
|
||||
// Settle at natural position, then breathe
|
||||
x: [
|
||||
0,
|
||||
isFromRight ? -line.breatheAmp : line.breatheAmp,
|
||||
0,
|
||||
isFromRight ? -line.breatheAmp * 0.6 : line.breatheAmp * 0.6,
|
||||
0,
|
||||
],
|
||||
opacity: 0.12 + (i / LINES.length) * 0.28,
|
||||
}
|
||||
: { x: isFromRight ? 60 : -60, opacity: 0 }
|
||||
}
|
||||
transition={
|
||||
isInView
|
||||
? {
|
||||
x: {
|
||||
times: [0, 0.25, 0.5, 0.75, 1],
|
||||
duration: line.breatheDur,
|
||||
delay: line.delay,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.7,
|
||||
delay: line.delay,
|
||||
ease: 'easeOut',
|
||||
},
|
||||
}
|
||||
: { duration: 0.3 }
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user