p5.jsで花火を実装

打ち上げ花火

画面タッチで打ち上げ
スクロールで投稿を表示

目次
  1. 花火の種類と実装するためのコード
  2. コード実装の流れ(備忘録)
    1. 基本的なセットアップ
    2. 高機能にする

花火の種類と実装するためのコード

  • クリック(タップ)した位置を到達点にして花火を打ち上げ、花火の種類(継承クラス)を切り替えて表示する
  • 花火の種類はパラメータを追加するだけで作成できる

花火の種類(はじめて知りました💦)

  • 牡丹:尾を引かずに飛び散るように開く花火
  • 菊:尾を引いて開く花火
  • 芯入:花火が開いた際に中心部に同心円の小さい輪があるもの
  • 冠(かむろ)菊:地上まで垂れ下がるように尾を引く物
  • 柳:上空で柳の枝が垂れ下がるように光りが流れ落ちる花火
  • 型物:空に具体的な形を描く花火

コードがわからないくてもパラメータを追加するだけでいろいろな種類の花火を作ることができます

7/27 一部修正(フィニッシュのバグ)

コード

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>打ち上げ花火</title>
    <style>
        #webgl canvas{
        position: fixed;
        top: 0;
        left: 0;
        outline: none;
        display:block;
        cursor: pointer;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
     }
    </style>
</head>
<body>
    <div id="webgl"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js" integrity="sha512-d6sc8kbZEtA2LwB9m/ck0FhvyUwVfdmvTeyJRprmj7Wg9wRFtHDIpr6qk4g/y3Ix3O9I6KHIv6SGu9f7RaP1Gw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script>
        const INITIAL_GRAVITY = 0.3;
        const INITIAL_HUE = 30;
        const INITIAL_SATURATION = 30;
        const INITIAL_BRIGHTNSS = 100;
        const HISTORY_LENGTH = 10;
        const velocityFunctions = {
            circle: (p, min = 1, max = 15) => {
                return p5.Vector.random2D().mult(p.random(min, max));
            },
            heart: (p, min = 0.5, max = 0.5) => {
                let t = p.random(0, p.TWO_PI);
                let x = 16 * Math.pow(Math.sin(t), 3);
                let y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
                return p.createVector(x, -y).mult(p.random(min, max));
            }
        };
        const fireworkSettings = {
            threshold: 100,
            count: 150,
            velocityType: 'circle',
            velocityRange: [1, 15],
            velocityMultiplier: 0.9,
            gravity: 0.05,
            lifespanDecrement: 2,
            randomHue: true,
            hueRange: [0, 360],
            saturation: 100,
            brightness:100,
            strokeWeightValue: 4,
            trail: false,
            historyLength: 20,
            updateInterval: 5,
            finish: false,
            finishThreshold: 100,
            finishCount: 1,
            finishColor: true,
            finishHueRange: [0, 360],
            finishSaturation: 50,
            finishBrightness: 100,
            finishStrokeWeightValue: 4,
        };
      window.addEventListener("DOMContentLoaded",()=>{
        let fireworks = [];
        let fireworkIndex = 0;
        const sketch = (p) => {
            p.setup = () => {
                p.createCanvas(p.windowWidth, p.windowHeight).parent('webgl');
                p.frameRate(30);
                p.colorMode(p.HSB, 360, 100, 100, 255);
                const canvasElement = p.canvas;
                canvasElement.addEventListener('click', handleMouseClick);
                canvasElement.addEventListener('touchend', handleTouchEnd);
            };
            p.draw = () => {
                p.background(0, 0, 0, 255);
                for (let i = 0; i < fireworks.length; i++) {
                fireworks[i].update();
                fireworks[i].display();
                    if (fireworks[i].done()) {
                        fireworks.splice(i, 1);
                    }
                }
            };
            p.windowResized = () => {
                p.resizeCanvas(p.windowWidth, p.windowHeight);
            };
            const createFirework = (p, x, y) => {
                fireworks.length = 0;
                const fireworkClass = fireworkList[fireworkIndex];
                fireworks.push(new fireworkClass(p, x, y));
                fireworkIndex = (fireworkIndex + 1) % fireworkList.length;
            };
            const handleMouseClick = (event) => {
                const x = event.clientX;
                const y = event.clientY;
                createFirework(p, x, y);
            };
            const handleTouchEnd = (event) => {
                if (event.changedTouches && event.changedTouches.length > 0) {
                const x = event.changedTouches[0].clientX;
                const y = event.changedTouches[0].clientY;
                createFirework(p, x, y);
                }
                event.preventDefault();
            };
        };
        new p5(sketch);
        })

        class Particle {
            constructor(p, x, y, exploded =false, hue=INITIAL_HUE, settings = fireworkSettings) {
            this.hue = hue;
            this.p = p;
            this.position = exploded ? p.createVector(x, y) : p.createVector(x, p.height);
            this.acceleration = p.createVector(0, 0);
            this.gravity = this.p.createVector(0, INITIAL_GRAVITY);
            this.targetY = y;
            this.lifespan = 255;
            this.exploded = exploded;
            this.hasUpdated = false;
            this.settings = settings;
            this.finishLocation = null;
            this.hasFinished = false;
            // 10%の確率でリセットをスキップ
            this.finishSkipped = Math.random() < 0.1;
            exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
            }
            setInitialVelocity() {
            let initialVelocityY = -Math.sqrt(2 * Math.abs(INITIAL_GRAVITY) * (this.p.height - this.targetY));
            this.velocity = this.p.createVector(0, initialVelocityY);
            }
            setExplodedVelocity() {
            this.velocity = velocityFunctions[this.settings.velocityType](this.p, ...this.settings.velocityRange);
            }
            update(j) {
            this.acceleration.add(this.gravity);
            this.velocity.add(this.acceleration);
            this.position.add(this.velocity);
            this.acceleration.mult(0);
                if (this.exploded) {
                    this.hasUpdated = true;
                    this.lifespan -= this.settings.lifespanDecrement;
                    this.velocity.mult(this.settings.velocityMultiplier)
                    this.gravity = this.p.createVector(0, this.settings.gravity);
                    if (this.settings.finish && this.lifespan <= this.settings.finishThreshold && !this.hasFinished) {
                        this.finishLocation = this.position.copy();
                        if (this.finishLocation && j % this.settings.finishCount === 0) {
                            if (this.settings.finishCount === 1 && !this.finishSkipped) {
                                this.lifespan = 255;
                            } else if (this.settings.finishCount !== 1) {
                                this.lifespan = 255;
                            }
                        }
                    this.hasFinished = true;
                    }
                }
            }
            display() {
                if (this.finishLocation) {
                    const finishHue = this.settings.finishColor ? this.hue : this.p.random(this.settings.finishHueRange[0], this.settings.finishHueRange[1]);
                    this.p.stroke(finishHue, this.settings.finishSaturation, this.settings.finishBrightness, this.lifespan);
                    this.p.strokeWeight(this.settings.finishStrokeWeightValue);
                    this.p.point(this.finishLocation.x, this.finishLocation.y);
                }
                else if (this.exploded) {
                    if(this.hasUpdated){
                        this.p.stroke(this.hue, this.settings.saturation, this.settings.brightness, this.lifespan);
                        this.p.strokeWeight(this.settings.strokeWeightValue);
                        this.p.point(this.position.x, this.position.y)
                }
                }
                else {
                    this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, this.lifespan);
                    this.p.strokeWeight(4);
                    this.p.point(this.position.x, this.position.y);
                }
            }
            isDone() {
                const adjusted = !this.settings.finish ? 0 : 0 -(255 - this.settings.finishThreshold);
                return this.lifespan <= adjusted;
            }
            lifespanWatcher() {
                return this.exploded && this.lifespan <= this.settings.threshold;
            }
        }

        class TrailParticle extends Particle {
            constructor(p, x, y, exploded, hue, settings) {
                super(p, x, y, exploded, hue, settings);
                this.history = [];
                this.updateCounter = 0;
                exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
            }
            update(j) {
                super.update(j);
                if (!this.finishLocation) {
                    this.updateCounter++;
                    if (this.exploded && this.updateCounter % this.settings.updateInterval === 0) {
                        this.addHistory();
                    } else {
                        this.addHistory();
                    }
               }
            }
            addHistory() {
                let v = this.position.copy();
                this.history.push(v);
                const historyLength = this.exploded ? this.settings.historyLength : HISTORY_LENGTH;
                if (this.history.length > historyLength) {
                    this.history.shift();
                }
            }
            display() {
                if (this.finishLocation) {
                    const finishHue = this.settings.finishColor ? this.hue : this.p.random(this.settings.finishHueRange[0], this.settings.finishHueRange[1]);
                    this.p.stroke(finishHue, this.settings.finishSaturation, this.settings.finishBrightness, this.lifespan);
                    this.p.strokeWeight(this.settings.finishStrokeWeightValue)
                    this.p.point(this.finishLocation.x, this.finishLocation.y);
                } else {
                    super.display();
                    for (let i = 0; i < this.history.length; i++) {
                        let pos = this.history[i];
                        let alpha = 255 * (i / this.history.length) * (this.lifespan / 255);
                        if (this.exploded) {
                            this.p.stroke(this.hue, this.settings.saturation, this.settings.brightness, alpha);
                            this.p.point(pos.x, pos.y);
                        } else {
                            this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, alpha);
                            this.p.point(pos.x, pos.y);
                        }
                    }
               }
            }
        }

        class Firework {
            constructor(p, x, y) {
            this.p = p;
            this.firework = new TrailParticle(p, x, y);
            this.xUnit = p.width/10;
            this.yUnit = p.height/10;
            this.exploded = false;
            this.particleGroups = [];
            }
            setupExplosions() {}
            createParticles(overrides = {}, positionOffsets = { xOffset: 0, yOffset: 0 }) {
                const particles = [];
                const defaultSettings = fireworkSettings;
                const settings = { ...defaultSettings, ...overrides };
                const randomHue = settings.randomHue ? Math.round(this.p.random(0, 36)) * 10 : null;
                const baseX = Math.min(Math.max(this.firework.position.x + positionOffsets.xOffset, 0), this.p.width);
                const baseY = Math.min(Math.max(this.firework.position.y + positionOffsets.yOffset, 0), this.p.height);
                for (let i = 0; i < settings.count; i++) {
                const particleHue = settings.randomHue ? randomHue : this.p.random(settings.hueRange[0], settings.hueRange[1]);
                const particle = settings.trail ?
                    new TrailParticle(this.p, baseX, baseY, true, particleHue, settings) :
                    new Particle(this.p, baseX, baseY, true, particleHue, settings);
                particles.push(particle);
                }
                this.particleGroups.push({ particles:particles });
            }
            update() {
                if (!this.exploded) {
                    this.firework.update();
                    if (this.firework.velocity.y >= 0) {
                        this.exploded = true;
                        this.setupExplosions();
                    }
                } else {
                for (let i = 0; i < this.particleGroups.length; i++) {
                    let particles = this.particleGroups[i].particles;
                    let belowThresholdCount = 0;
                    for (let j = particles.length - 1; j >= 0; j--) {
                        let particle = particles[j];
                        particle.update(j);
                        if (particle.lifespanWatcher()) {
                            belowThresholdCount++;
                        }
                        if (particle.isDone()) {
                            particles.splice(j, 1);
                        }
                    }
                    if (belowThresholdCount === 0) {
                        break;
                    }
                }
                this.particleGroups = this.particleGroups.filter(group => group.particles.length > 0);
              }
            }
            display() {
                if (!this.exploded) {
                    this.firework.display();
                } else {
                for (let i = 0; i < this.particleGroups.length; i++) {
                    let particles = this.particleGroups[i].particles;
                    for (let j = 0; j < particles.length; j++) {
                        let particle = particles[j];
                        particle.display();
                    }
                }
              }
            }
            done() {
            return this.exploded && this.particleGroups.every(particleGroup => particleGroup.particles.length === 0);
            }
        }

        class Firework1 extends Firework {
            setupExplosions() {
              this.createParticles()
            }
        }
       /*
        花火の種類をこの下に追加
       */

       /*
       新しい名前(継承クラス)を追加する
        */
       const fireworkList = [Firework1,];

    </script>
