Advertisement

JavaScript Animation That Works (Part 3 of 4)

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In our first post in this series, we introduced spriting, and how it can be used to do easy and effective cross-browser animation on the web. In the second post, we got some simple animations up and running, although they had a fair amount of bugs and the code was certainly not ready to go live.

Today, we are going to address those bugs and clean up our code so we can publish it on a page without fear of crashing any code using a method called encapsulation.

Variable Scope

To really explain what was so wrong with the code in our last step, and why encapsulation is important, we need to first explain variable scope.

Imagine you are working with the code below. You have a helpful variable in your function do_this(), and you would like to use that same variable in another function, do_that(), but you run into a little problem.

function do_this(){
  var very_helpful_variable = 20;
  ...
  // This shows '20', just like you expect
  alert(very_helpful_variable); 
}

function do_that(){
  alert(very_helpful_variable); // But this shows 'undefined'!
}

Your variable works great within the function it was declared, but outside of that function, it is as if it never existed! This is because do_that() is not within the scope of the variable very_helpful_variable.

Variables are only available inside the block of code where they are declared, this is their scope. Once that block of code is finished running, its variables are erased.

Take a look at these examples:

var w = 1;

function a(){
  var x = 2;
  
  function b(){
    var y = 3;
    
    alert(w); // works
    alert(x); // works
    alert(y); // works
    alert(z); // undefined
  }
  
  alert(w); // works
  alert(x); // works
  alert(y); // undefined
  alert(z); // undefined
}

function c(){
  var z = 4;
  
  alert(w); // works
  alert(x); // undefined
  alert(y); // undefined
  alert(z); // works
  
  b(); // undefined
}

alert(w); // works
alert(x); // undefined
alert(y); // undefined
alert(z); // undefined

First we have the variable w, which is declared outside of any functions. It is called a global variable, and it will work anywhere because its scope is the entire document.

Next is the variable x, since it is declared inside of the function a(), it will only work inside of that function. This also includes working inside function b(), since b() is inside of a().

However, a variable defined inside of b() (like y) will not work in the outer function, since that is outside of its scope.

You may also notice that we attempted unsuccessfully to call the function b() from inside the function c(); function names follow the same rules as other variables.

One other quirk with JavaScript, if we just start using a variable name inside a function without declaring it with the keyword var, then the browser will assume that that variable should be global. So, if you don't make sure you always declare your variables with the var keyword, you will end up with global variables and not realize it!

So, to summarize: whenever we declare a variable, we can use it within that block of code or inside any nested blocks inside it. If we try to use it outside of its scope, the value is set to undefined.

This is why in our last post, we put the timer variable outside the functions that used it, since we needed to still grab that variable after the functions had ended.

var timer; // This is a global variable
  
function run_right(stage, left){
  ...
  timer = setTimeout(function(){run_right(2, left);}, 200);
  ...
}

function stop_running(){
  document.getElementById('j').style.backgroundPosition = "0px 0px";
  // If 'timer' wasn't set as global, we couldn't stop it here
  clearTimeout(timer);
}

In order to clear the timer, we needed stop_running() to be within scope for the variable timer. So, we made timer a global variable that could be used everywhere, what could be wrong with that?

The Problem With Global Variables

In any given scope, it is impossible to have two items that are called the same thing. If you were to try to have two different variables with the same name, the browser would just write over one of them. So, if we had a variable named timer, and had a separate variable also named timer that was called within the same scope, one of them would delete and take the place of the other, and we would have havoc in our code. If we had a global variable called timer, then it would interfere with any other variable named timer contained anywhere in the page - including any and all attached JavaScript libraries and external files.

This is a huge source of headaches, you have just seen a really neat JavaScript plug-in somewhere, and you download it onto your site, and suddenly all of your other plug-ins crash... One of the plug-ins was sloppy with global variables, happened to share the same name with something else, your browser trips over itself, and the whole page comes to a grinding halt.

What makes this even worse is that you will never notice this problem when you first test the code. Like our animation code from the last post, it will work great by itself. But, the more pieces you add, the more likely the chances of having a naming conflict, and you will be stuck sorting through a dozen different JavaScript files trying to figure out which two aren't getting along.

Now you may be asking yourself, "Global variables are so convenient! What if I just watch my code really carefully and make sure I don't have any conflicts?" That might work in a perfect world, but in reality you will often have several people working on different parts of the same page, or have to come back and update different parts of your code years later, or even have code from third-parties on your page that will be out of your control (like paid advertising).

So, in short, you wouldn't want global variables any more than you would want exposed wiring along the walls of your house or exposed machinery in your car, it's just a matter of time before something happens that gums up the works. Thankfully, there is a better way that avoids these pitfalls.

Encapsulation

We can have all of the benefits of global variables without the problems by using a technique called encapsulation. Think of it like you are building a wall around your code with only a few special doors, nothing can get in or out of that code unless you specifically allow it.

JavaScript has a type of variable called an object. Objects are user-defined collections of data that contain information and functions (referred to as properties and methods, respectively). We are going to write a function that creates a special object that has all the functions we need "baked" into it, and it will even allow us to have more than one robot without having to duplicate our code!

We start by defining a new function with a variable name. We will need to pass the variable a few arguments, I'm going to pass it the HTML element that we will be animating, plus some unique values for running speed and jump height so we can vary those from robot to robot.

var RobotMaker = function(robot, run_speed, jump_height){

  // We will put all of our functions and variables in this area. 
  // This is inside our 'impenetrable' wall, so nothing in this 
  // area will conflict with other code.    
  
  return {
    // Inside here, we place all of our 'doors' ... 
    // these will be the only way anything can get
    // in or out of this code.
    // And, since this is still within the same 'scope' 
    // as RobotMaker, we can use any variables mentioned above! 
  }
}

Since we are going to be putting all of our functions inside of our new "wall", now would be a good time to revisit what bugs we had with the original code. (You can see that in action here)

You may notice that if we click two run buttons (or a run and jump button) without clicking the Stop button in-between, J will continue to do both actions. A second problem is that no matter which direction J is facing, when we click the Jump or Stop button, he faces right every time. Finally, if you click the Jump button again while J is falling from a first jump, he will continue to fall through the page in an endless loop.

In order to address these things, we need to be more specific about what we want to happen with each of our functions:

When we click Run Right:

  1. If J is jumping, do nothing and continue the jump
  2. If J is running left, stop him running left
  3. Run to the right and animate to the proper frame
  4. If J reaches the end of the stage, stop running and stand facing right

When we click Run Left:

  1. If J is jumping, do nothing and continue the jump
  2. If J is running right, stop him running right
  3. Run to the left and animate to the proper frame
  4. If J reaches the end of the stage, stop running and stand facing left

When we click Stop Running:

  1. If J is jumping, do nothing and continue the jump (we don't want to stop in midair!)
  2. If running right or left, stop running
  3. If facing right, stand facing right. If facing left, stand facing left

When we click Jump:

  1. If J is jumping, do nothing and continue the jump (we don't want to jump again in midair!)
  2. If J is running right or left, stop running
  3. Start the jump. If J is facing right, jump facing right. If facing left, jump facing left
  4. Land facing the same direction as the jump

First of all, we are going to add a few more variables now. Since the timer should behave differently for running and jumping, we will have two separate timers. We also want to introduce a boolean (true/false) variable to track if we should be facing left or right, and we will make a stage variable just to save us from having to type out the full element name.

// Inside the RobotMaker function ... 
var stage = document.getElementById('stage');
var run_timer, jump_timer;
var face_right = true;

Then we are going to add back in our functions for running right, running left, and jumping. These are going to be mostly the same, with a few differences. First of all, all the references to the element that we are animating can be replaced with the variable robot (which will be passed as one of the arguments in the RobotMaker function). Second, we have made some slight changes to the running speed and jumping height in the functions so we can vary those by passing different values. Third, we are using the face_right variable to track which direction J is facing (and in the jumping function, using face_right to decide which jumping sprite to show). Finally, we are using separate timers for running and jumping.

// Inside the RobotMaker function ... 
function run_r(phase, left){
  face_right = true;
  if ((left + (15 * run_speed)) < (stage.offsetWidth - robot.offsetWidth)){
    
    left = left + (15 * run_speed);
    robot.style.left = left+"px";
    switch (phase){
      case 1:
        robot.style.backgroundPosition = "-40px 0px";
        run_timer = setTimeout(function(){run_r(2, left);}, 200);
        break;
      case 2:
        robot.style.backgroundPosition = "-80px 0px";
        run_timer = setTimeout(function(){run_r(3, left);}, 200);
        break;
      case 3:
        robot.style.backgroundPosition = "-120px 0px";
        run_timer = setTimeout(function(){run_r(4, left);}, 200);
        break;
      case 4:
        robot.style.backgroundPosition = "-80px 0px";
        run_timer = setTimeout(function(){run_r(1, left);}, 200);
        break;
    }
  } else {
    robot.style.backgroundPosition = "0px 0px";
  }
}  
  
function run_l(phase, left){
  face_right = false;
  if (0 < robot.offsetLeft - (15 * run_speed)){
    
    left = left - (15 * run_speed);
    robot.style.left = left+"px";
    switch (phase){
      case 1:
        robot.style.backgroundPosition = "-40px -50px";
        run_timer = setTimeout(function(){run_l(2, left);}, 200);
        break;
      case 2:
        robot.style.backgroundPosition = "-80px -50px";
        run_timer = setTimeout(function(){run_l(3, left);}, 200);
        break;
      case 3:
        robot.style.backgroundPosition = "-120px -50px";
        run_timer = setTimeout(function(){run_l(4, left);}, 200);
        break;
      case 4:
        robot.style.backgroundPosition = "-80px -50px";
        run_timer = setTimeout(function(){run_l(1, left);}, 200);
        break;
    }
  } else {
    robot.style.backgroundPosition = "0px -50px";
  }
}
  
function jmp(up, top){
  if (face_right){
    robot.style.backgroundPosition = "-160px 0px";
  } else {
    robot.style.backgroundPosition = "-160px -50px";
  }

  if (up && (robot.offsetTop > (20 * (1 / jump_height)))){
    top = top - (top * .1);
    robot.style.top = top+"px";
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else if (up) {
    up = false;
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else if (!up && (robot.offsetTop < 115)){
    top = top + (top * .1);
    robot.style.top = top+"px";
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else {
    robot.style.top = "120px";
    if (face_right){
      robot.style.backgroundPosition = "0px 0px";
    } else {
      robot.style.backgroundPosition = "0px -50px";
    }

    jump_timer = false;
  }
  
}

All of these variables and functions are inside of our "wall", so we now need to make "doors" to be able to access only what we need. These four "doors" will be object methods for the same four functions we had previously and will reference the protected functions above. Also, we will complete our bug fixing by checking in each function if the jump_timer is going, and then making sure to clear the run_timer. Remember, these two timers are in scope anywhere inside of the RobotMaker() function, so we can use them here. However, since they are not global variables, we won't run into any trouble with them elsewhere.

// Inside the RobotMaker function ... 
return {
  run_right : function(){
    if (!jump_timer || jump_timer == undefined){
      clearTimeout(run_timer); 
      run_r(1, robot.offsetLeft);
    }
  },
  
  run_left : function(){
    if (!jump_timer || jump_timer == undefined){
      clearTimeout(run_timer); 
      run_l(1, robot.offsetLeft);
    }
  }, 
  
  stop_running : function(){
    if (!jump_timer || jump_timer == undefined){
      clearTimeout(run_timer);
      if (face_right){
        robot.style.backgroundPosition = "0px 0px";
      } else {
        robot.style.backgroundPosition = "0px -50px";
      }
    }
  },
  
  jump : function(){
    if (!jump_timer || jump_timer == undefined){
      clearTimeout(run_timer);
      jmp(true, robot.offsetTop);
    }
  } 
  
}

Now that we have written a function that creates objects, we can use it as many times as we like to make objects that have the animation properties we want. At the bottom of our page, we will declare two new RobotMaker objects, and pass them the element we want to animate, a running speed, and a jumping height.

var j = RobotMaker(document.getElementById('j'), 1, 1);
var j2 = RobotMaker(document.getElementById('j2'), .8, 5);

Now we have no danger of anything in the RobotMaker() function leaking out and interfering with our code, and we can still get to the functions we want through the "doors" that we installed like this:

<input type="button" value="Run Left" onclick="j.run_left();" />

So, now you can see the finished product on the hyrgo Pen.

Notice how there is no longer any issues with the functions interfering with one another, and you can operate each robot individually without affecting the other. Encapsulation is an incredibly important technique, and you should really become familiar with it if you want to do any interactive web design.

If you'd like, please checkout all of this code, fully commented, and you can get the sprites using the following links: here's the first sprites and here's the second ones. Please note that in order for the same code to work with both sprites, I needed to make the second sprite in the exact same format and dimensions as the first.

Conclusion

So that wraps up part three of spriting! In our next and final post, I will replace those buttons with making our robots follow the mouse around the screen, and show you how to set up event listeners and enable support across browsers and touch devices.

Advertisement