HTML5のCanvasでのグロー表現を使って花火っぽい何かを打ち上げる

もともとグロー表現が好きなのもあり、canvasでグロー効果ってどうやるんだろうって始まったのが2、3年くらい前でした。

いろんなコードを見てても、決定的な参考例が中々見つからずだったのですが・・・・その後も「そういえばグロー効果を探さなきゃ」と思い出す度に、インターネット上を飢えた狼のように探してたら、以下の記事を見つけることができました。

Ambient Canvas Backgrounds

表現が綺麗で、感動して、「これどうやっているのかな?」と思ってコードを見てみると、非常に参考になりました。これを元に色んなグロー表現のコードを書いて遊んでいたのですが、そのうちの一つが以下の花火っぽいものを打ち上げるコードです。以下のコードは物理シミュレーションなどをしているわけではなく、花火っぽい感じにしているので、花火っぽいと表現しています。後々、花火関連の物理を取り入れてちゃんとしたものも作ってみたいですね。

今回のグロー表現ですが、使用しているテクニックは、IEとSafariでは、機能しません。IEはともかく、Safariで使えないのは残念ですね。blurやbrightnessを自分で実装すればいけるかもしれませんが。

See the Pen
Canvas Fireworks
by Shunichi Fujita (@funnydreamer)
on CodePen.

グロー効果

上記の参考記事のグロー表現はどうやっているのかというと、大まかにいうと以下のようになっています。

  1. canvasを2枚(AとBとします)用意します。Aは、オフスクリーン用(裏で処理する用)で、ページのHTML上には追加せず、BのみをHTML上(今回は)に追加しています。
  2. Aの方に、花火の毎フレームごとの絵を描きます。(AはHTML上に無いのでブラウザ上では見えません)
  3. Bにぼかし効果と、明度アップ効果を設定します。これは、CanvasRenderingContext2Dにfilterプロパティーがあり、ctx.filter=’blur(8px) brightness(200%)’という書き方で、設定できます。cssのfilterに記述の仕方も、効果も似ています。
  4. BにCanvasRenderingContext2DのglobalCompositeOperationプロパティを’lighter’に設定します。これは合成処理の指定で、ligherは、canvasに上書きする時に、もともとある絵に、カラーを加算するという指定になるので、色を加算すると白に近くなるため明るくなります。
  5. Bにぼかし効果と明度アップ効果が設定されている時にAの内容をBに描画すると、Aの絵が、明るくぼけた感じでBに描かれます。もし、もともと絵があるところに、上書きする場合は、lighter効果で明るくなります。
  6. ぼかしの値(filter=’blur(8px) brightness(200%)’)を変えて、もういちど、Aの絵をBに描きます。こうすることでよりグロー効果を際立たせています。
  7. 現在Bにはピンぼけした絵しかないため、今度は、Aの絵をそのままBに描きます。

説明だけだと分かりにくいので、下に、サンプルを用意しました。上記の「4」が、「ぼかし(大)適用」に、「5」が「ぼかし(小)適用」、「6」が「通常描画適用」になります。それぞれON/OFFにして、効果の違いをみると、イメージしやすくなるんじゃないでしょうか?

See the Pen
Glow Test
by Shunichi Fujita (@funnydreamer)
on CodePen.

花火っぽいなにかのコードのポイント

だいたい以下のような流れになっています。

  1. 打ち上げ位置をランダムに決定する。
  2. 打ち上げ位置が決まったら、下から上に向かって花火を飛ばす。
  3. 速度が段々ゆっくりになり、フォーエドアウトし、十分速度が遅くなったあたりで、花火を開花させる。
  4. 開花したら、段々ゆっくりになりつつ、下に落ちていきます。
  5. ゆれながらフェードアウトします。

1次元配列

もともとの参考のコード内で一次元配列を使っていますね。花火のサンプルでは、以下のfireArrayの部分です。

private fireCount: number = 256;
private firePropCount: number = 9;
private firePropsLength: number = this.fireCount * this.firePropCount;
private fireArray = new Float32Array(this.firePropsLength);

(略)

this.fireArray.set(
  [x, y, vx, vy, life, ttl, speed, radius, hue],
  i,
);

(略)

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];

一次元配列は管理がしにくくはなりますが、やはり、グラフィック処理のように重い処理は、一次元配列を使った方が、速いです。昔、大学の授業で、JAVAを使い数値解析系の何かの計算を一次元配列バージョンと二次元配列バージョンで比較を行ったのですが、数倍の差で一次元配列バージョンのが早かったです。もし、速度が求められる計算とかをするなら、一次元配列に置き換えて考えると良いです。

画面焼けみたいな跡が残る

this.ctx.a.save();
# this.bgOverwriteColor = 'hsla(260,0%,0%,0.1)'
this.ctx.a.fillStyle = this.bgOverwriteColor;
this.ctx.a.fillRect(0, 0, this.canvas.a.width, this.canvas.a.height);
this.ctx.a.restore();

this.ctx.b.fillStyle = this.bgColor;
this.ctx.b.fillRect(0, 0, this.canvas.a.width, this.canvas.a.height);

上記部分で、bgOverwriteColor(不透明度10%の黒)で毎フレーム、canvas全体を上書きしています。上書きされるたびに、少しずつ色が薄くなり、これにより、残像のような効果が残り尾を引く花火のような表現ができます。本来なら真っ黒になるまで上書きされていくと思っていたのですが、そうはならず、一度色が着いた場所が、rgbにすると(4,4,4)とか値が残り、発色が綺麗なMacBookのディスプレイでみると、白っぽい跡が残ってしまいます。globalCompositeOperationの設定のせいかなと思って変えてみましたが、だめでした。参考元のようにアルファを1で上書きすれば、跡は残りませんが、もちろん、残像効果も残りません。

そこで、以下のようにして、ピクセル単位で、色を調べて、閾値以下だったら、黒にするというような処理を追加しました。

if (this.time % 10 == 0) {
    let imageData = this.ctx.a.getImageData(0, 0, this.canvas.a.width, this.canvas.a.height);
    let data = imageData.data;
    for (let j = 0; j < data.length; j += 4) {
        const sum = data[j] + data[j + 1] + data[j + 2];
        if (sum < 30) {
            data[j] = 0;
            data[j + 1] = 0;
            data[j + 2] = 0;
        }
    }
    this.ctx.a.putImageData(imageData, 0, 0);
}

見た感じ、毎フレーム処理する必要は無さそうだったので、10回に1回、処理をするようにしました。ピクセル単位で処理するのは重いのでなるべく回数は減らしたいですね。今回は、rgbのr,g,bの合計が30以下の時に、r=0,g=0,b=0にしています。もっとスマートな方法もありそうな気がしますが、ひとまず、動作も軽快に跡が消えたので良しとしましょう。もしかすると、m1のMacBook Airだから、重さを感じないだけなのかもしれませんが・・・笑

funnydreamer
栃木生まれのミドルエイジ。フロントエンドとデザインの領域におりましたが、最近はマーケティングやライティングにPythonによる自動化など何でも屋になってきました。趣味は、ゲーム、アニメ、自転車(ポタリング)、カフェ巡り、お絵描きと自称多趣味。ケーキはショートケーキが好物。

ADD COMMENT

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください