夏なのでJavaScriptのCanvas APIを使って花火の残像も含めて再現しました。
ウインドウを最大化して全画面で見るといい雰囲気です。
やっていること
- 花火の1つ1つの点は、実際には小さな6角形を描画している
- 色をランダムにしただけだが、それっぽく見える
- 花火は全てEffectクラスのインスタンスを配列に格納して管理
- 画面から消えたEffectクラスはきちんと削除する(isAliveプロパティで判断)
- キャンバスのglobalAlphaプロパティを使うと残像を表現できる
ソースコード
index.html
1 | <!DOCTYPE html> |
2 | < html > |
3 | < head > |
4 | < meta charset = "UTF-8" > |
5 | < meta http-equiv = "X-UA-Compatible" content = "IE=edge" > |
6 | < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > |
7 | < link rel = "stylesheet" href = "style.css" > |
8 | < script src = "Functions.js" type = "text/javascript" ></ script > |
9 | < script src = "Effect.js" type = "text/javascript" ></ script > |
10 | < script src = "main.js" type = "text/javascript" ></ script > |
11 | < title >fire works</ title > |
12 | </ head > |
13 | < body > |
14 | < div id = "wrapper" > |
15 | < canvas id = "canvas" width = "" height = "" ></ canvas > |
16 | </ div > |
17 | </ body > |
18 | </ html > |
style.css
主にキャンバスを端末の全画面で表示するための設定です
1 | /* style.css */ |
2 | @charset "utf-8" ; |
3 |
4 | *{ |
5 | margin : 0 ; |
6 | padding : 0 ; |
7 | } |
8 |
9 | html, body, #wrapper{ |
10 | width : 100% ; |
11 | height : 100% ; |
12 | } |
13 |
14 | #canvas{ |
15 | display : block ; |
16 | } |
Function.js
指定範囲の乱数生成、ランダムカラー生成、6角形の描画用など花火を描画するのに利用した関数群です
1 | // 指定範囲の乱数を生成する関数(min~max) |
2 | function randInt(min, max){ |
3 | return Math.floor(Math.random() * (max+1-min)+min); |
4 | } |
5 |
6 | // ランダムカラー文字列を生成して返すアロー関数式 |
7 | const getRandomColor = ()=>{ |
8 | const get256 = ()=>{ return Math.floor(Math.random()*256); }; // 0 ~ 255を返す |
9 | let [r, g, b] = [get256(), get256(), get256()]; // ランダムでRGBカラーを設定 |
10 | let color = `rgb(${r}, ${g}, ${b})`; // 文字列生成 'rgb(XX, XXX, XXX)' |
11 | return color; |
12 | }; |
13 |
14 | // オブジェクトを消去 |
15 | function deleteObjects(){ |
16 | // エフェクト |
17 | for (let i in effects){ |
18 | if (!effects[i].isAlive) effects.splice(i, 1); |
19 | } |
20 | } |
21 |
22 | // 多角形を塗りつぶし描画する関数(頂点の数、中心x座標、中心y座標、半径、色) |
23 | const fillPolygon = function (n, cx, cy, r, tilt, color){ |
24 | const p = Math.floor(360 / n) |
25 | let theta = -90 + tilt; // 角度修正(キャンバスでは3時方向が0度扱いのため12時方向を0度とする) |
26 | let polygon = []; |
27 |
28 | while (theta<360-90){ // 全ての頂点を求める |
29 | const pos = { |
30 | x: r * Math.cos(theta*Math.PI/180) + cx, |
31 | y: r * Math.sin(theta*Math.PI/180) + cy, |
32 | }; |
33 | polygon.push(pos); |
34 | theta += p; // 次の点の位置 |
35 | } |
36 |
37 | // 塗りつぶし多角形を描画する |
38 | g.fillStyle = color; |
39 | g.beginPath(); |
40 | for (let i=0; i<polygon.length; i++){ |
41 | if (i==0){ |
42 | g.moveTo(polygon[i].x, polygon[i].y); |
43 | } |
44 | else { |
45 | g.lineTo(polygon[i].x, polygon[i].y); |
46 | } |
47 | } |
48 | g.closePath(); // パスを閉じる |
49 | g.fill(); |
50 | } |
Effect.js
花火の1つの点を表現するためのクラスです
1 | /* Effect.js : エフェクトクラス*/ |
2 | |
3 | class Effect{ |
4 | // コンストラクタ(x座標, y座標, 発射角度, 速度, 落ちる速度, 色) |
5 | constructor(x, y, angle, speed, fallSpeed, color){ |
6 | this .x = x; // x座標 |
7 | this .y = y; // y座標 |
8 | this .angle = angle; // 発射角度 |
9 | this .speed = speed; // 速度 |
10 | this .fallSpeed = fallSpeed; // 落ちる速度 |
11 | this .color = color; // 色 |
12 | this .isAlive = true ; |
13 |
14 | // 発射角度と速度からxy方向の速度計算 |
15 | this .vx = this .speed * Math.cos( this .angle / 180 * Math.PI); |
16 | this .vy = this .speed * Math.sin( this .angle / 180 * Math.PI); |
17 | } |
18 |
19 | // 移動メソッド |
20 | move(){ |
21 | this .x += this .vx; |
22 | this .vy += this .fallSpeed; |
23 | this .y += this .vy; |
24 |
25 | // 画面から消えたら消去 |
26 | if ( this .y < 0 || this .y > canvas.height || this .x < 0 || this .x > canvas.width){ |
27 | this .isAlive = false ; |
28 | } |
29 | } |
30 | |
31 | // 描画メソッド |
32 | draw(){ |
33 | fillPolygon(6, this .x, this .y, 3, 0, this .color); |
34 | } |
35 |
36 | // 更新処理 |
37 | update(){ |
38 | this .move(); |
39 | this .draw(); |
40 | } |
41 | } |
main.js
全体を管理しています
1 | // main.js |
2 | const SKY_COLOR = "#003" ; // 夜空の色 |
3 | let wrapper = null ; |
4 | let canvas = null ; |
5 | let g = null ; |
6 | let effects = []; // エフェクト格納用配列 |
7 | let frameCount = 0; // フレーム数 |
8 |
9 | // 描画更新処理 |
10 | function mainLoop(){ |
11 | // 一定間隔でエフェクトを生成 |
12 | if (frameCount % 60 == 0){ |
13 | // エフェクト生成 |
14 | const x = randInt(0, canvas.width); |
15 | const y = randInt(0, canvas.height); |
16 | const speed = randInt(1, 5); |
17 | const [color1, color2, color3, color4] = [getRandomColor(), getRandomColor(), getRandomColor(), getRandomColor()]; |
18 |
19 | for (let angle of [0, 30, 60, 90, 120, 150, 180, -30, -60, -90, -120, -150]){ |
20 | const effect = new Effect(x, y, angle, speed, 0.05, color1); |
21 | effects.push(effect); |
22 | |
23 | const effect2 = new Effect(x+10*Math.cos(angle/180*Math.PI), y+10*Math.sin(angle/180*Math.PI), angle, speed, 0.05, color2); |
24 | effects.push(effect2); |
25 |
26 | const effect3 = new Effect(x+25*Math.cos(angle/180*Math.PI), y+25*Math.sin(angle/180*Math.PI), angle, speed, 0.05, color3); |
27 | effects.push(effect3); |
28 | |
29 | const effect4 = new Effect(x+50*Math.cos(angle/180*Math.PI), y+50*Math.sin(angle/180*Math.PI), angle, speed, 0.05, color4); |
30 | effects.push(effect4); |
31 | |
32 | const effect5 = new Effect(x+70*Math.cos(angle/180*Math.PI), y+70*Math.sin(angle/180*Math.PI), angle, speed, 0.05, color1); |
33 | effects.push(effect5); |
34 | |
35 | } |
36 | } |
37 |
38 | // キャンバスクリア(残像を利用) |
39 | g.globalAlpha = 0.1; |
40 | g.fillStyle = SKY_COLOR; |
41 | g.fillRect(0, 0, canvas.width, canvas.height); |
42 | g.globalAlpha = 1; |
43 |
44 | // エフェクト描画を更新 |
45 | for (let effect of effects){ |
46 | effect.update(); |
47 | } |
48 |
49 | // オブジェクト消去 |
50 | deleteObjects(); |
51 |
52 | // フレームカウント |
53 | frameCount++; |
54 |
55 | // フレーム毎に再帰呼び出し |
56 | requestAnimationFrame(mainLoop); |
57 | } |
58 |
59 | /* |
60 | * キャンバスのサイズをウインドウに合わせて変更 |
61 | */ |
62 | function getSize(){ |
63 | // キャンバスのサイズを再設定 |
64 | canvas.width = wrapper.offsetWidth; |
65 | canvas.height = wrapper.offsetHeight; |
66 | } |
67 | |
68 | /* |
69 | * リサイズ時(キャンバスの中心と時計の縮尺を再設定) |
70 | */ |
71 | window.addEventListener( "resize" , function (){ |
72 | getSize(); |
73 | }); |
74 |
75 | window.addEventListener( "load" , ()=>{ |
76 | wrapper = document.getElementById( "wrapper" ); |
77 | // キャンバス取得 |
78 | canvas = document.getElementById( "canvas" ); |
79 | g = canvas.getContext( "2d" ); |
80 |
81 | // キャンバスをウインドウサイズに設定 |
82 | getSize(); |
83 |
84 | frameCount = 0; |
85 |
86 | // 描画更新処理を開始 |
87 | mainLoop(); |
88 | }); |
本物の花火の中では、真っ白でゆっくり下に消えながら落ちていくしだれ柳みたいな花火が個人的に一番好きです。
コメント