Help us understand the problem. What is going on with this article?

花火の作り方

突然なんですけども去年の

Leverages 12日目のAdvent CalendarのAngular記事
Leverages 14日目のAdvent Calendarのkotlin記事

のプログラムと解説記事を寄稿したの実は私でした。そろそろ、こんな記事が投稿されていることなど忘れ去られてる頃だと思いますので、良かったら見てみてください。

これらの記事でkotlinの方のPVは当時Google様がkotlinを正式に開発言語としてサポートすると発表があった当時話題になったのでそれなりに見られていたようで嬉しかったです。なお、全てのコード順番にファイルにコピペするだけで動くように調整して、動作確認超念入りに行なった(テスト書いてない時点で念入りなのかということについては議論の余地がありますが、ご容赦ください だって非同期テストとかとってもめんどくさ)

初心者入門と言いつつ結構踏み込んだ実装をしていたり、長いし、読むの疲れるし、難しいかもしれないし。読まれないだろーなー読んでも作って貰うところまではいかないーだろーなーとは思いつつ、丁寧に書いたので、記事の節々に一応技術的な価値は担保できてるんじゃないかなーと思っています。

ただ、ぶっちゃけ勉強はそこそこしたもののkotlinやAngular開発に最強に詳しい訳ではないので、その方面の人からマサカリ飛んでくるだろーなーと戦々恐々としてたところ、思ったほど飛んでこなかったため、これ全然見られてないんじゃね?的な不安が過りつつも、興味を持って質問していただける方がいたりマサカリ飛ばしてくれる方がいたことに深い感謝を感じました。

そんなMaking秘話もありつつ、実はAngularの記事に一箇所だけ実装について解説していない箇所があります。それは・・・

fireworks.gif

つまり・・・、

fireworks.gif

おまけの・・・、

fireworks.gif

これですね。

今回は綺麗な花火を打ち上げたいあなたのために、この記事で使った花火の実装の仕組みを解説しちゃいます。なお、花火 jsやらfireworks jsやらで検索かけるといっぱい引っかかることから解るように、やり方は一つだけではなかったりするため、もっと賢い花火の作り方を知っている方は容赦無くマサカリをぶん投げていただければ幸いです。というより本番用のコードはおまけであり、寧ろ色々雑に作ったところがあるので、解説用は少し修正しています。

ソースコードはこの辺

Color.tsについては、RGBとHSLを扱うための変換コードなので特に解説はしません。他のライブラリを用いても良いです。

fireworks.ts
import { RGB, HSL, Color } from './color';

const QTY = 360;
const SIZE = 2.0;
const DECAY = 0.98;
const PSEUDO_GRAVITY = 1.5;

class Spark {
  public static radian = Math.PI * 2;
  public size = SIZE;
  static generateSpark(x, y) {
    const theta   = Math.random() * Spark.radian;
    const velocity = Math.random() * 5;
    return new Spark(
      x,
      y,
      Math.cos(theta) * velocity,
      Math.sin(theta) * velocity,
      Color.randHsl(100, 90, 55, 45, 1.0, 0.9).toString(),
      velocity,
      Math.random() > 0.5
    );
  }
  constructor(
    private posX: number,
    private posY: number,
    private velX: number,
    private velY: number,
    private col: string,
    private velocity: number,
    private sw: boolean
  ) {}
  addVelocity() {
    this.posX += this.velX;
    this.posY += this.velY;
    return this;
  }
  computeDecay(d) {
    this.velX *= d;
    this.velY *= d;
    this.size *= d;
    return this;
  }
  computePseudoGravity(g) {
    this.posY += g;
    return this;
  }
  updateNextTick() {
    this.addVelocity();
    this.computeDecay(DECAY);
    this.computePseudoGravity(PSEUDO_GRAVITY);
  };
}

