Hello! welcome and happy new year! Join me while I down a few margaritas and attempt to explain how YOU can create yourself some cool fireworks using WebGL.

This tutorial will step through a number of concepts that I've written about previously, so if you find the following code too difficult to follow, consider checking out some other posts I've written:


We're going to code these using ES2015 JavaScript syntax. So if you're using CodePen you'll need to make sure that your JavaScript is set to use Babel. If you're building locally you'll need to have Babel in your build set up in order for the code to work in all browsers.

I won't cover every piece of code in the instructions, but I'll be adding a pen for each step of the way, so you can check out the full source code step by step.

Set up the pixi.js stage

We'll be using pixi.js to create our fireworks. Pixi is an awesome library that makes using 2D webGL a lot easier to manage. After including the lastest pixi version in our resources (https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.2.2/pixi.min.js) we'll be ready to go.

First we'll create a Pixi renderer the size of our window, and set it to transparent so the background of our page can show through:

  let width = window.innerWidth;
let height = window.innerHeight;

const renderer = PIXI.autoDetectRenderer(width, height, {
  transparent: true
});

Create a Pixi stage (for our objects)

  const stage = new PIXI.Container();

Add the renderer view to our html document

  document.body.appendChild(renderer.view);

Finally, we'll add a render loop that will create the animation. Eventually the loop function will update the objects on our stage, but for the moment it will just tell the renderer to render on each animation frame.

  const loop = () => {
  requestAnimationFrame(loop);
  renderer.render(stage);
}

loop();

Here's all the code so far in a Pen, it's not doing anything yet but we're prepped to start adding things to our Pixi stage

Add a particle

What we need now is to start adding particles to the pixi stage. We're going to want some control over these particles, to be able to give them a velocity and update their properties. So we'll create a particle Class to control each of the particles.

The Particle class will have some properties:

  • sprite: (the Pixi sprite) that will be added to the stage
  • velocity: the velocity at which the sprite will move across the stage

The Particle class will also have some methods:

  • to set the position of the sprite
  • to set the velocity
  • to update the sprite's position (add velocity to position)
  class Particle {
  constructor() {
    this.sprite = new PIXI.Sprite(texture);
    this.sprite.scale.x = 0.4;
    this.sprite.scale.y = 0.4;
    this.velocity = {x: 0, y: 0};
  }

  setPosition(pos) {
    this.sprite.position.x = pos.x;
    this.sprite.position.y = pos.y;
  }

  setVelocity(vel) {
    this.velocity = vel;
  }

  update() {
    this.sprite.position.x += this.velocity.x;
    this.sprite.position.y += this.velocity.y;
  }
}

We're also going to add a function to "launch" the particle:

  • set the position to the bottom of the stage, half way across
  • set the velocity of the particle so it moves up in to the "sky"
  • add the particle to the stage so it can be rendered
  const launchParticle = () => {
  particle = new Particle();
  particle.setPosition({x: width/2, y: height});
  particle.setVelocity({x: -0.4, y: -1.6});
  stage.addChild(particle.sprite);
}

Finally, we can update our loop function so that it updates the position of our particle on each animation frame.

  const loop = () => {
  requestAnimationFrame(loop);
  particle.update();
  renderer.render(stage);
}

Here's the pen! we have one particle launching itself in to the sky.

Create a firework

So while our particle is travelling in to the sky, it has to explode like a firework once it reaches a certain height. So we'll check the height of the particle on each update, and once it is high enough we'll explode it using an explode function.

First, in order to create this effect, we need to have more than one particle, so we'll need an array to keep the particles in:

  const particles = [];

Now to check the height of the launched particle, in our Particle update method:

  if (this.toExplode && !this.exploded) {
  // explode
  if (this.sprite.position.y < height/2) {
    this.sprite.alpha = 0;
    this.exploded = true;
    explode(this.sprite.position);
  }
}

