/* ========================================================================= 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 = 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; 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();