class FireWorksRenderrer {
  public cvs;
  public ctx;
  public fws;
  width;
  height;
  left;
  top;
  fps = 0;
  constructor(cvs) {
    this.cvs = cvs || document.getElementsByTagName('canvas');
    this.ctx = cvs.getContext('2d');
    this.width = this.ctx.canvas.width;
    this.height = this.ctx.canvas.height;
    this.left = this.ctx.canvas.getBoundingClientRect().left;
    this.top = this.ctx.canvas.getBoundingClientRect().top;
    this.fws = [];
  }
  draw(spark) {
    this.ctx.beginPath();
    this.ctx.arc(spark.posX, spark.posY, spark.size, 0, Spark.radian, true);
    if ( spark.sw ) {
      this.ctx.fillStyle = "#FFFFFF";
      spark.sw = false;
    } else {
      this.ctx.fillStyle = spark.col;
      spark.sw = true;
    }
    this.ctx.fill();
  }
  public explode(x, y) {
    x -= this.left;
    y -= this.top;
    for (let i = 0; i < QTY; i += 1) {
      this.fws.push(Spark.generateSpark(x, y));
    }
  }
  public update() {
    let len = this.fws.length;
    for ( let i = len - 1 ; i >= 0; i--) {
      const s = this.fws[i];
      s.updateNextTick();
      if ( s.size < 0.1 || s.posX < 5 || s.posX > this.width || s.posY > this.height) {
        this.fws.splice(i, 1);
        len -= 1; //処理には関係ないが念の為、切り取った分を減らしておく
      }
    }
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
    this.ctx.fillRect(0, 0, this.width, this.height);
  }
  public render() {
      const len = this.fws.length;
      this.fps += 1;
      for (let i = 0; i < len; i += 1) {
        const s = this.fws[i];
        this.draw(s);
      }
  }
}

export {Spark, FireWorksRenderrer}

花火の動きは

this.addVelocity();
this.computeDecay(DECAY);
this.computePseudoGravity(PSEUDO_GRAVITY);

花火一個一個の移動を以上のように3つのプロセスに分けてプログラミングしています。今回は物理現象はあくまで模擬的にしてあるため、ほぼ、実際の物理現象とは乖離しています。addVelocityでは現在の速度分ポジションをずらし、computeDecayでは速度を減速させ、computePseudoGravityでは、落下方向へのポジション移動を行っています。火花の質量や速度の単位が計測不能なので、重力は適当です。

Sparkオブジェクトを初期値として用意してあげて、制限時間いっぱいで描画して上げれば、火花の動きがプログラミングできます。
初期化の場合は、以下の値をランダムに決めておきます。

  • 初期火花の中心地
  • 火花ごとの移動方向
  • 火花ごとの移動速度
  • 発光状態のフラグ

これによって、中心地からそれぞれの火花の移動速度と方向が違うため、火花が散らばっているように見せることができます。
描画はact関数を使い

this.ctx.arc(spark.posX, spark.posY, spark.size, 0, Spark.radian, true);

移動方向に弧を描いてるように見せます。これは、

class FireWorksRenderrer

に対応していますが、

次にこれらのSparkオブジェクトを用いてrederを行ってくれるrenderrerを作ります。
renderrerでは、

  • 一定時間ごとに新しい花火の中心地を作成(exploade)
  • 現在の状態を描画(render)
  • 花火の移動を更新(update)

メソッドをタイマーで呼び出しています。このとき、それぞれの火花をすべて同じ配列で管理していますが、分けて実行するようにしても構いません。
60FPSに設定して1000 / 60にしています。renderのintervalを0にしておいているのは、常にupdateの更新があったら割り込むためです。

listener.ts
import { FireWorksRenderrer, Spark } from './fireworks'

function _cvs(cvs) {
    if ( typeof cvs === 'string') {
      cvs = document.querySelector(cvs);
    }
    return cvs;
}

export default function(cvs, milliseconds) {
  return new Promise((resolve, reject) => {
    const fwr = new FireWorksRenderrer(_cvs(cvs) );
    const timers = [
      setInterval(function(){
        fwr.explode(fwr.width * Math.random(), fwr.height * Math.random());
      } , 1000),
      setInterval(fwr.render.bind(fwr), 0),
      setInterval(fwr.update.bind(fwr), 1000 / 60 )
    ];
    setTimeout(
      () => timers.forEach ((id) => {
        fwr.ctx.clearRect(0, 0, fwr.width, fwr.height)
        clearInterval( id as number );
        resolve(cvs);
    }), milliseconds);
  });
}

あとは、canvas要素を取得して、描画を行える用にトリガーを作りましょう。
この関数をimportしてFireworksとして実行させてみましょう。

sample.ts
 FireWorks('canvas', 4000);

のようにmilliseccondsで花火の継続時間を設定してあげれば終了です。
コツさえ覚えれば簡単だったのではないでしょうか?

たまにはこんな息抜きプログラミングもどうぞ。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away