Swap video bg for aerial Antananarivo image, drive plane with the mouse

User-supplied aerial illustration of Antananarivo (Lake Anosy + Rova
hill + surrounding city, 1920px wide JPG, 241 KB) replaces the
parachute-drop video as the static intro backdrop. The video files
are deleted from /videos.

The plane no longer orbits a scroll timeline. Now:
- Page is a single viewport, no scroll, no act labels, no scroll hint.
- Mouse X (0..1) drives plane.position.x from -16 (offscreen left) to
  +16 (offscreen right), with plane.position.y descending from +5 to
  -2 — so the plane enters from the upper-left and exits lower-right.
- Pitch/roll/yaw lerp toward small targets that depend on mouse X, so
  the plane banks naturally as it crosses.
- Background image gets a softer mouse parallax (-16/-10px) via the
  existing --mx/--my CSS vars, now updated from intro-scene.js.
- Three.js cloud spheres are gone; the photo is the entire backdrop.
- ScrollTrigger + the GSAP timeline are removed; the page no longer
  needs gsap at all (the script tag stayed for now in case it comes
  back, but the dependency could be dropped on a future pass).
- CTA button is back to plain visible/centered, no reveal animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MVA Global Fret 2026-05-05 11:35:11 +02:00
parent d99e2a5fc1
commit eba88207c4
6 changed files with 78 additions and 220 deletions

BIN
assets/antananarivo-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View File

