Hirosaji Tech Blog 🍙

Web開発の記事が多め。絵師支援の記事も少し。

MapboxのFreeCamera APIでScrollyTellingを作る

ルートに沿って、スクロールでカメラをぬるぬる移動させる3DのScrollyTellingを実装しました。

Mapbox FreeCameraAPI 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合目からの登頂ルート」をテーマにしました。 選んだのは、登山客の半数以上が利用する吉田ルートの登山道です。

データは、OpenStreetMapOSM)から取得しました。

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});

ザックリどんなことをしてるかと言うと...

  1. まず Turf.js でスクロール率を rate という変数で管理
  2. その rate で、カメラの注視点/カメラ自体の移動ルートからスクロール率に即した位置を算出
  3. 算出したそれぞれの位置を、FreeCamera API のオブジェクトに設定(このとき lookAtPoint を使う)
  4. カメラを移動

をスクロールイベントが発火するたびに繰り返しています。
(スクロールイベントは EventListener と requestAnimationFrame で管理)

改善点

lookAtPoint を使う場合は現状、カメラ自体の移動ルートをうまくデザインする必要があります。

デザインに失敗すると例えば、注視点の位置を追い越して、カメラが予期せぬ方向を向いてしまいます。また、前述した3D酔いの問題もあります。
そのため、現状の実装ではデータ作りのプロトタイピングが必須です。

上手い解決法が無いものか...。

参考リンク