feat: replace literal decorations with AbstractBore and AbstractTrench
Former-commit-id: 4f7c5fcc7203aa4a36aabd8ffc13cf9f28a0f77a
This commit is contained in:
@@ -7,8 +7,8 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Button } from '@/components/ui/Button';
|
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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
|
|
||||||
interface CompetenceBentoGridProps {
|
interface CompetenceBentoGridProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -170,15 +170,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Decorative: large abstract drill bit spinning slowly in background */}
|
{/* Minimalist Abstract Drilling */}
|
||||||
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
|
<AbstractBore className="top-1/4 opacity-40" invert />
|
||||||
<DrillBitSpinner size={600} duration={25} glow={false} />
|
{/* Minimalist Abstract Trenching */}
|
||||||
</div>
|
<AbstractTrench className="bottom-0 opacity-40" invert />
|
||||||
{/* Decorative: abstract earth strata / excavation layers */}
|
|
||||||
<StratumLines placement="bottom-left" invert />
|
|
||||||
<StratumLines placement="bottom-right" invert />
|
|
||||||
{/* Organic wave divider */}
|
|
||||||
<DrillTeeth position="bottom" fill="var(--color-neutral)" />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import Image from 'next/image';
|
|||||||
import Reveal from '@/components/Reveal';
|
import Reveal from '@/components/Reveal';
|
||||||
import { Badge, Container, Heading } from '@/components/ui';
|
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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
|
||||||
|
|
||||||
export interface HeroSectionProps {
|
export interface HeroSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -73,14 +72,10 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
{/* Decorative: large abstract drill bit spinning slowly in background */}
|
{/* Minimalist Abstract Drilling (HDD) */}
|
||||||
<div className="absolute top-1/2 -translate-y-1/2 -right-48 md:-right-64 pointer-events-none opacity-[0.03]">
|
<AbstractBore className="top-1/3 -translate-y-1/2 opacity-60" />
|
||||||
<DrillBitSpinner size={800} duration={20} glow={false} />
|
{/* Minimalist Abstract Trenching */}
|
||||||
</div>
|
<AbstractTrench className="bottom-0 opacity-60" />
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
|
||||||
<DrillOrbit side="right" />
|
|
||||||
{/* Organic wave divider — displaced earth strata */}
|
|
||||||
<DrillTeeth position="bottom" fill="var(--color-primary-dark)" />
|
|
||||||
</section>
|
</section>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +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';
|
import { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
|
|
||||||
interface HeroVideoProps {
|
interface HeroVideoProps {
|
||||||
data?: any;
|
data?: any;
|
||||||
@@ -109,8 +109,8 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
{/* Organic wave divider — displaced earth strata */}
|
{/* Minimalist Abstract Drilling */}
|
||||||
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" className="z-[5]" />
|
<AbstractBore className="bottom-0 opacity-40 z-[5]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { motion } from 'framer-motion';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
|
||||||
|
|
||||||
export interface Reference {
|
export interface Reference {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -154,14 +153,10 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{/* Decorative: large abstract drill bit spinning slowly in background */}
|
{/* Minimalist Abstract Drilling */}
|
||||||
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
|
<AbstractBore className="top-1/4 opacity-30" />
|
||||||
<DrillBitSpinner size={600} duration={25} glow={false} />
|
{/* Minimalist Abstract Trenching */}
|
||||||
</div>
|
<AbstractTrench className="bottom-0 opacity-30" />
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
|
||||||
<DrillOrbit side="left" />
|
|
||||||
{/* Organic wave divider */}
|
|
||||||
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +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';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
|
|
||||||
export interface ServicePanelData {
|
export interface ServicePanelData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -230,8 +230,8 @@ export function ServiceDetailGrid({
|
|||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
{/* Organic wave divider */}
|
{/* Minimalist Abstract Trenching */}
|
||||||
<DrillTeeth position="bottom" fill="#fafafa" />
|
<AbstractTrench className="bottom-0 opacity-20" invert />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import * as React from 'react';
|
|||||||
import { motion, Variants } from 'framer-motion';
|
import { motion, Variants } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
|
||||||
|
|
||||||
interface SubCompanyTilesProps {
|
interface SubCompanyTilesProps {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
@@ -200,14 +199,10 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
|||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
{/* Decorative: large abstract drill bit spinning slowly in background */}
|
{/* Minimalist Abstract Drilling */}
|
||||||
<div className="absolute top-1/3 -translate-y-1/2 -left-64 pointer-events-none opacity-[0.04]">
|
<AbstractBore className="top-1/4 opacity-40" />
|
||||||
<DrillBitSpinner size={700} duration={25} glow={false} />
|
{/* Minimalist Abstract Trenching */}
|
||||||
</div>
|
<AbstractTrench className="bottom-0 opacity-40" />
|
||||||
{/* Decorative: abstract HDD bore trajectory */}
|
|
||||||
<DrillOrbit side="right" />
|
|
||||||
{/* Organic wave divider */}
|
|
||||||
<DrillTeeth position="bottom" fill="var(--color-neutral-dark)" />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useLocale } from 'next-intl';
|
|||||||
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
|
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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||||
import { DrillTeeth } from '@/components/ui/decorations/DrillTeeth';
|
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||||
|
|
||||||
interface CompanyInfo {
|
interface CompanyInfo {
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
@@ -22,8 +22,6 @@ 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" />
|
||||||
|
|
||||||
@@ -155,11 +153,11 @@ export function Footer({ companyInfo }: FooterProps) {
|
|||||||
<div className="flex items-center gap-2"><Zap className="w-4 h-4 text-white/40" /><span>High Performance</span></div>
|
<div className="flex items-center gap-2"><Zap className="w-4 h-4 text-white/40" /><span>High Performance</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Decorative: large abstract drill bit spinning slowly in background */}
|
{/* Minimalist Abstract Drilling */}
|
||||||
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
|
<AbstractBore className="top-0 opacity-20" invert />
|
||||||
<DrillBitSpinner size={600} duration={30} glow={false} />
|
{/* Minimalist Abstract Trenching */}
|
||||||
</div>
|
<AbstractTrench className="bottom-0 opacity-20" invert />
|
||||||
{/* Decorative: abstract earth strata */}
|
{/* Abstract earth strata */}
|
||||||
<StratumLines placement="bottom-right" />
|
<StratumLines placement="bottom-right" />
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import React from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { useTransition } from './TransitionProvider';
|
import { useTransition } from './TransitionProvider';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
|
|
||||||
|
|
||||||
export function PageTransitionShutter() {
|
export function PageTransitionShutter() {
|
||||||
const { isTransitioning, transitionMessage } = useTransition();
|
const { isTransitioning, transitionMessage } = useTransition();
|
||||||
@@ -159,28 +158,19 @@ export function PageTransitionShutter() {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* ── DrillBit loading indicator (replaces three dots) ── */}
|
{/* ── Abstract horizontal energy pulse (Loader) ── */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mt-8 relative flex items-center justify-center"
|
className="mt-12 relative flex items-center justify-center w-48 h-[2px] bg-white/10 overflow-hidden rounded-full"
|
||||||
initial={{ opacity: 0, scale: 0.5 }}
|
initial={{ opacity: 0, scaleX: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scaleX: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.5 }}
|
exit={{ opacity: 0, scaleX: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||||
>
|
>
|
||||||
{/* Outer pulsing ring */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute rounded-full border border-primary/30"
|
className="absolute left-0 top-0 bottom-0 bg-primary w-1/3 rounded-full blur-[1px]"
|
||||||
animate={{ scale: [1, 1.8, 1], opacity: [0.5, 0, 0.5] }}
|
animate={{ x: ['-100%', '300%'] }}
|
||||||
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut' }}
|
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||||
style={{ width: 120, height: 120 }}
|
|
||||||
/>
|
/>
|
||||||
<motion.div
|
|
||||||
className="absolute rounded-full border border-primary/15"
|
|
||||||
animate={{ scale: [1, 2.4, 1], opacity: [0.3, 0, 0.3] }}
|
|
||||||
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut', delay: 0.4 }}
|
|
||||||
style={{ width: 120, height: 120 }}
|
|
||||||
/>
|
|
||||||
<DrillBitSpinner size={80} duration={3} glow />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Background bore arc (full width, bottom) */}
|
{/* Background bore arc (full width, bottom) */}
|
||||||
|
|||||||
71
components/ui/decorations/AbstractBore.tsx
Normal file
71
components/ui/decorations/AbstractBore.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface AbstractBoreProps {
|
||||||
|
className?: string;
|
||||||
|
invert?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractBore
|
||||||
|
*
|
||||||
|
* Visualizes Horizontal Directional Drilling (HDD).
|
||||||
|
* A smooth, parabolic underground arc. A beam of energy (the drill/cable)
|
||||||
|
* travels along this path, fading in gently at the start and fading out at the end.
|
||||||
|
* Purely abstract, no literal drill bits or circles.
|
||||||
|
*/
|
||||||
|
export function AbstractBore({ className = '', invert = false }: AbstractBoreProps) {
|
||||||
|
const color = invert ? 'rgba(10, 61, 40, 0.6)' : 'rgba(14, 122, 92, 0.6)';
|
||||||
|
const glowColor = invert ? 'rgba(10, 61, 40, 1)' : 'rgba(20, 184, 138, 1)';
|
||||||
|
|
||||||
|
// Parabolic path: Starts left, dips deep in the middle, comes up right
|
||||||
|
const pathD = "M -100 50 C 200 50 300 250 500 250 C 700 250 800 50 1100 50";
|
||||||
|
// The length of this cubic bezier is approx 1260
|
||||||
|
const pathLength = 1260;
|
||||||
|
// The length of the moving beam
|
||||||
|
const beamLength = 200;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1000 300"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className={`absolute pointer-events-none select-none w-full ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Subtle track background */}
|
||||||
|
<path
|
||||||
|
d={pathD}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
opacity={0.15}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Moving Energy Beam */}
|
||||||
|
<motion.path
|
||||||
|
d={pathD}
|
||||||
|
stroke={glowColor}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
// Dasharray: beam length, then gap for the rest of the path
|
||||||
|
strokeDasharray={`${beamLength} ${pathLength}`}
|
||||||
|
animate={{
|
||||||
|
// Move from right off-screen to left off-screen (or vice versa)
|
||||||
|
// Starting at pathLength pushes the dash off the start of the curve.
|
||||||
|
// Ending at -beamLength pushes it completely off the end.
|
||||||
|
strokeDashoffset: [pathLength, -beamLength],
|
||||||
|
// Fade in during the first 20%, stay solid, fade out during last 20%
|
||||||
|
opacity: [0, 1, 1, 0]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 6,
|
||||||
|
ease: "linear",
|
||||||
|
repeat: Infinity,
|
||||||
|
times: [0, 0.15, 0.85, 1] // Syncs with the offset to fade exactly at edges
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
components/ui/decorations/AbstractTrench.tsx
Normal file
70
components/ui/decorations/AbstractTrench.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface AbstractTrenchProps {
|
||||||
|
className?: string;
|
||||||
|
invert?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractTrench
|
||||||
|
*
|
||||||
|
* Visualizes open trench construction (Offene Bauweise / Graben).
|
||||||
|
* A sharp, geometric profile representing a cut into the earth.
|
||||||
|
* A beam of energy flows through the trench, hugging the sharp corners,
|
||||||
|
* fading in and out at the horizontal edges.
|
||||||
|
* Purely abstract, architectural styling.
|
||||||
|
*/
|
||||||
|
export function AbstractTrench({ className = '', invert = false }: AbstractTrenchProps) {
|
||||||
|
const color = invert ? 'rgba(10, 61, 40, 0.6)' : 'rgba(14, 122, 92, 0.6)';
|
||||||
|
const glowColor = invert ? 'rgba(10, 61, 40, 1)' : 'rgba(20, 184, 138, 1)';
|
||||||
|
|
||||||
|
// Geometric trench path: straight, 45-deg drop, straight bottom, 45-deg rise, straight
|
||||||
|
const pathD = "M -100 50 L 300 50 L 350 200 L 650 200 L 700 50 L 1100 50";
|
||||||
|
// Length of this polyline is approx 1260
|
||||||
|
const pathLength = 1260;
|
||||||
|
const beamLength = 250;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1000 300"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className={`absolute pointer-events-none select-none w-full ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Subtle track background */}
|
||||||
|
<path
|
||||||
|
d={pathD}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
opacity={0.15}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Moving Energy Beam */}
|
||||||
|
<motion.path
|
||||||
|
d={pathD}
|
||||||
|
stroke={glowColor}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
// Dasharray: beam length, then gap for the rest of the path
|
||||||
|
strokeDasharray={`${beamLength} ${pathLength}`}
|
||||||
|
animate={{
|
||||||
|
strokeDashoffset: [pathLength, -beamLength],
|
||||||
|
opacity: [0, 1, 1, 0]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 7.5,
|
||||||
|
ease: "linear",
|
||||||
|
repeat: Infinity,
|
||||||
|
times: [0, 0.15, 0.85, 1], // Syncs with the offset to fade exactly at edges
|
||||||
|
delay: 1.5 // Offset from AbstractBore if used together
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
'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.
|
|
||||||
*
|
|
||||||
* The wrapping div uses a radial-gradient mask so it fades naturally
|
|
||||||
* into whatever background it's placed on — no hard edge clipping.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface DrillBitSpinnerProps {
|
|
||||||
size?: number;
|
|
||||||
color?: string;
|
|
||||||
duration?: number;
|
|
||||||
opacity?: number;
|
|
||||||
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;
|
|
||||||
|
|
||||||
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,
|
|
||||||
// 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"
|
|
||||||
>
|
|
||||||
{/* 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})`}>
|
|
||||||
<rect
|
|
||||||
x={r - bladeW / 2}
|
|
||||||
y={r - coreR}
|
|
||||||
width={bladeW}
|
|
||||||
height={bladeLen}
|
|
||||||
rx={bladeW / 2}
|
|
||||||
fill={color}
|
|
||||||
opacity={0.7}
|
|
||||||
/>
|
|
||||||
<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 */}
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion, useInView } from 'framer-motion';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DrillOrbit
|
|
||||||
*
|
|
||||||
* A glowing point looping along a parabolic arc —
|
|
||||||
* abstractly evoking a horizontal directional drill path.
|
|
||||||
*
|
|
||||||
* 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 {
|
|
||||||
side?: Side;
|
|
||||||
invert?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)';
|
|
||||||
|
|
||||||
// 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}
|
|
||||||
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,
|
|
||||||
// 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>
|
|
||||||
<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>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
{/* Bore arc */}
|
|
||||||
<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 — fades in/out at path ends */}
|
|
||||||
{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%'],
|
|
||||||
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%'],
|
|
||||||
opacity: dotOpacityKeyframes,
|
|
||||||
}}
|
|
||||||
transition={{ ...transition, times: dotOpacityTimes }}
|
|
||||||
/>
|
|
||||||
{/* Inner white highlight */}
|
|
||||||
<motion.circle
|
|
||||||
r={2}
|
|
||||||
fill="white"
|
|
||||||
style={{ offsetPath: `path('${PATH}')`, offsetDistance: '0%' } as React.CSSProperties}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ['0%', '100%'],
|
|
||||||
opacity: dotOpacityKeyframes.map((v) => v * 0.8),
|
|
||||||
}}
|
|
||||||
transition={{ ...transition, times: dotOpacityTimes }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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