こんにちは! @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 としてエクスポートされたファイルです。
開発環境
Vue CLI プロジェクト の GDS (GameWithDesignSystem) でコンポーネントを追加実装しました。
GDS は、こちらの記事で紹介しております。
本エントリーの公開時点での各種依存バージョンは、次の通りです。
- @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アニメーションをマウントされます。
props
の dataElement
では、親要素を span
または div
に指定できるオプションを設けました。
<component>
の :is
へ代入すると、指定した HTML タグがレンダリングされます。
<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"
からのインポートは、省略できる仕組みを導入しています。
アニメーションを動作させる実装は、以下のとおりです。
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"
に設定すると、バンドルファイルが思惑通りにコード分割されるようになります。
アニメーション開始
アニメーションの動作を開始するときに必要な container
にあたる要素得るには、VueUse の templateRef
を用いると便利です。
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
ドキュメントをご覧ください。
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
文を記述できるようになります。
ファイルの列挙と、ファイルの書き込みは、JavaScript でシェルを実行できるライブラリの zx を用いました。
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
のコードになります。
JavaScript から CLI を作成するためには、js-fire を用いました。
以上で、 $ pnpm lottie
コマンドで CoreMicroAnimationData.ts
が自動生成されます。
まとめ
data-animation
属性 で指定したアニメーションが再生される Web コンポーネント を実装できました。
アニメーションデータはバンドルファイルに静的にインポートされているわけではなく、ダイナミックインポートで必要なアニメーションデータのみ読み込まれます。
読み込みファイルを抑えることによって、動きのある要素を配置してもメインコンテンツの読み込みへの影響を小さくできることが期待できます。
また、今後アニメーションの数が増えていっても、ダイナミックインポートでアニメーションデータをインポートしていくので問題なさそうです。
アニメーションファイル名は、機械的にコードへ反映されるので定義の追加漏れもなく、メンテンナンスコストが増える心配もありません。
本エントリーでは、マウント時に自動再生するだけの実装を示しました。
マウント後のユーザーインタラクションを反映するには、 lottie-web
の API を適宜活用して SFC の <template>
でインベントハンドラと組み合わせるていくと、より実用的なコンポーネントなると思います。
lottie.loadAnimation()
で アニメーションインスタンスが生成されていますので、 アニメーションインスタンスに対して lottie-web
の API からアニメーション操作可能です。
lottie-web
のソースコードは JavaScript ですが、型定義ファイルが同 Repository にあります。
TypeScript でも利用可能なので、ご興味があれば、ぜひお試しください!