34 lines of JS is enough...

I have seen this background animation many times all over CodePen and internet in general. It is pleasent to spectate and is not distracting when the user is busy dealing with other features of the app/website.

But

Just why is this so complicated? I have even seen this animation implemented with 150 lines! From the side, animation seems to be very easy to code and there isn't much to it other than particles changing their direction on touch with the wall. There is no acceleration, there is no need for vectors, there is no user interaction at all.

Let's just fix it.

i assume you know the basics of HTML5 2D canvas API

So, where do we begin? Lets have a general idea of what is happening:

  • There are particles of different
    • speed
    • opacity
    • size
  • Particles are going in the opposite direction after they hit the wall.

You might say: "well, that is pretty simple, I guess". I know right?

Step #0. Onto the initial setup

If you have read my articles/tutorials before, then you know I use basically the same structure every time. This time is no different.

HTML
  <canvas id=c></canvas>

The id=c will turn into a global variable. We won't need to do getElementsById('c') because, apparently, it is already a global variable. Don't ask me why.

CSS
  canvas {
    position: absolute;
    top: 0px;
    left: 0px;
}

We are just removing the ugly scroll-bars, by making the body height to be automatically 0, because the contents are position: absolute;.

JS
  (()=>{
    let $ = c.getContext("2d"),
        w = c.width = window.innerWidth,
        h = c.height = window.innerHeight,
        //predefines...
})()

Again, things are obvious. We are defining the context variable, and assigning the variables w and h and the canvas's width and height at the same time to the window's width and height.

      let pi2 = Math.PI*2,
            random = t=>Math.random()*t,
            binRandom = t=>Math.random()<t;

PI2 and these two functions will be used many times later, so I created a variable for them.

random(n) returns Math.random() multiplied by n

and

binRandom(n) (from binary random) will return true if Math.random() is smaller than n

Let's just display something on our screen real quick so that we know we are doing something:

      function draw(){
        $.fillStyle="#222";
        $.fillRect(0,0,w,h);
        requestAnimationFrame(draw);
    }
    draw(); //do not forget to call the function

Just a background for our bouncing particles that we are about to make.

Step #1. Let's get to particles.

Let's review what we see as particles

Particle is an object that has it's own:

  • Size
  • Position
  • Speed
  • Opacity

now that we know that, let's just fill one array with many particles like that:

  let arr = new Array(500 /* amount */).fill().map(p=>{
    return {
        p: {x: random(w), y: random(h) }, //position
        v: { //velocity
            x: binRandom(0.5)? random(1) : random(-1),
            y: binRandom(0.5)? random(1) : random(-1)
        },
        s: random(1)+2, //size
        o: random(1)+.3 //opacity
    }
})

There is much stuff being defined here so let's go step-by-step.

Array processing
  1. We create an array. The array is empty (!) even though it has the length of 500. The map() function maps through each object in the array and returns a new one if return is specified. We can't run the function map() yet because, as I mentioned, the arr is empty, hence there is nothing to map through.
  2. We fill() the array with undefined elements. Even though each element in the array is still undefined, it is there and the array is not empty anymore.
  3. We map() through each undefined object in the array and return a new object that we are going to discuss right now.
Particle values
  • Position is random across the width and height of the screen.
  • Velocity would look really stupid if the direction is only positive. That is why, we are using binRandom(.5) to have a random speed in random directions.
  • Size This whole +2 addition's purpose is obvious - to set the minimal value to two, and then it would be up to 3 with Math.random()
  • Opacity Same trick. Minimal opacity of .3. Notice that opacity can also be a max of 1.3. That means that there will be more particles that are not transparent at all. (if that is a good or a bad, depends on your taste).

Step #2. Let's render the particles.

If you know basic Canvas API, you can even skip this part - it's too easy.

  function draw(){
    //stuff
    arr.forEach(p=>{
        $.fillStyle="rgba(255,255,255,"+p.o+")";
        $.beginPath();
        $.arc(p.p.x, p.p.y, p.s, 0, pi2);
        $.closePath();
        $.fill();
    })
}

Well, there isn't much to explain. We draw the circle with opacity of p.o, size of p.s, at p.p.x and p.p.y.

What we will see on our screen is a bunch of circles of different opacity and size. But they aren't moving and we are about to fix that.

Step #3. Lets move the particles.

  arr.forEach(p=>{
    p.p.x += p.v.x;
    p.p.y += p.v.y;
    if (p.p.x > w || p.p.x < 0) p.v.x *= -1;
    if (p.p.y > h || p.p.y < 0) p.v.y *= -1;
    //rendering stuff
}

Those conditional functions are checking if the position on each axis is over the borders. If it does then velocity of the particle on that axis will be reflected.

I don't know why, but positions are locked as soon as they touch the border if i put the position update after the conditional functions. I moved it before the conditional functions and it all worked fine.

At this point we should have about 30 lines of code, and you can see our amazing animation working.

Step #4. Resizing.

I love CodePen because there are many genious people. I was always doing resizing through an eventListener. On my previous tutorial Mark (@Blindman67) suggested one-liner replacement to my eventListener to make it shorter:

  (h !== innerHeight || w!==innerWidth) && (w=c.width=innerWidth,h=c.height=innerHeight);

And that's it! We now have a very simple animation of drifting bouncing particles.


7,578 2 176