From c52ac514b1b74a60125095fcea9f9b95c71d1b0f Mon Sep 17 00:00:00 2001 From: MVA Global Fret Date: Tue, 5 May 2026 00:04:37 +0200 Subject: [PATCH] Drop the red Paris-Tana line, add mouse parallax to the Earth video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracking the line against the rotating globe was finicky and never quite aligned. Strip out the SVG route, the great-circle slerp, the 3D projection and the debug overlay — the rotating Earth alone is the visual now. Re-add a subtle mouse parallax: the video shifts up to 22px against the cursor, the CTA button tilts 8px the opposite way for a 3D feel. On mobile, deviceorientation drives the same vars. Co-Authored-By: Claude Opus 4.6 --- css/parallax.css | 23 +++-- index.html | 253 +++++------------------------------------------ 2 files changed, 37 insertions(+), 239 deletions(-) diff --git a/css/parallax.css b/css/parallax.css index 9bdd3dd..6dc8109 100644 --- a/css/parallax.css +++ b/css/parallax.css @@ -11,6 +11,8 @@ --gold-light: #e0c98a; --red: #ff2a3d; --white: #fff; + --mx: 0; + --my: 0; } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -90,19 +92,21 @@ html, body { overflow: hidden; } -/* Video et SVG partagent EXACTEMENT le même cadrage object-fit:cover. - C'est ce qui garantit que la ligne 3D reste alignée avec la Terre. */ .layer { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; + 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; + transform: translate(calc(var(--mx) * -22px), calc(var(--my) * -22px)) scale(1.06); } .layer-tint { @@ -111,16 +115,16 @@ html, body { linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%); } -.layer-route { - display: block; -} - /* ── BOUTON CTA centré ──────────────────────────────────────────────────── */ .cta-btn { position: absolute; top: 50%; left: 50%; - transform: translate(-50%, -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; display: inline-flex; align-items: center; @@ -145,7 +149,10 @@ html, body { } .cta-btn:hover { - transform: translate(-50%, -50%) scale(1.04); + transform: translate( + calc(-50% + var(--mx) * 8px), + calc(-50% + var(--my) * 8px) + ) scale(1.04); box-shadow: 0 28px 75px rgba(197, 165, 90, 0.7), 0 0 0 12px rgba(197, 165, 90, 0.12), diff --git a/index.html b/index.html index 65953ee..cac4897 100644 --- a/index.html +++ b/index.html @@ -36,49 +36,6 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Accéder au site @@ -113,199 +70,33 @@ } })(); - /* Ligne rouge 3D synchronisée à la rotation de la Terre -------------- - Calibration empirique sur la vidéo earth-rotation.mp4 : - - Centre de la Terre dans le frame 1280×720 : (640, 360) - - Rayon visible : 340 px - - Longitude centrale à t=0 : 150°E - - Vitesse de rotation : -6°/s (apparent) - Override en URL : ?lon0=150&rate=-6&debug=1 - -------------------------------------------------------------------- */ + /* 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 params = new URLSearchParams(location.search); - const EARTH_CX = +params.get('cx') || 640; - const EARTH_CY = +params.get('cy') || 360; - const EARTH_R = +params.get('r') || 340; - const LON0 = parseFloat(params.get('lon0') ?? '150'); - const ROT_RATE = parseFloat(params.get('rate') ?? '-6'); - const DEBUG = params.has('debug'); + const root = document.documentElement; + let targetX = 0, targetY = 0, currentX = 0, currentY = 0; + const ease = 0.08; - const PARIS = { lat: 48.85, lon: 2.35 }; - const TANA = { lat: -18.9, lon: 47.5 }; - - const video = document.getElementById('earthVideo'); - const routeGroup = document.getElementById('routeGroup'); - const halo = document.getElementById('routeHalo'); - const line = document.getElementById('routeLine'); - const pulseDot = document.getElementById('pulseDot'); - const pinParis = document.getElementById('pinParis'); - const pinTana = document.getElementById('pinTana'); - - const DEG = Math.PI / 180; - - // 3D unit-sphere vector (y axis = north pole, rotation around y) - function toCart(latDeg, lonDeg) { - const la = latDeg * DEG, lo = lonDeg * DEG; - return [Math.cos(la) * Math.sin(lo), - Math.sin(la), - Math.cos(la) * Math.cos(lo)]; - } - function rotateY(v, angDeg) { - const a = angDeg * DEG, c = Math.cos(a), s = Math.sin(a); - return [v[0]*c + v[2]*s, v[1], -v[0]*s + v[2]*c]; - } - function project(v) { - return { - x: EARTH_CX + v[0] * EARTH_R, - y: EARTH_CY - v[1] * EARTH_R, - z: v[2] // > 0 = front side - }; - } - // Slerp on great circle - function slerp(a, b, t) { - const dot = Math.max(-1, Math.min(1, a[0]*b[0] + a[1]*b[1] + a[2]*b[2])); - const omega = Math.acos(dot); - if (omega < 1e-6) return [a[0], a[1], a[2]]; - const s = Math.sin(omega); - const c1 = Math.sin((1 - t) * omega) / s; - const c2 = Math.sin(t * omega) / s; - return [a[0]*c1 + b[0]*c2, a[1]*c1 + b[1]*c2, a[2]*c1 + b[2]*c2]; + 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(); - const parisVec = toCart(PARIS.lat, PARIS.lon); - const tanaVec = toCart(TANA.lat, TANA.lon); + window.addEventListener('mousemove', (e) => { + targetX = (e.clientX / window.innerWidth - 0.5) * 2; + targetY = (e.clientY / window.innerHeight - 0.5) * 2; + }, { passive: true }); - // Pre-compute great-circle samples in original (unrotated) space - const SAMPLES = 60; - const arcVecs = []; - for (let i = 0; i <= SAMPLES; i++) arcVecs.push(slerp(parisVec, tanaVec, i / SAMPLES)); - - // Debug overlay : projected sphere outline + equator + longitude grid - const dbgLayer = document.getElementById('debugLayer'); - const dbgEarth = document.getElementById('dbgEarth'); - const dbgEquator = document.getElementById('dbgEquator'); - const dbgGrid = document.getElementById('dbgGrid'); - const dbgLabel = document.getElementById('dbgLabel'); - if (DEBUG) { - dbgLayer.style.display = ''; - dbgEarth.setAttribute('cx', EARTH_CX); - dbgEarth.setAttribute('cy', EARTH_CY); - dbgEarth.setAttribute('r', EARTH_R); - } - function buildArcPath(vecs, rotAngle) { - let d = '', open = false; - for (const v of vecs) { - const p = project(rotateY(v, rotAngle)); - if (p.z > 0) { - d += (open ? ' L ' : 'M ') + p.x.toFixed(1) + ' ' + p.y.toFixed(1); - open = true; - } else { open = false; } - } - return d; - } - // Pre-compute equator + meridians (every 30°) - const eqVecs = []; - for (let i = 0; i <= 180; i++) eqVecs.push(toCart(0, i * 2 - 180)); - const meridianSets = []; - for (let lon = -180; lon < 180; lon += 30) { - const set = []; - for (let lat = -90; lat <= 90; lat += 5) set.push(toCart(lat, lon)); - meridianSets.push(set); - if (DEBUG) { - const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - p.setAttribute('stroke', '#ffea00'); - p.setAttribute('stroke-width', '0.6'); - p.setAttribute('fill', 'none'); - p.setAttribute('opacity', '0.5'); - p.setAttribute('stroke-dasharray', '3 3'); - p.dataset.lon = lon; - dbgGrid.appendChild(p); - } - } - - function update() { - // Time source : video.currentTime (loops automatically). Fallback : Date.now() - const t = (video && !isNaN(video.currentTime)) ? video.currentTime : (performance.now() / 1000); - - // We rotate the world by -(LON0 + ROT_RATE * t) so that the visible - // central longitude at time t equals (LON0 + ROT_RATE * t). - const rotAngle = -(LON0 + ROT_RATE * t); - - const pPar = project(rotateY(parisVec, rotAngle)); - const pTan = project(rotateY(tanaVec, rotAngle)); - - // Build the visible portion of the arc - let pathD = ''; - let allPoints = []; - let visibleCount = 0; - for (const v of arcVecs) { - const p = project(rotateY(v, rotAngle)); - allPoints.push(p); - if (p.z > 0) visibleCount++; - } - if (visibleCount === arcVecs.length) { - // Fully visible → smooth single path - pathD = 'M ' + allPoints[0].x.toFixed(1) + ' ' + allPoints[0].y.toFixed(1); - for (let i = 1; i < allPoints.length; i++) { - pathD += ' L ' + allPoints[i].x.toFixed(1) + ' ' + allPoints[i].y.toFixed(1); - } - } else { - // Partial → only emit the consecutive visible run(s) - let segOpen = false; - for (let i = 0; i < allPoints.length; i++) { - const p = allPoints[i]; - if (p.z > 0) { - if (!segOpen) { - pathD += (pathD ? ' M ' : 'M ') + p.x.toFixed(1) + ' ' + p.y.toFixed(1); - segOpen = true; - } else { - pathD += ' L ' + p.x.toFixed(1) + ' ' + p.y.toFixed(1); - } - } else { - segOpen = false; - } - } - } - line.setAttribute('d', pathD); - halo.setAttribute('d', pathD); - - // Pin positions + visibility - pinParis.setAttribute('transform', 'translate(' + pPar.x.toFixed(1) + ' ' + pPar.y.toFixed(1) + ')'); - pinTana .setAttribute('transform', 'translate(' + pTan.x.toFixed(1) + ' ' + pTan.y.toFixed(1) + ')'); - pinParis.style.opacity = pPar.z > 0 ? 1 : 0; - pinTana .style.opacity = pTan.z > 0 ? 1 : 0; - - // Pulse dot — travels Paris → Tana along visible portion in 3.6s loops - const pulseT = (t / 3.6) % 1; - let idx = Math.floor(pulseT * SAMPLES); - if (idx > SAMPLES) idx = SAMPLES; - const pPulse = allPoints[idx]; - if (pPulse && pPulse.z > 0) { - pulseDot.setAttribute('cx', pPulse.x.toFixed(1)); - pulseDot.setAttribute('cy', pPulse.y.toFixed(1)); - pulseDot.style.opacity = Math.min(1, pPulse.z * 2); - } else { - pulseDot.style.opacity = 0; - } - - // Debug overlay update - if (DEBUG) { - dbgEquator.setAttribute('d', buildArcPath(eqVecs, rotAngle)); - const meridianPaths = dbgGrid.children; - for (let i = 0; i < meridianPaths.length; i++) { - meridianPaths[i].setAttribute('d', buildArcPath(meridianSets[i], rotAngle)); - } - const central = ((LON0 + ROT_RATE * t) % 360 + 540) % 360 - 180; - dbgLabel.textContent = 't=' + t.toFixed(1) + 's central=' + central.toFixed(1) + '° rate=' + ROT_RATE + '°/s lon0=' + LON0; - } - - // Group opacity = fade out when both pins are on the back side - const visFactor = Math.max(0, Math.min(1, (Math.max(pPar.z, pTan.z) + 0.05) * 4)); - routeGroup.setAttribute('opacity', visFactor.toFixed(2)); - - requestAnimationFrame(update); - } - requestAnimationFrame(update); + 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 }); })();