Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Pfz.AnimationManagement.js

4.86/5 (20 votes)
20 Jan 2014CPOL10 min read 23.8K   476  
A fluent library for interactive animations in JavaScript

See the samples running

Cyberminds 57 example.

CollisionDetection + Explosions example.

Background

Sometime ago I wrote the Fluent and Imperative Animations article in which I presented an animation library capable of mixing imperative and declarative animations for .NET. Now it is the time to present a version of such library for JavaScript.

To those who know me, it may seem strange that I am writing an article for JavaScript considering how much I love C#. In fact, if Microsoft decided to make Silverlight more supported I would probably stay using Silverlight, but considering that Silverlight is discontinued and that even my wife was not being able to see the animated web pages in her computer, I decided to use something cross-platform, and this is JavaScript.

Why an animation library?

Before starting to write the article for JavaScript, I searched to see if there are already made animation libraries for JavaScript.

In some places I've found the information that we don't need animation libraries as CSS is already capable of creating animations. In others I've found that even jQuery allows to write animations.

Well, the problem with CSS animations is that they are declarative only. There's no way to put imperative code as part of the animation and the work-arounds to try to do that make the code too complicated. Also, many of the special effects are still "vendor-specific" requiring many different ways of writing the same thing to support different browsers or the effect will simply fail. I've even found a small game written only in CSS, but it uses so many "tricks" to try to avoid JavaScript code that it is not for designers anymore... but it is not code for programmers either. So, in my opinion, CSS is not good if we want interactive animations.

And about jQuery, maybe it is my misunderstanding but apparently it only has basic effects, so it is far from ideal to write a game. The library I am going to present here is already tested to write games and, even if the game is not complete now, well... most of the code is a simply port from C# version, which also means that if you want to learn a single library to create animations, you will be able to reutilise most of the concepts (and even method names, aside capitalization) in C# and in JavaScript.

Also, making animations in JavaScript has the advantage that the effects are less likely to require vendor specific parameters than animations written in CSS.

How it works?

Conceptually everything starts from an interface with the functions reset() and update().

The update() function will receive the elapsed time since the last call, in milliseconds.

The reset() function, well, it is responsible for resetting the actual animation to its original values.

And to effectively run an animation, it is enough to call the the AnimationManager.add() function giving the animation object.

Even if this principle looks too simple, it is extremely powerful as it can be decorated in many different ways and it allows to create an interactive animation with this code:

Java
AnimationBuilder.
beginParallel().
  beginLoop().
    beginPrematureEndCondition(function () { return _horizontalMovement <= 0; }).
      rangeBySpeed(function () { return _left; }, 700-64, 120, function (value) { _left = value; playerCharacter.style.left = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _horizontalMovement >= 0; }).
      rangeBySpeed(function () { return _left; }, 0, 120, function (value) { _left = value; playerCharacter.style.left = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _verticalMovement <= 0; }).
      rangeBySpeed(function () { return _top; }, 500-64, 120, function (value) { _top = value; playerCharacter.style.top = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _verticalMovement >= 0; }).
      rangeBySpeed(function () { return _top; }, 0, 120, function (value) { _top = value; playerCharacter.style.top = value; }).
    endPrematureEndCondition().
  endLoop().
endParallel();

You can see it running here. You can use the cursor keys to move the ship.

In fact, I said that it is an "interface" because each implementation can do whatever it wants with those functions. So, the things that are happening to make this example work are:

  • The RangeBySpeedAnimation receives the initialValue, the finalValue, the amount that the value will change in one second and a function to update the UI. By using a simple formula, if you ask for an animation to move 100 pixels in one second, if the function is invoked after 530 milliseconds, it will increase the value by 53. This will naturally compensate slowdowns that may happen, which is much better than using a fixed "position += something" at every "tick";
  • The LoopAnimation is a "decorator" animation, which will keep redirecting the calls to its inner animation but, when the inner animation ends, will reset it;
  • The PrematureEndCondition is another "decorator". But in its case, it will only call the inner animation when the end condition is not true. As soon as it is true, it stops its inner animation;
  • The ParallelAnimation and the SequentialAnimation are the animations that "glue" all the parts together. As the name says, one runs animations in parallel and the other as a sequence. The best trait of the Sequence animation is that it doesn't care how much time one animation takes. The next animation will only start when the previous one ends, be it one second after, be it 3 hours later;
  • As you may imagine the entire "expression" is using those animations, but it is written with a Fluent API (that's why the names don't match exactly what was typed in the expression);
  • There are other decorators that can be used to accelerate or desaccelerate an inner animations (so you can ask for an already written animation to run slower, without having to rewrite all the times) and as a simple interface, you can create your own animations/decorators by simply creating objects with the update and reset functions.

