ルートに沿って、スクロールでカメラをぬるぬる移動させる3DのScrollyTellingを実装しました。
実装では、タイトル通りMapbox GL JSのFreeCamera APIを利用しています。
FreeCamera API とは
Mapbox GL JSがv1からv2にメジャーアップデートした際に追加された新機能です。このAPIを使って、3Dマップ上のカメラの位置や角度を自由に操作することができます。
カメラの位置や角度は、FreeCameraOptionオブジェクト内の次のインスタンスで管理されています。
- lookAtPoint
- setPitchBearing
今回は lookAtPoint を利用します。
詳細は公式ドキュメントをご確認ください。
デモとフルコード
▽デモ
https://bl.ocks.org/Hirosaji/raw/fc6c210918712e8290677ce7a46bff1d/
▽フルコード
要点の解説
今回のデモでは、スクロールイベントとFreeCamera APIを連動させたScrollyTellingを実装しました。
FreeCamera APIでは、カメラを制御する2つのルートのデータを使います。1つはカメラの注視点の移動ルート。もう1つはカメラ自体の移動ルートのデータです。まずは、これらのデータを作る一例を紹介します。
データ①: カメラの注視点の移動ルート
ここで使うデータは、カメラの中央に位置する注視点の移動ルートです。つまり、ScrollyTellingのテーマとなる何らかの経路のデータです。
デモでは「富士山の5合目からの登頂ルート」をテーマにしました。 選んだのは、登山客の半数以上が利用する吉田ルートの登山道です。
データは、OpenStreetMap(OSM)から取得しました。
OSMからOSMデータを取得できたら、GeoJSONに変換します。
こうして作ったGeoJSONから、使いたいルートを抽出しましょう。
(OSMを使う際は、クレジット表記を忘れず)
データ②: カメラ自体の移動ルート
カメラ自体が辿るルートです。高度のパラメータを別途指定できるので、データ①と同じでも良いです。
しかし、例えばジグザグのルートが含まれると、カメラはそのルートに沿って激しく振られてしまいます。
そのユーザ体験は3D酔いの原因になるため、データの簡素化が必要です。
データの簡素化には、mapshaper が便利です。
コードの要点
FreeCamera API を操作するコードを一部抜粋して解説します。
... // get the overall length of each route so we can interpolate along them const routeLength = turf.length(turf.lineString(targetRoute)); const cameraRouteLength = turf.length(turf.lineString(targetRoute)); function frame() { ... // use the rate to get a point that is the appropriate length along the route // this approach syncs the camera and route positions ensuring they move // at roughly equal rates even if they don't contain the same number of points const alongRoute = turf.along( turf.lineString(shortTargetRoute), routeLength * rate ).geometry.coordinates; const alongCamera = turf.along( turf.lineString(cameraRoute), cameraRouteLength * rate ).geometry.coordinates; const camera = map.getFreeCameraOptions(); // set the position and altitude of the camera camera.position = mapboxgl.MercatorCoordinate.fromLngLat( { lng: alongCamera[0], lat: alongCamera[1] }, cameraAltitude ); // tell the camera to look at a point along the route camera.lookAtPoint({ lng: alongRoute[0], lat: alongRoute[1] }); map.setFreeCameraOptions(camera); } let ticking = false; function scrollEvent() { if (!ticking) { requestAnimationFrame(frame); ticking = true; } } document.addEventListener('scroll', scrollEvent, {passive: true});
ザックリどんなことをしてるかと言うと...
- まず Turf.js でスクロール率を rate という変数で管理
- その rate で、カメラの注視点/カメラ自体の移動ルートからスクロール率に即した位置を算出
- 算出したそれぞれの位置を、FreeCamera API のオブジェクトに設定(このとき lookAtPoint を使う)
- カメラを移動
をスクロールイベントが発火するたびに繰り返しています。
(スクロールイベントは EventListener と requestAnimationFrame で管理)
改善点
lookAtPoint を使う場合は現状、カメラ自体の移動ルートをうまくデザインする必要があります。
デザインに失敗すると例えば、注視点の位置を追い越して、カメラが予期せぬ方向を向いてしまいます。また、前述した3D酔いの問題もあります。
そのため、現状の実装ではデータ作りのプロトタイピングが必須です。
上手い解決法が無いものか...。