Creative Coding with SVGs
zuubaDigital

Pure JavaScript animations

Next we'll create an animation without adding or removing css. Let’s try to replicate the animation from the CSS animation section, where we simply move the circle up and down.

We'll start off by getting a reference to the circle using querySelector.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <svg ...> <circle id="my-circle" cx="150" cy="150" r="10" fill="green" /> </svg> <script> const circ = document.querySelector("#my-circle"); let xpos = 50; for (let i = 0; i < 400; i++) { circ.setAttribute("cx", xpos); xpos += 1; } </script>

You can probably guess that we'll be changing the position of the circle using the setAttribute method we demonstrated in the Changing SVGs Dynamically section.

Let's say we just want to move the circle from the top of the SVG to the bottom. You might think that we can simply use a loop to increase the circle’s cy attribute. It won’t work. Why?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <svg ...> <circle id="my-circle" cx="150" cy="150" r="10" fill="green" /> </svg> <script> const circ = document.querySelector("#my-circle"); let ypos = 0; for (let i = 0; i < 300; i++) { circ.setAttribute("cx", ypos); ypos += 1; } </script>

The circle’s cy attribute is indeed being updated, but the browser runs through all of the loop iterations so fast you won’t even see it. All you’ll see is the green dot at the final cy position. In terms of the browsers frame rate, all of the animation occurs on a single frame.

requestAnimationFrame

In order to animate the circle, we’ll need to update it’s position once per frame. We do this using the browser’s requestAnimationFrame() method.

First, let’s create a method that increments the circle’s cy attribute:

1 2 3 4 5 6 7 8 9 const circ = document.querySelector("#my-circle"); let ypos = 10; moveCircle(); function moveCircle(){ circ.setAttribute("cy", ypos); ypos += 1; }

As a reminder, the browser has a own frame rate of about 60 fps. This means that it repaints itself 60 times per second. The requestAnimationFrame method is used to call an animation function prior to “repainting” the screen. So you’ll want to pass it a function that updates some visual aspect of the svg - in this case, the moveCircle() method.

We'll recursively call the moveCircle method by adding the window.requestAnimationFrame to the end of the function. So the function runs, updates the cy value, and then calls itself to run again on the next frame. The method keeps going until cy >= 290. Once it reaches that value, a return statement prevents the requestAnimationFrame method from being called again.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 const circ = document.querySelector("#my-circle"); let ypos = 10; moveCircle(); function moveCircle(){ ypos += 1; circ.setAttribute("cy", ypos); if (ypos >= 290) return; window.requestAnimationFrame(moveCircle); }

Instead of just stopping, let's make the ball bounce off the wall and head the other direction. In order to do this, we'll need to create a speed variable.

let speed = 1;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const circ = document.querySelector("#my-circle"); let ypos = 10; let speed = 1; moveCircle(); function moveCircle(){ ypos += 1; circ.setAttribute("cy", ypos); if (ypos >= 290) return; window.requestAnimationFrame(moveCircle); }

Now let's update the moveCircle method so that when the ypos variable is greaterthan or equal to 290, whe change the speed variable so the ball heads in the opposite direction. An easy way to do this is to just multiply it by -1.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const circ = document.querySelector("#my-circle"); let ypos = 10; let speed = 1; moveCircle(); function moveCircle(){ ypos += 1; circ.setAttribute("cy", ypos); if(ypos >= 290){ speed *= -1; ypos = 290; } window.requestAnimationFrame(moveCircle); }

As you can see, the ball bounces off the bottom but not the top. We’ll also need to reverse the speed when the circle hits the top edge of the svg (ypos < 10).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const circ = document.querySelector("#my-circle"); let ypos = 10; let speed = 1; moveCircle(); function moveCircle(){ ypos += 1; circ.setAttribute("cy", ypos); if(ypos >= 290){ speed *= -1; ypos = 290; } if(ypos < 10){ speed *= -1; ypos = 10; } window.requestAnimationFrame(moveCircle); }

Let's make this work in two dimensions by adding speed variables for both x and y, and updating the speed variables when the ball collides with ANY of the walls.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const circ = document.querySelector("#my-circle"); let xpos = 100; let ypos = 150; let x_speed = 1; let y_speed = 1; moveCircle(); function moveCircle(){ circ.setAttribute("cx", xpos); circ.setAttribute("cy", ypos); xpos += x_speed; ypos += y_speed; if(xpos > 190){ xpos = 190; x_speed *= -1; } else if(xpos < 10){ xpos = 10; x_speed *= -1; } if(ypos > 290){ ypos = 290; y_speed *= -1; } else if(ypos < 10){ ypos = 10; y_speed *= -1; } window.requestAnimationFrame(moveCircle); }

With hard-coded speed variables, the animation is the same every time. We’ll fix that by setting random values for x_speed and y_speed.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <script> const circ = document.querySelector("#my-circle"); let xpos = 100; let ypos = 150; let x_speed = Math.random() * 10 - 5; let y_speed = Math.random() * 10 - 5; moveCircle(); function moveCircle(){ circ.setAttribute("cx", xpos); circ.setAttribute("cy", ypos); xpos += x_speed; ypos += y_speed; if(xpos > 190){ xpos = 190; x_speed *= -1; } else if(xpos < 10){ xpos = 10; x_speed *= -1; } if(ypos > 290){ ypos = 290; y_speed *= -1; } else if(ypos < 10){ ypos = 10; y_speed *= -1; } window.requestAnimationFrame(moveCircle); } </script>

Note: for the random speed values, I could have just multiplied a number by Math.random() like this:

let x_speed = Math.random() * 10;

