diff --git a/assets/airplane.glb b/assets/airplane.glb new file mode 100644 index 0000000..f3815d2 Binary files /dev/null and b/assets/airplane.glb differ diff --git a/index.html b/index.html index b23bd93..da648d2 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,10 @@ + @@ -12,11 +18,12 @@ - + diff --git a/js/intro-scene.js b/js/intro-scene.js index cc7db5a..5855d66 100644 --- a/js/intro-scene.js +++ b/js/intro-scene.js @@ -1,24 +1,29 @@ /* ========================================================================= 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é. + 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. + + 3D model credit (CC-BY): "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); -/* ── Setup ──────────────────────────────────────────────────────────────── */ +/* ── 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(); @@ -26,104 +31,48 @@ const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerH camera.position.set(0, 1.4, 18); camera.lookAt(0, 0, 0); -/* ── Lighting ───────────────────────────────────────────────────────────── */ -const sun = new THREE.DirectionalLight(0xfff1d6, 1.4); // chaude, golden hour +/* ── 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.55); +const skyFill = new THREE.HemisphereLight(0xa8c5ff, 0x3a2a1a, 0.65); scene.add(skyFill); -const ambient = new THREE.AmbientLight(0xffffff, 0.18); +const ambient = new THREE.AmbientLight(0xffffff, 0.20); 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); +/* ── Plane (GLTF) ──────────────────────────────────────────────────────── */ +const planeHolder = new THREE.Group(); +scene.add(planeHolder); -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 loader = new GLTFLoader(); +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()); + model.position.sub(center); -const plane = new THREE.Group(); + const wrapper = new THREE.Group(); + 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; + planeHolder.add(wrapper); + }, + undefined, + (err) => console.error('Failed to load airplane.glb:', err) +); -// 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) ───────────────────────────────────── */ +/* ── Cloud sprites ─────────────────────────────────────────────────────── */ const clouds = new THREE.Group(); const cloudMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 1.0, metalness: 0, transparent: true, opacity: 0.85 @@ -138,7 +87,7 @@ for (let i = 0; i < 14; i++) { } scene.add(clouds); -/* ── État animé piloté par le scroll ───────────────────────────────────── */ +/* ── Scroll-driven progress ────────────────────────────────────────────── */ const state = { progress: 0 }; ScrollTrigger.create({ @@ -149,23 +98,23 @@ ScrollTrigger.create({ onUpdate: self => { state.progress = self.progress; } }); -/* Étiquettes : fade-in/out aux bonnes plages de scroll */ -const tl = gsap.timeline({ +/* Cross-fade des étiquettes d'acte */ +const labelTl = 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) +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 : caché jusqu'à ~92% du scroll, puis fade in */ +/* CTA reveal — last ~10% of scroll */ 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)', + opacity: 1, scale: 1, scrollTrigger: { trigger: '.scroll-stage', start: 'bottom-=400 bottom', @@ -177,16 +126,15 @@ gsap.fromTo('#ctaBtn', } ); -/* ── Render loop ────────────────────────────────────────────────────────── */ +/* ── 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 + /* 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; camera.position.set( @@ -196,12 +144,12 @@ function tick() { ); 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; + /* 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; - /* Nuages : rotation lente autour de l'avion */ + /* Clouds slow rotation */ clouds.rotation.y = t * 0.04 - p * 0.6; clouds.position.y = Math.sin(t * 0.3) * 0.4; @@ -209,7 +157,7 @@ function tick() { requestAnimationFrame(tick); } -/* ── Resize ─────────────────────────────────────────────────────────────── */ +/* ── Resize ────────────────────────────────────────────────────────────── */ function resize() { const w = window.innerWidth; const h = window.innerHeight;