www.apress.com

17/04/04

Modeling the Solar System with the Canvas Element

By Mark J. Collins

You can use the canvas element in HTML5 to create some fun graphics. Canvas is very different from SVG – the main thing is that canvas is completely implemented in JavaScript. The only part that is in the markup is a simple element definition like this:   
        <canvas id="myCanvas" width="400" height="400">
           Canvas is not supported on this browser
        </canvas>

Instead, you’ll define the content by calling the various drawing methods using JavaScript. Just like with the audio and video elements, the markup within the canvas element is used when the browser does not support canvas. You can use this to provide the appropriate fallback content.

In this article, you’ll draw a moving model of the solar system. For the sake of time, you’ll show only the earth, sun, and moon. This implementation will take advantage of these two important features of canvas:

  • Paths
  • Transformations

Using Paths

The only simple shape that canvas supports is the rectangle. For all other shapes you must define a path. The basic approach to defining paths in canvas is similar to SVG. You use a move command to set the starting point and then some combination of line and curve commands to draw a shape.

In canvas, you always start with a beginPath() command. After calling the desired drawing commands, the path is completed by calling either stroke() to draw an outline of the shape or fill() to fill in the shape. The shape is not actually drawn to the canvas until either stroke() or fill() is called. If you call beginPath() again, before completing the current shape (with a call to stroke() or fill()), the canvas will ignore the previous uncompleted commands. The same strokeStyle and fillStyle properties that you used with rectangles also define the color of the path.

The actual drawing commands are as follows:

  • moveTo()
  • lineTo()
  • arcTo()
  • bezierCurveTo()
  • quadraticCurveTo()

In addition, these functions can be used for drawing:

  • closePath(): This performs a lineTo() command from the current position to the starting position to close in the shape. If you use the fill() command, the closePath() function is automatically called if you’re not currently at the starting position.
  • arc(): This draws an arc at the specified location; you don’t have to move there first.

However, this is still treated as a path; you need to first call beginPath(), and the arc is not actually drawn until you call either stroke() or fill().

Drawing Arcs

The arc() command is one that you’ll likely use a lot and will be important in this example. The arc()

command takes the following parameters:

        arc(x, y, radius, start, end, counterclockwise)

The first two parameters specify the x and y coordinates of the center point. The third parameter specifies the radius. The fourth and fifth parameters determine the starting and ending points of the arc. These are specified as an angle from the x-axis. The 0° angle is the right side of the circle; a 90° angle would be the bottom edge of the circle. The angles are specified in radians, however, not degrees.

Unless you’re drawing a full circle, the direction of the arc is important. For example, if you drew an  arc from 0° to 90°, the arc would be 1/4 of a circle, from the right side to the bottom. However, using the same endpoints but drawing in a counterclockwise direction, that arc would be 3/4 of the circle. The final parameter, if true, indicates that the arc should be drawn in a counterclockwise direction. This parameter is optional. If you don’t specify it, it will draw the arc in a clockwise direction.

At first, transformations in canvas may seem a bit confusing, but they can be quite helpful once you understand how they work. First, transformations have no effect on what has already been drawn on the canvas. Instead, transformations modify the grid system that will be used to draw subsequent shapes. I will demonstrate three types of transformations in this chapter.

  • Translating
  • Rotating
  • Scaling

As I mentioned earlier, a canvas element uses a grid system where the origin is at the top-left corner of the canvas. So, a point at 100, 50 will be 100 pixels to the right and 50 pixels down from that corner.

Transformations simply adjust the grid system. For example, the following command will shift the origin 100 pixels to the right and 50 pixels down:

        context.translate (100, 50);

This is illustrated in the following figure:

New Content Item

Figure 1. Translating the context origin

Now when you move to 10, 20, since this is relative to the new origin, the actual position (relative to the canvas), will be 110, 70. You might be wondering why you would want to do this. Well, suppose you were drawing a picture of the U.S. flag, which has 50 stars on it. A five-pointed star is a fairly complex shape to draw, which will require a number of drawing commands. Once you have drawn the first star, you’ll need to repeat the process 49 more times, each time using different values.

By simply translating the context to the right a little, you can repeat the same commands using the same values. But now the star will be in a different location. Granted, you could accomplish the same thing by creating a drawStar() function that accepted x, y parameters. Then call this 50 times, passing in different values. However, once you get used to using transformation, you will find this easier, especially with the other types such as rotation.

