GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

マイクロアニメーションコンポーネントを実装しました #GameWith #TechWith #vue #vuejs #vuecli #lottie #lottieweb

こんにちは! @53able です。

今回は、再利用可能なマイクロアニメーションを実装した話を行っていきたいと思います。

Web コンポーネント(Web component) カスタム要素の data-animation 属性に指定したアニメーションが再生される仕様のコンポーネントを作りました。

<gds-core-micro-animation
    data-element=""
    data-animation=""
    >
   任意の HTML
</ gds-core-micro-animation>
  • data-element: アニメーション要素のコンテナ要素
  • data-animation: JSONファイル名

指定するアニメーションは、Adobe After Effects で作成したアニメーションを JSON としてエクスポートされたファイルです。

airbnb.design

開発環境

Vue CLI プロジェクト の GDS (GameWithDesignSystem) でコンポーネントを追加実装しました。

GDS は、こちらの記事で紹介しております。

tech.gamewith.co.jp

本エントリーの公開時点での各種依存バージョンは、次の通りです。

  • @vue/cli v5.0.8
  • typescript v4.8.2
  • lottie-web v5.9.6
  • vue v2.7.10
  • js-fire v1.0.0
  • template-file v5.1.0
  • zx v7.0.7
  • NodeJS v16.14.2
  • pnpm v7.9.3

ディレクトリ構成

CoreMicroAnimation.vue がビルドのエントリーポイントとなって、Web コンポーネントのカスタムタグを定義するバンドルファイルが出力されます。

setup() は、CoreMicroAnimationSetup.ts に記述されており、CoreMicroAnimation.vue からインポートされています。

src
├ assets
│ └ lottie
│   └ core-micro-animation
│     ├ sample_0.json
│     ├ sample_1.json
│     └ sample_2.json
└ packages
  └ Core
    ├ global
    │ ├ CoreMicroAnimation.vue
    │ └ CoreMicroAnimationSetup.ts
    └ partial
      └ dependency
        └ CoreMicroAnimationData.ts

アニメーションファイル名

data-animation 属性で JSON ファイル名を指定するため、 props の型定義が必要になります。

そこで、CoreMicroAnimationData.ts にて JSON ファイル名を記述し、そこから Union 型を形成します。

export namespace CoreMicroAnimationData {
  export const names = [
    "sample_0",
    "sample_1",
    "sample_2",
  ] as const;
  export type Name = typeof names[number]; // "sample_0" | "sample_1" | "sample_2"
}

SFC

lottie-web で、 <div ref="containerRef" /> に SVGアニメーションをマウントされます。

propsdataElement では、親要素を span または div に指定できるオプションを設けました。

<component>:is へ代入すると、指定した HTML タグがレンダリングされます。

ja.vuejs.org

<template>
  <component
    :is="dataElement"
  >
    <slot></slot>
    <div ref="containerRef" />
  </component>
</template>

<script lang="ts">
import { PropType } from "vue";

import {
  CoreMicroAnimationSetup as setup,
  CoreMicroAnimationProps as Props,
} from "./CoreMicroAnimationSetup";

const props = {
  dataElement: {
    type: String as PropType<Props["dataElement"]>,
    default: "div",
  },
  dataAnimation: {
    type: String as PropType<Props["dataAnimation"]>,
    default: undefined,
  },
};

export default defineComponent({
  props,
  setup,
});
</script>

Composition API: setup()

"vue" からのインポートは、省略できる仕組みを導入しています。

tech.gamewith.co.jp

アニメーションを動作させる実装は、以下のとおりです。

import lottie, { AnimationItem } from "lottie-web";

import { CoreMicroAnimationData } from "../partial/dependency/CoreMicroAnimationData";

type Props = {
  dataElement: "span" | "div";
  dataAnimation?: CoreMicroAnimationData.Name;
};

export type CoreMicroAnimationProps = Props;

type State = {
  lottieAnimation?: AnimationItem;
};

export const CoreMicroAnimationSetup = (
  props: Props
): void => {
  const state = reactive<State>({
    lottieAnimation: undefined,
  });
  const containerRef = templateRef<HTMLElement>("containerRef");

  const loadAnimationData = async (name: CoreMicroAnimationData.Name) => {
    try {
      const data = await import(`src/assets/lottie/core-micro-animation/${name}.json`);
      return data;
    } catch (error) {
      console.error(`lottie animation data load error. name: ${name}`);
    }
  };

  onMounted(async () => {
    const name = props.dataAnimation;
    if (name === undefined) return;
    if (CoreMicroAnimationData.names.indexOf(name) < 0)
      throw new Error(
        `data-animation の値が不正です。\n有効値: \n  ${CoreMicroAnimationData.names.join(
          "\n  "
        )}`
      );
    const data = await loadAnimationData(name);
    if (data === undefined) return;
    state.lottieAnimation = lottie.loadAnimation({
      container: containerRef.value,
      renderer: "svg",
      loop: true,
      autoplay: true,
      animationData: data,
    });
  });
};

ダイナミックインポート

Lottile のアニメーションデータである JSON ファイルの読み込みをダイナミックインポートにすると、再生するアニメーションデータのみ読み込まれます。

