svgとJavascriptでシンプルな花火を打ち上げたい

# 概要
今回、初投稿ということで研修の一環でJavascriptを用いてsvgで描画したイラストを動かして花火をあげてみたので調べた内容をまとめてみました。

自分なりの解釈も混じっていますので、間違っている内容があればご指摘いただければと思います。

# 完成イメージ
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="ExEjzpy" data-user="mrysk" 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/mrysk/pen/ExEjzpy">
  firework1</a> by Yusuke Mori (<a href="https://codepen.io/mrysk">@mrysk</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

　今回は下記の記事を参考にこういったシンプルな形の花火アニメーションを作成しました。
この記事を見れば基本的なことはまとめられていますので、今回の実装で個人的に詰まった点や追加で調べた内容のみ記述していきます。

参考にした元記事（フーノページ）：

https://fuuno.net/ani/ani48/ani48.html

# svgで線を引きたい
そもそもsvgを使用することが初めてだったので、実際にhtml内でsvgタグを使って線を引くところから始めました。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="oNqLZrQ" data-user="mrysk" 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/mrysk/pen/oNqLZrQ">
  line</a> by Yusuke Mori (<a href="https://codepen.io/mrysk">@mrysk</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

ここでは300×300(`width`と`height`)のsvgブロックの中に同じく300×300の`viewBox`（アートボード）を用意し、線の始点と終点をviweBoxの大きさの%で指定しています。
%で指定しているのは細かい座標指定をしなくてもよくするためです。

# 花火の元の図形を描きたい
svgで線を描く方法を確認したので早速花火の元となる図形を描いてみました。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="XWEKRrZ" data-user="mrysk" 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/mrysk/pen/XWEKRrZ">
  default hanabi</a> by Yusuke Mori (<a href="https://codepen.io/mrysk">@mrysk</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

座標指定が大変泥臭いのでエンジニアの皆さんは賢くやってあげてください。
始点をすべて中心（50%,50%）からにしているのは見た目のわかりやすさもありますが、この後のアニメーションをうまく動かすためなので、逆にしないようにしましょう。

# アニメーションを付けたい
花火らしい振る舞いをさせるために、中心から広がるようなアニメーションをつけました。
<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="yLKJXBR" data-user="mrysk" 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/mrysk/pen/yLKJXBR">
  animation1</a> by Yusuke Mori (<a href="https://codepen.io/mrysk">@mrysk</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

cssの内容については下記を記事でアニメーション付きで紹介されています。
参考（フーノページ）：

https://fuuno.net/ani/ani16/ani16.html

ここでは、`stroke-dasharray`の%指定がlineの大きさではなく`viewBox`の大きさが参照されていることを注意すると理解が進みました。

<p class="codepen" data-height="300" data-default-tab="result" data-slug-hash="mdxEwLq" data-user="mrysk" 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/mrysk/pen/mdxEwLq">
  animation_line</a> by Yusuke Mori (<a href="https://codepen.io/mrysk">@mrysk</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


1段目の形から2段目の形に変化していくアニメーションができました。この右半分が今回使いたい花火らしい動きになっていることが確認できます。
ということは`stroke-dashoffset`を使うことできそうですね。`stroke-dashoffset`は指定した値だけ、図形をずらして表示するパラメータです。
下記はアニメーションのざっくりとした注釈です。
```css:
#line3 {
  /*破線の配置指定：破線10%空白40%を交互に表示する*/
  stroke-dasharray:10% 40%;
  /*アニメーションの指定：
    アニメーション「hanabi」を、2.5秒かけて、
    終端をゆっくり、無限回繰り返す*/
  animation: hanabi 2.5s ease-out infinite;
}

@keyframes hanabi {
  /*アニメーションの終端*/
  to {
    /*破線の配置指定：破線と空白を50%ずつ交互に表示する*/
    stroke-dasharray:50%;
  }
}
```

# その後なんやかんやして
前項目まででアニメーションがなんとなく理解できましたので、そこから試行錯誤することで下のコードが完成しました。
技術的なことは元の記事そのままなので詳しく知りたい方はそちらをご覧ください。ここでは変更点だけ確認しておきたいと思います。
```html:html
<svg id = "outer" viewBox="0 0 400 300">
	<svg id = "inner" viewBox="0 0 300 300" x = "-100%">
		<g id="line">
			<line x1="50%" y1="50%" x2="100.0%" y2="50.0%" ></line>
			<line x1="50%" y1="50%" x2="96.98%" y2="67.10%"></line>
			<line x1="50%" y1="50%" x2="88.30%" y2="82.14%"></line>
			<line x1="50%" y1="50%" x2="75.0%"  y2="93.30%"></line>
			<line x1="50%" y1="50%" x2="58.68%" y2="99.24%"></line>
			<line x1="50%" y1="50%" x2="41.32%" y2="99.24%"></line>
			<line x1="50%" y1="50%" x2="25.00%" y2="93.30%"></line>
			<line x1="50%" y1="50%" x2="11.70%" y2="82.14%"></line>
			<line x1="50%" y1="50%" x2="3.015%" y2="67.10%"></line>
			<line x1="50%" y1="50%" x2="0.0%"   y2="50.00%"></line>
			<line x1="50%" y1="50%" x2="3.02%"  y2="32.90%"></line>
			<line x1="50%" y1="50%" x2="11.70%" y2="17.86%"></line>
			<line x1="50%" y1="50%" x2="25.00%" y2="6.70%" ></line>
			<line x1="50%" y1="50%" x2="41.32%" y2="0.76%" ></line>
			<line x1="50%" y1="50%" x2="58.68%" y2="0.76%" ></line>
			<line x1="50%" y1="50%" x2="75.00%" y2="6.70%" ></line>
			<line x1="50%" y1="50%" x2="88.30%" y2="17.86%"></line>
			<line x1="50%" y1="50%" x2="96.98%" y2="32.90%"></line>
		 </g>
	</svg>
</svg>
```

```css:css
#outer{
  /* outerの背景を黒にして見やすく */
  background-color:black;
}

#inner{
  /* 花火がpaddingの外側でも表示 */
  overflow:visible;
}

#line {
  /* 線が細く見づらかったので太くしてみました */
	stroke-width:3px;
  /* 50%ずらして表示 */
	stroke-dashoffset:50%;
  /* 破線10%と空白40%を交互に表示 */
  stroke-dasharray:10% 40%;
  /* アニメーションhanabiを2.5sかけて終端をゆうっくり１度だけ実行する*/
	animation: hanabi 2.5s ease-out;
}

@keyframes hanabi {
  /* 60%時点で縦方向に動いていないことをセット */
  60% {
    transform:translateY(0%);
  }
  
  /* 90%時点で図形の不透明度が１であることをセット */
  90% {
    opacity:1;
  }
  
  /* アニメーションの終了時の情報をセット */
  to {
    /* 破線と空白が50%ずつ交互に */
    stroke-dasharray:50%;
    /* 図形を下方向に20%ずらして表示 */
    transform:translateY(20%);
    /* 図形を透明に */
    opacity:0;
  }
}
```

```javascript:javascript
// 外枠と中身のSVG要素をidから取得　..1
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");

// 花火の色を６色用意　..2
const iro = [ "red", "blue", "yellow", "green", "orange", "purple" ];

// 花火を途中で止めるためにカウンタを用意　..10
let timerId;
let count=0;

// 一連の処理を1つの関数に　..3
function fireworks() {

  // innerを複製　..4
  let clone = inner.cloneNode(true);

  // アニメーション終了時に削除するように設定　..5
  clone.children[0].addEventListener("animationend",function() {
    clone.remove();
  });

  // 各属性値をランダムに生成　..6
  let horizon = String(Math.random()*100 - 25) + "%";
  let vertical = String(Math.random()*100 - 50) + "%";
  let randomcolor = iro[Math.floor(Math.random()*6)];
  let ookisa = String(Math.random()*20 +30) + "%";

  // 属性をセット　..7
  clone.setAttribute("x",horizon);
  clone.setAttribute("y",vertical);
  clone.setAttribute("width","auto");
  clone.setAttribute("height",ookisa);
  clone.children[0].style.stroke = randomcolor;

  // outerに追加　..8
  outer.appendChild(clone);

  // カウンタをインクリメントし、30発上げたら終了 ..11
  count++
  if(count == 30){
    clearInterval(timerId);
  }
}

// 500ミリ秒毎に繰り返す ..9
timerId = setInterval(fireworks,500);
```
- **変更点①：各線の泥臭い数値指定**
これは以前にもお詫びしている点でございます。改めて皆さんにはsvgから読み込むなりきれいに記述していただければと思います。
- **変更点②：cssをすべてまとめた**
元記事に比べるとなんだかcssが多いように感じられますが、cssを記述文字通りまとめているだけですので、なんてことはありません。線を太くしたくらいですね。
- **変更点③：回数指定で止まるように**
javascriptのコメント10,11の部分です。
今回はアプリの一部として利用したかったので、無限に動いてほしくなかったためこのような変更を行っています。以下参考記事。

https://developer.mozilla.org/ja/docs/Web/API/clearInterval

https://www.sejuku.net/blog/24425

# 最後に
今回は初めて触れるsvgとjavascriptを使用して、シンプルな花火を打ち上げる方法に関して個人的に躓いた点などをまとめました。
乱雑に書いてみましたがいかがでしたでしょうか。

また学びをメモとして投稿していこうと思います。
最後まで閲覧いただきありがとうございました。

### その他参考にさせていただいた記事

https://qiita.com/mochizukikotaro/items/625ea40fdc209b9028a0

https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/stroke-dasharray

### 更にすごい花火を打ち上げたい方向けの記事

https://qiita.com/iNaoki04/items/5d420440cf3d89f54f82