Mapbox Newsletter WEEKLY TIPSの解説 -「ホバー効果を作成」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「ホバー効果を作成」についての解説です。このサンプルではeventの中でも、特にmousemoveとmouseleaveの使い方を紹介しています。また、Newsletterの購読はこちらからお申し込みいただけます。
コードを確認
まずExamplesのコードを見に行きましょう。
日本語サイト
英語サイト
基本的に同じコードですが、英語版はスタイルがMapbox Streets v12にアップグレードされているのでこちらを使用します。Mapbox Streets v11ではデフォルトのプロジェクションがWebメルカトルであるのに対し、Mapbox Streets v12ではGlobe(3D表示された地球)なので、印象がかなり異なります。
HTML/CSS
まずHTMLを見ていきましょう。
以下は地図を表示するエレメントを作成しています。
<div id="map"></div>
Mapの作成
次にJavaScriptのコードを見ていきます。以下のコードはいつも通り、Mapオブジェクトを作成しています。containerで地図を表示するHTMLエレメントのidを指定します。
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/streets-v12',
center: [-100.486052, 37.830348],
zoom: 2
});
変数の準備
選択されたPolygonのidを一次保存する変数を宣言しています。
let hoveredPolygonId = null;
ソース、レイヤーの作成
loadイベント(map.on('load', () => {})の中身)で1つのソース、2つのレイヤーを追加しています。
まずソースを作成しています。アメリカ合衆国の州の形を表現したポリゴンが格納されたGeoJSONをstatesというidのソースとして読み込んでいます。
map.addSource('states', {
'type': 'geojson',
'data': 'https://docs.mapbox.com/mapbox-gl-js/assets/us_states.geojson'
});
次に1つ目のレイヤーをstate-fillsというidで作成しています。ソースは先程作成したstatesを使用します。このレイヤーは州のPolygonの色と不透明度を表現しています。マウスカーソルがある州の上に乗っかっている時には、その州だけ不透明度を1にし、それ以外は0.5にすることでマウスカーソルのある州を目立たせるホバー効果を実現しています。
map.addLayer({
'id': 'state-fills',
'type': 'fill',
'source': 'states',
'layout': {},
'paint': {
'fill-color': '#627BC1',
'fill-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
1,
0.5
]
}
});
fill-opacityのExpressionsが少し複雑なので分解してみていきます。
-
case: 第1引数に条件、第2引数に条件にマッチした際の値、第3引数に条件、第4引数に条件にマッチした際の値、...、第2n+1引数に全てにマッチしなかったときの値を記述します。ここでは第1引数が['boolean'...]、第2引数が1、第3引数が0.5なので、「['boolean'...]が真ならば1、そうでないなら0.5」がfill-opacityの値になります。 -
boolean: 引数の真偽値を返します。複数の引数があるときは前から評価していき、真偽値が得られたところで評価を終了します。ここでは['feature-state', 'hover']がnullになることがあるので、その場合は第2引数であるfalseがbooleanの評価値となります。 -
feature-state: feature stateの値を取得します。ここではhoverの値を取得します。あとから出てきますが、feature stateはsetFeatureStateで設定された値であって、ソースのプロパティとは無関係です。feature stateが付与されていないときはnullが返ります。そのため、['boolean', ...]を使う必要がありました。
2つ目のレイヤーは州の境界線を表現しています。
map.addLayer({
'id': 'state-borders',
'type': 'line',
'source': 'states',
'layout': {},
'paint': {
'line-color': '#627BC1',
'line-width': 2
}
});
マウスイベント
いよいよマウスイベントです。
mousemove
まず1つ目がmousemoveイベントです。このイベントは地図上でマウスが動く度に発火します。ただし、第2引数にレイヤーidが指定されている場合には、そのレイヤーが表示されている部分を動いたときのみ発火します。ここではstate-fillsが指定されているので、州のPolygon上を移動したときのみ発火します。
また、コールバック関数の引数eにはqueryRenderedFeaturesで取得できるものと同じ値が入っています。そこでif (e.features.length > 0)ではfeatureが1個以上取れているかどうかを確認しています。あとでe.features[0]のようにインデックス0に直接アクセスするコードがあるので念のために1個以上あることを確認しています。state-fillsレイヤー上でしか発火しないことから必ずfeatureが1個はあるはずですが、念のために確認しているということでしょう。
map.on('mousemove', 'state-fills', (e) => {
if (e.features.length > 0) {
中身...
}
});
中身を見ていきます。ifは飛ばして続きから見ます。まず、hoveredPolygonId = e.features[0].idでマウスカーソルが乗っかっているPolygonのidをhoveredPolygonIdに格納しています。次にsetFeatureStateでfeature stateの値を設定しています。第1引数は値を設定するfeatureのソースとidを指定します。第2引数は実際の値を指定します。先程レイヤーのところで見たhoverというfeature stateがここで設定されます。
そして飛ばしたifに戻ります。if (hoveredPolygonId !== null)なので、hoveredPolygonIdに値が入っていれば必ず実行されます。先程見た通り、マウスカーソルが一度でもどこかの州に乗っかっていればこの値は設定されています。中身を見るとsetFeatureStateでhoverをfalseにしています。これはある州から別の州にマウスカーソルが移動した際、前の州のホバー効果を消すために必要な処理です。同じ州内であれば一度hoverはfalseになりますが、直後の処理でtrueに戻るのでホバー効果に影響はありません。
if (hoveredPolygonId !== null) {
map.setFeatureState(
{ source: 'states', id: hoveredPolygonId },
{ hover: false }
);
}
hoveredPolygonId = e.features[0].id;
map.setFeatureState(
{ source: 'states', id: hoveredPolygonId },
{ hover: true }
);
mousemove
2つ目がmouseleaveイベントです。第2引数に指定されたレイヤーからマウスカーソルが出たタイミングで発火します。ここでは最後にホバー効果を得ていたPolygonを元に戻しています。
map.on('mouseleave', 'state-fills', () => {
if (hoveredPolygonId !== null) {
map.setFeatureState(
{ source: 'states', id: hoveredPolygonId },
{ hover: false }
);
}
hoveredPolygonId = null;
});
まとめ
マウスの移動に応じてホバー効果を実現する方法について確認しました。ご覧の通り、一般的なGUIプログラミングのようにオブジェクトのプロパティを直接変えることはできません。レイヤーとしてホバー状態を定義しておき、実際にホバー状態にしたいfeatureのプロパティやfeature stateを変更するという2段構えで効果を実現します。慣れないうちはわかりにくいですが、スタイルの書き方に習熟してくると違和感無くなってくるので大丈夫です。
Discussion