132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
'use client';
|
||
|
||
import { motion } from 'framer-motion';
|
||
|
||
/**
|
||
* DrillBitSpinner
|
||
*
|
||
* Abstract animated drill head cross-section.
|
||
* Resembles an HDD tri-blade drill bit seen from the front.
|
||
*
|
||
* - 3 rotating cutting blades at 120° intervals
|
||
* - Outer halo ring
|
||
* - Inner core circle
|
||
* - Continuous rotation with optional pulse glow
|
||
*
|
||
* Zero layout impact — position it absolutely in its parent.
|
||
*/
|
||
|
||
interface DrillBitSpinnerProps {
|
||
/** Diameter in pixels */
|
||
size?: number;
|
||
/** Color of the drill elements */
|
||
color?: string;
|
||
/** Rotation duration in seconds */
|
||
duration?: number;
|
||
/** Overall opacity 0–1 */
|
||
opacity?: number;
|
||
/** Whether to show a pulse glow ring */
|
||
glow?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
export function DrillBitSpinner({
|
||
size = 120,
|
||
color = '#0e7a5c',
|
||
duration = 8,
|
||
opacity = 1,
|
||
glow = true,
|
||
className = '',
|
||
}: DrillBitSpinnerProps) {
|
||
const r = size / 2;
|
||
const outerR = r * 0.92;
|
||
const bladeLen = r * 0.62;
|
||
const bladeW = r * 0.18;
|
||
const coreR = r * 0.22;
|
||
|
||
// 3 blade tip positions at 120° intervals
|
||
// Each blade: a rounded rectangle swept from center outward
|
||
const blades = [0, 120, 240].map((deg) => {
|
||
const rad = (deg * Math.PI) / 180;
|
||
return {
|
||
x: r + Math.cos(rad) * (bladeLen * 0.5),
|
||
y: r + Math.sin(rad) * (bladeLen * 0.5),
|
||
rotate: deg,
|
||
};
|
||
});
|
||
|
||
return (
|
||
<div
|
||
className={`relative pointer-events-none select-none ${className}`}
|
||
style={{ width: size, height: size, opacity }}
|
||
aria-hidden="true"
|
||
>
|
||
{/* Glow bloom (pulsing) */}
|
||
{glow && (
|
||
<motion.div
|
||
className="absolute inset-0 rounded-full"
|
||
style={{ background: `radial-gradient(circle, ${color}40 0%, transparent 70%)` }}
|
||
animate={{ scale: [1, 1.4, 1], opacity: [0.6, 0.2, 0.6] }}
|
||
transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
|
||
/>
|
||
)}
|
||
|
||
{/* The rotating drill assembly */}
|
||
<motion.svg
|
||
viewBox={`0 0 ${size} ${size}`}
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="absolute inset-0 w-full h-full"
|
||
animate={{ rotate: 360 }}
|
||
transition={{ duration, repeat: Infinity, ease: 'linear' }}
|
||
>
|
||
{/* Outer halo ring */}
|
||
<circle
|
||
cx={r} cy={r} r={outerR}
|
||
stroke={color}
|
||
strokeWidth={size * 0.018}
|
||
strokeDasharray={`${outerR * 0.4} ${outerR * 0.15}`}
|
||
opacity={0.35}
|
||
/>
|
||
|
||
{/* Three cutting blades */}
|
||
{blades.map((blade, i) => (
|
||
<g key={i} transform={`rotate(${blade.rotate} ${r} ${r})`}>
|
||
{/* Main blade body */}
|
||
<rect
|
||
x={r - bladeW / 2}
|
||
y={r - coreR}
|
||
width={bladeW}
|
||
height={bladeLen}
|
||
rx={bladeW / 2}
|
||
fill={color}
|
||
opacity={0.7}
|
||
transform={`rotate(0 ${r} ${r})`}
|
||
/>
|
||
{/* Blade tip accent */}
|
||
<circle
|
||
cx={r}
|
||
cy={r - coreR - bladeLen + bladeW / 2}
|
||
r={bladeW * 0.55}
|
||
fill={color}
|
||
opacity={0.9}
|
||
/>
|
||
</g>
|
||
))}
|
||
|
||
{/* Inner core ring */}
|
||
<circle
|
||
cx={r} cy={r} r={coreR + size * 0.025}
|
||
stroke={color}
|
||
strokeWidth={size * 0.022}
|
||
opacity={0.5}
|
||
/>
|
||
|
||
{/* Center nozzle (mud port) */}
|
||
<circle cx={r} cy={r} r={coreR * 0.55} fill={color} opacity={0.85} />
|
||
<circle cx={r} cy={r} r={coreR * 0.22} fill="white" opacity={0.6} />
|
||
</motion.svg>
|
||
</div>
|
||
);
|
||
}
|