site-mva-global-fret/js/intro-scene.js
MVA Global Fret ad9ad43487 Use the correct Antananarivo aerial image, lift plane to upper half
Background was inadvertently the wrong Gemini export (an unrelated
airliner-over-mountains photo) — sorted out and replaced with the
intended aerial illustration of Antananarivo (Lake Anosy + Rova
hill + city, 487 KB, 1920px wide).

Also lifted the plane's trajectory: y goes from +7 (offscreen
upper-left) to +2 (still upper half, exiting right) instead of
+5..-2. The plane now stays clearly in the upper third of the
viewport, leaving room for the centered CTA button and the city
detail at the bottom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 11:39:08 +02:00

140 lines
5.8 KiB
JavaScript

/* =========================================================================
INTRO SCENE — MVA Global Fret
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 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';
/* ── Renderer & camera ─────────────────────────────────────────────────── */
const canvas = document.getElementById('three-canvas');
const renderer = new THREE.WebGLRenderer({
canvas, alpha: true, antialias: true, powerPreference: 'high-performance'
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
const scene = new THREE.Scene();
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) ───────────────────────────────────────── */
const sun = new THREE.DirectionalLight(0xfff1d6, 1.5);
sun.position.set(-6, 8, 4);
scene.add(sun);
const skyFill = new THREE.HemisphereLight(0xa8c5ff, 0x3a2a1a, 0.65);
scene.add(skyFill);
const ambient = new THREE.AmbientLight(0xffffff, 0.20);
scene.add(ambient);
/* ── Plane (GLTF) ──────────────────────────────────────────────────────── */
const planeHolder = new THREE.Group();
scene.add(planeHolder);
const loader = new GLTFLoader();
loader.load(
'assets/airplane.glb',
(gltf) => {
const model = gltf.scene;
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
const wrapper = new THREE.Group();
wrapper.add(model);
const targetSize = 8.5;
wrapper.scale.setScalar(targetSize / Math.max(size.x, size.y, size.z));
/* 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)
);
/* ── 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 };
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 });
/* 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();
/* Lerp doux vers la cible souris */
mouse.x += (mouse.tx - mouse.x) * 0.06;
mouse.y += (mouse.ty - mouse.y) * 0.06;
/* 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));
/* 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 = 7 - mouse.x * 5; // descend de +7 à +2 (haut de l'écran)
/* Léger bobbing autonome pour qu'il reste vivant même sans bouger la souris */
const bob = Math.sin(t * 0.9) * 0.12;
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);
}
/* ── Resize ────────────────────────────────────────────────────────────── */
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', resize);
resize();
tick();