</body>
</html>

コードの、一番下のこの部分を

class Firework1 extends Firework {
    setupExplosions() {
       this.createParticles()
    }
}

このように編集すると

class Firework1 extends Firework {
    setupExplosions() {
    this.createParticles({ threshold: 250, velocityRange: [1, 12]}, {xOffset: this.xUnit*2, yOffset: this.yUnit});
    this.createParticles({ threshold: 250, velocityRange: [1, 12]}, {xOffset: -this.xUnit*2, yOffset: -this.yUnit*1.5});
    this.createParticles({ threshold: 200, velocityRange: [1, 12]}, {xOffset: this.xUnit, yOffset: -this.yUnit*2.5});
    this.createParticles({ threshold: 250, count: 200, velocityRange: [1, 25], velocityMultiplier: 0.9, gravity: 0.01, randomHue:false, hueRange:[180, 220], saturation: 10, strokeWeightValue: 2.5, trail: true, historyLength: 20, updateInterval: 30});
    this.createParticles({threshold:200, count:200, randomHue:false, hueRange:[10, 30], strokeWeightValue: 5.5 });
    this.createParticles({velocityRange: [1, 25], count:300, randomHue:false, hueRange:[0, 40], saturation:50, finish:true, finishColor: false, finishHueRange: [0, 60]});
    }
}

こんな感じの花火になります

粒子グループ(this.createParticles())を作成する
パラメータをカスタマイズする際のデフォルト設定のテンプレート
*パラメータ数は多いですが、値を変更しないものは不要です
特定の機能が必要な場合のみ設定するパラメータもあります

//コピペ用
 this.createParticles({ threshold: 100, count: 150, velocityType: 'circle', velocityRange: [1, 15], velocityMultiplier: 0.9, gravity: 0.05, lifespanDecrement: 2,
    randomHue: true, hueRange: [0, 360], saturation: 100, brightness:100, strokeWeightValue: 4,
    trail: false, historyLength: 20, updateInterval: 5,
    finish: false, finishThreshold: 100, finishCount: 3, finishColor: true, finishHueRange: [0, 360], finishSaturation: 50, finishBrightness: 100, finishStrokeWeightValue: 4,}
    , {xOffset: this.xUnit*0, yOffset: this.yUnit*0 });

パラメータの説明

