fix: DrillOrbit fades in/out at path edges, DrillBitSpinner uses radial mask instead of hard clip
Former-commit-id: 3aea27f3e17984634206ca817d074b18e712933e
This commit is contained in:
@@ -6,24 +6,23 @@ import { useRef } from 'react';
|
||||
/**
|
||||
* DrillOrbit
|
||||
*
|
||||
* A glowing point continuously looping along a parabolic arc —
|
||||
* evoking the trajectory of a horizontal directional drill bit.
|
||||
* A glowing point looping along a parabolic arc —
|
||||
* abstractly evoking a horizontal directional drill path.
|
||||
*
|
||||
* The arc is drawn once on scroll-entry. The dot then loops forever.
|
||||
* Place inside any `relative overflow-hidden` section. Zero layout impact.
|
||||
* The SVG has a horizontal linear-gradient mask: the entire
|
||||
* element fades in from the edge and out toward the opposite side.
|
||||
* The travelling dot also animates its own opacity to fade in/out
|
||||
* at the path endpoints — no abrupt appearance or disappearance.
|
||||
*/
|
||||
|
||||
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({
|
||||
@@ -37,6 +36,18 @@ export function DrillOrbit({
|
||||
const color = invert ? '#084c39' : '#0e7a5c';
|
||||
const glowColor = invert ? 'rgba(8,76,57,0.35)' : 'rgba(14,122,92,0.45)';
|
||||
|
||||
// Fade from transparent → opaque → transparent over the animation cycle
|
||||
// so the dot smoothly appears from the edge and disappears at the far edge.
|
||||
const dotOpacityKeyframes = [0, 0, 1, 1, 1, 0, 0];
|
||||
const dotOpacityTimes = [0, 0.08, 0.18, 0.5, 0.82, 0.92, 1];
|
||||
|
||||
const transition = {
|
||||
duration: 5.5,
|
||||
ease: 'easeInOut' as const,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 1.5,
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
@@ -50,11 +61,15 @@ export function DrillOrbit({
|
||||
width: 'clamp(360px, 55vw, 800px)',
|
||||
height: 'clamp(180px, 27vw, 420px)',
|
||||
transform: side === 'left' ? 'scaleX(-1)' : undefined,
|
||||
// Horizontal mask: fades at the near edge (0→20%) and far edge (80%→100%)
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)',
|
||||
maskImage:
|
||||
'linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)',
|
||||
}}
|
||||
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>
|
||||
@@ -62,16 +77,9 @@ export function DrillOrbit({
|
||||
<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) ── */}
|
||||
{/* Bore arc */}
|
||||
<motion.path
|
||||
d={PATH}
|
||||
stroke={color}
|
||||
@@ -83,7 +91,7 @@ export function DrillOrbit({
|
||||
transition={{ duration: 2.5, ease: [0.4, 0, 0.2, 1] }}
|
||||
/>
|
||||
|
||||
{/* ── Dashed ground-level reference ── */}
|
||||
{/* Dashed ground-level reference */}
|
||||
<motion.line
|
||||
x1="0" y1="80" x2="800" y2="80"
|
||||
stroke={color}
|
||||
@@ -95,7 +103,7 @@ export function DrillOrbit({
|
||||
transition={{ duration: 1.6, ease: 'easeOut' }}
|
||||
/>
|
||||
|
||||
{/* ── Continuously looping drill-head ── */}
|
||||
{/* Continuously looping drill-head — fades in/out at path ends */}
|
||||
{isInView && (
|
||||
<>
|
||||
{/* Outer glow bloom */}
|
||||
@@ -104,25 +112,33 @@ export function DrillOrbit({
|
||||
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 }}
|
||||
animate={{
|
||||
offsetDistance: ['0%', '100%'],
|
||||
opacity: dotOpacityKeyframes,
|
||||
}}
|
||||
transition={{ ...transition, times: dotOpacityTimes }}
|
||||
/>
|
||||
{/* 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 }}
|
||||
animate={{
|
||||
offsetDistance: ['0%', '100%'],
|
||||
opacity: dotOpacityKeyframes,
|
||||
}}
|
||||
transition={{ ...transition, times: dotOpacityTimes }}
|
||||
/>
|
||||
{/* 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 }}
|
||||
animate={{
|
||||
offsetDistance: ['0%', '100%'],
|
||||
opacity: dotOpacityKeyframes.map((v) => v * 0.8),
|
||||
}}
|
||||
transition={{ ...transition, times: dotOpacityTimes }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user