JavaScript Animation that Works (Part 4 of 4)

By Steven Riche

In the first part of this series, we introduced the idea of using spriting as an easy, cross-browser way of having interactive javascript animation for the web. In the second part, we got some animation working, and in the third we cleaned up our code and made it ready for the web.

Introduction of JavaScript Animation

Now, in our final part today, we will walk through setting up event handlers so that instead of responding to clicked buttons, our robots will follow the mouse around the screen. In the process, we will also talk about making the code cross-browser friendly and touch screen enabled.

If you take a look at our code from last time, you will see that while the code runs well (and with multiple robots), there isn’t a very easy way to tell the code to run.

Event Handlers

Event handlers are commands that tell certain code to run when certain events are triggered. For example, you could have my_function() run whenever a user clicks on your div with the id 'my_div'. Or, you could have my_other_function() run whenever a user moves their mouse over 'my_other_div'.

In theory, this is a pretty simple and straightforward idea. Unfortunately, once you start getting different browsers involved, this can get a bit confusing. In an ideal world, every web browser would interpret the same code and HTML in the same way, and developers would write code one time and it would work the same for every user. In the real world, different browsers may have completely different commands to do the same thing (*cough* *cough* Internet Explorer), and so sometimes trying to get a single piece of code to run the same on all browsers can feel like herding cats. Recently, the situation has been getting much better, as Chrome, Firefox, Safari, and Opera all respond very similarly to code, Internet Explorer 9 and 10 have become much more in line with standards than earlier versions, and almost no one uses Internet Explorer 7 or 6 anymore. So, for our code, we will be getting event handlers to work for both modern browsers and Internet Explorer 8.

As a side note, this is a case where it really pays to use a robust JavaScript library, such as jQuery. jQuery does all the work for you in cross-browser testing, so you will only need to enter one command and the jQuery library will translate it for each browser behind the scenes. Additionally, many of the commands in jQuery are much more intuitive and simpler than the core JavaScript as well.

But, since I am stubborn, and since this is a learning opportunity, we are going to continue on the hard way and do all of this solely with JavaScript and no dependencies!

Page Interaction

So, our first step will be to decide how exactly we want to interact with the page. When I move my mouse over the stage area, I want all of the robots to run towards the mouse. When they reach the mouse, or if the mouse is directly above them, I want them to stop running. If the mouse crosses over them, I want them to jump. And finally, when the mouse leaves the stage area, I want them to stop running. We will start with attaching these events inside the RobotMaker function:

stage.addEventListener('mousemove', stage_mousemove_listener, false);
robot.addEventListener('mouseover', robot_mouseover_listener, false);
stage.addEventListener('mouseout', stage_mouseout_listener, false);

So, in the above lines, we have said that whenever the user moves the mouse inside the stage element, we will trigger a function called stage_mousemove_listener() (notice we do not include the parentheses in the command). Similarly, when the user moves the mouse over the robot element, it triggers robot_mouseover_listener(), and when the user moves the mouse outside of the stage, it triggers stage_mouseout_listener().

Unfortunately, as we mentioned before, Internet Explorer 8 and below has a (similar but) different command to do the same thing, so we will need to test to know which command the user’s browser will understand and do that method.

if (stage.addEventListener){ // We will test to see if this command is available
  stage.addEventListener('mousemove', stage_mousemove_listener, false);
  robot.addEventListener('mouseover', robot_mouseover_listener, false);
  stage.addEventListener('mouseout', stage_mouseout_listener, false);
} else { // If not, we have to use IE commands
  stage.attachEvent('onmousemove', stage_mousemove_listener);
  robot.attachEvent('onmouseover', robot_mouseover_listener);
  stage.attachEvent('onmouseout', stage_mouseout_listener);	
}

You may notice that the format of the commands is very similar, but has some major differences – one says 'addEventListener' while the other says 'attachEvent'. One says 'mousemove' while the other says 'onmousemove'. One requires a third parameter, while the other only uses two. Mixing any of these up will cause the command to not run. These are the kinds of things that will make you want to bang your head against the wall. Unfortunately, this isn’t the end of the extra coding we will need to do for cross-browser capability.

Listening Functions

Next, we are going to write the listening functions. We will start with the function that is triggered when the user mouses over the stage. Since this is a mousemove listener, this function will trigger every time the mouse is moved inside the stage area (meaning it will trigger several times a second while the mouse is moving). This function will need to compare the location of the robot with the location of the mouse, and make the robot behave accordingly. Each time the function is triggered, it will check if the robot needs to continue running the same direction or change behaviors. So, it will need to be something like this:

// Inside of RobotMaker

// We will need to introduce a few extra variables to track
var mouseX; // For tracking horizontal mouse position
var running_dir = ''; // For tracking if (and where) robot is currently running
var stageOffset; // For tracking the position of the stage

