diff --git a/assets/parcel.png b/assets/parcel.png new file mode 100644 index 0000000..b74a952 Binary files /dev/null and b/assets/parcel.png differ diff --git a/js/intro-scene.js b/js/intro-scene.js index 8e7a280..fb71a53 100644 --- a/js/intro-scene.js +++ b/js/intro-scene.js @@ -65,6 +65,72 @@ loader.load( (err) => console.error('Failed to load airplane.glb:', err) ); +/* ── Colis parachutés ────────────────────────────────────────────────── + À intervalle régulier (et tant que l'avion est dans le cadre), on + spawn un sprite « parachute + colis » à la position de l'avion. + Chaque colis tombe lentement et grossit (effet de zoom dû à la + descente vers la caméra). Il s'efface en fin de course. +*/ +const parcelTex = new THREE.TextureLoader().load('assets/parcel.png'); +parcelTex.colorSpace = THREE.SRGBColorSpace; + +const PARCEL_SPAWN_EVERY = 1.4; // un colis toutes les ~1.4s +const PARCEL_FALL_SPEED = 1.4; // unités monde/s +const PARCEL_DRIFT = 0.45; // dérive horizontale (sortie de soute) +const PARCEL_INITIAL_SCALE = 0.35; +const PARCEL_FINAL_SCALE = 2.6; +const PARCEL_LIFETIME = 5.5; // s avant de disparaître + +const parcels = []; +let lastSpawn = 0; + +function spawnParcel(x, y) { + const mat = new THREE.SpriteMaterial({ + map: parcelTex, transparent: true, opacity: 0 + }); + const sprite = new THREE.Sprite(mat); + sprite.position.set(x + (Math.random() - 0.5) * 0.4, y - 0.2, 0.2); + sprite.scale.setScalar(PARCEL_INITIAL_SCALE); + scene.add(sprite); + parcels.push({ + sprite, age: 0, + vx: (Math.random() - 0.5) * PARCEL_DRIFT, // dérive latérale légère + sway: 0.5 + Math.random() * 0.6, // freq de balancement + swayAmp: 0.08 + Math.random() * 0.08 + }); +} + +function updateParcels(dt, t) { + for (let i = parcels.length - 1; i >= 0; i--) { + const p = parcels[i]; + p.age += dt; + const life = p.age / PARCEL_LIFETIME; + + /* Trajectoire : chute douce + dérive + balancement parachute */ + p.sprite.position.y -= PARCEL_FALL_SPEED * dt; + p.sprite.position.x += p.vx * dt; + p.sprite.position.x += Math.sin(t * p.sway) * p.swayAmp * dt; + + /* Grandit en tombant — perspective */ + const k = Math.min(1, life * 1.1); + p.sprite.scale.setScalar( + PARCEL_INITIAL_SCALE + (PARCEL_FINAL_SCALE - PARCEL_INITIAL_SCALE) * k + ); + + /* Fade-in rapide au spawn, fade-out lent à la fin */ + let opacity = 1; + if (life < 0.06) opacity = life / 0.06; + else if (life > 0.85) opacity = (1 - life) / 0.15; + p.sprite.material.opacity = Math.max(0, Math.min(1, opacity)); + + if (life >= 1 || p.sprite.position.y < -10) { + scene.remove(p.sprite); + p.sprite.material.dispose(); + parcels.splice(i, 1); + } + } +} + /* ── Souris ───────────────────────────────────────────────────────────── L'avion avance automatiquement à vitesse de croisière (BASE_SPEED). Bouger la souris ajoute un boost qui le pousse plus vite vers la fin. @@ -137,6 +203,14 @@ function tick() { planeHolder.position.set(px, py + bob, 0); + /* Spawn d'un colis parachuté à intervalle régulier — uniquement + pendant que l'avion est dans (ou proche de) la zone visible. */ + if (p > 0.04 && p < 0.92 && t - lastSpawn > PARCEL_SPAWN_EVERY) { + spawnParcel(px, py + bob); + lastSpawn = t; + } + updateParcels(dt, t); + /* Avec nez à -X et up = +Y : - rotation.z = PITCH (négatif = nez en l'air) - rotation.x = ROLL