@ -1,7 +1,8 @@
/* ========================================================================= /* =========================================================================
PARALLAX INTRO MVA Global Fret PARALLAX INTRO MVA Global Fret
Page scrollable (4 viewports) avec scène Three.js fixée. La timeline Page unique. Photo aérienne d'Antananarivo en fond + avion 3D piloté
pilote la caméra et l'avion 3D. À 100% du scroll, le bouton CTA apparaît. par la souris (entre par le haut-gauche, sort par la droite). Bouton
CTA doré centré, toujours visible.
========================================================================= */ ========================================================================= */
:root { :root {
@ -22,12 +23,10 @@ html, body {
background: var(--navy-deep); background: var(--navy-deep);
} }
html { scroll-behavior: smooth; } .parallax-body {
body.parallax-body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
color: var(--white); color: var(--white);
overflow-x: hidden; overflow: hidden;
} }
/* ── HEADER ─────────────────────────────────────────────────────────────── */ /* ── HEADER ─────────────────────────────────────────────────────────────── */
@ -86,16 +85,12 @@ body.parallax-body {
} }
.lang-switcher button:hover:not(.active) { color: var(--white); } .lang-switcher button:hover:not(.active) { color: var(--white); }
/* SCÈNE FIXE (vidéo + voile + canvas Three.js) /* ── STAGE ──────────────────────────────────────────────────────────────── */
.stage-fixed reste plein-écran pendant que la page se déroule derrière. .stage {
*/ position: relative;
.stage-fixed {
position: fixed;
inset: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
z-index: 1;
} }
.layer { .layer {
@ -107,90 +102,20 @@ body.parallax-body {
will-change: transform; will-change: transform;
} }
.layer-video { /* Photo de fond — léger zoom + parallaxe souris discrète */
.layer-bg {
object-fit: cover; object-fit: cover;
object-position: center; 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 { .layer-tint {
background: background:
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.55) 100%), radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.32) 100%),
linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%); linear-gradient(180deg, transparent 55%, rgba(5, 5, 24, 0.40) 100%);
} }
.layer-three { .layer-three { display: block; z-index: 2; }
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); }
}
/* ── BOUTON CTA centré ──────────────────────────────────────────────────── */ /* ── BOUTON CTA centré ──────────────────────────────────────────────────── */
.cta-btn { .cta-btn {
@ -198,7 +123,7 @@ body.parallax-body {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 50; z-index: 10;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@ -219,11 +144,7 @@ body.parallax-body {
inset 0 1px 0 rgba(255, 255, 255, 0.45); inset 0 1px 0 rgba(255, 255, 255, 0.45);
transition: box-shadow 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), 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); 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 { .cta-btn:hover {
transform: translate(-50%, -50%) scale(1.04); transform: translate(-50%, -50%) scale(1.04);
@ -275,13 +196,8 @@ body.parallax-body {
padding: 16px 36px; padding: 16px 36px;
font-size: 0.98rem; font-size: 0.98rem;
} }
.act-label { font-size: 0.85rem; bottom: 22%; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.cta-btn::after { animation: none; } .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; }
} }

View File

@ -18,7 +18,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="css/parallax.css"> <link rel="stylesheet" href="css/parallax.css">
<!-- Three.js (ESM via importmap) + addons (GLTFLoader, etc.) --> <!-- Three.js (ESM via importmap) + addons (GLTFLoader) -->
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
@ -27,9 +27,6 @@
} }
} }
</script> </script>
<!-- GSAP + ScrollTrigger (UMD) -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
</head> </head>
<body class="parallax-body"> <body class="parallax-body">
@ -45,43 +42,24 @@
</div> </div>
</header> </header>
<!-- Couches fixes : vidéo + voile + canvas Three.js (le plan 3D vole dessus) --> <main class="stage">
<div class="stage-fixed">
<video class="layer layer-video" id="introVideo"
autoplay loop muted playsinline preload="auto"
poster="videos/parachute-poster.jpg">
<source src="videos/parachute-drop.mp4" type="video/mp4">
</video>
<!-- Photo aérienne d'Antananarivo en fond -->
<img class="layer layer-bg" src="assets/antananarivo-bg.jpg" alt="" aria-hidden="true">
<!-- Voile très léger pour la lisibilité du bouton -->
<div class="layer layer-tint"></div> <div class="layer layer-tint"></div>
<!-- Canvas Three.js : avion piloté par la souris -->
<canvas id="three-canvas" class="layer layer-three"></canvas> <canvas id="three-canvas" class="layer layer-three"></canvas>
<!-- Étiquettes des actes — apparaissent / disparaissent selon le scroll --> <!-- Bouton CTA centré -->
<div class="act-label" id="label-paris"><span>Paris · CDG</span></div> <a href="accueil.html" class="cta-btn">
<div class="act-label" id="label-cruise"><span>Vol cargo<br>10 000 km</span></div>
<div class="act-label" id="label-tana"><span>Antananarivo</span></div>
<!-- Indicateur scroll (visible jusqu'à ce qu'on scrolle) -->
<div class="scroll-hint" id="scrollHint">
<span data-i18n="intro.scrollHint">Faites défiler</span>
<i class="fa-solid fa-chevron-down"></i>
</div>
<!-- CTA — caché jusqu'à la fin du scroll -->
<a href="accueil.html" class="cta-btn" id="ctaBtn">
<span class="cta-btn-shine"></span> <span class="cta-btn-shine"></span>
<span data-i18n="intro.ctaBtn">Accéder au site</span> <span data-i18n="intro.ctaBtn">Accéder au site</span>
<i class="fa-solid fa-arrow-right"></i> <i class="fa-solid fa-arrow-right"></i>
</a> </a>
</div>
<!-- Sentinelles invisibles : leur scroll-position pilote la timeline -->
<main class="scroll-stage" aria-hidden="true">
<section class="act"></section>
<section class="act"></section>
<section class="act"></section>
<section class="act"></section>
</main> </main>
<script src="js/translations.js"></script> <script src="js/translations.js"></script>
@ -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 });
</script> </script>
<script type="module" src="js/intro-scene.js"></script> <script type="module" src="js/intro-scene.js"></script>

View File

