Chrome でビルドする

マルチデバイス ウェブに LEGO® ブロックを導入

Build with Chrome は、オーストラリアで最初にリリースされた、パソコン用 Chrome ユーザー向けの楽しい試験運用版です。2014 年に再リリースされ、全世界での利用、LEGO® MOVIETM との提携、モバイル デバイスのサポートが新たに追加されました。この記事では、プロジェクトから得られたいくつかの知見をご紹介します。特に、パソコンのみのエクスペリエンスからマウス入力とタップ入力の両方をサポートするマルチスクリーン ソリューションへの移行についてご紹介します。

「Build with Chrome」の歴史

Build with Chrome の最初のバージョンは、2012 年にオーストラリアでリリースされました。まったく新しい方法でウェブの力を実証し、Chrome をまったく新しいユーザー層に紹介したいと考えました。

このサイトは主に 2 つの部分で構成されており、ユーザーは LEGO ブロックを使って作品を作ることができる「構築」モードと、LEGO 化されたバージョンの Google マップで作品をブラウジングできる「探索」モードです。

インタラクティブな 3D は、優れた LEGO 建築体験をユーザーに提供するために不可欠でした。2012 年、WebGL はパソコンのブラウザでのみ一般公開されていたため、Build はパソコンのみのエクスペリエンスとしてターゲットにされました。データ探索ツールでは Google マップを使用して作品を表示しましたが、十分にズームすると、作品を 3D で表示する地図の WebGL 実装に切り替わり、引き続き Google マップをベースプレートのテクスチャとして使用しています。私たちは、あらゆる年代の LEGO ファンが簡単かつ直感的に創造性を表現し、お互いが生み出した作品を探求できる環境を構築したいと考えました。

2013 年、Google は「Build with Chrome」を新しいウェブ テクノロジーに拡大することを決定しました。その 1 つが Chrome for Android の WebGL です。これにより、Build with Chrome をモバイル エクスペリエンスへと進化させることが可能になります。手始めに、「Builder Tool」のハードウェアについて質問する前に、モバイルアプリと比較してブラウザで直面するジェスチャーの動作や触覚の反応を理解するために、タッチのプロトタイプを開発しました。

レスポンシブ フロントエンド

タップ入力とマウス入力の両方が可能なデバイスをサポートする必要がありました。しかし、小さなタッチ スクリーンで同じ UI を使用することは、スペースに制約があるため、最適とは言えません。

Build では、ズームイン / ズームアウト、ブロックの色の変更、そしてもちろんブロックの選択、回転、配置など、多くのインタラクティビティが行われます。ユーザーが多くの時間を費やすことが多いツールであるため、頻繁に使用するすべてのものにすばやくアクセスでき、快適に操作できることが重要です。

インタラクティブ性の高いタッチアプリを設計すると、画面がすばやく小さく感じられ、操作中にユーザーの指で画面の大部分を覆う傾向があることがわかります。Builder を使用すると、このことが明らかになりました。デザインする際には、グラフィックのピクセルではなく、物理画面サイズを考慮する必要があります。ボタンやコントロールの数を最小限に抑え、実際のコンテンツ専用の画面領域をできるだけ確保することが重要です。

目標は、Build がタッチデバイスで自然に操作できるようにすることでした。単に元のデスクトップ実装にタップ入力を追加するだけでなく、実際にタップ用に作ったもののように感じられるようにすることでした。UI には 2 つのバリエーションを作成しました。1 つは大画面のパソコン用とタブレット用、もう 1 つは画面の小さいモバイル デバイス用です。可能であれば、単一の実装を使用し、モードをスムーズに切り替えることをおすすめします。今回のケースでは、この 2 つのモードの間にエクスペリエンスに大きな違いがあるため、特定のブレークポイントを使用することにしました。この 2 つのバージョンには多くの共通点があり、ほとんどのことを 1 つのコード実装で行おうとしましたが、UI の一部の機能は両者で動作が異なります。

