Svelte und Spring-Animationen

Avatar of Adam Rackis
Adam Rackis am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Spring-Animationen sind eine wunderbare Möglichkeit, UI-Interaktionen zum Leben zu erwecken. Anstatt eine Eigenschaft einfach nur mit konstanter Geschwindigkeit über einen Zeitraum zu ändern, ermöglichen uns Federn, Dinge mithilfe von Federn physik zu bewegen, was den Eindruck erweckt, dass sich eine echte Sache bewegt und für Benutzer natürlicher wirken kann.

Ich habe bereits früher über Spring-Animationen geschrieben. Dieser Beitrag basierte auf React und verwendete react-spring für die Animationen. Dieser Beitrag wird ähnliche Ideen in Svelte untersuchen.

CSS-Entwickler! Wenn es darum geht, das Gefühl von Animationen zu kontrollieren, denkt man oft an Easing. Spring-Animationen können als eine Unterkategorie von Easing betrachtet werden, die auf realer Physik basiert.

Svelte verfügt tatsächlich über integrierte Federn, ohne dass externe Bibliotheken benötigt werden. Wir werden wiederholen, was in der ersten Hälfte meines früheren Beitrags über react-spring behandelt wurde. Aber danach werden wir uns eingehend mit allen Möglichkeiten befassen, wie diese Federn mit Svelte verwendet werden können, und die reale Implementierung für einen zukünftigen Beitrag beiseite legen. Obwohl das enttäuschend erscheinen mag, verfügt Svelte über eine Reihe wunderbarer, einzigartiger Funktionen, die kein Gegenstück in React haben und die effektiv mit diesen Animationsprimitiven integriert werden können. Wir werden einige Zeit damit verbringen, darüber zu sprechen.

Eine weitere Anmerkung: Einige der hier und da eingestreuten Demos mögen seltsam aussehen, da ich die Federn so konfiguriert habe, dass sie extra „federnd“ sind, um einen deutlicheren Effekt zu erzielen. Wenn Sie den Code für eine davon haben, stellen Sie sicher, dass Sie eine Federeinstellung finden, die für *Sie* funktioniert.

Hier ist ein wunderbarer REPL, den Rich Harris erstellt hat, um alle verschiedenen Federeinstellungen und deren Verhalten zu zeigen.

Eine kurze Einführung in Svelte Stores

Bevor wir beginnen, machen wir eine sehr, sehr schnelle Tour durch Svelte Stores. Während Svelte-Komponenten mehr als fähig sind, Zustände zu speichern und zu aktualisieren, hat Svelte auch das Konzept eines Stores, der es Ihnen ermöglicht, Zustände außerhalb einer Komponente zu speichern. Da die Spring API von Svelte Stores verwendet, werden wir hier schnell die wesentlichen Teile einführen.

Um eine Instanz eines Stores zu erstellen, können wir den Typ writable importieren und ihn wie folgt erstellen:

import { writable } from "svelte/store";
const clicks = writable(0);

Die Variable clicks ist ein Store, der den Wert 0 hat. Es gibt zwei Möglichkeiten, einen neuen Wert für einen Store festzulegen: die Methoden set und update. Die erstere empfängt den Wert, auf den Sie den Store setzen, während die letztere einen Callback empfängt, der den aktuellen Wert akzeptiert und den neuen Wert zurückgibt.

function increment() {
  clicks.update(val => val + 1);
}
function setTo5() {
  clicks.set(5);
}

Zustand ist nutzlos, wenn man ihn nicht konsumieren kann. Dafür bieten Stores eine subscribe-Methode, die es Ihnen ermöglicht, über neue Werte benachrichtigt zu werden — aber wenn Sie sie innerhalb einer Komponente verwenden, können Sie dem Namen des Stores das $-Zeichen voranstellen, was Svelte mitteilt, nicht nur den aktuellen Wert des Stores anzuzeigen, sondern ihn auch zu aktualisieren, wenn er sich ändert. Zum Beispiel:

<h1>Value {$clicks}</h1>
<button on:click={increment}>Increment</button>
<button on:click={setTo5}>Set to 5</button>

Hier ist ein vollständiges, funktionierendes Beispiel dieses Codes. Stores bieten eine Reihe weiterer Funktionen, wie z. B. abgeleitete Stores, die es Ihnen ermöglichen, Stores miteinander zu verketten, lesbare Stores und sogar die Möglichkeit, benachrichtigt zu werden, wenn ein Store zum ersten Mal beobachtet wird und wenn er keine Beobachter mehr hat. Aber für die Zwecke dieses Beitrags ist der oben gezeigte Code alles, worüber wir uns Sorgen machen müssen. Konsultieren Sie die Svelte-Dokumentation oder das interaktive Tutorial für weitere Informationen.

