🦜

もう一つのParty Parrot

2023/05/29に公開

はじめに

yukinaritさんの記事「mapbox-gl-js上でParrotをPartyさせる方法3選」に加えてもう一つParrotをPartyさせる方法をご紹介します。

Partyのさせかた

yukinaritさんの記事にもあった通り、シンボルレイヤーに動画GIFを指定してもアニメーションしません。しかし、layouticon-imageプロパティで指定している画像を次々と変更していくと、パラパラ漫画の要領でアニメーションが表現できます。

早速試してみましょう。

画像の準備

まず、Party ParrotのGIF動画を以下のサイトからダウンロードします。

次に、ImageMagickを使ってGIF動画をフレームごとに分解します。

% convert parrot.gif +adjoin frame.gif

これでframe-0.gifからframe-9.gifまでの10枚のGIF画像が作成されました。

コードを書く

シンボルレイヤーに任意の画像をアイコンとして表示する方法は以下のサンプルが参考になります。

ただし、画像の枚数が多い場合にloadImageメソッドをそのまま使うとネストが深くなります。今回は画像が10枚なので、10階層となりコードの見通しが相当悪くなることが予想されます。

そこで、Mapboxのドキュメントでは該当する記述は見つかりませんでしたが、以下の記事で紹介されている通りloadImageはPromise化しておくほうが良いでしょう。

まず、Parrotを表示させるポイントを指定するGeoJSONを作成します。

const geojson = {
  type: "geojson",
  data: {
    type: "FeatureCollection",
    features: [
      {
        type: "Feature",
        geometry: {
          type: "Point",
          coordinates: [139.763906, 35.6811649]
        }
      }
    ]
  }
};

地図の表示はいつも通りです。

mapboxgl.accessToken = YOUR_MAPBOX_PUBLIC_TOKEN_HERE;
const map = new mapboxgl.Map({
  container: "map",
  style: "mapbox://styles/mapbox/light-v11",
  zoom: 14,
  center: [139.763906, 35.6811649]
});

ヘルパー関数を準備します。getImageUrlはParrotの画像のフルパスを返す関数です。loadImageMap#loadImageをPromise化したものです。

const getImageUrl = (name) => {
  return `https://raw.githubusercontent.com/OttyLab/Zenn/party-parrot/code/articles/e8702cb8d3ec0e/images/${name}`
}

const loadImage = (url) => {
  return new Promise((resolve, reject) => {
    map.loadImage(url, (error, image) => {
      if (error) {
        reject(error);
      } else {
        resolve(image);
      }
    });
  });
};

ここからの処理は地図がロードされた後に行います。また、ヘルパー関数のloadImageが非同期関数のため、全体をasync関数でくるんでいます。

map.on("load", () => {
  (async () => {
  ...
  })();
});

まず、ヘルパー関数を使って10枚の画像を取得します。Promise.allにより、全部ダウンロードが成功したときのみ処理が次に進みます。

const images = await Promise.all([
  loadImage(getImageUrl('frame-0.gif')),
  loadImage(getImageUrl('frame-1.gif')),
  loadImage(getImageUrl('frame-2.gif')),
  loadImage(getImageUrl('frame-3.gif')),
  loadImage(getImageUrl('frame-4.gif')),
  loadImage(getImageUrl('frame-5.gif')),
  loadImage(getImageUrl('frame-6.gif')),
  loadImage(getImageUrl('frame-7.gif')),
  loadImage(getImageUrl('frame-8.gif')),
  loadImage(getImageUrl('frame-9.gif')),
]);

ダウンロードした画像をaddImageで登録します。

images.forEach((image, index) => {
  map.addImage("parrot" + index, image);
});

addSourceでソースの追加、addLayerでレイヤーの作成をしています。パラパラ漫画化するため、icon-imageのイメージ名はcountの値が後ろに付きます。

map.addSource("point", geojson);

let count = 0;
map.addLayer({
  id: "parrot",
  type: "symbol",
  source: "point",
  layout: {
    "icon-image": `parrot${count}`,
    "icon-size": 0.25
  }
});

最後に、50ms間隔でicon-imageを変更する処理を入れます。setLayoutPropertyメソッドでレイヤーのlayoutのプロパティを変更できます。第一引数がレイヤー名、第二引数がプロパティ名、第三引数がその値です。ここではcountをインクリメントすることで次の画像に切り替えます。

setInterval(() => {
  map.setLayoutProperty("parrot", "icon-image", `parrot${++count % (images.length)}`);
}, 50);

結果は以下のとおりです。

setIntervalの第二引数の値を変更することでParty速度を変更できます。

もっとParty!

それではParrotの数が増えたときの挙動を見てみましょう。こちらの記事ではサークルレイヤーの実装はパフォーマンスが良いことがわかりました。これはアニメーションを実行するシンボルレイヤーについても同じことが言えます。

東京周辺の領域にランダムにPointデータを配置したGeoJSONを用いてParrotを表示したものが以下になります。パン(スクロール)してもスムーズに追従することがわかります。

試しにMarkerで実装するとかなり重いことがわかります。

まとめ

一定間隔でシンボルレイヤーに指定するicon-imageを入れ替えることで、パラパラ漫画の要領でアニメーションを表示できることがわかりました。一見強引な手法に見えますが、パフォーマンス劣化が少ないこと、モバイルSDK(Android, iOS, Flutter)でも同じ手法が使用できることから有用な手段と言えます。

モバイルSDKでの実装はまた別の記事で取り組んでみようと思います。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion