Convert intro into a scroll-driven 3D cinematic
Restructure the page so the first 4 viewports of scroll drive a Three.js scene composited on top of the Antananarivo parachute video. What's there: - Three.js (ESM, r158 via importmap) renders a low-poly cargo airliner built from primitives: cylinder fuselage, cone nose, sphere cockpit (dark glass + emissive), box wings/tail/fin, cylinder engines with torus intakes, gold trim band, navy fin with gold logo box. No external model file. - Hemisphere + directional + ambient lights tuned for golden-hour fill. - 14 cloud spheres scattered around the plane, slowly rotating. - GSAP + ScrollTrigger drive a single progress value scrubbed against scroll position. Inside the rAF loop, the camera arcs from rear-left (-0.6 rad) to front-right (+1.1 rad), radius dipping mid-flight, and the plane rolls slightly with scroll. - Three act labels (Paris CDG / Vol cargo / Antananarivo) cross-fade at 20%/40%-60%/72% scroll positions via a chained gsap timeline. - Gold CTA button stays opacity:0 + pointer-events:none until the last ~10% of scroll, then fades and scales in. Hover transform rebuilt without the old mouse-parallax tilt (fights the scroll animation). - Scroll hint pill (chevron + "Faites défiler") at the bottom of the first viewport, fades out on first scroll event. - prefers-reduced-motion shortcut: scroll stage hidden, CTA visible, no animation. Page reverts to a static screen with the video bg. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6f6cb672ac
commit
341dca7cb5
121
css/parallax.css
121
css/parallax.css
@ -1,7 +1,7 @@
|
||||
/* =========================================================================
|
||||
PARALLAX INTRO — MVA Global Fret
|
||||
Page unique, fixe (pas de scroll). Vidéo Terre rotative + ligne rouge 3D
|
||||
Paris ↔ Antananarivo (suit la rotation du globe) + bouton centré.
|
||||
Page scrollable (4 viewports) avec scène Three.js fixée. La timeline
|
||||
pilote la caméra et l'avion 3D. À 100% du scroll, le bouton CTA apparaît.
|
||||
========================================================================= */
|
||||
|
||||
:root {
|
||||
@ -19,13 +19,15 @@
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--navy-deep);
|
||||
}
|
||||
|
||||
.parallax-body {
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body.parallax-body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--white);
|
||||
background: var(--navy-deep);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── HEADER ─────────────────────────────────────────────────────────────── */
|
||||
@ -36,7 +38,7 @@ html, body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 36px;
|
||||
z-index: 50;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.parallax-logo {
|
||||
@ -84,12 +86,16 @@ html, body {
|
||||
}
|
||||
.lang-switcher button:hover:not(.active) { color: var(--white); }
|
||||
|
||||
/* ── STAGE ──────────────────────────────────────────────────────────────── */
|
||||
.stage {
|
||||
position: relative;
|
||||
/* ── SCÈNE FIXE (vidéo + voile + canvas Three.js) ────────────────────────
|
||||
.stage-fixed reste plein-écran pendant que la page se déroule derrière.
|
||||
*/
|
||||
.stage-fixed {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.layer {
|
||||
@ -101,8 +107,6 @@ html, body {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Parallaxe vidéo : suit la souris avec easing. Sur-dimensionnée pour
|
||||
masquer les bords quand elle bouge. */
|
||||
.layer-video {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
@ -115,17 +119,86 @@ html, body {
|
||||
linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%);
|
||||
}
|
||||
|
||||
.layer-three {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ── SCROLL STAGE — sentinelles invisibles qui donnent la hauteur ──────── */
|
||||
.scroll-stage {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.scroll-stage .act {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── ÉTIQUETTES D'ACTE ──────────────────────────────────────────────────── */
|
||||
.act-label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.05rem;
|
||||
color: var(--white);
|
||||
text-shadow: 0 4px 18px rgba(0, 0, 0, 0.7);
|
||||
opacity: 0;
|
||||
}
|
||||
.act-label span {
|
||||
display: inline-block;
|
||||
padding: 14px 28px;
|
||||
background: rgba(5, 5, 24, 0.45);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 8px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* ── INDICATEUR SCROLL ─────────────────────────────────────────────────── */
|
||||
.scroll-hint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 36px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
|
||||
animation: scrollBob 1.8s ease-in-out infinite;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
.scroll-hint.hidden { opacity: 0; pointer-events: none; }
|
||||
.scroll-hint i { font-size: 1rem; }
|
||||
|
||||
@keyframes scrollBob {
|
||||
0%, 100% { transform: translate(-50%, 0); }
|
||||
50% { transform: translate(-50%, 8px); }
|
||||
}
|
||||
|
||||
/* ── BOUTON CTA centré ──────────────────────────────────────────────────── */
|
||||
.cta-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
/* Bouge légèrement en sens INVERSE des couches → effet de tilt 3D */
|
||||
transform: translate(
|
||||
calc(-50% + var(--mx) * 8px),
|
||||
calc(-50% + var(--my) * 8px)
|
||||
);
|
||||
z-index: 10;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 50;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@ -146,13 +219,14 @@ html, body {
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||
transition: box-shadow 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
/* Caché au départ ; révélé via GSAP en fin de scroll */
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cta-btn.revealed { pointer-events: auto; }
|
||||
|
||||
.cta-btn:hover {
|
||||
transform: translate(
|
||||
calc(-50% + var(--mx) * 8px),
|
||||
calc(-50% + var(--my) * 8px)
|
||||
) scale(1.04);
|
||||
transform: translate(-50%, -50%) scale(1.04);
|
||||
box-shadow:
|
||||
0 28px 75px rgba(197, 165, 90, 0.7),
|
||||
0 0 0 12px rgba(197, 165, 90, 0.12),
|
||||
@ -201,8 +275,13 @@ html, body {
|
||||
padding: 16px 36px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
.act-label { font-size: 0.85rem; bottom: 22%; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cta-btn::after { animation: none; }
|
||||
.scroll-hint { animation: none; display: none; }
|
||||
.scroll-stage { display: none; }
|
||||
.cta-btn { opacity: 1; pointer-events: auto; }
|
||||
html { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
73
index.html
73
index.html
@ -11,6 +11,18 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/parallax.css">
|
||||
|
||||
<!-- Three.js (ESM via importmap) -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- GSAP + ScrollTrigger (UMD) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
|
||||
</head>
|
||||
<body class="parallax-body">
|
||||
|
||||
@ -26,8 +38,8 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="stage">
|
||||
|
||||
<!-- Couches fixes : vidéo + voile + canvas Three.js (le plan 3D vole dessus) -->
|
||||
<div class="stage-fixed">
|
||||
<video class="layer layer-video" id="introVideo"
|
||||
autoplay loop muted playsinline preload="auto"
|
||||
poster="videos/parachute-poster.jpg">
|
||||
@ -36,12 +48,33 @@
|
||||
|
||||
<div class="layer layer-tint"></div>
|
||||
|
||||
<a href="accueil.html" class="cta-btn">
|
||||
<canvas id="three-canvas" class="layer layer-three"></canvas>
|
||||
|
||||
<!-- Étiquettes des actes — apparaissent / disparaissent selon le scroll -->
|
||||
<div class="act-label" id="label-paris"><span>Paris · CDG</span></div>
|
||||
<div class="act-label" id="label-cruise"><span>Vol cargo<br>10 000 km</span></div>
|
||||
<div class="act-label" id="label-tana"><span>Antananarivo</span></div>
|
||||
|
||||
<!-- Indicateur scroll (visible jusqu'à ce qu'on scrolle) -->
|
||||
<div class="scroll-hint" id="scrollHint">
|
||||
<span data-i18n="intro.scrollHint">Faites défiler</span>
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</div>
|
||||
|
||||
<!-- CTA — caché jusqu'à la fin du scroll -->
|
||||
<a href="accueil.html" class="cta-btn" id="ctaBtn">
|
||||
<span class="cta-btn-shine"></span>
|
||||
<span data-i18n="intro.ctaBtn">Accéder au site</span>
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Sentinelles invisibles : leur scroll-position pilote la timeline -->
|
||||
<main class="scroll-stage" aria-hidden="true">
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
<section class="act"></section>
|
||||
</main>
|
||||
|
||||
<script src="js/translations.js"></script>
|
||||
@ -70,34 +103,12 @@
|
||||
}
|
||||
})();
|
||||
|
||||
/* Parallaxe souris : la vidéo et le bouton se décalent légèrement.
|
||||
--mx, --my sont des floats dans [-1, +1] mappés sur la position
|
||||
souris, avec easing. Sur mobile, on utilise l'orientation du device. */
|
||||
(function () {
|
||||
const root = document.documentElement;
|
||||
let targetX = 0, targetY = 0, currentX = 0, currentY = 0;
|
||||
const ease = 0.08;
|
||||
|
||||
function loop() {
|
||||
currentX += (targetX - currentX) * ease;
|
||||
currentY += (targetY - currentY) * ease;
|
||||
root.style.setProperty('--mx', currentX.toFixed(4));
|
||||
root.style.setProperty('--my', currentY.toFixed(4));
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
loop();
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
targetX = (e.clientX / window.innerWidth - 0.5) * 2;
|
||||
targetY = (e.clientY / window.innerHeight - 0.5) * 2;
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('deviceorientation', (e) => {
|
||||
if (e.gamma == null || e.beta == null) return;
|
||||
targetX = Math.max(-1, Math.min(1, e.gamma / 30));
|
||||
targetY = Math.max(-1, Math.min(1, (e.beta - 45) / 30));
|
||||
}, { passive: true });
|
||||
})();
|
||||
/* Scroll-hint : disparaît dès qu'on commence à scroller */
|
||||
window.addEventListener('scroll', () => {
|
||||
document.getElementById('scrollHint')?.classList.add('hidden');
|
||||
}, { once: true, passive: true });
|
||||
</script>
|
||||
|
||||
<script type="module" src="js/intro-scene.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
222
js/intro-scene.js
Normal file
222
js/intro-scene.js
Normal file
@ -0,0 +1,222 @@
|
||||
/* =========================================================================
|
||||
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();
|
||||
Loading…
Reference in New Issue
Block a user