Compare commits
1 Commits
esp32thing
...
feat/lb-fl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd18d0cd22 |
163
data/index.html
163
data/index.html
@@ -529,6 +529,143 @@
|
||||
);
|
||||
}
|
||||
|
||||
// -------- Fly-down Animation --------
|
||||
// Wird ausgelöst beim Status-Übergang finished -> ready (kurz vor dem
|
||||
// Auto-Reset des Backends). Damit bleibt die große Zeit oben sichtbar
|
||||
// bis der Backend resettet, und fliegt dann erst nach unten.
|
||||
|
||||
// Snapshot der Quelle einfrieren, BEVOR sie versteckt wird.
|
||||
function captureSourceSnapshot(el) {
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return null;
|
||||
const cs = window.getComputedStyle(el);
|
||||
return {
|
||||
rect,
|
||||
fontSize: cs.fontSize,
|
||||
fontFamily: cs.fontFamily,
|
||||
fontWeight: cs.fontWeight,
|
||||
color: cs.color,
|
||||
text: el.textContent,
|
||||
};
|
||||
}
|
||||
|
||||
function flyDownFromSnapshot(srcSnap, destEl) {
|
||||
if (!srcSnap || !destEl) return;
|
||||
const dstRect = destEl.getBoundingClientRect();
|
||||
if (dstRect.width === 0 || dstRect.height === 0) return;
|
||||
|
||||
// ---- Phase 1: bestehende Einträge nach unten "schieben" ----
|
||||
// Wir verstecken den (bereits gerenderten) neuen Top-Eintrag und
|
||||
// setzen die Geschwister visuell an die Position, die sie VOR
|
||||
// dem neuen Eintrag hatten (eine Slot-Höhe nach oben). Dann
|
||||
// gleiten sie animiert in ihre natürliche Position herunter.
|
||||
const container = destEl.parentElement;
|
||||
const siblings = container
|
||||
? Array.from(
|
||||
container.querySelectorAll(".leaderboard-entry")
|
||||
).filter((e) => e !== destEl)
|
||||
: [];
|
||||
|
||||
let shiftPx = dstRect.height;
|
||||
if (container) {
|
||||
const cs = window.getComputedStyle(container);
|
||||
const gap =
|
||||
parseFloat(cs.rowGap) || parseFloat(cs.gap) || 0;
|
||||
shiftPx += gap;
|
||||
}
|
||||
|
||||
// Dest sofort verstecken (Layout-Slot bleibt erhalten)
|
||||
destEl.style.visibility = "hidden";
|
||||
|
||||
// Geschwister hochsetzen (instant, ohne Transition)
|
||||
siblings.forEach((s) => {
|
||||
s.style.transition = "none";
|
||||
s.style.transform = `translateY(-${shiftPx}px)`;
|
||||
});
|
||||
// Reflow, damit der "instant"-Setup wirkt
|
||||
if (siblings.length > 0) siblings[0].getBoundingClientRect();
|
||||
|
||||
// Slide nach unten zur natürlichen Position
|
||||
const slideMs = 280;
|
||||
siblings.forEach((s) => {
|
||||
s.style.transition = `transform ${slideMs}ms cubic-bezier(0.4, 0, 0.2, 1)`;
|
||||
s.style.transform = "translateY(0)";
|
||||
});
|
||||
|
||||
// ---- Phase 2: nach dem Slide den Ghost einfliegen lassen ----
|
||||
const flyMs = 800;
|
||||
setTimeout(() => {
|
||||
const ghost = document.createElement("div");
|
||||
ghost.textContent = srcSnap.text;
|
||||
ghost.style.position = "fixed";
|
||||
ghost.style.left = srcSnap.rect.left + "px";
|
||||
ghost.style.top = srcSnap.rect.top + "px";
|
||||
ghost.style.width = srcSnap.rect.width + "px";
|
||||
ghost.style.height = srcSnap.rect.height + "px";
|
||||
ghost.style.margin = "0";
|
||||
ghost.style.padding = "0";
|
||||
ghost.style.zIndex = "9999";
|
||||
ghost.style.pointerEvents = "none";
|
||||
ghost.style.color = srcSnap.color;
|
||||
ghost.style.fontFamily = srcSnap.fontFamily;
|
||||
ghost.style.fontWeight = srcSnap.fontWeight;
|
||||
ghost.style.fontSize = srcSnap.fontSize;
|
||||
ghost.style.lineHeight = "1";
|
||||
ghost.style.display = "flex";
|
||||
ghost.style.alignItems = "center";
|
||||
ghost.style.justifyContent = "center";
|
||||
ghost.style.textShadow = "0 0 12px rgba(0, 255, 136, 0.8)";
|
||||
ghost.style.borderRadius = "8px";
|
||||
ghost.style.transition =
|
||||
`left ${flyMs}ms cubic-bezier(0.55, 0, 0.3, 1),` +
|
||||
`top ${flyMs}ms cubic-bezier(0.55, 0.05, 0.3, 1.1),` +
|
||||
`width ${flyMs}ms cubic-bezier(0.55, 0, 0.3, 1),` +
|
||||
`height ${flyMs}ms cubic-bezier(0.55, 0, 0.3, 1),` +
|
||||
`font-size ${flyMs}ms cubic-bezier(0.55, 0, 0.3, 1),` +
|
||||
`color ${flyMs}ms ease-out,` +
|
||||
`text-shadow ${flyMs}ms ease-out`;
|
||||
document.body.appendChild(ghost);
|
||||
|
||||
// Reflow erzwingen, damit die Anfangsposition wirkt
|
||||
ghost.getBoundingClientRect();
|
||||
|
||||
// Endwerte: aktuelle Position des .time-Spans im Eintrag
|
||||
const destTimeSpan = destEl.querySelector(".time") || destEl;
|
||||
const destTimeRect = destTimeSpan.getBoundingClientRect();
|
||||
const destStyle = window.getComputedStyle(destTimeSpan);
|
||||
|
||||
ghost.style.left = destTimeRect.left + "px";
|
||||
ghost.style.top = destTimeRect.top + "px";
|
||||
ghost.style.width = destTimeRect.width + "px";
|
||||
ghost.style.height = destTimeRect.height + "px";
|
||||
ghost.style.fontSize = destStyle.fontSize;
|
||||
ghost.style.color = destStyle.color;
|
||||
ghost.style.textShadow = "0 0 6px rgba(0, 255, 136, 0.55)";
|
||||
|
||||
setTimeout(() => {
|
||||
ghost.remove();
|
||||
destEl.style.visibility = "";
|
||||
// Sibling-Transforms aufräumen (sie sind eh schon bei 0)
|
||||
siblings.forEach((s) => {
|
||||
s.style.transition = "";
|
||||
s.style.transform = "";
|
||||
});
|
||||
}, flyMs + 20);
|
||||
}, slideMs);
|
||||
}
|
||||
|
||||
// Bestimmt das Ziel-Element (erster Leaderboard-Eintrag) für eine Lane.
|
||||
function findFlyDest(lane) {
|
||||
const containerId =
|
||||
leaderboardData && leaderboardData.mode === "different"
|
||||
? "leaderboard-container-" + lane
|
||||
: "leaderboard-container-1";
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return null;
|
||||
return container.querySelector(".leaderboard-entry");
|
||||
}
|
||||
|
||||
function updateLeaderboardDisplay() {
|
||||
const box1 = document.getElementById("best-times-1");
|
||||
const box2 = document.getElementById("best-times-2");
|
||||
@@ -827,6 +964,11 @@
|
||||
.then((data) => {
|
||||
timer1 = data.time1;
|
||||
timer2 = data.time2;
|
||||
|
||||
// Alte Status-Werte sichern, BEVOR sie überschrieben werden
|
||||
const oldStatus1 = status1;
|
||||
const oldStatus2 = status2;
|
||||
|
||||
// Status nur übernehmen, wenn der Wert gültig ist.
|
||||
// Bei unvollständiger ESP-Response (Last) bleibt der
|
||||
// bisherige Status erhalten statt "Status unbekannt".
|
||||
@@ -837,7 +979,28 @@
|
||||
learningMode = data.learningMode;
|
||||
learningButton = data.learningButton || "";
|
||||
lastSync = Date.now();
|
||||
|
||||
// Übergang finished -> ready erkennen.
|
||||
// Snapshot der großen Zeit JETZT einfrieren, bevor
|
||||
// kickDisplayScheduler/updateDisplay sie versteckt.
|
||||
const fly1 =
|
||||
oldStatus1 === "finished" && status1 === "ready"
|
||||
? captureSourceSnapshot(document.getElementById("time1"))
|
||||
: null;
|
||||
const fly2 =
|
||||
oldStatus2 === "finished" && status2 === "ready"
|
||||
? captureSourceSnapshot(document.getElementById("time2"))
|
||||
: null;
|
||||
|
||||
kickDisplayScheduler();
|
||||
|
||||
// Animation auf nächsten Frame, wenn updateDisplay durch ist
|
||||
if (fly1 || fly2) {
|
||||
requestAnimationFrame(() => {
|
||||
if (fly1) flyDownFromSnapshot(fly1, findFlyDest(1));
|
||||
if (fly2) flyDownFromSnapshot(fly2, findFlyDest(2));
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Fehler beim Laden deiner Daten:", error)
|
||||
|
||||
Reference in New Issue
Block a user