23
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-13

概要

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

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

完成イメージ

See the Pen firework1 by Yusuke Mori (@mrysk) on CodePen.

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

参考にした元記事(フーノページ):

svgで線を引きたい

そもそもsvgを使用することが初めてだったので、実際にhtml内でsvgタグを使って線を引くところから始めました。

See the Pen line by Yusuke Mori (@mrysk) on CodePen.

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

花火の元の図形を描きたい

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

See the Pen default hanabi by Yusuke Mori (@mrysk) on CodePen.

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

アニメーションを付けたい

花火らしい振る舞いをさせるために、中心から広がるようなアニメーションをつけました。

See the Pen animation1 by Yusuke Mori (@mrysk) on CodePen.

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

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

See the Pen animation_line by Yusuke Mori (@mrysk) on CodePen.

1段目の形から2段目の形に変化していくアニメーションができました。この右半分が今回使いたい花火らしい動きになっていることが確認できます。
ということはstroke-dashoffsetを使うことできそうですね。stroke-dashoffsetは指定した値だけ、図形をずらして表示するパラメータです。
下記はアニメーションのざっくりとした注釈です。

#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
<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
#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かけて終端をゆうっくり1度だけ実行する*/
	animation: hanabi 2.5s ease-out;
}

@keyframes hanabi {
  /* 60%時点で縦方向に動いていないことをセット */
  60% {
    transform:translateY(0%);
  }
  
  /* 90%時点で図形の不透明度が1であることをセット */
  90% {
    opacity:1;
  }
  
  /* アニメーションの終了時の情報をセット */
  to {
    /* 破線と空白が50%ずつ交互に */
    stroke-dasharray:50%;
    /* 図形を下方向に20%ずらして表示 */
    transform:translateY(20%);
    /* 図形を透明に */
    opacity:0;
  }
}
javascript
// 外枠と中身のSVG要素をidから取得 ..1
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");

// 花火の色を6色用意 ..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の部分です。
    今回はアプリの一部として利用したかったので、無限に動いてほしくなかったためこのような変更を行っています。以下参考記事。

最後に

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

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

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

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

23
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
7