How to dynamically create animation paths through JavaScript?

Viewed 196

I have an animated background in the works.

What I'm aiming for is for the "bubbles" to travel in a random trajectory at a random velocity. Upon reaching the edge of the screen, it teleports back to the opposite side to continue the path.

I would do the animation through CSS, but I want the path each "bubble" follows to be random.

Something like this for each element.

Each element follows its own randomly generated path and repeats it indefinitely.

How would I achieve this effect through JavaScript?

This is the project in it's current state.

Codepen : https://codepen.io/JosephChunta/pen/WNrKxeY

Current JavaScript

const orderedNumber = document.querySelectorAll('.backgroundBubble');
var colors = [
  '#ff5b00', '#b8d500', '#795ced', 
  '#ab004a', '#21aec0', '#fe9300' 
];
for (li of orderedNumber) {
  var random_color = colors[Math.floor(Math.random() * colors.length)];
  li.style['background'] = random_color;
  var random_dimensions = Math.floor(Math.random() * 20 + 5);
  li.style['width'] = random_dimensions + "px";
  li.style['height'] = random_dimensions + "px";
  var random_left = Math.floor(Math.random() * 99 + 1);
  li.style['left'] = random_left + "%";
  var random_top = Math.floor(Math.random() * 99 + 1);
  li.style['top'] = random_top + "%";
}

"Bubble" HTML

<div class="context">
  <h1>Hello</h1>
</div>

<div class="area">
  <ul class="circles">
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
  </ul>
</div>
2 Answers

Here's a solution that might work for you:

  • Each element (circle) moves across a random path using the transform: translate( x, y ) CSS property

  • Each element gets a random x, y offset by which it moves in a slightly different trajectory

  • When each element hits the limits of the viewport (window.innerWidth = far right, window.innerHeight = bottom, 0 = top and left) it switches direction by negating the initial offset values offsetX and offsetY

  • The animation is currently being implemented using setInterval but a better candidate might be requestAnimationFrame.

