feat: replace technical drawings with premium animated design elements (DrillOrbit + StratumLines)

Former-commit-id: f56a9d40b6b4068691b1d8851da455da6514fa3c
This commit is contained in:
2026-05-11 10:49:56 +02:00
parent 8a9b30f926
commit 8fc4cadd19
8 changed files with 310 additions and 1 deletions

View 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>
);
}

View 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>
);
}