Ein Crashkurs über Federn

Lassen Sie uns eine kurze Einführung in Federn durchgehen und was sie leisten. Wir betrachten eine einfache Benutzeroberfläche, die einen präsentationsbezogenen Aspekt einiger Elemente ändert — Opazität und Transformation — und schauen uns dann die Animation dieser Änderung an.

Dies ist eine minimale Svelte-Komponente, die die opacity eines <div> umschaltet und die x-Achsen-transform eines anderen umschaltet (ohne Animation).

<script>
  let shown = true;
  let moved = 0;

  const toggleShow = () => (shown = !shown);
  const toggleMove = () => (moved = moved ? 0 : 500);
</script>

<div style="opacity: {shown ? 1 : 0}">Content to toggle</div>
<br />
<button on:click={toggleShow}>Toggle</button>
<hr />
<div class="box" style="transform: translateX({moved}px)">I'm a box.</div>
<br />
<button on:click={toggleMove}>Move it!</button>

Diese Änderungen werden sofort angewendet, also lassen Sie uns sie animieren. Hier kommen Federn ins Spiel. In Svelte ist eine Feder ein Store, auf den wir den gewünschten Wert setzen, aber anstatt sich sofort zu ändern, verwendet der Store intern Federn physik, um den Wert allmählich zu ändern. Wir können dann unsere Benutzeroberfläche an diesen sich ändernden Wert binden, um eine schöne Animation zu erhalten. Sehen wir es in Aktion.

<script>
  import { spring } from "svelte/motion";

  const fadeSpring = spring(1, { stiffness: 0.1, damping: 0.5 });
  const transformSpring = spring(0, { stiffness: 0.2, damping: 0.1 });

  const toggleFade = () => fadeSpring.update(val => (val ? 0 : 1));
  const toggleTransform = () => transformSpring.update(val => (val ? 0 : 500));
  const snapTransform = () => transformSpring.update(val => val, { hard: true });
</script>

<div style="opacity: {$fadeSpring}">Content to fade</div>
<br />
<button on:click={toggleFade}>Fade Toggle</button>

<hr />

<div class="box" style="transform: translateX({$transformSpring}px)">I'm a box.</div>
<br />
<button on:click={toggleTransform}>Move it!</button>
<button on:click={snapTransform}>Snap into place</button>

Wir erhalten unsere Federfunktion von Svelte und richten verschiedene Federninstanzen für unsere Opazitäts- und Transformationsanimationen ein. Die Transformationsfederkonfiguration ist bewusst so eingestellt, dass sie *extra federnd* ist, um später zu zeigen, wie wir Federnanimationen vorübergehend deaktivieren und gewünschte Änderungen sofort anwenden können (was später nützlich sein wird). Am Ende des Skriptblocks befinden sich unsere Klick-Handler zum Festlegen der gewünschten Eigenschaften. Dann binden wir im HTML unsere sich ändernden Werte direkt an unsere Elemente... und das war's! Das ist alles, was es für grundlegende Federnanimationen in Svelte gibt.

Der einzige verbleibende Punkt ist die Funktion snapTransform, bei der wir unsere Transformationsfeder auf ihren aktuellen Wert setzen, aber auch ein Objekt als zweites Argument übergeben, mit hard: true. Dies hat den Effekt, dass der gewünschte Wert sofort und ohne Animation angewendet wird.

Diese Demo, sowie die restlichen grundlegenden Beispiele, die wir in diesem Beitrag betrachten werden, sind hier:

Höhe animieren

Die Animation der height ist schwieriger als bei anderen CSS-Eigenschaften, da wir die tatsächliche Höhe kennen müssen, zu der wir animieren. Leider können wir nicht zu einem Wert von auto animieren. Das würde für eine Feder keinen Sinn ergeben, da die Feder eine reelle Zahl benötigt, um die richtigen Werte mittels Federn physik interpolieren zu können. Und wie es sich herausstellt, kann man nicht einmal mit regulären CSS-Übergängen eine auto-Höhe animieren. Glücklicherweise gibt uns die Webplattform ein praktisches Werkzeug, um die Höhe eines Elements zu ermitteln: einen ResizeObserver, der eine ziemlich gute Unterstützung unter den Browsern genießt.

