中つ国のフロントエンド

マルチデバイス開発のチュートリアル

Chrome 試験運用版「A Journey Through Middle-earth」の開発に関する最初の記事では、モバイル デバイス向けの WebGL 開発に焦点を当てました。この記事では、残りの HTML5 フロントエンドの作成時に直面した課題、問題、解決策について説明します。

同じサイトの 3 つのバージョン

まず、画面サイズとデバイスの機能の観点から、この試験運用版をデスクトップ パソコンとモバイル デバイスの両方で機能するように調整する方法について説明します。

このプロジェクト全体は非常に「映画的」なスタイルに基づいています。デザイン面では、映画の魔法を感じてもらうために、横向きの固定フレーム内にエクスペリエンスを収めています。このプロジェクトの大部分はインタラクティブなミニ「ゲーム」で構成されているため、フレームから溢れ出しても意味がありません。

さまざまなサイズに合わせてデザインを適応させる方法の例として、ランディング ページを挙げることができます。

イーグルがランディング ページに連れてきてくれました。
イーグルがランディング ページに連れてきてくれました。

このサイトには、パソコン、タブレット、モバイルの 3 つのモードがあります。レイアウトを処理するためだけでなく、ランタイムで読み込まれるアセットを処理し、さまざまなパフォーマンス最適化を追加する必要があるためです。デスクトップ パソコンやノートパソコンよりも解像度が高いものの、スマートフォンよりもパフォーマンスが低いデバイスについては、最終的なルールを定義するのは簡単ではありません。

Google では、ユーザー エージェント データを使用してモバイル デバイスを検出し、ビューポート サイズ テストを使用して、その中からタブレット(645 ピクセル以上)をターゲットに設定しています。レイアウトはメディアクエリまたは JavaScript による相対/パーセンテージ ポジショニングに基づいているため、各モードですべての解像度をレンダリングできます。

この場合のデザインはグリッドやルールに基づいておらず、セクションごとにかなり異なるため、使用するブレークポイントやスタイルは、具体的な要素とシナリオによって異なります。sass-mixin とメディアクエリを使って完璧なレイアウトを設定しても、マウスの位置や動的オブジェクトに基づくエフェクトを追加する必要があり、結局すべてを JavaScript で書き直す必要が生じたことが何度もありました。

また、ヘッドタグに現在のモードを含むクラスを追加して、その情報をスタイルで使用できるようにします。以下の例(SCSS)をご覧ください。

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

YouTube では、360x320 程度までのすべてのサイズをサポートしていますが、没入型のウェブ エクスペリエンスを作成する際には、かなりの課題がありました。デスクトップでは、可能な限り大きなビューポートでサイトを表示できるように、スクロールバーが表示される最小サイズが設定されています。モバイル デバイスでは、インタラクティブ エクスペリエンスまで横向きと縦向きの両方を許可し、インタラクティブ エクスペリエンスではデバイスを横向きにするよう求める予定です。横向きほど没入感がないとの反論もありましたが、サイトは非常によくスケーリングされたため、そのままにしておきました。

レイアウトは、入力タイプ、デバイスの向き、センサーなどの機能検出と混同しないことが重要です。これらの機能は、これらのすべてのモードに存在し、すべてにまたがるようにする必要があります。マウスとタッチの同時サポートはその一例です。画質は Retina に対応していますが、パフォーマンスは別問題です。品質が低くてもパフォーマンスが優れている場合もあります。たとえば、Retina ディスプレイの WebGL エクスペリエンスでは、キャンバスの解像度は半分になります。解像度が同じであれば、4 倍のピクセル数をレンダリングする必要があります。

開発中は、特に新機能や多数のプリセットが搭載されている Chrome Canary で、DevTools のエミュレータ ツールを頻繁に使用しました。これは、設計をすばやく検証するのに適した方法です。それでも、実際のデバイスで定期的にテストする必要がありました。その理由の 1 つは、サイトが全画面表示に適応しているためです。縦方向にスクロールするページでは、ほとんどの場合、スクロール時にブラウザ UI が非表示になります(iOS7 の Safari では現在この問題が発生しています)。しかし、この点に関係なく、すべてを適合させる必要がありました。また、エミュレータでプリセットを使用し、画面サイズの設定を変更して、使用可能なスペースの減少をシミュレートしました。実際のデバイスでテストすることは、メモリ使用量とパフォーマンスをモニタリングするためにも重要です。

