Gooey Fire

On the way to gooey fire

Today, we're going to make the gooey fire you see below.

Ingredients

We need 3 ingredients to make gooey fire:

Particles

Particles are small shapes that, when combined with lots of other ones, create neat effects. They usually only exist for a short period of time, especially when they are used to simulate smoke, fire, or snow. A particle's life is the amount of time the particle exists.

Here's an example of how we might create a simple Particle module/class.

				
function Particle() {
	// initial values of internal variables
	var x = 0;
	var y = 0;
	var lifespan = 0;
	var life = lifespan;

	var xbounds = [0,0];
	var ybounds = [0,0];

	var isAlive = false;

	function my(){}

	// the following functions allow access to the inner variables
	my.x = function(value) {
		if (!arguments.length) return x;  // call particle.x() to "get" x
		x = value;                        // call particle.x(value) to "set" x = value
		return my;
	};

	my.y = function(value) {
		if (!arguments.length) return y;
		y = value;
		return my;
	};

	my.life = function(value) {
		if (!arguments.length) return life;
		lifespan = value;
		life = clamp(value, 0, lifespan);
		isAlive = life > 0 ? true : false;
		return my;
	};

	my.lifeRemainingPercentage = function() {
		return life / lifespan;
	};

	my.xbounds = function(lower, upper) {
		if (!arguments.length) return xbounds;
		xbounds = [Math.min(lower, upper), Math.max(lower, upper)];
		return my;
	};

	my.ybounds = function(lower, upper) {
		if (!arguments.length) return ybounds;
		ybounds = [Math.min(lower, upper), Math.max(lower, upper)];
		return my;
	};

	my.isAlive = function(bool) {
		if (!arguments.length) return isAlive;
		isAlive = bool;
		return my;
	};

	my.hasLifeRemaining = function() {
		if (life > 0)
			return true;
		else
			return false;
	};

	// checks to see if the particle is within the boundaries or not
	my.isInsideBounds = function() {
		if (x >= xbounds[0] && x <= xbounds[1] && y >= ybounds[0] && y <= ybounds[1])
			return true;
		else
			return false;
	};

	my.decreaseLife = function(value) {
		if (!arguments.length)
			life--;
		else
			life -= value;

		my.isAlive(my.hasLifeRemaining());
	};

	my.move = function(vx, vy) {
		x += vx;
		y += vy;
	};

	return my;
}
				
				

If we were to instantiate a particle using the class, it wouldn't do or show anything. This is because the class doesn't implement the particle lifecycle, which involves:

  1. Creating the particle.
  2. Updating and drawing the particle during the course of its life.
  3. Destroying the particle once its life has been exhausted.

By design, the lifecycle was not implemented in the Particle class. We'll see why in the next section on Emitters. For now, let's take a look at each section of the lifecycle using a simple example.

In our example, we're going to create 5 particles, placing them in random locations and with random velocities. We're going to update and draw the particles' locations and opacity based on their velocities and the amounts of life they have remaining, respectively. Note that as the particle's life dwindles, the particle slowly fades away. Finally, whenever a particle's life reaches zero, we'll destroy that particle and immediately create a new one.

Let's start by looking at the createParticle function:

				
function createParticle() {
	// create a new Particle using the Particle module
	var particle = Particle();

	// define values for the particle
	particle
		.life(100)
		.xbounds(0, width)
		.ybounds(0, height);


	// place the particle randomly in the bounds of the view
	var xLoc = randReal(0, width);
	var yLoc = randReal(0, height);

	particle
		.x(xLoc)
		.y(yLoc);

	// since our particle is a function, and functions are objects, we can attach new attributes to it
	// let's define random velocities and attach them to the particle
	var vx = randReal(-3, 3);
	var vy = randReal(-3, 3);

	particle.vx = vx;
	particle.vy = vy;

	// finally, we'll want to display our particle
	// since we will be using SVG Filters, let's use SVG elements
	// attach a new svg circle to this particle (using D3)
	var svgCircle = svg.append("circle");

	particle.svgElement = svgCircle;

	return particle;
}
				
				

Next, let's look at the update function.

				
function updateParticle(particle) {
	// move the particle based on its velocity
	var vx = particle.vx;
	var vy = particle.vy;
	particle.move(vx, vy);

	// decrease the particle's life
	particle.decreaseLife();

	// mark the particle as dead if it is outside of the x and y bounds
	if (!particle.isInsideBounds()) {
		particle.isAlive(false);
	}
}
				
				

Now, the draw function.

				
function drawParticle(particle) {
	// grab the svg circle from the particle
	var circle = particle.svgElement;

	// using D3 to manipulate the SVG circle, draw it according to the particle's stats
	circle
			.attr("cx", particle.x())
			.attr("cy", particle.y())
			.attr("r", 5)
			.attr("fill", "#ff9900") // orange
			.attr("fill-opacity", particle.lifeRemainingPercentage());
}
				
				

Finally, the destroy function. Note that destroying particles is a little tricky. We'll need to traverse the particle array in reverse order because we're going to remove dead particle elements from the array.

				
// code snippet showing how to remove a particle from a particle array

// traverse the array in reverse order
for (var i = particleArray.length - 1; i >= 0; i--) {
	// get the particle
	var particle = particleArray[i];

	if (!particle.isAlive()) {
		// destroy the particle
		destroyParticle(particle);

		// then, remove the particle from the array using slice
		particleArray.slice(i, 1);

		// and, finally, we can create a new particle to take its place
		particleArray.push(createParticle());
	}
}

function destroyParticle(particle) {
	// remove SVG element
	particle.svgElement.remove();
}
				
				

Putting all of the lifecycle code together, we get the following example. Click the Play button to see it.

Here's all of the JavaScript code for the example:

				
var width = 950;
var height = 290;

// select the div where we'll put the SVG container
var div = d3.select("#vis");

// create the SVG container
var svg = div.append("svg")
		.attr("width", width)
		.attr("height", height);


var particleArray = [];

// use the createParticle function (below) to make 5 particles
for (var i = 0; i < 5; i++) {
	particle = createParticle();
	particleArray.push(particle);
}

function createParticle() {
	// create a new Particle using the Particle module
	var particle = Particle();

	// define values for the particle
	particle
		.life(100)
		.xbounds(0, width)
		.ybounds(0, height);


	// place the particle randomly in the bounds of the view
	var xLoc = randReal(0, width);
	var yLoc = randReal(0, height);

	particle
		.x(xLoc)
		.y(yLoc);

	// since our particle is a function, and functions are objects, we can attach new attributes to it
	// let's define random velocities and attach them to the particle
	var vx = randReal(-3, 3);
	var vy = randReal(-3, 3);

	particle.vx = vx;
	particle.vy = vy;

	// finally, we'll want to display our particle
	// since we will be using SVG Filters, let's use SVG elements
	// attach a new svg circle to this particle (using D3)
	var svgCircle = svg.append("circle");

	particle.svgElement = svgCircle;

	return particle;
}

function updateParticle(particle) {
	// move the particle based on its velocity
	var vx = particle.vx;
	var vy = particle.vy;
	particle.move(vx, vy);

	// decrease the particle's life
	particle.decreaseLife();

	// mark the particle as dead if it is outside of the x and y bounds
	if (!particle.isInsideBounds()) {
		particle.isAlive(false);
	}
}

function drawParticle(particle) {
	// grab the svg circle from the particle
	var circle = particle.svgElement;

	// using D3 to manipulate the SVG circle, draw it according to the particle's stats
	circle
			.attr("cx", particle.x())
			.attr("cy", particle.y())
			.attr("r", 5)
			.attr("fill", "#ff9900") // orange
			.attr("fill-opacity", particle.lifeRemainingPercentage());
}

function destroyParticle(particle) {
	// remove SVG element
	particle.svgElement.remove();
}


var raf;

// select the Play button
var playButton = d3.select("#play");

// click the Play button to toggle loop() on/off
var playing = false;
playButton.on("click", function(){
	if (playing) {
		cancelAnimationFrame(raf);
		playButton.text("Play");
	}
	else {
		raf = requestAnimationFrame(loop);
		playButton.text("Pause");
	}

	playing = !playing;
});