this.createParticles(
{
   threshold: 100,  //次の粒子グループを表示するタイミング(0から255までの数字)
                    //0は粒子が全部消えたら次の粒子グループを表示 
                    //255は同じタイミング 

   count: 150,     // 粒子の数

   velocityType: 'circle',  //'circle' 円形に拡がる 又は 'heart'ハート型に拡がる

   velocityRange: [1, 15],  //粒子の初速のベクトルの長さの範囲、最小値と最大値の間でランダムに設定されます
                            //最大値が大きいほど、粒子は広範囲に拡散
                            //同じ数字が指定されると、輪のような均一な形状
                
   velocityMultiplier: 0.9, //減速係数で、1に近い値ほど広範囲に拡散します(ゆっくり減速する)
                 //velocityRangeの範囲とvelocityMultiplierの減衰は相互作用します
                 //例えば、速度の範囲が広くても、減速係数が低い場合、拡散が限定されます

   gravity: 0.05,   //重力の強さを指定する数値 大きいほど粒子は下に流れる

   lifespanDecrement: 2, //粒子の寿命を減少させる量を指定する数値
                         //大きいほど速く消滅する
 
   randomHue: true,  //色相を1つの粒子グループで同じ色にして、ランダムに生成する場合は true

       // randomHueがfalseの場合にhueRangeを使用
       hueRange: [0, 360],  // 色相の範囲を指定(0から360までの数字)
                      
   saturation: 100,   //粒子の彩度(0から100までの数字)

   brightness: 100,  //粒子の明度(0から100までの数字)

   strokeWeightValue: 4, //粒子の太さ

   trail: false,  //粒子が残像を描くかどうかを示すブール値(trueの場合:残像を描く)

  //trailがtrueの場合にhistoryLength, updateIntervalを設定できる
        historyLength: 10, //パーティクルの残像を記録する際の位置の履歴の数
                //値が大きいと残像が長く続くが
                          //大きいすぎると、計算負荷が増大し粒子が消滅した後に履歴が残って無駄になる場合がある

        updateInterval: 2,  //更新間隔を指定(何フレームごとに更新するか)
                 //数値が大きいほど負荷軽減になる


   finish: false, // 消える時にピカッとなる演出(フィニッシュ)を描くかどうかを示すブール値(trueの場合:フィニッシュを描く)

 //finishがtrueの場合にfinish....を設定できる
        finishThreshold: 0, //フィニッシュの表示タイミング(0から255までの数字)
                          //0は親が消えた時
                          //255は親が表示された時

        finishCount: 1, //フィニッシュの粒子数 
                      //3は親の数の3分の1、2は親の2分の1、1は親とほぼ同じ数

        finishStrokeWeightValue: 4, //フィニッシュの粒子の太さ

        finishColor: true, //trueは親と同じ色でピカピカはしない
                           //falseはfinishHueRangeの範囲が大きい場合はピカピカする

            //finishColorがfalseの場合にfinishHueRangeを使用
            finishHueRange: [0, 360], // フィニッシュの粒子の色相の範囲を指定(0から360までの数字)

        finishSaturation: 50, //フィニッシュの粒子の彩度(0から100までの数字)

        finishBrightness: 100 //フィニッシュの粒子の明度(0から100までの数字)

}, 
// 花火の発射位置の調整(調整が不要な場合は省略可能)
//幅や高さを10等分した値がxUnitやyUnitです(もちろん数字でもいい)
//xが横軸で負の値は左方向
//yが縦軸で負の値は上方向
{
   xOffset: this.xUnit * 0,  
   yOffset: this.yUnit * 0
}
);

色について
Hue(色相)、Saturation(彩度)、 Brightness(明度)で表す方法(HBS)を使います

  • 色相 (Hue) :360°の円周に色を配置
    赤(0)黄(60)緑(120)青緑(180)青(240)赤紫(300)
  • 彩度(Saturation):0~100 (0:白・100:鮮やか)
  • 明度(Brightness):0~100 (0:暗い・100:明るい)

アニメーションの見た目が変わる可能性について

粒子の数が多く、特に尾を引くような開花型の花火(trail: true)を表示する場合、計算処理が複雑になります
コンピュータのCPUやGPUにかかる負荷が増大し、アニメーションの動きが不安定になったり、遅延が発生することがあります
*使用しているデバイスのハードウェアの性能に大きく依存します

対策

  • 粒子の数を可能な限り減らす
  • trailがtrueの場合にhistoryLengthを可能な限り小さく
    updateIntervalを可能な限り大きくする

花火の種類を増やす

//コードの下に追加
//Firework2の部分に新しい名前をつける
class Firework2 extends Firework {
    setupExplosions() {
       //ここに粒子グループを追加していく
       this.createParticles()
    }
}

//この部分に新しい名前を追加する
const fireworkList = [Firework1, Firework2];

以上です

ここからは試行錯誤した備忘録😅

コード実装の流れ(備忘録)

コードについて
今回、複数のスケッチを1つのページに含める場合も想定してインスタンスモードを使用
ファイルを分割することも考慮してクラスはスケッチ関数の外で定義し、pオブジェクトをコンストラクタに渡して使用
しかし、グローバルモードスケッチ関数内でクラスを定義する場合はp5の関数やオブジェクトはグローバルスコープまたはスケッチ関数のスコープ内で利用可能なため、pオブジェクトを渡す必要はありません

またp5のmousePressedメソッドは、特定の状況でスクロールが阻害されることがあったので、「clickとtouchend」イベントリスナーを使っています

全体の構造について

  • fireworkSettingsオブジェクト
    デフォルト設定を格納したオブジェクト
  • Particleクラス
    個々のパーティクルの動作を管理するクラス
    パーティクルの状態を更新して描画
  • TrailParticleクラス
    Particleクラスを継承し、粒子の軌跡を履歴として保持する機能を追加
    残像を残す粒子を描画
  • Fireworkクラス
    花火全体の動作を管理するクラス
    打ち上げ時のパーティクルと、その後の爆発を制御
    爆発後は、花火の種類に基づいて複数のパーティクルグループ生成、セットアップする機能、順番に爆発させる機能を持つ
  • Fireworkクラスの継承クラス
    さまざまな複数のパーティクルグループを組み合わせてリッチな花火を作成する

基本的なセットアップ

打ち上げ用パーティクル
Particleクラスで、花火がクリックされた位置の高さに到達したら落下する「速度」を計算
*targetY に到達するための速度を計算
v(速度) = sqrt(2 *g(重力) * h(初期位置とターゲットの高さの差))

<div id="webgl"></div>
#webgl canvas{
    position: fixed;
    top: 0;
    left: 0;
    outline: none;
    display:block;
    cursor: pointer; //ポインター(通常は手の形)に変更する
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0); //タップ時のデフォルトのハイライトカラーを無効にする
}
import p5 from 'p5'

const INITIAL_GRAVITY = 0.3; // 重力の定数
let fireworks = []; // Fireworkオブジェクトを保持する配列

