Animace ve Svelte

Animace jsou na našich webových stránkách a aplikacích přítomny více než kdy jindy. Mohou vypadat a vypadat jinak, pokud jsou správně provedeny a zaujmout vaše uživatele.
V tomto příspěvku se dozvíme, jak lze ve Svelte používat animace a jak můžete rozšířit ty, které jsou dodávány s knihovnou.

Moduly

Svelte dodává řadu modulů, které nám pomohou při vytváření animací.
Prozkoumáme každý z nich, abychom pochopili, co dělají.

  • animovat
  • zmírnění
  • pohyb
  • přechod

štíhlé/zmírněné

Tento balíček obsahuje řadu funkcí s rovnicemi pro vytváření různých křivek náběhu/doběhu
Dostupné křivky jsou:

  • zpět
  • odskok
  • kruh
  • kubický
  • elastické
  • výstava
  • čtyřkolka
  • kvart
  • kvint
  • sinus

Můžete si však vytvořit svou vlastní funkci, pokud je to funkce, která bude přijímat 1 parametr v rozsahu od 0 do 1 (1 představuje celkovou dobu trvání animace) a vrací jinou hodnotu, rovněž v rozsahu od 0 do 1.

úklon/pohyb

V tomto balíčku jsou exportovány dvě funkce:tweened a spring .

Oba vrátí reaktivní hodnotu, která bude interpolovat mezi hodnotami daný sadou parametrů.

Všimněte si, že tyto funkce nemusí nutně nic vizuálně animovat, ale spíše vytvářejí rampu mezi hodnotami. Tyto hodnoty pak lze zobrazit nebo přiřadit něčemu jinému, například vlastnostem CSS.

Obě funkce mohou interpolovat čísla, data, pole a objekty. Můžete také poskytnout jinou funkci pro interpolaci hodnot.

doplněno

Pojďme inicializovat novou aplikaci Svelte, abychom viděli, jak to funguje.

npm init vite

✔ Project name: · svelte-animations
✔ Select a framework: · svelte
✔ Select a variant: · svelte-ts

cd svelte-web-components
pnpm install //use the package manager you prefer
pnpm run dev

// remove default Counter component
rm src/lib/Counter.svelte

Vymažte App.svelte aby obsahovala pouze to, co nyní potřebujeme.

<script>
    // add imports here
</script>

<main>
</main>

<style>
  :root {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
      Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  }

  main {
    text-align: center;
    padding: 1em;
    margin: 0 auto;
  }

  :global(main > * + *)  {
    margin-top: 24px;
  }
</style>

Vytvořím novou komponentu s názvem Tasks.svelte uvnitř lib složku.

<script lang="ts">
  import { tweened } from 'svelte/motion';
  export let tasks: { id; title; date }[] = [];

  let selected;
  tasks = tasks.sort((a, b) => {
    if (a.date > b.date) {
      return 1;
    } else if (a.date === b.date) {
      return 0;
    } else {
      return -1;
    }
  });

  function pad(num) {
    if (num < 10) {
      return `0${num}`;
    }
    return num;
  }

  function getDate(date) {
    return date
      ? `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(
          date.getDate(),
        )}`
      : '';
  }

  function getTime(date) {
    return date ? `${pad(date.getHours())}:${pad(date.getMinutes())}` : '';
  }

  let now = new Date();
  let date = tweened(now, { duration: 500 });

  function selectTask(task) {
    selected = task.id;
    date.set(task.date);
  }
</script>

<div class="task-view">
  <div class="task-list">
    <h2>Next tasks</h2>
    <ul>
      {#each tasks as task}
        <li
          class={selected === task.id ? 'selected' : ''}
          on:click={() => selectTask(task)}
        >
          {task.title}
        </li>
      {/each}
    </ul>
  </div>
  <div class="task-details">
    <h2>When?</h2>
    {#if selected}
      <p>{getDate($date)}</p>
      <p>{getTime($date)}</p>
    {/if}
  </div>
</div>

<style>
  .task-view {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    width: 300px;
    border: 2px solid #4f4f4f;
    border-radius: 8px;
    padding: 16px;
  }
  li {
    padding: 4px 8px;
  }
  li.selected {
    background-color: lightcyan;
  }
  li:hover {
    background-color: lightgray;
  }
</style>

Komponenta obdrží seznam úkolů s názvem a datem a poté kliknutím na kterýkoli z nich vytvoříme přechod mezi těmito daty. (Podívejte se, jak se automaticky přihlašujeme k reaktivní hodnotě a před název proměnné přidáváme $ )

Pojďme aktualizovat aplikaci, aby tuto komponentu používala.

<script lang="ts">
  import Tasks from './lib/Tasks.svelte';

  let tasks = [
    { id: 1, title: 'Meeting', date: new Date('2021-12-17T03:24:00') },
    { id: 2, title: 'Gym', date: new Date('2021-08-22T09:12:00') },
    { id: 3, title: 'Movie', date: new Date('2021-09-01T22:07:00') },
  ];
</script>

<main>
  <Tasks {tasks} />
</main>
<!-- ... -->

A výsledek vypadá takto:

V tomto příkladu animujeme hodnotu, ale tyto změny můžeme také aplikovat na vlastnosti CSS.

Vytvořme další příklad, který toho dosáhne. (Tweened.svelte ).

<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';

  const toColor = tweened([255, 0, 0], {
    duration: 2000,
    easing: cubicOut,
  });

  let loop = () =>
    toColor
      .set([255, 0, 0])
      .then(() => toColor.set([0, 255, 0]))
      .then(() => toColor.set([0, 0, 255]))
      .then(() => loop());
  loop();
</script>

<div style={'background-color:rgb(' + $toColor.join(',') + ')'} />

<style>
  div {
    display: block;
    width: 100px;
    height: 100px;
  }
</style>

Zde jsme vytvořili jeden div a použijte doplněné k interpolaci hodnot pole.

Když nastavíme hodnotu pomocí set funkce, vrátí příslib, který se vyřeší, když je dosaženo konečné hodnoty (pro naše účely animace skončila). Poté spustíme novou hodnotu pomocí set znovu. Můžeme vidět v akci, jak můžeme interpolovat hodnoty pole.

Musíme pamatovat na aktualizaci naší aplikace

<script lang="ts">
// ...
  import Tweened from './lib/Tweened.svelte';
// ...
</script>

<main>
  <!-- ... -->
  <Tweened />
</main>

Možné parametry pro doplnění jsou:delay (čas před spuštěním), duration (v milisekundách), easing (jedna z výše uvedených funkcí náběhu/doběhu), interpolate (a (from, to) => t => value) funkce

Jaro

Jaro funguje jinak při přechodu proměnné z jedné hodnoty na druhou. Můžeme nastavit tři parametry:stiffness , damping , který nastaví, jak se pružina chová při usazování v konečné hodnotě, a precision , který určí, kdy bude hodnota považována za vypořádanou.

Vytvořme novou komponentu s názvem Spring.svelte

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

    const number = spring(0,{
    stiffness: 0.1,
    damping: 0.08
});

function changeValueTo(newValue) {
    number.set(newValue)
}

function resetValue() {
    number.set(0, {hard:true})
}

</script>

<div>
    <span>{$number.toFixed(1)}</span>
    <button on:click={() => changeValueTo(10)}>To 10</button>
    <button on:click={() => changeValueTo(100)}>To 100</button>
    <button on:click={() => changeValueTo(1000)}>To 1000</button>
    <button  on:click={() => resetValue()}>Reset</button>
</div>

<style>
    div {
        display: flex;
        flex-direction:column;
        max-width:300px;
    }
</style>

Naše komponenta má number reaktivní hodnota, která se při změně odrazí, až se nakonec ustálí v požadovaném výsledku. Čím větší je vzdálenost k cílové hodnotě, tím více se odskočí .

Abychom mohli komponentu importovat, musíme aktualizovat naši aplikaci.

<script lang="ts">
// ...
  import Spring from './lib/Spring.svelte';
// ...
</script>

<main>
  <!-- ... -->
  <Spring />
</main>

Takto vypadá konečný výsledek.

úhlopříčka/přechod

Přechod je funkce s následujícím podpisem:

(node: HTMLElement, params: any) => {
    delay?: number,
    duration?: number,
    easing?: (t: number) => number,
    css?: (t: number, u: number) => string,
    tick?: (t: number, u: number) => void
}

Modul Svelte/transition obsahuje řadu funkcí, které nám umožní animovat náš DOM:blur , draw , fade , fly , scale , slide a crossfade (tato poslední funkce vrací dvě přechodové funkce)

Používají se s transition , in nebo out směrnice.
Přechod se provede, když prvek vstoupí nebo opustí DOM. S touto direktivou introstart jsou dostupné čtyři události , introend , outrostart , outroend jsou spuštěny vždy, když počáteční nebo závěrečná animace začíná a končí.

in a out direktivy fungují jako transition , ale fungují pouze tehdy, když je prvek přidán nebo odebrán.

Vytvořte novou komponentu s názvem Transition.svelte .

<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import {
    blur,
    crossfade,
    draw,
    fade,
    fly,
    scale,
    slide,
  } from 'svelte/transition';

  let show = false;
  let interval;

  let [from, to] = crossfade({
    fallback: () => {
      return { css: (t, u) => 'color:red' };
    },
  });

  onMount(() => {
    interval = setInterval(() => {
      show = !show;
    }, 2000);
  })

  onDestroy(() => {
    if (interval) {
      clearInterval(interval);
    }  
  });
</script>

<div class="playground">
  <div class="transition-item">
    <svg
      fill="#ffffff"
      width="32"
      height="32"
      viewBox="0 0 16 16"
      xmlns="http://www.w3.org/2000/svg"
    >
      {#if show}
        <path
          in:draw={{ duration: 1500 }}
          d="M1.414213562373095 0 16 14.585786437626904 L14.585786437626904 16 L0 1.414213562373095"
        />
        <path
          in:draw={{ duration: 1500 }}
          d="M14.585786437626904 0 L16 1.414213562373095 L1.414213562373095 16 L0 14.585786437626904"
        />
      {/if}
    </svg>
  </div>
  <div class="transition-item teleport">
    <div>
      {#if show}
        <span in:from={{ key: 'a' }} out:to={{ key: 'a' }}>cross...</span>
      {/if}
    </div>
    <div>
      {#if !show}
        <span in:from={{ key: 'a' }} out:to={{ key: 'a' }}>...fade</span>
      {/if}
    </div>
  </div>

  {#if show}
    <div class="transition-item" transition:blur>
      <span>Blur</span>
    </div>
    <div class="transition-item" transition:fade>
      <span>Fade</span>
    </div>
    <div class="transition-item" transition:fly={{ x: 30 }}>
      <span>Fly</span>
    </div>
    <div class="transition-item" transition:scale={{ start: 10 }}>
      <span>Scale</span>
    </div>
    <div class="transition-item" transition:slide>
      <span>Slide</span>
    </div>
  {/if}
</div>

<style>
  .teleport {
    display: flex;
    flex-direction: row;
    justify-content: center;
    width: 200px;
    margin-left:auto;
    margin-right:auto;
    border: 2px solid #4f4f4f;
    border-radius: 8px;
    padding: 16px;

  }
  .teleport > div {
      width: 100px;
    }

  svg {
    height: 128px;
    width: 128px;
  }

  path {
    stroke: black;
  }

  .transition-item + .transition-item {
    margin-top: 40px;
  }
</style>

Do tohoto příkladu jsem přidal všechny poskytnuté animace, takže si s nimi můžete pohrát.

Vlastní přechody

Můžeme vytvořit vlastní přechody vytvořením funkce, která přijímá HTML element a konfigurační objekt a vrací objekt s požadovanými vlastnostmi.

Vytvoříme novou funkci s názvem skew .

export function skew(node: HTMLElement, {delay = 0, duration = 1000, easing = cubicInOut, deg = 45} = {}) {
        const style = getComputedStyle(node);
        const target_opacity = +style.opacity;
        const transform = style.transform === 'none' ? '' : style.transform;
        return {
            delay,
            duration,
            easing,
            css: (_t, u) => `
                transform: ${transform} skew(${deg * u}deg);
                opacity: ${target_opacity * _t}
            `
        };
}

delay , duration a easing jsou docela standardní pro všechny dodávané funkce, takže je ponecháme stejné pro snadné použití. kouzlo se děje v našem css vlastnictví. Na základě našich parametrů přidáme skew transformaci. u není nic jiného než 1-_t , takže v tomto případě začneme od deg (je použito zkosení) na 0 (žádné zkosení), když je prvek zobrazen.
Po odstranění se stane opak.

Pojďme to otestovat vytvořením nové komponenty. (Skew.svelte )

<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import { skew } from './skew';

  export let skewOptions = {};

  let show = false;
  let interval;

  onMount(() => {
    interval = setInterval(() => {
      show = !show;
    }, 2000);
  });

  onDestroy(() => {
    if (interval) {
      clearInterval(interval);
    }
  });
</script>

<div class="playground">
  {#if show}
    <div class="transition-item" transition:skew={skewOptions}>
      <span>Skew</span>
    </div>
  {/if}
</div>

štíhlý/animovaný

Tento balíček exportuje jednu funkci:flip .
Animace se mají používat s animate směrnice.

Všimněte si, že existuje požadavek na použití této směrnice.
Prvek, který používá direktivu animate, musí být bezprostředním potomkem každého zakódovaného bloku.

Animace se spouští, když prvky each blok jsou změněny.

Podpis animace je:

(node: HTMLElement, { from: DOMRect, to: DOMRect } , params: any) => {
    delay?: number,
    duration?: number,
    easing?: (t: number) => number,
    css?: (t: number, u: number) => string,
    tick?: (t: number, u: number) => void
}

Jak můžete vidět, podpis je velmi podobný podpisu přechodů. Tuto podobnost využijeme později.

Vytvořte novou komponentu a otestujte, co flip a animate směrnice dělat.

<!-- Flip.svelte -->

<script lang="ts">
  import { flip } from 'svelte/animate';
  let things = [
    { id: 1, name: 'foo', ready: true },
    { id: 2, name: 'bar', ready: false },
    { id: 3, name: 'baz', ready: true },
    { id: 4, name: 'fizz', ready: false },
  ];
  let sortBy = { field: 'id', order: 'DESC' };

  let sortedThings = things;

  function sortById() {
    if (
      sortBy.field !== 'id' ||
      (sortBy.field === 'id' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return 1;
        } else if (a.id < b.id) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return -1;
        } else if (a.id < b.id) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'DESC' };
    }
  }
  function sortByName() {
    if (
      sortBy.field !== 'name' ||
      (sortBy.field === 'name' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return 1;
        } else if (a.name < b.name) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return -1;
        } else if (a.name < b.name) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'DESC' };
    }
  }
  function sortByReadyState() {
    if (
      sortBy.field !== 'ready' ||
      (sortBy.field === 'ready' && sortBy.order === 'DESC')
    ) {
      sortedThings = [
        ...sortedThings.filter((x) => x.ready),
        ...sortedThings.filter((x) => !x.ready),
      ];
      sortBy = { field: 'ready', order: 'ASC' };
    } else {
      sortedThings = [
        ...sortedThings.filter((x) => !x.ready),
        ...sortedThings.filter((x) => x.ready),
      ];
      sortBy = { field: 'ready', order: 'DESC' };
    }
  }
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:flip>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  td {
    width: 100px;
  }
  .container {
    width: 100vw;
    display: flex;
    flex-direction: row;
  }
  table,
  tr,
  td,
  th {
    border: 1px solid gray;
    border-collapse: collapse;
  }
  th {
    cursor: pointer;
  }
</style>

Vytvořili jsme tabulku se 4 řádky a možností seřadit řádky podle různých vlastností.

Prvky jsou uvnitř každého bloku s klíčem (nezapomeňte, že je to požadavek).
Jedna ze skvělých věcí na animate směrnice je, že budou animovány pouze položky, které se změní. Zbytek zůstane takový, jaký byl.

Výsledek vypadá takto.

rozšíření a opětovné použití animací s přechody

Protože jsou přechody a animace velmi podobné, můžeme použít přechody k rozšíření flip nebo vytvořte nové animace.

animace z přechodů

Pokud se podíváme na oba typy funkcí, můžeme vidět, že jsme schopni vytvořit obalovou funkci, která převede náš přechod na animaci.

export function toAnimation<T>(
  fn: (node: HTMLElement, params) => T,
): (node: HTMLElement, { from, to }, params) => T {
  return (node, _animations, params = {}) => {
    return fn(node, params);
  };
}

Poté můžeme převést jeden z našich přechodů a aplikovat jej pomocí direktivy animate.

<!--AnimationFromTransition.svelte -->
<script>
  import { fade } from 'svelte/transition';
  import { toAnimation } from './toAnimation';

  let fadeAnimation = toAnimation(fade);

 // ... same as Flip.svelte
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:fadeAnimation={{ duration: 400 }}>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  /* same as Flip.svelte*/
</style>

Nyní se přeuspořádané prvky místo přesouvání roztmívají/ztrácejí.

Rozšíření otočení

Animaci převrácení můžeme rozšířit i o přechody. Znovu vytvořím funkci wrapper.

// extendFlip.ts

import { flip } from 'svelte/animate';
export function extendFlip(fn) {
  return (node, animations, params = {}) => {
    let flipRes = flip(node, animations, params);
    let transitionRes = fn(node, params);

    let getTransform = (str) => {
      let results = str.match(/transform: (.*);/);
      if (results && results.length) {
        return results[results.length - 1];
      }
      return '';
    };

    let mergeTransform = (css1, css2) => {
      return `transform: ${getTransform(css1)} ${getTransform(css2)};`;
    };

    return {
      ...flipRes,
      css: (t, u) =>
        `${transitionRes.css(t, u)}; ${mergeTransform(
          flipRes.css(t, u),
          transitionRes.css(t, u),
        )};`,
    };
  };
}

Naše funkce získá přechodovou funkci a sloučí vlastnost transform, kterou vrací, s tou z flip .

Nyní se podívejme na mírně upravenou verzi předchozí komponenty:

<script>
  import { scale, blur } from 'svelte/transition';
  import { extendFlip } from './extendFlip';

  let flipAndBlur = extendFlip(blur);
  let flipAndScale = extendFlip(blur);

  let things = [
    { id: 1, name: 'foo', ready: true },
    { id: 2, name: 'bar', ready: false },
    { id: 3, name: 'baz', ready: true },
    { id: 4, name: 'fizz', ready: false },
  ];

  let sortBy = { field: 'id', order: 'DESC' };

  let sortedThings = things;

  function sortById() {
    if (
      sortBy.field !== 'id' ||
      (sortBy.field === 'id' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return 1;
        } else if (a.id < b.id) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.id > b.id) {
          return -1;
        } else if (a.id < b.id) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'id', order: 'DESC' };
    }
  }
  function sortByName() {
    if (
      sortBy.field !== 'name' ||
      (sortBy.field === 'name' && sortBy.order === 'DESC')
    ) {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return 1;
        } else if (a.name < b.name) {
          return -1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'ASC' };
    } else {
      sortedThings = things.sort((a, b) => {
        if (a.name > b.name) {
          return -1;
        } else if (a.name < b.name) {
          return 1;
        }
        return 0;
      });
      sortBy = { field: 'name', order: 'DESC' };
    }
  }
  function sortByReadyState() {
    if (
      sortBy.field !== 'ready' ||
      (sortBy.field === 'ready' && sortBy.order === 'DESC')
    ) {
      sortedThings = [
        ...sortedThings.filter((x) => x.ready),
        ...sortedThings.filter((x) => !x.ready),
      ];
      sortBy = { field: 'ready', order: 'ASC' };
    } else {
      sortedThings = [
        ...sortedThings.filter((x) => !x.ready),
        ...sortedThings.filter((x) => x.ready),
      ];
      sortBy = { field: 'ready', order: 'DESC' };
    }
  }
