diff --git a/assets/antananarivo-bg.jpg b/assets/antananarivo-bg.jpg new file mode 100644 index 0000000..0c49f52 Binary files /dev/null and b/assets/antananarivo-bg.jpg differ diff --git a/css/parallax.css b/css/parallax.css index 20970b1..6726f1f 100644 --- a/css/parallax.css +++ b/css/parallax.css @@ -1,7 +1,8 @@ /* ========================================================================= PARALLAX INTRO — MVA Global Fret - Page scrollable (4 viewports) avec scène Three.js fixée. La timeline - pilote la caméra et l'avion 3D. À 100% du scroll, le bouton CTA apparaît. + Page unique. Photo aérienne d'Antananarivo en fond + avion 3D piloté + par la souris (entre par le haut-gauche, sort par la droite). Bouton + CTA doré centré, toujours visible. ========================================================================= */ :root { @@ -22,12 +23,10 @@ html, body { background: var(--navy-deep); } -html { scroll-behavior: smooth; } - -body.parallax-body { +.parallax-body { font-family: 'Inter', sans-serif; color: var(--white); - overflow-x: hidden; + overflow: hidden; } /* ── HEADER ─────────────────────────────────────────────────────────────── */ @@ -86,16 +85,12 @@ body.parallax-body { } .lang-switcher button:hover:not(.active) { color: var(--white); } -/* ── SCÈNE FIXE (vidéo + voile + canvas Three.js) ──────────────────────── - .stage-fixed reste plein-écran pendant que la page se déroule derrière. -*/ -.stage-fixed { - position: fixed; - inset: 0; +/* ── STAGE ──────────────────────────────────────────────────────────────── */ +.stage { + position: relative; width: 100vw; height: 100vh; overflow: hidden; - z-index: 1; } .layer { @@ -107,90 +102,20 @@ body.parallax-body { will-change: transform; } -.layer-video { +/* Photo de fond — léger zoom + parallaxe souris discrète */ +.layer-bg { object-fit: cover; object-position: center; - transform: translate(calc(var(--mx) * -22px), calc(var(--my) * -22px)) scale(1.06); + transform: translate(calc(var(--mx) * -16px), calc(var(--my) * -10px)) scale(1.05); } .layer-tint { background: - radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.55) 100%), - linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%); + radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.32) 100%), + linear-gradient(180deg, transparent 55%, rgba(5, 5, 24, 0.40) 100%); } -.layer-three { - display: block; - z-index: 2; -} - -/* ── SCROLL STAGE — sentinelles invisibles qui donnent la hauteur ──────── */ -.scroll-stage { - position: relative; - z-index: 5; - pointer-events: none; -} -.scroll-stage .act { - height: 100vh; -} - -/* ── ÉTIQUETTES D'ACTE ──────────────────────────────────────────────────── */ -.act-label { - position: absolute; - left: 50%; - bottom: 18%; - transform: translateX(-50%); - z-index: 30; - pointer-events: none; - text-align: center; - font-family: 'Poppins', sans-serif; - font-weight: 700; - letter-spacing: 4px; - text-transform: uppercase; - font-size: 1.05rem; - color: var(--white); - text-shadow: 0 4px 18px rgba(0, 0, 0, 0.7); - opacity: 0; -} -.act-label span { - display: inline-block; - padding: 14px 28px; - background: rgba(5, 5, 24, 0.45); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 8px; - line-height: 1.35; -} - -/* ── INDICATEUR SCROLL ─────────────────────────────────────────────────── */ -.scroll-hint { - position: absolute; - left: 50%; - bottom: 36px; - transform: translateX(-50%); - z-index: 40; - display: inline-flex; - flex-direction: column; - align-items: center; - gap: 8px; - font-family: 'Poppins', sans-serif; - font-weight: 600; - font-size: 0.78rem; - letter-spacing: 2px; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.85); - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); - animation: scrollBob 1.8s ease-in-out infinite; - transition: opacity 0.5s ease; -} -.scroll-hint.hidden { opacity: 0; pointer-events: none; } -.scroll-hint i { font-size: 1rem; } - -@keyframes scrollBob { - 0%, 100% { transform: translate(-50%, 0); } - 50% { transform: translate(-50%, 8px); } -} +.layer-three { display: block; z-index: 2; } /* ── BOUTON CTA centré ──────────────────────────────────────────────────── */ .cta-btn { @@ -198,7 +123,7 @@ body.parallax-body { top: 50%; left: 50%; transform: translate(-50%, -50%); - z-index: 50; + z-index: 10; display: inline-flex; align-items: center; gap: 16px; @@ -219,11 +144,7 @@ body.parallax-body { inset 0 1px 0 rgba(255, 255, 255, 0.45); transition: box-shadow 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1); - /* Caché au départ ; révélé via GSAP en fin de scroll */ - opacity: 0; - pointer-events: none; } -.cta-btn.revealed { pointer-events: auto; } .cta-btn:hover { transform: translate(-50%, -50%) scale(1.04); @@ -275,13 +196,8 @@ body.parallax-body { padding: 16px 36px; font-size: 0.98rem; } - .act-label { font-size: 0.85rem; bottom: 22%; } } @media (prefers-reduced-motion: reduce) { - .cta-btn::after { animation: none; } - .scroll-hint { animation: none; display: none; } - .scroll-stage { display: none; } - .cta-btn { opacity: 1; pointer-events: auto; } - html { scroll-behavior: auto; } + .cta-btn::after { animation: none; } } diff --git a/index.html b/index.html index da648d2..6a1a985 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ - + - - - @@ -45,43 +42,24 @@ - -
- +
+ + + +
+ - -
Paris · CDG
-
Vol cargo
10 000 km
-
Antananarivo
- - -
- Faites défiler - -
- - - + + Accéder au site -
- -
-
-
-
-
@@ -109,11 +87,6 @@ }); } })(); - - /* Scroll-hint : disparaît dès qu'on commence à scroller */ - window.addEventListener('scroll', () => { - document.getElementById('scrollHint')?.classList.add('hidden'); - }, { once: true, passive: true }); diff --git a/js/intro-scene.js b/js/intro-scene.js index 5855d66..6418d54 100644 --- a/js/intro-scene.js +++ b/js/intro-scene.js @@ -1,20 +1,16 @@ /* ========================================================================= INTRO SCENE — MVA Global Fret - Three.js scene driven by scroll. The cargo airliner is a GLTF model - loaded at runtime; GSAP + ScrollTrigger animate the camera position - and exposition labels as the page scrolls. The CTA fades in at the - end of the timeline. + Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris : + il entre par le haut-gauche quand la souris est à gauche, traverse + l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll. - 3D model credit (CC-BY): "Airplane" by Poly by Google + 3D model credit (CC-BY 3.0) : « Airplane » by Poly by Google https://poly.pizza/m/a3XrQkLNna9 ========================================================================= */ import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; -const { gsap, ScrollTrigger } = window; -gsap.registerPlugin(ScrollTrigger); - /* ── Renderer & camera ─────────────────────────────────────────────────── */ const canvas = document.getElementById('three-canvas'); const renderer = new THREE.WebGLRenderer({ @@ -27,8 +23,8 @@ renderer.toneMappingExposure = 1.05; const scene = new THREE.Scene(); -const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 1000); -camera.position.set(0, 1.4, 18); +const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000); +camera.position.set(0, 0, 22); camera.lookAt(0, 0, 0); /* ── Lighting (golden-hour fill) ───────────────────────────────────────── */ @@ -51,9 +47,6 @@ loader.load( 'assets/airplane.glb', (gltf) => { const model = gltf.scene; - /* Compute bbox at native scale, then offset model so its center sits - at the wrapper's origin. The wrapper scales everything together, - so the center offset stays valid after scaling. */ const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); @@ -63,95 +56,71 @@ loader.load( wrapper.add(model); const targetSize = 8.5; wrapper.scale.setScalar(targetSize / Math.max(size.x, size.y, size.z)); - /* The Poly by Google plane sits with nose along +X; turn it so the - nose faces the camera at the start of the timeline. */ - wrapper.rotation.y = Math.PI / 2; + /* Pivote le modèle pour que le nez pointe vers la droite (+X) */ + wrapper.rotation.y = -Math.PI / 2; planeHolder.add(wrapper); }, undefined, (err) => console.error('Failed to load airplane.glb:', err) ); -/* ── Cloud sprites ─────────────────────────────────────────────────────── */ -const clouds = new THREE.Group(); -const cloudMat = new THREE.MeshStandardMaterial({ - color: 0xffffff, roughness: 1.0, metalness: 0, transparent: true, opacity: 0.85 -}); -for (let i = 0; i < 14; i++) { - const cl = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 10), cloudMat); - const r = 18 + Math.random() * 14; - const a = Math.random() * Math.PI * 2; - cl.position.set(Math.cos(a) * r, (Math.random() - 0.4) * 9, Math.sin(a) * r - 8); - cl.scale.set(1.6 + Math.random() * 1.4, 0.9 + Math.random() * 0.6, 1.6 + Math.random() * 1.4); - clouds.add(cl); -} -scene.add(clouds); +/* ── Souris ───────────────────────────────────────────────────────────── + - mouseX, mouseY : 0..1 normalisés + - Sur écran sans souris (touch/mobile), valeur lente d'auto-scroll +*/ +const mouse = { tx: 0.0, ty: 0.5, x: 0.0, y: 0.5 }; -/* ── Scroll-driven progress ────────────────────────────────────────────── */ -const state = { progress: 0 }; +window.addEventListener('mousemove', (e) => { + mouse.tx = Math.max(0, Math.min(1, e.clientX / window.innerWidth)); + mouse.ty = Math.max(0, Math.min(1, e.clientY / window.innerHeight)); +}, { passive: true }); -ScrollTrigger.create({ - trigger: '.scroll-stage', - start: 'top top', - end: 'bottom bottom', - scrub: 0.6, - onUpdate: self => { state.progress = self.progress; } -}); - -/* Cross-fade des étiquettes d'acte */ -const labelTl = gsap.timeline({ - scrollTrigger: { trigger: '.scroll-stage', start: 'top top', end: 'bottom bottom', scrub: 0.4 } -}); -labelTl - .fromTo('#label-paris', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.05 }, 0.02) - .to( '#label-paris', { opacity: 0, y: -20 }, 0.20) - .fromTo('#label-cruise', { opacity: 0, y: 20 }, { opacity: 1, y: 0 }, 0.40) - .to( '#label-cruise', { opacity: 0, y: -20 }, 0.60) - .fromTo('#label-tana', { opacity: 0, y: 20 }, { opacity: 1, y: 0 }, 0.72) - .to( '#label-tana', { opacity: 0, y: -20 }, 0.92); - -/* CTA reveal — last ~10% of scroll */ -gsap.fromTo('#ctaBtn', - { opacity: 0, scale: 0.85 }, - { - opacity: 1, scale: 1, - scrollTrigger: { - trigger: '.scroll-stage', - start: 'bottom-=400 bottom', - end: 'bottom bottom', - scrub: 0.4, - onLeave: () => document.getElementById('ctaBtn').classList.add('revealed'), - onEnterBack: () => document.getElementById('ctaBtn').classList.remove('revealed'), - } +/* Sur mobile : utilise l'orientation gamma (gauche-droite) si dispo */ +window.addEventListener('deviceorientation', (e) => { + if (e.gamma == null) return; + mouse.tx = Math.max(0, Math.min(1, (e.gamma + 30) / 60)); + if (e.beta != null) { + mouse.ty = Math.max(0, Math.min(1, (e.beta - 20) / 60)); } -); +}, { passive: true }); + +/* Variable CSS pour la parallaxe douce de la photo de fond */ +const root = document.documentElement; /* ── Render loop ───────────────────────────────────────────────────────── */ const clock = new THREE.Clock(); + function tick() { const t = clock.getElapsedTime(); - const p = state.progress; - /* Camera arc around the plane */ - const camAngle = -0.6 + p * 1.7; - const camRadius = 18 - p * 6 + Math.sin(p * Math.PI) * -3; - const camHeight = 1.4 + Math.sin(p * Math.PI) * 1.8 - p * 0.6; + /* Lerp doux vers la cible souris */ + mouse.x += (mouse.tx - mouse.x) * 0.06; + mouse.y += (mouse.ty - mouse.y) * 0.06; - camera.position.set( - Math.sin(camAngle) * camRadius, - camHeight, - Math.cos(camAngle) * camRadius - ); - camera.lookAt(0, 0, 0); + /* Mappe sur les variables CSS (parallaxe légère du fond) */ + root.style.setProperty('--mx', ((mouse.x - 0.5) * 2).toFixed(4)); + root.style.setProperty('--my', ((mouse.y - 0.5) * 2).toFixed(4)); - /* Plane bob + roll */ - planeHolder.position.y = Math.sin(t * 0.9) * 0.15; - planeHolder.rotation.z = Math.sin(t * 0.5) * 0.04 + (p - 0.5) * 0.18; - planeHolder.rotation.y = Math.sin(t * 0.4) * 0.025; + /* Position de l'avion : + - mouse.x = 0 → arrive haut-gauche (hors champ) + - mouse.x = 0.5 → centré + - mouse.x = 1 → sortie en bas-droite (hors champ) + */ + const px = -16 + mouse.x * 32; // -16 à +16 + const py = 5 - mouse.x * 7; // descend de +5 à -2 selon X + /* Léger bobbing autonome pour qu'il reste vivant même sans bouger la souris */ + const bob = Math.sin(t * 0.9) * 0.12; - /* Clouds slow rotation */ - clouds.rotation.y = t * 0.04 - p * 0.6; - clouds.position.y = Math.sin(t * 0.3) * 0.4; + planeHolder.position.set(px, py + bob, 0); + + /* Roulis : penche dans le sens du mouvement (mais tourne le nez vers la droite) */ + const targetRoll = -0.18 - (mouse.x - 0.5) * 0.25; // léger pivot quand on traverse + const targetPitch = -0.18 - mouse.x * 0.10; // légèrement piqué + const targetYaw = (mouse.x - 0.5) * 0.10; // soupçon de yaw + + planeHolder.rotation.z += (targetRoll - planeHolder.rotation.z) * 0.08; + planeHolder.rotation.x += (targetPitch - planeHolder.rotation.x) * 0.08; + planeHolder.rotation.y += (targetYaw - planeHolder.rotation.y) * 0.08; renderer.render(scene, camera); requestAnimationFrame(tick); diff --git a/videos/parachute-drop.mp4 b/videos/parachute-drop.mp4 deleted file mode 100644 index caa279e..0000000 Binary files a/videos/parachute-drop.mp4 and /dev/null differ diff --git a/videos/parachute-poster.jpg b/videos/parachute-poster.jpg deleted file mode 100644 index 1985e4a..0000000 Binary files a/videos/parachute-poster.jpg and /dev/null differ