site-mva-global-fret/index.html
MVA Global Fret eb01e75a3f Recalibrate intro line: shift LON0 to 150, bump rate to -6 deg/s
The pins were drifting east of the actual cities — Paris ended up over
Italy and Antananarivo in the Indian Ocean. Linear regression over five
calibration frames (visible central longitude at t=0,10,15,18,22) gives
LON0~151, rate~-6.0, which I round to 150 and -6.

Also expose all knobs as URL params (?lon0=&rate=&cx=&cy=&r=) and add a
?debug=1 overlay that draws the projected sphere outline + equator +
30 deg meridian grid, so the calibration can be eyeballed live.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 23:59:01 +02:00

313 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVA Global Fret — Bienvenue</title>
<meta name="description" content="MVA Global Fret — Le pont aérien entre Paris et Antananarivo.">
<link rel="icon" type="image/png" href="PNG MVA GLOBAL FRET.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
</head>
<body class="parallax-body">
<header class="parallax-header">
<div class="parallax-logo" aria-label="MVA Global Fret">
<img src="PNG MVA GLOBAL FRET.png" alt="MVA Global Fret">
<span>MVA Global Fret</span>
</div>
<div class="lang-switcher" role="group" aria-label="Choisir la langue">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</header>
<main class="stage">
<video class="layer layer-video" id="earthVideo"
autoplay loop muted playsinline preload="auto"
poster="videos/earth-poster.jpg">
<source src="videos/earth-rotation.mp4" type="video/mp4">
</video>
<div class="layer layer-tint"></div>
<svg class="layer layer-route" id="routeSvg"
viewBox="0 0 1280 720" preserveAspectRatio="xMidYMid slice"
xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="redGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<linearGradient id="redLine" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff5b6e" stop-opacity="0.7"/>
<stop offset="50%" stop-color="#ff2a3d" stop-opacity="1"/>
<stop offset="100%" stop-color="#ff5b6e" stop-opacity="0.7"/>
</linearGradient>
</defs>
<g id="debugLayer" style="display:none">
<circle id="dbgEarth" r="0" cx="0" cy="0" fill="none" stroke="#00e5ff" stroke-width="1.5" opacity="0.7"/>
<path id="dbgEquator" d="" stroke="#ffea00" stroke-width="1" fill="none" opacity="0.7" stroke-dasharray="4 4"/>
<g id="dbgGrid"></g>
<text id="dbgLabel" x="20" y="40" fill="#00e5ff" font-family="monospace" font-size="18" font-weight="700"></text>
</g>
<g id="routeGroup" opacity="0">
<path id="routeHalo" d="" stroke="#ff2a3d" stroke-width="9" fill="none" opacity="0.3" filter="url(#redGlow)"/>
<path id="routeLine" d="" stroke="url(#redLine)" stroke-width="2" fill="none" stroke-linecap="round" filter="url(#redGlow)"/>
<circle id="pulseDot" r="4" fill="#fff" filter="url(#redGlow)"/>
<g id="pinParis">
<circle r="10" fill="#ff2a3d" opacity="0.18"/>
<circle r="5" fill="#ff2a3d" opacity="0.55" filter="url(#redGlow)"/>
<circle r="2.5" fill="#fff"/>
</g>
<g id="pinTana">
<circle r="10" fill="#ff2a3d" opacity="0.18"/>
<circle r="5" fill="#ff2a3d" opacity="0.55" filter="url(#redGlow)"/>
<circle r="2.5" fill="#fff"/>
</g>
</g>
</svg>
<a href="accueil.html" class="cta-btn">
<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>
</main>
<script src="js/translations.js"></script>
<script>
/* i18n minimal ------------------------------------------------------- */
(function () {
const lang = localStorage.getItem('mva-lang') || 'fr';
applyLang(lang);
document.querySelectorAll('.lang-switcher button').forEach(btn => {
btn.addEventListener('click', () => applyLang(btn.dataset.lang));
});
function applyLang(l) {
document.documentElement.lang = l;
localStorage.setItem('mva-lang', l);
document.querySelectorAll('.lang-switcher button').forEach(b =>
b.classList.toggle('active', b.dataset.lang === l)
);
const t = window.translations?.[l];
if (!t) return;
document.querySelectorAll('[data-i18n]').forEach(el => {
const keys = el.dataset.i18n.split('.');
let v = t;
for (const k of keys) v = v?.[k];
if (v != null) el.textContent = v;
});
}
})();
/* 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
-------------------------------------------------------------------- */
(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 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];
}
const parisVec = toCart(PARIS.lat, PARIS.lon);
const tanaVec = toCart(TANA.lat, TANA.lon);
// 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);
})();
</script>
</body>
</html>