The rotate transformation doesn’t move the origin; instead, it rotates the x- and y-axes by the specified amount. A positive amount is used for a clockwise rotation, and a negative value is used to rotate counterclockwise. The next figure demonstrates how a rotate transformation works.

New Content Item

Figure 2. Rotating the drawing context’s grid

Note: I indicated the rotation angle as 30° since that is what most people are familiar with. However, the rotate() command expects the value in radians. If your geometry is a little rusty, a full circle is 360° or 2π radians. In JavaScript, you can use the Math.PI property to get the value of π (pi). For example, 30° is 1/12 of a full circle, so you can write this as (Math.PI*2/12). In general, radians are calculated as degrees * (Math.PI/180).

You can use multiple transformations. For example, you can translate the origin and then rotate the x- or y-axis. You can also rotate the grid some more and translate again. Each transformation is always relative to the current position and orientation.

Saving the Context State

The state of a drawing context includes the various properties such as fillStyle and strokeStyle that you have already used. It also includes the accumulation of all transformations that have been applied. If you start using multiple transformations, getting back to the original state may be difficult. Fortunately, the drawing context provides the ability to save and then restore the state of the context.

The current state is saved by calling the save() function. Saving the state pushes the current state onto a stack. Calling the restore() function pops the most recently saved state off the stack and makes that the current state. This is illustrated in the next figure:

New Content Item

Figure 3. Saving and restoring the drawing context state

You should generally save the state before doing any transformations, especially complex ones. When you have finished drawing whatever elements needed the transformation, you can restore the state to the way it was. Remember, changing the state by setting the fillStyle or performing a transformation does not affect what has already been drawn.

Drawing the Solar System

