-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Light a 'Fire' with Canvas and Particles. #7
Comments
I was staring at a candle the other day, and the flame fascinated me. It forms a constant shape of light. And if you move the candle, the flame stretches like a ribbon. It would be cool to draw the flame with code. But I'm not talking about simulating the burning process, that would be another story. This is what I got: flame.movThe IdeaI learned to draw ribbons on canvas a few years ago. It's a perfect way to simulate the flame shape. So the idea came up to my mind:
Let's do itI choose to do this the 'OOP' way. Because we have several similar concept: position, velocity, particle... which are basically 2D vectors. Step 1: Build the bubble gun.We need a class Vector {
constructor({ x = 0, y = 0 }) {
this.set(x, y);
}
set(x, y) {
this.x = x;
this.y = y;
return this;
}
add(v) {
return this.set(this.x + v.x, this.y + v.y);
}
} And a class Particle extends Vector {
constructor({ x = 0, y = 0, v = new Vector({}) }) {
super({ x, y });
this.v = v;
}
update() {
this.add(this.v);
}
render(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, 4, 0, PI_2);
ctx.closePath();
ctx.stroke();
}
} Finally, a class Flame extends Vector {
constructor({ x = 0, y = 0, canvas, ...params }) {
super({ x, y });
this.canvas = canvas.cloneNode();
this.ctx = this.canvas.getContext("2d");
this.ctx.strokeStyle = "orangered";
this.particles = new Map();
this.pIndex = 0;
setInterval(this.spawn, 1000 / 10);
}
spawn = () => {
const p = new Particle({
x: this.x,
y: this.y,
v: new Vector({
x: Math.random() * 4 - 2,
y: Math.random() * 4 - 2
})
});
this.particles.set(this.pIndex, p);
this.pIndex++;
};
update() {
for (const [i, p] of this.particles) {
p.update();
}
}
render() {
this.update();
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (const [i, p] of this.particles) {
p.render(this.ctx);
}
return this.canvas;
}
} Give the particles a random Step 2: Let the wind blow.We need the particles to affect by a 'wind' which is a force. And this can be done through Newton’s second law. (m = 1 to make it simple)
It only takes about 10 particles to do the work, so we'll remove the old ones to keep a class Flame {
...
spawn = () => {
...
if (this.pIndex > this.maxParticle) {
this.particles.delete(this.pIndex - this.maxParticle);
}
...
};
update() {
for (const [i, p] of this.particles) {
p.v.add(this.wind);
p.update();
}
}
... Step 3: Link them.You'll notice that the oldest particles are getting away too fast, we need them to stay close to their siblings to form a stable line. So add a class Particle {
...
render(ctx) {
const tv = new Vector({ len: this.size, angle: this.v.angle + PI_H });
const a = this.addNew(tv);
const b = this.subtractNew(tv);
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
...
}
...
} class Flame {
...
update() {
for (const [i, p] of this.particles) {
const np = this.particles.get(i + 1) ?? p;
...
if (p.link.length > this.maxDistance) {
p.link.length = this.maxDistance;
p.set(np.x + p.link.x, np.y + p.link.y);
}
}
}
...
} Step 4.1: Get curves out of particles.Quadratic Béziers fit here but we need two endpoints render() {
...
for (const [i, p] of this.particles) {
const np = this.particles.get(i + 1) ?? p;
const pp = this.particles.get(i - 1) ?? p;
const pAngle = p.link.angle + PI_H;
const npAngle = np.link.angle + PI_H;
const ppAngle = pp.link.angle + PI_H;
const a = pp.addNew(tv.setLenAngle(pp.size, ppAngle));
const b = pp.subtractNew(tv);
const c = p.addNew(tv.setLenAngle(p.size, pAngle));
const d = p.subtractNew(tv);
const e = np.addNew(tv.setLenAngle(np.size, npAngle));
const f = np.subtractNew(tv);
const ac = a.addNew(c).multiply(0.5);
const ec = e.addNew(c).multiply(0.5);
const bd = b.addNew(d).multiply(0.5);
const fd = f.addNew(d).multiply(0.5);
this.ctx.moveTo(ac.x, ac.y);
this.ctx.quadraticCurveTo(c.x, c.y, ec.x, ec.y);
this.ctx.lineTo(fd.x, fd.y);
this.ctx.quadraticCurveTo(d.x, d.y, bd.x, bd.y);
this.ctx.closePath();
this.ctx.stroke();
}
} Step 4.2: Shape the ribbon.It's kind of like a flame now but without the shape. It can be done by controlling the
Step 4.3: Draw curves in one path.Just an optimization to draw the flame in one single path to get rid of the bar between. Now we have the flame. Step 5: Control the flame.Now add a control panel to it.
ctrl.movWell, hope you enjoy it. I'll see you next time.
|
Update 2023 Check out the demo with @9am/ctrl-panel 🎉 |
The text was updated successfully, but these errors were encountered: