Motionを触ってみる

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

## :thinking: Motionとは

MotionはFramer MotionとMotion Oneが統合されて生まれたアニメーションライブラリです。ユーザーの操作や状況に併せたアニメーションを簡単に実装できるのが特徴です。

元々Framer（ノーコードのWebサイト作成ツール）から提供されていたアニメーションライブラリでしたが、作者のMatt Perry氏がFramerから独立したタイミングで、ライブラリも独立しました。

https://motion.dev/blog/framer-motion-is-now-independent-introducing-motion

Framer MotionはReact向けのアニメーションライブラリでしたが、MotionではVanilla JS向けのAPIが提供されています。
※Motion Oneの機能がMotionとして提供されている形ですが、Framer MotionはMotion Oneを単純にラップしているだけでもないので、同等の機能を提供するわけではありません。

本記事では、Vanilla JS向けのAPI（Motion Oneの機能）を使ったアニメーションの実装方法を紹介します。

## :wrench: 環境構築

環境構築はViteのテンプレートを使って行います。以下のコマンドを実行して、プロジェクトを作成します。

```bash
npm create vite@latest my-motion -- --template vanilla
cd my-motion
npm install motion
```

CDNを使って読み込むこともできます。
今回はViteを使っていますが、LPなど簡易的なWebサイトの制作ではCDN呼び出しを使っても良さそうです。

```html
<script src="https://cdn.jsdelivr.net/npm/motion@11.11.17/dist/motion.js"></script>
<script>
  const { animate, scroll } = Motion
</script>
```

jsファイル上でESモジュールを直接読み込むこともできます。

```js
import { animate, scroll } from "https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm";
```

:::note
Motionはとても軽量で、すべての機能を使ってもgzip圧縮されたサイズは約17kbです。GSAP（約23kb）と比較しても軽量ですし、tree-shakingも効くので、よりサイズは小さくなります。
機能が制限される代わりにバンドルサイズを落とした`motion/mini`（約2.5kb！）も提供されており、合わせて利用することもできます。
:::

## :robot: バージョン確認

```bash
"motion": "^11.11.17"
```

## :school: motionの基本的な使い方

もっとも簡単にMotionを使う方法は、`animate`関数を使ってアニメーションを定義することです。

```js
import { animate } from "motion";

// Element(s)
const box = document.getElementById("box");

animate(box, { opacity: 0 }, { duration: 0.5 }); // 0.5秒でopacityを0にする

// Selectors
animate("div", { x: [0, 100] }, { ease: "easeIn" }); // 100px右に移動
```

第一引数にアニメーションを適用する要素、第二引数にアニメーションのプロパティ、第三引数にオプションを定義します。

アニメーションのプロパティには数値を配列で渡すことで、アニメーションの開始と終了の値を指定できます。

オプションからは、アニメーションの挙動を調整できます。

<img width="50%" alt="1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512467/d86a019d-09b7-c279-789a-54dd2b227b2c.gif">

https://motion.dev/docs/animate

## :compression: 便利な関数

ユーザーインタラクションに応じたアニメーションを実装するために、Motionには便利な関数が用意されているので幾つか紹介します。

### `scroll`

`animate`関数から返される`AnimationPlaybackControls`を渡すことで、スクロール時のアニメーションを実装できます。

```js
import { animate, scroll } from "motion";

const box = document.getElementById("box");
const animation = animate(box, { opacity: 0 });

scroll(animation);
```

アニメーションの進行度は、スクロールの進行度と連動します。

第二引数でオプションを渡すことで、連動する対象の要素やスクロールの軸、進行度の範囲を指定できます。

<img width="50%" alt="2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512467/f84f2b88-a81e-44ff-6f82-7e0416cdf6a6.gif">

https://motion.dev/docs/scroll

### `inView`

要素がビューポートに入った際、callback関数を実行します。
実装的にはIntersection Observer APIのラッパーっぽいです。

```js
import { inView } from "motion";

const box = document.getElementById("box");

inView(box, () => {
  console.log("in view");
});
```

<img width="50%" alt="3.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512467/bcc81c52-b770-c03e-4104-70b78882f01c.gif">

`inView`に渡す関数は、通常要素がビューポートに入った際、一度だけ実行されます。
このcallback関数内で関数を返すことで、要素がビューポートから外れた際に実行される関数を定義できます。

```js
inView(box, () => {
  console.log("in view");

  return () => {
    console.log("out of view");
  };
});
```

この場合、ビューポートを出入りするたびにcallbackが実行される様になります。

https://motion.dev/docs/inview

