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:
61
components/animations/AnimatedCounter.tsx
Normal file
61
components/animations/AnimatedCounter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
components/animations/FloatingElements.tsx
Normal file
20
components/animations/FloatingElements.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
components/animations/ParallaxRocket.tsx
Normal file
122
components/animations/ParallaxRocket.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
components/animations/ScrollReveal.tsx
Normal file
62
components/animations/ScrollReveal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user