An active ziggurat

Let’s now implement a generalized version of the moveChildren function which gets called with the root node of a scene graph followed by zero or more behavior functions.

function moveChildren(root, ...fncs) {
    let children = root.children;
    children.forEach(function (child, i, children) {
        let animFncs = fncs.map(g => g(child, i, children));
        child.update = sequence(...animFncs);
        subject.register(child);
    });
}

For every child of root:

  • Line 4 maps a function that calls each behavior function on child. The resulting array of methods gets bound to the variable animFncs.
  • Line 5 sequences the methods in animFncs and assigns the resulting method to the property child.update. In the call to sequence, the spread operator (…) is used to expand the array animFncs into multiple separate arguments.
  • Line 6 registers the child with subject.

The following program combines three behaviors: block rotation, color cycling, and block ‘uplift’. We’ve already defined animation generators for the first two behaviors, and we’ll do so shortly for uplift. The ziggurat is made of 60 hexagonal blocks that are 0.1 units thick.

An active ziggurat

The GUI controls, which are grouped into folders by behavior, provide the arguments for the animation generators. Each folder includes a checkbox for switching behavior on or off. These checkboxs supply the boolean values for the variables doRotation, doColor, and doUplift in the following code, used to set up the animation whenever the settings are changed:

let animFncs = [];
if (doRotation) animFncs.push(makeArithYRotator(rpsA, rpsB));
if (doColor) animFncs.push(makeColorAnimator(colorRate, saturation));
if (doUplift) animFncs.push(makeUpliftAnimator(ppsA, ppsB, hi));
moveChildren(zig, ...animFncs);

For completeness, we define the uplift behavior, which translates blocks along the vertical y-axis. The blocks’ rate of motion follows an arithmetic sequence: Block i translates at rate a + bi pps. Here pps stands for positions per second. The blocks shuttle up and down between two parallel ‘barrier’ planes defined by y=0 and y=hi where hi > 0. Whenever a block moves past a barrier plane, it gets translated by an equal amount to the legal side of that barrier and its direction is reversed.

function makeUpliftAnimator(ppsA, ppsB, hi) {
    function f(child, i) {
        child.pps = ppsA + ppsB * i;
        child.dir = 1;
        return function(delta) {
            this.position.y += this.dir * this.pps * delta;
            if (this.position.y > hi) { // above upper barrier
                this.position.y = hi - (this.position.y % hi);
                this.dir *= -1;
            }
            if (this.position.y < 0) { // below lower barrier
                this.position.y *= -1;
                this.dir *= -1;
            }
        }
    }
    return f;
}

Reversing a block’s direction when it reaches a barrier ensures that the blocks shuttle back and forth between the two barriers. If we instead leave the blocks’ direction property dir fixed, their direction is determined solely by the signs of ppsA and ppsB. For example, if both arguments are negative, the blocks accumulate at the lower barrier and escape upward only when one or both of the arguments is changed to a positive value. The uplift folder includes a sticky checkbox to toggle this behavior.