function stage_mousemove_listener(e){

  // Find the horizontal position of the mouse inside of the stage ...  
  // That position will be saved in 'mouseX'

  // Then we compare 'mouseX' to the robot, and decide if we need to run differently
  if (((robot.offsetLeft + (15 * run_speed)) < (mouseX - robot.offsetWidth)) && running_dir !== 'r' && (!jump_timer || jump_timer === undefined)){ 
    // If the mouse is in the stage and to the right of the robot, make run right, if not already
    running_dir = 'r';
    clearTimeout(run_timer);
    run_r(1, robot.offsetLeft);
  } else if ((mouseX < robot.offsetLeft - (15 * run_speed)) && running_dir !== 'l' && (!jump_timer || jump_timer === undefined)) {
    // If the mouse is in the stage and to the left of the robot, make run left, if not already
    running_dir = 'l';
    clearTimeout(run_timer);
    run_l(1, robot.offsetLeft);
  } else if ((robot.offsetLeft < mouseX) && ((robot.offsetLeft + robot.offsetWidth) > mouseX) && running_dir !== '' && (!jump_timer || jump_timer === undefined)) {
    // If the mouse is in the stage and over a robot, stop and clear running_dir
    running_dir = '';
    clearTimeout(run_timer);
    if (face_right){
      robot.style.backgroundPosition = "0px 0px";
    } else {
      robot.style.backgroundPosition = "0px -50px";
    }
  }
  // If none of the above is true, then we let our current behavior continue
}

So, in the function above, once we are able to find mouseX, we compare it to where the robot is and trigger or stop the different running functions as needed. Unfortunately, finding mouseX is a bit tricky, since mouse position is another thing that different browsers do differently. In lieu of (more) complicated and long-winded explanations, here is the cross-browser method for finding mouseX, as inspired from the excellent Quirksmode blog (which is a great source for more advanced JavaScript studying).

function stage_mousemove_listener(e){
  var posX = 0;
  if (!e){
    var e = window.event;
  }

  if (e.pageX) {
    posX = e.pageX;
  } else if (e.clientX) {
    posX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  }
  mouseX = posX - stageOffset.xpos; // And we find mouseX!	
}

We have an argument called e in the function, even though we don’t pass it anything. Since this is an event listener, we can have an automatic variable called e that stores event information like mouse data. But because different browsers store it differently, we have to add a lot of extra steps.

We finally find mouseX by finding posX (which is the x-position of the mouse on the page) and subtracting how far the stage is from the far left of the page (stored in stageOffset.xpos). This gives us how far from the left edge of the stage the mouse is, which we can directly compare with robot.offsetLeft. Since the stage could be located differently around the page depending on the layout, we will also need to find the exact pixel offset of the stage for the function to be accurate, and store that information in stageOffset. Fortunately there is a neat trick we can use to find an element’s absolute offset with this function from Vishal Astik’s blog.

// Inside RobotMaker
var x = 0;
var y = 0;
function find_stage_offset (el){
  x = el.offsetLeft;
  y = el.offsetTop;
  el = el.offsetParent;

  while(el !== null) {
    x = parseInt(x) + parseInt(el.offsetLeft);
    y = parseInt(y) + parseInt(el.offsetTop);
    el = el.offsetParent;
  }

  return {xpos: x, ypos: y};
}
var stageOffset = find_stage_offset(stage);

So now that we have written the mousemove listener, the others will be much easier. For the robot mouseover listener, we only need to check if the robot is already jumping, and if not, stop the run timer and make it jump.

function robot_mouseover_listener(){
  if (!jump_timer || jump_timer === undefined){
    clearTimeout(run_timer);
    jmp(true, robot.offsetTop);
  }
}

The mouseout listener is also pretty simple. We just need to reset some of our variables we are using to track the robot, and if the robot isn’t jumping, return the robot to the standing sprite.

function stage_mouseout_listener(){
  mouseX = undefined;
  running_dir = '';
  if (!jump_timer || jump_timer === undefined){
    clearTimeout(run_timer);
    if (face_right){
      robot.style.backgroundPosition = "0px 0px";
    } else {
      robot.style.backgroundPosition = "0px -50px";
    }
  }
}

Animation Functions

The functions that animate the running and jumping motions haven’t changed much this time. We have just added the tracking variable running_dir, taken out the statement that checks if the robot is about to hit the wall (since this is redundant with our mouseout function), and add a bit of code to the jump function that checks again if the robot should start running if the mouse is within the stage after it lands from a jump. Here’s the final code (quite large):