@ -1,20 +1,16 @@
/* ========================================================================= /* =========================================================================
INTRO SCENE MVA Global Fret INTRO SCENE MVA Global Fret
Three.js scene driven by scroll. The cargo airliner is a GLTF model Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris :
loaded at runtime; GSAP + ScrollTrigger animate the camera position il entre par le haut-gauche quand la souris est à gauche, traverse
and exposition labels as the page scrolls. The CTA fades in at the l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll.
end of the timeline.
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 https://poly.pizza/m/a3XrQkLNna9
========================================================================= */ ========================================================================= */
import * as THREE from 'three'; import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const { gsap, ScrollTrigger } = window;
gsap.registerPlugin(ScrollTrigger);
/* ── Renderer & camera ─────────────────────────────────────────────────── */ /* ── Renderer & camera ─────────────────────────────────────────────────── */
const canvas = document.getElementById('three-canvas'); const canvas = document.getElementById('three-canvas');
const renderer = new THREE.WebGLRenderer({ const renderer = new THREE.WebGLRenderer({
@ -27,8 +23,8 @@ renderer.toneMappingExposure = 1.05;
const scene = new THREE.Scene(); const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 1000); const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.4, 18); camera.position.set(0, 0, 22);
camera.lookAt(0, 0, 0); camera.lookAt(0, 0, 0);
/* ── Lighting (golden-hour fill) ───────────────────────────────────────── */ /* ── Lighting (golden-hour fill) ───────────────────────────────────────── */
@ -51,9 +47,6 @@ loader.load(
'assets/airplane.glb', 'assets/airplane.glb',
(gltf) => { (gltf) => {
const model = gltf.scene; 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 box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3());
@ -63,95 +56,71 @@ loader.load(
wrapper.add(model); wrapper.add(model);
const targetSize = 8.5; const targetSize = 8.5;
wrapper.scale.setScalar(targetSize / Math.max(size.x, size.y, size.z)); 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 /* Pivote le modèle pour que le nez pointe vers la droite (+X) */
nose faces the camera at the start of the timeline. */ wrapper.rotation.y = -Math.PI / 2;
wrapper.rotation.y = Math.PI / 2;
planeHolder.add(wrapper); planeHolder.add(wrapper);
}, },
undefined, undefined,
(err) => console.error('Failed to load airplane.glb:', err) (err) => console.error('Failed to load airplane.glb:', err)
); );
/* ── Cloud sprites ─────────────────────────────────────────────────────── */ /* Souris
const clouds = new THREE.Group(); - mouseX, mouseY : 0..1 normalisés
const cloudMat = new THREE.MeshStandardMaterial({ - Sur écran sans souris (touch/mobile), valeur lente d'auto-scroll
color: 0xffffff, roughness: 1.0, metalness: 0, transparent: true, opacity: 0.85 */
}); const mouse = { tx: 0.0, ty: 0.5, x: 0.0, y: 0.5 };
for (let i = 0; i < 14; i++) {
const cl = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 10), cloudMat); window.addEventListener('mousemove', (e) => {
const r = 18 + Math.random() * 14; mouse.tx = Math.max(0, Math.min(1, e.clientX / window.innerWidth));
const a = Math.random() * Math.PI * 2; mouse.ty = Math.max(0, Math.min(1, e.clientY / window.innerHeight));
cl.position.set(Math.cos(a) * r, (Math.random() - 0.4) * 9, Math.sin(a) * r - 8); }, { passive: true });
cl.scale.set(1.6 + Math.random() * 1.4, 0.9 + Math.random() * 0.6, 1.6 + Math.random() * 1.4);
clouds.add(cl); /* 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));
} }
scene.add(clouds); }, { passive: true });
/* ── Scroll-driven progress ────────────────────────────────────────────── */ /* Variable CSS pour la parallaxe douce de la photo de fond */
const state = { progress: 0 }; const root = document.documentElement;
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'),
}
}
);
/* ── Render loop ───────────────────────────────────────────────────────── */ /* ── Render loop ───────────────────────────────────────────────────────── */
const clock = new THREE.Clock(); const clock = new THREE.Clock();
function tick() { function tick() {
const t = clock.getElapsedTime(); const t = clock.getElapsedTime();
const p = state.progress;
/* Camera arc around the plane */ /* Lerp doux vers la cible souris */
const camAngle = -0.6 + p * 1.7; mouse.x += (mouse.tx - mouse.x) * 0.06;
const camRadius = 18 - p * 6 + Math.sin(p * Math.PI) * -3; mouse.y += (mouse.ty - mouse.y) * 0.06;
const camHeight = 1.4 + Math.sin(p * Math.PI) * 1.8 - p * 0.6;
camera.position.set( /* Mappe sur les variables CSS (parallaxe légère du fond) */
Math.sin(camAngle) * camRadius, root.style.setProperty('--mx', ((mouse.x - 0.5) * 2).toFixed(4));
camHeight, root.style.setProperty('--my', ((mouse.y - 0.5) * 2).toFixed(4));
Math.cos(camAngle) * camRadius
);
camera.lookAt(0, 0, 0);
/* Plane bob + roll */ /* Position de l'avion :
planeHolder.position.y = Math.sin(t * 0.9) * 0.15; - mouse.x = 0 arrive haut-gauche (hors champ)
planeHolder.rotation.z = Math.sin(t * 0.5) * 0.04 + (p - 0.5) * 0.18; - mouse.x = 0.5 centré
planeHolder.rotation.y = Math.sin(t * 0.4) * 0.025; - 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 */ planeHolder.position.set(px, py + bob, 0);
clouds.rotation.y = t * 0.04 - p * 0.6;
clouds.position.y = Math.sin(t * 0.3) * 0.4; /* 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); renderer.render(scene, camera);
requestAnimationFrame(tick); requestAnimationFrame(tick);

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB