【TypeScript】花火

# はじめに
きっかけは前回記事[【HTML&CSS】花火](https://qiita.com/nina_s/items/e124f356ee247178ffc2)の通り、HTML5プロフェッショナル認定試験の勉強をしている中で、ふと、花火が作れそうだなと思ったことですが、色々探しているうちにすごいキレイな花火がTypeScriptで実装されているのを見つけたので、アレンジさせて頂くことにしました。

- 参考サイト [HTML5のCanvasでのグロー表現を使って花火っぽい何かを打ち上げる](https://funnydreamer.com/blog/2021/03/01/html5%E3%81%AEcanvas%E3%81%A7%E3%81%AE%E3%82%B0%E3%83%AD%E3%83%BC%E8%A1%A8%E7%8F%BE%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%8A%B1%E7%81%AB%E3%81%A3%E3%81%BD%E3%81%84%E4%BD%95%E3%81%8B%E3%82%92%E6%89%93/)

# アレンジした花火
こんな感じにしてみました！
千輪花火を追加するのが難しかったですが、キレイに描画できたかなと思います。

　① 花火の色がランダムに変わるように変更
　② 花火が開いた後に千輪花火を追加

<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="qBLLzqo" data-user="ifnezsju-the-selector" style="height: 500px; 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/ifnezsju-the-selector/pen/qBLLzqo">
  Firework_ts2</a> by Nina (<a href="https://codepen.io/ifnezsju-the-selector">@ifnezsju-the-selector</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# メソッド・クラス
コード内の主要なメソッドとクラスを説明します。

1. `Firework` クラス:
   - `fireCount`: 花火の粒子の総数。
   - `firePropsLength`: 各花火のプロパティの合計数。
   - `fireArray`: 花火のプロパティを格納するためのFloat32Array。
   - `baseTTL`, `rangeTTL`, `maxTTL`: 花火の寿命（時間）に関するプロパティ。
   - `baseSpeed`, `rangeSpeed`: 花火の速度に関するプロパティ。
   - `baseRadius`, `rangeRadius`: 花火の半径に関するプロパティ。
   - `baseHue`: 花火の色相に関するプロパティ。
   - `bgColor`: 背景の色。
   - `bgOverwriteColor`: 背景の色を上書きする色。
   - `fillStyle`: 描画に使用される塗りつぶしのスタイル。
   - `canvas`: Canvas要素のコンテナ。
   - `ctx`: Canvasのコンテキスト。
   - `launch`: 花火の発射ポイントに関するプロパティ。
   - `noise`: `simplenoise` ライブラリへの参照。

2. `created` メソッド:
   - ページの読み込み時に呼び出され、Canvasを作成し、リサイズ処理をセットアップし、花火の初期化を行います。

3. `createCanvas` メソッド:
   - キャンバス要素を作成し、2つのコンテキスト（aとb）を取得します。aコンテキストはアニメーションの描画用、bコンテキストは効果（ぼかしや明るさなど）のために使用されます。

4. `resize` メソッド:
   - ウィンドウのリサイズイベント時に呼び出され、Canvasのサイズを調整し、中央の位置を更新します。

5. `getParticleIndex` メソッド:
   - 花火のプロパティのインデックスを取得します。

6. `getInitTTL`, `getInitSpeed`, `getInitRadius` メソッド:
   - 花火のプロパティ（寿命、速度、半径）の初期値を取得します。

7. `rand`, `randIn` メソッド:
   - ランダムな数値を生成します。

8. `fadeInOut` メソッド:
   - 指定された時間と周期に対するフェードイン/フェードアウトの計算を行います。

9. `initLaunch` メソッド:
   - 花火の発射ポイントの初期化を行います。

10. `initThousandPetals`, `initParticles`, `initParticle` メソッド:
    - 花火粒子の初期化を行います。

11. `drawParticle` メソッド:
    - 花火粒子を描画します。

12. `updateThousandPetals`, `updateParticle` メソッド:
    - 花火粒子の更新を行います。

13. `draweThousandPetals`, `drawParticles`, `drawLaunch` メソッド:
    - 花火アニメーションの描画を制御します。

14. `renderGlow`, `renderToScreen` メソッド:
    - グロー効果を描画し、最終結果をCanvasにレンダリングします。

15. `draw` メソッド:
    - アニメーションを描画するためのメインループです。特定の条件に従って、花火を発射し、千輪花火を描画します。

最後に、`Firework` クラスをインスタンス化し、`created` メソッドを呼び出してアニメーションを開始します。

# アレンジ説明

### ① 花火の色がランダムに変わるように変更
参考サイトでは`hue: this.baseHue + 80`と固定になっていたので、ランダムになるように変更しました。

```ts
    private initLaunch = (): void => {
        this.launchX = this.centerX + (Math.random() - 0.5) * this.canvas.a.width * 0.8;
        // ★①★ 色をランダムに
        this.launch = {
            x: this.launchX,
            y: this.canvas.a.height,
            hue: Math.random() * 360,
            vx: 0,
            vy: -1 * (Math.random() * 2 + 4),
        };
    }
```

### ② 花火が開いた後に千輪の花火を追加
ポイントとなる追加箇所のみ下記に説明します。

- 千輪花火の準備
変数`maxX`と`isOpen`を追加していますが、`maxX`は開いた大花火の一番右側の粒子の位置を格納し、
`isOpen`は大花火が開いたかどうかをbooleanで格納しています。

```ts
    // ★★★ 大花火
    private drawParticles = (): void => {
        this.allLife++;
      
        for (let i = 0; i < this.firePropsLength; i += this.firePropCount) {
            this.updateParticle(i);
        }
      
        // ★②★ 大花火が開いたら千輪花火の準備をする
        if (this.allLife > this.maxTTL) {
            this.allLife = 0;
          
            this.maxX = -Infinity
            for (let i = 0; i < this.fireArray.length; i += this.firePropCount) {
                const x = this.fireArray[i];
                this.maxX = Math.max(this.maxX, x);
            }
            this.isOpen = true
            this.initThousandPetals();
        }
    }
```

- 千輪花火の初期化
`firePropsLength`を９等分し、大花火が開いた場所に9個千輪用の花火を作成します。
`launchX`は大花火開始のx座標なので、`maxX`-`launchX`が大花火の半径です。
9個の千輪用の花火が開いた時に大花火の範囲に収まるように調整してます。


```ts
    // ★②★ 千輪の初期化
    private initThousandPetals = (): void => {
        for (let i = 0; i < this.firePropsLength; i += this.firePropCount) {
            if (i < this.firePropsLength / 9) {
                // ★★★ 右
                this.initParticle(i, (this.maxX - this.launchX) * 0.5, 0);
            } else if (i < this.firePropsLength * 2 / 9) {
                // ★★★ 右上
                this.initParticle(i, (this.maxX - this.launchX) * 0.35, (this.maxX - this.launchX) * 0.35);
            } else if (i < this.firePropsLength * 3 / 9) {
                // ★★★ 右下
                this.initParticle(i, (this.maxX - this.launchX) * 0.35, (this.maxX - this.launchX) * -0.35);
            } else if (i < this.firePropsLength * 4 / 9) {
                // ★★★ 左
                this.initParticle(i, (this.maxX - this.launchX)*-0.5, 0);
            } else if (i < this.firePropsLength * 5 / 9) {
                // ★★★ 左上
                this.initParticle(i, (this.maxX - this.launchX) * -0.35, (this.maxX - this.launchX) * 0.35);
            } else if (i < this.firePropsLength * 6 / 9) {
                // ★★★ 左下
                this.initParticle(i, (this.maxX - this.launchX) * -0.35, (this.maxX - this.launchX) * -0.35);
            } else if (i < this.firePropsLength * 7 / 9) {
                // ★★★ 上
                this.initParticle(i, 0, (this.maxX - this.launchX) * 0.5);
            } else if (i < this.firePropsLength * 8 / 9 ) {
                // ★★★ 下
                this.initParticle(i, 0, (this.maxX - this.launchX) * -0.5);
            } else {
                // ★★★ 真ん中
                this.initParticle(i, 0, 0);
            }
        }
    }
```

- 千輪花火の更新
`vx`と`vy`に掛ける数は小さい方が花火がゆっくり開き、花火の大きさが小さくなるので、大花火の`0.983`よりも小さい`0.935`にしました。
（開くのを遅くしたので動きがヌルッとしていてちょっと気持ち悪いような気もしますが、、）
最後にキラキラするように、千輪花火の寿命の25%過ぎたらランダムで点滅させています。

```ts
    // ★②★ 千輪の更新
    private updateThousandPetals = (i: number): void => {
        const pi: ParticleIndex = this.getParticleIndex(i);
        let x: number;
        let y: number;
        let vx: number;
        let vy: number;
        let life: number;
        let ttl: number;
        let speed: number;
        let radius: number;
        let x2: number;
        let y2: number;
        let radius2: number;
        let hue: number;
        let swing: number;
        let blink: number;

        x = this.fireArray[pi.x];
        y = this.fireArray[pi.y];
        vx = this.fireArray[pi.vx];
        vy = this.fireArray[pi.vy];
        life = this.fireArray[pi.life];
        ttl = this.fireArray[pi.ttl];
        speed = this.fireArray[pi.speed];
        radius = this.fireArray[pi.radius];
        hue = this.fireArray[pi.hue];

        if (life > ttl) {
            return;
        }

        // ★★★ 小さい花火になるように調整
        vx = vx * 0.935;
        vy = vy * 0.935;
        vy = vy + 0.008;

        if (life > ttl * 0.5) {
            swing = Math.cos((360 * (life * 40 * speed) / ttl) * Math.PI / 180) / 10;
        } else {
            swing = 0;
        }      

        x2 = x + vx + swing;
        y2 = y + vy;

        if (life > ttl * 0.25) {
            // ★★★ 最後キラキラさせる
            blink = Math.random() < 0.5 ? 0 : 1;
        } else {
            blink = 1;
        }

        radius2 = Math.min(radius * (1 - life / ttl) + 1, radius);

        this.drawParticle(x, y, x2, y2, life, ttl, radius2, hue, blink);

        life++;

        this.fireArray[pi.x] = x2;
        this.fireArray[pi.y] = y2;
        this.fireArray[pi.vx] = vx;
        this.fireArray[pi.vy] = vy;
        this.fireArray[pi.life] = life;
        this.fireArray[pi.radius] = radius;

    }
```

# さいごに
Canvasを使うとすごいリアルになりますね！
グロー効果というのも初めて知りましたが、とても勉強になりました。

参考になれば幸いです！