Day 6: Googleマップのピンを、Googleフォントのアスタリスク(*)で表示する
My Ideal Map App という、Google Maps のユーザー体験を改善する web アプリを作っている。改善しようとしているのは、ユーザーが興味を持った場所を保存し、現在地や検索した場所の周辺に保存した場所を表示して、忘れてた場所を思い出す、というプロセス(詳しくは Day 1 を参照)。
そのプロセスにおいて重要なUI要素が、ユーザーが保存した場所を示す「ピン」。この記事では、私がどのようにして「ピン」の形と色をカスタマイズして、webアプリに埋め込むGoogleマップに表示させたかを記録する。誰かの参考になるかもしれないので。
プログラミングの話とデザインの話が入れ混じっているので、どちらかにのみ興味のある方は、適宜読み飛ばしてください。
模擬データをJSON形式で用意
まだ、データサーバーを作っていないので、ユーザーが保存した場所のリストを模擬データ(mock data)としてファイルに保存する。データ形式は、ウェブアプリ作成でよく使われる JSON。
地理データ用の GeoJSON 形式にするべきなのかもしれないが、模擬データとして使う分には、余計な文字列が増えるので、現時点では保留。データベースをデザインしてデータサーバーを作る時に再考する。
模擬データの内容は、この記事を収録するマガジン『京都で英語版webアプリを個人開発する100日間』にこれまで登場してきた、京都のお気に入りの場所。
それぞれの場所は、オブジェクトとして定義する。例えば、Day 3 に出てきた Lorimer Kyoto は、以下のようになる。
{
"id": 0,
"name": "Lorimer Kyoto",
"latitude": 34.99365616,
"longitude": 135.76098633
},
このようなオブジェクトを並べて、配列 (array) を作り、"places" というプロパティの値とする。そうすることで、`places[i]` とすれば、i番目の場所のデータを読み出すことができる。
{
"places": [
{
"id": 0,
"name": "Lorimer Kyoto",
"latitude": 34.99365616,
"longitude": 135.76098633
},
{
"id": 1,
"name": "Walden Woods Kyoto",
"latitude": 34.99334717,
"longitude": 135.76315308
},
{
"id": 2,
"name": "DD Shokudo Kyoto",
"latitude": 35.00046921,
"longitude": 135.76280212
},
{
"id": 3,
"name": "Okaffe Kyoto",
"latitude": 35.00230408,
"longitude": 135.76216125
},
{
"id": 4,
"name": "Bread & Espresso & Arashiyama Garden",
"latitude": 35.01486588,
"longitude": 135.67489624
},
{
"id": 5,
"name": "Fukuda Art Museum",
"latitude": 35.01386642,
"longitude": 135.67628479
},
{
"id": 6,
"name": "Osen",
"latitude": 35.00626373,
"longitude": 135.77001953
},
{
"id": 7,
"name": "Gion Niti",
"latitude": 35.00185013,
"longitude": 135.77493286
}
]
}
このデータを`mockUserData.json` として保存し、以下のJavaScriptコードで読み込む。
import userData from 'mockUserData.json';
これで、`userData.places[0]` と指定すれば一番目のデータを、`userData.places[1]` とすれば二番目のデータを、読み出すことができる(JavaScript の配列は、0から数え始める。参考:MDN Web Docs)。
Googleマップに複数のピンを表示させる
模擬データで指定した8つの場所にピンを表示させるために、Google Maps Platform の公式ドキュメントの "Complext Marker Icons" というページに載っているコードを応用した。
まず、8つのデータそれぞれについて同様の処理をするループを作る。
for (let i = 0; i < userData.places.length; i++) {
const userPlace = userData.places[i];
}
場所データは、毎回`userPlace`として保存される。こうすることで、例えば一番最初のデータは、
userPlace = {
id: 1,
name: "Lorimer Kyoto",
latitude: 34.99365616,
longitude: 135.76098633
};
と指定するのと同じになる。
次に、ループ内の処理として、Google Maps JavaScript API が提供する `google.maps.Marker()` という関数を実行してピンを表示させる。
for (let i = 0; i < userData.places.length; i++) {
const userPlace = userData.places[i];
// 追加
new google.maps.Marker({
map: map,
position: {
lat: userPlace.latitude,
lng: userPlace.longitude,
},
title: userPlace.name,
});
}
一つ目の引数である `map` については後で説明することにして、二つ目の引数 `position` は、ピンを表示させる緯度・経度を指定するので、`userPlace.latitude` 及び `userPlace.longitude` とする。
最後の引数 `title` は、ユーザーがカーソルをピンの上に移動させた時に表示される文字列(いわゆる tooltip)を指定する。また、弱視の人がスクリーン・リーダーを利用する時に聴くことができる "accessible names" の指定にもなる。これは、ユーザーが保存した場所の名前である `userPlace.name` とする。
最初の引数 `map` は、ピンを表示させる地図を指定する。前回の記事で紹介した通り、以下のコードが地図をウェブページに埋め込んでくれるのだが、この時に指定した `map` という変数を使う。
map = new google.maps.Map(googlemap.current, {
center: {lat: 35.011636, lng: 135.768029}, // Kyoto City Hall
zoom: 17,
mapId: nightMode ? mapIdNighttime : mapIdDaytime,
});
以上のコードを実行すると、Google Maps の象徴である赤いピンが以下のように表示される。
Googleマップの赤いピン(著者によるスクリーンショット)
もちろん、ピンの形も色もカスタマイズする。まずは形から。
ピンの形をアスタリスク(*)にする
ピンの形を、地図アプリでよく目にするアイコンにはしたくない。なぜなら、デザイン・コンセプトである "Dye Me In Your Hue From The Sky" (訳すと「空からあなた色に私を染めて」。詳しくは Day 2 を参照)にふさわしくないから。普通のアイコンでは、気になる場所を保存していくことでGoogleマップをユーザー色に染める(つまり、個人的な地図にする)、というアプリのコア機能のニュアンスが伝わらない。
そこで、My Ideal Map App は「*」(アスタリスク)をピンとして利用する。もちろん、普通のアスタリスクでは「ユーザー色」にならないので、Cormorant という Google Fonts で無料提供されているフォントの Bold ウェイトに搭載されている、独特なデザインのアスタリスクを用いる。
Cormorant Bold に搭載されているアスタリスク(画像元:Google Fonts)
アスタリスクは、地図上のピンとして、意外と理にかなっている。通常は、文章中で脚注があることを示すために使われるが、My Ideal Map App では、保存した場所にメモを付け加えることができるので、「この場所にはメモがありますよ」という印としてふさわしい。
また、アスタリスクは強調にも使われる。データ・サイエンスにおいては、統計学的に有意である値にアスタリスクをつける。特に回帰分析の結果を示した表において、アスタリスクがついていれば、その値がゼロである確率は限りなく小さい、という意味になる。このようなアスタリスクの含意は、地図上のユーザーが関心を持つ場所を示すのにふさわしい。
しかし、通常のよく見るアスタリスクでは「ユーザー色」にならない。そこで、ヨーロッパの書体の歴史を辿ってみると、産業革命以前に金属活字として作られた書体では、アスタリスクなどの記号に独特のデザインがなされていることが多い。Cormorant という書体は、16世紀の書体である Garamond を参考にして作られているので、記号のデザインに遊び心がある。これを利用しない手はない。
Cormorant にはウェイトが5種類あり、それぞれを地図上に示して見えやすさを検討した結果、一番太いウェイトである Cormorant Bold のアスタリスクを採用することにした。
書体の文字を SVG に変換する
では、書体の文字を Google マップのピンとして表示させるにはどうすればいいか。色々調べた結果、文字の形を、google-font-to-svg-path というオープンソースのwebアプリを使って、SVG形式の画像データに変換するという方法を見つけた。
このwebアプリ、Googleフォントで無料提供されている書体をドロップダウンメニューで選んで、変換したい文字・記号を入力すると、すぐにSVG形式の画像に変換してくれる。
google-font-to-svg-path のユーザーインターフェース(筆者によるスクリーンショット)
SVG画像は、テキストデータなので、そのコードも表示される。このコードのうち、Googleマップのピンとして使うのに必要なのは、`<path>`要素の`d`属性の値。この長い文字列が、画像の輪郭線を指定する(詳しくは MDN Web Docs を参照)。
もう一つ必要なのが、`<svg>`要素の`viewBox`属性の値のうち、三つ目と四つ目の数字。この二つの数字が、四角い画像データの右下の角の座標を指定する。この座標を基準にして、画像の中のどの点が地図上の緯度・経度で指定した場所に対応するかを決めることができる。
ひとまず、以上三つの値を変数として保存する。
const asterisk = {
width: 37.788,
height: 38.136,
path:
'M 37.598 21.2 L 20.098 18.5 A 0.223 0.223 0 0 1 19.953 18.442 Q 19.872 18.374 19.801 18.226 A 1.561 1.561 0 0 1 19.748 18.1 A 0.969 0.969 0 0 1 19.72 18.014 Q 19.643 17.729 19.837 17.626 A 0.396 0.396 0 0 1 19.898 17.6 L 31.198 11.1 A 0.656 0.656 0 0 1 31.467 11.044 Q 32.344 11.044 33.848 13.15 Q 35.598 15.6 36.898 18.45 A 31.297 31.297 0 0 1 37.195 19.119 Q 38.087 21.203 37.637 21.203 A 0.235 0.235 0 0 1 37.598 21.2 Z M 15.498 17.8 L 2.398 16.3 Q 1.598 16.122 1.509 13.888 A 14.869 14.869 0 0 1 1.498 13.3 A 27.715 27.715 0 0 1 1.704 10.008 A 35.748 35.748 0 0 1 2.148 7.25 Q 2.776 4.109 3.264 4.471 A 0.303 0.303 0 0 1 3.298 4.5 L 15.798 16.9 Q 15.998 17.1 15.848 17.5 Q 15.707 17.876 15.522 17.81 A 0.212 0.212 0 0 1 15.498 17.8 Z M 0.098 27.8 L 15.698 19.8 L 15.898 19.7 A 0.71 0.71 0 0 1 16.312 19.835 A 1.012 1.012 0 0 1 16.448 19.95 Q 16.698 20.2 16.598 20.4 L 11.098 32.3 A 0.333 0.333 0 0 1 10.939 32.483 Q 10.796 32.565 10.537 32.59 A 2.552 2.552 0 0 1 10.298 32.6 A 4.849 4.849 0 0 1 9.486 32.522 Q 8.662 32.38 7.508 31.982 A 27.544 27.544 0 0 1 6.348 31.55 A 34.706 34.706 0 0 1 3.629 30.342 A 26.81 26.81 0 0 1 1.648 29.25 Q -0.327 28.046 0.065 27.816 A 0.247 0.247 0 0 1 0.098 27.8 Z M 17.698 16 L 15.098 3.2 A 0.591 0.591 0 0 1 15.088 3.094 Q 15.088 2.049 18.848 1.05 A 39.76 39.76 0 0 1 21.416 0.452 Q 22.679 0.205 23.782 0.093 A 17.021 17.021 0 0 1 25.498 0 Q 26.687 0 26.6 0.392 A 0.286 0.286 0 0 1 26.598 0.4 L 18.698 16 Q 18.609 16.266 18.245 16.296 A 1.182 1.182 0 0 1 18.148 16.3 Q 17.715 16.3 17.699 16.022 A 0.379 0.379 0 0 1 17.698 16 Z M 21.198 38 L 18.298 20.8 L 18.298 20.7 A 0.26 0.26 0 0 1 18.338 20.568 Q 18.425 20.421 18.698 20.25 Q 19.098 20 19.198 20.1 L 28.798 29 A 0.559 0.559 0 0 1 28.987 29.367 Q 29.078 30.056 28.023 31.593 A 17.385 17.385 0 0 1 27.698 32.05 Q 25.898 34.5 23.598 36.55 A 29.312 29.312 0 0 1 23.025 37.048 Q 21.492 38.345 21.242 38.096 A 0.171 0.171 0 0 1 21.198 38 Z',
};
SVGデータをGoogleマップのピンにする
ここまで来たら、あとは、Google Maps Platform の公式ドキュメントに書いてあるコードを応用すればいい。
Googleマップのピンの形状を指定する引数 `icon` を以下のように記述する。
icon: {
anchor: new google.maps.Point(
asterisk.width / 2,
asterisk.height / 2
),
path: asterisk.path,
},
`path` は、ピンとして利用する図形の輪郭線を指定する。なので、`asterisk.path` とする。
`anchor` は、地図のズームレベルを変えた時に、ピン画像のどの位置が地図に固定されるかを指定する。デフォルト設定は、画像の左上の角なので、アスタリスクのような点対称のアイコンだと、ズームするたびに、アイコンの中心に対応する地図上の地点が変わってしまい、ピンが移動しているように見えてしまう。そこで、ピン画像の中心が固定されるように、中心点を表す座標
(asterisk.width / 2, asterisk.height / 2)
を指定する。ただ、このような書式は JavaScript にはないので、GoogleマップAPIが提供する `google.maps.Point()` という関数を用いて、コンピューターに座標として認識してもらう。
で、この `icon` を `google.maps.Marker()` の引数として追加指定する。
new google.maps.Marker({
// 追加
icon: {
anchor: new google.maps.Point(
asterisk.width / 2,
asterisk.height / 2
),
path: asterisk.path,
},
map: map,
position: {
lat: userPlace.latitude,
lng: userPlace.longitude
},
title: userPlace.name
});
このコードを実行すると、地図上にピンが以下のように表示される。
カスタム設定したGoogleマップのピン(筆者によるスクリーンショット)
これでは見にくい。デフォルト設定では、SVG画像を利用したピンは、輪郭線が黒、塗りつぶしの色は透明になるため。
というわけで、最後に、ピンの色を指定する。
ピンの色をカスタム設定する
まず、デザインの観点から色の選び方について書いて、その後に、色を指定するプログラミングの方法を書きます。
塗りつぶしの色 (fill color)
My Ideal Map App では、ユーザーが保存した場所を示すピンの色を、デフォルト設定では、黄色とすることにした(後日、他の色に変更できるように仕様を改善する予定)。
黄色は、最も目立つ純色である(なお、このことは科学的に説明できる。Blauch (2014) を参照)。黄色に「警告」「注意」という含意があるのは、これが理由だと思っている。黄色は、人間が嫌でも注目してしまう色であり、その結果として、身の回りの状況に注意を払うきっかけをもたらす色である。だから、信号や、サッカーのイエローカードなどに使われる。
My Ideal Map App の核となる機能は、ユーザーに自分が過去に興味を持って保存した場所に注目させること。その役割を果たすために黄色ほどふさわしい色はない。
* * *
しかし、「黄色」といっても画面上の色として指定するには何通りもある。どの「黄色」を選ぶべきか。グラフィック面で困ったときは、ムードボードに立ち返る(Day 3 参照)。
ライトモード用のムードボードの中にある黄色は、20世紀前半の抽象画家モンドリアンの作品だった。
モンドリアンの1913年の作品「Tableau No. 2/Composition No. VII」 (画像元: Gugghenheim Museum)
誰のものでもない Google マップを、自分が興味のある場所を保存することで「自分色」に染めていく、という My Ideal Map App のデザイン・コンセプト(詳しくは Day 2 を参照)を、この作品はうまく表現している。黒の線と灰色の部分は My Ideal Map App における Googleマップの配色、黄色の部分はユーザーが保存する場所のピンの色に対応する。
UIデザインアプリの Sketch を利用して、この作品の黄色い部分のHSL色空間におけるカラーコードを調べてみると、色相の値は、38度から51度(オレンジっぽい黄色)。それぞれ試した結果、色相51度が地図の配色に最もしっくりきた。
* * *
誰のものでもないので、Googleマップの配色には、無彩色か彩度の低い色を使ってきた(Day 4 および Day 5 を参照)。
したがって「自分色」であるピンは、対照的に彩度が高い色を使うべきということになる。
* * *
しかし、この作品で使われている黄色のトーンだと、彩度が低すぎて、グレーの地図の中で目立たない。そこで彩度を、純色の割合が90%になるまで高める。カラーコーディネーター試験で出てくる「対照トーン配色」というやつだ。
PCCS トーンマップで示した「対照トーン配色」(画像元:街の外壁塗装やさん)
純色の割合を90%にしたのは、実際に試した結果、これより高いと彩度の低い地図の配色との調和が崩れ、これより低いと目立たなくなるから。
明度は、明るめにする。ライトモード用の地図の配色は、白や明るめのグレーが主体なので、同様に白が混ざる割合が高めの色を選ぶことで、調和を取るため。
自作の無料webアプリ、Triangulum Color Picker を用いて、以上の条件を満たす色を探した結果、#f6d410 にたどり着いた。
Triangulum Color Picker で #f6d410 を選んだ時の UI 画面(筆者によるスクリーンショット)
輪郭線の色 (stroke color)
しかし、#f6d410 という色は、背景となる地図の色との明度差が小さく、ピンをユーザーにはっきりと認識してもらうことができない。かといって、明度を下げてしまうと、今度は目立たなくなってしまう。
Google マップアプリはこの問題をどう解決しているのだろう、と思って、調べてみると、ピンの輪郭線の色が、塗りつぶしの色よりも若干暗めに設定されていた。毎日使っているにも関わらず、今まで気づかなかった。。。
レストランの場所を示すピンを例にとってみる。
Google マップアプリのレストランを示すピン(筆者によるスクリーンショット)
塗りつぶしに使われているオレンジ色は #f09824。輪郭線のオレンジ色は、#e88522。明度差を contrast-ratio.com で調べてみると、若干輪郭線の方が暗い。
#f09824 と #e88522 の明度差(画像元:contrast-ratio.com)
塗りつぶしの色と色相はほぼ同じで明度が低い色を輪郭線にすることで、背景となる地図との明度差を大きくし、 ピンの認識を容易にしている。
これに倣って、My Idela Map App におけるピンの輪郭線の色を以下のように選んだ。
・色相は同じで51度。
・明度は、背景となる地図との明度比が 3:1 になるようにする。道路が #ffffff で、市街地の色が #898989 なので(Day 4 参照)、どちらとも明度比が 3:1 以上になるためには、黒との明度比が 2:1 になる必要がある(#898989 と黒の明度比が 6:1 なので)。
・彩度は、以上の二つの要件を満たす中で最も高い値にする。
自作の無料webアプリ、Triangulum Color Picker を用いて、以上の要件を満たす色を探すと、`#463d01` となった。
Triangulum Color Picker で #463d01 を選んだ時の UI 画面(筆者によるスクリーンショット)
ダークモード用の色選びについては後述することにして、ここで、以上の色でピンを表示させるためのプログラミングの話に切り替えます。
Googleマップのピンの色を指定するプログラミングコード
ピンの色を指定するのに必要なパラメーターは三つ。
まず、`fillColor`。塗りつぶしの色を指定する。上記の通り、#f6d410 にする。
次に、`fillOpacity`。塗りつぶしの色の不透明度(opacity)を指定する。デフォルトで値が0(つまり透明)に設定されているので、値を1にしないと、`fillColor`をどんな色にしても表示されないままになる。忘れがちなので要注意。
最後に `strokeColor`。輪郭線の色を指定する。上記の通り、#463d01 にする。輪郭線は、デフォルトで不透明度1に設定されているので、`strokeOpacity` を指定する必要はない(半透明にしたければ、0.5などの値を指定する)。
この三つのパラメーターを、ピンに用いるアイコン画像を指定する時に用いた `icon` という引数を値として設定すればいい。先ほどのコードと合わせると以下のようになる。
new google.maps.Marker({
icon: {
anchor: new google.maps.Point(
cormorantBoldAsterisk.width / 2,
cormorantBoldAsterisk.height / 2,
),
fillColor: '#f6d410', // 追加
fillOpacity: 1, // 追加
path: cormorantBoldAsterisk.path,
strokeColor: '#463d01', // 追加
},
map: map,
position: {
lat: userPlace.latitude,
lng: userPlace.longitude,
},
title: userPlace.name,
});
このコードを実行すると、ピンは以下のように表示される。
ダークモード用のピンの色
My Ideal Map App は、午後6時から朝6時までの間にアクセスすると、自動的にダークモードになる(なぜそうしたのかは、Day 5 を参照)。したがって、UI の色を決める際には、常にダークモード用の色も決めないといけない。
ピンの色については、ライトモード用に選んだ黄色を、そのまま色相は同じで、彩度や明度を落とせばいいだろう、と思って試したところ、地図の配色との相性がどうしても合わない。
ヴィジュアル面で困ったときは、ムードボードに立ち返る。ダークモード用のムードボードに使われている黄色を探してみると、以下のアート作品に行き当たった。
マキコ・ナカムラの2014年の作品「Magic Dusts」(画像元: John Martin Gallery)
ロンドンでインテリアデザインを学んでいた時に、立ち寄ったギャラリーで見つけた作品。黒系の顔料で染めた紙に、金箔を故意に雑に貼り付けて、自然にひび割れたり掠れたりするのに任せ、絶妙な模様を生んでいる。
この作品の金色部分の色相は、42度前後。この色相をダークモード用のピンに使うことにする。ライトモードでは51度だったので、より赤みを帯びた黄色。
彩度は、純色の割合を80%に落とす。これより低いと目立たなくなってしまう。他方、ライトモードでは90%だったが、それではダークモードの配色の中では浮いてしまう。ダークモードの場合、彩度をライトモードよりも低くした方がいい、というのはよく言われることである(例えば、Kravets 2021)。
明度は、暗めにする。ダークモードの地図の配色は、暗めの灰色や、純色と暗めの灰色を混ぜた色で構成されているので、それと呼応するように。
自作の無料webアプリ、Triangulum Color Picker を用いて、以上の要件を満たす色を探すと、`#dfa513` となった。
Triangulum Color Picker で #dfa513 を選んだ時の UI 画面(筆者によるスクリーンショット)
* * *
ピンの輪郭線の色は、背景色である地図のダークモード配色との明度比が少なくとも 3:1 を満たすようにしたい。道路の色が、黒との明度比が 5.11:1 であるオレンジ色 #ae6f2f なので(Day 5 参照)、最低でも黒との明度比が 15.33:1 を満たす必要がある。
黄色はもともと明度が高い色相なので、彩度が高いままでも、黒との明度比 15.33:1 を満たすことができるが、そうすると、ピンの塗りつぶしの色との境界が曖昧になり、目がチカチカする。なので、彩度を思い切って下げて、ライトモードの輪郭線の色と同様に、純色の割合が27%前後になるようにした。
自作の無料webアプリ、Triangulum Color Picker を用いて、以上の要件を満たす色を探すと、`#efdba7` となった。
Triangulum Color Picker で #efdba7 を選んだ時の UI 画面(筆者によるスクリーンショット)
* * *
ダークモードで表示された場合に、ピンの色を上記の通りに変更するには、ユーザーのいる場所の時刻が午後6時から午前6時の間であることを示す nightMode という変数が true であるか false であるかをチェックし(詳しくは Day 5 参照)、true であればダークモードの色に設定すればいい。
塗りつぶしの色と輪郭線の色はセットなので、まずは以下のように ternary operator を使って、`yellow` という変数をオブジェクトとして定義する。
const yellow = nightMode
? {
fillColor: '#dfa513',
strokeColor: '#efdba7',
}
: {
fillColor: '#f6d410',
strokeColor: '#463c01',
};
そして、spread operator (...) を用いて、ピンのアイコンの色を設定する。
new google.maps.Marker({
icon: {
...yellow, // 変更
anchor: new google.maps.Point(
cormorantBoldAsterisk.width / 2,
cormorantBoldAsterisk.height / 2,
),
fillOpacity: 1,
path: cormorantBoldAsterisk.path,
},
map: map,
position: {
lat: userPlace.latitude,
lng: userPlace.longitude,
},
title: userPlace.name,
});
`fillOpacity: 1` を消してしまわないように。消してしまうと、ピンの色が透明になってパニクる(経験談)。
このコードを実行すると、ピンが以下のように表示される。
デモ
以上、この記事で説明したコードのデモはこちら(Cloudflare Pages を利用):https://363d7dac.mima.pages.dev/
午後6時から午前6時の間にアクセスした場合は、ダークモードになっているはずです。もしそうでなかったら、コメント欄でバグレポートをお願いします。
次のステップ
次回は、現在位置表示、場所の検索、または、タグ別に保存した場所を表示する、そのいずれかについて書く予定です。それぞれに Git で feature branch を作って、同時進行で作業しています。どれかにつまづいたら、保留して他の作業をすることで、つまづいた原因に気づいたりできるので。
注: この記事は、英語で書いた同内容の記事を和訳すると同時に日本人向けに編集したものです。
引用文献
Blauch, David N. (2014) “Color”, Virtual Chemistry Experiments (site discontinued; archived on February 16, 2016)
Kravets, Una (2021) "The new responsive: Web design in a component-driven world", web.dev, May 19, 2021.