MapLibre GL JSで魔法陣を出現させる

:::note info
これは [MIERUNE AdventCalendar 2022](https://qiita.com/advent-calendar/2022/mierune)　18日目の記事です。
昨日は @Kanahiro さんによる [THREE.jsでGPGPUにより頂点を操作しパーティクルをアニメーションする](https://qiita.com/Kanahiro/items/8927619c64831972c1d2) でした。
:::


↓こんな感じにMaplibre GL JSで地図上に魔法陣が出現する演出を作りたいと思います。
<p class="codepen" data-height="400" data-default-tab="result" data-slug-hash="vYaBZey" data-user="satoshi7190" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/satoshi7190/pen/vYaBZey">
   Magic circle</a> by satoshi7190 (<a href="https://codepen.io/satoshi7190">@satoshi7190</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 地図とマーカーを用意する
まずは、地図を生成して中心に[マーカー](https://maplibre.org/maplibre-gl-js-docs/example/add-a-marker/)を設置します。
```javascript:JavaScript
map = new maplibregl.Map({
  container: "map",
  style: {
    version: 8,
    sources: {
      real: {
        type: "raster",
        tiles: [
          "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg"
        ],
        tileSize: 256,
        attribution:
          "地図の出典：<a href='https://www.gsi.go.jp/' target='_blank'>国土地理院</a>"
      }
    },
    layers: [
      {
        id: "real",
        type: "raster",
        source: "real",
        minzoom: 0,
        maxzoom: 24,
        paint: {
          "raster-brightness-max": 1
        }
      }
    ]
  },
  center: [141.361, 43.064],
  zoom: 13,
  pitch: 60
});

map.on("load", () => {
    //マーカー設置
  new maplibregl.Marker()
      .setLngLat([141.361, 43.064])
      .addTo(map);
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/a796bc4d-9696-24fe-9586-da7688a3531e.png)

# 魔法陣を描くのに必要なdiv要素を作る

次に、魔法陣に必要なパーツ（div要素）をJavascriptで生成します。
```javascript:JavaScript
//魔法陣のパーツごとの要素の生成
const customMarker = document.createElement("div");
const space = document.createElement("div");
const magic = document.createElement("div");
const magicFirstcircle = document.createElement("div");
const magicText = document.createElement("div");
const magicSecondcircle = document.createElement("div");
const magicLine = document.createElement("div");
const magicEmblem = document.createElement("div");

//魔法陣に呪文のテキストを書き込む
const magicTextData = [
  "m",
  "a",
  "p",
  "l",
  "i",
  "b",
  "r",
  "e",
  "m",
  "a",
  "p",
  "b",
  "o",
  "x",
  "g",
  "l",
  "j",
  "s"
];
magicTextData.map((text) => {
  const textElement = document.createElement("span");
  textElement.textContent = text;
  magicText.appendChild(textElement);
});

//魔法陣の中のライン生成
for (i = 0; i < 16; i++) {
  const magicLineChild = document.createElement("p");
  magicLine.appendChild(magicLineChild);
}

//クラスを定義
customMarker.className = "marker";
space.className = "space";
magic.className = "magic";
magicFirstcircle.className = "magic-firstcircle";
magicText.className = "magic-text";
magicSecondcircle.className = "magic-secondcircle";
magicLine.className = "magic-line";
magicEmblem.className = "magic-emblem";

//魔法陣のパーツを組み立てる
magic.appendChild(magicFirstcircle);
magic.appendChild(magicText);
magic.appendChild(magicSecondcircle);
magic.appendChild(magicLine);
magic.appendChild(magicEmblem);
space.appendChild(magic);
customMarker.appendChild(space);
```
上記のJavaScriptで生成されたdiv要素の構造が下記の通りになります。

```html:HTML
<div class="space">
    <div class="magic">
        <div class="magic-firstcircle"></div>
        <div class="magic-text">
            <span>m</span><span>a</span><span>p</span><span>l</span><span>i</span><span>b</span><span>r</span><span>e</span><span>m</span><span>a</span><span>p</span><span>b</span><span>o</span
            ><span>x</span><span>g</span><span>l</span><span>j</span><span>s</span>
        </div>
        <div class="magic-secondcircle"></div>
        <div class="magic-line">
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
            <p></p>
        </div>
        <div class="magic-emblem"></div>
    </div>
</div>
```
そして、このjavascriptで生成したdiv要素を格納した変数`customMarker`をマーカーのインスタンス生成時の`new maplibregl.Marker()`の引数にすることで、先ほどのdiv要素をマーカーとして地図の上に表示させることができます。
- [公式ドキュメントの例](https://maplibre.org/maplibre-gl-js-docs/example/custom-marker-icons/)


```diff_javascript
//マーカー設置
- new maplibregl.Marker()
+ new maplibregl.Marker(customMarker)
.setLngLat([141.361, 43.064])
.addTo(map);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/aff560dd-0085-c47b-b054-19bddfc34064.png)

CSSで何もスタイリングしてないので、div要素の中のテキストが表示されただけです。

# 魔法陣をCSSで描く

ここからCSSでスタイリングして魔法陣を描きます。
今回はかなり記述が長くなるのでSassで書きました。


```scss:Scss
/* 魔法陣を形成する空間 */
.space {
  transform: scale(1.5);
}

/* 魔法陣の色 */
$color: #00ffd0;

@mixin size($value) {
  width: $value;
  height: $value;
}

@mixin center {
  position: absolute;
  margin: auto;
  inset: 0;
}

/* 魔法陣本体 */
.magic {
  position: relative;
  @include size(200px);
  /* 魔法陣 外側の円 */
  &-firstcircle {
    @include center;
    @include size(95%);
    border: 2px solid $color {
      radius: 50%;
    }
  }
  /* 魔法陣 文字 全体位置 */
  &-text {
    @include center;
    @include size(98%);
    /* 魔法陣 各文字 */
    span {
      position: absolute;
      top: 50%;
      bottom: 0;
      left: 48%;
      color: $color;
      padding-top: 41%;
      transform-origin: top center;
      /* 魔法陣の各文字を回転させる */
      @for $i from 1 through 17 {
        &:nth-of-type(#{$i * 1}) {
          transform: rotate(20deg * $i);
        }
      }
    }
  }
  /* 魔法陣 内側の円 */
  &-secondcircle {
    @include center;
    @include size(83%);
    border: 2px solid $color {
      radius: 50%;
    }
  }

  /* 魔法陣 空間  */
  &-line {
    @include center;
    @include size(83%);
    /* 魔法陣  */
    p {
      @include center;
      width: 86%;
      height: 50%;
      border: 1px solid $color;
      transform-origin: center;
      /* 魔法陣 各ライン*/
      @for $i from 1 through 16 {
        &:nth-of-type(#{$i * 1}) {
          transform: rotate(20deg * $i);
        }
      }
    }
  }

  /* 魔法陣の紋章 */
  &-emblem {
    @include size(25%);
    @include center;
    padding: auto;
    border: 2px solid $color;
    border-radius: 50%;

    @mixin emblem($value) {
      content: "";
      @include size(100%);
      position: absolute;
      border: 2px solid $color;
      transform: translate3d(-2px, -2px, 0) rotate($value);
    }
    &::before {
      @include emblem(0deg);
    }
    &::after {
      @include emblem(45deg);
    }
  }
}
```


このように書くとマーカーが魔法陣に変身します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/411c60b4-7b5d-e0a4-a897-e2e19275c4ce.png)

魔法陣がそのまま画面に表示されているだけで違和感があり、地図の上に出ているように見えません。
マーカーのオプションに`rotationAlignment: "map"`と`pitchAlignment: "map"`を記述することで、マーカーを地図の平面に合わせことができます。（地図にマーカを直接貼り付けるようなイメージです。）これで魔法陣が地図の上に乗っかります。

```diff_javascript:JavaScript
//マーカー設置
- new maplibregl.Marker(customMarker)
+ new maplibregl.Marker(customMarker, {
+    rotationAlignment: "map",
+    pitchAlignment: "map"
+ })
.setLngLat([141.361, 43.064])
.addTo(map);
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/de4cbb98-47d0-60b7-5fa4-a843a35b1406.png)

# アニメーション効果を加える
あとは、アニメーションの演出をCSSで書き加えます。
先ほどのSassに`@keyframes`のアニメーションを付け足していきます。
```diff_scss:Scss
/* 魔法陣を形成する空間 */
.space {
  transform: scale(1.5);
+ animation: scale 3s;
+ /* 魔法陣の呼び出し */
+ @keyframes scale {
+   0% {
+     filter: brightness(20);
+     transform: rotate(-720deg) scale(0);
+   }
+   50% {
+     filter: brightness(20);
+     transform: rotate(0deg) scale(1.5);
+   }
+   100% {
+     filter: brightness(1);
+     transform: rotate(0deg) scale(1.5);
+   }
+ }
}

/* 魔法陣の色 */
$color: #00ffd0;

@mixin size($value) {
  width: $value;
  height: $value;
}

@mixin center {
  position: absolute;
  margin: auto;
  inset: 0;
}

/* 魔法陣本体 */
.magic {
  position: relative;
  @include size(200px);
+ animation: rotation 5s linear infinite;
+ /* 魔法陣の回転 */
+ @keyframes rotation {
+   100% {
+     transform: rotate(360deg);
+   }
+ }
  /* 魔法陣 外側の円 */
  &-firstcircle {
    @include center;
    @include size(95%);
    border: 2px solid $color {
      radius: 50%;
    }
+   animation: energy 2s linear alternate infinite;
  }
  /* 魔法陣 文字 全体位置 */
  &-text {
    @include center;
    @include size(98%);
    /* 魔法陣 各文字 */
    span {
      position: absolute;
      top: 50%;
      bottom: 0;
      left: 48%;
      color: $color;
      padding-top: 41%;
      transform-origin: top center;
      /* 魔法陣の各文字を回転させる */
      @for $i from 1 through 17 {
        &:nth-of-type(#{$i * 1}) {
          transform: rotate(20deg * $i);
        }
      }
    }
  }
  /* 魔法陣 内側の円 */
  &-secondcircle {
    @include center;
    @include size(83%);
    border: 2px solid $color {
      radius: 50%;
    }
+   animation: energy 2s linear alternate infinite;
+   /* 魔法陣のグロー効果 */
+   @keyframes energy {
+     100% {
+       box-shadow: $color 0px 0px 20px;
+     }
+   }
  }

  /* 魔法陣 空間 */
  &-line {
    @include center;
    @include size(83%);
    /* 魔法陣 */
    p {
      @include center;
      width: 86%;
      height: 50%;
      border: 1px solid $color;
      transform-origin: center;
      /* 魔法陣 六芒星 各ライン*/
      @for $i from 1 through 16 {
        &:nth-of-type(#{$i * 1}) {
          transform: rotate(20deg * $i);
        }
      }
+     animation: energy 2s linear alternate infinite, drawline 2s;
+     @keyframes drawline {
+       0% {
+         width: 20%;
+         opacity: 0;
+       }
+       50% {
+         width: 20%;
+         opacity: 0;
+       }
+       100% {
+         width: 86%;
+       }
+     }
    }
  }

  /* 魔法陣の紋章 */
  &-emblem {
    @include size(25%);
    @include center;
    padding: auto;
    border: 2px solid $color;
    border-radius: 50%;
+   animation: flashing 2s linear alternate infinite, fadein 2s;

    @mixin emblem($value) {
      content: "";
      @include size(100%);
      position: absolute;
      border: 2px solid $color;
      transform: translate3d(-2px, -2px, 0) rotate($value);
+     animation: flashing 2s linear alternate infinite;
    }
    &::before {
      @include emblem(0deg);
    }
    &::after {
      @include emblem(45deg);
    }
+   /* 紋章の出現 */
+   @keyframes fadein {
+     0% {
+       opacity: 0;
+     }
+     50% {
+       opacity: 0;
+     }
+     100% {
+       opacity: 1;
+     }
+   }

+   /* 紋章のグロー効果 */
+   @keyframes flashing {
+     0% {
+       filter: drop-shadow(0px 0px 0px $color);
+     }
+     100% {
+       filter: drop-shadow(0px 0px 12px $color);
+     }
+   }
  }
}
```
そして、魔法陣が出現する時に背景の地図が暗くなる演出をJavaScriptのほうに加えます。（個人的な好みで魔法陣を出すタイミングと地図を暗くする演出を`setTimeout`で0.5秒遅らせてます。）
```diff_javascript:JavaScript
map.on("load", () => {
+ setTimeout(() => {
+   map.setPaintProperty("real", "raster-brightness-max", 0.4);
    //マーカー設置
    new maplibregl.Marker(customMarker, {
      rotationAlignment: "map",
      pitchAlignment: "map"
    })
      .setLngLat([141.361, 43.064])
      .addTo(map);
+ }, 500);
});
```
地図上に魔法陣が出現する演出が出来上がりました。
![mahouzin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1575765/816fe711-f28c-087b-d2cd-479bd4604156.gif)

魔法陣が出現する時の表現は`transform:`の`rotate`で回転させながら`scale`で全体を膨張させ、`filter:`の`brightness`で魔法陣の色を一瞬明るくすることで表現しています。
魔法陣の中の複数のラインは、少しタイミングをずらしてから円の中心から円の端に広がるように動かしてます。ちなみにこのラインは複数のアウトラインの長方形を少しずつ回転させながら重ね合わせてるだけです。
その後のグロー効果は`box-shadow`や`filter:`の`drop-shadow`の値をアニメーションで変動させることで表現しています。

# 参考記事
- [HTMLとCSSで魔法陣を描いてみる](https://qiita.com/yomogenium/items/789f9d35cefa54e97b31)






:::note info
明日は@northprintさんにです！お楽しみにー
:::