We write a program that spins a ziggurat’s blocks around the vertical y-axis. Every block spins at a random rate that is no faster than rps revolutions per second. They rotate in counter-clockwise (CCW) rotation when viewed from above or, if we choose, in either sense of rotation.
One approach is to edit our ziggurat-construction program to include animation. But coupling construction and behavior in this way would require us to rewrite this program whenever we want to define a new animation for the ziggurat. Instead, we take a different approach. The scene graph generated by the (nonrecursive) ziggurat function is shallow. It consists of a root node whose children, corresponding to the blocks, are ordered from the bottom of the ziggurat to the top. We write a function moveChildren
that traverses the list of children and attaches a behavior to each one in turn. Specifically, function moveChildren
gets called with the root node and a function f that returns an update method. For each child, moveChildren
calls f on the current child and attaches the method it returns to the child’s update property. It then registers the child with subject.
function moveChildren(root, f) { let children = root.children; children.forEach(function (child, i, children) { child.update = f(child, i, children); subject.register(child); }); }
For the program we’re after, a suitable function f gets called with a child node, sets its rps property to a random float between 0 and the maximum rate rps, and returns the spinY
function:
function f(child) { child.rps = rps * getRandomFloat(0, 1); return spinY; }
But this allows only for CCW sense of rotation assuming rps is positive, and it isn’t clear where rps gets defined. Better to define a function representing the class of functions providing the behavior we seek. Function makeRandomYRotator
gets called with a maximum rotation rate and a boolean indicating sense of rotation, and returns a function for producing just this behavior.
function makeRandomYRotator(rps, ccw=false) { let lo = ccw ? 0 : -1; let spinY = makeSpin(1); function f(child) { child.rps = rps * getRandomFloat(lo, 1); return spinY; } return f; }
Where we assume the arguments for function ziggurat
are given, the following code produces a ziggurat whose blocks spin at random rate no faster than one CCW revolution every four seconds:
zig = ziggurat(nbrLevels, nbrSides, height, scale); moveChildren(zig, makeRandomYRotator(0.25, true));
Calling moveChildren
on the same ziggurat zig
but with a different function generated by makeRandomYRotator
changes the animation:
// spin no faster than one revolution every 2 seconds in either direction moveChildren(zig, makeRandomYRotator(0.5, false));
Although calling moveChildren
a second time redundantly registers the ziggurat’s blocks with subject
, this does no harm since they’re already registered. The important thing is that it assigns a new behavior to each block.
An interesting question is how we might compose behaviors, such as spinning each block while changing its color. There are a few issues to this. First, how do we coordinate the behaviors? Note that the functions returned by makeSpin
all rely on an object’s rps, so a behavior that combines two such functions on the same object share this same property which might not be what we want. This is easily remedied in this case, but how do we coordinate behaviors generally? How do we ensure that behaviors get performed in a certain order, when order matters as it sometimes does? And what about when the scene graph being animated has a more general structure than is assumed by the moveChildren
function? We’ll look into these things a bit later.