feat: distribute animated DrillBitSpinner across all major components as a background watermark

Former-commit-id: ad28a5d93b773be795cd0d73fd6ed2accd257a10
This commit is contained in:
2026-05-11 11:00:07 +02:00
parent 8fc4cadd19
commit c97ee638d9
7 changed files with 180 additions and 29 deletions

View File

@@ -7,6 +7,7 @@ import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { StratumLines } from '@/components/ui/decorations/StratumLines';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
interface CompetenceBentoGridProps {
badge?: string;
@@ -168,6 +169,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
})}
</div>
</div>
{/* Decorative: large abstract drill bit spinning slowly in background */}
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
<DrillBitSpinner size={600} duration={25} glow={false} />
</div>
{/* Decorative: abstract earth strata / excavation layers */}
<StratumLines placement="bottom-left" invert />
<StratumLines placement="bottom-right" invert />

View File

@@ -4,6 +4,7 @@ import Reveal from '@/components/Reveal';
import { Badge, Container, Heading } from '@/components/ui';
import { Button } from '@/components/ui/Button';
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
export interface HeroSectionProps {
title: string;
@@ -71,6 +72,10 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
)}
</div>
</Container>
{/* Decorative: large abstract drill bit spinning slowly in background */}
<div className="absolute top-1/2 -translate-y-1/2 -right-48 md:-right-64 pointer-events-none opacity-[0.03]">
<DrillBitSpinner size={800} duration={20} glow={false} />
</div>
{/* Decorative: abstract HDD bore trajectory */}
<DrillOrbit side="right" />
</section>

View File

@@ -6,6 +6,7 @@ import Link from 'next/link';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
export interface Reference {
id: string;
@@ -152,6 +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>
</Link>
</div>
{/* Decorative: large abstract drill bit spinning slowly in background */}
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
<DrillBitSpinner size={600} duration={25} glow={false} />
</div>
{/* Decorative: abstract HDD bore trajectory */}
<DrillOrbit side="left" />
</section>

View File

@@ -5,6 +5,7 @@ import { motion, Variants } from 'framer-motion';
import Image from 'next/image';
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
import { DrillOrbit } from '@/components/ui/decorations/DrillOrbit';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
interface SubCompanyTilesProps {
badge?: string;
@@ -198,6 +199,10 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
})}
</motion.div>
</div>
{/* Decorative: large abstract drill bit spinning slowly in background */}
<div className="absolute top-1/3 -translate-y-1/2 -left-64 pointer-events-none opacity-[0.04]">
<DrillBitSpinner size={700} duration={25} glow={false} />
</div>
{/* Decorative: abstract HDD bore trajectory */}
<DrillOrbit side="right" />
</section>

View File

@@ -4,6 +4,7 @@ import { useLocale } from 'next-intl';
import { ShieldCheck, Leaf, Lock, Accessibility, Zap } from 'lucide-react';
import { LanguageSwitcher } from './LanguageSwitcher';
import { StratumLines } from '@/components/ui/decorations/StratumLines';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
interface CompanyInfo {
contactEmail: string;
@@ -151,6 +152,10 @@ 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>
</div>
{/* Decorative: large abstract drill bit spinning slowly in background */}
<div className="absolute top-1/2 -translate-y-1/2 -right-48 pointer-events-none opacity-[0.03]">
<DrillBitSpinner size={600} duration={30} glow={false} />
</div>
{/* Decorative: abstract earth strata */}
<StratumLines placement="bottom-right" />
</footer>

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useTransition } from './TransitionProvider';
import Image from 'next/image';
import { DrillBitSpinner } from '@/components/ui/decorations/DrillBitSpinner';
export function PageTransitionShutter() {
const { isTransitioning, transitionMessage } = useTransition();
@@ -144,35 +145,36 @@ export function PageTransitionShutter() {
)}
</AnimatePresence>
{/* Extremely minimalist transition indicator */}
<motion.div
className={`${transitionMessage ? 'mt-4' : 'mt-6'} flex gap-1`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
{/* ── DrillBit loading indicator (replaces three dots) ── */}
<motion.div
className="mt-8 relative flex items-center justify-center"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.5, delay: 0.3, ease: [0.22, 1, 0.36, 1] }}
>
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="w-1.5 h-1.5 rounded-full bg-primary"
initial={{ opacity: 0.3, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.4,
repeat: Infinity,
repeatType: "reverse",
delay: i * 0.15
}}
/>
))}
{/* Outer pulsing ring */}
<motion.div
className="absolute rounded-full border border-primary/30"
animate={{ scale: [1, 1.8, 1], opacity: [0.5, 0, 0.5] }}
transition={{ duration: 2.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>
{/* Abstract HDD bore arc — decorative element during page transitions */}
{/* Background bore arc (full width, bottom) */}
<svg
viewBox="0 0 700 220"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute bottom-0 left-0 w-full h-32 sm:h-44 opacity-[0.07] pointer-events-none"
className="absolute bottom-0 left-0 w-full h-36 sm:h-52 opacity-[0.10] pointer-events-none"
>
<motion.path
d="M -50 60 C 120 60 180 180 350 180 C 520 180 580 60 750 60"
@@ -190,13 +192,6 @@ export function PageTransitionShutter() {
animate={{ pathLength: 1 }}
transition={{ duration: 1.2, ease: 'easeOut' }}
/>
<motion.circle
r={7}
fill="#0e7a5c"
style={{ offsetPath: "path('M -50 60 C 120 60 180 180 350 180 C 520 180 580 60 750 60')", offsetDistance: '0%' } as React.CSSProperties}
animate={{ offsetDistance: ['0%', '100%'] }}
transition={{ duration: 1.8, ease: 'easeInOut', delay: 0.4, repeat: Infinity, repeatDelay: 0.6 }}
/>
</svg>
</motion.div>
</motion.div>

View File

@@ -0,0 +1,131 @@
'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 01 */
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>
);
}