Today, we're going to make the gooey fire you see below.
We need 3 ingredients to make gooey fire:
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:
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);
}
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);
}
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:
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:
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!