中つ国のフロントエンド

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

Chrome 試験運用版「中つ国の旅」の開発に関する最初の記事では、モバイル デバイス向けの WebGL 開発に焦点を当てました。この記事では、他の HTML5 フロントエンドを開発する際に直面した課題、問題、解決策について説明します。

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

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

プロジェクト全体は、非常に「シネマティック」なスタイルに基づいており、デザイン面では、エクスペリエンスを横向き指向の固定フレーム内に収め、映画から魔法を引き出そうとしました。プロジェクトの大部分はインタラクティブなミニ「ゲーム」で構成されているため、フレームからはみ出すようにするのは理にかないでしょう。

ランディング ページは、さまざまなサイズに合わせてどのようにデザインを調整するかを示す一例です。

イーグルスがたった今、ランディング ページにアクセスしたのです。
たった今、ランディング ページが表示されました。

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

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

この場合のデザインはグリッドやルールに基づいているわけではなく、セクション間でかなり独自性があるため、どのブレークポイントやスタイルを使用するかは、特定の要素やシナリオによって異なります。適切な sass-mixins とメディアクエリを使用して完璧なレイアウトをセットアップした後、マウスの位置や動的オブジェクトに基づいて効果を追加する必要があったため、すべてを JavaScript で書き換える必要がありました。

また、現在のモードを head タグに指定してクラスを追加し、その情報をスタイル内で使用できるようにしています。次に例を示します。(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.
   }
 }
}

約 360×320 まであらゆるサイズに対応していますが、ウェブで臨場感あふれるエクスペリエンスを実現するのは、かなり困難でした。パソコンの場合、可能な限り大きなビューポートでサイトを体験していただくため、スクロールバーを表示する前に最小サイズが設定されています。モバイル デバイスでは、インタラクティブなエクスペリエンスまで、横向きモードと縦向きモードの両方を許可することにしました。インタラクティブ エクスペリエンスでは、デバイスを横向きにする必要があります。反対に、縦向きでは横向きほど臨場感は得られませんが、サイトの規模はかなり拡大したため、そのまま掲載しました。

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

開発中は DevTools のエミュレータ ツールを頻繁に使用していましたが、特に Chrome Canary では新機能が改善され、多数のプリセットが用意されました。設計を迅速に検証できる優れた方法です。それでも、実際のデバイスで定期的にテストを行う必要がありました。その理由の 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() で配列に保存され、デコードされてすぐに使用できます。元のスプライトシートはガベージ コレクションの対象となり、必要な最小限のデータのみがメモリに保存されます。デコードされていない画像を保存する方が実際には少ないかもしれませんが、この方法でシーケンスをスクラブする間のパフォーマンスが向上します。フレームは 640x400 と非常に小さく、スクラブ中にのみ表示されます。停止すると、高解像度の画像が読み込まれてすぐにフェードインします。

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 つは画面上に表示される位置、もう 1 つはモジュール自体の再生時間です。視差移動を行う場合、多くの場合、対象物の開始位置と終了位置を計算して、視野内にある想定位置と同期させるのが困難です。モジュールがビューに入り、内部タイムラインが再生され、再びアニメーションでビューから消えたタイミングを正確に把握しておくと便利です。

各モジュールには、上部に微妙な黒のレイヤがあり、中央に配置したときに完全に透明になるように不透明度が調整されます。これにより、一度に 1 つのモジュールに集中できるため、エクスペリエンスが向上します。

ページのパフォーマンス

正常に機能するプロトタイプからジャンクのないリリース バージョンに移行するには、推測ではなくブラウザで何が起きているかを把握する必要があります。ここで、Chrome DevTools がおすすめです。

私たちはサイトの最適化に多くの時間を費やしてきました。もちろん、スムーズなアニメーションを実現するうえで、ハードウェア アクセラレーションの強制適用は最も重要なツールの一つです。Chrome DevTools でカラフルな列と赤い四角形を探します。このトピックに関する有益な記事は数多くあるため、すべてに目を通す必要があります。スキップしたフレームを削除するとすぐに報酬が得られますが、再びフレームに戻ったときのフラストレーションもすぐさまです。彼らは、これからも登場するでしょう。これは継続的なプロセスであり、イテレーションが必要です。

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

不要なハードウェア アクセラレーションを強制的に適用しないでください。特にメモリの制約が多い iOS では、多くのコンテナをハードウェア アクセラレーションする必要がある場合、GPU メモリがあっという間に一杯になり、望ましくない結果が生じる可能性があります。小さなアセットを読み込んで 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 のようなツールが今後どのように進化していくのか楽しみです。その他のアニメーション ツールやワークフローに関するご提案は、コメント欄に自由にご記入ください。

おわりに

プロジェクトのすべての部分がリリースされて最終結果を見ると、最新のモバイル ブラウザの状態にかなり感銘を受けたことでしょう。このプロジェクトを始動したとき、どれほどシームレスで、統合され、パフォーマンスが高いかについては、はるかに期待を抱いていました。私たちにとって素晴らしい学習体験であり、反復とテストに(多くの)時間を費やしたことで、最新のブラウザの仕組みについての理解が深まりました。推測から判断に至る、この種のプロジェクトの制作時間を短縮するには、それが必要です。