HTML5 2D game development: Manipulating time, Part 2

Using time transducers to implement nonlinear effects

In this series, HTML5 maven David Geary shows you how to implement an HTML5 2D video game one step at a time. In this installment, learn how to bend time to your will to create nonlinear motion and color changes.

Share:

David Geary, Author and speaker, Clarity Training, Inc.

David GearyThe author of Core HTML5 Canvas, David Geary is also the co-founder of the HTML5 Denver User's Group and the author of eight Java books, including the best-selling books on Swing and JavaServer Faces. David is a frequent speaker at conferences, including JavaOne, Devoxx, Strange Loop, NDC, and OSCON, and is a three-time JavaOne Rock Star. He is the author of the HTML5 2D game development, JSF 2 fu, and GWT fu article series for developerWorks.



12 March 2013

Also available in Chinese Russian Japanese

"Manipulating time, Part 1" (developerWorks, February 2013), a prerequisite for this article, discussed the implementation of the runner's jump behavior in Snail Bait. That implementation resulted in linear motion, meaning that the runner ascended and descended at a constant rate of speed. In the physical world, however, gravity causes anyone jumping under her own power to slow on the ascent and speed up on the descent.

This article's superficial goal is to modify the runner's jump behavior so that she jumps in a more natural motion, as depicted in Figure 1. The article's underlying motivation, however, is to show you how to warp time to create nonlinear effects for any of time's derivatives, such as motion or color change.

Figure 1. A natural jumping sequence
An animated character in the stages of a jump sequence

In this article, learn how to:

Transducer

Dictionary.com's definition of transducer:

noun
a device that receives a signal in the form of one type of energy and converts it to a signal in another form: A microphone is a transducer that converts acoustic energy into electrical impulses.

  • Implement time transducers
  • Use animation timers — a stopwatch with a transducer — to warp time
  • Create nonlinear motion with time transducers
  • Create nonlinear color changes with time transducers
  • Implement realistic jumping, pulsating, and bouncing
  • Pause behaviors

Animation timers and time transducers

I've adopted the term transducer from the realms of electrical engineering and motion pictures, and applied it to time. Time transducers are functions that convert time from one value to another, as shown in Figure 2.

Figure 2. A transducer
An illustration of two stopwatches showing different elapsed times, with the following JavaScript function between the stopwatches: function (percentComplete) { return Math.pow(percentComplete, 6); }

As Listing 1 attests, it's a simple matter to combine time transducers and stopwatches to implement an animation timer that can warp time. See the "Timed animations: Stopwatches" section in the previous article for a discussion of the stopwatch's implementation.

Listing 1. An animation timer
// AnimationTimer..................................................................
//
// An animation runs for a duration, in milliseconds. 
//
// You can supply an optional transducer function that modifies the percent
// completed for the animation. That modification lets you incorporate
// nonlinear motion, such as ease-in, ease-out, elastic, etc.
   
AnimationTimer = function (duration, transducer)  {
   this.transducer = transducer;

   if (duration !== undefined) this.duration = duration;
   else                        this.duration = 1000;

   this.stopwatch = new Stopwatch();
};

AnimationTimer.prototype = {
       start: function () { this.stopwatch.start();           },
        stop: function () { this.stopwatch.stop();            },
       pause: function () { this.stopwatch.pause();           },
     unpause: function () { this.stopwatch.unpause();         },
    isPaused: function () { return this.stopwatch.isPaused(); },
   isRunning: function () { return this.stopwatch.running;    },
       reset: function () { this.stopwatch.reset();           },

   isExpired: function () {
      return this.stopwatch.getElapsedTime() > this.duration;
   },

   getElapsedTime: function () {
      var elapsedTime = this.stopwatch.getElapsedTime(),
          percentComplete = elapsedTime / this.duration;

      if (percentComplete >= 1) {
         percentComplete = 1.0;
      }

      if (this.transducer !== undefined && percentComplete > 0) {
         elapsedTime = elapsedTime *
                       (this.transducer(percentComplete) / percentComplete);
      }

      return elapsedTime;
   },

};

Animation timers are essentially stopwatches with two additional features. First, because they are timers, animation timers have a duration, specified in milliseconds, and they provide an isExpired() method that tells you whether the timer has expired.

Second, animation timers have an optional time transducer. The timer's getElapsedTime() method runs the stopwatch's elapsed time through the transducer and returns the result.


Using time transducers to implement motion tweening

