This section is about using JavaScript's Document Object Model (DOM).

  1. Bouncing Ball
  2. Cheerleader
  3. Turtles All the Way Down
  4. Randy the Raptor (long)

The following links may be helpful:

1. Bouncing Ball

Create a page which contains an animated bouncing ball. You can view an example of this page here. You are given ball.html and ball.css, and you write ball.js.

ball.html contains the following HTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
      <title>Bouncing Ball</title>
      <link href="ball.css" type="text/css" rel="stylesheet" />
      <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
         type="text/javascript"></script>
      <script src="ball.js" type="text/javascript"></script>
   </head>

   <body>
      <div>
         <img src="redball.png" alt="red ball" id="ball" />
      </div>
   </body>
</html>

ball.css contains the following styles:

#ball {
   position: fixed;
   top: 0px;
}

This problem involves using the setInterval JavaScript function to update the ball's position every 20 milliseconds. You can position the ball using the CSS properties top and left. The ball has position: fixed, so the ball will be positioned from the top left corner of the page.

You will need to store two pieces of information about the ball: it's current y position and its current y velocity. Every 20 milliseconds, the ball's position should be updated by adding the current velocity into it:

    ballY += ballVelocity;

Every 20 milliseconds, the ball's velocity should also be updated by a certain value representing gravity:

    ballVelocity += gravity;

You can choose a value for gravity that feels right. Finally, to make the ball bounce, you must test to see if the ball's Y position is greater than the height of the window, and then flip the sign of the ball's velocity. You can access the width and height of the window by using window.innerWidth and window.innerHeight.

Solution

ball.js

var ballY = 0;
var ballVelocity = 0;

window.onload = function() {
    $("ball").style.top = ballY + "px";
    $("ball").style.left = (window.innerWidth / 2) + "px";
    setInterval(update, 20);
};

function update() {
    $("ball").style.top = ballY + "px";
    ballY += ballVelocity;
    ballVelocity += 1;
    
    if (ballY > window.innerHeight) {
        ballVelocity *= -.9;
    }
}

2. Cheerleader

Given the following HTML file, write the JavaScript code cheerleader.js to echo typed characters to the screen as in this working example.

cheerleader.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
   "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
   <head>
      <title>Cheerleader!</title>
      <link rel="stylesheet" type="text/css" href="cheerleader.css" />
      <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
         type="text/javascript"></script>
      <script type="text/javascript" src="cheerleader.js"></script>
   </head>
   
   <body>
      <h1>CSE 190 M Cheerleader!</h1>
      <div id="cheerleader"><img src="cheerleader.jpg" alt="Cheerleader!" /></div>
      
      <ul id="cheers"></ul>
      
      <p>Type a letter to cheer!</p>
   </body>
</html>

Inject the pressed keys as li elements inside #cheers. Each character should be in upper case, followed by an exclamation point. The lecture slide on key events will probably be useful to help you determine which key was pressed and convert it from a code to a character.

After you have made the pressed keys appear, modify your code so that each cheer removes itself from the page after two seconds (i.e., 2000 milliseconds).

Solution

cheerleader.js

document.observe('dom:loaded', function() {
   document.observe('keypress', cheer);
});

function cheer(event) {
   var cheer = $(document.createElement('li'));
   cheer.innerHTML = String.fromCharCode(event.charCode).toUpperCase() + "!";
   setTimeout(function() {
      unCheer(cheer);
   }, 2000);
   $('cheers').appendChild(cheer);
}

function unCheer(cheer) {
   $(cheer).remove();
}

3. Turtles All the Way Down

A well-known scientist (some say it was Bertrand Russell) once gave a public lecture on astronomy. He described how the earth orbits around the sun and how the sun, in turn, orbits around the center of a vast collection of stars called our galaxy. At the end of the lecture, a little old lady at the back of the room got up and said: What you have told us is rubbish. The world is really a flat plate supported on the back of a giant tortoise. The scientist gave a superior smile before replying, What is the tortoise standing on? You're very clever, young man, very clever, said the old lady. But it's turtles all the way down!

Stephen Hawking, A Brief History of Time

(see also: Turtles all the way down on Wikipedia)

