🏇

Svelteでviewport内に来たときに表示アニメーション

2023/09/24に公開

毎回どうやってやればよかったっけってなるのでメモがてら。

どういうやつ?

こういうやつです。

表示アニメーションのスクリーンショット

viewportに入った時にアニメーションされます。

表示アニメーションのスクリーンショット2

コード

↓のようなアクションを作り、

tweens.js
import { bounceInOut } from "svelte/easing";
import { tweened } from "svelte/motion";

export const tweens = (
  node,
  { duration, animation, easing = bounceInOut, delay = 0 },
) => {
  const store = tweened(0, { duration, easing });
  const unsubscribe = store.subscribe((value) => animation(node, value));

  const callback = (entries) => {
    const entry = entries[0];
    let inView = entry.isIntersecting;

    if (!inView) return;

    setTimeout(() => store.set(1), delay);
  };

  const intersectionObserver = new IntersectionObserver(callback, {});
  intersectionObserver.observe(node);

  return {
    destroy() {
      intersectionObserver.disconnect();
      unsubscribe();
    },
  };
};

.svelteでは↓のようにuse:tweensとして使用します。

App.svelte
<script>
  import { tweens } from "./tweens.js";

  const letterAnimation = (node, value) => {
    node.style.opacity = value.toString();
    node.style.transform = `translateY(${(1 - value) * -20}px)`;
  };
</script>

<main>
  <p>
    {#each "Proof that Tony Stark has a heart." as letter, index}
      <span
        class="letter"
	
        use:tweens={{
          duration: 500,
          animation: letterAnimation,
          delay: index * 100,
        }}>{letter}</span
      >
    {/each}
  </p>
</main>

<style>
...
</style>

詳しく解説

2段階で解説します。

  1. アニメーションさせるアクションの作成
  2. 表示時にアニメーションさせる。

アニメーションさせるアクションの作成

アクションとは?

アクションというのは、次のような要素と任意のオプションを引数に取り、クリーンアップ関数を返す関数です。

export const mySuperAction = (
  node, // element
  { ... }, // option
) => {
  // some process
  
  return {
    destroy() {
	// clean up.
    },
  };
};

これを、svelteファイルでは次のようにuse:mySuperActionと追加することで要素がマウントされた時に、mySuperActionが実行されます。

<div use:mySuperAction>
...
</div>

useが追加された要素は引数に渡されます。

より詳しく知りたい方は、↓をご覧ください。

アニメーション

そして、そんなアクションの中ではtweenedというsvelteのビルトインパッケージを使ってアニメーションを作っています。

tweenedは、内部的にはストアですが、与えた値までを瞬間的にではなく、段階的に変更してくれるように実装されています。

例えば、以下のようなボタン操作によって数値が更新されるようなUIをストアで作った例です。

これは、通常、ストアを使って↓みたいに書かれているわけですが、

store.js
import { writable } from 'svelte/store';

export const count = writable(0);

これをそのままtweenedに書き換えると...

tweened.js
// import { writable } from 'svelte/store';
import { tweened } from "svelte/motion"

// export const count = writable(0);
export const count = tweened(0)

という動作になります。段階的に値を変更してくれるストアという意味がわかるのではないでしょうか?

最初、このtweenedで作ったストアの値を0に設定し、任意のタイミングで1にすることで、アニメーションさせているというわけです。

  const store = tweened(0, { duration, easing });
  ...
  const callback = (entries) => {
   ...

    setTimeout(() => store.set(1), delay);// ここで1に
  };

表示時にアニメーションさせる。

そして、その任意のタイミングというのが、ここではviewportに表示された時です。

それを実現するのにIntersection Observer APIを使います。

const intersectionObserver = new IntersectionObserver(callback, {});
intersectionObserver.observe(node);

callback内でisIntersectingを見ることで、viewport内であればアニメーションするようにしています。

  const callback = (entries) => {
    const entry = entries[0];
    let inView = entry.isIntersecting;

    if (!inView) return;

    setTimeout(() => store.set(1), delay);
  };

余談ですが、AstroからSvelteを使っている場合はこんなことしなくても、Astro側でSvelteコンポーネントにclient:visibleを指定することで、同じように表示時にアニメーションさせることができます。

Discussion