Beginnen wir mit einer rohen Höhenanimation eines Elements, die einen „Slide-Down“-Effekt erzeugt, den wir in anderen Beispielen schrittweise verfeinern. Wir verwenden ResizeObserver, um an die Höhe eines Elements zu binden. Ich sollte erwähnen, dass Svelte eine offsetHeight-Bindung hat, die verwendet werden kann, um die Höhe eines Elements direkter zu binden, aber sie ist mit einigen <iframe>-Hacks implementiert, die dazu führen, dass sie nur auf Elementen funktioniert, die Kinder empfangen können. Das wäre für die meisten Anwendungsfälle wahrscheinlich gut genug, aber ich werde einen ResizeObserver verwenden, da er am Ende einige schöne Abstraktionen ermöglicht.

Zuerst binden wir die Höhe eines Elements. Sie empfängt das Element und gibt einen beschreibbaren Store zurück, der einen ResizeObserver initialisiert, der den height-Wert bei Änderungen aktualisiert. So sieht das aus:

export default function syncHeight(el) {
  return writable(null, (set) => {
    if (!el) {
      return;
    }
    let ro = new ResizeObserver(() => el && set(el.offsetHeight));
    ro.observe(el);
    return () => ro.disconnect();
  });
}

Wir starten den Store mit dem Wert null, was wir als „noch nicht gemessen“ interpretieren werden. Das zweite Argument für writable wird von Svelte aufgerufen, wenn der Store aktiv wird, was er sein wird, sobald er in einer Komponente verwendet wird. Dann starten wir den ResizeObserver und beginnen, das Element zu beobachten. Dann geben wir eine Bereinigungsfunktion zurück, die Svelte für uns aufruft, wenn der Store nicht mehr irgendwo verwendet wird.

Sehen wir uns das in Aktion an:

<script>
  import syncHeight from "../syncHeight";
  import { spring } from "svelte/motion";

  let el;
  let shown = false;
  let open = false;
  let secondParagraph = false;

  const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });
  $: heightStore = syncHeight(el);
  $: heightSpring.set(open ? $heightStore || 0 : 0);

  const toggleOpen = () => (open = !open);
  const toggleSecondParagraph = () => (secondParagraph = !secondParagraph);
</script>

<button on:click={ toggleOpen }>Toggle</button>
<button on:click={ toggleSecondParagraph }>Toggle More</button>
<div style="overflow: hidden; height: { $heightSpring }px">
  <div bind:this={el}>
    <div>...</div>
    <br />
    {#if secondParagraph}
    <div>...</div>
    {/if}
  </div>
</div>

Unsere Variable el enthält das Element, das wir animieren. Wir weisen Svelte an, es über bind:this={el} auf das DOM-Element zu setzen. heightSpring ist unsere Feder, die den Höhenwert des Elements speichert, wenn es geöffnet ist, und null, wenn es geschlossen ist. Unser heightStore hält es mit der aktuellen Höhe des Elements auf dem neuesten Stand. el ist anfangs undefiniert, und syncHeight gibt einen Junk-beschreibbaren Store zurück, der im Grunde nichts tut. Sobald el dem <div>-Knoten zugewiesen ist, wird diese Zeile erneut ausgeführt — dank der $:-Syntax — und unser beschreibbarer Store mit dem ResizeObserver, der zuhört, abgerufen.

Dann:

$: heightSpring.set(open ? $heightStore || 0 : 0);

…hört auf Änderungen des Öffnungswerts und auch auf Änderungen des Höhenwerts. In beiden Fällen aktualisiert es unseren Federstore. Wir binden die Höhe im HTML, und wir sind fertig!

Stellen Sie sicher, dass Sie overflow auf hidden auf diesem äußeren Element setzen, damit der Inhalt ordnungsgemäß abgeschnitten wird, während die Elemente zwischen geöffnetem und geschlossenem Zustand wechseln. Auch Änderungen an der Höhe des Elements werden animiert, was Sie am „Toggle More“-Button sehen können. Sie können dies in der eingebetteten Demo im vorherigen Abschnitt ausführen.

Beachten Sie, dass diese Zeile oben:

$: heightStore = syncHeight(el);

…derzeit einen Fehler verursacht, wenn Server-Side Rendering (SSR) verwendet wird, wie in diesem Bug erklärt. Wenn Sie kein SSR verwenden, müssen Sie sich keine Sorgen machen, und natürlich kann es sein, dass der Bug zum Zeitpunkt des Lesens behoben wurde. Aber die Umgehung besteht einfach darin, Folgendes zu tun:

let heightStore;
$: heightStore = syncHeight(el);

…was funktioniert, aber kaum ideal ist.

Wahrscheinlich möchten wir nicht, dass sich das <div> beim ersten Rendern öffnet. Auch der federnde Öffnungseffekt ist schön, aber beim Schließen ist der Effekt ruckelig aufgrund von Bildflimmern. Das können wir beheben. Um zu verhindern, dass unser anfängliches Rendern animiert wird, können wir die Option { hard: true } verwenden, die wir zuvor gesehen haben. Ändern wir unseren Aufruf zu heightSpring.set zu diesem:

$: heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));

