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;