Declarative API

The Declarative API is there only to make things look more declarative and even to present the functions on the right places.

For example, you can create a WaitAnimation at any moment, but it will be useless if put inside a ParallelAnimation. Now try putting it inside a SequentialAnimation and it will have a meaning.

In the Fluent API, the wait() function only exists inside the SequentialAnimation (created with beginSequence()) and not inside the the ParallelAnimation.

The functions of the declarative API

The function of the declarative API are:

beginSequence()
endSequence()
Creates a sequence animation, which is an animation capable of playing other animations (inner animations) one after the other.
beginParallel()
endParallel()
Creates a parallel animation, which is an animation that plays all its inner animations in parallel.
add(animationOrFunction) Adds an already created animation object or an animation function to the actual animation group or decorator.
If a function is given as the animation, such function will be called as if it was an "update", with the time elapsed since the last call. Such function can process the data directly (and so return true if it should be called again or false if it ended) or it can return another animation to be run, so you can check some conditions and effectively return another specific animation to be run.
range(initialValue, finalValue, duration, updateFunction) Creates an animation that goes from the initial value to the final value in a specific duration (in milliseconds). Each time the animation "ticks" it will call the updateFunction giving the calculated value considering the initialValue, finalValue and duration.
It is important to note that the initialValue, the finalValue or the duration can be functions that actually return those values. This is useful if the value is not present when the declarative expression is created.

This animation shows two ranges running in parallel with different end-values but with the same duration.
rangeBySpeed(initialValue, finalValue, speed, updateFunction) This is similar to the previous one but instead of giving a specific time for the animation you give an "speed", which is the amount the value will change per second.
Using a speed instead of fixed time is preferrable when the animation is interactive. Also, as happens with the range() function, the parameters initialValue, finalValue and speed can be function that return those values when invoked.

This animation shows two rangeBySpeed animations running in parallel with different end-values but with the same speed.
beginRunCondition(condition)
endRunCondition()
Puts a condition to run a single inner animation (note, the inner animation can be a parallel ou sequence animation, so you can indirectly play many animations). After the animation starts playing it is not important if the condition changes, it will continue playing.
beginPrematureEndCondition(condition)
endPrematureEndCondition()
This functions put a condition to end the inner animation prematurely. It is extremely useful to make player characters stop moving when a key is released.
beginLoop()
endLoop()
This decorator will restart its inner animation as soon as it ends.
beginTimeMultiplier(factor)
endTimeMultiplier()
Multiplies the values given to the inner animation timelapses by the given factor, which can be a direct value or a function that generates such value. This effectively makes the animation run faster or slower.
beginPauseCondition(condition)
endPauseCondition()
At every tick the condition is checked. If it is true, then the inner animation is not executed. This effectively pauses the inner animation while the condition is true.
beginSegmentedTime(interval, segmentCompleted)
endSegmentedTime()
Independently if there's a slowdown of an entire second, the segmented animation will update its inner animations using the given interval as its maximum interval, effectively playing the inner animations many times if needed. This is useful if, for example, you want to apply collision detections "per frame" but using declarative (and time based) animations.
The optional segmentCompleted function is called every time a segment is completed (so it is not called if the timelapse is smaller than the segment size) which gives you the right moment to apply collision detections, for example.
wait(duration) Waits until the given time (in millisecond) passes. The duration can be a function that may return a different wait time at every call. This function is only available inside sequences.
waitCondition(condition) Waits until the condition function returns true. As happens with the wait() function, it is only available inside sequences.