ユーザー エージェント データを使用してモバイル デバイスを検出し、ビューポートのサイズを確認して、小画面のモバイル UI を使用するかどうかを判断します。物理的な画面サイズの信頼できる値を取得することは難しいため、「大画面」に適したブレークポイントを設定するのはやや困難です。幸い、今回のケースでは、大画面のタッチデバイスに小画面の UI を表示するかどうかは問題になりません。このツールは引き続き正常に動作しますが、一部のボタンが少し大きすぎるように感じることがあります。最後に、ブレークポイントを 1, 000 ピクセルに設定します。横表示で 1,000 ピクセルを超えるウィンドウからサイトを読み込むと、大画面バージョンが表示されます。

2 つの画面サイズとエクスペリエンスについて少しお話しましょう。

大画面、マウスとタップのサポート

大画面バージョンは、マウスをサポートするすべてのデスクトップ パソコンと、大画面を搭載したタッチデバイス(Google Nexus 10 など)に配信されます。このバージョンは、利用できるナビゲーション コントロールの点で、元のデスクトップ ソリューションに近いものですが、タップのサポートといくつかのジェスチャーが追加されています。UI はウィンドウ サイズに応じて調整されるため、ユーザーがウィンドウのサイズを変更すると、UI の一部が削除またはサイズ変更される場合があります。これには CSS メディアクエリを使用します。

例: 設定可能な高さが 730 ピクセル未満の場合、探索モードでズーム スライダー コントロールは表示されません。

@media only screen and (max-height: 730px) {
    .zoom-slider {
        display: none;
    }
}

小画面、タップサポートのみ

このバージョンは、モバイル デバイスと小型タブレット(Nexus 4 と Nexus 7 がターゲット デバイス)に配信されます。このバージョンにはマルチタッチのサポートが必要です。

小画面のデバイスでは、コンテンツの表示領域をできるだけ広げる必要があります。そのため、使用頻度の低い要素を見えないようにして、わずかな調整を行い、スペースを最大化しました。

  • ビルドブロック選択ツールは、ビルド中に最小化されてカラーセレクタになります。
  • ズームと画面の向きのコントロールをマルチタッチ ジェスチャーに置き換えました。
  • また、Chrome の全画面表示機能は、画面スペースを確保するのに便利です。
大画面でビルドする
大画面向けに開発。ブロック選択ツールは常に表示され、右側にいくつかのコントロールがあります。
小さい画面でのビルド
小さな画面をベースにする。レンガ選択ツールが最小化され、一部のボタンが削除されました。

WebGL のパフォーマンスとサポート

最新のタッチデバイスにはかなりのパワフルな GPU が搭載されていますが、それでもデスクトップから遠く離れているため、特に多くの作品を同時にレンダリングする必要がある「3D 探索」モードでは、パフォーマンスに課題があることがわかっていました。

創造的には、形状が複雑で透明度も向上した新しいタイプのブロックをいくつか追加したいと考えました。これらのブロックは通常、GPU の負荷が非常に大きい機能です。ただし、下位互換性を確保し、最初のバージョンからの作成を引き続きサポートする必要があるため、クリエイティブ内のブリックの総数を大幅に減らすなど、新たな制限を設定できませんでした。

Build の最初のバージョンでは、1 つの作成に使用できるブロック数の上限が設定されていました。残りブロックの個数を示す「ブリック メーター」が表示されていました。新しい実装では、一部の新しいブリックが標準のブリックよりもブリック メーターへの影響が大きいため、ブリックの総数を若干減らしています。これは、適切なパフォーマンスを維持しながら新しいブロックを組み込む方法の一つでした。

3D 探索モードでは、ベースプレート テクスチャの読み込み、作品の読み込み、作品のアニメーション化とレンダリングなど、多くのことが同時に行われます。この処理には GPU と CPU の両方から多くのものが必要になるため、Chrome DevTools で多数のフレーム プロファイリングを実行して、これらの部分を可能な限り最適化しました。モバイル デバイスでは、多くのクリエイティブを同時にレンダリングしなくても済むように、少しズームを作品に近づけることにしました。

一部のデバイスでは WebGL シェーダーを再検討して簡素化する必要がありましたが、Google は常にこれを解決して前進する方法を見つけ出しました。

WebGL 以外のデバイスのサポート

