いきなり日曜大工

好きなことを好きなだけやる

Leaflet マーカー編 #2 CSS Sprites + L.DivIcon のマーカー

アイコン作成の進捗

前回の記事を書いた時点では GIMP だけでアイコン画像を作っていたんですが、ある日なぜかパスのアンカーからハンドルが出なくなってしまったため(結構調べましたが未解決)、フリーのドロー系ソフトを探しまして Inkscape を使い始めました。

Inkscape

https://inkscape.org/ja/

BotW のスクリーンショットからアイコンのパスとり中。

恥ずかしながら GIMP 以外にフリーの画像作成ソフトがあるって知りませんでしたもので、ドロー系のフリーソフトあったんですね!

パスの操作が段違いに楽で、GIMP のときと作業スピードが全然違います。操作に慣れてくると前回の記事に当初載せていた GIMP だけで作ったアイコンよりきれいにパスがとれるようになったので Inkscape でパスを全部作り直しました(画像も差し替えました)。

というわけで Inkscape のおかげで当面必要なアイコンは揃いました。

CSS スプライトによる Leaflet 向けアイコン

CSS スプライト

CSS スプライトは web サイトに使われるアイコンなどの小さな画像群を1つの画像にまとめ、そこからスタイルシートで表示したい部分だけをポジションとサイズ指定して切り出し表示させる手法です。

今回は自作の BotW のアイコンからスプライト用の画像を作成し、そこから切り出し表示されるアイコンを使うマーカーを作ります。

画像スプライトを作る

これまでに作ったアイコンをまとめて1枚の画像にします。

1つ 48×48 で、48*9×48*2 ピクセル

見やすさのために背景をつけていますが実際は透過 PNG です。

CSS の定義

スプライト用 CSS の定義は1回書いてしまうと後からいじるのが大変なので、カスタムプロパティを使って簡単にサイズ変更ができるようにしておきます。

アイコンは全て正方形で作っているので、–icon1 から –icon4 の値を変えるだけでそのサイズを一辺とする大きさのアイコンにサイズ変更できます。

.icon-sprite {
  background-image: url(/journals/botw_demo/images/icon_sprite.png);
  background-repeat: no-repeat;
  --icon1: 21px;    /* 城・町・馬宿・店 */
  --icon2: 17px;    /* 集落(カラカラバザールなど) */
  --icon3: 30px;    /* シーカータワー */
  --icon4: 25px;    /* 祠・古代研究所 */
}

.icon-castle, .icon-village, .icon-stable, .icon-shop,
.icon-inn, .icon-armor, .icon-jewelry, .icon-dye {
  background-size: calc(9*var(--icon1)) calc(2*var(--icon1));
  width:  var(--icon1);
  height: var(--icon1);
  margin-top:  calc(-0.5*var(--icon1));
  margin-left: calc(-0.5*var(--icon1));
}
.icon-castle  { background-position: 0 0; }
.icon-village { background-position: calc(-1*var(--icon1)) 0; }
.icon-stable  { background-position: calc(-2*var(--icon1)) 0; }
.icon-shop    { background-position: calc(-4*var(--icon1)) 0; }
.icon-inn     { background-position: calc(-5*var(--icon1)) 0; }
.icon-armor   { background-position: calc(-6*var(--icon1)) 0; }
.icon-jewelry { background-position: calc(-7*var(--icon1)) 0; }
.icon-dye     { background-position: calc(-8*var(--icon1)) 0; }

.icon-settlement {
  background-size: calc(9*var(--icon2)) calc(2*var(--icon2));
  background-position: calc(-3*var(--icon2)) 0;
  width:  var(--icon2);
  height: var(--icon2);
  margin-top:  calc(-0.5*var(--icon2));
  margin-left: calc(-0.5*var(--icon2));
}

.icon-tower {
  background-size: calc(9*var(--icon3)) calc(2*var(--icon3));
  background-position: calc(-1*var(--icon3)) calc(-1*var(--icon3));
  width:  var(--icon3);
  height: var(--icon3);
  margin-top:  calc(-0.5*var(--icon3));
  margin-left: calc(-0.5*var(--icon3));
}

