diff --git a/web/frontend/index.html b/web/frontend/index.html
index cb962c7..c79d954 100644
--- a/web/frontend/index.html
+++ b/web/frontend/index.html
@@ -257,22 +257,24 @@ img { display: block; }
#app.side #content { margin-left: calc(var(--sidebar-w) + 24px); }
/* ---- Der Umzug der Leiste -----------------------------------------------
- Nachgebaut nach der App: die Spalte klappt nach **oben** weg und kommt von
- oben zurück, die Insel schiebt sich **seitwärts** hinaus und herein.
- Zusammen sieht es aus, als zöge dieselbe Leiste um — erst zusammenklappen,
- dann hinüberschieben.
+ Sie **zieht um**, sie verschwindet nicht und taucht woanders wieder auf.
+ Das ist der Unterschied zwischen einer Leiste, die man mit den Augen
+ verfolgt, und zweien, von denen eine geht und eine kommt — und es ist das,
+ was Mac und iPad tun.
+
+ Gemacht wird das mit FLIP: die alte Lage messen, umbauen, die neue Lage
+ messen, und das Element vom alten Zustand in den neuen fahren lassen. Der
+ Weg dazwischen ist eine einzige Bewegung, obwohl der Aufbau in der Mitte
+ ausgetauscht wurde. Über CSS allein ginge das nicht — `right: auto` und
+ `bottom: auto` lassen sich nicht animieren, und genau die wechseln hier.
Der Inhalt rückt gleichzeitig nach; ohne das bliebe links ein Loch stehen,
bis die Leiste angekommen ist. */
-.island { transition: transform .3s cubic-bezier(.32,.72,0,1), opacity .22s ease; }
-#content { transition: margin-left .32s cubic-bezier(.32,.72,0,1); }
-
-#chrome.moving .island { opacity: 0; transform: translateX(-115%); }
-#app.side #chrome.moving .island { opacity: 0; transform: translateY(-115%); }
+#content { transition: margin-left .42s cubic-bezier(.32,.72,0,1); }
/* Wer Bewegung nicht mag, bekommt keine. */
@media (prefers-reduced-motion: reduce) {
- .island, #content, #chrome, .card .art, .shot { transition: none !important; }
+ #content, .card .art, .shot, .island { transition: none !important; }
}
#app.side .pad-top { padding-top: var(--s-loose); }
@@ -566,29 +568,7 @@ function buildIsland() {
? ''
// Rechteck mit Spalte links — die Leiste zieht an die Seite.
: '';
- // Erst wegklappen, dann umziehen, dann zurückkommen. Ohne die Pause
- // dazwischen tauscht der Browser die Leiste in einem Bild aus, und vom
- // Umzug ist nichts zu sehen.
- nav.onclick = async () => {
- const chrome = el('chrome');
- const quick = matchMedia('(prefers-reduced-motion: reduce)').matches;
-
- if (!quick) {
- chrome.classList.add('moving');
- await new Promise((r) => setTimeout(r, 210));
- }
-
- setPref('nav', P.nav === 'side' ? 'top' : 'side');
- buildIsland();
-
- if (!quick) {
- // Ein Bild abwarten, damit die neue Form ihre Ausgangsstellung
- // übernimmt, bevor sie hereinfährt — sonst gibt es keinen Weg zu gehen.
- requestAnimationFrame(() => requestAnimationFrame(() => {
- el('chrome').classList.remove('moving');
- }));
- }
- };
+ nav.onclick = () => moveNavigation(P.nav === 'side' ? 'top' : 'side');
isl.appendChild(nav);
// Die Marke steht nur in der Spalte — oben nähme sie Platz weg, den die
@@ -606,6 +586,51 @@ function buildIsland() {
add('Abmelden', 'out');
}
+/* Lässt die Leiste von ihrer alten Lage in die neue fahren.
+ ----------------------------------------------------------------------------
+ FLIP, in vier Schritten: **F**irst — wo steht sie jetzt? **L**ast — wo steht
+ sie nach dem Umbau? **I**nvert — sie zurück an die alte Stelle rechnen.
+ **P**lay — die Rechnung auf null laufen lassen.
+
+ Auch die Größe wird mitgefahren, nicht nur die Stelle: die Insel ist oben
+ breit und flach, an der Seite schmal und hoch, und ein Sprung in der Form
+ mitten in einer sonst ruhigen Bewegung fällt mehr auf als gar keine
+ Bewegung. Die Beschriftungen verziehen sich dabei kurz — bei vier Zehnteln
+ sieht man das als Bewegung, nicht als Verzerrung. */
+function moveNavigation(to) {
+ const chrome = el('chrome');
+ const before = chrome.querySelector('.island').getBoundingClientRect();
+ const quick = matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+ setPref('nav', to);
+ buildIsland();
+
+ if (quick) return;
+
+ const island = chrome.querySelector('.island');
+ const after = island.getBoundingClientRect();
+ if (!before.width || !after.width) return;
+
+ island.animate(
+ [
+ {
+ transformOrigin: 'top left',
+ transform: `translate(${before.left - after.left}px, ${before.top - after.top}px)`
+ + ` scale(${before.width / after.width}, ${before.height / after.height})`,
+ },
+ { transformOrigin: 'top left', transform: 'none' },
+ ],
+ { duration: 420, easing: 'cubic-bezier(.32,.72,0,1)' }
+ );
+
+ // Die Beschriftungen gegen die Stauchung aufblenden, damit aus dem
+ // Verziehen kein Lesen wird.
+ island.animate(
+ [{ opacity: 0.55 }, { opacity: 1 }],
+ { duration: 260, easing: 'ease-out' }
+ );
+}
+
function go(key) {
if (key === 'out') return signOut();
current = key;