function run_r(phase, left){
  face_right = true;
  running_dir = 'r';
  if ((left + (15 * run_speed)) < (mouseX - robot.offsetWidth)){ // if mouse is to the right, run

    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 if ((left + (15 * run_speed)) < mouseX) { // if mouse if above, stop
    robot.style.backgroundPosition = "0px 0px";
    running_dir = '';
} else { // if mouse is to the left, run left
    running_dir = 'l';
    run_l(1, robot.offsetLeft);
  }
}

function run_l(phase, left){
  face_right = false;
  running_dir = 'l';
  if (mouseX < robot.offsetLeft - (15 * run_speed)){ // if mouse is to the left, run

    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 if (mouseX < (robot.offsetLeft + robot.offsetWidth - (15 * run_speed))){ // if mouse overhead, stop
    robot.style.backgroundPosition = "0px -50px";
    running_dir = '';
} else { // if mouse is to the right, run right
    running_dir = 'r';
    run_r(1, robot.offsetLeft);
  }
}

function jmp(up, top){
  running_dir = '';
  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 * 0.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 * 0.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;
    if (mouseX !== undefined){
      if (((robot.offsetLeft + (15 * run_speed)) < (mouseX - robot.offsetWidth)) && running_dir !== 'r'){ 
        // make run right, if not already
        running_dir = 'r';
        clearTimeout(run_timer);
        run_r(1, robot.offsetLeft);
      } else if ((mouseX < robot.offsetLeft - (15 * run_speed)) && running_dir !== 'l') {
        // make run left, if not already
        running_dir = 'l';
        clearTimeout(run_timer);
        run_l(1, robot.offsetLeft);
      }
    }
  }
}

So, now, we have our rewritten functions that work great across all browsers … unless those browsers have touch input. We still have a bit more to go to make our robots run on everything. Since touch screens behave a bit differently, we will need to do some extra coding on our event listeners.

Supporting Touch Screens

We need to make some new rules for touch screens: If the screen is touched anywhere in the stage, the robot will run to that spot until the finger is lifted. If the user touches the robot, the robot will jump. First of all, we will add some extra touch event handlers to our earlier function, and we are going to write the code in such a way that it will run automatically whenever the RobotMaster function is called.

(function (){
  if (stage.addEventListener){
    stage.addEventListener('touchstart', stage_mousemove_listener, false);
    stage.addEventListener('touchmove', stage_mousemove_listener, false);
    stage.addEventListener('touchend', stage_mouseout_listener, false);

    stage.addEventListener('mousemove', stage_mousemove_listener, false);
    robot.addEventListener('mouseover', robot_mouseover_listener, false);
    stage.addEventListener('mouseout', stage_mouseout_listener, false);
  } else {
    stage.attachEvent('onmousemove', stage_mousemove_listener);
    robot.attachEvent('onmouseover', robot_mouseover_listener);
    stage.attachEvent('onmouseout', stage_mouseout_listener);
  }
})();

We won’t have to worry about the touch listeners being in the Internet Explorer 8 format, and if any device doesn’t have touch support it will ignore the listeners. Now we will need to update the stage_mousemove_listener() function to behave differently if the browser has touch capability.

function stage_mousemove_listener(e){	
/*
 * First we check if this is a touch screen device (if it has e.touches)
 */
  if (e.touches){
    e.preventDefault(); // we want to cancel what the browser would usually do if touched there
    // If the touch was within the boundaries of the stage...
    if ((e.touches[0].pageX > stageOffset.xpos) 
    && (e.touches[0].pageX < (stageOffset.xpos + stage.offsetWidth))
    && (e.touches[0].pageY > stageOffset.ypos)
    && (e.touches[0].pageY < (stageOffset.ypos + stage.offsetHeight))){
      // we set the mouseX to equal the px location inside the stage
      mouseX = e.touches[0].pageX - stageOffset.xpos; 
    } else { // if the touch was outside the stage, we call the mouseout listener
      stage_mouseout_listener();
    }

    /*
     * If the touch is directly on the robot, then we stop the run timer and make the robot jump
     */
    if ((e.touches[0].pageX > robot.offsetLeft) && (e.touches[0].pageX < (robot.offsetLeft + robot.offsetWidth))
    && (e.touches[0].pageY > (stageOffset.ypos + stage.offsetHeight - robot.offsetHeight))
    && (e.touches[0].pageY < (stageOffset.ypos + stage.offsetHeight))
    && (!jump_timer || jump_timer === undefined)){
      clearTimeout(run_timer);
      jmp(true, robot.offsetTop);
    }

  } else { // Finding the mouseX for non-touch devices...
    // All of our non-touch device code here
  }
}

You might notice that we no longer have any “doors” in our RobotMaker function, but since we are calling all of our code with event handlers that we are assigning inside RobotMaker, we no longer need them! For both our stage, and our characters, we will want to add a bit of CSS specially for touch devices so it will not try to cut and paste any images when a user holds down a finger on them.

#stage, .character {
  -webkit-user-select: none;
}

And finally, we will declare all of our robots at the bottom of the page, using the same format as our event handler function to have the code run automatically when the page loads – this method also prevents these robot objects from being global variables, so the only global variable we have in this entire script is the RobotMaker() function.

(function(){
  var j = RobotMaker(document.getElementById('j'), 1, 1);
  var j2 = RobotMaker(document.getElementById('j2'), .8, 5);
  var j3 = RobotMaker(document.getElementById('j3'), 1.1, .5);
  var j4 = RobotMaker(document.getElementById('j4'), .5, .75);
})();

Please checkout the final result in all of its glory!

Conclusion

I highly encourage you to study the entire (and fully commented!) code, and you can download all four robot sprites here as well.

Happy animating!

Source: Nettuts+

I've worked very hard for this, so please help me by donating a cup of coffee. Thanks in advance.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *