LoginSignup
128
81

【TypeScript】花火

Last updated at Posted at 2023-10-16

はじめに

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

アレンジした花火

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

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

See the Pen Firework_ts2 by Nina (@ifnezsju-the-selector) on CodePen.

メソッド・クラス

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

  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と固定になっていたので、ランダムになるように変更しました。

    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),
        };
    }

② 花火が開いた後に千輪の花火を追加

ポイントとなる追加箇所のみ下記に説明します。

  • 千輪花火の準備
    変数maxXisOpenを追加していますが、maxXは開いた大花火の一番右側の粒子の位置を格納し、
    isOpenは大花火が開いたかどうかをbooleanで格納しています。
    // ★★★ 大花火
    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等分し、大花火が開いた場所に9個千輪用の花火を作成します。
    launchXは大花火開始のx座標なので、maxX-launchXが大花火の半径です。
    9個の千輪用の花火が開いた時に大花火の範囲に収まるように調整してます。
    // ★②★ 千輪の初期化
    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);
            }
        }
    }
  • 千輪花火の更新
    vxvyに掛ける数は小さい方が花火がゆっくり開き、花火の大きさが小さくなるので、大花火の0.983よりも小さい0.935にしました。
    (開くのを遅くしたので動きがヌルッとしていてちょっと気持ち悪いような気もしますが、、)
    最後にキラキラするように、千輪花火の寿命の25%過ぎたらランダムで点滅させています。
    // ★②★ 千輪の更新
    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を使うとすごいリアルになりますね!
グロー効果というのも初めて知りましたが、とても勉強になりました。

参考になれば幸いです!

128
81
0

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
128
81