With these features at your disposal, let’s draw a simple model of the solar system.

  1. Create a new web page, Solar.html, using the basic markup:
    <!DOCTYPE html>
        
    <html  lang="en">
      <head>
        <meta charset="utf-8" />
        <title>Chapter 23 - Solar System</title>
        <script src="Solar.js" defer></script>
      </head>
      <body>
      </body>
    </html>

  2. Add the canvas element inside the body element.

    <canvas id="solarSystem" width="450" height="400">
      Not supported

    </canvas>

  3. Add a new Solar.js script using the following code. this code gets the canvas element and then obtains the 2d drawing context, just like the previous example.

    "use strict";

    // Get the canvas context

    var  solarCanvas  =  document.getElementById("solarSystem");
    var   solarContext   =   solarCanvas.getContext("2d");

  4. Add the animateSS() function to the Solar.js file using the following code.

    function  animateSS()  {

    var ss = document.getElementById('solarSystem') var  ssContext  =  ss.getContext('2d');


    // Clear the canvas and draw the background ssContext.clearRect(0, 0, 450, 400);
    ssContext.fillStyle = "#2F1D92";
    ssContext.fillRect(0,  0,  450,  400);


    ssContext.save();


    // Draw the sun ssContext.translate(220,  200);
    ssContext.fillStyle  =  "yellow";
    ssContext.beginPath();

    ssContext.arc(0, 0, 15, 0, Math.PI * 2, true);
    ssContext.fill();


    // Draw  the  earth  orbit ssContext.strokeStyle   =   "black"; ssContext.beginPath();

    ssContext.arc(0, 0, 150, 0, Math.PI * 2); ssContext.stroke();


    ssContext.restore()

    }


    The animateSS() function is what does the real work. It clears the entire area and then fills it with dark blue. the rest of the code relies on transformations, so it first saves the drawing context and then restores it when finished.

    This animateSS() function uses the translate() function to move the origin to the approximate midpoint of the canvas. the sun and the earth orbits are drawn using the arc() function. notice the center point for both is 0, 0 since the context’s origin is now in the middle of the canvas. also, notice the start angle is 0 and the end angle is specified as Math.PI *  2. In radians, this is a full circle or 360°. the arc for the sun is filled in, and the orbit is not

  5. Call the setInterval() function to call the animateSS() function every 100 milliseconds.
        setInterval(animateSS, 100);

  6. Display the page in the browser. So far, the drawing is not very interesting; it’s a sun with an orbit drawn around it, as shown here:
    New Content Item
    Figure 4. The initial solar system drawing
    Now you’ll draw the earth and animate it around the orbit. Normally the earth will revolve around the sun once every 365.24 days, but we’ll speed this up a bit and complete the trip in 60 seconds. To determine where to put the earth each time the canvas is redrawn, you must calculate the number of seconds. The amount of rotation per second is calculated as Math.PI *2 / 60. Multiply this value by the number of seconds to determine the angle where the earth should be.

  7. Add the following code that is italicized. This code uses the rotate() function  to rotate the drawing context the appropriate angle. Since the arc for the earth orbit is 150px, this code then uses the translate() function to move the context 150 pixels to the right so the earth can be drawn at the adjusted 0,0 coordinate. Notice that this is combining two separate transforms, one to rotate based the earth position in its orbit and one to translate the appropriate distance from the sun. The earth is then drawn using a filled arc with a center point of 0,0, the new origin of the context.

    // Draw  the  earth  orbit
    ssContext.strokeStyle   =   "black";
    ssContext.beginPath();

    ssContext.arc(0, 0, 150, 0, Math.PI * 2);
    ssContext.stroke();


    // Compute the current time in seconds (use the milliseconds

    // to  allow  for  fractional  parts). var now = new Date();

    var seconds = ((now.getSeconds() * 1000) + now.getMilliseconds()) / 1000;

    //---------------------------------------------

    // Earth

    //---------------------------------------------

    // Rotate the context once every 60 seconds var
    anglePerSecond = ((Math.PI * 2) /
    60);
    ssContext.rotate(anglePerSecond * seconds);
    ssContext.translate(150,  0);


    // Draw the earth ssContext.fillStyle
    =  "green";
    ssContext.beginPath();
    ssContext.arc(0, 0, 10, 0, Math.PI * 2, true);
    ssContext.fill();


    ssContext.restore()

  8. Save your changes and refresh the browser. now you should see the earth make its way around the sun, as shown in this figure.
    New Content Item
    Figure 5. Adding the earth to the drawing
    Now you’ll show the moon revolving around the earth, which will demonstrate the real power of using transformations. The specific position of the moon is based on two moving objects. While it’s certainly possible to compute this using some complex formulas (scientists have been doing this for centuries), however, with transformations, you don’t have to. The drawing context was rotated the appropriate angle based on current time (number of seconds). It was then translated by the radius of the orbit, so the earth is now at the origin of the context. It doesn’t really matter where the earth is; you can simply draw the moon relative to the current origin.

  9. You will now draw the moon just like you drew the earth. Instead of the origin being at the sun and rotating the earth around the sun, the origin is on the earth, and you’ll rotate the moon around the earth. the moon will rotate around the earth approximately once each month; in other words, it will complete about 12 revolutions for each earth orbit. So, you’ll need to rotate 12 times faster. the anglePerSecond is now computed as 12  *  ((Math.PI  *  2)  / 60). Add the following code shown italicized.

    // Draw the earth
    ssContext.fillStyle  =  "green";
    ssContext.beginPath();
    ssContext.arc(0, -0, 10, 0, Math.PI * 2, true);
    ssContext.fill();


    //---------------------------------------------
    // Moon
    //---------------------------------------------
    // Rotate the context 12 times for every earth revolution
    anglePerSecond = 12 * ((Math.PI * 2) / 60);
    ssContext.rotate(anglePerSecond * seconds);
    ssContext.translate(0,  35);


    // draw the moon ssContext.fillStyle   =   "white";
    ssContext.beginPath();
    ssContext.arc(0, 0, 5, 0, Math.PI * 2, true); ssContext.fill();

    ssContext.restore()

    Note: There are about 12.368 lunar months per solar year. You can make your model more accurate by using this figure instead of 12 in the preceding code.

  10. Save your changes and refresh the browser. You should now see the moon rotating around the earth, as shown below.
    New Content Item
    Figure 6. Including the moon

About the Author

Mark Collins has been developing software solutions for over 30 years. Some of the key technology areas of his career include COM, .NET, SQL Server, and SharePoint. He currently supports a large non-profit organization, serving as data architect and back office automation and integration specialist. You can see more info at his company TheCreativePeople.

Want more? This article is excerpted from Pro HTML5 with CSS, JavaScript, and Multimedia by Mark Collins, ISBN 978-1-48422-462-5. Get your copy today and learn how to build modern interactive websites including many more examples demonstrating fundamentals and advanced features.