…und sehen wir uns dann an, wie wir eine getConfig-Funktion schreiben können, die ein Objekt mit der Eigenschaft hard zurückgibt, das für das erste Rendern auf true gesetzt wurde. Hier ist, was ich mir ausgedacht habe:

let shown = false;

const getConfig = val => {
  let active = typeof val === "number";
  let immediate = !shown && active;
  //once we've had a proper height registered, we can animate in the future
  shown = shown || active;
  return immediate ? { hard: true } : {};
};

Denken Sie daran, dass unser Höhen-Store anfänglich null enthält und erst eine Zahl erhält, wenn der ResizeObserver zu laufen beginnt. Wir nutzen dies aus, indem wir nach einer tatsächlichen Zahl suchen. Wenn wir eine Zahl haben und noch nichts angezeigt haben, dann wissen wir, dass wir unseren Inhalt sofort anzeigen müssen, und das tun wir, indem wir den sofortigen Wert festlegen. Dieser Wert löst schließlich den hard-Konfigurationswert in der Feder aus, den wir zuvor gesehen haben.

Lassen Sie uns nun die Animation so anpassen, dass sie beim Schließen unseres Inhalts etwas weniger federnd ist. So flackern die Dinge beim Schließen nicht. Als wir unsere Feder ursprünglich erstellten, spezifizierten wir Steifigkeit und Dämpfung, wie folgt:

const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });

Es stellt sich heraus, dass das spring-Objekt selbst diese Eigenschaften beibehält, die jederzeit gesetzt werden können. Aktualisieren wir diese Zeile:

$: heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));

Dies erkennt Änderungen am Öffnungswert (und am heightStore selbst) zur Aktualisierung der Feder. Lassen Sie uns auch die Einstellungen der Feder ändern, abhängig davon, ob wir öffnen oder schließen. So sieht das aus:

$: {
  heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));
  Object.assign(
    heightSpring,
    open ? { stiffness: 0.1, damping: 0.3 } : { stiffness: 0.1, damping: 0.5 }
  );
}

Jetzt, wenn wir einen neuen open- oder height-Wert erhalten, rufen wir heightSpring.set wie zuvor auf, aber wir setzen auch stiffness- und damping-Werte für die Feder, die angewendet werden, je nachdem, ob das Element geöffnet ist. Wenn es geschlossen ist, setzen wir damping auf 0,5, was die Federung reduziert. Natürlich können Sie alle diese Werte nach Belieben anpassen und konfigurieren! Das können Sie im Abschnitt „Animate Height Different Springs“ der Demo sehen.

Sie werden vielleicht bemerken, dass unser Code ziemlich schnell wächst. Wir haben viel Boilerplate hinzugefügt, um einige dieser Anwendungsfälle abzudecken, also lassen Sie uns das aufräumen. Insbesondere werden wir eine Funktion erstellen, die unsere Feder erstellt und die auch eine sync-Funktion exportiert, um unsere Federkonfiguration, das anfängliche Rendern usw. zu verwalten.

import { spring } from "svelte/motion";

const OPEN_SPRING = { stiffness: 0.1, damping: 0.3 };
const CLOSE_SPRING = { stiffness: 0.1, damping: 0.5 };

export default function getHeightSpring() {
  const heightSpring = spring(0);
  let shown = false;

  const getConfig = (open, val) => {
    let active = typeof val === "number";
    let immediate = open && !shown && active;
    // once we've had a proper height registered, we can animate in the future
    shown = shown || active;
    return immediate ? { hard: true } : {};
  };

  const sync = (open, height) => {
    heightSpring.set(open ? height || 0 : 0, getConfig(open, height));
    Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);
  };

  return { sync, heightSpring };
}

Hier gibt es viel Code, aber es ist alles der Code, den wir bisher geschrieben haben, nur in einer einzigen Funktion verpackt. Jetzt ist unser Code zur Verwendung dieser Animation auf Folgendes reduziert:

const { heightSpring, sync } = getHeightSpring();
$: heightStore = syncHeight(el);
$: sync(open, $heightStore);

Sie können das im Abschnitt „Animate Height Cleanup“ der Demo sehen.

Einige Svelte-spezifische Tricks

Lassen Sie uns einen Moment innehalten und über einige Möglichkeiten nachdenken, wie sich Svelte von React unterscheidet und wie wir dies nutzen können, um das, was wir haben, weiter zu verbessern.

Erstens sind die Stores, die wir bisher zum Speichern von Federn und Ändern von Höhenwerten verwendet haben, im Gegensatz zu den React-Hooks *nicht* an die Komponentendarstellung gebunden. Es handelt sich um einfache JavaScript-Objekte, die *überall* konsumiert werden können. Und, wie oben angedeutet, können wir sie imperativ abonnieren, sodass sie sich ändernde Werte manuell beobachten.

Svelte hat auch etwas namens Aktionen. Dies sind Funktionen, die einem DOM-Element hinzugefügt werden können. Wenn das Element erstellt wird, ruft Svelte die Funktion auf und übergibt das Element als erstes Argument. Wir können auch zusätzliche Argumente angeben, die Svelte übergeben soll, und eine update-Funktion bereitstellen, damit Svelte diese Werte erneut ausführt, wenn sie sich ändern. Eine weitere Sache, die wir tun können, ist die Bereitstellung einer cleanup-Funktion, die Svelte aufruft, wenn es das Element zerstört.

Fassen wir diese Werkzeuge zu einer einzigen Aktion zusammen, die wir einfach auf ein Element anwenden können, um die gesamte Animation zu handhaben, die wir bisher geschrieben haben:

export default function slideAnimate(el, open) {
  el.parentNode.style.overflow = "hidden";

  const { heightSpring, sync } = getHeightSpring();
  const doUpdate = () => sync(open, el.offsetHeight);
  const ro = new ResizeObserver(doUpdate);

  const springCleanup = heightSpring.subscribe((height) => {
    el.parentNode.style.height = `${ height }px`;
  });

  ro.observe(el);

  return {
    update(isOpen) {
      open = isOpen;
      doUpdate();
    },
    destroy() {
      ro.disconnect();
      springCleanup();
    }
  };
}

Unsere Funktion wird mit dem Element, das wir animieren möchten, sowie dem Öffnungswert aufgerufen. Wir setzen das übergeordnete Element auf overflow: hidden. Dann verwenden wir dieselbe getHeightSpring-Funktion wie zuvor, richten unseren ResizeObserver ein usw. Die eigentliche Magie steckt hier.

const springCleanup = heightSpring.subscribe((height) => {
  el.parentNode.style.height = `${height}px`;
});

Anstatt unsere heightSpring an das DOM zu binden, abonnieren wir manuell Änderungen und setzen dann selbst die Höhe, manuell. Wir würden normalerweise keine manuellen DOM-Updates durchführen, wenn wir ein JavaScript-Framework wie Svelte verwenden, aber in diesem Fall ist es für eine Hilfsbibliothek, was meiner Meinung nach völlig in Ordnung ist.

Im von uns zurückgegebenen Objekt definieren wir eine update-Funktion, die Svelte aufruft, wenn sich der open-Wert ändert. Wir aktualisieren das ursprüngliche Argument dieser Funktion, das die Funktion schließt (d. h. eine Closure darum erstellt) und rufen dann unsere update-Funktion auf, um alles zu synchronisieren. Svelte ruft die destroy-Funktion auf, wenn unser DOM-Knoten zerstört wird.

Das Beste daran ist, dass die Verwendung dieser Aktion kinderleicht ist:

<div use:slideAnimate={open}>

Das ist alles. Wenn sich open ändert, ruft Svelte unsere update-Funktion auf.

Bevor wir fortfahren, nehmen wir noch eine kleine Änderung vor. Beachten Sie, wie wir die Federung entfernen, indem wir die Federkonfiguration ändern, wenn wir die Ansicht mit dem Button „Toggle“ einklappen; wenn wir das Element jedoch durch Klicken auf den Button „Toggle More“ *kleiner* machen, schrumpft es mit der üblichen Federung. Das gefällt mir nicht und ich bevorzuge es, dass schrumpfende Größen sich mit derselben Physik bewegen, die wir zum Einklappen verwenden.

Beginnen wir damit, diese Zeile in der getHeightSpring-Funktion zu entfernen:

Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);

Diese Zeile befindet sich in der sync-Funktion, die getHeightSpring erstellt hat und die unsere Federeinstellungen bei jeder Änderung aktualisiert, basierend auf dem open-Wert. Wenn sie weg ist, können wir unsere Feder mit der „Open“-Federkonfiguration starten:

const heightSpring = spring(0, OPEN_SPRING);