### `stagger`

複数要素をズラしてアニメーションしたい場合、`stagger`関数を利用することで簡単に実現できます。

```js
import { animate, stagger } from "motion";

animate(
  ".item",
  {
    opacity: [0, 1],
    translateY: [100, 0],
  },
  {
    duration: 0.5,
    type: "spring",
    delay: stagger(0.1),
  }
);
```

<img width="50%" alt="3.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512467/38f475eb-5e6d-7439-2f2b-faae7215becf.gif">

https://motion.dev/docs/stagger

### `spring`

`spring`関数を使うことで、スプリングアニメーションを実装できます（ばねのような挙動）

```js
import { animate, spring } from "motion";

animate(div, { x: 100 }, { type: spring });
```

<img width="50%" alt="4.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512467/d32293aa-d07d-3c9a-ea97-28b5130a3cc7.gif">

※motionではデフォルトで使えるアニメーションです（`type: "spring"`としても同じ）

https://motion.dev/docs/spring

## :space_invader: Motionを使ったUIアニメーション

Motionの基本的な使い方を紹介しましたが、これだけでもアニメーション実装で十分な機能が揃っていることがわかりました。
ここからは、幾つかUIアニメーションの実装例を紹介します。

### 通知アニメーション

簡単な通知表示にアニメーションを適用してみました。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="NPKKmzQ" data-pen-title="easy notify animation" data-preview="true" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/NPKKmzQ">
  easy notify animation</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

`animate`関数はPromiseを返すので、ユーザーのアクションに応じたアニメーションを簡単に実装できます。

```js
async delete(id) {
  const targetEl = document.querySelector(`.notify[data-id="${id}"]`);
  await animate(targetEl, { opacity: [1, 0], x: [0, 100] });
  this.notifyArray = this.notifyArray.filter((item) => item.id !== id);
  this.render();
}
```

### ハンバーガーメニュー

ハンバーガーメニューのアニメーションを実装してみました。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="ZYzzZRO" data-pen-title="global menu open/close animation" data-preview="true" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/ZYzzZRO">
  global menu open/close animation</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

ここでもPromiseが活用できます。
`stagger`関数を用いることで細かい計算を放棄できる点も嬉しい。

```js
await animate(
  ".bg div",
  { width: "100%" },
  { duration: 0.5, type: "spring", delay: stagger(0.1) }
);
await animate(
  ".global .menu li",
  {
    opacity: [0, 1],
    y: [-100, 0],
  },
  { delay: stagger(0.1) }
);
```

ここではopacityの指定などを配列で行っています。
from~toの関係で記述してeaseの設定値に従い変化しますが、offsetを用いる事でタイミングを自分で設定することもできます。

```js
animate(
  particle,
  { opacity: [0, 1, 0] },
  { offset: [0, 0.9, 1], duration: 0.7 }
};
```

### SVGアニメーション

SVGをアニメーションさせることもできます。
こちらの実装例はドキュメントに記載された実装例とほぼ同じです。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="qEWEVqw" data-pen-title="favorite button" data-preview="true" data-user="tenorichan" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tenorichan/pen/qEWEVqw">
  favorite button</a> by tenori (<a href="https://codepen.io/tenorichan">@tenorichan</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

単純な移動からfillの変更、flubberのような（2点間のパスを補完してくれる）ライブラリを用いることで、モーフィングアニメーションも実現できます。

## :mortar_board: 終わり

元々Framer MotionはReactで宣言的に書けるのが強みのライブラリでしたが、MotionのVanilla JS向けAPI（Motion One）はかなり命令的な書き方になっています。VueやSvelteで利用する場合は、宣言的に書けるようなラッパーがないと辛くなりそうです。

一方でペライチLPなどをよりリッチにしたい場合には必要十分で、ユーザーの操作や状況に応じたアニメーションを簡単に実装できるのは魅力的です。

Framer Motionの記事はたくさんあるので、React向けAPIである`motion/react`については触れませんでした。
ですがReactで利用する場合に`motion/react`は宣言的にシンプルな記述で自由度の高いアニメーションを実装できるので、オススメのライブラリです。

ドキュメントも豊富で、カスタマイズ要素も多くあるので色々な表現に利用できます。よりリッチなアニメーションを実装したい場合には、是非利用してみてください。

https://motion.dev/docs

## :books: 参考

- [Framer Motion is now independent, introducing Motion](https://motion.dev/blog/framer-motion-is-now-independent-introducing-motion)
- [motion doocs](https://motion.dev/docs)

## :rice_ball: 一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。