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:
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
interface CompetenceBentoGridProps {
|
interface CompetenceBentoGridProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -76,8 +77,8 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-neutral text-neutral-dark overflow-hidden relative">
|
<section className="py-24 bg-neutral text-neutral-dark relative overflow-hidden">
|
||||||
<div className="container">
|
<div className="container relative z-10 px-4">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
<div className="flex flex-col md:flex-row justify-between items-end mb-12">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
<h2 className="text-primary font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
||||||
@@ -176,6 +177,8 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
{/* Decorative: abstract earth strata / excavation layers */}
|
{/* Decorative: abstract earth strata / excavation layers */}
|
||||||
<StratumLines placement="bottom-left" invert />
|
<StratumLines placement="bottom-left" invert />
|
||||||
<StratumLines placement="bottom-right" invert />
|
<StratumLines placement="bottom-right" invert />
|
||||||
|
{/* Organic wave divider */}
|
||||||
|
<DrillTeeth position="bottom" fill="var(--color-neutral)" />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Badge, Container, Heading } from '@/components/ui';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
export interface HeroSectionProps {
|
export interface HeroSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -78,6 +79,8 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
{/* Decorative: abstract HDD bore trajectory */}
|
||||||
<DrillOrbit side="right" />
|
<DrillOrbit side="right" />
|
||||||
|
{/* Organic wave divider — displaced earth strata */}
|
||||||
|
<DrillTeeth position="bottom" fill="var(--color-primary-dark)" />
|
||||||
</section>
|
</section>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
interface HeroVideoProps {
|
interface HeroVideoProps {
|
||||||
data?: any;
|
data?: any;
|
||||||
@@ -108,6 +109,8 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Organic wave divider — displaced earth strata */}
|
||||||
|
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" className="z-[5]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Image from 'next/image';
|
|||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
export interface Reference {
|
export interface Reference {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -83,7 +84,7 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-neutral-dark text-white overflow-hidden relative select-none">
|
<section className="py-24 bg-neutral-dark text-white relative overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
||||||
|
|
||||||
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6">
|
<div className="container relative z-10 mb-12 flex flex-col md:flex-row justify-between items-end gap-6">
|
||||||
@@ -159,6 +160,8 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
</div>
|
</div>
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
{/* Decorative: abstract HDD bore trajectory */}
|
||||||
<DrillOrbit side="left" />
|
<DrillOrbit side="left" />
|
||||||
|
{/* Organic wave divider */}
|
||||||
|
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
export interface ServicePanelData {
|
export interface ServicePanelData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -229,6 +230,8 @@ export function ServiceDetailGrid({
|
|||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Organic wave divider */}
|
||||||
|
<DrillTeeth position="bottom" fill="#fafafa" />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Image from 'next/image';
|
|||||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||||
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
interface SubCompanyTilesProps {
|
interface SubCompanyTilesProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -205,6 +206,8 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
</div>
|
</div>
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
{/* Decorative: abstract HDD bore trajectory */}
|
||||||
<DrillOrbit side="right" />
|
<DrillOrbit side="right" />
|
||||||
|
{/* Organic wave divider */}
|
||||||
|
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
|
|||||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||||
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
import { StratumLines } from '@/components/ui/decorations/StratumLines';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
||||||
|
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
||||||
|
|
||||||
interface CompanyInfo {
|
interface CompanyInfo {
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
@@ -21,6 +22,8 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-[#050B14] text-neutral-light py-16 relative overflow-hidden border-t border-white/5">
|
<footer className="bg-[#050B14] text-neutral-light py-16 relative overflow-hidden border-t border-white/5">
|
||||||
|
{/* Organic wave divider at the top */}
|
||||||
|
<DrillTeeth position="top" fill="#050B14" />
|
||||||
{/* Subtle background tech grid */}
|
{/* Subtle background tech grid */}
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,23 @@ export function PageTransitionShutter() {
|
|||||||
initial={{ y: '100%' }}
|
initial={{ y: '100%' }}
|
||||||
animate={{ y: '0%' }}
|
animate={{ y: '0%' }}
|
||||||
exit={{ y: '-100%' }}
|
exit={{ y: '-100%' }}
|
||||||
transition={{ duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
|
||||||
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-hidden"
|
className="fixed left-0 w-full z-[9998] bg-[#050B14] pointer-events-none flex flex-col items-center justify-center overflow-visible"
|
||||||
style={{
|
style={{
|
||||||
height: '130vh',
|
height: '110vh',
|
||||||
top: '-15vh',
|
top: '-5vh',
|
||||||
clipPath: 'polygon(0% 8%, 8.3% 0%, 16.6% 8%, 25% 0%, 33.3% 8%, 41.6% 0%, 50% 8%, 58.3% 0%, 66.6% 8%, 75% 0%, 83.3% 8%, 91.6% 0%, 100% 8%, 100% 92%, 91.6% 100%, 83.3% 92%, 75% 100%, 66.6% 92%, 58.3% 100%, 50% 92%, 41.6% 100%, 33.3% 92%, 25% 100%, 16.6% 92%, 8.3% 100%, 0% 92%)'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Top organic wave edge */}
|
||||||
|
<svg viewBox="0 0 1440 48" preserveAspectRatio="none" className="absolute top-0 -translate-y-full left-0 w-full h-[clamp(16px,3vw,48px)]" style={{ transform: 'translateY(-99%) scaleY(-1)' }}>
|
||||||
|
<path d="M0,48 L0,28 C120,36 240,6 480,12 C720,18 840,42 1080,24 C1200,16 1320,32 1440,28 L1440,48 Z" fill="#050B14" />
|
||||||
|
<path d="M0,48 L0,36 C180,42 360,16 600,22 C840,28 960,44 1200,32 C1320,26 1400,38 1440,36 L1440,48 Z" fill="#050B14" opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
{/* Bottom organic wave edge */}
|
||||||
|
<svg viewBox="0 0 1440 48" preserveAspectRatio="none" className="absolute bottom-0 translate-y-full left-0 w-full h-[clamp(16px,3vw,48px)]" style={{ transform: 'translateY(99%)' }}>
|
||||||
|
<path d="M0,48 L0,28 C120,36 240,6 480,12 C720,18 840,42 1080,24 C1200,16 1320,32 1440,28 L1440,48 Z" fill="#050B14" />
|
||||||
|
<path d="M0,48 L0,36 C180,42 360,16 600,22 C840,28 960,44 1200,32 C1320,26 1400,38 1440,36 L1440,48 Z" fill="#050B14" opacity="0.6" />
|
||||||
|
</svg>
|
||||||
{/* Subtle grid background */}
|
{/* Subtle grid background */}
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:32px_32px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
||||||
|
|
||||||
|
|||||||
@@ -8,24 +8,15 @@ import { motion } from 'framer-motion';
|
|||||||
* Abstract animated drill head cross-section.
|
* Abstract animated drill head cross-section.
|
||||||
* Resembles an HDD tri-blade drill bit seen from the front.
|
* Resembles an HDD tri-blade drill bit seen from the front.
|
||||||
*
|
*
|
||||||
* - 3 rotating cutting blades at 120° intervals
|
* The wrapping div uses a radial-gradient mask so it fades naturally
|
||||||
* - Outer halo ring
|
* into whatever background it's placed on — no hard edge clipping.
|
||||||
* - Inner core circle
|
|
||||||
* - Continuous rotation with optional pulse glow
|
|
||||||
*
|
|
||||||
* Zero layout impact — position it absolutely in its parent.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface DrillBitSpinnerProps {
|
interface DrillBitSpinnerProps {
|
||||||
/** Diameter in pixels */
|
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Color of the drill elements */
|
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Rotation duration in seconds */
|
|
||||||
duration?: number;
|
duration?: number;
|
||||||
/** Overall opacity 0–1 */
|
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
/** Whether to show a pulse glow ring */
|
|
||||||
glow?: boolean;
|
glow?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -44,8 +35,6 @@ export function DrillBitSpinner({
|
|||||||
const bladeW = r * 0.18;
|
const bladeW = r * 0.18;
|
||||||
const coreR = r * 0.22;
|
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 blades = [0, 120, 240].map((deg) => {
|
||||||
const rad = (deg * Math.PI) / 180;
|
const rad = (deg * Math.PI) / 180;
|
||||||
return {
|
return {
|
||||||
@@ -58,7 +47,15 @@ export function DrillBitSpinner({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative pointer-events-none select-none ${className}`}
|
className={`relative pointer-events-none select-none ${className}`}
|
||||||
style={{ width: size, height: size, opacity }}
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
opacity,
|
||||||
|
// Radial gradient mask: the spinner fades out toward all edges.
|
||||||
|
// This prevents any hard clip when partially outside the container.
|
||||||
|
WebkitMaskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)',
|
||||||
|
maskImage: 'radial-gradient(circle at center, black 20%, transparent 70%)',
|
||||||
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{/* Glow bloom (pulsing) */}
|
{/* Glow bloom (pulsing) */}
|
||||||
@@ -92,7 +89,6 @@ export function DrillBitSpinner({
|
|||||||
{/* Three cutting blades */}
|
{/* Three cutting blades */}
|
||||||
{blades.map((blade, i) => (
|
{blades.map((blade, i) => (
|
||||||
<g key={i} transform={`rotate(${blade.rotate} ${r} ${r})`}>
|
<g key={i} transform={`rotate(${blade.rotate} ${r} ${r})`}>
|
||||||
{/* Main blade body */}
|
|
||||||
<rect
|
<rect
|
||||||
x={r - bladeW / 2}
|
x={r - bladeW / 2}
|
||||||
y={r - coreR}
|
y={r - coreR}
|
||||||
@@ -101,9 +97,7 @@ export function DrillBitSpinner({
|
|||||||
rx={bladeW / 2}
|
rx={bladeW / 2}
|
||||||
fill={color}
|
fill={color}
|
||||||
opacity={0.7}
|
opacity={0.7}
|
||||||
transform={`rotate(0 ${r} ${r})`}
|
|
||||||
/>
|
/>
|
||||||
{/* Blade tip accent */}
|
|
||||||
<circle
|
<circle
|
||||||
cx={r}
|
cx={r}
|
||||||
cy={r - coreR - bladeLen + bladeW / 2}
|
cy={r - coreR - bladeLen + bladeW / 2}
|
||||||
@@ -122,7 +116,7 @@ export function DrillBitSpinner({
|
|||||||
opacity={0.5}
|
opacity={0.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Center nozzle (mud port) */}
|
{/* Center nozzle */}
|
||||||
<circle cx={r} cy={r} r={coreR * 0.55} fill={color} opacity={0.85} />
|
<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} />
|
<circle cx={r} cy={r} r={coreR * 0.22} fill="white" opacity={0.6} />
|
||||||
</motion.svg>
|
</motion.svg>
|
||||||
|
|||||||
@@ -6,24 +6,23 @@ import { useRef } from 'react';
|
|||||||
/**
|
/**
|
||||||
* DrillOrbit
|
* DrillOrbit
|
||||||
*
|
*
|
||||||
* A glowing point continuously looping along a parabolic arc —
|
* A glowing point looping along a parabolic arc —
|
||||||
* evoking the trajectory of a horizontal directional drill bit.
|
* abstractly evoking a horizontal directional drill path.
|
||||||
*
|
*
|
||||||
* The arc is drawn once on scroll-entry. The dot then loops forever.
|
* The SVG has a horizontal linear-gradient mask: the entire
|
||||||
* Place inside any `relative overflow-hidden` section. Zero layout impact.
|
* 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';
|
type Side = 'left' | 'right';
|
||||||
|
|
||||||
interface DrillOrbitProps {
|
interface DrillOrbitProps {
|
||||||
/** Which side of the section to anchor to */
|
|
||||||
side?: Side;
|
side?: Side;
|
||||||
/** Invert colors for light backgrounds */
|
|
||||||
invert?: boolean;
|
invert?: boolean;
|
||||||
className?: string;
|
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';
|
const PATH = 'M -20 80 C 150 80 200 340 400 340 C 600 340 650 80 820 80';
|
||||||
|
|
||||||
export function DrillOrbit({
|
export function DrillOrbit({
|
||||||
@@ -37,6 +36,18 @@ export function DrillOrbit({
|
|||||||
const color = invert ? '#084c39' : '#0e7a5c';
|
const color = invert ? '#084c39' : '#0e7a5c';
|
||||||
const glowColor = invert ? 'rgba(8,76,57,0.35)' : 'rgba(14,122,92,0.45)';
|
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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -50,11 +61,15 @@ export function DrillOrbit({
|
|||||||
width: 'clamp(360px, 55vw, 800px)',
|
width: 'clamp(360px, 55vw, 800px)',
|
||||||
height: 'clamp(180px, 27vw, 420px)',
|
height: 'clamp(180px, 27vw, 420px)',
|
||||||
transform: side === 'left' ? 'scaleX(-1)' : undefined,
|
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"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
{/* Glow filter for the drill-head */}
|
|
||||||
<filter id="drill-glow" x="-80%" y="-80%" width="260%" height="260%">
|
<filter id="drill-glow" x="-80%" y="-80%" width="260%" height="260%">
|
||||||
<feGaussianBlur stdDeviation="10" result="blur" />
|
<feGaussianBlur stdDeviation="10" result="blur" />
|
||||||
<feMerge>
|
<feMerge>
|
||||||
@@ -62,16 +77,9 @@ export function DrillOrbit({
|
|||||||
<feMergeNode in="SourceGraphic" />
|
<feMergeNode in="SourceGraphic" />
|
||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</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>
|
</defs>
|
||||||
|
|
||||||
{/* ── Bore arc (draws in once, stays) ── */}
|
{/* Bore arc */}
|
||||||
<motion.path
|
<motion.path
|
||||||
d={PATH}
|
d={PATH}
|
||||||
stroke={color}
|
stroke={color}
|
||||||
@@ -83,7 +91,7 @@ export function DrillOrbit({
|
|||||||
transition={{ duration: 2.5, ease: [0.4, 0, 0.2, 1] }}
|
transition={{ duration: 2.5, ease: [0.4, 0, 0.2, 1] }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ── Dashed ground-level reference ── */}
|
{/* Dashed ground-level reference */}
|
||||||
<motion.line
|
<motion.line
|
||||||
x1="0" y1="80" x2="800" y2="80"
|
x1="0" y1="80" x2="800" y2="80"
|
||||||
stroke={color}
|
stroke={color}
|
||||||
@@ -95,7 +103,7 @@ export function DrillOrbit({
|
|||||||
transition={{ duration: 1.6, ease: 'easeOut' }}
|
transition={{ duration: 1.6, ease: 'easeOut' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ── Continuously looping drill-head ── */}
|
{/* Continuously looping drill-head — fades in/out at path ends */}
|
||||||
{isInView && (
|
{isInView && (
|
||||||
<>
|
<>
|
||||||
{/* Outer glow bloom */}
|
{/* Outer glow bloom */}
|
||||||
@@ -104,25 +112,33 @@ export function DrillOrbit({
|
|||||||
fill={glowColor}
|
fill={glowColor}
|
||||||
filter="url(#drill-glow)"
|
filter="url(#drill-glow)"
|
||||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
animate={{
|
||||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
offsetDistance: ['0%', '100%'],
|
||||||
|
opacity: dotOpacityKeyframes,
|
||||||
|
}}
|
||||||
|
transition={{ ...transition, times: dotOpacityTimes }}
|
||||||
/>
|
/>
|
||||||
{/* Core dot */}
|
{/* Core dot */}
|
||||||
<motion.circle
|
<motion.circle
|
||||||
r={5}
|
r={5}
|
||||||
fill={color}
|
fill={color}
|
||||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
animate={{
|
||||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
offsetDistance: ['0%', '100%'],
|
||||||
|
opacity: dotOpacityKeyframes,
|
||||||
|
}}
|
||||||
|
transition={{ ...transition, times: dotOpacityTimes }}
|
||||||
/>
|
/>
|
||||||
{/* Inner white highlight */}
|
{/* Inner white highlight */}
|
||||||
<motion.circle
|
<motion.circle
|
||||||
r={2}
|
r={2}
|
||||||
fill="white"
|
fill="white"
|
||||||
opacity={0.8}
|
|
||||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
||||||
animate={{ offsetDistance: ['0%', '100%'] }}
|
animate={{
|
||||||
transition={{ duration: 5.5, ease: 'easeInOut', repeat: Infinity, repeatDelay: 1.5 }}
|
offsetDistance: ['0%', '100%'],
|
||||||
|
opacity: dotOpacityKeyframes.map((v) => v * 0.8),
|
||||||
|
}}
|
||||||
|
transition={{ ...transition, times: dotOpacityTimes }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
62
components/ui/decorations/DrillTeeth.tsx
Normal file
62
components/ui/decorations/DrillTeeth.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DrillTeeth
|
||||||
|
*
|
||||||
|
* Abstract, minimalist section divider inspired by HDD bore paths.
|
||||||
|
* Not literal teeth — instead, a subtle organic wave that hints at
|
||||||
|
* earth strata displaced by subsurface drilling.
|
||||||
|
*
|
||||||
|
* Two gentle asymmetric curves create a refined, modern transition
|
||||||
|
* between sections. Think Stripe/Linear, not construction site.
|
||||||
|
*
|
||||||
|
* Usage: Place inside a section with `relative overflow-hidden`.
|
||||||
|
* The SVG renders outside the box via translate, creating overlap.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DrillTeethProps {
|
||||||
|
/** Which edge of the parent section */
|
||||||
|
position?: 'top' | 'bottom';
|
||||||
|
/** Fill color — should match the CURRENT section's background */
|
||||||
|
fill?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DrillTeeth({
|
||||||
|
position = 'bottom',
|
||||||
|
fill = 'currentColor',
|
||||||
|
className = '',
|
||||||
|
}: DrillTeethProps) {
|
||||||
|
const isTop = position === 'top';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 right-0 w-full pointer-events-none z-20 ${
|
||||||
|
isTop ? 'top-0 -translate-y-[99%]' : 'bottom-0 translate-y-[99%]'
|
||||||
|
} ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ lineHeight: 0 }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1440 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className="w-full h-[clamp(16px,3vw,48px)] block"
|
||||||
|
style={isTop ? { transform: 'scaleY(-1)' } : undefined}
|
||||||
|
>
|
||||||
|
{/* Primary curve — gentle asymmetric wave */}
|
||||||
|
<path
|
||||||
|
d="M0,48 L0,28 C120,36 240,6 480,12 C720,18 840,42 1080,24 C1200,16 1320,32 1440,28 L1440,48 Z"
|
||||||
|
fill={fill}
|
||||||
|
/>
|
||||||
|
{/* Secondary subtle offset — adds depth like a geological layer */}
|
||||||
|
<path
|
||||||
|
d="M0,48 L0,36 C180,42 360,16 600,22 C840,28 960,44 1200,32 C1320,26 1400,38 1440,36 L1440,48 Z"
|
||||||
|
fill={fill}
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user