Die Leiste zieht um, statt zu verschwinden und wiederzukommen
Der erste Versuch klappte die Spalte nach oben weg und schob die Insel seitwärts herein. Das sind zwei Leisten, von denen eine geht und eine kommt — Mac und iPad lassen dieselbe umziehen, und man verfolgt sie dabei mit den Augen. Jetzt FLIP: alte Lage messen, umbauen, neue Lage messen, und das Element vom alten in den neuen Zustand fahren lassen. Die Größe fährt mit, denn die Insel ist oben breit und flach und an der Seite schmal und hoch; ein Sprung in der Form mitten in einer ruhigen Bewegung fällt mehr auf als gar keine Bewegung. Über CSS allein ginge das nicht — "right: auto" und "bottom: auto" lassen sich nicht animieren, und genau die wechseln hier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2874a89a6e
commit
653951c1d8
+58
-33
@@ -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() {
|
||||
? '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2" y="3.5" width="16" height="13" rx="2.5"/><path d="M2 8h16" /><rect x="2" y="3.5" width="16" height="4.5" rx="2.5" fill="currentColor" stroke="none" opacity=".9"/></svg>'
|
||||
// Rechteck mit Spalte links — die Leiste zieht an die Seite.
|
||||
: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2" y="3.5" width="16" height="13" rx="2.5"/><path d="M7.5 3.5v13"/><rect x="2" y="3.5" width="5.5" height="13" rx="2.5" fill="currentColor" stroke="none" opacity=".9"/></svg>';
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user