const loadAnimationData = async (name: CoreMicroAnimationData.Name) => {
  try {
    const data = await import(
      `src/assets/lottie/core-micro-animation/${name}.json`
    );
    return data;
  } catch (error) {
    console.error(`lottie animation data load error. name: ${name}`);
  }
};

非同期Webコンポーネントモードでビルドすると、sample_0.json , sample_1.json , sample_2.json が分散されてバンドルファイルが出力されます。

tsconfig.json は、 "module": "ESNext" に設定すると、バンドルファイルが思惑通りにコード分割されるようになります。

typescript-jp.gitbook.io

アニメーション開始

アニメーションの動作を開始するときに必要な container にあたる要素得るには、VueUse の templateRef を用いると便利です。

vueuse.org

onMounted のタイミングで、<div ref="containerRef" /> を参照できます。

const containerRef = templateRef<HTMLElement>("containerRef");

onMounted(() => {
  state.lottieAnimation = lottie.loadAnimation({
    container: containerRef.value,
    renderer: "svg",
    loop: true,
    autoplay: true,
    animationData: data,
  });
});

lottie.loadAnimation() の詳細は、lottie-web ドキュメントをご覧ください。

airbnb.io

JSONファイル名列挙自動化

JSON ファイル名を CoreMicroAnimationData.ts に記述していますが、src/assets/lottie/core-micro-animation 配下の JSON ファイルと正確には関連していません。

ファイル構成からコードの再現性を担保しておくために、ファイル名を機械的に列挙し、CoreMicroAnimationData.ts を自動生成していきたいと思います。

CoreMicroAnimationData.ts を自動生成するコマンドを packages.json"scripts" に追加します。

"lottie": "node ./tools/generate-lottie-imports.mjs execute"

generate-lottie-imports.mjs の実装は以下の通りです。

import { $ } from "zx";
import fire from "js-fire";
import { render } from "template-file";

const root = process.cwd();

const template = `/**
 * src/assets/lottie/core-micro-animation の JSON ファイル名から自動生成されるファイルです
 */
export namespace CoreMicroAnimationData {
  export const names = [
    {{#fileList}}
    "{{this}}",
    {{/fileList}}
  ] as const;
  export type Name = typeof names[number];
}`;

const generateLottieImports = {
  __description__: "📦  lottie JSON ファイル名からアニメーション名の定義ファイルを生成",
  execute: async () => {
    const files = await $`find ${root}/src/assets/lottie/core-micro-animation -type f -name "*.json"`;
    const fileList = files.stdout.split("\n").filter(elm=> elm.length > 0).map((path) => path.split("/").pop().replace(".json", ""));
    const result = render(template, { fileList });
    await $`echo ${result} > ${root}/src/packages/Core/partial/dependency/CoreMicroAnimationData.ts`;
  },
};

fire(generateLottieImports);

拡張子を mjs にすると、import 文を記述できるようになります。

nodejs.org

ファイルの列挙と、ファイルの書き込みは、JavaScript でシェルを実行できるライブラリの zx を用いました。

github.com

find で指定したディレクトの JSON ファイルを列挙し、 echo でファイル出力します。

const files = await $`find ${root}/src/assets/lottie/core-micro-animation -type f -name "*.json"`;
const fileList = files.stdout.split("\n").filter(elm=> elm.length > 0).map((path) => path.split("/").pop().replace(".json", ""));
const result = render(template, { fileList });
await $`echo ${result} > ${root}/src/packages/Core/partial/dependency/CoreMicroAnimationData.ts`;

ファイルに書き込むテキストは、template-file の render() で変数展開した後、 echo へ渡しています。

render() で得たテキストが、 CoreMicroAnimationData.ts のコードになります。

github.com

JavaScript から CLI を作成するためには、js-fire を用いました。

github.com

以上で、 $ pnpm lottie コマンドで CoreMicroAnimationData.ts が自動生成されます。

まとめ

data-animation 属性 で指定したアニメーションが再生される Web コンポーネント を実装できました。

アニメーションデータはバンドルファイルに静的にインポートされているわけではなく、ダイナミックインポートで必要なアニメーションデータのみ読み込まれます。

読み込みファイルを抑えることによって、動きのある要素を配置してもメインコンテンツの読み込みへの影響を小さくできることが期待できます。

また、今後アニメーションの数が増えていっても、ダイナミックインポートでアニメーションデータをインポートしていくので問題なさそうです。

アニメーションファイル名は、機械的にコードへ反映されるので定義の追加漏れもなく、メンテンナンスコストが増える心配もありません。

本エントリーでは、マウント時に自動再生するだけの実装を示しました。

マウント後のユーザーインタラクションを反映するには、 lottie-web の API を適宜活用して SFC の <template> でインベントハンドラと組み合わせるていくと、より実用的なコンポーネントなると思います。

lottie.loadAnimation() で アニメーションインスタンスが生成されていますので、 アニメーションインスタンスに対して lottie-web の API からアニメーション操作可能です。

lottie-web のソースコードは JavaScript ですが、型定義ファイルが同 Repository にあります。

github.com

TypeScript でも利用可能なので、ご興味があれば、ぜひお試しください!