How We Did It: Parallax Word Cloud

At 2wav, we like parallax effects, assuming they're done tastefully. In fact, one of the staple features of our web site has been a parallax scrolling word cloud featuring some of our favorite technologies. For our 2016 site, we wanted to take this idea and apply it to individual portfolio projects.

One challenge we've always found with parallax effects is the amount of work it takes to get it looking good. First, you need to find a good library (we've used skrollr in past projects), then you need to identify how each object will scroll, which usually means making decisions for each element. For our new site, we decided to explore automating that work.

Components

We identified two main components of a parallax effect that create the illusion of depth:

  1. Object Size: Larger objects appearing near the viewer.
  2. Object Speed: Fast moving objects "sliding" past slow-moving ones.

With that in mind, we need our solution to automate at least those two properties, though that will clearly be insufficient to actually look good.

Object size is is easy, just vary the size between elements. For object speed, we had the option of either varying the animation time of each element or the distance each element travelled (objects with a larger distance to travel move faster). We decided on the latter, mostly because it cleaned up the CSS.

Markup/CSS

Here's a sample of what a word cloud might look like for us:

<div class="work_words">
  <ul>
    <li>Automation</li>
    <li>Meteor</li>
    <li>Systech</li>
    <li>Device Control</li>
    <li>Z-Wave</li>
    <li>Thingworx</li>
    <li>Scripting</li>
    <li>Universal Control</li>
    <li>Multi-Site</li>
    <li>Internet of Things</li>
  </ul>
</div>

And here's the SCSS they share:

.work_words {
  display: block;
  width: 30%;
  height: 100%;
  position: absolute;
  right: 0;
  top: 0;
  ul {
    list-style-type: none;
  }
  li {
    position: absolute;
    font-weight: bold;
    list-style-type: none;
    transform: translateY(100vh);
    transition: transform 1700ms ease-out;
    visibility: hidden;
  }
}

Of note, we're using transform: translate(); to move these elements. It performs better than top, especially on Safari. The web site does fall back to top if modernizr detects that the browser doesn't support css transforms.

Javascript

As we said, the very least, we need to automate the size and distance of each li. And we want a function that will do that for whatever work we happen to care about. In our case, we've ended up splitting it into two functions, a setup function and a move function. We used jQuery, but you could certainly do this without.

Helpers

First, we need a couple helper functions to generate random integers or reals in the bounds we specify:

// Returns an integer between two values (inclusive)
// From http://stackoverflow.com/a/7228322
function randomIntFromInterval(min,max) {
    return Math.floor(Math.random()*(max-min+1)+min);
}

// Get a random real instead
function getRandomArbitrary(min, max) {
  return Math.random() * (max - min) + min;
}

Setup

Here's how the setup function works. You feed it a work number (the id of the work element) and it uses variables you provide to assign translate and font-size css values to each li, as well as a data value called top_target, which we'll use in a second. Here are the variables we set:

  • top_bound: The absolute highest the item can be.
  • bottom_bound: The lowest we allow an item.
  • randomness: How much variability we want in placement.
  • left_push: How far to the left we push each element (set using randomIntFromInterval).
  • font_size: The size of the text (set using randomIntFromInterval).
function cloud_setup(work_number) {
// Variables
  var top_bound = 5;
  var bottom_bound = 70;
  var randomness = .2;
  var left_push = randomIntFromInterval(15,50);
  var font_swing = randomIntFromInterval(100,175);
  var opacity_swing = randomIntFromInterval(2,4);

Next, we figure out where everything can fit.

// We only run this if we're on desktop (the word cloud is hidden otherwise)
  if ( $('body').width() >= 920 ) {
    // First, generate an array of acceptable top positions
    // Step one: figure out how many lis there are
    var li_count = $('#work' + work_number + ' .work_words li').length;

    // We're going to distribute these items between these top and bottom bounds:

    // So let's figure out how much space we need
    var usable_area = bottom_bound - top_bound;
    // Figure out how much space each item should take up
    var envelope = usable_area / li_count;
    //  And then we need to know the center point
    var envelope_center = envelope / 2;
    // Now we need to make an array of these center points, plus some randomness
    var top_array = [];   
      var variance_top = 1 + randomness;
      var variance_bottom = 1 - randomness;

    for (i = 1; i < li_count+1; i++) {
        var variance = getRandomArbitrary(variance_bottom,variance_top);
        var array_entry = (top_bound + envelope_center) + ((i-1)*envelope);
        array_entry = array_entry * variance;
        top_array.push(array_entry)
    }

Ok, so that's done. Next step? Setting up each word. The first thing that happens is setting the word offscreen (top or bottom, depending on which way we're scrolling (tracked with the current_work and last_work variables.

$('#work' + work_number + ' .work_words li').each(function(){
      // Where the word starts (generally offscreen, so >100)
      // We need to know if the words will be moving up or down
      // First a special case for work 1, which needs extra distance due to the section sliding into view
      if ( current_work == 1 ) {
        var top_start = randomIntFromInterval(410,480); 
      }
      else if ( current_work >= last_work ) {
        var top_start = randomIntFromInterval(110,180); 
      }
      else {
        var top_start = randomIntFromInterval(-10,-80);
      }

      
  // Now we start grabbing values from the top_array at random
  var ri = Math.floor(Math.random() * top_array.length); 
  var top_end = top_array.splice(ri, 1); 
  // Assign these values, but we need to figure out if we support transform!
  if ( $('html').is('.supports, .csstransforms') ) {
    $( this ).css({'transform': 'translateY('+top_start+'vh)', 'left': left_push +'%', 'font-size': font_swing + '%', 'opacity': '.' + opacity_swing});         
  }
  // If not, we animate using top, which is less performant (to say the least)
  else {
    $( this ).css({'top': top_start+'vh)', 'left': left_push +'%', 'font-size': font_swing + '%', 'opacity': '.' + opacity_swing});
  }

Ok, last bit! We need to give each li a data entry telling it how far to move.

// Give each a data entry that tells it where to end up when the show_work function is fired
  $( this ).data('top_target',top_end);

// All sorts of closing tags here, just copy and paste with glee!   
    });
  }
  
}

Moving the Cloud

With that massive block done, here's what the actual function to move the thing looks like. It's much more simple, we promise—all you do is feed it a work number and it moves the cloud.

function move_word_cloud(work_number) {
  // Grabs each word in the cloud of the specified work
  $('#work' + work_number + ' .work_words li').each(function(){
    // Reads the data assigned in the word setup function and finds out where the word is supposed to go
    var new_top = $(this).data('top_target');
    // Animates it there
    $( this ).animate({
        
        top: new_top + "%"
      }, 3000);
  });
};

And there it is! The part of this function that we really like is that it allows us to have a different presentation of the word cloud each time, so each time a visitor scrolls a work into view, they seem something new to delight them.