デザイナーから、「今回のプロジェクトではフレームアニメーションを使いたいんだ。きれいに動くように実装してほしい」と言われたらどうしますか? フロントエンド開発者には、デスクトップとモバイルを問わず、すべてのブラウザーで滑らかに動き、ハイパフォーマンスでメンテナンスしやすいフレームアニメーションの実装が求められます。
このチュートリアルでは、HTML、CSS、JavaScriptを使ってアニメーションを作成する方法を紹介します。改善を繰り返しながら、プロジェクトにとって最善の結果を達成しましょう。
フレームアニメーションとは?
フレームアニメーションについてのAdobeによる定義は以下のとおりです。
すべてのフレームでステージのコンテンツが変化するので、単にステージ上を移動するようなアニメーションではなく、画像が各フレームで変化するような複雑なアニメーションに適しています。
言い換えると、アニメーションの対象は一連の画像によって表現されます。各画像がアニメーションの1フレームを形成し、フレームが次のフレームに置き変わる変化のレートによって、画像が動いているような錯覚を生み出します。
ZeissのWebサイトのアニメーション「まばたきする目」を作成しながら、ワークフロー全体を説明します。
アニメーションフレームの作成に使う一連の画像と、完成イメージです。
このチュートリアルでは、レスポンシブWebデザインに対応し、いろいろなサイズの画面に合わせてスケーリングするメリットからSVG画像を使います。SVG画像を使いたくないなら、PNG、JPEG、GIF画像フォーマットやHTML5のCanvasを使ってWebアニメーションを制作できます。代替手段は記事の最後で紹介します。
分かりやすくするため、jQueryライブラリーとAutoprefixerを使います。コードにブラウザー固有のCSSプレフィックスは付けません。
ではコーディングを始めます。
1.ソース画像の切替によるフレームアニメーション
最初の方法はとても簡単です。
HTMLドキュメントにimg要素を作成します。この要素は複数の画像のコンテナとして機能し、一度に1つずつ、アニメーションフレームを次のフレームに置きかえます。
<img class="eye-animation"
src="/images/Eye-1.svg"
alt="blinking eye animation"/>
CSSは以下のとおりです。
.eye-animation {
width: 300px;
}
次のステップは、一定の間隔で現在の画像を次の画像に動的に置きかえます。これで動いているような錯覚を生み出します。
setTimeoutでも実装できますが、requestAnimationFrameならパフォーマンスの観点から以下のメリットが得られます。
- 画面上のほかのアニメーションの品質に影響しない
- ユーザーが別のタブに移動するとブラウザーがアニメーションを停止する
ページロード時にrequestAnimationFrameを呼び出し、この関数に組み込まれたアニメーションのstartTimeパラメーターと、step関数を渡します。
コードの各ステップで、ソース画像が更新されてからの経過時間をチェックします。経過時間がフレームの所要時間を超過すると画像を更新します。
ここでは無限に繰り返すアニメーションを作成します。上のコードでは最後のフレームかチェックし、最後ならframeNumberを1にリセットします。それ以外はframeNumberを「1」インクリメントします。
コードで簡単に画像をループできるように、画像の名前は「images/Eye-1.svg」「images/Eye-2.svg」「images/Eye-3.svg」のように増加する連番を持つ同一構造にし、同じ場所に置きます。
最後に再びrequestAnimationFrameを呼び出し、プロセス全体を続行します。
このままでは、アニメーションの開始時、最初の画像しかロードされないため動きません。アニメーションのループ中にimg要素のsrc属性をコードが更新するまで、表示するほかの画像がブラウザーに伝わらないため起こります。アニメーションをスムーズに動かすには、ループ開始前に画像をプリロードします。
実現する方法はいろいろあります。気に入っているのは、以下に示すように複数のdivを非表示(hidden)にして追加し、background-imageプロパティの設定で目的の画像を示す方法です。
完成したCodePenのデモです。
この手法のメリットとデメリットを挙げます。
デメリット:
- HTTP v1.1では、複数の画像のロードが必要な場合、初回アクセス時にページのロード時間が長くなることがある
- モバイル機器でアニメーションの品質が低下する場合がある。理由はimg要素のsrc属性が更新されるたびにブラウザーが再描画を実行しなければならないため(詳しくはPaul Lewisのブログ投稿を参照)
メリット:
- 宣言的:コードは一連の画像をループするだけでよい
- 画像が1カ所に固定される:画像の表示がブレない(メリットになる理由は後述)
2.画像の透明度変更によるフレームアニメーション
ブラウザーによる再描画を避けるため、ソース画像を置きかえる代わりに画像の透明度(opacity)を変更します。
ページのロード時にすべての画像をopacity: 0でレンダリングし、表示するタイミングで特定のフレームをopacity: 1に設定します。
これでレンダリングパフォーマンスは改善されますが、画像をすべてプリロードすることは変わりません(ページにほかの画像が複数あり、すべてロードされるまで待つのが望ましくない場合、面倒なことになりかねません)。さらに画像が複数あるため、初回のページロード時間が長くなります。
コード全体を以下に示します。
Pug、Twig、React、Angularといったテンプレートエンジン機能や、例のようにJavaScriptを使って複数のdivを追加すればHTMLコードの重複を避けられます。
3.スプライトの位置変更によるフレームアニメーション
複数の画像のダウンロードを回避するには、1枚のスプライト画像を使います。ここまでバラバラに作っていたフレーム画像を順番に1列に並べ、1枚にまとめたスプライト画像を作ります。最初の画像を左、最後の画像を右にして、CSSアニメーションでフレームごとにスプライトを左から右に動かすわけです。
上のコードではフレーム数に合わせてbackground-sizeプロパティを設定しています。フレーム数が18なので、設定は1800%です。
開始時のbackground-position値をleftに設定し、アニメーション開始時に最初の画像を表示します。
コードではキーフレームアニメーションを使ってanimation-durationプロパティに設定した時間(上の例では1.3s)をかけて背景の位置を徐々にright(右)に変化させています。
animation-timing-functionプロパティでstepごとのアニメーションを作成すれば、隣り合うフレームを同時に半分ずつ表示するのを確実に避けられます。この機能はChris MabryのIntroduction to CSS sprite sheet animation(CSSスプライトシートアニメーションの紹介)に解説があります。
JavaScriptは不要です。
デメリット:
- スプライトの位置が変わるたびにブラウザーの再描画が必要なので、モバイルではアニメーションの品質が低下する可能性がある
- 小数点以下の桁数が多いと、アニメーションが左右にブレる場合がある(画像サイズを小数点以下2桁に丸めればこの問題を解決できる)
メリット:
- JavaScriptが不要
- 画像を1つだけロードするので、初回ページロード時のパフォーマンスが良好
4.Transformでスプライトを動かすことによるフレームアニメーション
前述の方法と同様、ブラウザーの再描画を避けることで、これまでに実装したソリューションをアップグレードします。background-positionプロパティではなくtransformプロパティを変更することで実現します。
ラッパー内にアニメーションさせるdivを置きます。背景ではなくHTML要素全体の位置を置き換えます。
次に、divをラッパー内でposition:absoluteに設定し、translateXプロパティを使ってアニメーションを開始します。
-94.44444444%という奇妙な値は、スライドが18枚あり、最初の画像から17スライド分移動するためです(「17 / 18 * 100%」の計算結果です)。
明らかに良くなりました。
しかしIEは、translateプロパティで値の%指定が使えないバグがあります。IEではtranslateプロパティで値を%指定できません。caniuse.comのknown issues(既知の問題)タブに例と説明が掲載されています。
Windows 7でIE10とIE11を使った場合、「translate transform」の値をアニメーションに使用するとピクセル値として解釈するバグがあります。
フォールバックとして、JavaScriptを使ってブラウザーを検出し、特定のケース用に別のアニメーションを作成する必要があります。
下のコードのtransform: translate3d(0, 0, 0)で、要素を別の構成レイヤーに移すようブラウザーに指示することで、レンダリングパフォーマンスが改善されます。
下のライブデモでコードを試してください。
デメリット:
- 画像の精度を上げすぎるとアニメーションがブレる
- IEでは動かないが、フォールバックが有効
メリット:
- ロードする画像が1つなので、初回のページロード時のパフォーマンスが良好
- 再描画が関係しないので、モバイルでアニメーションの品質が低下しない
フレームアニメーションにインラインSVGを使用
可能な改善策に、外部リソースで画像を指定する代わりにSVG画像をインライン化する(つまりHTMLページに直接SVGコードを投入する)方法があります。
一般的に、外部リソースはブラウザーがキャッシュします。再アクセス時に、ブラウザーはサーバーにリクエストを送らずにローカルにキャッシュしたファイルを使います。
ランディングページなどの再アクセスの可能性が低いページには、インラインSVGが有意義です。サーバーへのリクエスト数を減らし、初回ページロード時間を短縮できます。
レンダリングパフォーマンスにはスプライトのTransform(Sprite-Transform)
パフォーマンスを正確に紹介するため、4つの手法すべてのパフォーマンスをテストしました。Chromeを使ったテスト結果です。
試してみたい人は、任意のブラウザーを選んでjsPerf.comでテストを再現できます。
5.WebのフレームアニメーションでGIFを使わない理由
サイズの異なる画面に合わせたスケーリング機能が不要なら、GIFファイルも選択肢にできますが、スケーラビリティだけでなく、停止や逆再生、別のアニメーションとの結合といった制御機能は期待できません。ファイルサイズも大きくなり、パフォーマンスに影響します。
GIFよりもSVGのほうが望ましい理由は、Sara Soueidanの記事を参考にしてください。
6.Canvasがフレームアニメーションに不向きな理由
小さい画面で一度に複数の要素を使ったアニメーションを作るなら、パフォーマンスが抜群なCanvasがおすすめですが、デメリットもあります。
- Canvasではインラインアセットが使えない。アクセスが一度だけのページでは最良の選択肢ではない
- ソリューションの作成にCanvas APIの知識が必要で、アプリケーションのメンテナンスコストが増加する
- DOMのイベントに対応していない。たとえばアニメーションが終わると、canvas要素外のDOMでは使えない
CanvasとSVGを比較した賛否をめぐる議論はこちらです。Canvasを使うならWilliam Maloneのチュートリアルを参考にしてください。最善の結果を実現できる方法について説明があります。
最後に
Webのフレームアニメーション実装用に使う選択肢はたくさんあります。その中から最善のものを選ぶとなると迷います。選択時に役立つポイントを示します。
- スケーラビリティとレスポンシブ対応を重視するなら、GIF/PNG/JPEGよりもSVGがおすすめ
- レンダリングパフォーマンスを重視するなら、opacityプロパティとtransformプロパティを使ったアニメーション実装がおすすめ
- 初回ページロード時のパフォーマンスを重視するなら、複数の外部アセットよりもスプライトやインラインSVGがおすすめ
- 再アクセス時のページロードパフォーマンスを重視するなら、インラインSVGよりもスプライトがおすすめ
- コードが読みやすくなり、チーム内のメンテナンスコストを削減できる限り、開発者と開発チームが使いやすいと思えるソリューションを選ぶと良い
本記事はGiulio Mainardiが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Frame by Frame Animation Tutorial with CSS and JavaScript)
[翻訳:新岡祐佳子/編集:Livit]