レスポンシブで滑らかなフレームアニメーションをCSSとJSで実装する方法

2017/09/21

Michael Romanov

146

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

Webサイトを楽しく見せることができるフレームアニメーション。SVG画像とCSS、JavaScriptで滑らかな動きを実現する方法、注意点を解説します。

デザイナーから、「今回のプロジェクトではフレームアニメーションを使いたいんだ。きれいに動くように実装してほしい」と言われたらどうしますか? フロントエンド開発者には、デスクトップとモバイルを問わず、すべてのブラウザーで滑らかに動き、ハイパフォーマンスでメンテナンスしやすいフレームアニメーションの実装が求められます。

このチュートリアルでは、HTML、CSS、JavaScriptを使ってアニメーションを作成する方法を紹介します。改善を繰り返しながら、プロジェクトにとって最善の結果を達成しましょう。

フレームアニメーションとは?

フレームアニメーションについてのAdobeによる定義は以下のとおりです。

すべてのフレームでステージのコンテンツが変化するので、単にステージ上を移動するようなアニメーションではなく、画像が各フレームで変化するような複雑なアニメーションに適しています。

言い換えると、アニメーションの対象は一連の画像によって表現されます。各画像がアニメーションの1フレームを形成し、フレームが次のフレームに置き変わる変化のレートによって、画像が動いているような錯覚を生み出します。

ZeissのWebサイトのアニメーション「まばたきする目」を作成しながら、ワークフロー全体を説明します。

アニメーションフレームの作成に使う一連の画像と、完成イメージです。

Blinking eye sprite for frame by frame animation

Blinking eye frame by frame animation

このチュートリアルでは、レスポンシブ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ならパフォーマンスの観点から以下のメリットが得られます。

  • 画面上のほかのアニメーションの品質に影響しない
  • ユーザーが別のタブに移動するとブラウザーがアニメーションを停止する
jQueryコード
const $element = $('.eye-animation');
const imagePath = '/images';
const totalFrames = 18;
const animationDuration = 1300;
const timePerFrame = animationDuration / totalFrames;
let timeWhenLastUpdate;
let timeFromLastUpdate;
let frameNumber = 1;

function step(startTime) {
  if (!timeWhenLastUpdate) timeWhenLastUpdate = startTime;

  timeFromLastUpdate = startTime - timeWhenLastUpdate;

  if (timeFromLastUpdate > timePerFrame) {
    $element.attr('src', imagePath + `/Eye-${frameNumber}.svg`);
    timeWhenLastUpdate = startTime;

    if (frameNumber >= totalFrames) {
      frameNumber = 1;
    } else {
       frameNumber = frameNumber + 1;
    }        
  }

  requestAnimationFrame(step);
}

ページロード時に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プロパティの設定で目的の画像を示す方法です。

jQueryコード
$(document).ready(() => {
  for (var i = 1; i < totalFrames + 1; i++) {
    $('body').append(`<div id="preload-image-${i}" style="background-image: url('${imagePath}/Eye-${i}.svg');"></div>`);
  }
});

完成した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アニメーションでフレームごとにスプライトを左から右に動かすわけです。

HTML
<div class="eye-animation"></div>
CSS
.eye-animation {  
  width: 300px;
  height: 300px;

  background-image: url('/images/blinking-eye-sprite.svg');
  background-size: 1800%, 100%;
  background-position: left;
  background-repeat:no-repeat;

  animation-name: eye-fill;
  animation-duration: 1.3s;
  animation-timing-function: steps(17);
  animation-iteration-count: infinite;
}

@keyframes eye-fill {
  from { 
    background-position: left; 
  }
  to { 
    background-position: right; 
  }
}

上のコードではフレーム数に合わせて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桁に丸めればこの問題を解決できる)

Wobbling frame by frame animation of blinking eye

アニメーションがブレている例

メリット:

  • JavaScriptが不要
  • 画像を1つだけロードするので、初回ページロード時のパフォーマンスが良好