function loop() {
	// traverse the array in reverse order
	for (var i = particleArray.length - 1; i >= 0; i--) {
		// get the particle
		var particle = particleArray[i];

		// update the particle
		updateParticle(particle);

		// draw the particle
		drawParticle(particle);

		// if the particle is dead...
		if (!particle.isAlive()) {
			// destroy the particle (which in this case simply means to remove the SVG element from the
			// particle to make sure that the DOM tree doesn't get too crowded with unused SVG elements)
			destroyParticle(particle);

			// then, remove the particle from the array using splice
			particleArray.splice(i, 1);

			// and, finally, we can create a new particle to take its place
			particleArray.push(createParticle());
		}
	}

	// keep the loop going
	raf = requestAnimationFrame(loop);
}
					
				
				

Emitters

Working with arrays of particles is a little cumbersome. It can be confusing, especially if multiple arrays of particles need to be used. We can use emitters to help us.

An emitter is an object that manages an array of particles. It automatically handles the emission and removal parts of dealing with particle arrays. If we feed it the different particle lifecycle functions, it will also handle those for us. Best of all, we can feed different lifecycle functions to different emitters to produce a variety of particle effects without having to confuse ourselves with handling the different arrays!

This last point is why we are not going to implement the lifecycle functions in the Particle class. Each emitter should be the conductor, telling each particle where to start, how to move, how to appear, and what to do when it expires.

Here's the code for a simple Emitter module/class:

				
function Emitter() {
	var x = 0;
	var y = 0;
	var particleList = [];
	var maxParticles = 0;
	var emissionRate = 1;

	// empty: user should define how to create particles
	var createParticle = function(p){};

	// empty: user should define how to update particles
	var updateParticle = function(p){};
	
	// empty: user should define how to draw particles
	var drawParticle = function(p){};

	// empty: user should define how to destroy particles
	var destroyParticle = function(p){};

	function my(){}

	my.x = function(value) {
		if (!arguments.length) return x;
		x = value;
		return my;
	};

	my.y = function(value) {
		if (!arguments.length) return y;
		y = value;
		return my;
	};

	my.emissionRate = function(value) {
		if (!arguments.length) return emissionRate;
		emissionRate = clamp(value, 1, maxParticles);
		return my;
	};

	my.maxParticles = function(value) {
		if (!arguments.length) return maxParticles;
		maxParticles = value;
		return my;
	};

	function emitParticles() {
		for (var i = 0; i < emissionRate; i++) {
			if (particleList.length < maxParticles) {
				// create the particle object to pass to the createParticle function
				// default location of particles set to emitter's location
				var particle = Particle().x(x).y(y);
				var newParticle = createParticle(particle);
				
				particleList.push(newParticle);
			}
			else {
				break;
			}
		}
	}

	my.update = function() {
		emitParticles();

		// update and draw each particle in the list
		for (var i = particleList.length - 1; i >= 0; i--) {
			var particle = particleList[i];
			updateParticle(particle);
			drawParticle(particle);

			// if the particle is "dead", remove it from the list
			if (!particle.isAlive()) {
				destroyParticle(particle);
				particleList.splice(i,1);
			}
		}
	};

	my.createParticle = function(createParticleFunction) {
		if (createParticleFunction !== undefined) {
			if (typeof(createParticleFunction) === "function") {
				createParticle = createParticleFunction;
			}
		}
		return my;
	};

	my.updateParticle = function(updateParticleFunction) {
		if (updateParticleFunction !== undefined) {
			if (typeof(updateParticleFunction) === "function") {
				updateParticle = updateParticleFunction;
			}
		}
		return my;
	};

	my.drawParticle = function(drawParticleFunction) {
		if (drawParticleFunction !== undefined) {
			if (typeof(drawParticleFunction) === "function") {
				drawParticle = drawParticleFunction;
			}
		}
		return my;
	};

	my.destroyParticle = function(destroyParticleFunction) {
		if (destroyParticleFunction !== undefined) {
			if (typeof(destroyParticleFunction) === "function") {
				destroyParticle = destroyParticleFunction;
			}
		}
		return my;
	};

	return my;
}					
				
				

Let's implement the same example using an emitter. Since it's the exact same example as before, we won't watch it. Instead, we'll jump straight into the code:

				
var width = 950;
var height = 290;

// select the div where we'll put the SVG container
var div = d3.select("#vis");

// create the SVG container
var svg = div.append("svg")
		.attr("width", width)
		.attr("height", height);