</script>

<div class="container">
  <table>
    <tr>
      <th on:click={sortById}>id</th>
      <th on:click={sortByName}>name</th>
      <th on:click={sortByReadyState}>ready</th>
    </tr>
    {#each sortedThings as thing (thing.id)}
      <tr animate:flipAndBlur>
        <td>{thing.id}</td>
        <td>
          {thing.name}
        </td>
        <td><input type="checkbox" bind:checked={thing.ready} /></td>
      </tr>
    {/each}
  </table>
</div>

<style>
  td {
    width: 100px;
  }
  .container {
    width: 100vw;
    display: flex;
    flex-direction: row;
  }
  table,
  tr,
  td,
  th {
    border: 1px solid gray;
    border-collapse: collapse;
  }
  th {
    cursor: pointer;
  }
</style>

A výsledky:

Rozostření + převrácení

Měřítko + překlopení

Závěrečná slova

Svelte odvedl skvělou práci, díky které byly animace a přechody snadné s jejich API. Poskytované funkce fungují skvěle v mnoha scénářích.

Doufám, že vás tento příspěvek na blogu zve k prozkoumání API, rozšíření toho, co již existuje, a sdílení s ostatními uživateli.
Tyto příklady jsou k dispozici v tomto repozitáři.

Toto Dot Labs je moderní webová poradenská služba zaměřená na pomoc společnostem realizovat jejich úsilí o digitální transformaci. Odborné architektonické pokyny, školení nebo konzultace v oblasti React, Angular, Vue, Web Components, GraphQL, Node, Bazel nebo Polymer naleznete na adrese thisdotlabs.com.

Toto Dot Media se zaměřuje na vytvoření inkluzivního a vzdělávacího webu pro všechny. Prostřednictvím událostí, podcastů a bezplatného obsahu vás informujeme o pokroku na moderním webu. Chcete-li se to dozvědět, navštivte thisdot.co.