Skip to content
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

Open
9am opened this issue Jul 28, 2022 · 2 comments
Open

Light a 'Fire' with Canvas and Particles. #7

9am opened this issue Jul 28, 2022 · 2 comments
Assignees
Labels
animation Animation canvas Canvas javascript JavaScript

Comments

@9am
Copy link
Owner

9am commented Jul 28, 2022

A simple way to draw a dynamic fire flame on the page.

tools

hits

@9am
Copy link
Owner Author

9am commented Jul 28, 2022

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.mov

The Idea

I 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:

  1. Shoot particles in random directions from the mouse position at intervals.
  2. Apply a force that performs as the wind to the particles. If the wind goes North, it will be like a bubble gun.
  3. For each particle, draw a line perpendicular to the velocity with a length L.
  4. Apply different L to different Particles, it's possible to build a flame shape by connecting the points of the Lines with curves.

idea-all

Let's do it

I 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 Vector class as the parent class, which is a fancy way of describing [x, y] with several functions to alter the x and y.

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 Particle class extends Vector to describe the orange dot in the pictures above. So the [x, y] stand for the position of the particle, and we add a velocity v to Particle to make it move. The v itself is a Vector too, the x and y mean the delta value of the position per frame in each direction.

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 Flame class which also extends Vector. It continuously produces Particle and is in charge of updating them and rendering the flame.

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 v and throw a requestAnimationFrame there, we have a bubble gun now.
s-1
Edit step-1

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)

position(t+∆t) = position(t) + ∆t * F(t) / m

It only takes about 10 particles to do the work, so we'll remove the old ones to keep a maxParticle number of particles.

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();
        }
    }
	...

s-2
Edit step-2

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 maxDistance to limit the distance between them. And drawing a line perpendicular to the velocity with size gives us something like a kite.

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);
            }
        }
    }
	...
}

s-3
Edit step-3

Step 4.1: Get curves out of particles.

Quadratic Béziers fit here but we need two endpoints EP and a control point CP. We'll draw the curves like this:
idea-5

    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();
        }
    }

s-4 1
Edit step-4.1

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 size of particles. If the newest particle is p0 and the oldest is p1, and by controlling the function size = ƒ(x); x ∈ [0, 1], we can shape the ribbon to the flame.

The ƒ(x) in this example:
(x) => (x > 0.7) ? Math.sqrt(1 - x) * 50 : Math.pow(x - 1, 2) * -30 + 30,
curve

s-4 2
Edit step-4.2

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.

s-4 3
Edit step-4.3

Step 5: Control the flame.

Now add a control panel to it.

  1. Change wind direction or power.
  2. Change particle numbers or distance or the frequency of spawning them.
  3. Change the color
ctrl.mov

Edit step-5

Well, hope you enjoy it. I'll see you next time.


@9am 🕘

@9am 9am changed the title Light A 'Fire' with Canvas and Particles. Light a 'Fire' with Canvas and Particles. Jul 28, 2022
@9am 9am added javascript JavaScript canvas Canvas labels Jul 28, 2022
@9am 9am added the animation Animation label Apr 10, 2023
@9am 9am self-assigned this Apr 14, 2023
@9am
Copy link
Owner Author

9am commented Jun 30, 2023

Update 2023

Check out the demo with @9am/ctrl-panel 🎉

Edit with-ctrl-panel
ctrl-panel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
animation Animation canvas Canvas javascript JavaScript
Projects
None yet
Development

No branches or pull requests

1 participant