const sketch = (p) => {
  // p5.jsの初期設定
  p.setup = () => {
    p.createCanvas(p.windowWidth, p.windowHeight).parent('webgl');
    p.frameRate(30); //デバイスのスペックによる差を最小限に抑えるためfps30に
    p.stroke(255);
    p.strokeWeight(4);

    // clickとtouchendイベントリスナーを追加
    const canvasElement = p.canvas;
    canvasElement.addEventListener('click', handleMouseClick);
    canvasElement.addEventListener('touchend', handleTouchEnd);
  };

  // p5.jsのdrawループ
  p.draw = () => {
    p.background(0); // 背景を黒で塗りつぶす
    for (let i = 0; i < fireworks.length; i++) {
      fireworks[i].update();
      fireworks[i].display();
    }
  };

  // ウィンドウサイズが変更されたときにキャンバスサイズを更新
  p.windowResized = () => {
    p.resizeCanvas(p.windowWidth, p.windowHeight);
  };

  // 新しいFireworkオブジェクトを作成し、fireworks配列に追加
  const createFirework = (p, x, y) => {
    fireworks.push(new Firework(p, x, y));
  };

  // マウスクリックイベントの座標を取得して花火を作成
  const handleMouseClick = (event) => {
    const x = event.clientX;
    const y = event.clientY;
    createFirework(p, x, y);
  };

  // タッチエンドイベントの座標を取得して花火を作成
  const handleTouchEnd = (event) => {
    if (event.changedTouches && event.changedTouches.length > 0) {
      const x = event.changedTouches[0].clientX;
      const y = event.changedTouches[0].clientY;
      createFirework(p, x, y);
    }
    event.preventDefault();
  };
};

// p5.jsのインスタンスを作成してスケッチを実行
new p5(sketch);

class Particle {
  // パーティクルの初期位置、速度、加速度を設定
  constructor(p, x, y) {
    this.p = p;
    this.position = p.createVector(x, p.height); // 初期位置(画面下)
    this.acceleration = p.createVector(0, 0); // 加速度
    this.gravity = this.p.createVector(0, INITIAL_GRAVITY); // 重力
    this.targetY = y; // 目標の高さ
    // 速度を計算 (v = sqrt(2 * g * h))
    let initialVelocityY = -Math.sqrt(2 * Math.abs(INITIAL_GRAVITY) * (p.height - y));
    this.velocity = p.createVector(0, initialVelocityY); // 初速度
  }

  // 加速度に重力を加えて、速度と位置を更新、加速度をリセット
  update() {
    this.acceleration.add(this.gravity);
    this.velocity.add(this.acceleration);
    this.position.add(this.velocity);
    this.acceleration.mult(0);
  }

  // パーティクルを表示
  display() {
    this.p.point(this.position.x, this.position.y);
  }
}

class Firework {
  // Fireworkクラスのコンストラクタ
  constructor(p, x, y) {
    this.p = p;
    // Particleクラスのインスタンスを作成
    this.firework = new Particle(p, x, y);
  }

  // Fireworkの更新処理
  update() {
    this.firework.update();
  }

  // Fireworkの描画処理
  display() {
    this.firework.display();
  }
}

花火の爆発を実装をする
Particleクラスで、explodedフラグを追加して打ち上げ用パーティクルと爆発パーティクルを区別
Fireworkクラスでは爆発時の爆発用パーティクルを格納する配列を作成するメソッドを追加、update メソッドどdisplayメソッドでexplodedフラグで条件分岐

class Particle {
    // explodedフラグを追加
    constructor(p, x, y, exploded) {
      this.p = p;
      // explodedフラグに基づいて位置を設定
      this.position = exploded ? p.createVector(x, y) : p.createVector(x, p.height);
      this.acceleration = p.createVector(0, 0);
      this.gravity = this.p.createVector(0, INITIAL_GRAVITY);
      this.targetY = y;
      // explodedフラグに基づいて速度を設定、この行はconstructor関数の最後に
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity(); 
    }

    // 打ち上げ時の速度
    setInitialVelocity() {
      let initialVelocityY = -Math.sqrt(2 * Math.abs(INITIAL_GRAVITY) * (this.p.height - this.targetY));
      this.velocity = this.p.createVector(0, initialVelocityY);
    }

    // 爆発時の速度
    setExplodedVelocity() {
      this.velocity = p5.Vector.random2D(); 
      this.velocity.mult(this.p.random(1, 5));
    }
    // 位置と速度を更新
    update() {
      this.acceleration.add(this.gravity);
      this.velocity.add(this.acceleration);
      this.position.add(this.velocity);
      this.acceleration.mult(0);
    }
    // パーティクルを表示
    display() {
      this.p.point(this.position.x, this.position.y);
    }
  }

  class Firework {
    constructor(p, x, y) {
      this.p = p;
      // 打ち上げ用パーティクル explodedフラグはfalse
      this.firework = new Particle(p, x, y, false);
      // explodedフラグの初期値
      this.exploded = false;
      // 爆発用パーティクルを格納する配列
      this.particles = [];
    }

    // 爆発用パーティクルをparticles配列に格納するメソッド
    explode() {
      for (let i = 0; i < 100; i++) {
        // Particleインスタンス、explodedフラグはtrue
        let particle = new Particle(this.p, this.firework.position.x, this.firework.position.y, true);
        this.particles.push(particle);
      }
    }

    // 花火の更新処理
    update() {
      if (!this.exploded) {
        this.firework.update();
        // 最高点に達したら爆発(花火が上昇する間はvelocity.yが負の値なので)
        if (this.firework.velocity.y >= 0) {
          this.explode(); // 爆発用パーティクルを生成
          this.exploded = true; // explodedフラグをtrueに
        }
      } else {
        // 爆発後のパーティクルを更新
        for (let i = 0; i < this.particles.length; i++) {
          this.particles[i].update();
        }
      }
    }

    // 花火の描画処理
    display() {
      if (!this.exploded) {
        this.firework.display();
      } else {
        // 爆発後のパーティクルを表示
        for (let i = 0; i < this.particles.length; i++) {
          this.particles[i].display();
        }
      }
    }
  }

爆発時の速度について
速度(velocity)は大きさ(速さ)と方向の両方を持つベクトル量のこと

  • p5.Vector.random2D() :ランダムな方向を持つ2次元ベクトルを生成する関数(方向は0度から360度の間でランダム、長さは1)
  • velocity.mult(this.p.random(1, 5)) :速度(velocity)にランダムな数を乗算することでベクトルの長さ(パーティクルの拡がり)を1から5の範囲でランダムに変更

パーティクルの寿命と透明度を管理
1、Particleクラスで、パーティクルが生成されてから一定の時間が経過すると徐々に消えていく効果を追加
*lifespan を徐々に減少させ、その値に基づいてパーティクルの「透明度」を調整する
デフォルトのカラーモードはcolorMode(RGB) ではすべての色成分(赤、緑、青、透明度)を 0 から 255 の範囲で設定するので、lifespanの初期値を255にする
2、Fireworkクラスで、パーティクルの寿命が尽きたら配列から削除して不要な計算を減らす

class Particle {
    constructor(p, x, y, exploded) {
      // 省略
      this.lifespan = 255;//lifespanの初期値を255
      this.exploded = exploded; //explodedフラグはメソッドで使用
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
    // 省略
    update() {
        this.velocity.add(this.acceleration);
        this.position.add(this.velocity);
        this.acceleration.mult(0);
      // 爆発パーティクルの寿命を減少
        if (this.exploded) {
          this.lifespan -= 4;
        }
    }
    display() {
         // 爆発時は寿命に基づいて透明度を設定
        if (this.exploded) {
            this.p.stroke(255, this.lifespan);
            this.p.strokeWeight(3);
          } else {
            this.p.stroke(255);
            this.p.strokeWeight(4);
        }
       this.p.point(this.position.x, this.position.y);
    }
    /*
    パーティクルの寿命が尽きたかどうかを判定ためのisDoneメソッドを作成
    Fireworkクラスのupdateメソットでparticles配列から寿命が尽きたパーティクルを削除するために利用する
    */
    isDone() {
        return this.lifespan <= 0;
      }
  }


class Firework {
  // 省略
   update() {
        if (!this.exploded) {
           // 省略
          } else{
               /*
                配列から要素を削除する際にインデックスのずれを防ぐため
                配列の後ろから前に向かって要素をチェックして削除
                */
              for (let i = this.particles.length - 1; i >= 0; i--) {
                    this.particles[i].update();
                   // パーティクルの寿命が尽きたら配列から削除
                    if (this.particles[i].isDone()) {
                    this.particles.splice(i, 1);
                   }
              }
        }
    }
 // 省略
}

終了した花火をfireworks配列から削除して、不要な計算を減らす
Fireworkクラスに花火が終了したかどうかを判定するdone()メソッドを追加
draw 関数内で done() メソッドを使用して、終了した花火オブジェクトを配列から削除

const sketch = (p) => {
// 省略
  p.draw = () => {
    p.background(0);
    for (let i = fireworks.length - 1; i >= 0; i--) {
        fireworks[i].update();
        fireworks[i].display();
        // 花火が終了したら配列から削除
        if (fireworks[i].done()) {
          fireworks.splice(i, 1);
        }
    }
  };
 // 省略
};
new p5(sketch);


class Firework {
  // 省略
   
    done() {  // 花火が終了したかどうかを判定
        return this.exploded && this.particles.length === 0;
      }
  }

1、黒背景に透明度を設定すると、各フレームで背景が少し透明に描画されるため、パーティクルの残像が残る
2、爆発時の速度の調整(拡がりを抑える)
爆発時にvelocityを大きめに設定し、その後、velocity.mult(0.95);により徐々に速度を減少させる

const sketch = (p) => {
   // 省略
  p.draw = () => {
    p.background(0, 30); //透明度を追加
   // 省略
  };
  // 省略
};

class Particle {
   constructor(p, x, y, exploded) {
     //省略
       else {
        this.velocity = p5.Vector.random2D()
        //減速させるのでベクトルの長さは大きめに
        this.velocity.mult(this.p.random(1, 10));
      }
     //省略
    }
  //省略
    update() {
        this.velocity.add(this.acceleration);
        this.position.add(this.velocity);
        this.acceleration.mult(0);
        if (this.exploded) {
           this.velocity.mult(0.95) //速度を0.95倍に
           this.lifespan -= 4;
        }
    }
  //省略
  }

色をつける
HSBカラーモードを利用する
打ち上げ時の色は統一、爆発時には色相 (Hue)の範囲を指定してランダムな色を作成できるようにする

カラーモードについて
p5.jsのcolorMode()関数は色を扱うためのモードを設定する関数で、デフォルトはRGBカラーモードです

  • RGBカラーモード:色は赤R、緑G、青Bの3つの成分で指定、各成分の値はデフォルトで0から255の範囲
  • HSBカラーモード:色は色相(Hue)、彩度(Saturation)、明度(Brightness)の3つの成分で指定、デフォルトの範囲はすべて0から255

colorMode()関数の基本的な指定方法

colorMode(mode); // カラーモードを設定(RGBまたはHSB)
colorMode(mode, max); // すべての値の最大値を設定
colorMode(mode, max1, max2, max3, maxA); // 各成分の最大値を個別に設定
const INITIAL_HUE = 30; //打ち上げ時の色相 (Hue) 
const INITIAL_SATURATION = 30; //打ち上げの時の彩度(Saturation)
const INITIAL_BRIGHTNSS = 100; //打ち上げの時の明度(Brightness)

const sketch = (p) => {
  p.setup = () => {
    //省略
    // カラーモードをHSBに設定
    p.colorMode(p.HSB, 360, 100, 100, 255);
    //不要  p.stroke(255);
    // 不要  p.strokeWeight(4);
    //省略    
  };
  p.draw = () => {
    // 背景を透明度30で黒に設定(残像効果)
    p.background(0, 0, 0, 30);
    //省略
  };
 //省略
};
new p5(sketch);

class Particle {
    // 色相を引数として受け取る
    constructor(p, x, y, exploded, hue) {
      this.hue = hue; 
     //省略 
    }
   //省略
    display() {
        if (this.exploded) {
          // 爆発時のパーティクルの色と透明度を設定
          this.p.stroke(this.hue, 100, 100, this.lifespan);
          this.p.strokeWeight(3);
        } else {
          // 打ち上げ時
          this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, this.lifespan);
          this.p.strokeWeight(4);
        }
        this.p.point(this.position.x, this.position.y);
    }
   //省略
}

class Firework {
    constructor(p, x, y) {
      this.p = p;
     // 打ち上げ用パーティクル
      this.firework = new Particle(p, x, y, false, INITIAL_HUE);
     // 色相を0から360度の範囲で10度刻みのランダムな値を設定
      this.hue = Math.round(p.random(0, 36)) * 10;
      //省略
    }
    //省略
    explode() {
        for (let i = 0; i < 200; i++) {
          // 爆発時のパーティクルに色相を渡す
          let particle = new Particle(this.p, this.firework.position.x, this.firework.position.y, true, this.hue);
          //省略
        }
    }
   //省略
}

爆発時のそれぞれにパーティクルの色をランダムにする場合は

class Firework {
    constructor(p, x, y) {
    /*
      this.hue = Math.round(p.random(0, 36)) * 10; は不要
     */
    }
 //省略
    explode() {
        for (let i = 0; i < 200; i++) {
       /*
            それぞれのパーティクルにランダム値 を渡す this.p.random(0, 360)
          */
          let particle = new Particle(this.p, this.firework.position.x, this.firework.position.y, true, this.p.random(0, 360));
     //省略
        }
    }
}

ここまでのコード

import p5 from 'p5'

const INITIAL_GRAVITY = 0.3;
const INITIAL_HUE = 30;
const INITIAL_SATURATION = 30;
const INITIAL_BRIGHTNSS = 100;
let fireworks = [];

const sketch = (p) => {
  p.setup = () => {
    p.createCanvas(p.windowWidth, p.windowHeight).parent('webgl');
    p.frameRate(30);
    p.colorMode(p.HSB, 360, 100, 100, 255);
    const canvasElement = p.canvas;
    canvasElement.addEventListener('click', handleMouseClick);
    canvasElement.addEventListener('touchend', handleTouchEnd);
  };
  p.draw = () => {
    p.background(0, 0, 0, 30);
    for (let i = 0; i < fireworks.length; i++) {
      fireworks[i].update();
      fireworks[i].display();
        if (fireworks[i].done()) {
            fireworks.splice(i, 1);
        }
    }
  };
  p.windowResized = () => {
    p.resizeCanvas(p.windowWidth, p.windowHeight);
  };
  const createFirework = (p, x, y) => {
    fireworks.push(new Firework(p, x, y));
  };
  const handleMouseClick = (event) => {
    const x = event.clientX;
    const y = event.clientY;
    createFirework(p, x, y);
  };
  const handleTouchEnd = (event) => {
    if (event.changedTouches && event.changedTouches.length > 0) {
      const x = event.changedTouches[0].clientX;
      const y = event.changedTouches[0].clientY;
      createFirework(p, x, y);
    }
    event.preventDefault();
  };
};
new p5(sketch);

class Particle {
    constructor(p, x, y, exploded,hue) {
      this.hue = hue;
      this.p = p;
      this.position = exploded ? p.createVector(x, y) : p.createVector(x, p.height);
      this.acceleration = p.createVector(0, 0);
      this.gravity = this.p.createVector(0, INITIAL_GRAVITY);
      this.targetY = y;
      this.lifespan = 255;
      this.exploded = exploded;
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
    setInitialVelocity() {
      let initialVelocityY = -Math.sqrt(2 * Math.abs(INITIAL_GRAVITY) * (this.p.height - this.targetY));
      this.velocity = this.p.createVector(0, initialVelocityY);
    }
    setExplodedVelocity() {
      this.velocity = p5.Vector.random2D();
      this.velocity.mult(this.p.random(1, 10));
    }
    update() {
      this.acceleration.add(this.gravity);
      this.velocity.add(this.acceleration);
      this.position.add(this.velocity);
      this.acceleration.mult(0);
        if (this.exploded) {
            this.velocity.mult(0.95)
            this.lifespan -= 4;
        }
    }
    display() {
        if (this.exploded) {
            this.p.stroke(this.hue, 100, 100, this.lifespan);
            this.p.strokeWeight(3);
          } else {
            this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, this.lifespan);
            this.p.strokeWeight(4);
        }
       this.p.point(this.position.x, this.position.y);
    }
    isDone() {
        return this.lifespan <= 0;
    }
}

class Firework {
    constructor(p, x, y) {
      this.p = p;
      this.firework = new Particle(p, x, y, false, INITIAL_HUE);
      this.hue = Math.round(p.random(0, 36)) * 10;
      this.exploded = false;
      this.particles = [];
    }
    explode() {
      for (let i = 0; i < 100; i++) {
        let particle = new Particle(this.p, this.firework.position.x, this.firework.position.y, true, this.hue);
        this.particles.push(particle);
      }
    }
    update() {
      if (!this.exploded) {
        this.firework.update();
        if (this.firework.velocity.y >= 0) {
          this.explode();
          this.exploded = true;
        }
      } else {
            for (let i = this.particles.length - 1; i >= 0; i--) {
                this.particles[i].update();
                if (this.particles[i].isDone()) {
                this.particles.splice(i, 1);
                }
            }
        }
    }
    display() {
      if (!this.exploded) {
        this.firework.display();
      } else {
        for (let i = 0; i < this.particles.length; i++) {
          this.particles[i].display();
        }
      }
    }
    done() {
        return this.exploded && this.particles.length === 0;
    }
}

高機能にする

背景の透明度を変更せずに残像効果を実現するために
Particleクラスを継承して新しいTrailParticleクラスを作成する
具体的には、パーティクルが表示されるたびに位置の履歴を保持し、それを描画することで残像効果を作る
*負荷軽減のために2フレームごとの処理にする

const HISTORY_LENGTH = 10; //打ち上げ時の履歴

const sketch = (p) => {
//省略
  p.draw = () => {
    p.background(0, 0, 0, 255);  //不透明にする
 //省略
  };
//省略
};
//省略

//TrailParticleクラスを作成
class TrailParticle extends Particle {
    // TrailParticleクラスのコンストラクタ
    constructor(p, x, y, exploded, hue) {
        // 親クラスのコンストラクタを呼び出し
        super(p, x, y, exploded, hue);
        this.history = []; // パーティクルの位置の履歴を保持する配列
        this.updateCounter = 0; // 更新カウンター
        // explodedフラグに基づいて速度を設定
        exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
    update() {
        // 親クラスのupdateメソッドを呼び出し
        super.update();
        this.updateCounter++; //フレーム毎に更新カウンターをインクリメント
        if (this.exploded && this.updateCounter % 2 === 0) {
             this.addHistory();
        } else {
             this.addHistory(); //打ち上げ時はフレーム毎にする
        }
    }
    //履歴管理のメソッド
    addHistory() {
        // 現在の位置を履歴に追加
        let v = this.position.copy();
        this.history.push(v);
        // 爆発前と爆発後で履歴の長さを変える
        const historyLength = this.exploded ? 10 : HISTORY_LENGTH;
        // 履歴の長さが最大を超えたら古い履歴を削除
        if (this.history.length > historyLength) {
            this.history.shift();
        }
    }

    // パーティクルとその残像を表示する
    display() {
        // 親クラスのdisplayメソッドを呼び出し
        super.display();
        // 残像の表示
        for (let i = 0; i < this.history.length; i++) {
            let pos = this.history[i];
            // 透明度の計算に this.lifespan を加味
            let alpha = 255 * (i / this.history.length) * (this.lifespan / 255);
            if (this.exploded) {
                this.p.stroke(this.hue, 100, 100, alpha);
                this.p.point(pos.x, pos.y);
            } else {
                this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, alpha);
                this.p.point(pos.x, pos.y);
            }          
        }
    }
}

class Firework {
    constructor(p, x, y) {
     //省略
      //打ち上げ時は、残像を残したいのでTrailParticleにする
      this.firework = new TrailParticle(p, x, y, false, INITIAL_HUE);
    //省略
    }
    //省略
 }

複数のパーティクルグループを作成して時間差で爆発させる
1、(任意)クリックしたら既存の花火をリセットして、新しい花火を打ち上げる
*多数のパーティクルが生成されると処理が重くなるため
2、Fireworkクラスを改良して、単一の爆発用パーティクルグループを生成するメソッドと、複数の爆発用パーティクルグループをセットするメソッドを追加
3、時間差で爆発される(lifespanを閾値にする)
Particleクラス
lifespanWatcherメソッドを追加
*lifespanを監視する
最初のフレームでの描画はスキップする為のフラグ(hasUpdated)を追加
*初期位置で点が描画されないようにするため
Fireworkクラス
particleGroups配列のループ処理
パーティクルグループのすべてのパーティクルが閾値以上の場合はbreakでループを抜ける
寿命が尽きたパーティクルは削除
パーティクルすべて消えたパーティクルグループはparticleGroups配列から削除

const sketch = (p) => {
  p.setup = () => {
 //省略
  const createFirework = (p, x, y) => {
    //既存の花火をリセット
    fireworks.length = 0; 
    fireworks.push(new Firework(p, x, y));
  };
 //省略
};
new p5(sketch);

class Particle {
     constructor(p, x, y, exploded, hue){
      //省略
      this.hasUpdated = false; //最初のフレームでの描画をスキップするためのフラグを追加
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
   update() {
      //省略
      if (this.exploded) {
        this.hasUpdated = true; //爆発後trueに設定
        //省略
      }
    }
   display() {
       if (this.exploded) {
          if(this.hasUpdated){ //ここでhasUpdatedをチェック
            this.p.stroke(this.hue,100, 100, this.lifespan);
            this.p.strokeWeight(3);
            this.p.point(this.position.x, this.position.y);
         }
       } else {
            this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, this.lifespan);
            this.p.strokeWeight(4);
            this.p.point(this.position.x, this.position.y);
       }
    }
   //省略

   //lifespanWatcherメソッドを追加
    lifespanWatcher() {
        return this.exploded && this.lifespan <= 0;
    }
}

class Firework {
    constructor(p, x, y) {
      this.p = p;
      this.firework = new TrailParticle(p, x, y, false, INITIAL_HUE);
      this.hue = Math.round(p.random(0, 36)) * 10;
      this.exploded = false;
    //this.particles = []; 不要
    // 爆発用パーティクルグループを格納する配列
      this.particleGroups = [];
    }

    //explode() {}  explodeメソッドは不要

    // 一度に複数の爆発用パーティクルグループをセットするメソッド (テスト用に3回)
    setupExplosions() {
      this.createParticles();
      this.createParticles();
      this.createParticles();
    }
    // 単一の爆発用パーティクルグループを生成するメソッド
    createParticles() {
        const particles = [];
        for (let i = 0; i < 100; i++) {
          let particle = new Particle(this.p, this.firework.position.x, this.firework.position.y, true, this.hue);
          particles.push(particle);
        }
        //particleGroups配列に追加
        this.particleGroups.push({ particles: particles });
    }
  // 花火の更新処理
    update() {
        if (!this.exploded) {
            this.firework.update();
            if (this.firework.velocity.y >= 0) {
              //  this.explode(); 不要
                this.exploded = true;
              // 複数の爆発用パーティクルを生成
                this.setupExplosions();
            }
        } else { 
          //particleGroupsをループ処理
          for (let i = 0; i < this.particleGroups.length; i++) {
              let particles = this.particleGroups[i].particles;
              let belowThresholdCount = 0;
              for (let j = particles.length - 1; j >= 0; j--) {
                 let particle = particles[j];
                  particle.update();
                  //lifespanが閾値以下になったらカウントしてbreakしないようにする
                  if (particle.lifespanWatcher()) { 
                     belowThresholdCount++; 
                  }
                  //寿命が尽きたパーティクルは削除
                  if (particle.isDone()) {  
                      particles.splice(j, 1); 
                  }
              }
            // すべてのパーティクルが閾値以上の場合
            if (belowThresholdCount === 0) { 
                break; //ループを抜ける
            }
          }
          // パーティクルがすべて消えたグループはparticleGroupsから削除する
          this.particleGroups = this.particleGroups.filter(group => group.particles.length > 0);
      }
    }
  // 花火の描画処理
    display() {
        if (!this.exploded) {
            this.firework.display();
        } else {
        //particleGroupsをループ処理
          for (let i = 0; i < this.particleGroups.length; i++) {
            let particles = this.particleGroups[i].particles;
            for (let j = 0; j < particles.length; j++) {
                let particle = particles[j];
                particle.display();
            }
         }
      }
    }
    done() {
      // 全ての爆発用パーティクルグループが空になったら終了と判定
      return this.exploded && this.particleGroups.every(particleGroup => particleGroup.particles.length === 0);
    }
  }

1、パーティクルグループのデフォルト設定を管理するためのfireworkSettingsオブジェクトを作成
2、ParticleクラスとTrailParticleクラスでは
コンストラクタ関数にデフォルト引数を設定、fireworkSettingsから取得した値を設定する
4、FireworkクラスのcreateParticles メソッドで「単一のパーティクルグループごと」に「パラメータの上書き」と「爆発時の位置変更」ができるようにする

//形状に基づく速度計算関数
const velocityFunctions = {
    circle: (p, min = 1, max = 15) => {
        return p5.Vector.random2D().mult(p.random(min, max));
    },
    heart: (p, min = 0.5, max = 0.5) => {
        let t = p.random(0, p.TWO_PI);
        let x = 16 * Math.pow(Math.sin(t), 3);
        let y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
        return p.createVector(x, -y).mult(p.random(min, max));
    }
};
const fireworkSettings = {
    threshold: 100,
    count: 150,
    velocityType: 'circle',
    velocityRange: [1, 15],
    velocityMultiplier: 0.9,
    gravity: 0.05,
    lifespanDecrement: 2,
    randomHue: true,
    hueRange: [0, 360],
    saturation: 100,
    brightness:100,
    strokeWeightValue: 4,
    trail: false,
    historyLength: 20,
    updateInterval: 5,
};
class Particle {
    //デフォルト引数を設定
    constructor(p, x, y, exploded =false, hue=INITIAL_HUE, settings=fireworkSettings) {
      // 省略
      this.settings = settings;  //追加
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity(); //この行はconstructor関数の最後にくること
    }
     // 省略
    setExplodedVelocity() {
        //変更
        this.velocity = velocityFunctions[this.settings.velocityType](this.p, ...this.settings.velocityRange);
    update() {
      // 省略
      if (this.exploded) { //爆発後
        this.hasUpdated=true;
        //変更
        this.lifespan -= this.settings.lifespanDecrement;
        this.velocity.mult(this.settings.velocityMultiplier)
         //重力も上書きできるように追加する
        this.gravity = this.p.createVector(0, this.settings.gravity);
      }
    }
    display() {
        if (this.exploded) {
            if(this.hasUpdated){ 
              //変更
               this.p.stroke(this.hue, this.settings.saturation, this.settings.brightness, this.lifespan);
               this.p.strokeWeight(this.settings.strokeWeightValue);
               this.p.point(this.position.x, this.position.y) 
            }
          } 
         // 省略
    }

    // 省略

    lifespanWatcher() {
        //変更
        return this.exploded && this.lifespan <= this.settings.threshold;
    }
  }

class TrailParticle extends Particle {
   //settingsを追加
   constructor(p, x, y, exploded, hue, settings) {
        super(p, x, y, exploded, hue, settings);
        // 省略
    }
   update() {
        super.update();
        if (!this.finishLocation) {
            this.updateCounter++;
            //変更
            if (this.exploded && this.updateCounter % this.settings.updateInterval === 0) {
                this.addHistory();
            }  
           // 省略
       }
    }
    addHistory() {
        // 省略
        //変更
        const historyLength = this.exploded ? this.settings.historyLength : HISTORY_LENGTH;
       // 省略
    }
    display() {
        super.display();
        for (let i = 0; i < this.history.length; i++) {
            // 省略;
            if (this.exploded) { //爆発後
                //変更
                this.p.stroke(this.hue, this.settings.saturation, this.settings.brightness, alpha);
            }   
          // 省略;
        }
    }
}
class Firework {
    constructor(p, x, y) {
      this.p = p;
      this.firework = new TrailParticle(p, x, y);  //デフォルト引数を使用
    //this.hue = Math.round(p.random(0, 36)) * 10; createParticlesメソッドで設定するので不要
    //位置の調整用に、キャンバスのサイズを10分割した単位を作成しておく
      this.xUnit = p.width/10;
      this.yUnit = p.height/10;
     // 省略
    }
    setupExplosions() {
      // createParticlesメソッドの使用例
        this.createParticles();
        this.createParticles({threshold:255, velocityType:'heart', velocityRange:[0.6, 0.6], trail: true, randomHue:false});
        this.createParticles(null, {xOffset: this.xUnit*2, yOffset: this.yUnit });
    }
    /*
    設定をオーバーライドできるように変更
    overrides: 設定を上書きするオブジェクト(引数にオブジェクトを渡すことで順番関係なく上書きするものだけが指定できる) 
    positionOffsets: 位置オフセットを指定するオブジェクト
    */
   createParticles(overrides = {}, positionOffsets = { xOffset: 0, yOffset: 0 }) {
        const particles = [];
         //fireworkSettingsオブジェクトからデフォルト設定を取得
        const defaultSettings = fireworkSettings;
         // デフォルト設定と上書き設定をマージして新しい設定を作成
        const settings = { ...defaultSettings, ...overrides };
         // ランダムな色相を生成するかどうかを設定に基づいて決定
        const randomHue = settings.randomHue ? Math.round(this.p.random(0, 36)) * 10 : null;
         // 爆発位置のX座標とY座標を計算し、キャンバス内に収める
        const baseX = Math.min(Math.max(this.firework.position.x + positionOffsets.xOffset, 0), this.p.width);
        const baseY = Math.min(Math.max(this.firework.position.y + positionOffsets.yOffset, 0), this.p.height);
         // パーティクルを生成し、particles配列に追加
        for (let i = 0; i < settings.count; i++) {
          const particleHue = settings.randomHue ? randomHue : this.p.random(settings.hueRange[0], settings.hueRange[1]);
          const particle = settings.trail ?
              new TrailParticle(this.p, baseX, baseY, true, particleHue, settings) :
              new Particle(this.p, baseX, baseY, true, particleHue, settings);
          particles.push(particle);
         }
        //particleGroups配列に追加
        this.particleGroups.push({ particles:particles });
   }

   // 省略

 }

消える時にピカッとなる演出(フィニッシュ)を追加できるようにする
1、fireworkSettingsにパラメーターを追加
2、インデックスを渡す(表示する数の調整用)
3、Particle クラス
finishLocationにパーティクルが消滅する位置を保存し、この位置にフィニッシュパーティクルを発生させる
4、TrailParticleクラスの調整
finishLocationが設定されている場合、残像効果を無視して特定の位置にフィニッシュパーティクルを表示する

const fireworkSettings = {
       //省略
      finish: false,
      finishThreshold: 100, 
      finishCount: 1, 
      finishColor: true,
      finishHueRange: [0, 360],
      finishSaturation: 50, 
      finishBrightness: 100,
      finishStrokeWeightValue: 4, 
};
class Firework {
   //省略
      update() {
        if (!this.exploded) {
             //省略
        } else {
          //省略
           //  j (インデックス)を渡す(表示するフィニッシュパーティクル数の調整に使用)
              particle.update(j);
         //省略
        }
      }
     //省略
}
class Particle {
    constructor(p, x, y, exploded =false, hue=INITIAL_HUE, settings=fireworkSettings) {
     //省略
      this.finishLocation = null; // finishの位置を格納する変数
      this.hasFinished = false; // lifespanのリセットが繰り返えされないように、finishが一度だけ行われたかどうかを追跡する
      exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
    //省略
    update(j) { //インデックス追加
         //省略
        if (this.exploded) {
            this.hasUpdated=true;
            this.lifespan -= this.settings.lifespanDecrement;
            this.velocity.mult(this.settings.velocityMultiplier)
            this.gravity = this.p.createVector(0, this.settings.gravity);
          //フィニッシュパーティクルを表示する条件を追加
            if (this.settings.finish && this.lifespan <= this.settings.finishThreshold && !this.hasFinished) {
              this.finishLocation = this.position.copy();
              if(this.finishLocation && j % this.settings.finishCount === 0){
                this.lifespan = 255;
              }
             this.hasFinished = true;
            }
   
        }
    }

   display() { 
        if (this.finishLocation) {
            const finishHue = this.settings.finishColor ? this.hue : this.p.random(this.settings.finishHueRange[0], this.settings.finishHueRange[1]);
            this.p.stroke(finishHue, this.settings.finishSaturation, this.settings.finishBrightness, this.lifespan);
            this.p.strokeWeight(this.settings.finishStrokeWeightValue);
            this.p.point(this.finishLocation.x, this.finishLocation.y);
        }
        // 爆発後のパーティクルの表示
        else if (this.exploded) {
            if(this.hasUpdated){
                this.p.stroke(this.hue, this.settings.saturation, this.settings.brightness, this.lifespan);
                this.p.strokeWeight(this.settings.strokeWeightValue);
                this.p.point(this.position.x, this.position.y)
          }
        }
        // 打ち上げ中のパーティクルの表示
        else {
            this.p.stroke(INITIAL_HUE, INITIAL_SATURATION, INITIAL_BRIGHTNSS, this.lifespan);
            this.p.strokeWeight(4);
            this.p.point(this.position.x, this.position.y);
        }
   }

  //省略
}
class TrailParticle extends Particle {
  //省略
    update(j) { //インデックスを追加
        super.update(j); //インデックスを追加
     //条件ブロック追加(finishLocationが設定されていない場合にのみ履歴を更新)
        if (!this.finishLocation) {
            this.updateCounter++; 
            if (this.exploded && this.updateCounter % this.settings.updateInterval === 0) {
                this.addHistory();
            } else {
                this.addHistory();//打ち上げ時
            }
       }
    }
  //省略
    display() {
     // finishLocationが設定されている場合、その位置にピカピカ光る表示を行う
        if (this.finishLocation) {
            const finishHue = this.settings.finishColor ? this.hue : this.p.random(this.settings.finishHueRange[0], this.settings.finishHueRange[1]);
            this.p.stroke(finishHue, this.settings.finishSaturation, this.settings.finishBrightness, this.lifespan);
            this.p.strokeWeight(this.settings.finishStrokeWeightValue)
            this.p.point(this.finishLocation.x, this.finishLocation.y);
        } else { 
            super.display(); //基本的な表示ロジックは継承
            //以下省略 
  
        }

    }
}

追記:フィニッシュを表示するときの不具合を修正

class Particle {
  constructor(p, x, y, exploded =false, hue=INITIAL_HUE, settings = fireworkSettings) {
    //省略
    // 10%の確率でリセットをスキップ
    this.finishSkipped = Math.random() < 0.1;
    exploded ? this.setExplodedVelocity() : this.setInitialVelocity();
    }
   //省略
   update(j) {
     //省略
        if (this.exploded) {
            //省略
            if (this.settings.finish && this.lifespan <= this.settings.finishThreshold && !this.hasFinished) {
                this.finishLocation = this.position.copy();
                // if(this.finishLocation && j % this.settings.finishCount === 0){
                //  this.lifespan = 255
                // }
             //ここを修正 finishCountが1の場合、粒子数は90%程度にする
                if (this.finishLocation && j % this.settings.finishCount === 0) {
                     // 10%のパーティクルだけリセットをスキップ
                    if (this.settings.finishCount === 1 && !this.finishSkipped) {
                        this.lifespan = 255;
                    } else if (this.settings.finishCount !== 1) {
                        this.lifespan = 255;
                    }
                }
             this.hasFinished = true;
            }
        }
    }
    //省略
    isDone() {
        // return this.lifespan <= 0;
        //ここを修正   finishがtrueの場合のパーティクルの寿命を伸ばす
        const adjusted = !this.settings.finish ? 0 : 0 - (255 - this.settings.finishThreshold);
        return this.lifespan <= adjusted;
    }
    //省略
}

Fireworkクラスを継承するクラスを作成、 setupExplosions メソッドを上書きしていろいろなタイプの花火を作成
クリック時に継承クラスを順番に切り替えて表示する

let fireworkIndex = 0; //追加

const sketch = (p) => {
 //省略
  const createFirework = (p, x, y) => {
    fireworks.length = 0;
    // fireworkListに使用する継承クラスを追加していく
    const fireworkList = [Firework1];
    // インデックスを使用して花火のクラスを選択
    const fireworkClass = fireworkList[fireworkIndex];
    fireworks.push(new fireworkClass(p, x, y));
    // インデックスを更新して、次のクリックで次の花火が表示されるようにする
    fireworkIndex = (fireworkIndex + 1) % fireworkList.length;
  };
 //省略
};
new p5(sketch);

//FireworkクラスのsetupExplosionsは空にする
class Firework {
   //省略
    setupExplosions() {}
   //省略
 }  

//継承クラスの例
class Firework1 extends Firework {
    setupExplosions() {
       this.createParticles();
    }
}