Now comes the exploding! To create the explosion we're going to launch 10 particles out from the position of our original launched particle. We can use the following equation to generate the velocities of each of the particles so they spray out in a circular shape, in the way a firework explodes.

velocity(x) = radius * Cos(angle);
velocity(y) = radius * Sin(angle);

When we give each particle an angle around the circle, and generate their velocity, we can create the spraying effect.

  const explode = (position) => {
  const steps = 10;
  const radius = 4;
  for (var i = 0; i < steps; i++) {
    // get velocity
    const x = radius * Math.cos(2 * Math.PI * i / steps);
    const y = radius * Math.sin(2 * Math.PI * i / steps);
    // add particle
    const particle = new Particle();
    particle.fade = true;
    particle.setPosition(position);
    particle.setVelocity({x, y});
    stage.addChild(particle.sprite);
    particles.push(particle);
  }
}

Now we've got multiple particles on the stage, we need to use our loop function to update all of them. So we'll loop through the particles array and call update on each particle in each animation frame.

  const loop = () => {
  requestAnimationFrame(loop);
  for (var i = 0, l = particles.length; i < l; i++) {
    particles[i].update();  
  }
  renderer.render(stage);
}

The pen with the code so far:

Multiple fireworks, different colors, random positions

So far, we've used one texture (image) to create our particles. But different color fireworks are more exciting than red fireworks. So we're going to load a number of different textures to create our fireworks. I've chosen to use rainbow colored dots here, but you could use any image you like as your firework images. You just need a link to the image so it can be loaded as a Pixi texture.

Let's create an array of textures and load each of our images in to them. Because I've named my images rp-0, rp-1 and so on I can use a for loop to load and insert them in to the array.

  const textures = [];
let currentTexture = 0;

const initTextures = () => {
  for (let i = 0; i < 10; i++) {
    textures.push(PIXI.Texture.fromImage(`https://s3-us-west-2.amazonaws.com/s.cdpn.io/53148/rp-${i}.png?123`));
  }
}

now, in our launchParticle function we'll pass in a texture and a randomized scale to the new Particle. Then we'll cycle to the next texture in our textures array (or back to the beginning). This way, each new firework will have a different color and a different size.

  const launchParticle = () => {
  const particle = new Particle(textures[currentTexture], Math.random()*0.5);
  currentTexture++;
  if (currentTexture > 9) currentTexture = 0;
  //...

We can also randomize the starting position of the particle, so they will be launched from varying positions across the bottom of the screen. By randomizing the velocity, they'll travel in different directions across the "sky".

  particle.setPosition({x: Math.random()*width, y: height});
particle.setVelocity({x: -1.5 + Math.random()*3, y: -1.9 + Math.random()*-1});

We just updated the code to pass texture and scale parameters to our particle, so we'll need to update our Particle class to accept a texture and scale and use that on the particle sprite.

  class Particle {
  constructor(texture, scale) {
    this.sprite = new PIXI.Sprite(texture);
    this.sprite.scale.x = scale;
    this.sprite.scale.y = scale;
    this.velocity = {x: 0, y: 0};
    this.explodeHeight = 0.4 + Math.random()*0.5;
  }

We've set a texture and scale on the particle, we'll want the explosion particles to have the same properties, so we'll need to pass these properties as parameters through to our explode function as well.

In the Particle update method:

  // explode
if (this.sprite.position.y < height*this.explodeHeight) {
  this.sprite.alpha = 0;
  this.exploded = true;
  explode(this.sprite.position, this.sprite.texture, this.sprite.scale.x);
}

Updating the explode function to use a texture and scale parameter:

  const explode = (position, texture, scale) => {
  const steps = 8 + Math.round(Math.random()*6);
  const radius = 4;
  for (let i = 0; i < steps; i++) {
    // get velocity
    const x = radius * Math.cos(2 * Math.PI * i / steps);
    const y = radius * Math.sin(2 * Math.PI * i / steps);
    // add particle
    const particle = new Particle(texture, scale);
    particle.fade = true;
    particle.setPosition(position);
    particle.setVelocity({x, y});
    stage.addChild(particle.sprite);
    particles.push(particle);
  }
}

Finally, at the end of our launchParticle we can add a (randomized) timeout in which a new firework will be launched, this will keep the fireworks launching over and over.

  // launch a new particle
setTimeout(launchParticle, 200+Math.random()*600);

Don't forget to load your textures before running your loop and beginning the animation!

  initTextures();
launchParticle();
loop();

Here's the pen after fourth step, it is starting to look like a celebration!

Some finesse: refactor for performance, add gravity.

What we've made so far looks pretty cool, and you may be quite happy to leave it like this. There's one little issue I have with the code so far, and that is that we're generating a new Particle every time we need one, this means that over time, we'll fill up the browser memory with particles that have faded out and are not even visible on the screen.This can have some negative effects on the performance of our animation. So we're going to change up this code to re-use particles when possible.

Let's create a getParticle function, that will either find and return a "used" particle or create a new one if there isn't any available. First, the function will check the particles array for a particle with an alpha of 0 (no longer visible). If it finds one, it will reset the particle to use the texture and scale desired. If there are no used particles available, it will generate a new one and add it to the array.

  const getParticle = (texture, scale) => {
  // get the first particle that has been used
  let particle;
  // check for a used particle (alpha <= 0)
  for (var i = 0, l = particles.length; i < l; i++) {
    if (particles[i].sprite.alpha <= 0) {
      particle = particles[i];
      break;
    }
  }
  // update characteristics of particle
  if (particle) {
    particle.reset(texture, scale);
    return particle;
  }

  // otherwise create a new particle
  particle = new Particle(texture, scale);
  particles.push(particle);
  stage.addChild(particle.sprite);
  return particle;
}

The getParticle function above calls particle.reset to reset the properties of the particle, so we'll add that method to our Particle class:

  reset(texture, scale) {
  this.sprite.alpha = 1;
  this.sprite.scale.x = scale;
  this.sprite.scale.y = scale;
  this.sprite.texture = texture;
  this.velocity.x = 0;
  this.velocity.y = 0;
  this.toExplode = false;
  this.exploded = false;
  this.fade = false;
}

So now, instead of calling new Particle() when we need a particle in the launchParticle and explode functions, we can call getParticle(). This means that we'll re-use Particle objects, which makes for beter performance.

Another change I'd like to make to our fireworks animation is to add some gravity to the particles, to make them appear a little more lifelike. In order to do this, we'll change our particle update method to add gravity (of 0.03) to the y-velocity on each animation frame.

  update() {
  this.sprite.position.x += this.velocity.x;
  this.sprite.position.y += this.velocity.y;
  this.velocity.y += gravity;
  //...

Because there is gravity being applied to the particles, we'll need to make the launch velocity is strong enough to make the particle reach it's "explode height" before it starts falling. So we'll make the y-direction launch velocity increase with the window height. In our launchParticle function:

  const speed = height*0.01;
particle.setVelocity({
  x: -speed/2 + Math.random()*speed, 
  y: -speed + Math.random()*-1
});

And to finish this animation off, we'll make sure everything resizes on a window resize.

  const onResize = () => {
  width = window.innerWidth;
  height = window.innerHeight;
  // this resizes the pixi renderer and updates the display ratio to match
  renderer.view.style.width = width + "px";    
  renderer.view.style.height = height + "px";   
  renderer.resize(width,height);
}

window.addEventListener('resize', onResize);

Final Result & Source Code:

And that's our fireworks animation completed! Maybe you'd like to make yours with different images, or change the size and positioning of the explosions. I hope you've learned something from this step through and have fun with your own Pixi animations. Cheers and I wish you an excellent 2017!


5,085 3 70