Creating your own animation segments

As previously explained, this library is based only on the update() and reset() functions. By creating a new object with these 2 functions, where the update receives the milliseconds elapsed since the last call, you can create your own animation segments. The NumericRangeAnimation is the most basic example of this, but you may create an animation to animate colors or to use a non-linear result, for example.

But I want to explain the NumericRangeAnimation, so you can have a starting point:

Java
function NumericRangeAnimation(initialValue, finalValue, duration, updateFunction) {
    this._beforeStart = true;
    this.initialValue = initialValue;
    this.finalValue = finalValue;
    this.duration = duration;
    this.updateFunction = updateFunction;
};
// This constructor will simply store all the given parameters and set a value
// telling that it was not started yet.

NumericRangeAnimation.prototype.reset = function () {
    this._beforeStart = true;
}
// Well, the reset only says that it was not started yet, so the
// update will start it.

NumericRangeAnimation.prototype.update = function (elapsed) {
    // as you can see, if the animation was not started yet, it will
    // be started now.
    if (this._beforeStart) {
        this._beforeStart = false;
        this._total = 0;

        // the AnimationManager._getValue is to be considered a kind of
        // "internal method". It is capable of evaluating a function to 
        // get its result or it simply returns the value directly.
        // evaluating the function only when the animation is starting
        // is very important for this animation to work properly, as
        // we don't want a random function (for example) to be evaluated
        // at every frame.
        this._initialValue = AnimationManager._getValue(this.initialValue);
        this._finalValue = AnimationManager._getValue(this.finalValue);
        this._duration = AnimationManager._getValue(this.duration);
    }

    // here, if the total time is greator or equal the
    // duration, we update the animation with the latest value
    // and return false, telling that the animation ended.
    this._total += elapsed;
    if (this._total >= this._duration) {
        this.updateFunction(this._finalValue);
        return false;
    }

    // here we do the actual range calculation, call the update function
    // so the UI element can be updated and we return true, telling
    // that the animation is not finished yet.
    var remaining = this._duration - this._total;
    var value = ((this._finalValue * this._total) + (this._initialValue * remaining)) / this._duration;
    this.updateFunction(value);
    return true;
};

I hope you understand this animation segment by the comments I put here. But this is the non-fluent API. To make such animation available to the fluent API, we must "register" a new function in the fluent API, which will affect the result of many begin/end calls. To do this, we use the AnimationBuilder.registerAnimationBuilderModifier function, which receives a function to change the prototypes of all the already existing animation builder types.

As an example, if the range() function wasn't registered, we could register it with this:

Java
AnimationBuilder.registerAnimationBuilderModifier(function (prototype) {
  prototype.range = function (initialValue, finalValue, duration, updateFunction) {
    return this.add(new NumericRangeAnimation(initialValue, finalValue, duration, updateFunction));
  };
});

And with this the range animation is available inside the AnimationBuilder already existing segments.

JavaScript and C# libraries

I build much better examples in C# so, if you want, you can take a look into the C# examples to see what this library has the potential to do.

The C# code actually has more animation segments and it has the possibility to correct elapsed times between two segments in a sequence (if one animation will end in the next 10 milliseconds and the next time lapse comes with 15 miliseconds, 5 miliseconds go to the next animation). And, well, I made the javascript version simplified so it doesn't take care of this.

Yet I think the library is pretty powerful, in special because it allows to create interactive animations that have no specific duration. Some important things to remember if you look at the C# library are:

  • The JavaScript version uses milliseconds as the default timing while the C# one uses seconds or real TimeSpans;
  • The names in C# use a different capitalization than the names in JavaScript;
  • Actually the JavaScript version supports more parameters as functions. In C#, only some overloads exist that support delegates (yes, in this sense the JavaScript version is better);
  • As C# is a typed-language with generics, the C# Range can actually work with integers, double, Colors and any kind of range if you register it. The JavaScript version requires a new function (like a pointRange) for every different type that must be supported.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)