Nun lassen Sie uns unsere Federeinstellungen ändern, wenn entweder die Höhe unseres Inhalts sich ändert oder wenn sich der open-Wert ändert. Wir haben bereits die Möglichkeit, beides zu beobachten — unser ResizeObserver-Callback wird aufgerufen, wenn sich die Größe des Inhalts ändert, und die update-Funktion unserer Aktion wird immer dann aufgerufen, wenn sich open ändert.

Unser ResizeObserver-Callback kann wie folgt geändert werden:

let currentHeight = null;
const ro = new ResizeObserver(() => {
  const newHeight = el.offsetHeight;
  const bigger = newHeight > currentHeight;

  if (typeof currentHeight === "number") {
    Object.assign(heightSpring, bigger ? OPEN_SPRING : CLOSE_SPRING);
  }
  currentHeight = newHeight;
  doUpdate();
});

currentHeight speichert den aktuellen Wert, und wir prüfen ihn bei Größenänderungen, um zu sehen, in welche Richtung wir uns bewegen. Als Nächstes kommt die update-Funktion. So sieht sie nach unserer Änderung aus:

update(isOpen) {
  open = isOpen;
  Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);
  doUpdate();
},

Gleiche Idee, aber jetzt prüfen wir nur, ob open true oder false ist. Sie können diese Iterationen in den Abschnitten „Slide Animate“ und „Slide Animate 2“ der Demo sehen.

Übergänge

Bisher haben wir über die Animation von Elementen gesprochen, die sich bereits auf der Seite befinden, aber was ist mit der Animation eines Objekts, wenn es zum ersten Mal gerendert wird? Und wenn es unmounted wird? Das nennt man **Transition**, und es ist in Svelte integriert. Die Dokumentation deckt die gängigen Anwendungsfälle hervorragend ab, aber es gibt eine Sache, die noch nicht (direkt) unterstützt wird: Federbasierte Übergänge.

/explanation Beachten Sie, dass das, was Svelte „Transition“ nennt, und das, was CSS „Transition“ nennt, sehr unterschiedlich ist. CSS meint den Übergang eines Wertes zu einem anderen. Svelte bezieht sich auf Elemente, die vollständig in den DOM hinein und aus ihm heraus „übergehen“ (etwas, das CSS kaum unterstützt).

Um es klarzustellen: Die Arbeit, die wir hier leisten, dient dazu, Federanimationen in die Übergänge von Svelte zu integrieren. Dies wird derzeit nicht unterstützt und erfordert einige Tricks und Umgehungen, auf die wir eingehen werden. Wenn Sie keine Federn verwenden möchten, können die integrierten Übergänge von Svelte verwendet werden, die *deutlich* einfacher sind. Auch hier finden Sie weitere Informationen in der Dokumentation.

Die Funktionsweise von Übergängen in Svelte ist, dass wir eine Dauer in Millisekunden (ms) zusammen mit einer optionalen Easing-Funktion bereitstellen, dann liefert uns Svelte einen Callback mit einem Wert von 0 bis 1, der darstellt, wie weit der Übergang fortgeschritten ist, und wir wandeln diesen in beliebige CSS um, die wir wollen. Zum Beispiel:

const animateIn = () => {
  return {
    duration: 2000,
    css: t => `transform: translateY(${t * 50 - 50}px)`
  };
};

…wird wie folgt verwendet:

<div in:animateIn out:animateOut class="box">
  Hello World!
</div>

Wenn dieses <div> zum ersten Mal gemountet wird, ruft Svelte

  • unsere animateIn-Funktion auf,
  • ruft die CSS-Funktion auf unserem resultierenden Objekt *im Voraus* mit Werten von 0 bis 1 auf,
  • sammelt unser sich änderndes CSS-Ergebnis, und dann
  • kompiliert diese Ergebnisse zu einer CSS-Keyframes-Animation, die es dann auf das eingehende <div> anwendet.

Das bedeutet, dass unsere Animation als CSS-Animation läuft — nicht als JavaScript im Hauptthread — und kostenlos eine schöne Leistungssteigerung bietet.

Die Variable t beginnt bei 0, was zu einer Verschiebung von -50px führt. Wenn t sich 1 nähert, nähert sich die Verschiebung 0, ihrem Endwert. Der Aus-Übergang ist ungefähr derselbe, aber in umgekehrter Richtung, mit der zusätzlichen Funktion, den aktuellen Translationswert des Kastens zu erkennen und von dort auszugehen. Wenn wir ihn also hinzufügen und dann schnell entfernen, verlässt der Kasten seine aktuelle Position, anstatt voraus zu springen. Wenn wir ihn jedoch wieder hinzufügen, während er sich entfernt, wird er springen, etwas, worüber wir gleich sprechen werden.