.icon-start, .icon-shrine, .icon-shrine-dlc, .icon-lab {
  background-size: calc(9*var(--icon4)) calc(2*var(--icon4));
  width:  var(--icon4);
  height: var(--icon4);
  margin-top:  calc(-0.5*var(--icon4));
  margin-left: calc(-0.5*var(--icon4));
}
.icon-start      { background-position: 0 calc(-1*var(--icon4)); }
.icon-lab        { background-position: calc(-2*var(--icon4)) calc(-1*var(--icon4)); }
.icon-shrine     { background-position: calc(-3*var(--icon4)) calc(-1*var(--icon4)); }
.icon-shrine-dlc { background-position: calc(-4*var(--icon4)) calc(-1*var(--icon4)); }

このやり方のメリットはアイコンのサイズ変更が簡単にできるので後から微調整がしやすいことなんですが、地図のズームレベルに応じて動的にアイコンサイズを変えたいという場合にも対応できます。

後で地図コンテナにズームレベル変更毎に CSS のクラスがつく(書き換える)機能を実装するんですが、例えば地図コンテナにクラス .zoom-level-n をつけた場合

.zoom-level-5 .icon-sprite {
  --icon1: 21px;
  --icon2: 17px;
  --icon3: 30px;
  --icon4: 25px;
}

これだけでズームレベルごとにアイコンサイズの指定ができます。実際に使うかはまだわかりませんが。

L.Marker + L.DivIcon + CSS Sprites

L.DivIcon + CSS Sprites

CSS スプライトを使った Leaflet のアイコンを L.DivIcon で作ります。

// ハイラル城の場合
let castleIcon = L.divIcon({
    iconSize: null,
    className: 'icon-sprite icon-castle'
});

前回の L.Icon 同様 L.DivIcon にもマーカーの座標に対しての表示位置を調整するオプションがあったりするんですが、今回は動的にサイズ変更できる余地をもたせてアイコンの CSS を定義しているので位置の調整はアイコンの定義と一緒に margin-top と margin-left でアイコンサイズを反映させる形で定義しています。同じような理由で iconSize は null です。

Leaflet のソースを読んでみると L.Icon や L.DivIcon に定義するオプションは HTML になるときにインラインのスタイルシートに直接書かれてしまうので今作っているものと相性が良くなく、極力使わない方向でやっています。

L.Marker + L.DivIcon + CSS Sprites

アイコンができたらマーカーに渡します。

// ハイラル城の場合
let castleIcon = L.divIcon({
    iconSize: null,
    className: 'icon-sprite icon-castle'
});

L.marker([-68.57393, 89.7798663], {
    icon: castleIcon
}).addTo(map);

この調子で1つずつ定義するのはなんなので

spriteIcon = function(icon_type) {
    return L.divIcon({
        iconSize: null,
        className: 'icon-sprite icon-' + icon_type
    });
};

spriteMarker = function(latLng, icon, tooltip_content) {
    return L.marker(latLng, {
        icon: icon
    }).bindTooltip(tooltip_content);
};

const features = {
    "castle": [
        { "name": "ハイラル城",   "coord": [-68.5714221, 89.7825013] }
    ],
    "village": [
        { "name": "カカリコ村",   "coord": [-93.5109145, 121.9702149] },
        { "name": "ハテノ村",     "coord": [-111.2804698, 149.8845814] },
        { "name": "ゾーラの里",   "coord": [-71.8527647, 144.8710029] },
        { "name": "イチカラ村",   "coord": [-52.8896961, 155.6994031] },
        { "name": "ゴロンシティ", "coord": [-39.5698634, 120.081754] },
        { "name": "コログの森",   "coord": [-44.7275658, 100.4361455] },
        { "name": "リトの村",     "coord": [-49.8805212, 37.2175818] },
        { "name": "ゲルドの街",   "coord": [-123.670249, 33.8286072] },
        { "name": "ウオトリー村", "coord": [-132.4648395, 140.7748803] }
    ]
};

Object.keys(features).forEach(function(icon_type) {
    let icon = spriteIcon(icon_type);
    for(let i=0,len=features[icon_type].length; i<len; i++) {
        let feature = features[icon_type][i];
        spriteMarker(feature.coord, icon, feature.name).addTo(map);
    }
});

BotW Demo #5 – L.Marker, L.DivIcon and CSS Sprites

少しゲーム地図っぽくなってきました。今のところアイコン画像の作成よりも地図に載せる情報の座標データ作りとゲームプレイで収集した情報の整理に結構な時間がかかってますが、だいぶまとまってきています。