Sunday, September 12, 2010

Animating the Canvas part 4: Laying out Layers

As I am partially writing these classes for use with November's Tarot release, I am a lot further in the code than I would be if I was just working on this code Sunday afternoons (which is the plan for this blog from now on). I have been packaging up the source as it has reached logical development points but it will be a while before this series of articles catches up with the work. The source code is located at http://www.blazinggames.com/other/openLibrary/html5Libs.

With primitives started it is now time to get to the real code and start implementing the layers class. This class will be a base class for all of the other elements that get added to the canvas. The problem with this is that JavaScript does not currently support superclasses or inheritance. This is something that may come to a future version of JavaScript (ECMAScript version 6 unless it gets delayed like it did when such features were in ECMAScript version 4) but for now we have to manually perform the inheritance. While some browsers support a technique known as prototype chaining by exposing the __proto__ property, this is not always the case so manually copying the properties must be done for browser compatibility. A simple function can handle this making creating subclasses much easier, though at a bit of cost in efficiency and memory.

BGLayers.inheritProperties = function(child, supr)
{
    for (var property in supr) { 
        if (typeof child[property] == "undefined") 
            child[property] = supr[property]; 
    } 
}

The layers are loosely based on the layer classes that I created for Dozen Days of Words Episode 12: Morse Code but with the addition of dirty rectangle support and switching from absolute positions to relative positioning. Relative positioning adds a lot more flexibility for animation as any given layer only has to worry about it's position relative to it's parent. For this reason, a layer can be created with a width and a height which can, with the exception of the root (stage) layer, be any size as scaling will be done when the component is rendered onto the canvas.

The logical and real positions of the layer is dependent on the parent. This means that there should only be a single parent for a layer, though layers can have multiple children.  This means that when you add a child to a layer, that child is being assigned a new parent. If it already has a parent that parent is told that the child is being removed and the new parent becomes the parent. The addChild, removeChild, and setParent functions handle all this work. Note that these functions take advantage of the addDirty function to let the old and new parents know which part of the display has changed.

The addDirty function handles our dirty rectangle list, but at the moment is far from complete as I was more concerned with getting the code functional. Currently the addDirty function simply adds the dirty rectangle, expressed in real canvas coordinates, to the root layer. The root layer is simply the layer which does not have a parent. To find the layer, the parent is called until there is no parent. Right now no effort is being made to cull the list to eliminate overdrawing. This feature will be added in the future.

While for internal work the real size and location of the layer is not that important, for marking dirty rectangles and rendering them knowing the real location becomes vital. This is where the findRealPosition function comes into play. As this is such an important function, lets go over it in detail.

First, to speed things up, we don't want to constantly re-calculate this so we have a flag that lets us know if the real position is known and if so, we return the already calculated position. This means that when the logical position or parent layer position changes the flag has to be set to false so when the final code is put together the logical position information may be marked protected (sadly, there is no such thing as protected variables in JavaScript yet, so we have to go on the honour system).

    if (this.realPositionKnown)
        return this.realPosition;

If this is the root layer then it's logical position and size are the real thing. If it is not, then we need to get the real thing by getting the parent's real position by calling this function on the parent. If the parent is not the root layer it will call it's parent and so on until the real position can be determined.

    var parentPos = this.logicalPosition;
    var w = this.logicalSize.width;
    var h = this.logicalSize.height;
    if (this.parent != null) {
        parentPos = this.parent.findRealPosition();
        w = this.parent.logicalSize.width;
        h = this.parent.logicalSize.height;
    }

Knowing the real position of the parent then gives us the information we need to find our real position. This is done by determining how big we are logically compared to how big of an area we are in our parent's eyes. This results in scaling factors.

    var scaleX = parentPos.width / w;
    var scaleY = parentPos.height / h;

With the scaling factors known, we can simply calculate the position by multiplying the logical coordinates by the scale and adding the parent's position.

    this.realPosition.x = parentPos.x + this.logicalPosition.x * scaleX;
    this.realPosition.y = parentPos.y + this.logicalPosition.y * scaleY;
    this.realPosition.width = this.logicalPosition.width * scaleX;
    this.realPosition.height = this.logicalPosition.height * scaleY;
    this.realPositionKnown = true;
    return this.realPosition;

Now we know where to draw things, the next step is to actually implement the rendering  of things which we will do next week.

No comments: