feat: animated hero with parallax rocket + scroll reveal animations

- Add animated hero section with parallax rocket SVG that descends on scroll
- Add floating decorative particles and gradient layers in hero
- Add staggered text reveal animation on hero h1
- Create ScrollReveal component (IntersectionObserver-based fade/slide)
- Create AnimatedCounter component for stat numbers
- Add scroll animations to all sections (Problematique, System, Demos, AboutMe, FAQ, Contact, Footer)
- Add smooth FAQ accordion transitions
- Add extensive CSS keyframe animations (float, flame, particles, stat glow)

https://claude.ai/code/session_01V8YAjpqRQ3bfBYsABYsEgo
This commit is contained in:
Claude
2026-02-16 19:09:16 +00:00
parent e94a03f302
commit 6555969c30
13 changed files with 1143 additions and 444 deletions

View File

@@ -0,0 +1,61 @@
"use client";
import { useEffect, useRef, useState } from "react";
interface AnimatedCounterProps {
end: number;
duration?: number;
suffix?: string;
prefix?: string;
className?: string;
}
export default function AnimatedCounter({
end,
duration = 2000,
suffix = "",
prefix = "",
className = "",
}: AnimatedCounterProps) {
const [count, setCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const hasAnimated = useRef(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasAnimated.current) {
hasAnimated.current = true;
const startTime = performance.now();
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3);
setCount(Math.floor(eased * end));
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
},
{ threshold: 0.5 }
);
observer.observe(el);
return () => observer.disconnect();
}, [end, duration]);
return (
<span ref={ref} className={className}>
{prefix}{count}{suffix}
</span>
);
}

View File

@@ -0,0 +1,20 @@
"use client";
export default function FloatingElements() {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{/* Particules flottantes */}
<div className="absolute top-1/4 left-[10%] w-2 h-2 bg-orange/20 rounded-full animate-float-slow" />
<div className="absolute top-1/3 left-[25%] w-3 h-3 bg-white/5 rounded-full animate-float-medium" />
<div className="absolute top-1/2 left-[70%] w-2.5 h-2.5 bg-orange/15 rounded-full animate-float-fast" />
<div className="absolute top-[20%] left-[80%] w-1.5 h-1.5 bg-white/10 rounded-full animate-float-slow" />
<div className="absolute top-[60%] left-[15%] w-2 h-2 bg-orange/10 rounded-full animate-float-medium" />
<div className="absolute top-[70%] left-[50%] w-1 h-1 bg-white/15 rounded-full animate-float-fast" />
{/* Cercles décoratifs */}
<div className="absolute -top-20 -right-20 w-80 h-80 border border-white/5 rounded-full" />
<div className="absolute -bottom-32 -left-32 w-96 h-96 border border-orange/5 rounded-full" />
<div className="absolute top-1/4 right-[30%] w-48 h-48 border border-white/3 rounded-full" />
</div>
);
}

View File

@@ -0,0 +1,122 @@
"use client";
import { useEffect, useState } from "react";
export default function ParallaxRocket() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Rocket descends as user scrolls, with slight rotation
const translateY = scrollY * 0.6;
const rotate = Math.min(scrollY * 0.02, 15);
const opacity = Math.max(1 - scrollY / 1200, 0);
return (
<div
className="absolute right-4 md:right-12 lg:right-24 top-16 md:top-20 z-10 pointer-events-none"
style={{
transform: `translateY(${translateY}px) rotate(${rotate}deg)`,
opacity,
transition: "opacity 0.3s ease",
}}
>
{/* Rocket SVG */}
<svg
width="120"
height="200"
viewBox="0 0 120 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-20 h-32 md:w-28 md:h-48 lg:w-32 lg:h-52 drop-shadow-2xl"
>
{/* Flammes (animées) */}
<g className="animate-flame">
<path
d="M45 160 L60 195 L75 160"
fill="#E8772E"
opacity="0.9"
/>
<path
d="M50 160 L60 185 L70 160"
fill="#F5A623"
opacity="0.8"
/>
<path
d="M54 160 L60 175 L66 160"
fill="#FFD700"
opacity="0.7"
/>
</g>
{/* Corps principal de la fusée */}
<path
d="M40 140 L40 80 Q40 30 60 10 Q80 30 80 80 L80 140 Z"
fill="#1B2A4A"
stroke="#2A3D66"
strokeWidth="1"
/>
{/* Reflet sur le corps */}
<path
d="M50 130 L50 70 Q50 35 60 18 Q62 35 62 70 L62 130 Z"
fill="white"
opacity="0.08"
/>
{/* Fenêtre hublot */}
<circle cx="60" cy="70" r="12" fill="#2A3D66" stroke="#E8772E" strokeWidth="2" />
<circle cx="60" cy="70" r="8" fill="#111D36" />
<circle cx="56" cy="67" r="3" fill="white" opacity="0.3" />
{/* Ailerons gauche */}
<path
d="M40 120 L20 155 L40 145 Z"
fill="#E8772E"
/>
{/* Ailerons droit */}
<path
d="M80 120 L100 155 L80 145 Z"
fill="#E8772E"
/>
{/* Détails - lignes sur le corps */}
<line x1="40" y1="100" x2="80" y2="100" stroke="#2A3D66" strokeWidth="1" opacity="0.5" />
<line x1="40" y1="130" x2="80" y2="130" stroke="#2A3D66" strokeWidth="1" opacity="0.5" />
{/* Nez de la fusée - highlight */}
<path
d="M55 20 Q60 10 65 20"
fill="white"
opacity="0.15"
/>
{/* Logo H sur la fusée */}
<text
x="60"
y="108"
textAnchor="middle"
fill="white"
fontSize="16"
fontWeight="bold"
fontFamily="Inter, sans-serif"
>
H
</text>
</svg>
{/* Particules / traînée */}
<div className="absolute -bottom-4 left-1/2 -translate-x-1/2 flex gap-1">
<div className="w-1.5 h-1.5 bg-orange rounded-full animate-particle-1 opacity-60" />
<div className="w-1 h-1 bg-orange-light rounded-full animate-particle-2 opacity-40" />
<div className="w-1.5 h-1.5 bg-orange rounded-full animate-particle-3 opacity-50" />
</div>
</div>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import { useEffect, useRef, type ReactNode } from "react";
interface ScrollRevealProps {
children: ReactNode;
className?: string;
delay?: number;
direction?: "up" | "down" | "left" | "right" | "none";
duration?: number;
once?: boolean;
}
export default function ScrollReveal({
children,
className = "",
delay = 0,
direction = "up",
duration = 700,
once = true,
}: ScrollRevealProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.style.transitionDelay = `${delay}ms`;
el.classList.add("scroll-revealed");
if (once) observer.unobserve(el);
} else if (!once) {
el.classList.remove("scroll-revealed");
}
},
{ threshold: 0.15, rootMargin: "0px 0px -50px 0px" }
);
observer.observe(el);
return () => observer.disconnect();
}, [delay, once]);
const directionClass = {
up: "scroll-reveal-up",
down: "scroll-reveal-down",
left: "scroll-reveal-left",
right: "scroll-reveal-right",
none: "scroll-reveal-fade",
}[direction];
return (
<div
ref={ref}
className={`${directionClass} ${className}`}
style={{ transitionDuration: `${duration}ms` }}
>
{children}
</div>
);
}