132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
'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>
|
||
);
|
||
}
|