The problem is that this would always result in a positive number. If I want to have a potential range of positive and negative values from x to -x, I need to do this

let value = Math.random() * <2x> - x;

So to get values from 5 to -5:

let x_speed = Math.random() * 10 - 5;

*See the full example on codepen

simple particles

This is all well and good for one bouncing particle, but what if we wanted more? What if we wanted dozens, or even hundreds? We’ll need to create a particle class for that. Let’s start with the constructor. We’ll want to pass in several variables as parameters, the x and y position, the radius, and the width and height of the svg.

1 2 3 4 5 6 7 8 9 10 11 class Particle { constructor(x, y, r, w, h) { this.x = x; this.y = y; this.r = r; this.right = w - r; this.top = h - r; this.x_speed = Math.random() _ 10 - 5; this.y_speed = Math.random() _ 10 - 5; this.circ = null } }

We used the w (width) and h (height) variables to create a right and top variable. These will be used for wall collision detection.

Next, we’ll create an init method where we’ll dynamically create the circle element. We’ll need to pass in the “container” - the element that will hold all the particles.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> class Particle { constructor(x, y, r, w, h) { this.x = x; this.y = y; this.r = r; this.right = w - r; this.top = h - r; this.x_speed = Math.random() _ 10 - 5; this.y_speed = Math.random() _ 10 - 5; this.circ = null } init(container){ // ... initialization code will go here } }</script>

Now we'll dynamically create a circle element, add some styling, and add it to the container using the techniques we learned in the Changing SVGs Dynamically section

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <script> class Particle { constructor(x, y, r, w, h) { this.x = x; this.y = y; this.r = r; this.right = w - r; this.top = h - r; this.x_speed = Math.random() _ 10 - 5; this.y_speed = Math.random() _ 10 - 5; this.circ = null } init(container){ const namespace = "http://www.w3.org/2000/svg"; this.circ = document.createElementNS(namespace, "circle"); this.circ.setAttribute("cx", this.x); this.circ.setAttribute("cy", this.y); this.circ.setAttribute("r", this.r); this.circ.setAttribute("fill", "orange"); this.circ.setAttribute("stroke", "black"); this.circ.setAttribute("stroke-width", "3"); container.appendChild(this.circ); } }</script>

Now we’ll add an “update” method that will be used to change the circle’s position.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script> class Particle { constructor(x, y, r, w, h) { this.x = x; this.y = y; this.r = r; this.right = w - r; this.top = h - r; this.x_speed = Math.random() _ 10 - 5; this.y_speed = Math.random() _ 10 - 5; this.circ = null } init(container){ const namespace = "http://www.w3.org/2000/svg"; this.circ = document.createElementNS(namespace, "circle"); this.circ.setAttribute("cx", this.x); this.circ.setAttribute("cy", this.y); this.circ.setAttribute("r", this.r); this.circ.setAttribute("fill", "orange"); this.circ.setAttribute("stroke", "black"); this.circ.setAttribute("stroke-width", "3");this.r); container.appendChild(this.circ); } update() { this.x += this.x_speed; this.y += this.y_speed; } }</script>

Next we'll add the method we created earlier to bounce the particle off the walls.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <script> class Particle { constructor(x, y, r, w, h) { // constructor code } init(container){ // init code } update() { this.x += this.x_speed; this.y += this.y_speed; this.bounceIfNeeded(); } bounceIfNeeded() { if (this.x > this.right) { this.x = this.right; this.x_speed *= -1; } else if (this.x < this.r) { this.x = this.r; this.x_speed *= -1; } if (this.y > this.top) { this.y = this.top; this.y_speed *= -1; } else if (this.y < this.r) { this.y = this.r; this.y_speed *= -1; } } }</script>

Note that we're using the right and top variables for collision detection. It's a lot cleaner and more readable to use variables than hard-coded values.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <script> class Particle { constructor(x, y, r, w, h) { // constructor code } init(container){ // init code } update() { // update code } bounceIfNeeded() { // bounce code } draw(){ this.circ.setAttribute("cx", this.x); this.circ.setAttribute("cy", this.y); } }</script>

Now let’s put the particle class in action. We’ll need an array to hold all the particles, a variable for the number of particles, and a reference to the svg

1 2 3 4 5 6 7 8 9 10 11 12 13 14 <body> <svg id="particle-holder" ...> </svg> </body> <script> class Particle { // particle code } const particles = []; const num_particles = 30; const svg = document.querySelector("#particle-holder"); </script>

Let’s initialize all of the particles. We’ll pass each a starting x and y position (250, 250), a radius (15), and the size of the svg (500x500).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <body> <svg id="particle-holder" ...> </svg> </body> <script> class Particle { // particle code } const particles = []; const num_particles = 30; const svg = document.querySelector("#particle-holder"); initParticles(); function initParticles() { for (let i = 0; i < num_particles; i++) { const part = new Particle(250, 250, 15, 500, 500); part.init(svg); particles.push(part) } } </script>

Finally we’ll create a moveParticles method that will call each particle's update() method. Note that the moveParticles uses requestAnimationFrame to call itself every frame.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <body> <svg id="particle-holder" ...> </svg> </body> <script> class Particle { // particle code } const particles = []; const num_particles = 30; const svg = document.querySelector("#particle-holder"); initParticles(); moveParticles(); function initParticles() { for (let i = 0; i < num_particles; i++) { const part = new Particle(250, 250, 15, 500, 500); part.init(svg); particles.push(part) } } function moveParticles() { particles.forEach((part) => { part.update(); }) window.requestAnimationFrame(moveParticles) } </script>

Check out the full particle example on codepen