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