Sie können dies im Abschnitt „Basic Transition“ der Demo ausführen.

Übergänge, aber mit Federn

Während es eine Reihe von Easing-Funktionen gibt, die den Fluss einer Animation verändern, gibt es keine Möglichkeit, Federn *direkt* zu verwenden. Aber was wir tun *könnten*, ist, einen Weg zu finden, eine Feder *im Voraus* laufen zu lassen, die resultierenden Werte zu sammeln und dann, wenn unsere css-Funktion mit einem t-Wert von 0 bis 1 aufgerufen wird, den richtigen Federwert nachzuschlagen. Wenn t also 0 ist, benötigen wir offensichtlich den ersten Wert von der Feder. Wenn t 0,5 ist, wollen wir den Wert genau in der Mitte, und so weiter. Wir brauchen auch eine Dauer, die number_of_spring_values * 1000 / 60 beträgt, da es 60 Bilder pro Sekunde gibt.

Diesen Code schreiben wir hier nicht. Stattdessen verwenden wir die Lösung, die bereits in der svelte-helpers-Bibliothek existiert, ein Projekt, das ich gestartet habe. Ich habe eine kleine Funktion aus dem Svelte-Code, spring_tick, übernommen und dann eine separate Funktion geschrieben, um sie wiederholt aufzurufen, bis sie fertig ist, und die Werte dabei zu sammeln. Das, zusammen mit einer Übersetzung von t auf das richtige Element in diesem Array (oder einem gewichteten Durchschnitt, wenn keine direkte Übereinstimmung besteht), ist alles, was wir brauchen. Rich Harris hat bei Letzterem geholfen, wofür ich dankbar bin.

Einblenden

Nehmen wir an, ein großes rotes <div> ist ein Modal, das wir ein- und ausblenden wollen. So sieht eine animateIn-Funktion aus:

import { springIn, springOut } from "svelte-helpers/animation";
const SPRING_IN = { stiffness: 0.1, damping: 0.1 };

const animateIn = node => {
  const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);
  return {
    duration,
    css: t => `transform: translateY(${ tickToValue(t) }px)`
  };
};

Wir geben die Werte, zu denen wir federn wollen, sowie unsere Federkonfiguration an die springIn-Funktion weiter. Das gibt uns eine Dauer und eine Funktion, um den aktuellen tickToValue in den aktuellen Wert umzuwandeln, der in CSS angewendet werden soll. Das war's!

Ausblenden

Das Schließen des Modals ist dasselbe, mit einer kleinen Änderung:

const SPRING_OUT = { stiffness: 0.1, damping: 0.5, precision: 3 };

const animateOut = node => {
  const current = currentYTranslation(node);
  const { duration, tickToValue } = springOut(current ? current : 0, 80, SPRING_OUT);
  return {
    duration: duration,
    css: t => `transform: translateY(${ tickToValue(t) }px)`
  };
};

Hier prüfen wir die aktuelle Translationsposition des Modals und verwenden diese dann als Startpunkt für die Animation. Auf diese Weise, wenn der Benutzer das Modal öffnet und dann schnell schließt, verlässt es seine aktuelle Position, anstatt zu 0 zu teleportieren und *dann* zu verschwinden. Dies funktioniert, weil die animateOut-Funktion *beim* Unmounten des Elements aufgerufen wird, an dem Punkt, an dem wir das Objekt mit der duration-Eigenschaft und der css-Funktion generieren, damit die Animation berechnet werden kann.

Leider scheint das erneute Mounten des Objekts, während es sich im Ausblendvorgang befindet, nicht gut zu funktionieren. Die Funktion animateIn wird *nicht* *de novo* aufgerufen, sondern die ursprüngliche Animation wird wiederverwendet, was bedeutet, dass sie immer bei -80 beginnt. Glücklicherweise würde das bei einer typischen Modal-Komponente wahrscheinlich keine Rolle spielen, da ein Modal normalerweise durch Klicken auf etwas, wie z. B. das Hintergrund-Overlay, entfernt wird, was bedeutet, dass wir es erst wieder anzeigen können, bis dieses Overlay vollständig ausanimiert ist. Außerdem könnten wiederholtes Hinzufügen und Entfernen eines Elements mit bidirektionalen Übergängen eine lustige Demo ergeben, aber sie sind in der Praxis nicht sehr üblich, zumindest nach meiner Erfahrung.

