feat: content-aware HDDBore and TrenchLaying animations with drillbit transition
Former-commit-id: 1a541fc31c5b24aa083c3441a9ab642c7d49da59
This commit is contained in:
@@ -7,8 +7,8 @@ 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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
interface CompetenceBentoGridProps {
|
||||
badge?: string;
|
||||
@@ -170,10 +170,10 @@ export function CompetenceBentoGrid(props: CompetenceBentoGridProps) {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Minimalist Abstract Drilling */}
|
||||
<AbstractBore className="top-1/4 opacity-40" invert />
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-40" invert />
|
||||
{/* Content-Aware Animation: HDD Drilling */}
|
||||
<HDDBoreAnimation className="top-1/4 opacity-40" invert />
|
||||
{/* Content-Aware Animation: Trenching */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-40" invert />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import Image from 'next/image';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import { Badge, Container, Heading } from '@/components/ui';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
export interface HeroSectionProps {
|
||||
title: string;
|
||||
@@ -72,10 +72,10 @@ export const HeroSection: React.FC<HeroSectionProps> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
{/* Minimalist Abstract Drilling (HDD) */}
|
||||
<AbstractBore className="top-1/3 -translate-y-1/2 opacity-60" />
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-60" />
|
||||
{/* Content-Aware Animation: HDD Drilling & Pipe Pullback */}
|
||||
<HDDBoreAnimation className="top-1/3 -translate-y-1/2 opacity-60" />
|
||||
{/* Content-Aware Animation: Trench Cutting & Pipe Laying */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-60" />
|
||||
</section>
|
||||
</Reveal>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import Image from 'next/image';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
|
||||
interface HeroVideoProps {
|
||||
data?: any;
|
||||
@@ -109,8 +109,8 @@ export function HeroVideo(props: HeroVideoProps) {
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
{/* Minimalist Abstract Drilling */}
|
||||
<AbstractBore className="bottom-0 opacity-40 z-[5]" />
|
||||
{/* Content-Aware Animation: HDD Drilling */}
|
||||
<HDDBoreAnimation className="bottom-0 opacity-40 z-[5]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
export interface Reference {
|
||||
id: string;
|
||||
@@ -153,10 +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>
|
||||
{/* Minimalist Abstract Drilling */}
|
||||
<AbstractBore className="top-1/4 opacity-30" />
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-30" />
|
||||
{/* Content-Aware Animation: HDD Drilling */}
|
||||
<HDDBoreAnimation className="top-1/4 opacity-30" />
|
||||
{/* Content-Aware Animation: Trenching */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-30" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
export interface ServicePanelData {
|
||||
id: string;
|
||||
@@ -230,8 +230,8 @@ export function ServiceDetailGrid({
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-20" invert />
|
||||
{/* Content-Aware Animation: Trenching */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-20" invert />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import * as React from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
||||
import { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
interface SubCompanyTilesProps {
|
||||
badge?: string;
|
||||
@@ -199,10 +199,10 @@ export function SubCompanyTiles(props: SubCompanyTilesProps) {
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
{/* Minimalist Abstract Drilling */}
|
||||
<AbstractBore className="top-1/4 opacity-40" />
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-40" />
|
||||
{/* Content-Aware Animation: HDD Drilling */}
|
||||
<HDDBoreAnimation className="top-1/4 opacity-40" />
|
||||
{/* Content-Aware Animation: Trenching */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-40" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ 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 { AbstractBore } from '@/components/ui/decorations/AbstractBore';
|
||||
import { AbstractTrench } from '@/components/ui/decorations/AbstractTrench';
|
||||
import { HDDBoreAnimation } from '@/components/ui/decorations/HDDBoreAnimation';
|
||||
import { TrenchLayingAnimation } from '@/components/ui/decorations/TrenchLayingAnimation';
|
||||
|
||||
interface CompanyInfo {
|
||||
contactEmail: string;
|
||||
@@ -153,10 +153,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>
|
||||
{/* Minimalist Abstract Drilling */}
|
||||
<AbstractBore className="top-0 opacity-20" invert />
|
||||
{/* Minimalist Abstract Trenching */}
|
||||
<AbstractTrench className="bottom-0 opacity-20" invert />
|
||||
{/* Content-Aware Animation: HDD Drilling */}
|
||||
<HDDBoreAnimation className="top-0 opacity-20" invert />
|
||||
{/* Content-Aware Animation: Trenching */}
|
||||
<TrenchLayingAnimation className="bottom-0 opacity-20" invert />
|
||||
{/* Abstract earth strata */}
|
||||
<StratumLines placement="bottom-right" />
|
||||
</footer>
|
||||
|
||||
@@ -13,25 +13,19 @@ export function PageTransitionShutter() {
|
||||
{isTransitioning && (
|
||||
<motion.div
|
||||
key="shutter"
|
||||
initial={{ y: '100%' }}
|
||||
initial={{ y: '-100%' }}
|
||||
animate={{ y: '0%' }}
|
||||
exit={{ y: '-100%' }}
|
||||
exit={{ y: '100%' }}
|
||||
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-visible"
|
||||
style={{
|
||||
height: '110vh',
|
||||
top: '-5vh',
|
||||
height: '100vh',
|
||||
top: '0',
|
||||
}}
|
||||
>
|
||||
{/* 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" />
|
||||
{/* Bottom massive Drill Bit Teeth edge (leading edge cutting down) */}
|
||||
<svg viewBox="0 0 1000 100" preserveAspectRatio="none" className="absolute bottom-0 translate-y-full left-0 w-full h-[clamp(40px,8vw,120px)] text-[#050B14]">
|
||||
<path d="M0,0 L1000,0 L1000,10 L960,100 L920,10 L880,100 L840,10 L800,100 L760,10 L720,100 L680,10 L640,100 L600,10 L560,100 L520,10 L480,100 L440,10 L400,100 L360,10 L320,100 L280,10 L240,100 L200,10 L160,100 L120,10 L80,100 L40,10 L0,100 Z" fill="currentColor" />
|
||||
</svg>
|
||||
{/* 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%)]" />
|
||||
@@ -173,30 +167,6 @@ export function PageTransitionShutter() {
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* 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-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"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.6, ease: [0.4, 0, 0.2, 1], delay: 0.2 }}
|
||||
/>
|
||||
<motion.line
|
||||
x1="0" y1="60" x2="700" y2="60"
|
||||
stroke="white" strokeWidth="0.8" strokeDasharray="8 12"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.2, ease: 'easeOut' }}
|
||||
/>
|
||||
</svg>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
93
components/ui/decorations/HDDBoreAnimation.tsx
Normal file
93
components/ui/decorations/HDDBoreAnimation.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface HDDBoreAnimationProps {
|
||||
className?: string;
|
||||
invert?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* HDDBoreAnimation
|
||||
*
|
||||
* Visualizes Horizontal Directional Drilling (Spülbohren).
|
||||
* 1. A segmented drill rod (Gestänge) pushes through a subterranean arc (Pilotbohrung).
|
||||
* 2. It pulls back a thick, solid orange pipe (Rohreinzug).
|
||||
*
|
||||
* This directly ties to E-TIB's core services, visually representing the
|
||||
* physical act of laying a conduit pipe under an obstacle.
|
||||
*/
|
||||
export function HDDBoreAnimation({ className = '', invert = false }: HDDBoreAnimationProps) {
|
||||
// Track color
|
||||
const color = invert ? '#0a3d28' : '#0e7a5c';
|
||||
// Bright orange for the conduit pipe (Kabelschutzrohr)
|
||||
const pipeColor = '#ff6b00';
|
||||
|
||||
// Parabolic path under an implied obstacle
|
||||
const pathD = "M 50 100 C 300 100 400 250 600 250 C 800 250 900 100 1150 100";
|
||||
// Length of this cubic bezier is approx 1300
|
||||
const pathLen = 1300;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1200 300"
|
||||
preserveAspectRatio="none"
|
||||
className={`absolute pointer-events-none select-none w-full ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Planned path (subtle) */}
|
||||
<path
|
||||
d={pathD}
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5 15"
|
||||
fill="none"
|
||||
opacity={0.15}
|
||||
/>
|
||||
|
||||
{/* Step 1: Drill Rod (Pilotbohrung) pushing Left -> Right */}
|
||||
<motion.path
|
||||
d={pathD}
|
||||
stroke={color}
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
// Segmented rod look (40px rod, 5px gap)
|
||||
strokeDasharray={`40 5 ${pathLen}`}
|
||||
animate={{
|
||||
strokeDashoffset: [pathLen, 0, 0, 0, pathLen],
|
||||
opacity: [0, 1, 1, 0, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.4, 0.45, 0.46, 1] // 0-4s: push, 4-4.5s: pause, 4.6s: vanish
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Step 2: Cable/Pipe Pullback (Rohreinzug) Right -> Left */}
|
||||
<motion.path
|
||||
d={pathD}
|
||||
stroke={pipeColor}
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
// Solid pipe
|
||||
strokeDasharray={`${pathLen} ${pathLen}`}
|
||||
animate={{
|
||||
// Starts hidden at the right (offset = -pathLen)
|
||||
// Pulls back to left (offset = 0)
|
||||
strokeDashoffset: [-pathLen, -pathLen, 0, 0, -pathLen],
|
||||
opacity: [0, 0, 1, 1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.45, 0.85, 0.95, 1] // 0-4.5s: hidden, 4.5-8.5s: pull back, 8.5-9.5s: rest, 10s: reset
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
100
components/ui/decorations/TrenchLayingAnimation.tsx
Normal file
100
components/ui/decorations/TrenchLayingAnimation.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface TrenchLayingAnimationProps {
|
||||
className?: string;
|
||||
invert?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TrenchLayingAnimation
|
||||
*
|
||||
* Visualizes open trench construction (Offene Bauweise / Graben).
|
||||
* 1. A trench is cut into the earth line.
|
||||
* 2. A thick orange conduit pipe (Leerrohr) is laid into the trench.
|
||||
* 3. The trench is backfilled with earth.
|
||||
*
|
||||
* This directly ties to E-TIB's core services, showing the process
|
||||
* of laying cables rather than just abstract geometry.
|
||||
*/
|
||||
export function TrenchLayingAnimation({ className = '', invert = false }: TrenchLayingAnimationProps) {
|
||||
const color = invert ? '#0a3d28' : '#0e7a5c';
|
||||
const earthColor = invert ? 'rgba(10, 61, 40, 0.4)' : 'rgba(14, 122, 92, 0.4)';
|
||||
const pipeColor = '#ff6b00';
|
||||
|
||||
// The trench profile: flat, 45-deg drop, flat bottom, 45-deg rise, flat
|
||||
const trenchPath = "M 50 100 L 300 100 L 350 200 L 850 200 L 900 100 L 1150 100";
|
||||
// The dug out area (used for the backfill animation)
|
||||
const trenchFill = "M 300 100 L 350 200 L 850 200 L 900 100 Z";
|
||||
// Length of the polyline is approx 1300
|
||||
const pathLen = 1300;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 1200 300"
|
||||
preserveAspectRatio="none"
|
||||
className={`absolute pointer-events-none select-none w-full ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Ground surface line (always visible outside the trench) */}
|
||||
<line x1="0" y1="100" x2="1200" y2="100" stroke={color} strokeWidth="2" opacity={0.3} />
|
||||
|
||||
{/* Step 1: Trench cutting Left -> Right */}
|
||||
<motion.path
|
||||
d={trenchPath}
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray={`${pathLen} ${pathLen}`}
|
||||
animate={{
|
||||
strokeDashoffset: [pathLen, 0, 0, 0, pathLen]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
times: [0, 0.25, 0.8, 0.9, 1], // Cuts quickly (0-2.5s), stays open, closes at 9s
|
||||
ease: "linear"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Step 2: Pipe Laying (Drops into trench closely behind the cut) */}
|
||||
<motion.path
|
||||
d={trenchPath}
|
||||
stroke={pipeColor}
|
||||
strokeWidth="6"
|
||||
fill="none"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${pathLen} ${pathLen}`}
|
||||
animate={{
|
||||
strokeDashoffset: [pathLen, pathLen, 0, 0, pathLen],
|
||||
opacity: [0, 1, 1, 0, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
times: [0, 0.1, 0.4, 0.8, 1], // Starts unspooling at 1s, finishes laying at 4s
|
||||
ease: "linear"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Step 3: Sand/Earth backfill (Fades in over the pipe) */}
|
||||
<motion.path
|
||||
d={trenchFill}
|
||||
fill={earthColor}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, 0, 1, 1, 0]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
times: [0, 0.4, 0.5, 0.8, 1], // Fades in between 4s and 5s, covers the pipe
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user