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:
Scarriffle
2026-09-14 17:01:01 +02:00
co-authored by Claude Opus 5
parent 2874a89a6e
commit 653951c1d8
+58 -33
View File
@@ -257,22 +257,24 @@ img { display: block; }
#app.side #content { margin-left: calc(var(--sidebar-w) + 24px); } #app.side #content { margin-left: calc(var(--sidebar-w) + 24px); }
/* ---- Der Umzug der Leiste ----------------------------------------------- /* ---- Der Umzug der Leiste -----------------------------------------------
Nachgebaut nach der App: die Spalte klappt nach **oben** weg und kommt von Sie **zieht um**, sie verschwindet nicht und taucht woanders wieder auf.
oben zurück, die Insel schiebt sich **seitwärts** hinaus und herein. Das ist der Unterschied zwischen einer Leiste, die man mit den Augen
Zusammen sieht es aus, als zöge dieselbe Leiste um — erst zusammenklappen, verfolgt, und zweien, von denen eine geht und eine kommt — und es ist das,
dann hinüberschieben. 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, Der Inhalt rückt gleichzeitig nach; ohne das bliebe links ein Loch stehen,
bis die Leiste angekommen ist. */ bis die Leiste angekommen ist. */
.island { transition: transform .3s cubic-bezier(.32,.72,0,1), opacity .22s ease; } #content { transition: margin-left .42s cubic-bezier(.32,.72,0,1); }
#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%); }
/* Wer Bewegung nicht mag, bekommt keine. */ /* Wer Bewegung nicht mag, bekommt keine. */
@media (prefers-reduced-motion: reduce) { @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); } #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>' ? '<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. // 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>'; : '<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 nav.onclick = () => moveNavigation(P.nav === 'side' ? 'top' : 'side');
// 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');
}));
}
};
isl.appendChild(nav); isl.appendChild(nav);
// Die Marke steht nur in der Spalte — oben nähme sie Platz weg, den die // Die Marke steht nur in der Spalte — oben nähme sie Platz weg, den die
@@ -606,6 +586,51 @@ function buildIsland() {
add('Abmelden', 'out'); 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) { function go(key) {
if (key === 'out') return signOut(); if (key === 'out') return signOut();
current = key; current = key;