Eine letzte kurze Anmerkung zur Auswärts-Federkonfiguration: Sie haben vielleicht bemerkt, dass ich die Präzision lächerlich hoch eingestellt habe (3, wenn der Standardwert 0,01 ist). Dies teilt Svelte mit, wie nahe es an den Zielwert herankommen muss, bevor es entscheidet, dass es „fertig“ ist. Wenn Sie den Standardwert von 0,01 beibehalten, erreicht das Modal (fast) sein Ziel, verbringt dann viele Millisekunden damit, sich unmerklich näher heranzubewegen, bevor es entscheidet, dass es fertig ist, und entfernt sich dann aus dem DOM. Dies erweckt den Eindruck, dass das Modal klemmt oder anderweitig verzögert ist. Das Ändern der Präzision auf einen Wert von 3 behebt dies. Nun animiert das Modal dorthin, wo es hingehen soll (oder zumindest nahe genug), und verschwindet dann schnell.

Mehr Animation

Lassen Sie uns unser Modalbeispiel um eine letzte Anpassung erweitern. Lassen Sie es ein- und ausblenden, *während* es animiert wird. Wir können dafür keine Federn verwenden, da wir, wie gesagt, eine kanonische Dauer für den Übergang benötigen, und unsere Bewegungsfeder liefert diese bereits. Aber Federanimationen machen normalerweise Sinn für tatsächlich bewegte Elemente und nicht viel mehr. Lassen Sie uns also eine Easing-Funktion verwenden, um eine Fade-Animation zu erstellen.

Wenn Sie Hilfe bei der Auswahl der richtigen Easing-Funktion benötigen, schauen Sie sich diese praktische Visualisierung aus der Svelte-Dokumentation an. Ich werde die Funktionen quintOut und quadIn verwenden.

import { quintOut, quadIn } from "svelte/easing";

Unsere neue animateIn-Funktion sieht ziemlich ähnlich aus. Unsere css-Funktion macht dasselbe wie zuvor, aber sie übergibt auch den tickToValue-Wert durch die quintOut-Easing-Funktion, um unseren opacity-Wert zu erhalten. Da t während eines Ein-Übergangs von 0 bis 1 und während eines Aus-Übergangs von 1 bis 0 läuft, müssen wir nichts weiter tun, bevor wir sie auf opacity anwenden.

const SPRING_IN = { stiffness: 0.1, damping: 0.1 };
const animateIn = node =>; {
  const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);
  return {
    duration,
    css: t => {
      const transform = tickToValue(t);
      const opacity = quintOut(t);
      return `transform: translateY(${ transform }px); opacity: ${ opacity };`;
    }
  };
};

Unsere animateOut-Funktion ist ähnlich, aber wir wollen den *aktuellen* opacity-Wert des Elements greifen und die Animation dort beginnen lassen. Wenn das Element also gerade dabei ist auszublenden, mit einer Opazität von z. B. 0,3, wollen wir es nicht auf 1 zurücksetzen und dann ausblenden. Stattdessen wollen wir es von 0,3 aus ausblenden.

Das Multiplizieren der anfänglichen Opazität mit dem von der Easing-Funktion zurückgegebenen Wert erreicht dies. Wenn unser t-Wert bei 1 beginnt, dann ist 1 * 0,3 gleich 0,3. Wenn t 0,95 ist, berechnen wir 0,95 * 0,3, um einen Wert zu erhalten, der etwas weniger als 0,3 ist, und so weiter.

Hier ist die Funktion:

const animateOut = node => {
  const currentT = currentYTranslation(node);
  const startOpacity = +getComputedStyle(node).opacity;
  const { duration, tickToValue } = springOut(
    currentT ? currentT : 0,
    80,
    SPRING_OUT
  );
  return {
    duration,
    css: t => {
      const transform = tickToValue(t);
      const opacity = quadIn(t);
      return `transform: translateY(${ transform }px); opacity: ${ startOpacity * opacity }`;
    }
  };
};

Sie können dieses Beispiel in der Demo mit der Komponente „Spring Transition With Fade“ ausführen.

Abschließende Gedanken

Svelte macht viel Spaß! Nach meiner (zugegebenermaßen begrenzten) Erfahrung bietet es tendenziell äußerst einfache Primitiven und überlässt es Ihnen dann, das zu codieren, was Sie benötigen. Ich hoffe, dieser Beitrag hat Ihnen geholfen zu erklären, wie Federnanimationen in Ihren Webanwendungen gut eingesetzt werden können.

Und, hey, nur eine kurze Erinnerung, Barrierefreiheit bei der Arbeit mit Federn zu berücksichtigen, genau wie bei jeder anderen Animation. Die Kombination dieser Techniken mit etwas wie prefers-reduced-motion kann sicherstellen, dass nur diejenigen, die Animationen bevorzugen, sie auch erhalten.