(Take a look at the comments I've placed along with the newly inserted code.)

Codepen

const orderedNumber = document.querySelectorAll('.backgroundBubble');
var colors = [
  '#ff5b00', '#b8d500', '#795ced', 
  '#ab004a', '#21aec0', '#fe9300' 
];
const liData = [];

orderedNumber.forEach((li,index)=>{
  
  var random_color = colors[Math.floor(Math.random() * colors.length)];
  li.style['background'] = random_color;
  var random_dimensions = Math.floor(Math.random() * 20 + 5);
  li.style['width'] = random_dimensions + "px";
  li.style['height'] = random_dimensions + "px";
  var random_left = Math.floor(Math.random() * 99 + 1);
  li.style['left'] = random_left + "%";
  var random_top = Math.floor(Math.random() * 99 + 1);
  li.style['top'] = random_top + "%";
  // ADDING MOVEMENT VALUES: In here each element
  // gets some additional data that will be used to define its movement.
  liData[index] = {
    hasFlipped: false, // Has the element hit a viewport boundary? If yes, this property gets flipped: false -> true -> false
    targetX: 0, // Current x position. Starts at 0.
    targetY: 0, // Current y position. Starts at 0.
    offsetX: Math.floor( Math.random() * 10 ), // Random x offset by which the element will move across the x axis
    offsetY: Math.floor( Math.random() * 10 ) // Random y offset by which the element will move across the y axis
  }

});

setInterval(function(){
  orderedNumber.forEach((li,index)=>{
    // Next line will give us the top, left, right and bottom position of the element:
    const { top, left, right, bottom } = li.getBoundingClientRect();
    // Get the offsetX and offsetY so that we can move the element
    let { offsetX, offsetY } = liData[index];
    if ( liData[index].hasFlipped ){
      // Did the element just hit the top or left viewport boundaries?
      if ( top <= 0 || left <= 0 ){
        // ...if so, flip its movement direction
        liData[index].hasFlipped = false;
      }
      liData[index].targetX -= offsetX;
      liData[index].targetY -= offsetY;

    } else {
      // Did the element just hit the bottom, right viewport boundaries?
      // ...if so, flip its movement direction
      if ( bottom >= window.innerHeight || right >= window.innerWidth ){
        liData[index].hasFlipped = true;
      }
      liData[index].targetX += offsetX;
      liData[index].targetY += offsetY;
    }
      li.style.transform = `translate( ${liData[index].targetX}px,  ${liData[index].targetY}px )`;

  });

}, 50 )
@import url('https://fonts.googleapis.com/css?family=Exo:400,700');

*{
    margin: 0px;
    padding: 0px;
}

.context {
    width: 100%;
    position: absolute;
    top:50vh;
}

.context h1{
    text-align: center;
    color: #fff;
    font-size: 50px;
    font-family: 'Exo', sans-serif;
}


.area{
    background: #2a2e31;  
    background: -webkit-linear-gradient(to left, #8f94fb, #4e54c8); // #026e9f #03a9f4
    width: 100%;
    height:100vh;
}

.circles{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

.circles li{
    position: absolute;
    display: block;
    list-style: none;
    bottom: -150px;
    border-radius: 50%;
}
<div class="context">
  <h1>Hello</h1>
</div>

<div class="area">
  <ul class="circles">
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
  </ul>
</div>


How could the movement code be changed so instead of flipping it's movement direction, it bounces off the wall?

In that case, we can check whether the ball hits one of the boundaries, and if it hits one of the left/right sides, we flip the X sign (position -> negative -> positive -> ...) otherwise, if it hits one of the top/bottom sides we flip the Y sign.

Here's a sketch to make this clear:

enter image description here

And here's the refactored code to make this work:

const orderedNumber = document.querySelectorAll('.backgroundBubble');
var colors = [
  '#ff5b00', '#b8d500', '#795ced', 
  '#ab004a', '#21aec0', '#fe9300' 
];
const liData = [];

orderedNumber.forEach((li,index)=>{
  
  var random_color = colors[Math.floor(Math.random() * colors.length)];
  li.style['background'] = random_color;
  var random_dimensions = Math.floor(Math.random() * 20 + 5);
  li.style['width'] = random_dimensions + "px";
  li.style['height'] = random_dimensions + "px";
  var random_left = Math.floor(Math.random() * 99 + 1);
  li.style['left'] = random_left + "%";
  var random_top = Math.floor(Math.random() * 99 + 1);
  li.style['top'] = random_top + "%";
  // ADDING MOVEMENT VALUES:
  liData[index] = {
    targetX: 0,
    targetY: 0,
    offsetX: Math.floor( Math.random() * 10 ),
    offsetY: Math.floor( Math.random() * 10 ),
    directionX: 1, // Define each ball's direction using a multiplier. Positive means left-to-right. Negative means right-to-left.
    directionY: 1  // Same here, but for top-to-bottom and vice versa
  }

});

setInterval(function(){
  orderedNumber.forEach((li,index)=>{
    
    const { top, left, right, bottom } = li.getBoundingClientRect();
    let { offsetX, offsetY } = liData[index];
   
    // If we've just hit the top or bottom boundaries, we'll flip the Y direction:
    if ( top <= 0 || bottom >= window.innerHeight ){ 
      liData[index].directionY = -liData[index].directionY; 
    } 
    // If we've just hit the left or right boundaries, we'll flip the X direction:
    if ( left <= 0 || right >= window.innerWidth){ 
      liData[index].directionX = -liData[index].directionX;
    } 

    // The multiplier, either 1 or -1, defines the balls direction on each axis:
    liData[index].targetX += offsetX * liData[index].directionX; 
    liData[index].targetY += offsetY * liData[index].directionY; 

    li.style.transform = `translate( ${liData[index].targetX}px,  ${liData[index].targetY}px )`;

  });

}, 50 )
@import url('https://fonts.googleapis.com/css?family=Exo:400,700');

*{
    margin: 0px;
    padding: 0px;
}

.context {
    width: 100%;
    position: absolute;
    top:50vh;
}

.context h1{
    text-align: center;
    color: #fff;
    font-size: 50px;
    font-family: 'Exo', sans-serif;
}


.area{
    background: #2a2e31;  
    background: -webkit-linear-gradient(to left, #8f94fb, #4e54c8); // #026e9f #03a9f4
    width: 100%;
    height:100vh;
}

.circles{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

.circles li{
    position: absolute;
    display: block;
    list-style: none;
    bottom: -150px;
    border-radius: 50%;
}
<div class="context">
  <h1>Hello</h1>
</div>

<div class="area">
  <ul class="circles">
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
    <li class="backgroundBubble"></li>
  </ul>
</div>

Codepen


Next steps:

  • You can use the requestAnimationFrame instead of setInterval() to get better performance.

  • Experiment with various random schemes for the offset x and y values, in order to introduce more trajectories and elements moving at different speeds

This might work but instead of teleporting to the other end of the screen it bounces back creating a more interesting animation

 const orderedNumber = document.querySelectorAll('.backgroundBubble');
var colors = [
  '#ff5b00', '#b8d500', '#795ced', 
  '#ab004a', '#21aec0', '#fe9300' 
];
const liData = [];

orderedNumber.forEach((li,index)=>{
  
  var random_color = colors[Math.floor(Math.random() * colors.length)];
  li.style['background'] = random_color;
  var random_dimensions = Math.floor(Math.random() * 20 + 5);
  li.style['width'] = random_dimensions + "px";
  li.style['height'] = random_dimensions + "px";
  var random_left = Math.floor(Math.random() * 99 + 1);
  li.style['left'] = random_left + "%";
  var random_top = Math.floor(Math.random() * 99 + 1);
  li.style['top'] = random_top + "%";
  // ADDING MOVEMENT VALUES:
  liData[index] = {
    hasFlipped: false,
    targetX: 0,
    targetY: 0,
    offsetX: Math.floor( Math.random() * 10 ),
    offsetY: Math.floor( Math.random() * 10 )
  }

});

setInterval(function(){
  orderedNumber.forEach((li,index)=>{
    const { top, left, right, bottom } = li.getBoundingClientRect();
    let { offsetX, offsetY } = liData[index];
    if ( liData[index].hasFlipped ){
      if ( top <= 0 || left <= 0 ){
        liData[index].hasFlipped = false;
      }
      liData[index].targetX -= offsetX;
      liData[index].targetY -= offsetY;

    } else {
      if ( bottom >= window.innerHeight || right >= window.innerWidth ){
        liData[index].hasFlipped = true;
      }
      liData[index].targetX += offsetX;
      liData[index].targetY += offsetY;
    }
      li.style.transform = `translate( ${liData[index].targetX}px,  ${liData[index].targetY}px )`;

  });

}, 50 )

just replace that with your javascript and it will work

Related