Lesson 7 - Two Dimensional Ballistics

This lesson is similar to the previous in that it uses JavaScript and the DOM to provide the animation. But there are a few differences, so let's take a look. First off, like the previous lesson, the text of the lesson is plain HTML with a large left margin for the cannon, which is SVG. Also like the previous lesson, the SVG is positioned absolutely and on top of the HTML. However, unlike the previous lesson, the SVG in this case covers the whole page which allows drawing on top of the HTML anywhere on the page.

The structure of the files is again the same, with the base page being XHTML, which loads the SVG into an object tag. The SVG in turn loads the JavaScript via the SVG script tag.

The root svg element is very similar to the previous lesson, including the onload event handler:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
          width="1593" height="1739" onload="on_load(evt)" >

Note that we don't need to both with a viewBox this time since we will just use page coordinates. The onload handler is very similar to the previous lesson as well.

function on_load(evt)  {
    svgDoc = getSVGDoc(evt.target);

    CannonBall     = svgDoc.getElementById('CannonBall');
    TrajectoryInfo = svgDoc.getElementById('TrajectoryInfo');
    CannonBallPath = svgDoc.getElementById('CannonBallPath');
    FireButton     = svgDoc.getElementById('FireButton');
    FireButtonFill = svgDoc.getElementById('FireButtonFill');
    CannonSound    = window.parent.document.getElementById("audio-cannon");
}

There are a couple of additional items, notably the cannonball's path and the fire-button's IDs. We'll cover those as we go along.

The code for the Fire button handler is

function OnMouseDownFire(evt) {
    CannonSound.play();
    startTime = new Date();

    points="";
    bFlying = true;
    CannonBall.setAttribute("display", "inline" );

    velocity = INITIAL_VELOCITY;
    speedY = velocity * Math.sin(CANNON_ANGLE * DEG2RAD);
    speedX = velocity * Math.cos(CANNON_ANGLE * DEG2RAD);

    opacity = 1.0;
    CannonBallPath.setAttribute("points", points );
    CannonBallPath.setAttribute("stroke-opacity", opacity );
    FireButtonFill.setAttribute("fill", "#000080");
    FireButton.setAttribute("pointer-events", "none" );
    FireButton.setAttribute("opacity", "0.5");

    intervalID = window.setInterval('next_update()', 16);
}

First off, we call the HTML audio element for the cannon fire sound then set the start time for the animation. We then ensure that the points SVG polyline element, which will hold the trajectory of the cannonball's path is empty. We set the animation-in-progress flag to true. We then change the cannonball's display property to "inline" so it is no longer hidden. We calculate speed in X and Y of the cannonball as a function of the initial (arbitrary) velocity and and the angle of the cannon's barrel. Since we neglect friction, the speed in X and Y doesn't change over time though the position in Y will also be affected by gravity.

Then we set the opacity of the cannonball's path to 1.0 and set the SVG polyline's points attribute to point at our JavaScript variable points, which is a string. We will dyamically add coordinates to the string as the cannonball flies along it's trajectory. We set the polyline's stroke-opacity to 1.0. Note that we set the stroke-opacity becase we are stroking the path, not filling it.

We then set the fire-button to dark blue and the pointer-events to none so the user can't press the button while the cannonball is flying. We set the opacity of the button to 50% to visually indicate this.

Finally, we tell the browser to call our update function as fast as it can. Like the previous lesson, the update function is where all the work gets done.

function next_update ()  {

    elapsed = (Date.now() - startTime.getTime()) / 1000.0;

    if (posY < MIN_ALTITUDE && bFlying == true) {
        bFlying = false;
    }
    else
    {
        if (bFlying == true) {
            posX = elapsed * speedX;
            posY = elapsed * speedY - 0.5 * gravity * elapsed * elapsed;
            velocity = Math.sqrt(speedX*speedX + speedY*speedY);

            points = points + posX.toFixed(1) + ',';
            points = points + posY.toFixed(1) + ' ';

            CannonBall.setAttribute("cy", posY );
            CannonBall.setAttribute("cx", posX );
            CannonBallPath.setAttribute("points", points );

            TrajectoryInfo.firstChild.nodeValue = 't: ' + elapsed.toFixed(1) + ' v: ' 
                       + velocity.toFixed(1) + ' y: ' + posY.toFixed(1);
        }
        else {
            opacity -= OPACITY_DECREMENT;
            CannonBallPath.setAttribute("stroke-opacity", opacity );
            if (opacity <= 0) {
                window.clearTimeout( intervalID );
                points = "";
                FireButtonFill.setAttribute("fill", "#C00000");
                FireButton.setAttribute("opacity", "1");
                FireButton.setAttribute("pointer-events", "auto" );
            }
        }
    }
}

First we see how much time has elapsed. Then we check to see if we are currently animating and if the cannonball has fallen off the page. If it has, we clear the flying flag, but we DON'T stop the animation just yet because we haven't erased the cannonball's path yet.

If the cannonball is flying and HASN'T fallen off the page, then we need to update it's position and trajectory. Since there is no friction in our world, the position in X is easy to calculate given the horizontal speedX and the elapsed time. The position in Y is calculated in exactly the same fashion as the previous lesson, of course. We then append the X and Y coordinates onto the points string whcih appends them to the SVG polyline element, which causes it to be stroked in white. We then update the position of the cannonball itself. Finally, we update the text element with the trajectory info.

But if the cannonball ISN'T flying then it means it has fallen off the page so we don't need to update it's position anymore but we do need to erase the cannonball's path my slowly making the path more transparent. So we decrease the path's opacity by a small amount. Then, when the path is completely transparent, we turn clear the animation calls, set the points string to empty and re-enable the Fire button and turn it fully opaque and back to red.

And that's it! Click on this link to see the actual rendered demo in all its booming glory!