Swap video bg for aerial Antananarivo image, drive plane with the mouse
User-supplied aerial illustration of Antananarivo (Lake Anosy + Rova hill + surrounding city, 1920px wide JPG, 241 KB) replaces the parachute-drop video as the static intro backdrop. The video files are deleted from /videos. The plane no longer orbits a scroll timeline. Now: - Page is a single viewport, no scroll, no act labels, no scroll hint. - Mouse X (0..1) drives plane.position.x from -16 (offscreen left) to +16 (offscreen right), with plane.position.y descending from +5 to -2 — so the plane enters from the upper-left and exits lower-right. - Pitch/roll/yaw lerp toward small targets that depend on mouse X, so the plane banks naturally as it crosses. - Background image gets a softer mouse parallax (-16/-10px) via the existing --mx/--my CSS vars, now updated from intro-scene.js. - Three.js cloud spheres are gone; the photo is the entire backdrop. - ScrollTrigger + the GSAP timeline are removed; the page no longer needs gsap at all (the script tag stayed for now in case it comes back, but the dependency could be dropped on a future pass). - CTA button is back to plain visible/centered, no reveal animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d99e2a5fc1
commit
eba88207c4
BIN
assets/antananarivo-bg.jpg
Normal file
BIN
assets/antananarivo-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
114
css/parallax.css
114
css/parallax.css
@ -1,7 +1,8 @@
|
|||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
PARALLAX INTRO — MVA Global Fret
|
PARALLAX INTRO — MVA Global Fret
|
||||||
Page scrollable (4 viewports) avec scène Three.js fixée. La timeline
|
Page unique. Photo aérienne d'Antananarivo en fond + avion 3D piloté
|
||||||
pilote la caméra et l'avion 3D. À 100% du scroll, le bouton CTA apparaît.
|
par la souris (entre par le haut-gauche, sort par la droite). Bouton
|
||||||
|
CTA doré centré, toujours visible.
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -22,12 +23,10 @@ html, body {
|
|||||||
background: var(--navy-deep);
|
background: var(--navy-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
html { scroll-behavior: smooth; }
|
.parallax-body {
|
||||||
|
|
||||||
body.parallax-body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── HEADER ─────────────────────────────────────────────────────────────── */
|
/* ── HEADER ─────────────────────────────────────────────────────────────── */
|
||||||
@ -86,16 +85,12 @@ body.parallax-body {
|
|||||||
}
|
}
|
||||||
.lang-switcher button:hover:not(.active) { color: var(--white); }
|
.lang-switcher button:hover:not(.active) { color: var(--white); }
|
||||||
|
|
||||||
/* ── SCÈNE FIXE (vidéo + voile + canvas Three.js) ────────────────────────
|
/* ── STAGE ──────────────────────────────────────────────────────────────── */
|
||||||
.stage-fixed reste plein-écran pendant que la page se déroule derrière.
|
.stage {
|
||||||
*/
|
position: relative;
|
||||||
.stage-fixed {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer {
|
.layer {
|
||||||
@ -107,90 +102,20 @@ body.parallax-body {
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-video {
|
/* Photo de fond — léger zoom + parallaxe souris discrète */
|
||||||
|
.layer-bg {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
transform: translate(calc(var(--mx) * -22px), calc(var(--my) * -22px)) scale(1.06);
|
transform: translate(calc(var(--mx) * -16px), calc(var(--my) * -10px)) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-tint {
|
.layer-tint {
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.55) 100%),
|
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.32) 100%),
|
||||||
linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%);
|
linear-gradient(180deg, transparent 55%, rgba(5, 5, 24, 0.40) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-three {
|
.layer-three { display: block; z-index: 2; }
|
||||||
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é ──────────────────────────────────────────────────── */
|
/* ── BOUTON CTA centré ──────────────────────────────────────────────────── */
|
||||||
.cta-btn {
|
.cta-btn {
|
||||||
@ -198,7 +123,7 @@ body.parallax-body {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 50;
|
z-index: 10;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@ -219,11 +144,7 @@ body.parallax-body {
|
|||||||
inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||||
transition: box-shadow 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
|
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);
|
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 {
|
.cta-btn:hover {
|
||||||
transform: translate(-50%, -50%) scale(1.04);
|
transform: translate(-50%, -50%) scale(1.04);
|
||||||
@ -275,13 +196,8 @@ body.parallax-body {
|
|||||||
padding: 16px 36px;
|
padding: 16px 36px;
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
}
|
}
|
||||||
.act-label { font-size: 0.85rem; bottom: 22%; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.cta-btn::after { animation: none; }
|
.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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
45
index.html
45
index.html
@ -18,7 +18,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
<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">
|
<link rel="stylesheet" href="css/parallax.css">
|
||||||
|
|
||||||
<!-- Three.js (ESM via importmap) + addons (GLTFLoader, etc.) -->
|
<!-- Three.js (ESM via importmap) + addons (GLTFLoader) -->
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
@ -27,9 +27,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
</head>
|
||||||
<body class="parallax-body">
|
<body class="parallax-body">
|
||||||
|
|
||||||
@ -45,43 +42,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Couches fixes : vidéo + voile + canvas Three.js (le plan 3D vole dessus) -->
|
<main class="stage">
|
||||||
<div class="stage-fixed">
|
|
||||||
<video class="layer layer-video" id="introVideo"
|
|
||||||
autoplay loop muted playsinline preload="auto"
|
|
||||||
poster="videos/parachute-poster.jpg">
|
|
||||||
<source src="videos/parachute-drop.mp4" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
|
|
||||||
|
<!-- Photo aérienne d'Antananarivo en fond -->
|
||||||
|
<img class="layer layer-bg" src="assets/antananarivo-bg.jpg" alt="" aria-hidden="true">
|
||||||
|
|
||||||
|
<!-- Voile très léger pour la lisibilité du bouton -->
|
||||||
<div class="layer layer-tint"></div>
|
<div class="layer layer-tint"></div>
|
||||||
|
|
||||||
|
<!-- Canvas Three.js : avion piloté par la souris -->
|
||||||
<canvas id="three-canvas" class="layer layer-three"></canvas>
|
<canvas id="three-canvas" class="layer layer-three"></canvas>
|
||||||
|
|
||||||
<!-- Étiquettes des actes — apparaissent / disparaissent selon le scroll -->
|
<!-- Bouton CTA centré -->
|
||||||
<div class="act-label" id="label-paris"><span>Paris · CDG</span></div>
|
<a href="accueil.html" class="cta-btn">
|
||||||
<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 class="cta-btn-shine"></span>
|
||||||
<span data-i18n="intro.ctaBtn">Accéder au site</span>
|
<span data-i18n="intro.ctaBtn">Accéder au site</span>
|
||||||
<i class="fa-solid fa-arrow-right"></i>
|
<i class="fa-solid fa-arrow-right"></i>
|
||||||
</a>
|
</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>
|
</main>
|
||||||
|
|
||||||
<script src="js/translations.js"></script>
|
<script src="js/translations.js"></script>
|
||||||
@ -109,11 +87,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* 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>
|
||||||
|
|
||||||
<script type="module" src="js/intro-scene.js"></script>
|
<script type="module" src="js/intro-scene.js"></script>
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
INTRO SCENE — MVA Global Fret
|
INTRO SCENE — MVA Global Fret
|
||||||
Three.js scene driven by scroll. The cargo airliner is a GLTF model
|
Photo aérienne d'Antananarivo en fond, avion 3D piloté par la souris :
|
||||||
loaded at runtime; GSAP + ScrollTrigger animate the camera position
|
il entre par le haut-gauche quand la souris est à gauche, traverse
|
||||||
and exposition labels as the page scrolls. The CTA fades in at the
|
l'écran à mesure qu'elle bouge, et sort par la droite. Pas de scroll.
|
||||||
end of the timeline.
|
|
||||||
|
|
||||||
3D model credit (CC-BY): "Airplane" by Poly by Google
|
3D model credit (CC-BY 3.0) : « Airplane » by Poly by Google
|
||||||
https://poly.pizza/m/a3XrQkLNna9
|
https://poly.pizza/m/a3XrQkLNna9
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
|
||||||
const { gsap, ScrollTrigger } = window;
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
/* ── Renderer & camera ─────────────────────────────────────────────────── */
|
/* ── Renderer & camera ─────────────────────────────────────────────────── */
|
||||||
const canvas = document.getElementById('three-canvas');
|
const canvas = document.getElementById('three-canvas');
|
||||||
const renderer = new THREE.WebGLRenderer({
|
const renderer = new THREE.WebGLRenderer({
|
||||||
@ -27,8 +23,8 @@ renderer.toneMappingExposure = 1.05;
|
|||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 1000);
|
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera.position.set(0, 1.4, 18);
|
camera.position.set(0, 0, 22);
|
||||||
camera.lookAt(0, 0, 0);
|
camera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
/* ── Lighting (golden-hour fill) ───────────────────────────────────────── */
|
/* ── Lighting (golden-hour fill) ───────────────────────────────────────── */
|
||||||
@ -51,9 +47,6 @@ loader.load(
|
|||||||
'assets/airplane.glb',
|
'assets/airplane.glb',
|
||||||
(gltf) => {
|
(gltf) => {
|
||||||
const model = gltf.scene;
|
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 box = new THREE.Box3().setFromObject(model);
|
||||||
const size = box.getSize(new THREE.Vector3());
|
const size = box.getSize(new THREE.Vector3());
|
||||||
const center = box.getCenter(new THREE.Vector3());
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
@ -63,95 +56,71 @@ loader.load(
|
|||||||
wrapper.add(model);
|
wrapper.add(model);
|
||||||
const targetSize = 8.5;
|
const targetSize = 8.5;
|
||||||
wrapper.scale.setScalar(targetSize / Math.max(size.x, size.y, size.z));
|
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
|
/* Pivote le modèle pour que le nez pointe vers la droite (+X) */
|
||||||
nose faces the camera at the start of the timeline. */
|
wrapper.rotation.y = -Math.PI / 2;
|
||||||
wrapper.rotation.y = Math.PI / 2;
|
|
||||||
planeHolder.add(wrapper);
|
planeHolder.add(wrapper);
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
(err) => console.error('Failed to load airplane.glb:', err)
|
(err) => console.error('Failed to load airplane.glb:', err)
|
||||||
);
|
);
|
||||||
|
|
||||||
/* ── Cloud sprites ─────────────────────────────────────────────────────── */
|
/* ── Souris ─────────────────────────────────────────────────────────────
|
||||||
const clouds = new THREE.Group();
|
- mouseX, mouseY : 0..1 normalisés
|
||||||
const cloudMat = new THREE.MeshStandardMaterial({
|
- Sur écran sans souris (touch/mobile), valeur lente d'auto-scroll
|
||||||
color: 0xffffff, roughness: 1.0, metalness: 0, transparent: true, opacity: 0.85
|
*/
|
||||||
});
|
const mouse = { tx: 0.0, ty: 0.5, x: 0.0, y: 0.5 };
|
||||||
for (let i = 0; i < 14; i++) {
|
|
||||||
const cl = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 10), cloudMat);
|
window.addEventListener('mousemove', (e) => {
|
||||||
const r = 18 + Math.random() * 14;
|
mouse.tx = Math.max(0, Math.min(1, e.clientX / window.innerWidth));
|
||||||
const a = Math.random() * Math.PI * 2;
|
mouse.ty = Math.max(0, Math.min(1, e.clientY / window.innerHeight));
|
||||||
cl.position.set(Math.cos(a) * r, (Math.random() - 0.4) * 9, Math.sin(a) * r - 8);
|
}, { passive: true });
|
||||||
cl.scale.set(1.6 + Math.random() * 1.4, 0.9 + Math.random() * 0.6, 1.6 + Math.random() * 1.4);
|
|
||||||
clouds.add(cl);
|
/* 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));
|
||||||
}
|
}
|
||||||
scene.add(clouds);
|
}, { passive: true });
|
||||||
|
|
||||||
/* ── Scroll-driven progress ────────────────────────────────────────────── */
|
/* Variable CSS pour la parallaxe douce de la photo de fond */
|
||||||
const state = { progress: 0 };
|
const root = document.documentElement;
|
||||||
|
|
||||||
ScrollTrigger.create({
|
|
||||||
trigger: '.scroll-stage',
|
|
||||||
start: 'top top',
|
|
||||||
end: 'bottom bottom',
|
|
||||||
scrub: 0.6,
|
|
||||||
onUpdate: self => { state.progress = self.progress; }
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Cross-fade des étiquettes d'acte */
|
|
||||||
const labelTl = gsap.timeline({
|
|
||||||
scrollTrigger: { trigger: '.scroll-stage', start: 'top top', end: 'bottom bottom', scrub: 0.4 }
|
|
||||||
});
|
|
||||||
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 reveal — last ~10% of scroll */
|
|
||||||
gsap.fromTo('#ctaBtn',
|
|
||||||
{ opacity: 0, scale: 0.85 },
|
|
||||||
{
|
|
||||||
opacity: 1, scale: 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 ───────────────────────────────────────────────────────── */
|
/* ── Render loop ───────────────────────────────────────────────────────── */
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
const t = clock.getElapsedTime();
|
const t = clock.getElapsedTime();
|
||||||
const p = state.progress;
|
|
||||||
|
|
||||||
/* Camera arc around the plane */
|
/* Lerp doux vers la cible souris */
|
||||||
const camAngle = -0.6 + p * 1.7;
|
mouse.x += (mouse.tx - mouse.x) * 0.06;
|
||||||
const camRadius = 18 - p * 6 + Math.sin(p * Math.PI) * -3;
|
mouse.y += (mouse.ty - mouse.y) * 0.06;
|
||||||
const camHeight = 1.4 + Math.sin(p * Math.PI) * 1.8 - p * 0.6;
|
|
||||||
|
|
||||||
camera.position.set(
|
/* Mappe sur les variables CSS (parallaxe légère du fond) */
|
||||||
Math.sin(camAngle) * camRadius,
|
root.style.setProperty('--mx', ((mouse.x - 0.5) * 2).toFixed(4));
|
||||||
camHeight,
|
root.style.setProperty('--my', ((mouse.y - 0.5) * 2).toFixed(4));
|
||||||
Math.cos(camAngle) * camRadius
|
|
||||||
);
|
|
||||||
camera.lookAt(0, 0, 0);
|
|
||||||
|
|
||||||
/* Plane bob + roll */
|
/* Position de l'avion :
|
||||||
planeHolder.position.y = Math.sin(t * 0.9) * 0.15;
|
- mouse.x = 0 → arrive haut-gauche (hors champ)
|
||||||
planeHolder.rotation.z = Math.sin(t * 0.5) * 0.04 + (p - 0.5) * 0.18;
|
- mouse.x = 0.5 → centré
|
||||||
planeHolder.rotation.y = Math.sin(t * 0.4) * 0.025;
|
- 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;
|
||||||
|
|
||||||
/* Clouds slow rotation */
|
planeHolder.position.set(px, py + bob, 0);
|
||||||
clouds.rotation.y = t * 0.04 - p * 0.6;
|
|
||||||
clouds.position.y = Math.sin(t * 0.3) * 0.4;
|
/* 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);
|
renderer.render(scene, camera);
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
Loading…
Reference in New Issue
Block a user