Creating a particle effect using PIXI.js

PixiJS is a famous WebGL-wrapper, or in other words, a thing that gives you easy access to high graphics performance on a HTML canvas. I have seen a lot of beautiful hero canvases and thought something like that could work for my personal website too. I modified a pretty well-written codepen to produce this effect I'm quite happy with. I'll just go ahead and briefly describe how it works.

Initialization

I wanted to use an existing canvas for my PIXI app, which can be achieved as follows.

const canvas = document.getElementById("banner_canvas");
var app = new PIXI.Application(window.innerWidth,
                               window.innerHeight,
                               {view: canvas,
                                antialias: true,
                                transparent: true});

For performance and improved control over the particles, they are put into a container.

const drops = new PIXI.Container();
app.stage.addChild(drops)

We want to draw circles. It's possible to use PIXI.Graphics for drawing shapes such as circles. However, it's very inefficient to execute a new graphics fill on each draw call. A much better approach is to proceed as follows:

  1. Draw a circle using PIXI.Graphics.
  2. Save the circle in a texture.
  3. Use the texture to create a sprite for each particle.
// Create a base graphic for our sprites
const p = new PIXI.Graphics()
p.beginFill(COLOR)
p.drawCircle(0, 0, 100)
p.endFill()

// Generate a base texture from the base graphic
const baseTexture = app.renderer.generateTexture(p)
let particles = genParticles(baseTexture)

The particle generator and the updater

The particle generator works like this (paraphrased), nothing that interesting going on there.

const genParticles = (texture) =>
      new Array(AMOUNT).fill().map(p => {
          var p = new PIXI.Sprite(texture);

          // Spawn a particle at a random position
          p.x = Math.random()*window.innerWidth;
          p.y = Math.random()*window.innerHeight;

          // Set an initial velocity.
          p.vx = 1;

          drops.addChild(p);
          return p;
      });

The updater has some nice tricks.

app.ticker.add(function(delta) {
    if (
        app.renderer.height !== innerHeight ||
            app.renderer.width !== innerWidth
    ) {
        app.renderer.resize(innerWidth, innerHeight)
        drops.removeChildren()
        particles = genParticles(baseTexture)
    }

    for(let p of particles){
        p.x += delta*p.vx;
        p.y += delta*p.vy;
        if(p.x >= window.innerWidth + 20){
            p.x = -20;
        }
    }
    app.renderer.render(drops);
});

This will reset the animation and regenerate everything in case the window is resized so that the animation will not appear distorted.

if (
    app.renderer.height !== innerHeight ||
        app.renderer.width !== innerWidth
) {
    app.renderer.resize(innerWidth, innerHeight)
    drops.removeChildren()
    particles = genParticles(baseTexture)
}

This will put a particle that leaves the screen from the right slightly outside the left side of the screen. This will give an impression of an infinite amount of particles.

if(p.x >= window.innerWidth + 20){
    p.x = -20;
}

Conclusion

Since the resulting animation is pretty intense for a hero element, I combined it with some CSS masking to keep things under control. WebGL is really powerful since I didn't observe any lag with AMOUNT=2000. All in all, I had a lot of fun making this, and I strongly recommend trying PIXI.js if you haven't already.