/* ========================================================================= INTRO SCENE — MVA Global Fret Scène Three.js minimaliste : un avion-cargo low-poly construit à partir de primitives (cylindre + boîtes + cônes), peint aux couleurs de la marque. La timeline est pilotée par GSAP + ScrollTrigger : la position du scroll anime caméra, avion et étiquettes. À 100% du scroll, le bouton CTA est révélé. ========================================================================= */ import * as THREE from 'three'; const { gsap, ScrollTrigger } = window; gsap.registerPlugin(ScrollTrigger); /* ── Setup ──────────────────────────────────────────────────────────────── */ 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; 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); camera.lookAt(0, 0, 0); /* ── Lighting ───────────────────────────────────────────────────────────── */ const sun = new THREE.DirectionalLight(0xfff1d6, 1.4); // chaude, golden hour sun.position.set(-6, 8, 4); scene.add(sun); const skyFill = new THREE.HemisphereLight(0xa8c5ff, 0x3a2a1a, 0.55); scene.add(skyFill); const ambient = new THREE.AmbientLight(0xffffff, 0.18); scene.add(ambient); /* ── Avion (low-poly à partir de primitives) ───────────────────────────── Couleurs MVA : corps blanc cassé, accents navy + or sur les ailerons. */ const NAVY = new THREE.Color(0x1a1a3e); const GOLD = new THREE.Color(0xc5a55a); const BODY = new THREE.Color(0xf2f2f7); const matBody = new THREE.MeshStandardMaterial({ color: BODY, roughness: 0.45, metalness: 0.08 }); const matNavy = new THREE.MeshStandardMaterial({ color: NAVY, roughness: 0.55, metalness: 0.10 }); const matGold = new THREE.MeshStandardMaterial({ color: GOLD, roughness: 0.35, metalness: 0.55 }); const matWindow = new THREE.MeshStandardMaterial({ color: 0x0e1a30, roughness: 0.2, metalness: 0.4, emissive: 0x0a1428, emissiveIntensity: 0.4 }); const plane = new THREE.Group(); // Fuselage — cylindre couché le long de l'axe Z const fuselageGeo = new THREE.CylinderGeometry(0.55, 0.5, 7.2, 24); const fuselage = new THREE.Mesh(fuselageGeo, matBody); fuselage.rotation.x = Math.PI / 2; plane.add(fuselage); // Nez (cône avant) const nose = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.4, 24), matBody); nose.rotation.x = -Math.PI / 2; nose.position.z = 4.3; plane.add(nose); // Cockpit (sphère bleu nuit pour les vitres) const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.42, 18, 16, 0, Math.PI * 2, 0, Math.PI / 2), matWindow); cockpit.rotation.x = -Math.PI / 2; cockpit.position.set(0, 0.18, 3.4); plane.add(cockpit); // Bande dorée le long du fuselage (un cylindre fin) const goldBand = new THREE.Mesh(new THREE.CylinderGeometry(0.555, 0.555, 7.2, 24, 1, true), matGold); goldBand.rotation.x = Math.PI / 2; goldBand.scale.set(1.001, 0.08, 1.001); // bande fine plane.add(goldBand); // Ailes principales (boîte aplatie, légèrement en flèche via rotation Y) const wingGeo = new THREE.BoxGeometry(11, 0.18, 1.6); const wings = new THREE.Mesh(wingGeo, matBody); wings.position.set(0, -0.05, -0.4); plane.add(wings); // Bord doré sur les ailes const wingTrim = new THREE.Mesh(new THREE.BoxGeometry(11.05, 0.06, 0.18), matGold); wingTrim.position.set(0, -0.04, 0.32); plane.add(wingTrim); // Empennage horizontal (petites ailes arrière) const tail = new THREE.Mesh(new THREE.BoxGeometry(3.4, 0.14, 0.85), matBody); tail.position.set(0, 0.25, -3.0); plane.add(tail); // Empennage vertical (queue) const fin = new THREE.Mesh(new THREE.BoxGeometry(0.16, 1.45, 1.6), matNavy); fin.position.set(0, 0.85, -2.9); fin.geometry.translate(0, 0, -0.4); // base alignée arrière plane.add(fin); // Logo navy sur la queue (un petit cube doré) const logoBadge = new THREE.Mesh(new THREE.BoxGeometry(0.165, 0.45, 0.35), matGold); logoBadge.position.set(0, 1.1, -3.1); plane.add(logoBadge); // Réacteurs sous les ailes (cylindres) const engineGeo = new THREE.CylinderGeometry(0.35, 0.32, 1.5, 18); const engineL = new THREE.Mesh(engineGeo, matNavy); engineL.rotation.x = Math.PI / 2; engineL.position.set(-2.6, -0.45, 0.2); plane.add(engineL); const engineR = engineL.clone(); engineR.position.x = 2.6; plane.add(engineR); // Petites bouches d'admission dorées sur les réacteurs const intakeGeo = new THREE.TorusGeometry(0.35, 0.05, 10, 24); const intakeL = new THREE.Mesh(intakeGeo, matGold); intakeL.position.set(-2.6, -0.45, 0.95); plane.add(intakeL); const intakeR = intakeL.clone(); intakeR.position.x = 2.6; plane.add(intakeR); scene.add(plane); /* ── Nuages volants (sprites simples) ───────────────────────────────────── */ 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); /* ── État animé piloté par le scroll ───────────────────────────────────── */ const state = { progress: 0 }; ScrollTrigger.create({ trigger: '.scroll-stage', start: 'top top', end: 'bottom bottom', scrub: 0.6, onUpdate: self => { state.progress = self.progress; } }); /* Étiquettes : fade-in/out aux bonnes plages de scroll */ const tl = gsap.timeline({ scrollTrigger: { trigger: '.scroll-stage', start: 'top top', end: 'bottom bottom', scrub: 0.4 } }); tl.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 : caché jusqu'à ~92% du scroll, puis fade in */ gsap.fromTo('#ctaBtn', { opacity: 0, scale: 0.85 }, { opacity: 1, scale: 1, duration: 0.5, ease: 'cubic-bezier(0.2, 0.8, 0.2, 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 ────────────────────────────────────────────────────────── */ const clock = new THREE.Clock(); function tick() { const t = clock.getElapsedTime(); const p = state.progress; /* Caméra : large arrière → côté → arrière en s'éloignant à la fin */ // Arc autour de l'avion const camAngle = -0.6 + p * 1.7; // -0.6 rad → +1.1 rad const camRadius = 18 - p * 6 + Math.sin(p * Math.PI) * -3; // 18 → 12, dip au milieu const camHeight = 1.4 + Math.sin(p * Math.PI) * 1.8 - p * 0.6; camera.position.set( Math.sin(camAngle) * camRadius, camHeight, Math.cos(camAngle) * camRadius ); camera.lookAt(0, 0, 0); /* Avion : léger bob + roulis selon scroll */ plane.position.y = Math.sin(t * 0.9) * 0.15; plane.rotation.z = Math.sin(t * 0.5) * 0.04 + (p - 0.5) * 0.18; plane.rotation.y = Math.sin(t * 0.4) * 0.025; /* Nuages : rotation lente autour de l'avion */ clouds.rotation.y = t * 0.04 - p * 0.6; clouds.position.y = Math.sin(t * 0.3) * 0.4; 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();