煙火與煙


事實上之前的煙火在施放時並沒有煙,只有火花,可以結合兩個粒子系統來製作煙火在燃燒過程中所產生的煙硝軌跡。

在結合兩個粒子系統時,要注意,每個粒子都是獨立的,火花粒子就是火花粒子,不會與煙粒子相互影響,只不過煙粒子產生的位置是火花的目前位置,只要將位置的資訊傳遞給煙粒子物件就可以了。

指定煙粒子的方式採用比較簡單的方式,首先產生足夠的煙粒子,它們的狀態都為false,然後等待重新指定它們為復活狀態,每次繪製完火花後,在一堆煙粒子物件中尋找狀態為false的煙粒子,然後指定新的初始位置給這些粒子,如此煙粒子就會跟隨著火花的軌跡而產生了。

這邊需要之前製作的FireworkParticle與SmokeParticle類別,您可以直接看看 範例
  • FireworkAndSmoke.java
package cc.openhome.particle;

import java.awt.*;
import javax.swing.JApplet;

import static java.lang.Math.*;

public class FireworkAndSmoke extends JApplet implements Runnable {
private final int FIREWORK_MAX = 255;
private final int SMOKE_MAX = FIREWORK_MAX * 10;
private FireworkParticle[] fireworks; // 煙火粒子
private SmokeParticle[] smokes; // 煙粒子
private int xCenter, yCenter;
private Image offScreen;
private Graphics gOffScreen;

public void init() {
setSize(640, 480);
setBackground(Color.black); // 背景為黑色

// 建立粒子
fireworks = new FireworkParticle[FIREWORK_MAX];
smokes = new SmokeParticle[SMOKE_MAX];

// 煙火初始位置
xCenter = (int) (getWidth() / 2 + random() * 150 - 150);
yCenter = (int) (getHeight() / 2 + random() * 150 - 150);

for (int i = 0; i < FIREWORK_MAX; i++) {
fireworks[i] = new FireworkParticle();
}
for (int i = 0; i < SMOKE_MAX; i++) {
smokes[i] = new SmokeParticle();
}

// 建立次畫面
offScreen = createImage(getWidth(), getHeight());
gOffScreen = offScreen.getGraphics();
}

public void start() {
(new Thread(this)).start();
}

public void update(Graphics g) {
paint(g);
}

public void paint(Graphics g) {
g.drawImage(offScreen, 0, 0, this);
}

public void run() {
while (true) {
boolean replay = true;

for (FireworkParticle firework : fireworks) {
if (firework.isAlive()) {
replay = false;
break;
}
}

// 是否重新施放
if (replay) {
for (FireworkParticle firework : fireworks) {
firework.resume(xCenter, yCenter, FIREWORK_MAX);
firework.setLife((int) (random() * 20));
}
}

gOffScreen.clearRect(0, 0, getWidth(), getHeight());

for(SmokeParticle smoke : smokes) {
if (smoke.isAlive()) {
double sx = smoke.getPoint().getX();
double sy = smoke.getPoint().getY();
gOffScreen.setColor(smoke.getColor());
gOffScreen.fillOval((int) sx, (int) sy, 4, 4);
smoke.nextState();
}
}

for (FireworkParticle firework : fireworks) {
if (firework.isAlive()) {
double x = firework.getPoint().getX();
double y = firework.getPoint().getY();

gOffScreen.setColor(firework.getColor());
gOffScreen.fillOval((int) x, (int) y, 3, 3);

// 為煙火加上煙
for(SmokeParticle smoke : smokes) {
if (!smoke.isAlive()) {
smoke.setLife((int) (50 * random()));
smoke.resume((int) x, (int) y);
break;
}
}

firework.nextState();
}
}

// 重繪畫面
repaint();

// 暫停執行緒 150 毫秒
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

以下是使用HTML5 Canvas的方式(如果瀏覽器支援HTML5 Canvas,例如最新版的Firexfox、Chrome、IE9等,可以直接將下面的內容存為HTML或按下檔名連結,直接載入瀏覽器執行觀看結果:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=Big5" http-equiv="content-type">
<script type="text/javascript">
window.onload = function() {
function Color(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
this.toString = function() {
return 'rgb(' +
[this.r, this.g, this.b].join() + ')';
};
}

function Point(x, y) {
this.x = x || 0;
this.y = y || 0;
this.setLocation = function(x, y) {
this.x = x;
this.y = y;
};
}

function FireworkParticle() {
var LIFE_LESS_5 = new Color(255, 0, 0);
var LIFE_LESS_2 = new Color(0, 0, 255);
var position = new Point(); // 粒子的位置
var vx = vy = 0; // 粒子的速度
var color = null; // 粒子的顏色
var time = 0; // 粒子存活至今的時間
this.life = 0; // 粒子的生命值

this.resume = function(x, y, max) {
position.setLocation(x, y);
vx = Math.random() * max - Math.random() * max;
vy = Math.random() * max - Math.random() * max;
color = new Color(parseInt(Math.random() * 255),
parseInt(Math.random() * 255),
parseInt(Math.random() * 255));
time = 0;
};

this.isAlive = function() {
return this.life > 0;
};

this.getPoint = function() {
return position;
};

this.getColor = function() {
return color;
};

this.nextState = function() {
vy += 9.8 * time;
position.setLocation(
position.x + vx * 0.1,
position.y + vy * 0.01);
this.life--;
time++;
if (this.life < 2) {
color = LIFE_LESS_2;
} else if(this.life < 5) {
color = LIFE_LESS_5;
}
};
}

function SmokeParticle() {
var position = new Point(); // 位置
var vx = vy = 0; // 水平與垂直速度
this.weight = 0; // 重量
this.life = 0; // 生命週期

this.resume = function(x, y) {
position.setLocation(x, y);
vx = 0;
vy = -1;
this.weight = 10 * Math.random() + 1;
};

this.getPoint = function() {
return position;
};

this.getColor = function() {
return new Color(this.life, this.life, this.life);
};

this.isAlive = function() {
return this.life > 0;
};

this.nextState = function() {
position.setLocation(position.x, position.y + vy);
this.life--;
};
}

var canvas1 = document.getElementById('canvas1');
var canvas2 = document.getElementById('canvas2');
var context1 = canvas1.getContext('2d');
var context2 = canvas2.getContext('2d');


var FIREWORK_MAX = 200;
var SMOKE_MAX = FIREWORK_MAX * 10;

var fireworks = []; // 建立粒子

for(var i = 0; i < FIREWORK_MAX; i++) {
fireworks[i] = new FireworkParticle();
}

var smokes = [];
for(var i = 0; i < SMOKE_MAX; i++) {
smokes[i] = new SmokeParticle();
}

var context = context2;
setTimeout(function() {
var replay = true;
for(var i in fireworks) {
var firework = fireworks[i];
if(firework.isAlive()) {
replay = false;
break;
}
}

// 是否重新施放
if(replay) {
for(var i in fireworks) {
// 煙火初始位置
var xCenter = canvas1.width / 2 +
Math.random() * 150 - 150;
var yCenter = canvas1.height / 2.5 +
Math.random() * 150 - 150;
var firework = fireworks[i];
firework.resume(xCenter, yCenter, FIREWORK_MAX);
firework.life =
parseInt(Math.random() * 20);
}
}

context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(0, 0, canvas1.width, canvas1.height);

for(var i in smokes) {
var smoke = smokes[i];
if(smoke.isAlive()) {
var sx = smoke.getPoint().x;
var sy = smoke.getPoint().y;
context.fillStyle = smoke.getColor().toString();
context.beginPath();
context.arc(sx, sy, 2, 0, 2 * Math.PI, true);
context.closePath();
context.fill();
smoke.nextState();
}
}

for(var i in fireworks) {
var firework = fireworks[i];
if(firework.isAlive()) {
var x = firework.getPoint().x;
var y = firework.getPoint().y;

context.fillStyle =
firework.getColor().toString();
context.beginPath();
context.arc(x, y, 2, 0, 2 * Math.PI, true);
context.closePath();
context.fill();

firework.nextState();
}
}

// 為煙火加上煙
for(var i in fireworks) {
var firework = fireworks[i];
if(firework.isAlive()) {
var x = firework.getPoint().x;
var y = firework.getPoint().y;
for(var j in smokes) {
var smoke = smokes[j];
if (!smoke.isAlive()) {
smoke.life =
parseInt(50 * Math.random());
smoke.resume(x, y);
break;
}
}
}
}

if(context === context2) {
document.body.replaceChild(canvas2, canvas1);
context = context1;
}
else {
document.body.replaceChild(canvas1, canvas2);
context = context2;
}

setTimeout(arguments.callee, 200);
}, 200);
};
</script>
</head>
<body>
<canvas id="canvas1" width="640" height="480"></canvas>
<canvas id="canvas2" width="640" height="480"></canvas>
</body>
</html>