状態の処理

ランディング ページを過ぎると、中つ国の地図が表示されます。URL が変更されたことに気づきましたか?このサイトは、History API を使用してルーティングを処理するシングルページ アプリケーションです。

サイトの各セクションは、DOM 要素、遷移、アセットの読み込み、破棄などの機能のボイラープレートを継承する独自のオブジェクトです。サイトのさまざまな部分を操作すると、セクションが開始され、要素が DOM に追加または削除され、現在のセクションのアセットが読み込まれます。

ユーザーはいつでもブラウザの戻るボタンを押したり、メニューから移動したりできるため、作成されたものはすべてある時点で破棄する必要があります。タイムアウトとアニメーションは停止して破棄する必要があります。停止しないと、望ましくない動作、エラー、メモリリークが発生します。これは必ずしも簡単な作業ではありません。特に、締め切りが迫っていて、できるだけ早くすべてを入力する必要がある場合はなおさらです。

場所をアピールする

中つ国の美しい風景とキャラクターをアピールするため、横方向にドラッグまたはスワイプできる画像とテキストのモジュラー システムを構築しました。クリップの再生が終わるまで横方向の動きが停止する画像シーケンスなど、さまざまな範囲で異なる速度を設定できるようにするため、ここではスクロールバーを有効にしていません。

スランドゥイルのホール
Thranduil's Hall のタイムライン

タイムライン

開発を開始した時点では、各ロケーションのモジュールの内容は不明でした。さまざまな種類のメディアや情報を横長のタイムラインに表示するテンプレートを作成し、すべてを 6 回も再ビルドしなくても 6 つの異なる場所を自由に表示できるようにしたいと考えました。これを管理するため、設定とモジュールの動作に基づいてモジュールのパンを処理するタイムライン コントローラを作成しました。

モジュールと動作コンポーネント

サポートが追加されたモジュールは、画像シーケンス、静止画像、パララックス シーン、フォーカス シフト シーン、テキストです。

パララックス シーン モジュールには、ビューポートの進行状況をリッスンして正確な位置を特定する、カスタムのレイヤ数を持つ不透明な背景があります。

フォーカス シフト シーンは、パララックス バケットのバリエーションです。ただし、各レイヤに 2 つの画像を使用し、フェードインとフェードアウトを繰り返してフォーカスの変化をシミュレートします。ぼかしフィルタの使用を試みましたが、まだコストが高すぎるため、CSS シェーダーの登場を待つことにします。

テキスト モジュールのコンテンツは、TweenMax プラグイン Draggable でドラッグ可能になっています。スクロールホイールや 2 本の指でのスワイプを使って縦方向にスクロールすることもできます。スワイプして離すとフリング スタイルの物理演算を追加する throw-props-plugin に注目してください。

モジュールには、コンポーネントのセットとして追加される異なる動作も設定できます。それぞれに独自のターゲット セレクタと設定があります。要素の移動のための移動、ズームのためのスケーリング、情報オーバーレイ用のホットスポット、視覚的なテストのためのデバッグ指標、開始タイトル オーバーレイ、フレアレイヤなど。これらは DOM に追加されるか、モジュール内のターゲット要素を制御します。

これを設定すると、読み込むアセットを定義し、さまざまな種類のモジュールとコンポーネントを設定する構成ファイルだけで、さまざまなロケーションを作成できます。

画像シーケンス

パフォーマンスとダウンロード サイズの観点から最も難しいモジュールは、画像シーケンスです。このトピックについて、さまざまな記事があります。モバイルとタブレットでは、この画像は静止画像に置き換えられます。モバイルで適切な品質を実現するには、デコードしてメモリに保存するにはデータ量が多すぎます。最初は、背景画像とスプライトシートを使用するなど、複数の代替ソリューションを試しましたが、GPU がスプライトシートを切り替える必要があるときにメモリの問題と遅延が発生しました。次に、img 要素を入れ替えようとしましたが、これも遅すぎました。スプライトシートからキャンバスにフレームを描画するのが最もパフォーマンスが高かったため、その最適化を開始しました。各フレームの計算時間を節約するため、キャンバスに書き込む画像データは、一時的なキャンバスで前処理され、putImageData() で配列に保存され、デコードされて使用できる状態になります。元のスプライトシートはガベージ コレクションされ、必要な最小限のデータのみがメモリに保存されます。デコードされていない画像を保存する方が実際には少ないかもしれませんが、この方法ではシーケンスをスクラブする際のパフォーマンスが向上します。フレームは 640 x 400 とかなり小さいですが、スクラブ中にのみ表示されます。停止すると、高解像度の画像が読み込まれ、すぐにフェードインします。

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

スプライトシートは Imagemagick で生成されます。フォルダ内のすべての画像のスプライトシートを作成する方法を示す簡単な GitHub の例を次に示します。

モジュールのアニメーション化

モジュールをタイムラインに配置するには、画面外に表示されるタイムラインの非表示表現が「再生ヘッド」とタイムラインの幅を追跡します。これはコードだけでも可能ですが、開発とデバッグ時に視覚的に表現できると便利です。実際に実行すると、サイズ変更時に更新されてサイズが設定されます。一部のモジュールはビューポートを埋め尽くし、一部のモジュールには独自のアスペクト比があるため、すべての解像度ですべてが見えるように、かつ切り抜きすぎないようにスケーリングして配置するのは少し難しい作業でした。各モジュールには、画面上の表示位置を示す進行状況インジケーターと、モジュール自体の所要時間を示す進行状況インジケーターが 2 つあります。パララックス モーションを作成する際、オブジェクトの開始位置と終了位置を計算して、ビュー内の予想位置と同期させることは難しい場合があります。モジュールがビューに表示され、内部タイムラインを再生し、再びビューからアニメーションで消えるタイミングを正確に把握しておくとよいでしょう。

各モジュールの上部には、不透明度を調整する黒いレイヤがあり、中央の位置にあるときは完全に透明になります。これにより、一度に 1 つのモジュールに集中できるため、学習効果が向上します。

ページのパフォーマンス

動作するプロトタイプからジャンクのないリリース バージョンに移行すると、ブラウザで何が起こるかを推測から把握できるようになります。ここで Chrome DevTools が役に立ちます。

サイトの最適化にかなりの時間を費やしました。スムーズなアニメーションを実現するために、ハードウェア アクセラレーションを強制することは最も重要なツールの 1 つです。また、Chrome DevTools でカラフルな列や赤い長方形を探します。このトピックに関する優れた記事はたくさんあり、すべて読む必要があります。フレームのスキップを削除するとすぐに報酬が得られますが、再びスキップが発生すると、すぐに不満が募ります。はい、これは反復を必要とする継続的なプロセスです。

プロパティ、変換、CSS のツイニングには、Greensock の TweenMax を使用しています。コンテナで考え、新しいレイヤを追加しながら構造を可視化します。既存の変換は、新しい変換によって上書きされる可能性があることにご注意ください。2D 値のみをトゥイーンする場合、CSS クラスでハードウェア アクセラレーションを強制する translateZ(0) は 2D 行列に置き換えられます。このような場合にレイヤを加速モードに保つには、Tween でプロパティ「force3D:true」を使用して、2D マトリックスではなく 3D マトリックスを作成します。CSS と JavaScript の tween を組み合わせてスタイルを設定する際に、忘れがちです。

必要のない場所でハードウェア アクセラレーションを強制しないでください。多くのコンテナをハードウェアで高速化すると、GPU メモリがすぐに一杯になり、望ましくない結果になる可能性があります。特に、メモリに制約がある iOS ではその傾向が強くなります。小さいアセットを読み込んで CSS で拡大し、モバイル モードで一部のエフェクトを無効にすることで、大幅な改善ができました。

メモリリークも、スキルを磨く必要があった分野の一つです。さまざまな WebGL エクスペリエンス間を移動すると、多くのオブジェクト、マテリアル、テクスチャ、ジオメトリが作成されます。セクションを移動して削除するときにガベージ コレクションの準備ができていないと、しばらくするとメモリ不足でデバイスがクラッシュする可能性があります。

失敗した破棄関数でセクションを終了する。
失敗した破棄関数でセクションを終了する。
よくなりました。
よくなりました。

リークを見つけるために、DevTools でタイムラインを記録し、ヒープ スナップショットをキャプチャするという、非常に簡単なワークフローを実施しました。3D ジオメトリや特定のライブラリなど、除外できる特定のオブジェクトがある場合は、より簡単です。上記の例では、3D シーンがまだ残っており、ジオメトリを格納する配列もクリアされていませんでした。オブジェクトの場所を特定するのが難しい場合は、保持パスという便利な機能を使用して確認できます。ヒープ スナップショットで検査するオブジェクトをクリックすると、下のペインに情報が表示されます。小さなオブジェクトで適切な構造を使用すると、参照を探しやすくなります。

シーンが EffectComposer で参照されました。
シーンが EffectComposer で参照されました。

一般に、DOM を操作する前によく考えることをおすすめします。効率性についても考えてみましょう。可能であれば、ゲームループ内で DOM を操作しないでください。再利用できるように参照を変数に格納します。要素を検索する必要がある場合は、戦略的なコンテナへの参照を保存し、最も近い祖先要素内で検索することで、最短ルートを使用します。

レイアウト バグが発生した場合に、新しく追加された要素のディメンションの読み取りを遅らせます。また、クラスの削除/追加時にも遅らせます。または、レイアウトがトリガーされていることを確認します。ブラウザのバッチがスタイルに変更され、次回のレイアウト トリガー後に更新されないことがあります。これは大きな問題になることもありますが、理由があって存在するものなので、裏側の仕組みを学ぶと多くのことを学ぶことができます。

全画面表示

利用可能な場合は、Fullscreen API を使用して、メニューでサイトを全画面モードにすることができます。ただし、デバイスでは、ブラウザが全画面表示にすることを決定することもあります。iOS 版 Safari には以前、これを制御できるハックがありましたが、現在は使用できなくなりました。そのため、スクロールしないページを作成する場合は、このハックなしで動作するようにデザインを準備する必要があります。多くのウェブアプリが動作しなくなったため、今後のアップデートで修正される可能性があります。

アセット

テストのアニメーション化された手順。
テストのアニメーションによる手順

サイト全体で、さまざまな種類のアセットを使用しています。画像(PNG と JPEG)、SVG(インラインと背景)、スプライトシート(PNG)、カスタム アイコンフォント、Adobe Edge アニメーションを使用しています。要素をベクターベースにできないアセットやアニメーション(スプライトシート)には PNG を使用しますが、それ以外の場合は可能な限り SVG を使用します。

ベクター形式では、拡大しても画質が低下しません。すべてのデバイスで 1 つのファイル。

  • ファイルサイズが小さい。
  • 各部分を個別にアニメーション化できます(高度なアニメーションに最適)。たとえば、ホビット ロゴの「サブタイトル」(竜に奪われた王国)は、サイズが縮小されたときに非表示になります。
  • SVG HTML タグとして埋め込むことも、追加の読み込みなしで背景画像として使用することもできます(HTML ページと同じタイミングで読み込まれます)。

アイコン フォントは、スケーラビリティに関して SVG と同じ利点があり、色(ホバー、アクティブなど)のみを変更する必要があるアイコンなどの小さな要素には、SVG の代わりに使用されます。アイコンは非常に簡単に再利用できます。要素の CSS の「content」プロパティを設定するだけです。

アニメーション

コードで SVG 要素をアニメーション化すると、時間がかかる場合もあります。特に、デザイン プロセス中にアニメーションを大幅に変更する必要がある場合は、その傾向が顕著です。デザイナーとデベロッパー間のワークフローを改善するため、一部のアニメーション(ゲームの前の説明)には Adobe Edge を使用しています。アニメーションのワークフローは Flash に非常に近いため、チームにとって有益でしたが、いくつかの欠点もあります。特に、Edge アニメーションをアセット読み込みプロセスに統合する場合は、独自のローダと実装ロジックが付属しているためです。

ウェブ上でアセットや手作りのアニメーションを扱うための完璧なワークフローを実現するには、まだ長い道のりがあると感じています。Edge などのツールがどのように進化していくのか、楽しみにしております。他のアニメーション ツールやワークフローの提案があれば、コメント欄にぜひお寄せください。

まとめ

プロジェクトのすべての部分がリリースされ、最終的な結果を見てみると、最新のモバイル ブラウザの状態に非常に感銘を受けました。このプロジェクトを開始した当初は、シームレスな統合とパフォーマンスの向上を期待していた程度は、今よりもずっと低いものでした。私たちにとって、これは大きな学習の機会となりました。また、反復処理とテストに費やした時間(非常に長い時間です)は、最新のブラウザの仕組みを理解するうえで役に立ちました。このようなプロジェクトの制作時間を短縮し、推測から確信に変えていくには、この作業が不可欠です。