もう一つのParty Parrot
はじめに
yukinaritさんの記事「mapbox-gl-js上でParrotをPartyさせる方法3選」に加えてもう一つParrotをPartyさせる方法をご紹介します。
Partyのさせかた
yukinaritさんの記事にもあった通り、シンボルレイヤーに動画GIFを指定してもアニメーションしません。しかし、layout
のicon-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の画像のフルパスを返す関数です。loadImage
はMap#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での実装はまた別の記事で取り組んでみようと思います。
Discussion