function createParticle(particle) {
	// define values for the particle
	particle
		.life(100)
		.xbounds(0, width)
		.ybounds(0, height);


	// place the particle randomly in the bounds of the view
	var xLoc = randReal(0, width);
	var yLoc = randReal(0, height);

	particle
		.x(xLoc)
		.y(yLoc);

	// since our particle is a function, and functions are objects, we can attach new attributes to it
	// let's define random velocities and attach them to the particle
	var vx = randReal(-3, 3);
	var vy = randReal(-3, 3);

	particle.vx = vx;
	particle.vy = vy;

	// finally, we'll want to display our particle
	// since we will be using SVG Filters, let's use SVG elements
	// attach a new svg circle to this particle (using D3)
	var svgCircle = svg.append("circle");

	particle.svgElement = svgCircle;
}

function updateParticle(particle) {
	// move the particle based on its velocity
	var vx = particle.vx;
	var vy = particle.vy;
	particle.move(vx, vy);

	// decrease the particle's life
	particle.decreaseLife();

	// mark the particle as dead if it is outside of the x and y bounds
	if (!particle.isInsideBounds()) {
		particle.isAlive(false);
	}
}

function drawParticle(particle) {
	// grab the svg circle from the particle
	var circle = particle.svgElement;

	// using D3 to manipulate the SVG circle, draw it according to the particle's stats
	circle
			.attr("cx", particle.x())
			.attr("cy", particle.y())
			.attr("r", 5)
			.attr("fill", "#ff9900") // orange
			.attr("fill-opacity", particle.lifeRemainingPercentage());
}

function destroyParticle(particle) {
	// remove SVG element
	particle.svgElement.remove();
}


var emitter = Emitter()
		.x(width/2)
		.y(height/2)
		.maxParticles(5)
		.emissionRate(1)
		.createParticle(createParticle)
		.updateParticle(updateParticle)
		.drawParticle(drawParticle)
		.destroyParticle(destroyParticle);

var raf;

// select the Play button
var playButton = d3.select("#play");

// click the Play button to toggle loop() on/off
var playing = false;
playButton.on("click", function(){
	if (playing) {
		cancelAnimationFrame(raf);
		playButton.text("Play");
	}
	else {
		raf = requestAnimationFrame(loop);
		playButton.text("Pause");
	}

	playing = !playing;
});


function loop() {
	emitter.update();

	// keep the loop going
	raf = requestAnimationFrame(loop);
}				
				
				

Note that the createParticle function is different. This time, we don't have to create a Particle object. Instead, it's passed to us by the emitter. All we need to do is modify the particle's values to fit our needs! Another difference is that we don't have to return the modified particle. This is because JavaScript passes the reference to the particle object; when we make modifications, we're actually changing the original particle object in the emitter class.

Before we move on to SVG Filters, let's use our new Emitter class to set the stage for the gooey fire. We're going to place an emitter toward the bottom-center of the container and then tell the emitter to launch all particles randomly upward, in a cone-like fashion. Here's what this looks like.

Here's the code:

				
var width = 950;
var height = 290;

// select the div where we'll put the SVG container
var div = d3.select("#vis");

// create the SVG container
var svg = div.append("svg")
		.attr("width", width)
		.attr("height", height);


function createParticle(particle) {
	// define values for the particle
	particle
		.life(80)
		.xbounds(0, width)
		.ybounds(0, height);

	// note: there is no need to set the location (x,y) of the particles as their default is
	// set to be the emitter's location

	// random velocities that generally move in an upward, cone-like way
	var vx = randReal(-1, 1);
	var vy = randReal(-2, -1);

	particle.vx = vx;
	particle.vy = vy;

	// finally, we'll want to display our particle
	// since we will be using SVG Filters, let's use SVG elements
	// attach a new svg circle to this particle (using D3)
	var svgCircle = svg.append("circle");

	particle.svgElement = svgCircle;
}

function updateParticle(particle) {
	// move the particle based on its velocity
	var vx = particle.vx;
	var vy = particle.vy;
	particle.move(vx, vy);

	// decrease the particle's life
	particle.decreaseLife();

	// mark the particle as dead if it is outside of the x and y bounds
	if (!particle.isInsideBounds()) {
		particle.isAlive(false);
	}
}

