diff --git a/public/index.html b/public/index.html
index 74ac417..5948d21 100644
--- a/public/index.html
+++ b/public/index.html
@@ -22,7 +22,7 @@
-
+
-
+
diff --git a/public/live.js b/public/live.js
index 48f669d..bcf6f18 100644
--- a/public/live.js
+++ b/public/live.js
@@ -1755,105 +1755,80 @@
}
const matrixGreen = '#00ff41';
- const TRAIL_LEN = 6;
- const DURATION_MS = 1400;
- const SPAWN_EVERY_MS = DURATION_MS / 16; // ~87ms between chars
+ const TRAIL_LEN = Math.min(6, bytes.length);
+ const DURATION_MS = 1400; // total hop duration
+ const CHAR_INTERVAL = 0.06; // spawn a char every 6% of progress
+ const charMarkers = [];
+ let nextCharAt = CHAR_INTERVAL;
+ let byteIdx = 0;
const trail = L.polyline([from], {
color: matrixGreen, weight: 1.5, opacity: 0.2, lineCap: 'round'
}).addTo(pathsLayer);
- // Pre-create reusable char markers with CSS transitions
- const pool = [];
- for (let i = 0; i < TRAIL_LEN; i++) {
- const m = L.marker(from, {
- icon: L.divIcon({
- className: 'matrix-char matrix-char-anim',
- html: `${bytes[i % bytes.length]}`,
- iconSize: [24, 18],
- iconAnchor: [12, 9]
- }),
- interactive: false
- });
- pool.push({ marker: m, active: false, spawnedAt: 0 });
- }
-
- let poolIdx = 0;
- let byteIdx = 0;
+ const trailCoords = [from];
const startTime = performance.now();
- let lastSpawn = -Infinity;
- let trailCoords = [from];
function tick(now) {
const elapsed = now - startTime;
const t = Math.min(1, elapsed / DURATION_MS);
const lat = from[0] + (to[0] - from[0]) * t;
const lon = from[1] + (to[1] - from[1]) * t;
+ trailCoords.push([lat, lon]);
+ trail.setLatLngs(trailCoords);
- // Update trail polyline less often (every ~3 frames)
- if (trailCoords.length === 0 || t >= 1 || elapsed - (trailCoords.length * (DURATION_MS / 30)) >= 0) {
- trailCoords.push([lat, lon]);
- trail.setLatLngs(trailCoords);
+ // Remove old chars beyond trail length
+ while (charMarkers.length > TRAIL_LEN) {
+ const old = charMarkers.shift();
+ try { animLayer.removeLayer(old.marker); } catch {}
+ }
+
+ // Fade existing chars
+ for (let i = 0; i < charMarkers.length; i++) {
+ const age = charMarkers.length - i;
+ const op = Math.max(0.15, 1 - (age / TRAIL_LEN) * 0.7);
+ const size = Math.max(10, 16 - age * 1.5);
+ const el = charMarkers[i].marker.getElement();
+ if (el) { el.style.opacity = op; el.style.fontSize = size + 'px'; }
}
// Spawn new char at intervals
- if (elapsed - lastSpawn >= SPAWN_EVERY_MS && t < 0.95) {
- lastSpawn = elapsed;
- const slot = pool[poolIdx % TRAIL_LEN];
-
- // Recycle: remove old, update text
- if (slot.active) {
- // Just update position — CSS transition handles smoothness
- } else {
- slot.marker.addTo(animLayer);
- slot.active = true;
- // Apply CSS transition after first paint
- requestAnimationFrame(() => {
- const el = slot.marker.getElement();
- if (el) el.style.transition = 'transform 80ms linear, opacity 200ms ease';
- });
- }
-
- // Update byte text
- const el = slot.marker.getElement();
- if (el) {
- const span = el.querySelector('span');
- if (span) span.textContent = bytes[byteIdx % bytes.length];
- el.style.opacity = '1';
- el.style.fontSize = '16px';
- }
- slot.marker.setLatLng([lat, lon]);
- slot.spawnedAt = elapsed;
+ if (t >= nextCharAt && t < 1) {
+ nextCharAt += CHAR_INTERVAL;
+ const charEl = L.marker([lat, lon], {
+ icon: L.divIcon({
+ className: 'matrix-char',
+ html: `${bytes[byteIdx % bytes.length]}`,
+ iconSize: [24, 18],
+ iconAnchor: [12, 9]
+ }),
+ interactive: false
+ }).addTo(animLayer);
+ charMarkers.push({ marker: charEl });
byteIdx++;
- poolIdx++;
-
- // Fade older chars
- for (let i = 0; i < pool.length; i++) {
- if (!pool[i].active) continue;
- const age = elapsed - pool[i].spawnedAt;
- if (age > SPAWN_EVERY_MS * 0.5) {
- const fadeT = Math.min(1, age / (SPAWN_EVERY_MS * TRAIL_LEN));
- const op = Math.max(0.1, 1 - fadeT * 0.85);
- const sz = Math.max(10, 16 - fadeT * 6);
- const pel = pool[i].marker.getElement();
- if (pel) { pel.style.opacity = op; pel.style.fontSize = sz + 'px'; }
- }
- }
}
if (t < 1) {
requestAnimationFrame(tick);
} else {
- // Fade out everything
- for (const slot of pool) {
- const el = slot.marker.getElement();
- if (el) { el.style.transition = 'opacity 500ms ease'; el.style.opacity = '0'; }
+ // Fade out
+ const fadeStart = performance.now();
+ function fadeOut(now) {
+ const ft = Math.min(1, (now - fadeStart) / 500);
+ if (ft >= 1) {
+ for (const cm of charMarkers) try { animLayer.removeLayer(cm.marker); } catch {}
+ try { pathsLayer.removeLayer(trail); } catch {}
+ charMarkers.length = 0;
+ } else {
+ const op = 1 - ft;
+ for (const cm of charMarkers) {
+ const el = cm.marker.getElement(); if (el) el.style.opacity = op * 0.5;
+ }
+ trail.setStyle({ opacity: op * 0.15 });
+ requestAnimationFrame(fadeOut);
+ }
}
- trail.setStyle({ opacity: 0.05 });
- setTimeout(() => {
- for (const slot of pool) { try { animLayer.removeLayer(slot.marker); } catch {} }
- try { pathsLayer.removeLayer(trail); } catch {}
- }, 600);
+ setTimeout(() => requestAnimationFrame(fadeOut), 300);
if (onComplete) onComplete();
}
}
diff --git a/public/style.css b/public/style.css
index 319c9f9..de8c0a8 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1533,13 +1533,7 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
/* Matrix mode hex animation */
.matrix-char { background: none !important; border: none !important; }
-.matrix-char span {
- display: block; text-align: center; white-space: nowrap; line-height: 1;
- color: #fff; font-family: 'Courier New', monospace; font-size: 16px; font-weight: bold;
- text-shadow: 0 0 8px #00ff41, 0 0 16px #00ff41, 0 0 24px #00ff4160;
- pointer-events: none;
-}
-.matrix-char-anim { transition: transform 80ms linear, opacity 200ms ease; will-change: transform, opacity; }
+.matrix-char span { display: block; text-align: center; white-space: nowrap; line-height: 1; }
/* === Matrix Theme === */
.matrix-theme .leaflet-tile-pane {