JavaScriptで花火を作ってみよう！

## 花火を作ってみよう
ブラウザでお手軽に花火を作っちゃいましょう！
javaScript（[p5.js](https://p5js.org/)）だけで作れますので参考にしてみてください。

### 読者対象
- javascriptを扱える方
    - 配列がわかる
    - 関数がわかる
    - クラスがわかる
    - インスタンスがわかる
- processing（p5js）の基本がわかる方
    - setupの動きがわかる
    - drawの動きがわかる

## 完成イメージ
![fw5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126147/459b0ea4-268b-0823-9945-39f3f38ceb4d.gif)

## 手順
### まずは円を描きましょう
花火の元となる円を描きます。
この円をたくさん動かして花火を描きます。
<p class="codepen" data-height="600" data-theme-id="light" data-default-tab="js,result" data-user="I-NO" data-slug-hash="OJMYXVa" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="FW01">
  <span>See the Pen <a href="https://codepen.io/I-NO/pen/OJMYXVa">
  FW01</a> by Naoki Inomata (<a href="https://codepen.io/I-NO">@I-NO</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### 花火をたくさん打ち上げたいので、クラス化します
クラス化することでたくさんの円を一度に動かすことができます。
この後、玉の分だけクラスを生成して花火を描画します。
※動きはまだ変わりません。
<p class="codepen" data-height="600" data-theme-id="light" data-default-tab="js,result" data-user="I-NO" data-slug-hash="GRoaqqX" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="FW02">
  <span>See the Pen <a href="https://codepen.io/I-NO/pen/GRoaqqX">
  FW02</a> by Naoki Inomata (<a href="https://codepen.io/I-NO">@I-NO</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

### 火玉を打ち上がるようにアニメーションさせます
p5.jsの場合は画面の上部のY座標が0で下に行くほど値が高くなるので、ウインドウの高さ(480)から減算することで上昇を表現できます。
<p class="codepen" data-height="600" data-default-tab="js,result" data-slug-hash="MWmrgMY" data-user="I-NO" 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/I-NO/pen/MWmrgMY">
  FW03</a> by Naoki Inomata (<a href="https://codepen.io/I-NO">@I-NO</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

このプログラムの場合は打ち上がる高さのY座標を決めておいて、その高さまで徐々に減算するようにしています。

<p class="codepen" data-height="600" data-theme-id="light" data-default-tab="js,result" data-user="I-NO" data-slug-hash="vYLwKma" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="FW04">
  <span>See the Pen <a href="https://codepen.io/I-NO/pen/vYLwKma">
  FW04</a> by Naoki Inomata (<a href="https://codepen.io/I-NO">@I-NO</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

（以降の手順作成中…）

## 完成品
HTMLファイルを作って以下をコピーしてブラウザで開けば動きます。

```firework.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>p5* firework</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>

<body>
  <canvas>not work...</canvas>
</body>

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
<script>
  const Y_AXIS = 1;
  const X_AXIS = 2;
  let canvas;
  let fireworks = [];
  let star = [];

  function windowResized() {
    resizeCanvas(document.documentElement.clientWidth, document.documentElement.clientHeight);
    this.preStar();
  }

  function setup() {
    // キャンバスの設定
    canvas = createCanvas(document.documentElement.clientWidth, document.documentElement.clientHeight);
    canvas.position(0, 0);
    canvas.style("z-index", "-1");
    colorMode(RGB);
    frameRate(60);
    this.preStar();
  }

  function draw() {
    // 背景色を設定
    setGradient(0, 0, width, height, color(0, 0, 0), color(24, 32, 72), Y_AXIS);
    noStroke();

    // 星を描く
    this.drawStar();

    // 花火を打ち上げる間隔を調整
    if (0 === frameCount % 100) {
      // 打ち上がるスピード
      let speed = random(10, 30);
      fireworks.push(new FireWork(random(width), height, 0, speed, 0.98));
    }

    for (let fw of fireworks) {
      // 打ち切った花火を処理対象から外す（配列から削除する）
      if (2 === fw.getType || 30000 < fw.getFrame) {
        fireworks = fireworks.filter((n) => n !== fw);
        continue;
      }

      // 打ち上げアニメーションを呼び出す
      fw.fire();
    }
  }

  class FireWork {
    // 初期設定
    constructor(x, y, vx, vy, gv) {
      // フレームカウンター
      this.frame = 0;
      this.type = 0;
      this.next = 0;
      // 花火の色
      this.r = random(155) + 80;
      this.g = random(155) + 80;
      this.b = random(155) + 80;
      this.a = 255;

      // 初期位置
      this.x = x;
      this.y = y;

      // 玉の大きさ
      this.w = random(10, 5);

      // 打ち上がる高さ
      this.maxHeight = random(height / 6, height / 2);
      this.fireHeight = height - this.maxHeight;

      // 重力
      this.vx = vx;
      this.vy = vy;
      this.gv = gv;

      // 残像表示用配列
      this.afterImages = [];
      // 爆発用配列
      this.explosions = [];

      // 消えてから爆発までの遅延時間
      this.exDelay = random(10, 40);
      // 爆発の大きさ
      this.large = random(5, 15);
      // 爆発の玉の数
      this.ball = random(20, 100);
      // 爆発から消えるまでの長さ
      this.exend = random(20, 40);
      // 爆発のブレーキ
      this.exStop = 0.96;
    }

    get getFrame() {
      return this.frame;
    }

    get getType() {
      return this.type;
    }

    // 処理コントロール
    fire() {
      // 0:打ち上げ（初期） 1:爆発
      switch (this.type) {
        case 0:
          this.rising();
          break;
        case 1:
          this.explosion();
          break;
      }
    }

    // 打ち上げアニメーション
    rising() {
      // 頂点まで達したら消す
      if (this.y * 0.8 < this.maxHeight) {
        this.a = this.a - 6;
      }

      // 指定の高さまで上昇する
      this.x += this.vx;
      this.y -= this.vy * ((this.fireHeight - (height - this.y)) / this.fireHeight);

      // 残像を表示
      this.afterImages.push(new Afterimage(this.r, this.g, this.b, this.x, this.y, this.w, this.a));
      for (let ai of this.afterImages) {
        if (ai.getAlpha <= 0) {
          this.afterImages = this.afterImages.filter((n) => n !== ai);
          continue;
        }
        ai.rsImage();
      }

      // 打ち上げ表示
      this.update(this.x, this.y, this.w);

      // 全ての表示が消えたら処理の種類を変更する
      if (0 == this.afterImages.length) {
        if (0 === this.next) {
          // 消えてから爆発まで遅延させる
          this.next = this.frame + Math.round(this.exDelay);
        } else if (this.next === this.frame) {
          // 花火の大きさ
          for (let i = 0; i < this.ball; i++) {
            // 爆発の角度
            let r = random(0, 360);
            // 花火の内側を作る（バラバラ）
            let s = random(0.1, 0.9);
            let vx = Math.cos((r * Math.PI) / 180) * s * this.large;
            let vy = Math.sin((r * Math.PI) / 180) * s * this.large;
            this.explosions.push(new FireWork(this.x, this.y, vx, vy, this.exStop));
            // 花火の輪郭を作る（丸くなるようにする）
            let cr = random(0, 360);
            let cs = random(0.9, 1);
            let cvx = Math.cos((cr * Math.PI) / 180) * cs * this.large;
            let cvy = Math.sin((cr * Math.PI) / 180) * cs * this.large;
            this.explosions.push(new FireWork(this.x, this.y, cvx, cvy, this.exStop));
          }
          this.a = 255;
          this.type = 1;
        }
      }
    }

    // 爆発アニメーション
    explosion() {
      for (let ex of this.explosions) {
        ex.frame++;
        // 爆発し終わった花火を配列から除去する
        if (2 === ex.getType) {
          this.explosions = this.explosions.filter((n) => n !== ex);
          continue;
        }

        // 残像を描画
        if (0 === Math.round(random(0, 32))) {
          ex.afterImages.push(new Afterimage(this.r, this.g, this.b, ex.x, ex.y, ex.w, ex.a));
        }

        for (let ai of ex.afterImages) {
          if (ai.getAlpha < 0) {
            ex.afterImages = ex.afterImages.filter((n) => n !== ai);
            continue;
          }
          ai.exImage();
        }

        // 爆発を描画
        this.update(ex.x, ex.y, ex.w, ex.a);
        ex.x += ex.vx;
        ex.y += ex.vy;
        ex.vx = ex.vx * ex.gv;
        ex.vy = ex.vy * ex.gv;
        ex.vy = ex.vy + ex.gv / 30;
        if (this.exend < ex.frame) {
          ex.w -= 0.1;
          ex.a = ex.a - 4;
          if (ex.a < 0 && 0 === ex.afterImages.length) {
            ex.type = 2;
          }
        }
      }
    }

    // 花火を表示する
    update(x, y, w, a) {
      this.frame++;
      if (0 < this.a) {
        let c = color(this.r, this.g, this.b);
        c.setAlpha(a);
        fill(c);
        ellipse(x, y, w, w);
      }
    }
  }

  // 残像処理用クラス
  class Afterimage {
    constructor(r, g, b, x, y, w, a) {
      this.frame = 0;
      this.r = r;
      this.g = g;
      this.b = b;
      this.x = x;
      this.y = y;
      this.w = w;
      this.a = a;
      this.vx = random(-0.24, 0.24);
      this.vy = random(0.2, 0.8);
      this.vw = random(0.05, 0.2);
    }

    get getAlpha() {
      return this.a;
    }

    // 打ち上げ用
    rsImage() {
      if (0 < this.a) {
        this.update(this.r, this.g, this.b, this.x, this.y, this.w, this.a);
        this.r += 4;
        this.g += 4;
        this.b += 4;
        this.x = this.x + this.vx;
        this.y = this.y + this.vy;
        if (0 < this.w) {
          this.w = this.w - this.vw;
        }
        this.a = this.a - 4;
      }
    }

    // 爆発用
    exImage() {
      if (0 < this.a) {
        this.update(this.r, this.g, this.b, this.x, this.y, this.w, this.a);
        this.r += 2.5;
        this.g += 2.5;
        this.b += 2.5;
        this.x = this.x + this.vx;
        this.y = this.y + this.vy;
        if (0 < this.w) {
          this.w = this.w - this.vw;
        }
        this.a = this.a - 1.5;
      }
    }

    update(r, g, b, x, y, w, a) {
      this.frame++;
      let c = color(r, g, b);
      c.setAlpha(a);
      fill(c);
      ellipse(x, y, w, w);
    }
  }

  // グラデーションを描画
  function setGradient(x, y, w, h, c1, c2, axis) {
    noFill();

    if (axis === Y_AXIS) {
      // Top to bottom gradient
      for (let i = y; i <= y + h; i++) {
        let inter = map(i, y, y + h, 0, 1);
        let c = lerpColor(c1, c2, inter);
        stroke(c);
        line(x, i, x + w, i);
      }
    } else if (axis === X_AXIS) {
      // Left to right gradient
      for (let i = x; i <= x + w; i++) {
        let inter = map(i, x, x + w, 0, 1);
        let c = lerpColor(c1, c2, inter);
        stroke(c);
        line(i, y, i, y + h);
      }
    }
  }

  // 星を作成
  function preStar() {
    star = [];
    for (let i = 0; i < 100; i++) {
      star.push([random(width), random(height / 2), random(1, 4)]);
    }
  }

  // 星を描画
  function drawStar() {
    // 星を描く
    for (let s of star) {
      let c = color(random(150, 255), random(150, 255), 255);
      c.setAlpha(random(150, 200));
      fill(c);
      ellipse(s[0], s[1], s[2], s[2]);
    }
  }

</script>

</html>
```

## GitHubリポジトリはこちら
https://github.com/soramoyou04/p5js-animation-shelf