Thursday, 1 August 2013

more java-script drawing

In 2d games, animation is changing images, much like frames in a movie.  The problem for programmers like myself is... creating them.  I've been lucky that the graphics I will present here were created for me years and years ago by some very talented 2d artists.

So how does it work?  First of all, you need to create an "animation strip".  Basically a series of images the same size (preferably) that form your mini movie.  First thing to take into account is image transparency.

This is because 2d games are layered.  Whatever you draw last goes over the top of things already drawn.  So typically we construct scenes in layers.  Backgrounds first, then the player's and all things around us, and finally, usually last, display items such as health-bars, scores etc.

Images are always rectangular.  In the old days images used to have to be powers of 2 in size (e.g. their sizes had to be one of 2, 4, 8, 16, 32, et.etc) for hardware limitation reasons.  That limit doesn't exist in java-script.

I use a program called the Gimp (GNU Image Manipulation Program) for changing images and adding transparencies.  Its free and its fairly easy to use.

Drawing in General

First we need to load an image.  You can load any image from the Internet quite easily and hold on to it in java-script.  Here is a little class I used to load an image

function ImageLibrary()
{
    this.images = {};

    // setup a loader with a wait function
    this.load = function( imageUrl )
    {
        this.images[imageUrl] = null;
        var img = new Image();
        img.src = imageUrl;
        img.onload = function() { this.loaded(img, imageUrl); };
    };

    this.loaded = function( img, imageUrl )
    {
        this.images[imageUrl] = img;
    };
}

to use this little class it you would use the following java-script;

// create the library instance
var imgLibrary = new ImageLibrary();
// load an image
imgLibrary.load( 'img/image.jpg' );

There is only one problem with java-script images.  They load "asynchronously".  You tell java-script to download it (the img.src = imageUrl line does that) and java-script will go away in parallel, and tell you when its done.  The img.onload = function() takes care of that.  Its what we call a "callback".  The system will do its thing and we redirect it to "this.loaded" when it has done its thing.

Lets not worry about how to deal with that for now.  I will show you a complete sample now that loads three images and draws them onto your canvas in order of loading.

<html>
<body>
  <canvas id="gameCanvas" width="200" height="200"
    style="border:1px solid #000000;">
  </canvas>

  <script language="javascript">
  
 function ImageLibrary( context2d )
 {
     var self = this;
     var context = context2d;

     // load an image and draw it when its ready
     this.loadAndDraw = function( imageUrl, x, y )
     {
         var img = new Image();
         img.src = imageUrl;
         img.onload = function() { self.drawImage(img, x, y); };
     };

     // draw an image @ x,y rotated around its center by rot in degrees
     this.drawImage = function( img, x, y )
     {
            context.drawImage( img, x, y ); // adjust drawing
     };
 }

 // get canvas
    var screenCanvas = document.getElementById("gameCanvas");
    var screenContext = screenCanvas.getContext("2d");

 // create new library
 var lib = new ImageLibrary( screenContext );
 lib.loadAndDraw( "img/ship1.png", 10, 10);
 lib.loadAndDraw( "img/ship2.png", 20, 20);
 lib.loadAndDraw( "img/ship3.png", 30, 30);
  
  </script>

</body>
</html>

One little thing I need to make you aware of is a very important line in the java-script.

var self = this;

Here "self" is local private variable.  I called it "self" and it has no significance beyond that.  "self" is assigned "this".  "this" is a very special variable if you recall.  It is the object you're inside if you like.  It allows a thing to refer to itself.  The important part here is that "this" will change depending on where you are.  If you look at the code above for the "img.onload = function" part above, you'll see that I used "self" instead of "this".  This is because at that point inside the new "function() { ...", our "this" object will have changed and refer to another entity.  This is where "self" comes to the rescue.  The same problems occur if you use a library called jQuery.  I do use jQuery later on.  So remember "var self = this;"   :)

There, a start.  You can download that sample here.  The output of this program is as follows:



Drawing Animations

Lets do something more exciting, something grander.  Lets do something that moves.  There are two things we need to do to make an animation.  We already have an animation strip, shown here.


Now we need to write the code to display it after its loaded - and change it.  We're still keeping it simple at this point.  The code I'm showing you isn't ready for a game yet.  It does however form the foundations for one.

The image shown has 16 different animations in it.  The image goes across (i.e. the changes in the animation are horizontal.  The image is 2560 pixels wide by 120 pixels high.  Given it is a combination of 16 different images, this means that each individual image is 160 pixels across and 120 pixels high.

There is a function in java-script for drawing smaller parts of an image.  This is exactly what we'll use to draw the image.

That function is a variant of the draw-image we used above.  It takes a lot more parameters.

.drawImage( img, offsetX, offsetY, singleWidth, singleHeight,
     screenx, screeny, singleWidth, singleHeight );

Thats a lot of parameters.  "img" is of course the image itself.  This can be drawn after its loaded.  "offsetX" and "offsetY" are offsets inside the image we've loaded.  The first singleWidth and singleHeight parameter are how much to draw from the image strip.  "screenx" and "screeny" are the screen location/destination of where the image is to go.  The second "singleWidth" and "singleHeight" are the same as the first and are how big the image is to appear on the screen.

The only other thing we need is time.  A timer to be more exact.  We need to change the image (because it is an animation) over time.  What we need to do is the following about 20 times a second

1. each time, we clear the screen for a new "scene"
2. we draw the correct animation of the graphic
3. thats it - the timer will come around again and start at 1.

function ImageLibrary( context2d )
{
  var self = this;
  var context = context2d;
  var index = 0;
  var numAnimations = 16;
  var animationImage = null;

  // load an image and draw it when its ready
  this.loadAndDraw = function( imageUrl )
  {
    var img = new Image();
    img.src = imageUrl;
    img.onload = function()
    {
      animationImage = img;
      window.setInterval( self.drawImage, 50);
    };
  };

  this.drawImage = function()
  {
    if ( animationImage != null )
    {
      context.fillStyle = "#555555";
      context.fillRect(0, 0, 200, 200);
      var singleWidth = animationImage.width / numAnimations;
      var singleHeight = animationImage.height;
      var offsetX = singleWidth * index;
      var offsetY = 0;
      context.drawImage( animationImage, offsetX, offsetY, singleWidth,
   singleHeight, 10, 10, singleWidth, singleHeight
                        ); // adjust drawing
      index = index + 1;
      if ( index >= numAnimations )
      {
        index = 0;
      }
    }
  };
}

The code has changed considerably.  First there are three new variables.

    var index = 0;
    var numAnimations = 16;
    var animationImage = null;

The "numAnimations" is a hardwired value that works with the image I provided.  It means there are 16 images to display.  The first image is image 0 (zero).  This is what "index" is for.  It becomes the "pointer" to the current image we're looking at.  "index" changes every frame and will always be between 0 and 16 (it actually will go as high as 15, because we start at 0 and then return to 0).  "animationImage" is a place-holder for the image after it has been loaded.  This value isn't set until the image has been loaded.  This gives us a sure way to tell whether we're reading for drawing the animation or not.

The second set of changes is to the "onload" function.

  animationImage = img;
  window.setInterval( self.drawImage, 50);

The most important of these two is the "window.setInterval()" method.  It is a built in method of the browser that sets up a periodic timer.  I've set its value to "50" which means "wait 50 milliseconds before calling the function you have specified".  There are 20 lots of 50 milliseconds in a second, meaning this drawing will run at 20 frames per second (if your computer/phone can keep up with that).

This is the function that is being called 20 times per second.

  this.drawImage = function()
  {
    if ( animationImage != null )
    {
      context.fillStyle = "#555555";
      context.fillRect(0, 0, 200, 200);
      var singleWidth = animationImage.width / numAnimations;
      var singleHeight = animationImage.height;
      var offsetX = singleWidth * index;
      var offsetY = 0;
      context.drawImage( animationImage, offsetX, offsetY, singleWidth,
   singleHeight, 10, 10, singleWidth, singleHeight
                        ); // adjust drawing
      index = index + 1;
      if ( index >= numAnimations )
      {
        index = 0;
      }
    }
  };

It first checks if the image has been loaded ( animationImage != null ).  I then clears the canvas using "fillRect" with a darkish grey colour (#555555).  When then work out the size of the image by asking the image "how big are you" using "animationImage.width" and "animationImage.height".  We divide the width of the image by "numAnimations" because the image isn't a single image but 16 different images combined together.

Final step - we use "index" to show a different image each time.  We draw part of the image, and add "1" to index.  We then check if index has arrived at the end of the 16 animations, and if it has, we re-set it back to zero.  The output of this program is shown below.



  // get canvas
  var screenCanvas = document.getElementById("gameCanvas");
  var screenContext = screenCanvas.getContext("2d");

  // create new library
  var lib = new ImageLibrary( screenContext );
  lib.loadAndDraw( "img/base.png", 10, 10);


The rest of the program is as before.  We get the canvas, we create the "ImageLibrary" and load the animation graphic.  I've had to move a few things around to make it work.  Don't be put-off by that.  From a pure programming point of view, I've change the "ImageLibrary" too much and hijacked its function.  It no longer is a pure image library - it draws things too.  Here is something you need to get familiar with called "refactoring".  It means that your code has come to a point where it no longer fits its original design and needs to be cleaned up.

The above sample can be downloaded for personal viewing here.

No comments:

Post a Comment