The HTML5 canvas element provides a powerful low-level 2D graphics API, but it lacks higher-level abstractions found in other graphics frameworks, such as Flash. For example, Flash lets you define starting and ending frames for an animation, and Flash creates the frames in between according to a time transducer — which Flash refers to as a tweening function — that you supply. In this article, I implement motion tweening with Canvas.

Figure 3 illustrates two classic motion tweens: ease-in and ease-out. To illustrate those effects, the application shown in Figure 3 draws a vertical timeline that represents real time.

Figure 3. Ease-in (left) and ease-out (right)
Two series of screenshots of the Snail Bait runner, illustrating ease-in and ease-out

Ease-in, which is illustrated by the screenshots in the left column in Figure 3 from top to bottom, starts slowly — far behind the timeline — and gains speed at the end. Ease-out, illustrated in the right column, is the opposite effect, starting with a burst of speed and slowing at the end.

The ease-in and ease-out effects are implemented, from a mathematical standpoint, by the equations depicted by the two graphs in Figure 4. The horizontal axis represents the real-time percentage of the animation that is complete, whereas the vertical axis represents the completion percentage returned from an appropriate transducer. The straight lines in the graphs represent real time, and the curves show how the effects warp time.

Figure 4. Ease-in (f(x) = x^2) and ease-out (1 - (1-x)^2) transducers
Ease-in and ease-out graphs

The ease-in effect, shown in the left-hand graph in Figure 4, consistently reports less time than has actually passed; for example, when real time (on the horizontal axis) is halfway through the animation, an ease-in transducer reports that it's only one-quarter of the way through the animation.

The ease-out effect, shown in the right-hand graph in Figure 4, consistently reports more time than has actually passed; for example, when real time (on the vertical axis) is halfway through the animation, an ease-out transducer reports that it's three-quarters of the way through the animation.

Note that in both cases, at the end of the animation, the real time is equal to the transduced time.

The animation timer in Listing 1 is ready to warp time; all it needs is a time transducer to do so. Listing 2 shows AnimationTimer methods that create time transducers. The animation timer passes those transducer functions a value that represents an animation's completion percentage, and those functions return a modified completion percentage.

Listing 2. Making transducers
AnimationTimer.makeEaseOutTransducer = function (strength) {
   return function (percentComplete) {
      strength = strength ? strength : 1.0;

      return 1 - Math.pow(1 - percentComplete, strength*2);
   };
};

AnimationTimer.makeEaseInTransducer = function (strength) {
   strength = strength ? strength : 1.0;

   return function (percentComplete) {
      return Math.pow(percentComplete, strength*2);
   };
};

AnimationTimer.makeEaseInOutTransducer = function () {
   return function (percentComplete) {
      return percentComplete - Math.sin(percentComplete*2*Math.PI) / (2*Math.PI);
   };
};

AnimationTimer.makeElasticTransducer = function (passes) {
   passes = passes || 3;

   return function (percentComplete) {
       return ((1-Math.cos(percentComplete * Math.PI * passes)) *
               (1 - percentComplete)) + percentComplete;
   };
};

AnimationTimer.makeBounceTransducer = function (bounces) {
   var fn = AnimationTimer.makeElasticTransducer(bounces);

   bounces = bounces || 2;

   return function (percentComplete) {
      percentComplete = fn(percentComplete);
      return percentComplete <= 1 ? percentComplete : 2-percentComplete;
   }; 
};

AnimationTimer.makeLinearTransducer = function () {
   return function (percentComplete) {
      return percentComplete;
   };
};

Now that I have a way to warp time, I'll use it to implement a natural jumping motion.


Natural jumping motion

"Manipulating time, Part 1" discussed implementing the runner's jump behavior with linear motion. To time the jump, I used two stopwatches — one for the jump's ascent and another for the descent. As the ascent or descent progressed, the jump behavior used the appropriate stopwatch's elapsed time to determine how far to move the runner up or down, respectively, for each animation frame.

To illustrate the power of time transducers, Listing 3 shows the necessary modifications to Snail Bait to implement a natural, nonlinear jumping motion, instead of the stilted jumping that results from linear motion.

Listing 3. Creating the runner's timers
equipRunnerForJumping: function () {
      ...

      // On the ascent, the runner looses speed due to gravity (ease out)

      this.runner.ascendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseOutTransducer(1.0));

      // On the descent, the runner gains speed due to gravity (ease in)

      this.runner.descendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseInTransducer(1.0));
      ...

      };
   },
};

Instead of creating stopwatches to time the jump's ascent and descent, in Listing 3 I'm now using animation timers. The ascend timer is fitted with an ease-out transducer, which causes the ascent to begin with a burst of speed and slowly lose velocity during the ascent. The descent begins slowly and gains velocity as it progresses, by virtue of an ease-in transducer.


Fine-tuning time transducers

Because they use the power of a number to compute values, the curves shown in Figure 4 are known as power curves. Power curves are prevalent in many disciplines, from economics to animation tweening. Figure 4 shows curves for a power of 2; changing that number, as shown in Figure 5, results in different power curves.

Figure 5 shows three power curves for an ease-in effect. From left to right, they represent powers of 2, 3, and 4, respectively. Increasing the exponent exaggerates the ease-in effect.

Figure 5. Ease-in (f(x) = x^2) and ease-out (1 - (1-x)^2) transducers
Ease-in power curves

The AnimationTimer methods that create the ease-in and ease-out transducers in Listing 2 take an argument representing one-half of the exponent for their power curves. The default value is 1, which results in a power of 2.

By modifying the value you pass to AnimationTimer.makeEaseInTransducer() and AnimationTimer.makeEaseOutTransducer(), you can control the effect's strength. For example, in Listing 4, I increase the strength of the ease-in and ease-out effects during the runner's jump from 1.0 to 1.15. That small change slightly exaggerates both effects, which makes the runner hang in the air a little longer at the apex of the jump.

Listing 4. Creating the runner's timers
equipRunnerForJumping: function () {
      ...
      this.runner.ascendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseOutTransducer(1.15));

      this.runner.descendAnimationTimer =
         new AnimationTimer(this.runner.JUMP_DURATION/2,
                            AnimationTimer.makeEaseInTransducer(1.15));
      ...

      };
   },
};

Now that you've seen how to implement nonlinear motion, let's take a look at how to implement nonlinear effects for other derivatives of time.


Pulsating platforms: Nonlinear color change

Time's derivatives

If you know how fast an object is moving, you can derive its position from its initial location and the amount of time it has been moving (assuming constant velocity). Motion, therefore, is a derivative of time. If you control time, you automatically affect all its derivatives, such as motion or color changes.

As you've seen in the past few articles in this series, sprite behaviors — such as running and jumping — are derived from time; for example, the runner's location during a jump is determined by how much time has elapsed since the jump began.

Even though my intent was to implement nonlinear jumping, I did not implement motion transducers; instead, I implemented time transducers, because time transducers let me create nonlinear effects for any derivative of time, such as color change, as depicted in Figure 6.

Figure 6. A pulsating platform
Screenshot of a pulsating platform in Snail Bait

Figure 6 shows a platform that pulsates by constantly modifying its opacity. A linear color change would result in a blinking effect. But, instead, I want a pulsating effect, whereby the platform has an initial burst of color, followed by a slow draining of the color, followed by another burst, and so on. For that effect, I once again use an ease-in transducer and an ease-out transducer, as shown in Listing 5.

Listing 5. The pulse behavior's constructor
PulseBehavior = function (duration, opacityThreshold) {
   this.duration = duration || 1000;
   this.brightTimer = new AnimationTimer(this.time,
                                         AnimationTimer.makeEaseOutTransducer());

   this.dimTimer = new AnimationTimer(this.time, 
                                         AnimationTimer.makeEaseInTransducer());
   this.opacityThreshold = opacityThreshold;
},

Listing 5 shows the constructor for a pulse behavior. The duration is one-half of the amount of time it takes to display one pulse, and the opacity threshold is the dimmest the image will get during a single pulse.

Listing 6 shows the implementation of the pulse behavior's execute() method.

Listing 6. The pulse behavior's execute() method
PulseBehavior.prototype = { 
   ...

   execute: function(sprite, time, fps) {
      var elapsedTime;

      // If nothing's happening, start dimming and return

      if (!this.isDimming() && !this.isBrightening()) {
         this.startDimming(sprite);
         return;
      }

      if(this.isDimming()) {               // Dimming
         if(!this.dimTimer.isExpired()) {     // Not done dimming
            this.dim(sprite);
         }
         else {                            // Done dimming
            this.finishDimming(sprite);
         }
      }
      else if(this.isBrightening()) {      // Brightening
         if(!this.brightTimer.isExpired()) {  // Not done brightening
            this.brighten(sprite);
         }
         else {                            // Done brightening
            this.finishBrightening(sprite);
         }
      }
   },
};

The pulse behavior's execute() method implements pulsating — which consists of switching back and forth between dimming and brightening — at a high level of abstraction. The methods invoked in Listing 6 for dimming are shown in Listing 7.

Listing 7. Dimming
PulseBehavior.prototype = { 
   ...

   startDimming: function (sprite) {
      this.dimTimer.start();
   },
      
   isDimming: function () {
      return this.dimTimer.isRunning();
   },
      
   dim: function (sprite) {
      elapsedTime = this.dimTimer.getElapsedTime();  
      sprite.opacity = 1 - ((1 - this.opacityThreshold) *
                            (parseFloat(elapsedTime) / this.duration));
   },

   finishDimming: function (sprite) {
      var self = this;
      this.dimTimer.stop();
      setTimeout( function (e) {
         self.brightTimer.start();
      }, 100);
   },
};

The interesting part of Listing 7 is the dim() method, which dims the platform by reducing its opacity. That opacity reduction is calculated with the amount of time that has elapsed since dimming began. (More accurately, it's the amount of time that the dim timer says has elapsed.)

When dimming is finished, the finishDimming() method stops the dimming timer and, after a short pause, starts the brightening timer.

The pulse behavior's methods concerning brightening are in Listing 8.

Listing 8. Brightening
PulseBehavior.prototype = { 
   ...

   isBrightening: function () {
      return this.brightTimer.isRunning();
   },

   brighten: function (sprite) {
      elapsedTime = this.brightTimer.getElapsedTime();  
      sprite.opacity += (1 - this.opacityThreshold) *
                         parseFloat(elapsedTime) / this.duration;
   },

   finishBrightening: function (sprite) {
      var self = this;
      this.brightTimer.stop();
      setTimeout( function (e) {
         self.dimTimer.start();
      }, 100);
   },
};

Most sprite behaviors are nonlinear

In the physical world, most derivatives of time, such as motion or color changes, are nonlinear. From bouncing balls to sprinters coming off the blocks, nearly everything around us is nonlinear. Because of the ubiquity of nonlinearity in the real world, it's important to know how to implement it in your games.

The brightening methods are nearly identical to their dimming counterparts, except for the timer that each uses. Additionally, the brighten() method increases the platform sprite's opacity, instead of decreasing it, as was the case for the dim() method in Listing 7.


Pausing behaviors

The "Pausing the game" section in the third article in this series discussed how to pause Snail Bait. Now that I've added sprite behaviors to the game, I must pause those behaviors also, as illustrated in Listing 9.

Listing 9. Pausing and unpausing all of Snail Bait's sprite behaviors
SnailBait.prototype = {
   ...

   togglePausedStateOfAllBehaviors: function () {
      var behavior;
   
      for (var i=0; i < this.sprites.length; ++i) { 
         sprite = this.sprites[i];

         for (var j=0; j < sprite.behaviors.length; ++j) { 
            behavior = sprite.behaviors[j];

            if (this.paused) {
               if (behavior.pause) {
                  behavior.pause(sprite);
               }
            }
            else {
               if (behavior.unpause) {
                  behavior.unpause(sprite);
               }
            }
         }
      }
   },

   togglePaused: function () {
      var now = +new Date();

      this.paused = !this.paused;

      this.togglePausedStateOfAllBehaviors();
   
      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
};

Pausing all of the game's behaviors is a simple matter, as you can see from Listing 9. But it requires behaviors to implement pause() and unpause() methods, whereas previously the only requirement for an object to be a behavior was to implement an execute() method.

Behaviors implement their pause() and unpause() methods on a case-by-case basis. Listing 10 shows the implementation of those methods for the pulse behavior.

Listing 10. Pausing the pulse behavior
PulseBehavior.prototype = { 
   ...

   pause: function() {
      if (!this.dimTimer.isPaused()) {
         this.dimTimer.pause();
      }

      if (!this.brightTimer.isPaused()) {
         this.brightTimer.pause();
      }

      this.paused = true;
   },

   unpause: function() {
      if (this.dimTimer.isPaused()) {
         this.dimTimer.unpause();
      }

      if (this.brightTimer.isPaused()) {
         this.brightTimer.unpause();
      }

      this.paused = false;
   },

To pause or unpause the pause behavior, simply pauses or unpauses its timers, if they are running.


Next time

In the next article in this series, see you how Snail Bait implements collision detection and explosions. In a later installment, you'll learn how to make sprites fall by incorporating gravity into a sprite behavior.


Download

DescriptionNameSize
Sample codewa-html5-game7-code.zip1.2MB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Java technology
ArticleID=861044
ArticleTitle=HTML5 2D game development: Manipulating time, Part 2
publish-date=03122013