function drawParticle(particle) {
	// grab the svg circle from the particle
	var circle = particle.svgElement;

	// since fires cool as they move away from their sources, start with a "hot" yellow
	// color and move to a "cool" red color as the particle's life dwindles
	var color = "rgb(255," + Math.floor(255 * particle.lifeRemainingPercentage()) + ",0)";

	// using D3 to manipulate the SVG circle, draw it according to the particle's stats
	circle
			.attr("cx", particle.x())
			.attr("cy", particle.y())
			.attr("r", 10)
			.attr("fill", color)
			.attr("fill-opacity", particle.lifeRemainingPercentage());
}

function destroyParticle(particle) {
	// remove SVG element
	particle.svgElement.remove();
}


var emitter = Emitter()
		.x(width/2)
		.y(height * 0.6)
		.maxParticles(100)
		.emissionRate(1)
		.createParticle(createParticle)
		.updateParticle(updateParticle)
		.drawParticle(drawParticle)
		.destroyParticle(destroyParticle);


// matchstick
var rectW = 8;
var rectH = 100;
svg.append("rect")
	.attr("x", emitter.x() - rectW/2)
	.attr("y", emitter.y()+4)
	.attr("width", rectW)
	.attr("height", rectH)
	.attr("fill", "rgb(241, 230, 202)");

svg.append("rect")
	.attr("x", emitter.x() - 4)
	.attr("y", emitter.y()+4)
	.attr("width", 8)
	.attr("height", 8)
	.attr("fill", "#0099ff")
	.attr("rx", 1);

var raf;

// select the Play button
var playButton = d3.select("#play");

// click the Play button to toggle loop() on/off
var playing = false;
playButton.on("click", function(){
	if (playing) {
		cancelAnimationFrame(raf);
		playButton.text("Play");
	}
	else {
		raf = requestAnimationFrame(loop);
		playButton.text("Pause");
	}

	playing = !playing;
});


function loop() {
	emitter.update();

	// keep the loop going
	raf = requestAnimationFrame(loop);
}
				
				

SVG Filters

We know almost everything we need to at this point to be able to make it. What we're missing is the gooey effect.

I first learned out about the SVG gooey effect from Nadieh Bremer's post on creating a gooey effect during a transition. I highly recommend you visit her site and learn from her. Her stuff is great and so full of joy; you can really tell she loves what she does!

Back to the gooey filter. It's composed of three different pieces:

  1. Blur: We'll use a Gaussian blur effect on the SVG container.
  2. Color Matrix: We'll take that blur effect and transform it using a color matrix that will give us the gooey effect.
  3. Composition: We'll compose the original SVG container with the gooey effect to imube all our particles with gooey-ness.

Here's what the filter will look like (in D3-ese).

				
// SVG "gooey" filter
// code borrowed from: https://www.visualcinnamon.com/2015/05/gooey-effect.html
var defs = svg.append('defs');
var filter = defs.append('filter').attr('id','gooey');
filter.append('feGaussianBlur')
	.attr('in','SourceGraphic')
	.attr('stdDeviation','10')
	.attr('result','blur');
filter.append('feColorMatrix')
	.attr('in','blur')
	.attr('mode','matrix')
	.attr('values','1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7')
	.attr('result','gooey');
filter.append('feComposite')
	.attr('in','SourceGraphic')
	.attr('in2','gooey')
	.attr('operator','atop');
				
				

Let's add it to our particles and see what happens!

Hmm... It doesn't look quite right. Let's make a few changes:

  1. Reduce the particles' life so that the yellow-to-red effect still shows given the faster particle speeds.
  2. Add a random spray call to generate a normalized vector pointing in a random direction between two values. This helps us maintain our cone shape and helps make the colors look better.
  3. Increase the velocity each time by a little bit (using the random spray function).
  4. Clamp the opacity to between 0.6 and 1.

Ah, that's much better! Here's the final batch of JavaScript code:

				
var width = 950;
var height = 290;

// select the div where we'll put the SVG container
var div = d3.select("#vis");

// create the SVG container
var svg = div.append("svg")
		.attr("width", width)
		.attr("height", height);

var defs = svg.append('defs');
var filter = defs.append('filter').attr('id','gooey');
filter.append('feGaussianBlur')
	.attr('in','SourceGraphic')
	.attr('stdDeviation','10')
	.attr('result','blur');
