Self-host rotating Earth video, project red Paris-Tana line in 3D
Replace the Pexels stock clip with a higher-quality Earth-from-space loop hosted in /videos (720p, 60s, slowed 2x, 12 MB). The static SVG arc is gone — the route is now computed every frame from a 3D projection that follows the globe's rotation: - France (48.85N, 2.35E) and Antananarivo (-18.9N, 47.5E) are placed on a unit sphere, then rotated around the Y axis to match the apparent central longitude of the video at each frame (LON0=140 deg, omega =-5.75 deg/s, calibrated empirically from sample frames). - A great-circle arc is sampled with slerp and projected orthographically; only the front-side portion (z > 0) is drawn. - Pins fade out when their city rotates behind the globe; the whole group fades out when both endpoints are on the back side. Mouse parallax dropped — it would desync the SVG from the video and break alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e23cc9ee33
commit
8033c0fd02
@ -1,7 +1,7 @@
|
|||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
PARALLAX INTRO — MVA Global Fret
|
PARALLAX INTRO — MVA Global Fret
|
||||||
Page unique, fixe (pas de scroll). Vidéo Terre depuis l'espace + ligne rouge
|
Page unique, fixe (pas de scroll). Vidéo Terre rotative + ligne rouge 3D
|
||||||
Paris↔Antananarivo + bouton centré. Bouge à la souris.
|
Paris ↔ Antananarivo (suit la rotation du globe) + bouton centré.
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -11,8 +11,6 @@
|
|||||||
--gold-light: #e0c98a;
|
--gold-light: #e0c98a;
|
||||||
--red: #ff2a3d;
|
--red: #ff2a3d;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
--mx: 0;
|
|
||||||
--my: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@ -92,54 +90,37 @@ html, body {
|
|||||||
overflow: hidden;
|
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 {
|
.layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -10% -10%;
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────────────────
|
|
||||||
COUCHE 1 — VIDÉO Terre depuis l'espace (Pexels 854275, libre)
|
|
||||||
Bouge le PLUS à la souris (effet de profondeur).
|
|
||||||
────────────────────────────────────────────────────────────────────────── */
|
|
||||||
.layer-video {
|
.layer-video {
|
||||||
width: 120%;
|
|
||||||
height: 120%;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
filter: brightness(0.85) saturate(1.1) hue-rotate(-10deg);
|
|
||||||
transform: translate(calc(var(--mx) * -45px), calc(var(--my) * -45px)) scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────────────────
|
|
||||||
COUCHE 2 — Voile bleu nuit (palette MVA)
|
|
||||||
────────────────────────────────────────────────────────────────────────── */
|
|
||||||
.layer-tint {
|
.layer-tint {
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at center, rgba(20, 20, 50, 0.35) 0%, rgba(5, 5, 24, 0.85) 100%),
|
radial-gradient(ellipse at center, transparent 0%, rgba(5, 5, 24, 0.55) 100%),
|
||||||
linear-gradient(180deg, rgba(10, 10, 30, 0.55) 0%, rgba(5, 5, 24, 0.7) 100%);
|
linear-gradient(180deg, rgba(5, 5, 24, 0.25) 0%, rgba(5, 5, 24, 0.5) 100%);
|
||||||
inset: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────────────────
|
|
||||||
COUCHE 4 — Route Paris ↔ Antananarivo (ligne rouge fine)
|
|
||||||
Bouge légèrement (entre vidéo et bouton), donne une 3ème profondeur.
|
|
||||||
────────────────────────────────────────────────────────────────────────── */
|
|
||||||
.layer-route {
|
.layer-route {
|
||||||
inset: 0;
|
display: block;
|
||||||
transform: translate(calc(var(--mx) * -12px), calc(var(--my) * -12px));
|
|
||||||
}
|
}
|
||||||
.layer-route svg { width: 100%; height: 100%; display: block; }
|
|
||||||
|
|
||||||
/* ──────────────────────────────────────────────────────────────────────────
|
/* ── BOUTON CTA centré ──────────────────────────────────────────────────── */
|
||||||
BOUTON CTA centré — "Accéder au site"
|
|
||||||
Bouge en sens INVERSE des couches → effet de tilt.
|
|
||||||
────────────────────────────────────────────────────────────────────────── */
|
|
||||||
.cta-btn {
|
.cta-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -159,22 +140,18 @@ html, body {
|
|||||||
0 20px 60px rgba(197, 165, 90, 0.55),
|
0 20px 60px rgba(197, 165, 90, 0.55),
|
||||||
0 0 0 0 rgba(197, 165, 90, 0.4),
|
0 0 0 0 rgba(197, 165, 90, 0.4),
|
||||||
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),
|
||||||
/* Bouge légèrement en sens opposé des couches arrière (effet 3D) */
|
transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
transform: translate(
|
|
||||||
calc(-50% + var(--mx) * 8px),
|
|
||||||
calc(-50% + var(--my) * 8px)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-btn:hover {
|
.cta-btn:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.04);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 28px 75px rgba(197, 165, 90, 0.7),
|
0 28px 75px rgba(197, 165, 90, 0.7),
|
||||||
0 0 0 12px rgba(197, 165, 90, 0.12),
|
0 0 0 12px rgba(197, 165, 90, 0.12),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Effet shine au hover */
|
|
||||||
.cta-btn-shine {
|
.cta-btn-shine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: -120%;
|
top: 0; left: -120%;
|
||||||
@ -192,7 +169,6 @@ html, body {
|
|||||||
}
|
}
|
||||||
.cta-btn:hover i { transform: translateX(8px); }
|
.cta-btn:hover i { transform: translateX(8px); }
|
||||||
|
|
||||||
/* Pulse continu autour du bouton */
|
|
||||||
.cta-btn::after {
|
.cta-btn::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -203,8 +179,8 @@ html, body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@keyframes ctaPulse {
|
@keyframes ctaPulse {
|
||||||
0% { transform: scale(1); opacity: 0.7; }
|
0% { transform: scale(1); opacity: 0.7; }
|
||||||
100% { transform: scale(1.18); opacity: 0; }
|
100% { transform: scale(1.18); opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── RESPONSIVE ─────────────────────────────────────────────────────────── */
|
/* ── RESPONSIVE ─────────────────────────────────────────────────────────── */
|
||||||
@ -222,5 +198,4 @@ html, body {
|
|||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.cta-btn::after { animation: none; }
|
.cta-btn::after { animation: none; }
|
||||||
.layer-video, .layer-route, .cta-btn { transition: none; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
246
index.html
246
index.html
@ -28,73 +28,50 @@
|
|||||||
|
|
||||||
<main class="stage">
|
<main class="stage">
|
||||||
|
|
||||||
<!-- Couche 1 : vidéo Terre depuis l'espace (parallaxe la plus marquée) -->
|
<video class="layer layer-video" id="earthVideo"
|
||||||
<video class="layer layer-video"
|
|
||||||
autoplay loop muted playsinline preload="auto"
|
autoplay loop muted playsinline preload="auto"
|
||||||
poster="https://images.pexels.com/videos/854275/free-video-854275.jpg">
|
poster="videos/earth-poster.jpg">
|
||||||
<source src="https://videos.pexels.com/video-files/854275/854275-sd_960_540_30fps.mp4" type="video/mp4">
|
<source src="videos/earth-rotation.mp4" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<!-- Couche 2 : voile bleu pour cohérence palette + lisibilité -->
|
|
||||||
<div class="layer layer-tint"></div>
|
<div class="layer layer-tint"></div>
|
||||||
|
|
||||||
<!-- Couche 4 : route Paris ↔ Antananarivo (ligne rouge brillante + pulse) -->
|
<svg class="layer layer-route" id="routeSvg"
|
||||||
<div class="layer layer-route">
|
viewBox="0 0 1280 720" preserveAspectRatio="xMidYMid slice"
|
||||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="redGlow" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="redGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur stdDeviation="5" result="blur"/>
|
<feGaussianBlur stdDeviation="6" result="blur"/>
|
||||||
<feMerge>
|
<feMerge>
|
||||||
<feMergeNode in="blur"/>
|
<feMergeNode in="blur"/>
|
||||||
<feMergeNode in="SourceGraphic"/>
|
<feMergeNode in="SourceGraphic"/>
|
||||||
</feMerge>
|
</feMerge>
|
||||||
</filter>
|
</filter>
|
||||||
<linearGradient id="redLine" x1="0%" y1="0%" x2="100%" y2="100%">
|
<linearGradient id="redLine" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<stop offset="0%" stop-color="#ff5b6e" stop-opacity="0.55"/>
|
<stop offset="0%" stop-color="#ff5b6e" stop-opacity="0.7"/>
|
||||||
<stop offset="50%" stop-color="#ff2a3d" stop-opacity="1"/>
|
<stop offset="50%" stop-color="#ff2a3d" stop-opacity="1"/>
|
||||||
<stop offset="100%" stop-color="#ff5b6e" stop-opacity="0.55"/>
|
<stop offset="100%" stop-color="#ff5b6e" stop-opacity="0.7"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<!-- Halo flou autour de la ligne -->
|
<g id="routeGroup" opacity="0">
|
||||||
<path d="M 430 270 Q 620 90 910 600"
|
<path id="routeHalo" d="" stroke="#ff2a3d" stroke-width="9" fill="none" opacity="0.3" filter="url(#redGlow)"/>
|
||||||
stroke="#ff2a3d" stroke-width="10" fill="none"
|
<path id="routeLine" d="" stroke="url(#redLine)" stroke-width="2" fill="none" stroke-linecap="round" filter="url(#redGlow)"/>
|
||||||
opacity="0.25" filter="url(#redGlow)"/>
|
<circle id="pulseDot" r="4" fill="#fff" filter="url(#redGlow)"/>
|
||||||
|
|
||||||
<!-- Ligne fine principale -->
|
<g id="pinParis">
|
||||||
<path id="routePath" d="M 430 270 Q 620 90 910 600"
|
<circle r="10" fill="#ff2a3d" opacity="0.18"/>
|
||||||
stroke="url(#redLine)" stroke-width="1.5" fill="none"
|
<circle r="5" fill="#ff2a3d" opacity="0.55" filter="url(#redGlow)"/>
|
||||||
stroke-linecap="round"/>
|
<circle r="2.5" fill="#fff"/>
|
||||||
|
|
||||||
<!-- Pulse qui voyage de Paris vers Tana -->
|
|
||||||
<circle r="5" fill="#fff" filter="url(#redGlow)">
|
|
||||||
<animateMotion dur="3.6s" repeatCount="indefinite">
|
|
||||||
<mpath href="#routePath"/>
|
|
||||||
</animateMotion>
|
|
||||||
<animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.1;0.9;1" dur="3.6s" repeatCount="indefinite"/>
|
|
||||||
</circle>
|
|
||||||
<circle r="11" fill="#ff2a3d" opacity="0.6" filter="url(#redGlow)">
|
|
||||||
<animateMotion dur="3.6s" repeatCount="indefinite">
|
|
||||||
<mpath href="#routePath"/>
|
|
||||||
</animateMotion>
|
|
||||||
<animate attributeName="opacity" values="0;0.6;0.6;0" keyTimes="0;0.1;0.9;1" dur="3.6s" repeatCount="indefinite"/>
|
|
||||||
</circle>
|
|
||||||
|
|
||||||
<!-- Points fixes Paris et Antananarivo -->
|
|
||||||
<g transform="translate(430 270)">
|
|
||||||
<circle r="14" fill="#ff2a3d" opacity="0.18"/>
|
|
||||||
<circle r="6" fill="#ff2a3d" opacity="0.45" filter="url(#redGlow)"/>
|
|
||||||
<circle r="3" fill="#fff"/>
|
|
||||||
</g>
|
</g>
|
||||||
<g transform="translate(910 600)">
|
<g id="pinTana">
|
||||||
<circle r="14" fill="#ff2a3d" opacity="0.18"/>
|
<circle r="10" fill="#ff2a3d" opacity="0.18"/>
|
||||||
<circle r="6" fill="#ff2a3d" opacity="0.45" filter="url(#redGlow)"/>
|
<circle r="5" fill="#ff2a3d" opacity="0.55" filter="url(#redGlow)"/>
|
||||||
<circle r="3" fill="#fff"/>
|
<circle r="2.5" fill="#fff"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</g>
|
||||||
</div>
|
</svg>
|
||||||
|
|
||||||
<!-- Bouton centré : juste "Accéder au site" -->
|
|
||||||
<a href="accueil.html" class="cta-btn">
|
<a href="accueil.html" class="cta-btn">
|
||||||
<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>
|
||||||
@ -105,7 +82,7 @@
|
|||||||
|
|
||||||
<script src="js/translations.js"></script>
|
<script src="js/translations.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── i18n minimal ─────────────────────────────────────────────────────────
|
/* i18n minimal ------------------------------------------------------- */
|
||||||
(function () {
|
(function () {
|
||||||
const lang = localStorage.getItem('mva-lang') || 'fr';
|
const lang = localStorage.getItem('mva-lang') || 'fr';
|
||||||
applyLang(lang);
|
applyLang(lang);
|
||||||
@ -129,31 +106,144 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ── Mouse parallaxe avec easing ──────────────────────────────────────────
|
/* 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 : 140°E
|
||||||
|
- Vitesse de rotation : -5.75°/s (apparent)
|
||||||
|
La SVG utilise viewBox 1280×720 avec preserveAspectRatio="xMidYMid slice",
|
||||||
|
même comportement que le video object-fit:cover → alignement parfait.
|
||||||
|
-------------------------------------------------------------------- */
|
||||||
(function () {
|
(function () {
|
||||||
const root = document.documentElement;
|
const EARTH_CX = 640;
|
||||||
let targetX = 0, targetY = 0, currentX = 0, currentY = 0;
|
const EARTH_CY = 360;
|
||||||
const ease = 0.06;
|
const EARTH_R = 340;
|
||||||
|
const LON0 = 140; // central longitude visible à t=0
|
||||||
|
const ROT_RATE = -5.75; // °/s (négatif = view's central decreases)
|
||||||
|
|
||||||
function loop() {
|
const PARIS = { lat: 48.85, lon: 2.35 };
|
||||||
currentX += (targetX - currentX) * ease;
|
const TANA = { lat: -18.9, lon: 47.5 };
|
||||||
currentY += (targetY - currentY) * ease;
|
|
||||||
root.style.setProperty('--mx', currentX.toFixed(4));
|
const video = document.getElementById('earthVideo');
|
||||||
root.style.setProperty('--my', currentY.toFixed(4));
|
const routeGroup = document.getElementById('routeGroup');
|
||||||
requestAnimationFrame(loop);
|
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];
|
||||||
}
|
}
|
||||||
loop();
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', (e) => {
|
const parisVec = toCart(PARIS.lat, PARIS.lon);
|
||||||
targetX = (e.clientX / window.innerWidth - 0.5) * 2;
|
const tanaVec = toCart(TANA.lat, TANA.lon);
|
||||||
targetY = (e.clientY / window.innerHeight - 0.5) * 2;
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
window.addEventListener('deviceorientation', (e) => {
|
// Pre-compute great-circle samples in original (unrotated) space
|
||||||
if (e.gamma == null || e.beta == null) return;
|
const SAMPLES = 60;
|
||||||
targetX = Math.max(-1, Math.min(1, e.gamma / 30));
|
const arcVecs = [];
|
||||||
targetY = Math.max(-1, Math.min(1, (e.beta - 45) / 30));
|
for (let i = 0; i <= SAMPLES; i++) arcVecs.push(slerp(parisVec, tanaVec, i / SAMPLES));
|
||||||
}, { passive: true });
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
BIN
videos/earth-poster.jpg
Normal file
BIN
videos/earth-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
videos/earth-rotation.mp4
Normal file
BIN
videos/earth-rotation.mp4
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user