モバイル端末で見たときにサイトのコンテンツを重力で操作したい!
……というただそれだけの話なんですが、至る所に罠が散りばめられていて大変苦労しているのでまとめを書いておきます。
ちなみにほぼ全て Apple のせいです。
前提
多分よく知られている話です。
DeviceMotionEvent で端末の加速度を検出する
端末の加速度が変化すると devicemotion
イベントが走るので、window.addEventListener("devicemotion", ...)
等でイベントリスナを登録しておきます。
iOS 端末では許可のリクエストが必要
最初の罠がここにあります。
ほとんど Apple の独自仕様と言えるような仕様により[1]、Safari on iOS を利用する場合に限り DeviceMotionEvent.requestPermission()
を呼び出してユーザーに明示的に許可を求める必要があります。ちなみに、この関数は他のブラウザではそもそもサポートされていないので呼び出そうとするとエラーになります。したがって DeviceMotionEvent.requestPermission
が定義されているかどうかのチェックが必須となります。
さらに、この関数はいつでも呼び出して良いわけではなく、ユーザーのタップ操作[2]の結果生じるイベントハンドラ内で呼び出す必要があります。
成功するとユーザーに許可を求めるダイアログが開き、ユーザーが許可すると晴れて devicemotion
イベントが検知できるようになります。
iOS 端末では加速度の向きが逆
2つ目の罠です。MDN によると DeviceMotionEvent.accelerationIncludingGravity
は重力の影響を含んだ加速度を表し、重力加速度そのものを表すわけではありません。
例えば、鉛直上向きを Z 軸の正の向きとします。このとき、重力加速度自体は鉛直下向きにかかるので、Z- 方向に働きます。しかし、デバイスを手に持っている場合、手はデバイスを支えるために重力加速度に逆らってデバイスを上向きに加速させています。この加速度が accelerationIncludingGravity
の示す加速度になります。つまり、Z+ 方向の加速度が検出されている状態が正解になります。
しかし、iOS 端末ではこの値が Z- 方向になります。というか、acceleration
も含めて全ての加速度が反転していて、つまりシンプルに間違っており、仕様に反しています。
ではなぜ修正されないのかというと、恐らく「iOS 端末を検知して加速度を反転させるコード」が広く出回りすぎたせいで、今更直すとそれはそれで大変なことになるからではないかと思われます。
というわけで、navigator.userAgent
に "iPhone"
あるいは "iPad"
が含まれるかどうかを判定して、含まれる場合は加速度を反転させることで正しい加速度を得ることができるようになります。
闇
本題です。
User Agent は嘘をつく
3つ目の罠です。iPad の Safari はユーザーエージェントに "iPad"
を含みません[3]。代わりに "Macintosh"
を含みます。加速度センサー対応の Mac は聞いたことがないので、Macintoch が入ってたら iPad だと見なしてもまあ問題ないんじゃないでしょうか[4]。
このせいでサイトのコンテンツを iPad の Safari で見たときに重力加速度が反転していたようです。
しかし、本来 Apple が重力加速度を仕様に沿って扱っていれば User Agent による判別自体が不要だったはずなのです。現在新しい Accelerometer API の導入が計画されているようなので、そちらではぜひとも仕様に合わせてほしいところです。
デバイス座標系が一部の iPad で矛盾している
最後の罠です。W3C の仕様書によると、Device Motion で得られる加速度は device coordinate system によるとあります。デバイス座標系は、定義によると「端末を自然な向きで持ったとき、右が X+ 方向、上が Y+ 方向、手前が Z+ 方向」であるそうです。
ここで注意しなければならないのが、デバイス座標系は端末に固定された座標系であり、たとえ画面の回転ロックを解除し、スマホを横持ちにして横長のスクリーンモードでページを見ていたとしても、デバイス座標系は回転しないということです[5]。
つまり、重力でボールが落ちるようなコンテンツを横向きのスクリーンでも正しく表示したい場合、画面の回転を加味して DeviceMotionEvent
から取ってきた値を適切に回転させる必要があります。
となると、画面の回転状態を取得する必要が出てきますが、よく使われていた Window.orientation
は現在は非推奨になっており、代わりに Screen.orientation
を使うことが推奨されています。
さて、ここで「Screen.orientation
は自然な向きからのスクリーンの回転角を表す」ことが仕様により定められていますが、端末の自然な向きが縦向きとは限らないことに注意する必要があります。例えばPCのモニターなどは、キーボードに合わせた横長の向きが自然な向きになりそうですし、実際そう定義されています。現在では、一部の iPad では縦持ちの状態で Screen.orientation
が 90
を返します。つまり横向きで使う方を「自然」として想定しているようです。
さてさて、デバイス座標系の定義は
端末を自然な向きで持ったとき、右が X+ 方向、上が Y+ 方向、手前が Z+ 方向
でしたから、「縦持ちの状態で Screen.orientation
が 90
を返す」、すなわち「横持ちが自然な向きと想定されている」端末では、X 軸が画面の長い方に沿うようになっているはずですね。
なっていません。
iPad では。
普通に縦持ちの状態を基準にデバイス座標系が取られています。
しかも、自然な向きが異なる iPad 同士が同一の User Agent を有していることがあるので、もはや検出が不可能です。
終わりです。現状メジャーなデバイス全てで正しい向きの加速度を取ることは不可能です。
~終了~
希望
実は一つだけ救いがあり、それは「非推奨の Window.orientation
プロパティは現状確認されている全ての端末でデバイス座標系から見て正しい角度を返す」ということです。横持ちが自然な向きと定められているらしい(Screen.orientation
を信用するならば)iPad でも、Window.orientation
はデバイス座標系に矛盾しない 0
の値を返してくれます。
Screen.orientation
は比較的最近広く使えるようになった[6] API ですが、この状態では当分非推奨の Window.orientation
を使わざるを得ません。他のデバイスが足並み揃えて進んでいる中で、なまじシェア率の高い Apple のデバイスだけがコケるせいで Web 全体の進歩が後れを取っている一例と言えるでしょう(あくまで一例です)。
- この仕様自体は悪くないと思っています。端末の加速度って結構な情報持ってますしね。 ↩︎
-
長押し、スワイプ等は不可。
click
イベントハンドラ内では常に可能だが、touchend
等のイベントハンドラ内でも一定の条件(click
と同等の操作であるとブラウザが認めた場合)を満たせば可能。 ↩︎ - Chrome で見ると含まれるようです ↩︎
- なんか大昔の MacBook は加速度センサーに対応していたらしいみたいな話が流れてきました。マジで? ↩︎
- スクリーンと一緒に回転する座標系はスクリーン座標系と定義されています ↩︎
- iOS ではバージョン 16.4 (2023/3/27 発表) 未満の端末だと使えず、現状では「一般に使える」とは言い切れない普及率でもありそうです ↩︎