filter.append('feColorMatrix')
	.attr('in','blur')
	.attr('mode','matrix')
	.attr('values','1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7')
	.attr('result','gooey');
filter.append('feComposite')
	.attr('in','SourceGraphic')
	.attr('in2','gooey')
	.attr('operator','atop');

// add a group element for the particles
// this is where we'll apply the filter
var g = svg.append("g");
g.style("filter", "url(#gooey)");


function createParticle(particle) {
	// define values for the particle
	particle
		.life(30)
		.xbounds(0, width)
		.ybounds(0, height);

	// note: there is no need to set the location (x,y) of the particles as their default is
	// set to be the emitter's location

	// random velocities that generally move in an upward, cone-like way
	var angleStart = 270 - 25;
	var angleStop = 270 + 25;

	var vel = randomSpray(angleStart, angleStop);
	vel = scale(vel, 1);

	particle.vx = vel[0];
	particle.vy = vel[1];

	// radius of svg circle for this particle
	particle.r = 10;

	// finally, we'll want to display our particle
	// since we will be using SVG Filters, let's use SVG elements
	// attach a new svg circle to this particle (using D3)
	var svgCircle = g.append("circle");

	particle.svgElement = svgCircle;
}

function updateParticle(particle) {
	// move the particle based on its velocity
	// jitter the velocity a bit using randomness
	var angleStart = 270 - 55;
	var angleStop = 270 + 55;
	var vel = randomSpray(angleStart, angleStop);
	vel = scale(vel, 0.7);

	particle.vx += vel[0];
	particle.vy += vel[1];
	
	var vx = particle.vx;
	var vy = particle.vy;
	particle.move(vx, vy);

	// decrease the particle's life
	particle.decreaseLife();

	// mark the particle as dead if it is outside of the x and y bounds
	if (!particle.isInsideBounds()) {
		particle.isAlive(false);
	}
}

function drawParticle(particle) {
	// grab the svg circle from the particle
	var circle = particle.svgElement;

	// since fires cool as they move away from their sources, start with a "hot" yellow
	// color and move to a "cool" red color as the particle's life dwindles
	var color = "rgb(255," + Math.floor(255 * particle.lifeRemainingPercentage()) + ",0)";

	// set the opacity to be between 0.3 and 1
	var opacity = clamp(particle.lifeRemainingPercentage(), 0.6, 1);

	// using D3 to manipulate the SVG circle, draw it according to the particle's stats
	circle
			.attr("cx", particle.x())
			.attr("cy", particle.y())
			.attr("r", particle.r)
			.attr("fill", color)
			.attr("fill-opacity", opacity);
}

function destroyParticle(particle) {
	// remove SVG element
	particle.svgElement.remove();
}


var emitter = Emitter()
		.x(width/2)
		.y(height * 0.6)
		.maxParticles(80)
		.emissionRate(1)
		.createParticle(createParticle)
		.updateParticle(updateParticle)
		.drawParticle(drawParticle)
		.destroyParticle(destroyParticle);

// matchstick
var rectW = 8;
var rectH = 100;
svg.append("rect")
	.attr("x", emitter.x() - rectW/2)
	.attr("y", emitter.y()+4)
	.attr("width", rectW)
	.attr("height", rectH)
	.attr("fill", "rgb(241, 230, 202)");

svg.append("rect")
	.attr("x", emitter.x() - 4)
	.attr("y", emitter.y()+4)
	.attr("width", 8)
	.attr("height", 8)
	.attr("fill", "#0099ff")
	.attr("rx", 1);

var raf;

// select the Play button
var playButton = d3.select("#play");

// click the Play button to toggle loop() on/off
var playing = false;
playButton.on("click", function(){
	if (playing) {
		cancelAnimationFrame(raf);
		playButton.text("Play");
	}
	else {
		raf = requestAnimationFrame(loop);
		playButton.text("Pause");
	}

	playing = !playing;
});


function loop() {
	emitter.update();

	// keep the loop going
	raf = requestAnimationFrame(loop);
}
				
				

Thanks for reading! I hope you now understand particles and SVG filters a little more. If you want to clone the project, you can find it on my Github page!