メインコンテンツまでスキップ

セーフエリア

多くのスマートフォンでは、画面の一部が、ノッチやカメラの切り欠き、角丸、下端に沿ったホームインジケーターによって覆われています。ブラウザは、画面の残りの部分であるセーフエリアを、4つのCSS環境変数で表します。safe-area-inset-top、safe-area-inset-right、safe-area-inset-bottom、safe-area-inset-leftは、ページの各辺から覆われる可能性のある距離をCSSピクセルで表します。キャンバス内UIはこれらの値を知らないため、コードで読み取り、インターフェースの重要な部分をその内側に収めてください。

横向きの2台のスマートフォン。どちらも左側にカメラの切り欠き、下部にホームインジケーターがあります。1台目では、HUDの隅にあるエレメントが一部隠れています。2台目では、それらがセーフエリアの内側に収まっています

ページを画面の端まで広げる​

ページが特に指定しない限り、こうしたスマートフォンのブラウザは、横向きのときに画面の両側に帯を残すなどして、自らページを覆われる領域から遠ざけ、インセットとして0を報告します。画面全体を使うには、ページのviewportのmetaタグにviewport-fit=coverを指定してオプトインします。

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

エディターから公開したアプリケーションには、これが含まれています。自分で作成するページには、これを追加してください。

インセットの読み取り​

環境変数はCSSからしか使えないため、それらをパディングに指定したHTML要素を通して読み取ります。

// セーフエリアのインセットをパディングに指定した要素から、インセットをCSSピクセル単位で読み取る
function readSafeAreaInsets() {
const probe = document.createElement('div');
probe.style.cssText = 'position: fixed; visibility: hidden; pointer-events: none; ' +
'padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);';
document.body.appendChild(probe);
const style = getComputedStyle(probe);
const insets = {
top: parseFloat(style.paddingTop),
right: parseFloat(style.paddingRight),
bottom: parseFloat(style.paddingBottom),
left: parseFloat(style.paddingLeft)
};
probe.remove();
return insets;
}

インセットはスマートフォンの向きを変えると変わるため、キャンバスのサイズが変わるたびに読み取り直してください。

インターフェースをセーフエリア内に収める​

スコア、ボタン、メニューなど、インターフェースの重要な部分を、スクリーン全体を覆うグループエレメントの下に置き、そのグループにインセットと同じ大きさのマージンを設定します。これらの部分はスクリーンではなくグループにアンカーされるため、セーフエリアの内側にとどまります。一方、スクリーンにアンカーされた背景や装飾は、引き続きスクリーンの端まで届きます。

インセットはCSSピクセル単位で、マージンはスクリーンの単位で表されます。スクリーン空間のスクリーンでは、CSSピクセルの1ピクセルは描画バッファのcanvas.width / canvas.clientWidthピクセルに、スクリーンの1単位は描画バッファのscaleピクセルに相当します(スクリーンを参照)。インセットはページの端から測られるため、以下の例では、キャンバスがページ全体を覆っていることを前提としています。

// セーフエリア内にとどめるべきものをすべて入れる、スクリーン全体を覆うグループ
const safeArea = new pc.Entity('safe area');
safeArea.addComponent('element', {
type: pc.ELEMENTTYPE_GROUP,
anchor: [0, 0, 1, 1],
margin: [0, 0, 0, 0]
});
screen.addChild(safeArea);

// グループにアンカーされているため、一時停止ボタンはセーフエリアの内側にとどまる
const pauseButton = new pc.Entity('pause button');
pauseButton.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [1, 1, 1, 1],
pivot: [1, 1],
width: 80,
height: 80,
useInput: true
});
safeArea.addChild(pauseButton);
pauseButton.setLocalPosition(-20, -20, 0);

const applySafeArea = () => {
const insets = readSafeAreaInsets();
const canvas = app.graphicsDevice.canvas;
const units = (canvas.width / canvas.clientWidth) / screen.screen.scale;
safeArea.element.margin = new pc.Vec4(
insets.left * units,
insets.bottom * units,
insets.right * units,
insets.top * units
);
};
applySafeArea();
app.graphicsDevice.on('resizecanvas', () => applySafeArea());

HTMLとCSSで構築したインターフェースでは、例えばルート要素にpadding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)を指定して、環境変数を直接使えます。

スマートフォンなしでのテスト​

デスクトップのブラウザは、インセットとして0を報告します。デスクトップでレイアウトを確認するには、readSafeAreaInsetsが実際の値の代わりにテスト用の値を返すようにします。例えば、左側にカメラの切り欠きがある横向きのスマートフォンの値です。

// 実際のインセットの代わりに使うテスト用の値。公開前に削除すること
function readSafeAreaInsets() {
return { top: 0, right: 47, bottom: 21, left: 47 };
}

スマートフォンなしで実際のインセットを使ってテストするには、Xcodeに付属するiOS Simulatorなどのデバイスシミュレーターを使います。

関連情報​

  • スクリーン - インターフェースのスケーリングと、あらゆるキャンバスへの対応
  • HTMLとCSS - DOMで構築するインターフェース
  • モバイルUIセーフエリア - 再利用できるセーフエリアスクリプトを含むエディターのプロジェクト
  • env() - MDNのCSS環境変数の解説