Surviving the Zombie Apocalypse with HTML5 and SVG, Part 4: Dynamic SVG and Path Animation

Justin Whitney | June 28, 2013

Download the Assets

Although SVG has been around for a while, its adoption as part of HTML5 has given it new life. To celebrate that fact, this series of walkthroughs has explored what would happen if the dead got new life as well, by building a Zombie Apocalypse Survival Predictor created almost entirely from SVG and JavaScript.

In Part 3 of this series, the screaming human at the center of this predictor was surrounded by danger on all sides. JavaScript was used for the first time to manipulate both Core DOM and SVG DOM, adding dozens of new zombies and then flipping them using the “transform” attribute.

In this, the final part of the series, JavaScript will again be used, this time to throw the human a tether of hope in the form of shopping malls and rednecks. Will it be enough? Stay tuned until the end, when JavaScript is also used to calculate the human’s odds of survival and then to set the prediction meter by sniffing the browser’s actual SVG capabilities and acting accordingly.

Note: This walkthrough builds on the completed code of Part 3 of this series, which can be found as the source behind https://justinwhitney.com/zombies/zombies_part3.htm.

Adding More SVG Elements

Part 3 of this series showed how to animate one out of six of the increment/decrement controls of the Control Panel canvas: the More Zombies control. That won’t do at all. The Population control needs Less Zombies as well. But before we do that, the other SVG images need to be added. The human needs some protection.

Start with the shopping malls. For simplicity, assume this city has a maximum of four malls, which will be placed in the four corners. Because the mall count needs to be tracked, add a global array for malls at the top of the <script> section:

<script>
  var malls = new Array();
…

The newMall code will set the x,y coordinates for the new SVG element based on the number of malls in the array, adjusting for 100x100 dimensions of the mall image itself. The rest of the code is essentially the same as the newZombie code with the addition that the new SVG element will be pushed to the array for tracking purposes:

function newMall(){
  if (malls.length < 4) {
    var svg = document.createElementNS("https://www.w3.org/2000/svg","image");
    svg.setAttributeNS('https://www.w3.org/1999/xlink','href','building.svg');
    svg.setAttribute('width','471');
    svg.setAttribute('height','303');
    var scale = .21;
    var x = 0;
    var y = 0;
    var thisMall = malls.length;
    if (thisMall == 0 || thisMall == 2) {
      x = 20;
    } else {
      x = 480;
    }
      if (thisMall == 0 || thisMall == 1) {
        y = 10;
      } else {
        y = 300;
      }
    malls.push(svg);
    svg.setAttribute('transform','translate(' + (x) + ', ' + (y) + ')
      scale(' + scale + ', ' + scale + ')');
    document.getElementById('cityBox').appendChild(svg);
  }
}

Finally, add the mouse event to the mall increment button, which is the <path> element with the ID “mallMore”:

<path id="mallMore" d="M 300 150 l -50 -25 l 0 50 l 50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="newMall();" />

The result, with malls maxed out (and no zombies), should look like Figure 1.

Dynamically Added SVG Mall <image> Elements
Figure 1. Dynamically Added SVG Mall <image> Elements

While zombie rednecks are the worst (especially pain-cult zombie redneck families), a healthy redneck, armed with bow and arrow, can help one survive a veritable onslaught of the walking, or running, dead. Dynamically adding them to the city looks just like adding zombies or malls. But in this case, the rednecks will be placed inside the safe zone. For an added bonus, they’ll be flipped to face outward.

function newRedneck(){
  var svg = document.createElementNS("https://www.w3.org/2000/svg","image");
  svg.setAttributeNS('https://www.w3.org/1999/xlink','href','redneck.svg');
  svg.setAttribute('width','375');
  svg.setAttribute('height','950');
  scale = .07;
  var cityWidth = 600;
  var cityHeight = 400;
  var safezoneWidth = 200;
  var safezoneHeight = 200;
  var safezoneX = Math.round((cityWidth - safezoneWidth) / 2, 0);
  var safezoneY = Math.round((cityHeight - safezoneHeight) / 2, 0);
  var x = Math.floor(Math.random()*(safezoneWidth)) + safezoneX;
  var y = Math.floor(Math.random()*(safezoneHeight-100)) + safezoneY;
 
  flip = (x > (cityWidth / 2)) ? 1 : -1;    //flip rednecks left of center
  svg.setAttribute('transform','translate(' + (x) + ', ' + (y) + ')
    scale(' + (flip * scale) + ', ' + scale + ')');
  document.getElementById('cityBox').appendChild(svg);
}

Add the call to this function to the <path> element with the ID “redneckMore”:

<path id="redneckMore" d="M 300 250 l -50 -25 l 0 50 l 50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="newRedneck();" />

A quick test should produce a screamy human safely nestled within a raft of rednecks, as shown in Figure 2.

Dynamically Added Zombie Defense Team
Figure 2. Dynamically Added Zombie Defense Team

With all three elements in play, the city (Figure 3) has now come to life, or not.

Vision of the Future, Courtesy of JavaScript and SVG
Figure 3. Vision of the Future, Courtesy of JavaScript and SVG

Finishing Out the Control Panel

So far, so good. But to complete the Control Panel, three things need to happen:

  • Decrement buttons should be enabled
  • Text should be updated
  • Parameters should have upper and lower limits

The best way to accomplish all three objectives at once is to add a fourth function to handle all button responses and calls to the three creation functions. This script will be called tickSVG.

The newMall routine was a step in the right direction in terms of controlling the SVG population, so use that as a model. First, add global arrays for zombies and rednecks as well:

<script>
  var zombies = new Array();
  var malls = new Array();
  var rednecks = new Array();
…

Next, rather than adding elements to the DOM directly, each of the creation functions needs to return the new SVG element. So for each of the three functions, end with:

 

//document.getElementById('cityBox').appendChild(svg);
return svg;

Also, in newMall, remove the following line:

malls.push(svg);

The tickSVG function itself will manage the SVG <image> population based on parameters that are passed by the buttons. First the function updates the relevant <text> element with the new total. Second, based on max and min limits and current array size, tickSVG either creates a new <image> element or grabs the top element off an array stack, depending on which button was pushed. Last, it either pushes the new <image> element onto the array and adds it to the DOM, as was originally done in the newMall routine, or it pops it off the array and removes it from the DOM (note the use of the removeChild method).

function tickSVG(textName, increment, min, max) {
  var textElement = document.getElementById(textName);
  var currentValue = parseInt(textElement.textContent);
  currentValue += increment;
  if ( (currentValue <= max) && (currentValue >= min) ) {
    textElement.textContent = currentValue;
    isMore = (increment == Math.abs(increment));
    switch (textName) {
      case 'zombieText':
        var svgArray = zombies;
        var newSVG = (isMore ? newZombie() : svgArray[svgArray.length - 1]);
        break;
      case 'redneckText':
        var svgArray = rednecks;
        var newSVG = (isMore ? newRedneck() : svgArray[svgArray.length - 1]);
        break;
      case 'mallText':
        var svgArray = malls;
        var newSVG = (isMore ? newMall() : svgArray[svgArray.length - 1]);
        break;
    }
    if (isMore) {
      svgArray.push(newSVG);
      document.getElementById('cityBox').appendChild(newSVG);
    } else {
      document.getElementById('cityBox').removeChild(newSVG);
      svgArray.pop(svgArray.length - 1);
    }
  }
}

As a last thing, all the buttons need new onmouseup calls. Find each of the following paths and either replace the existing onmouseup call or add the new one. These calls to tickSVG include references to the <text> elements created in earlier steps, as well as somewhat arbitrary max and min values, with the exception of the malls, which were designed for a maximum of four.

<path id="zombieLess" d="M 50 50 l 50 -25 l 0 50 l -50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('zombieText',-100,0,3000);" />
<path id="zombieMore" d="M 300 50 l -50 -25 l 0 50 l 50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('zombieText',100,0,3000);" />
<path id="mallLess" d="M 50 150 l 50 -25 l 0 50 l -50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('mallText',-1,0,4);" />
<path id="mallMore" d="M 300 150 l -50 -25 l 0 50 l 50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('mallText',1,0,4);" />
<path id="redneckLess" d="M 50 250 l 50 -25 l 0 50 l -50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('redneckText',-1,0,10);" />
<path id="redneckMore" d="M 300 250 l -50 -25 l 0 50 l 50 -25" 
  stroke="black" stroke-width="1"
  fill="red" onmouseup="tickSVG('redneckText',1,0,10);" />

The fully revised code is linked at the end of this walkthrough. If it works correctly, the page should look something like Figure 4.

Fully Implemented Control Panel, aka The Apocalypse Is Nigh
Figure 4. Fully Implemented Control Panel, aka The Apocalypse Is Nigh

What Are the Odds?

The Zombie Apocalypse Survival Predictor needs one more piece to be complete. That Odds calculator needs to move. At the very least, it needs some DOM-manipulating JavaScript to update the 0% at the top and the position of the arrow. But for compliant browsers, JavaScript can add some extra special sauce to one of the animation techniques introduced in Part 2 of this series by making the arrow move along a dynamically generated path.

But first, some calculations. Add a new global variable at the top of the <script> section called currentOdds and set it to 0:

<script>
  var zombies = new Array();
  var malls = new Array();
  var rednecks = new Array();
  var currentOdds = 0;
…

Next, create a new function called calcOdds. After exhaustive research using rigorous methodology, the following formula has been carefully crafted to predict one’s survival during a zombie apocalypse:

function calcOdds() {
  var zombieCount = parseInt(document
        .getElementById('zombieText').textContent);
  if (zombieCount == 0) {
    currentOdds = 100;
  } else {
    var redneckCount = parseInt(document
        .getElementById('redneckText').textContent);
    var mallCount = parseInt(document
        .getElementById('mallText').textContent);
    var speed = document.getElementById('speedText').textContent;
    var threat = Math.round((zombieCount * 
        (speed == 'Slow' ? 2 : 18)) / 180);
    var protection = Math.round(((mallCount * 10) + 
        (redneckCount * 5)) / 60 * 100);
    currentOdds = Math.round((100 + protection - threat) / 2);
  }
  if (currentOdds > 100) currentOdds = 100;
  if (currentOdds < 0) currentOdds = 0;
}

Note that, as with the <text> element at the beginning of this walkthrough, the text value of the element can be reached by accessing its textContent property. Other than that distinction, the DOM looks the same as it does for HTML.

To test the calculation, find the group (<g>) element with the ID “oddsButton”. As explained in Part 1, the button is made up of two distinct elements, a <rect> and a <text> element. Fortunately, by grouping them, the parent <g> element allows styling and mouse events to be associated with the button as a whole. For now, temporarily add an onmouseup event to test calcOdds:

<g id="oddsButton" style="cursor: pointer;" 
  onmouseup="calcOdds(); alert(currentOdds);">

If this works, it’s time for the next step: moving the pointer. Create a new function called movePointer:

function movePointer() {
  calcOdds();
  document.getElementById('oddsText').textContent 
    = currentOdds + '%';
  var newY = -2 * currentOdds;    //relative Y coordinate
  newY += 300;    //actual Y coordinate
  document.getElementById('oddsPointer').setAttribute('points',
    "150," + newY + " 100," + (newY - 25) + " 100," + (newY + 25));
}

Yes, the newY calculation can be done in a single step. It was split up for a reason, which will be revealed soon. The noteworthy line here is the last one, the setAttribute function. Remember that oddsPointer was created as a <polygon> element, which works by defining a “points” attribute using absolute coordinates. Like most other attributes, points can be accessed via the DOM by grabbing the element and using setAttribute, and then specifying which attribute (“points”) and the new value. In this case, the new value is a text string consisting of three pairs of coordinates. The pointer is hard-coded in this example to be a right-pointing triangle in a 50x50 space. The Y coordinate of its center can range from 300 at the bottom of the gauge to 100 at the top, corresponding to odds of 0 to 100, respectively. The formula “newY = 300 – (2 * currentOdds)”, broken up here, should place the right tip of the triangle precisely along the scale marked by the tics created in Part 2 of this series.

To watch it in action, replace the oddsButton onmouseup event with a call to the new function:

<g id="oddsButton" style="cursor: pointer;" 
  onmouseup="movePointer();">

Populate the city with a few zombies, malls, and rednecks, maybe even toggle to fast zombies, and then click Calculate Odds. The pointer should jump to an extremely accurate prediction of one’s survival during an apocalypse, as demonstrated in Figure 5.

The Finished Product : A Life-Saving Tool
Figure 5. The Finished Product : A Life-Saving Tool

Bonus Feature: Dynamic Path Animation

Because this walkthrough is focused on JavaScript and DOM, here’s one more nice trick to add some animation to the pointer movement. As described back in Part 2, SVG can be animated (in compliant browsers) by adding an <animateMotion> element to an SVG element and then referencing a path. Taking it one step further, JavaScript can change the coordinates of that path dynamically, enabling movement anywhere at any time for any element on the screen.

To put words into action, find the oddsPointer polygon. Two changes need to be made. First, define a simple path with the ID “oddsPath”. Then reference that path within an <animateMotion> element, which itself is nested within oddsPointer. Note that the <polygon> element is going from self-closing to verbose, so be sure to double-check syntax.

<g id="oddsMeter" stroke="#000000" stroke-width="2">
  <path d="M100 100 h75 m-75 100 h75 m-75 100 h75" 
        stroke-width="3" />
  <path d="M100 150 h50 m-50 100 h50" />
  <path id="oddsPath" d="M 0 0 L 0 -200" />
  <polygon id="oddsPointer" points="150,300 100,275 100,325" 
        fill="#FF0000" stroke-width="1">
    <animateMotion dur="2s" begin="oddsButton.click">
      <mpath xlink:href="#oddsPath" />
    </animateMotion>
  </polygon>
  <rect x="90" y="75" rx="15" ry="10" 
        width="20" height="250" fill="#0000FF" />
</g>

The gotcha here is with the <path> element. Remember, an animation path doesn’t define actual coordinates but rather the relative motion of the animation. If this <path> were to be drawn on the screen, it would start at the left, top corner of the canvas and go straight up. But what it’s telling the pointer to do is to start from its current location (M 0 0) and go up 200px from there (L 0 -200).

Note that the animation is set to start when oddsButton is clicked, as set in the “begin” attribute. But if the button were to get clicked right now, the pointer would just jump to its correct position and then float upward. Unlike previous animation examples, a distinction needs to be made here between browsers that do and don’t recognize SVG animation. In general, it’s best to use document.implementation.hasFeature to check for feature implementation in a browser. In this case, the feature to look for is Animation in the SVG 1.1 spec. Add a function called supportsAnimation():

 

function supportsAnimation() {
  return document.implementation.hasFeature("https://www.w3.org/TR/SVG11/feature#Animation",
    "1.1");
}

This is one of several available feature strings that can be found at https://www.w3.org/TR/SVG11/feature.

Next, add one more global variable, this one to track the “L” value of the path so that the pointer always starts where it previously left off rather than resetting to its original position.

<script>
  var zombies = new Array();
  var malls = new Array();
  var rednecks = new Array();
  var currentOdds = 0;
  var previousL = 0;

This may be a little confusing given the prior description of how the animation path works. But keep in mind that the JavaScript will be changing the attribute of the <path>, not the <polygon>. And movement does not change the points attribute of the <polygon> either, so those points will remain constant using this method. Therefore, if the <path> always starts at M 0 0, then the <polygon> will always return to that coordinate before moving, causing an unsightly jump before animating. For that reason, the “M” value of the path needs to be changed every time so that the motion remains relative.

Go back to the movePointer function and add the sniffer. Now the mystery of the messy “newY” becomes clear. Based on browser capabilities, either the “d” attribute of the animation path will be changed in time for the animation to begin, or the points attribute of the polygon will be set:

function movePointer() {
  calcOdds();
  document.getElementById('oddsText').textContent = 
        currentOdds + '%';
  var newY = -2 * currentOdds;    //relative Y coordinate
  if (supportsAnimation()) {
    document.getElementById('oddsPath')
        .setAttribute('d','M 0 ' + previousL + ' L 0 ' + newY);
      previousL = newY;
  } else {
    newY += 300;    //actual Y coordinate
    document.getElementById('oddsPointer').setAttribute('points',
      "150," + newY + " 100," + (newY - 25) + " 100," + (newY + 25));
  }   
}

With that, this Zombie Apocalypse Survival Predictor is now complete! Try it out now at https://justinwhitney.com/zombies/zombies_part4.htm or look at the page’s source code for the final version of this walkthrough. All assets used on this page (courtesy of https://openclipart.org) can be found at https://justinwhitney.com/zombies/zombiesAssets.zip.

Wrapping Up

While the chances of surviving a zombie apocalypse may vary, the chances of being caught in one are fairly low. But as the CDC knows (https://www.cdc.gov/phpr/zombies.htm), companies that prepare for zombies are prepared for a lot of other things as well. I hope this series of walkthroughs has illustrated how to use SVG for visually dynamic, scalable Web apps. The skills learned here can be used for many, many other applications. Like preparing for asteroids. Because an asteroid apocalypse is definitely going to happen.


This article is part of the HTML5 tech series from the Internet Explorer team. Try-out the concepts in this article with 3 months of free BrowserStack cross-browser testing @ https://modern.IE.


About the Author

Justin WhitneyJustin Whitney is a programmer/writer/filmmaker living in the South. Due to the ratio of rednecks to population, his current chance of surviving a zombie apocalypse stands at roughly 66 percent, assuming the zombies are slow. He can be reached at webmaster@justinwhitney.com.