We will implement a version of the "turtles all the way down" model (commonly but falsely attributed to Hindu mythology) in which the earth rests on the back of an elephant, which in turn rests on the back of infinite tortoises.

Given the following HTML file (containing the earth, elephant, and a single tortoise), write the necessary JavaScript code turtles.js to give the page infinitely-scrolling turtles as in this working example.

turtles.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
   "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
   <head>
      <title>turtles all the way down</title>
      <link rel="stylesheet" type="text/css" href="turtles.css" />
      <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
         type="text/javascript"></script>
      <script type="text/javascript" src="turtles.js"></script>
   </head>
   
   <body>
      <div id="earth"></div>
      <div id="elephant"></div>
      <div class="turtle"></div>
   </body>
</html>

To solve this problem you will need to add a turtle to the bottom of the page every time the user scrolls to the bottom. Adding a single tortoise to the bottom of the page will result in having to scroll the new turtle into view — which in turn will cause a new tortoise to be created, and so on. In this way the page can scroll indefinitely.

You can attach a scroll event handler to the document, so that every time the page is scrolled that event handler will be executed. In order to determine when the user has scrolled to the bottom of the page there are three useful page measurements we can use:

window.scrollY
The portion of the document that has scrolled off-screen above the currently-visible portion.
window.innerHeight
The height of the visible area of the document on-screen.
document.body.getHeight()
The height of the body tag (i.e., the height of the entire page, on- and off-screen). This is a Prototype function.

When the user has scrolled to the very bottom of the page, the sum of scrollY (the off-screen portion above) and innerHeight (the on-screen portion) will be equal to the height of the body. That's when you'll want to add another turtle — which will grow the body and result in more scrolling.

To add a turtle, simply append to document.body a new div with the class of turtle.

Once you've made the page grow indefinitely, you can fix one small special case: what if the height of the window is greater than the initial height of the animals? (You can test this by zooming out a lot and then refreshing.) In this case the initial body height will be less than the window height, and the off-screen portion will be zero. How would you fix this?

problem adapted from original implementation by Kevin Wallace

Solution

turtles.js

document.observe('dom:loaded', function() {
   document.observe('scroll', turtles);
   turtles(); // in case window height is initially taller than animals
});

function turtles() {
   while (window.scrollY + window.innerHeight >= document.body.getHeight()) {
      var div = $(document.createElement('div'));
      div.addClassName('turtle');
      document.body.appendChild(div);
   }
}

4. Randy the Raptor (long)

(Click the image below to run the sample solution.) (solution JS code attack.js)

A raptor is on the loose. Rawr! He wants to stomp the townspeople. Write JavaScript code to allow the raptor to eat them. The HTML and CSS are already completely written; start from this skeleton of attack.html.

attack.html:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
   <!-- CSE 190M, Lab 6: Raptor (Rarrrr) -->
   
   <head>
      <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
      <title>Raptor</title>

      <link href="http://www.cs.washington.edu/education/courses/cse190m/09sp/labs/section6-raptor/solution/attack.css" type="text/css" rel="stylesheet" />

      <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js" type="text/javascript"></script>
      
      <!-- you write this -->
      <script src="attack.js" type="text/javascript"></script>
   </head>

   <body>
      <h1 class="normal">RANDY THE RAPTOR IS HUNGRY</h1>
      <div id="city">
         <img src="http://www.cs.washington.edu/education/courses/cse190m/09sp/labs/section6-raptor/solution/raptor.png" id="raptor" alt="Raptor" />
         <div id="people">
            <!-- 5 initial people to play with -->
            <div class="person"></div>
            <div class="person"></div>
            <div class="person"></div>
            <div class="person"></div>
            <div class="person"></div>
         </div>
      </div>
      
      <fieldset>
         <legend>Attack!</legend>
         
         <p>
            <button id="add">Add!</button>
            <button id="kill">Kill!</button>
            <label><input id="boys" type="radio" name="gender" /> Boys</label>
            <label><input id="girls" type="radio" name="gender" checked="checked" /> Girls</label>
         </p>
         
         <p>
            <button id="cleanup">Clean up!</button>
            <button id="stomp">Stomp!</button>
            <button id="enrage">Enrage!</button>
            <button id="patrol">Patrol!</button>
         </p>
      </fieldset>

      <fieldset id="legend">
         <legend>Legend</legend>
            <div><div class="person boy"></div> boy</div>
            <div><div class="person girl"></div> girl</div>
            <div><div class="person splat"></div> raptor splat!</div>
      </fieldset>
   </body>
</html>

Here are the behaviors to add:

problem by Stefanie Hatcher

Solution

attack.js

window.onload = function() {
    $("add").onclick = populate;
    $("kill").onclick = kill;
    $("stomp").onclick = stomp;
    $("patrol").onclick = patrol;
    $("enrage").onclick = enrageRaptor;
    $("cleanup").onclick = clearDead;
    
    // Make it a city of N Men...Initially
    var people = $$("#people .person");
    for (var i = 0; i < people.length; i++) {
        people[i].addClassName("boy");
    }
};

// Helper function to get which gender is currently selected.
function getGender() {
    if ($("boys").checked) {
        return "boy";
    } else {
        return "girl";
    }
}

// Add! button event handler; adds 5 people of current gender
function populate() {
    var gender = getGender();
    for (var i = 0; i < 5; i++) {
        var newPerson = document.createElement("div");
        newPerson.addClassName("person");
        newPerson.addClassName(gender);
        $("people").appendChild(newPerson);
    }
}

// Kill! button event handler
// tells raptor to randomly kill 1/5 of selected gender
function kill() {
    var gender = getGender();
    splat(gender);
}

// Get all guys or girls and splat one fifth of them
// Random targets, could over lap splats, 
// allowing for more randomness double death or not!
function splat(gender) {
    var peeps = $$("#people ." + gender);
    for (var i = 0; i < peeps.length / 5; i++) {
        var randomIndex = Math.floor(Math.random() * peeps.length);
        peeps[randomIndex].removeClassName(gender);
        peeps[randomIndex].addClassName("splat");   // so future kills won't choose splat victims
    }   
}

// Clean up the dead! button event handler
function clearDead() {
    var dead = $$("#people .splat");
    for (var i = 0; i < dead.length; i++) {
        dead[i].remove();
    }   
}

// Stomp! button event handler
function stomp() {
    var pxtop = parseInt($("raptor").getStyle("top"));
    $("raptor").style.top = ((pxtop + 75) % 150) + "px";
    splat("boy");
    splat("girl");
}

// Enrage! button event handler
function enrageRaptor() {
    // If enraged -- go back to normal, else get ENRAGED
    if ($("raptor").hasClassName("enrage")) {
        $("raptor").removeClassName("enrage");
        $$("h1")[0].removeClassName("enrage");
        $("raptor").style.width = parseInt($("raptor").getStyle("width")) - 50 + "px";
    } else {
        $("raptor").addClassName("enrage");
        $$("h1")[0].addClassName("enrage");
        $("raptor").style.width = parseInt($("raptor").getStyle("width")) + 50 + "px";
    }
}


// Patrol! event handler code (advanced)
var timer;

function patrol() {
    clearInterval(timer);
    timer = setInterval(patrolRight, 20);
}

function patrolRight() {
    var pxleft = parseInt($("raptor").getStyle("left"));
    pxleft += 4;
    $("raptor").style.left = pxleft + "px";
    if (pxleft >= 300) {
        clearInterval(timer);
        timer = setInterval(patrolLeft, 20);
    }       
}

function patrolLeft() {
    var pxleft = parseInt($("raptor").getStyle("left"));
    pxleft -= 4;
    $("raptor").style.left = pxleft + "px";
    if (pxleft <= 10) {
        clearInterval(timer);
        $("raptor").style.top = "5px";  // Reset the Raptor
        $("raptor").style.left = "10px";
    }       
}

Solutions

1. Bouncing Ball

ball.js

var ballY = 0;
var ballVelocity = 0;

window.onload = function() {
    $("ball").style.top = ballY + "px";
    $("ball").style.left = (window.innerWidth / 2) + "px";
    setInterval(update, 20);
};

function update() {
    $("ball").style.top = ballY + "px";
    ballY += ballVelocity;
    ballVelocity += 1;
    
    if (ballY > window.innerHeight) {
        ballVelocity *= -.9;
    }
}

2. Cheerleader

cheerleader.js

document.observe('dom:loaded', function() {
   document.observe('keypress', cheer);
});

function cheer(event) {
   var cheer = $(document.createElement('li'));
   cheer.innerHTML = String.fromCharCode(event.charCode).toUpperCase() + "!";
   setTimeout(function() {
      unCheer(cheer);
   }, 2000);
   $('cheers').appendChild(cheer);
}

function unCheer(cheer) {
   $(cheer).remove();
}

3. Turtles All the Way Down

turtles.js

document.observe('dom:loaded', function() {
   document.observe('scroll', turtles);
   turtles(); // in case window height is initially taller than animals
});

function turtles() {
   while (window.scrollY + window.innerHeight >= document.body.getHeight()) {
      var div = $(document.createElement('div'));
      div.addClassName('turtle');
      document.body.appendChild(div);
   }
}

4. Randy the Raptor

attack.js

window.onload = function() {
    $("add").onclick = populate;
    $("kill").onclick = kill;
    $("stomp").onclick = stomp;
    $("patrol").onclick = patrol;
    $("enrage").onclick = enrageRaptor;
    $("cleanup").onclick = clearDead;
    
    // Make it a city of N Men...Initially
    var people = $$("#people .person");
    for (var i = 0; i < people.length; i++) {
        people[i].addClassName("boy");
    }
};

// Helper function to get which gender is currently selected.
function getGender() {
    if ($("boys").checked) {
        return "boy";
    } else {
        return "girl";
    }
}

// Add! button event handler; adds 5 people of current gender
function populate() {
    var gender = getGender();
    for (var i = 0; i < 5; i++) {
        var newPerson = document.createElement("div");
        newPerson.addClassName("person");
        newPerson.addClassName(gender);
        $("people").appendChild(newPerson);
    }
}

// Kill! button event handler
// tells raptor to randomly kill 1/5 of selected gender
function kill() {
    var gender = getGender();
    splat(gender);
}

// Get all guys or girls and splat one fifth of them
// Random targets, could over lap splats, 
// allowing for more randomness double death or not!
function splat(gender) {
    var peeps = $$("#people ." + gender);
    for (var i = 0; i < peeps.length / 5; i++) {
        var randomIndex = Math.floor(Math.random() * peeps.length);
        peeps[randomIndex].removeClassName(gender);
        peeps[randomIndex].addClassName("splat");   // so future kills won't choose splat victims
    }   
}

// Clean up the dead! button event handler
function clearDead() {
    var dead = $$("#people .splat");
    for (var i = 0; i < dead.length; i++) {
        dead[i].remove();
    }   
}

// Stomp! button event handler
function stomp() {
    var pxtop = parseInt($("raptor").getStyle("top"));
    $("raptor").style.top = ((pxtop + 75) % 150) + "px";
    splat("boy");
    splat("girl");
}

// Enrage! button event handler
function enrageRaptor() {
    // If enraged -- go back to normal, else get ENRAGED
    if ($("raptor").hasClassName("enrage")) {
        $("raptor").removeClassName("enrage");
        $$("h1")[0].removeClassName("enrage");
        $("raptor").style.width = parseInt($("raptor").getStyle("width")) - 50 + "px";
    } else {
        $("raptor").addClassName("enrage");
        $$("h1")[0].addClassName("enrage");
        $("raptor").style.width = parseInt($("raptor").getStyle("width")) + 50 + "px";
    }
}


// Patrol! event handler code (advanced)
var timer;

function patrol() {
    clearInterval(timer);
    timer = setInterval(patrolRight, 20);
}

function patrolRight() {
    var pxleft = parseInt($("raptor").getStyle("left"));
    pxleft += 4;
    $("raptor").style.left = pxleft + "px";
    if (pxleft >= 300) {
        clearInterval(timer);
        timer = setInterval(patrolLeft, 20);
    }       
}

function patrolLeft() {
    var pxleft = parseInt($("raptor").getStyle("left"));
    pxleft -= 4;
    $("raptor").style.left = pxleft + "px";
    if (pxleft <= 10) {
        clearInterval(timer);
        $("raptor").style.top = "5px";  // Reset the Raptor
        $("raptor").style.left = "10px";
    }       
}
Valid XHTML 1.1 Valid CSS!