サイト訪問者のデバイスが WebGL をサポートしていない場合でも、ある程度はサイトが使えるようにしたいと考えました。キャンバス ソリューションや CSS3D 機能を使用して、3D を簡単に表現できる場合があります。残念ながら、WebGL を使用せずに 3D のビルド機能と探索機能を再現するのに十分なソリューションは見つかりませんでした。

一貫性を保つため、クリエイティブの表示スタイルはすべてのプラットフォームで統一する必要があります。2.5D ソリューションも試したかもしれませんが、この方法では作品が若干違うものになってしまいます。また、「Build with Chrome」の最初のバージョンで作成した作品が、新しいバージョンのサイトでも同じように表示され、最初のバージョンと同じようにスムーズに実行されるようにする方法についても検討する必要がありました。

Explore 2D モードは、WebGL 非対応デバイスでも引き続き利用できます。ただし、3D で新しい作品の作成や探索はできません。そのため、ユーザーは、WebGL 対応デバイスを使用している場合に、プロジェクトの深さや、このツールを使用することでどのようなコンテンツを作成できるかについて、引き続き把握できます。WebGL 非対応のユーザーにとって、このサイトの価値はそれほど高くないかもしれませんが、少なくともティーザーとして機能し、試しに利用してもらう必要があります。

WebGL ソリューションのフォールバック バージョンを保持できない場合もあります。パフォーマンス、視覚的なスタイル、開発とメンテナンスの費用など、さまざまな理由が考えられます。ただし、代替を実装しないことに決めた場合は、少なくとも WebGL 非対応の訪問者に対処し、サイトに完全にアクセスできない理由を説明し、WebGL 対応のブラウザを使用して問題を解決する方法を説明する必要があります。

アセット管理

2013 年、Google は新しいバージョンの Google マップをリリースしましたが、リリース以来、ユーザー インターフェースが大きく変更されています。そこで、新しい Google マップの UI に合わせて「Build with Chrome」を再設計することを決定し、その際に他の要素も取り入れました。新しいデザインは比較的平坦で、すっきりした無地とシンプルな形状になっています。これにより、多くの UI 要素に純粋な CSS を使用できるようになり、画像の使用を最小限に抑えることができました。

[探索] では、多くの画像を読み込む必要があります。作品用のサムネイル画像、ベースプレートの地図テクスチャ、最後に実際の 3D 作品を読み込む必要があります。Google では、新しい画像を常に読み込む際にメモリリークが発生しないように細心の注意を払っています。

3D 作品は、PNG 画像としてパッケージ化されたカスタム ファイル形式で保存されます。3D 作品のデータを画像として保存することで、作品をレンダリングするシェーダーにデータを直接渡すことができるようになりました。

ユーザーが作成したすべての画像について、すべてのプラットフォームで同じ画像サイズを使用できるため、ストレージと帯域幅の使用量を最小限に抑えることができます。

画面の向きの管理

縦向きから横向き、またはその逆に、画面のアスペクト比がどれだけ変化するかは忘れがちです。モバイル デバイスに適応するには、最初からこの点を考慮する必要があります。

スクロールが有効になっている従来のウェブサイトでは、CSS ルールを適用して、コンテンツとメニューを再配置するレスポンシブなサイトを作成できます。スクロール機能を使用できる限り、かなり管理しやすくなります。

この方法は Build でも使用しましたが、レイアウトを解決する方法は少し限られていました。コンテンツを常に表示しつつ、多くのコントロールやボタンにすばやくアクセスする必要があるためです。可変レイアウトは、ニュースサイトのような純粋なコンテンツ サイトにとっては理にかなったものですが、私たちのようなゲームアプリでは苦労しました。コンテンツの概要と快適な操作方法を維持しながら、横向きと縦向きの両方で機能するレイアウトを見つけるのは困難でした。最終的に、ビルドは横向きのみにすることにし、ユーザーにデバイスを回転させるよう指示しました。

どちらの向きでも、データ探索の方がはるかに簡単に解けました。一貫性のあるエクスペリエンスを実現するには、向きに応じて 3D のズームレベルを調整するだけで済みました。

ほとんどのコンテンツのレイアウトは CSS で制御されますが、画面の向きに関連するものは JavaScript で実装する必要があります。window.orientation を使用して画面の向きを特定する適切なクロスデバイス ソリューションがないことがわかったため、最終的には、window.innerWidth と window.innerHeight を比較してデバイスの向きを特定しました。

if( window.innerWidth > window.innerHeight ){
  //landscape
} else {
  //portrait
}

タップサポートの追加

ウェブ コンテンツにタップサポートを追加するのは簡単です。クリック イベントなどの基本的なインタラクティビティは、デスクトップ デバイスとタッチ対応デバイスで同じように機能しますが、より高度なインタラクションについては、タッチイベント(touchstart、touchmove、touchend)も処理する必要があります。この記事では、これらのイベントの基本的な使い方について説明します。Internet Explorer はタッチイベントをサポートしていません。代わりにポインタ イベント(pointerdown、pointermove、pointerup)を使用します。ポインタ イベントは標準化のために W3C に送信されていますが、現時点では Internet Explorer でのみ実装されています。

3D 探索モードでは、標準の Google マップの実装と同じナビゲーションが必要でした。1 本の指で地図をパンし、2 本の指でピンチ操作でズームしました。作品は 3D であるため、2 本の指で回転するジェスチャーも追加されました。通常は、タッチイベントを使用する必要があります。

イベント ハンドラでの 3D の更新やレンダリングなど、負荷の高いコンピューティングは回避することをおすすめします。代わりに、タップ入力を変数に格納し、requestAnimationFrame レンダリング ループで入力に応答します。これにより、マウスを同時に実装することも容易になり、対応するマウスの値を同じ変数に格納するだけで済みます。

まず、入力を格納するオブジェクトを初期化し、touchstart イベント リスナーを追加します。各イベント ハンドラで event.preventDefault() を呼び出します。これは、ブラウザでタップイベントの処理が継続されないようにするためです。これにより、ページ全体のスクロールやスケーリングなどの予期しない動作が発生する可能性があります。

var input = {dragStartX:0, dragStartY:0, dragX:0, dragY:0, dragDX:0, dragDY:0, dragging:false};
plateContainer.addEventListener('touchstart', onTouchStart);

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
    //start listening to all needed touchevents to implement the dragging
    document.addEventListener('touchmove', onTouchMove);
    document.addEventListener('touchend', onTouchEnd);
    document.addEventListener('touchcancel', onTouchEnd);
  }
}

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }
}

function onTouchEnd(event) {
  event.preventDefault();
  if( event.touches.length === 0){
    handleDragStop();
    //remove all eventlisteners but touchstart to minimize number of eventlisteners
    document.removeEventListener('touchmove', onTouchMove);
    document.removeEventListener('touchend', onTouchEnd);
    //also listen to touchcancel event to avoid unexpected behavior when switching tabs and some other situations
    document.removeEventListener('touchcancel', onTouchEnd);
  }
}

入力の実際の格納は、イベント ハンドラではなく、handleDragStart、handleDragging、handleDragStop という別のハンドラで行います。これは、マウス イベント ハンドラからもこれらを呼び出せるようにする必要があるためです。可能性は低いものの、ユーザーがタップとマウスを同時に使用する場合があることに留意してください。Google では、このようなケースに直接対処するのではなく、問題が起こらないことを確認しています。

function handleDragStart(x ,y ){
  input.dragging = true;
  input.dragStartX = input.dragX = x;
  input.dragStartY = input.dragY = y;
}

function handleDragging(x ,y ){
  if(input.dragging) {
    input.dragDX = x - input.dragX;
    input.dragDY = y - input.dragY;
    input.dragX = x;
    input.dragY = y;
  }
}

function handleDragStop(){
  if(input.dragging) {
    input.dragging = false;
    input.dragDX = 0;
    input.dragDY = 0;
  }
}

touchmove に基づいてアニメーションを実行する場合、最後のイベントからの差分移動も保存すると役に立つことがよくあります。たとえば、このパラメータは、Explore ですべてのベースプレート間を移動する際のカメラの速度のパラメータとして使用しています。ベースプレートをドラッグしているのではなく、実際にカメラを動かしているからです。

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );

  //execute animation based on input.dragDX, input.dragDY, input.dragX or input.dragY
 /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

埋め込みの例: タッチイベントを使用してオブジェクトをドラッグする。「Build with Chrome」で 3D 探索マップをドラッグする場合と同様の実装: http://cdpn.io/qDxvo

マルチタッチ ジェスチャー

マルチタッチ ジェスチャーの管理を簡素化できるフレームワークやライブラリはいくつか(HammerQuoJS など)もありますが、複数のジェスチャーを組み合わせて完全に制御したい場合は、ゼロから行うのが最善である場合もあります。

ピンチ操作と回転操作を管理するため、2 本目の指を画面に置いたときの 2 本の指の間の距離と角度を保存します。

//variables representing the actual scale/rotation of the object we are affecting
var currentScale = 1;
var currentRotation = 0;

function onTouchStart(event) {
  event.preventDefault();
  if( event.touches.length === 1){
    handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGestureStart(x1, y1, x2, y2){
  input.isGesture = true;
  //calculate distance and angle between fingers
  var dx = x2 - x1;
  var dy = y2 - y1;
  input.touchStartDistance=Math.sqrt(dx*dx+dy*dy);
  input.touchStartAngle=Math.atan2(dy,dx);
  //we also store the current scale and rotation of the actual object we are affecting. This is needed to support incremental rotation/scaling. We can't assume that an object is always the same scale when gesture starts.
  input.startScale=currentScale;
  input.startAngle=currentRotation;
}

touchmove イベントでは、この 2 本の指の間の距離と角度を継続的に測定します。次に、開始距離と現在の距離の差を使用してスケールを設定し、開始角度と現在の角度の差を使用して角度を設定します。

function onTouchMove(event) {
  event.preventDefault();
  if( event.touches.length  === 1){
    handleDragging(event.touches[0].clientX, event.touches[0].clientY);
  }else if( event.touches.length === 2 ){
    handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY );
  }
}

function handleGesture(x1, y1, x2, y2){
  if(input.isGesture){
    //calculate distance and angle between fingers
    var dx = x2 - x1;
    var dy = y2 - y1;
    var touchDistance = Math.sqrt(dx*dx+dy*dy);
    var touchAngle = Math.atan2(dy,dx);
    //calculate the difference between current touch values and the start values
    var scalePixelChange = touchDistance - input.touchStartDistance;
    var angleChange = touchAngle - input.touchStartAngle;
    //calculate how much this should affect the actual object
    currentScale = input.startScale + scalePixelChange*0.01;
    currentRotation = input.startAngle+(angleChange*180/Math.PI);
    //upper and lower limit of scaling
    if(currentScale<0.5) currentScale = 0.5;
    if(currentScale>3) currentScale = 3;
  }
}

各 touchmove イベント間の距離の変化は、ドラッグの例と同様の方法で使用できるかもしれませんが、連続的な移動が必要な場合には、多くの場合、この方法のほうが便利です。

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //execute transform based on currentScale and currentRotation
  /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX=0;
  input.dragDY=0;
}

必要に応じて、ピンチ操作や回転操作中にオブジェクトのドラッグを有効にすることもできます。その場合は、2 本の指の中心点をドラッグ ハンドラへの入力として使用します。

埋め込みの例: オブジェクトを 2D で回転してスケーリングする。Explore の地図の実装と同様: http://cdpn.io/izloq

同じハードウェア上でのマウスとタッチのサポート

現在、Chromebook Pixel など、マウス入力とタップ入力の両方に対応したノートパソコンがいくつかあります。注意しないと予期しない動作が発生することがあります。

重要なのは、タップのサポートを検出してマウス入力を無視するだけでなく、タップのサポートを同時にサポートする必要があることです。

タッチイベント ハンドラで event.preventDefault() を使用していない場合は、タッチに最適化されていないほとんどのサイトが引き続き動作し続けるように、エミュレートされたマウスイベントも発生します。たとえば、画面を 1 回タップした場合、これらのイベントは次の順序で素早く発生する可能性があります。

  1. タッチスタート
  2. タッチ移動
  3. タッチエンド
  4. マウスオーバー
  5. mousemove
  6. マウスダウン
  7. マウスアップ
  8. クリック

より複雑な操作の場合、これらのマウスイベントによって予期しない動作が発生し、実装に支障をきたすことがあります。event.preventDefault() をタッチイベント ハンドラで使用し、マウス入力を別々のイベント ハンドラで管理することをおすすめします。タッチイベント ハンドラで event.preventDefault() を使用すると、スクロールやクリック イベントなどの一部のデフォルトの動作もできなくなることに注意してください。

「Build with Chrome では、ユーザーがサイトをダブルタップしてもズームが発生しないようにしたかったのですが、これはほとんどのブラウザでは標準的な方法です。そのため、ビューポート メタタグを使用して、ユーザーがダブルタップしてもズームしないようブラウザに指示します。これにより、クリックの遅延(300 ミリ秒)も解消され、サイトの応答性が向上します。(クリック遅延は、ダブルタップによるズームが有効になっている場合に、シングルタップとダブルタップを区別するために設けられています)。

<meta name="viewport" content="width=device-width,user-scalable=no">

なお、この機能を使用する場合、ユーザーは画面を拡大して縮小できないため、あらゆる画面サイズでサイトを読み取れるようにするかどうかはデベロッパー次第です。

マウス、タップ、キーボード入力

「探索 3D モード」では、マウス(ドラッグ)、タップ(ドラッグ、ピンチ操作によるズーム、回転)、キーボード(矢印キーによる移動)の 3 つの方法で地図を操作する必要がありました。これらのナビゲーション メソッドの動作はどれも若干異なりますが、イベント ハンドラで変数を設定し、その操作は requestAnimationFrame ループで行います。requestAnimationFrame ループは、移動に使用されるメソッドを認識する必要はありません。

たとえば、3 つの入力方法すべてで地図の移動(dragDX と dragDY)を設定できます。キーボードの実装は次のとおりです。

document.addEventListener('keydown', onKeyDown );
document.addEventListener('keyup', onKeyUp );

function onKeyDown( event ) {
  input.keyCodes[ "k" + event.keyCode ] = true;
  input.shiftKey = event.shiftKey;
}

function onKeyUp( event ) {
  input.keyCodes[ "k" + event.keyCode ] = false;
  input.shiftKey = event.shiftKey;
}

//this needs to be called every frame before animation is executed
function handleKeyInput(){
  if(input.keyCodes.k37){
    input.dragDX = -5; //37 arrow left
  } else if(input.keyCodes.k39){
    input.dragDX = 5; //39 arrow right
  }
  if(input.keyCodes.k38){
    input.dragDY = -5; //38 arrow up
  } else if(input.keyCodes.k40){
    input.dragDY = 5; //40 arrow down
  }
}

function onAnimationFrame() {
  requestAnimationFrame( onAnimationFrame );
  //because keydown events are not fired every frame we need to process the keyboard state first
  handleKeyInput();
  //implement animations based on what is stored in input
   /*
  /
  */

  //because touchmove is only fired when finger is actually moving we need to reset the delta values each frame
  input.dragDX = 0;
  input.dragDY = 0;
}

埋め込みの例: マウス、タップ、キーボードを使用して移動: http://cdpn.io/catlf

まとめ

Build with Chrome をさまざまな画面サイズのタッチデバイスに対応させるのは、これまで多くのことを学びました。チームはタッチデバイスでこのレベルのインタラクティビティを行う経験があまりなく、その過程で多くのことを学びました。

最大の課題はユーザー エクスペリエンスとデザインをどのように解決するかということでした。技術的な課題は、さまざまな画面サイズ、タッチイベント、パフォーマンスの問題の管理でした。

タッチデバイスの WebGL シェーダーには問題もありましたが、想定以上にうまくいきました。デバイスはますますパワフルになり、WebGL の実装は急速に進歩しています。近い将来、さまざまなデバイスで WebGL が使用されるようになると思われます。

まだ作成していない場合は、ぜひ素晴らしいものを作りましょう