はじめに
きっかけは前回記事【HTML&CSS】花火の通り、HTML5プロフェッショナル認定試験の勉強をしている中で、ふと、花火が作れそうだなと思ったことですが、色々探しているうちにすごいキレイな花火がTypeScriptで実装されているのを見つけたので、アレンジさせて頂くことにしました。
アレンジした花火
こんな感じにしてみました!
千輪花火を追加するのが難しかったですが、キレイに描画できたかなと思います。
① 花火の色がランダムに変わるように変更
② 花火が開いた後に千輪花火を追加
See the Pen Firework_ts2 by Nina (@ifnezsju-the-selector) on CodePen.
メソッド・クラス
コード内の主要なメソッドとクラスを説明します。
-
Firework
クラス:-
fireCount
: 花火の粒子の総数。 -
firePropsLength
: 各花火のプロパティの合計数。 -
fireArray
: 花火のプロパティを格納するためのFloat32Array。 -
baseTTL
,rangeTTL
,maxTTL
: 花火の寿命(時間)に関するプロパティ。 -
baseSpeed
,rangeSpeed
: 花火の速度に関するプロパティ。 -
baseRadius
,rangeRadius
: 花火の半径に関するプロパティ。 -
baseHue
: 花火の色相に関するプロパティ。 -
bgColor
: 背景の色。 -
bgOverwriteColor
: 背景の色を上書きする色。 -
fillStyle
: 描画に使用される塗りつぶしのスタイル。 -
canvas
: Canvas要素のコンテナ。 -
ctx
: Canvasのコンテキスト。 -
launch
: 花火の発射ポイントに関するプロパティ。 -
noise
:simplenoise
ライブラリへの参照。
-
-
created
メソッド:- ページの読み込み時に呼び出され、Canvasを作成し、リサイズ処理をセットアップし、花火の初期化を行います。
-
createCanvas
メソッド:- キャンバス要素を作成し、2つのコンテキスト(aとb)を取得します。aコンテキストはアニメーションの描画用、bコンテキストは効果(ぼかしや明るさなど)のために使用されます。
-
resize
メソッド:- ウィンドウのリサイズイベント時に呼び出され、Canvasのサイズを調整し、中央の位置を更新します。
-
getParticleIndex
メソッド:- 花火のプロパティのインデックスを取得します。
-
getInitTTL
,getInitSpeed
,getInitRadius
メソッド:- 花火のプロパティ(寿命、速度、半径)の初期値を取得します。
-
rand
,randIn
メソッド:- ランダムな数値を生成します。
-
fadeInOut
メソッド:- 指定された時間と周期に対するフェードイン/フェードアウトの計算を行います。
-
initLaunch
メソッド:- 花火の発射ポイントの初期化を行います。
-
initThousandPetals
,initParticles
,initParticle
メソッド:- 花火粒子の初期化を行います。
-
drawParticle
メソッド:- 花火粒子を描画します。
-
updateThousandPetals
,updateParticle
メソッド:- 花火粒子の更新を行います。
-
draweThousandPetals
,drawParticles
,drawLaunch
メソッド:- 花火アニメーションの描画を制御します。
-
renderGlow
,renderToScreen
メソッド:- グロー効果を描画し、最終結果をCanvasにレンダリングします。
-
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),
};
}
② 花火が開いた後に千輪の花火を追加
ポイントとなる追加箇所のみ下記に説明します。
- 千輪花火の準備
変数maxX
とisOpen
を追加していますが、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);
}
}
}
- 千輪花火の更新
vx
とvy
に掛ける数は小さい方が花火がゆっくり開き、花火の大きさが小さくなるので、大花火の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を使うとすごいリアルになりますね!
グロー効果というのも初めて知りましたが、とても勉強になりました。
参考になれば幸いです!