4.Transformでスプライトを動かすことによるフレームアニメーション

前述の方法と同様、ブラウザーの再描画を避けることで、これまでに実装したソリューションをアップグレードします。background-positionプロパティではなくtransformプロパティを変更することで実現します。

ラッパー内にアニメーションさせるdivを置きます。背景ではなくHTML要素全体の位置を置き換えます。

次に、divをラッパー内でposition:absoluteに設定し、translateXプロパティを使ってアニメーションを開始します。

-94.44444444%という奇妙な値は、スライドが18枚あり、最初の画像から17スライド分移動するためです(「17 / 18 * 100%」の計算結果です)。

HTML

<div class="eye-animation__wrapper">
  <div class="eye-animation"></div>
</div>
Sassコード(CSSでも可)
.eye-animation {
  width: 1800%;
  height: 100%;
  background-image: url('/images/blinking-eye-sprite.svg');
  background-size: 100%, 100%;
  background-repeat:no-repeat;

  animation-name: eye-fill;
  animation-duration: 1.3s;
  animation-timing-function: steps(17);
  animation-iteration-count: infinite;

  position: absolute;
  left: 0;
  top: 0;

  &__wrapper {
    overflow: hidden;
    position: relative;
    width: 300px;
    height: 300px;
  }
}

@keyframes eye-fill {
  from { 
    transform: translateX(0); 
  }
  to { 
    transform: translateX(-94.44444444%); 
  }
}

明らかに良くなりました。

しかしIEは、translateプロパティで値の%指定が使えないバグがあります。IEではtranslateプロパティで値を%指定できません。caniuse.comのknown issues(既知の問題)タブに例と説明が掲載されています。

Windows 7でIE10とIE11を使った場合、「translate transform」の値をアニメーションに使用するとピクセル値として解釈するバグがあります。

フォールバックとして、JavaScriptを使ってブラウザーを検出し、特定のケース用に別のアニメーションを作成する必要があります。

下のコードのtransform: translate3d(0, 0, 0)で、要素を別の構成レイヤーに移すようブラウザーに指示することで、レンダリングパフォーマンスが改善されます

JavaScript
var isIE = /Edge\/\d./i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent);

if (_this.isIE) {
   $('html').addClass('ie');
}
Sassコード
// fallback for IE
.ie {
  .eye-animation {
    transform: translate3d(0, 0, 0);
    animation-name: eye-fill-ie;
  }
}

@keyframes eye-fill-ie {
  from {
    left: 0;
  }
  to {
    left: -1700%;
  }
}

下のライブデモでコードを試してください。

デメリット:

  • 画像の精度を上げすぎるとアニメーションがブレる
  • IEでは動かないが、フォールバックが有効

メリット:

  • ロードする画像が1つなので、初回のページロード時のパフォーマンスが良好
  • 再描画が関係しないので、モバイルでアニメーションの品質が低下しない

フレームアニメーションにインラインSVGを使用

可能な改善策に、外部リソースで画像を指定する代わりにSVG画像をインライン化する(つまりHTMLページに直接SVGコードを投入する)方法があります。

一般的に、外部リソースはブラウザーがキャッシュします。再アクセス時に、ブラウザーはサーバーにリクエストを送らずにローカルにキャッシュしたファイルを使います。

ランディングページなどの再アクセスの可能性が低いページには、インラインSVGが有意義です。サーバーへのリクエスト数を減らし、初回ページロード時間を短縮できます。

レンダリングパフォーマンスにはスプライトのTransform(Sprite-Transform)

パフォーマンスを正確に紹介するため、4つの手法すべてのパフォーマンスをテストしました。Chromeを使ったテスト結果です。

Performance table of frame by frame web animation in 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

Copyright © 2017, Michael Romanov All Rights Reserved.

Michael Romanov

Michael Romanov

フロントエンドに熱心で、すべてのデザイナーの友です。詳しくはBuzzwooで。

Loading...