DirectX Media for Animation Programmer's Guide |
![]() Previous |
![]() TOC |
![]() Index |
![]() Next |
Direct Animation supports a set of classes and methods that allow time-varying, interactive, behaviors to be constructed. These behaviors:
In order to support these qualities, Direct Animation for Java introduces two new general data types. These are the Behavior and the DXMEvent data types. This section explains these data types and shows how they are used to construct behaviors.
Independently of Direct Animation, a developer typically thinks of a behavior (here, we are using the term "behavior" quite loosely) as a branching timeline. A simple example is a cube that bounces along some defined path, comes to a halt, waits for someone to press the mouse button, and then flips around and bounces for another three seconds. This intuitive behavioral model is the basis of Direct Animation.
There are two important requirements that a behavior must have. They are:
Unfortunately, these two requirements are somewhat at odds. The second requirement implies the association of a starting time (in real-world time) with a behavior. This contradicts the first requirement of re-use and modularity because, once a behavior is running and has a start time bound to it, it is forever bound to that start time, and cannot be re-instanced at another start time.
These two, somewhat divergent, needs are both reflected in the Direct Animation data type Behavior. The sections below will discuss this data type and demonstrate how these two requirements are met.
From the point of view of behaviors and reactivity, by far the most fundamental data type encountered when programming Direct Animation is the Behavior. Generally, Behavior values are not directly manipulated. Its subclasses, such as NumberBvr or GeometryBvr, are manipulated instead. For example, if an application includes an image behavior, it will manipulate a ImageBvr. Similarly, a color behavior is a ColorBvr. However, from the point of view of how they act as behaviors, the specific type is immaterial. Therefore, we will simply refer to "behaviors" and Behavior. Direct Animation for Java exposes a large number of subclasses of the basic Behavior type that are designed specifically for interactive, animated multimedia. Just like any other Java value, behaviors can be explicitly manipulated in Java, passed to methods, returned from methods, and so on.
Therefore, Direct Animation also exposes a large number of methods on values of these data types. (These are enumerated in the Direct Animation for Java Reference Guide.) These methods allow rich, complex behaviors to be constructed out of simpler behaviors.
Note While designed specifically for interactive, animated multimedia, these data types and operations are also suitable for general media programming.
A common feature throughout the behavior classes is that operations generally take behaviors and produce new behaviors from them, rather than modifying the original inputs. For example, the expression:
cube.transform(xf1)
does not change the specified cube. Instead, it produces a new geometry that is a transform applied to the original cube.
The following example creates a red cube rotated around the y-axis by pi/3 radians (assume that a light has been created somewhere else in the program):
GeometryBvr c0 = importGeometry("cube.x"); GeometryBvr c1 = c0.diffuseColor(red); GeometryBvr c2 = c1.transform(rotate(yVector3, toBvr(Math.PI/3)));
Alternatively, because each of these methods produce a value, they could all be combined into the following statement:
GeometryBvr c2 = importGeometry("cube.x"); diffuseColor(red); transform(rotate(yVector3, toBvr(Math.PI/3)));
It is extremely important to remember that these calls always create new values and never modify the existing values. Thus, the code:
GeometryBvr c0 = importGeometry("cube.x"); //Incorrect Usage c0.diffuseColor(red); c0.transform(rotate(yVector3, toBvr(Math.PI/3)));
will not create a cube, make that cube red, then rotate that cube. Instead, c0 remains the original, imported cube. The second and third lines are useless because they don't do anything with their return values. The results of those operations are inacessible.
There were several reasons for this design decision. If we had decided to modify existing values, we would need to support a general cloning operation and to determine just how deep those clones should be made. Also, when operations return values they can be described by ordinary algebraic notation. (For example, to add two numbers, the expression "z = x + y", is much more familiar than "reg.init(x); reg.add(y); reg.assign(z)." ) It is often useful to describe media behaviors in algebraic terms.
Internally to the implementation, new values may or may not actually be constructed. However, from the developer's point of view, they always are. Additionally, when new values are constructed, they are very inexpensive, in terms of system resources. Generally, they are represented by a pointer to the old value and the data for the new attribute.
The example of the rotated cube showed how various methods (for example, importGeometry, diffuseColor, and transform) are used for creating behaviors. However, these behaviors were all constant. This is a degenerate (although very common and useful) form of behaviors. In fact, all behaviors are potentially time-varying and interactive. For example, changing toBvr(Math.PI/3) in the above example to localTime causes the red cube to spin around the y-axis at a rate of one radian per second. Here is the code:
GeometryBvr c2 = importGeometry("cube.x"); diffuseColor(red); transform(rotate(yVector3, localTime));
The behavior value localTime allows animation to be injected into behaviors. It is a number-valued behavior (NumberBvr) that starts at time 0 (we will define "starting" more precisely below), and increases at the rate of one unit per second. It can be used as an argument to any behavior-constructing method that takes a number-valued behavior as an argument to construct a time-varying behavior. For example, the statements:
NumberBvr sawtooth = mod(localTime, toBvr(1)); ColorBvr myCol = colorRgb(sawtooth, toBvr(0), toBvr(0));
first creates a number behavior that goes from 0 to 1, 0 to 1, 0 to 1, and so on. (It does this by taking the modulus of the ever-increasing localTime value and 1.) It then uses this value to create a time-varying color whose red component is that number behavior.
Animations constructed using localTime are impervious to fluctuations in the frame rate of the Direct Animation implementation. This means that the sawtooth behavior will go from 0 to 1 in a period of 1 second, no matter how many frames are actually generated. This is a very important abstraction mechanism provided by Direct Animation. It allows applications to be constructed independently of the hardware on which they will ultimately run. More powerful hardware and optimized Direct Animation implementations will produce higher-quality renderings and higher frame-rates, but the simulation being described will progress equivalently.
Furthermore, there are other forms of continuous input, such as mousePosition, which add more interactivity to behaviors. The mousePosition behavior, for example, provides the continuous 2D world-coordinate position of the mouse. It can be used as an argument to a translation method that requires a 2D vector to create, for example, a transformation that follows the mouse.
Behaviors can be thought of as very rich cells in a spreadsheet such as Excel. Spreadsheet cells can be referenced by other cells and can be used to build more complex cells. Changes to a cell propagate to all cells that use it. Similarly, in Direct Animation, a behavior X can refer to other behaviors, for example, Y and Z. As Y and Z change, so does X and all other behaviors that are built from Y and Z. However, unlike spreadsheet cells, these behaviors change as events occur and as time progresses. They also are richer than spreadsheet cells in that a much greater variety of data types is available.
The Switchable construct, described below, provides a programmatic means to procedurally change a started behavior. It is similar in spirit to changing the value of a spreadsheet cell by typing in a new value. All the behavior connectivity remains. Only the specific value changes.
Previous examples used the toBvr method, which takes a double precision Java number and makes it a constant NumberBvr behavior. This is necessary because the Direct Animation for Java methods generally take NumberBvr behaviors and not Java constant numbers. The toBvr method exists for all core Java types that have Direct Animation equivalents. For example, it converts boolean types and java.lang.String types to BooleanBvr and StringBvr respectively.
The only time that toBvr is used is when converting a Java number, boolean, or String to a Direct Animation type. It is not used for constants such as red, yVector3, and origin2, because these are defined as constant behaviors.
The behaviors constructed above all vary with time but do not change as the result of discrete events. However, being able to evolve as a result of events (either internal or triggered bysomeone) is of paramount importance for truly useful and general behaviors in interactive animations.
We will discuss the notion of an event in greater detail below, but for now, we will consider it to be a value representing an event that will occur at some time. The general type for events is DXMEvent.
This section discusses the until method, which allows developers to incorporate interactive elements into their animations. Here is an example of how to create a behavior that is red until the left mouse button is pressed, and then turns to blue:
ColorBvr c = (ColorBvr)until(red, leftButtonDown, blue);
In general, until takes a behavior, an event, and another behavior. It produces a new behavior. This new behavior is the first behavior until the event occurs. It then becomes the second behavior. Because until takes behaviors and returns a behavior, it can be nested, as the following example demonstrates:
ColorBvr c = (ColorBvr)until(red, leftButtonDown, until(blue, leftButtonDown, green));
In this example, the resulting behavior is red until the button is pressed. It is then blue until the button is pressed again, and then it is green.
Note that we cast the result to the proper subclass of Behavior, because until is defined on the Behavior superclass.
The above example could have been written as:
ColorBvr c1 = (ColorBvr)until(blue, leftButtonDown, green); ColorBvr c2 = (ColorBvr)until(red, leftButtonDown, c1);
Looking at the example in this form raises the following question: "When the leftButtonDown event occurs, c2 changes from red to c1, but c1 doesn't change from blue to green. Why not? Both of them appear to be waiting for the leftButtonDown event, so why doesn't the first occurrence of this event cause them both to change?"
The answer to this question is exactly what motivates the existence of another form of behavior called the running behavior. Running behaviors are represented through the Behavior type and its subclasses.
Earlier, we discussed the two properties that behaviors must have. The first is that they be modular and reusable in multiple contexts. The second is that they be able to be running, which means they can be started at a particular point in time. The descriptions of all of the behaviors thus far have been modular – that is, they were not tied to any particular starting time.
To run a behavior, both its local timeline and when it starts looking for events must be tied to a real world starting time. To provide it, the Behavior class includes a run method that takes a real-world time (called the start time) at which to start a behavior, and produces a new Behavior. We refer to the invocation of this method as "running a behavior," or "starting a behavior." Multiple calls to run on the same behavior will produce different running behaviors, each with a potentially different start time. Again, like the media operations, run does not change the behavior it is being applied to, but creates a new behavior instead. Also, calling run on any already running behavior results in that original behavior. The newly provided start time is ignored. Thus, running an already running behavior has no effect.
Clearly, there need to be restrictions on the start times that can be provided. For instance, if the start time is one year in the past, we certainly can't react to mouse button events that happened five months ago. Providing start times that are too far in the past or future will produce undefined results. The appropriate time is generally the event time that caused the event that is responsible for the application invoking run. This will be discussed in more detail below.
Analogies to running and non-running behaviors are actual computer programs. A non-running behavior is analogous to a program. The program itself is not running. A running behavior is akin to an invocation of that program, or a process. This analogy can also help with the intuition of why, when you invoke run on a behavior, you get a new behavior rather than changing the behavior to which it is being applied. Similarly, when you start a program, the program itself doesn't change. Instead, you get a new process that represents an instance of that program.
Often, a Direct Animation for Java program has no need to explicitly invoke run. It is invoked automatically by the system when it needs to run a behavior. However, there are occasions when explicitly running a behavior is important (more examples will be provided below) and, in any case, it is important to understand run in order to understand how behaviors and events really work.
The reason the above example behaves the way we want it to is because of the way until works. Consider the following example:
Behavior b1 = until(b2, ev, b3);
Assume that b1 is run with a start time of t0. This causes b2 to be started at t0 and the system to start looking for the first occurrence of the event ev after t0. It does not run b3. When the event does occur (we will call this time te), the system starts behavior b3 at time te (with local time starting at 0), and stops looking for event ev.
The chart below displays the timelines involved in this example. Global time is ever-increasing, the timeline for b1 and b2 start at global time t0 and local time 0. Two seconds later (te), ev occurs, and the timeline for b3 begins at local time 0. Note that b2 continues on, even though it is not accessed anymore. (The implementation cleans up unreferenced behaviors automatically.)
Now, reconsider our original example:
ColorBvr c1 = (ColorBvr)until(blue, leftButtonDown, green); ColorBvr c2 = (ColorBvr)until(red, leftButtonDown, c1);
When c2 is started, we look for the leftButtonDown event in c2, but not in c1. When we switch to c1, we look for the leftButtonDown event in c1.
Advanced Topic: When Does until Change Behaviors?
This subsection discusses a subtlety present in the Direct Animation event model.
Consider the statement:
until(b1, ev, b2);
If ev occurs at time te, b2 will not be sampled until the first sample time strictly greater than te. If this rule were not followed, many cyclically defined behaviors would infinitely recurse.
Consider this example:
until(red, predicate(gte(localTime, toBvr(2))), green);
If this behavior is sampled at local time 1.8, the result will be red. At local time 2.1, the predicate will hold true, but the Direct Animation implementation assumes that the event time is 2.1 (because this is the time the implementation became aware of the event). Because of the "strictly greater than" rule described above, the result will still be red. Not until the next sampling, for example at local time 2.4, will the result be green.
If the application wants to ensure that situations like this react more accurately, it can use the timer method, and rewrite the example as:
until(red, timer(toBvr(2)), green)
The timer method will be discussed in more detail later.
The until method and the localTime behavior interact in a very useful way. Recall that localTime is a behavior that, when run, starts at 0 and increases at the rate of 1 unit per second. Because until runs its component behaviors at different times, the use of localTime within an until method allows for the creation of distinct local timelines. For example:
NumberBvr sine = sin(localTime); NumberBvr slope = localTime; NumberBvr sineSlope = (NumberBvr)until(sine, predicate(gte(localTime, toBvr(5))), slope);
The sineSlope behavior behaves like the sine behavior until just after time 5 (in its local time line). It then behaves like the slope behavior. The following is a chart of a running behavior of sineSlope graphed against its local time line.
More generally, this means that any instances of localTime in a behavior will, when that behavior is running, start at 0, including when it is transitioned to as the result of an event.
This local timeline property of localTime is quite important for modularity because it allows behaviors to be fashioned that operate in their own local timelines. When placed in any until method, started at any time, they will follow their prescribed behavior.
We will now discuss the untilNotify method. If we examine these now-familiar examples:
ColorBvr c1 = (ColorBvr)until(blue, leftButtonDown, green);
and
NumberBvr n = (NumberBvr)until(localTime, leftButtonDown, localTime);
we see that this form doesn't describe every situation that might arise when designing a reactive behavior. Specifically, this form only works when component behaviors can be completely described at the exact time that the containing behavior is being described. In the example, this is the case. We know that we initially want the color green, and, once the button is pressed, we want the value of localTime.
However, consider a number behavior representing a timer that starts counting from 0 and increases 1 unit per second until the left button is pressed. At that time, the behavior is permanently set to the value that existed when the button was pressed. In more direct terms, consider a stopwatch. If we try to create a stopwatch using until, we encounter the following problem:
NumberBvr stopWatchWrong = (NumberBvr)until(localTime, leftButtonDown, ????);
What should "????" be? There is no way for us to describe it at the time we are constructing stopWatchWrong because we have no idea when the button is going to be pressed. What is required is some sort of delaying construct that allows this mystery behavior to be constructed when the left button occurs. In Java, object-oriented callbacks are used as the delaying construct.
Note In general, we need a closure here, and in Java, closures take the form of object-oriented callbacks, which are methods on interfaces.
The untilNotify method solves these types of problems. It uses a callback to delay constructing a behavior until the appropriate time. The untilNotify method takes a behavior b, an event ev, and a Java object j that implements the callback using the UntilNotifier Java interface. The untilNotify method returns a new behavior nb. When nb starts running, its value is b. When the event ev occurs, the system invokes the notify method of j, which returns a behavior c to the system. The system then starts c running, and the value of nb becomes this running instance of c. So, to implement our stopwatch example:
class StopWatch implements UntilNotifier { StopWatch() { _watch = (NumberBvr)untilNotify(localTime, leftButtonDown, this); } Behavior notify(Object eventData, Behavior previous. double eventTime, BehaviorCtx bc){ Behavior stoppedTime = previous.snapshot(eventTime, bc); return stoppedTime; } public NumberBvr _watch; }
Here are a few points about this example:
This section lists the ways behaviors are run, and how the start time that becomes associated with the running behavior is determined. The different ways of running behaviors are:
DXMEvents come from a number of sources. They are:
In addition to the above distinctions between events, there is an additional distinction. Some events may be "impulse" events, while some transition back and forth between an On and Off state. For instance, leftButtonDown is an impulse event. It only triggers when the mouse button goes from not being pressed to being pressed. Thus, if the mouse is already down when until(red, leftButtonDown, blue) is started, then the leftButtonDown event will not be triggered. However, predicate is an on/off event, following the state of the boolean on which it is based. Thus, the situation would respond differently if the behavior were until(red, predicate(leftButtonState), blue).
Certain operations that make sense for on/off events don't make sense for impulse events. Examples are andEvent on two impulse events (this will never trigger) and notEvent.
It is often necessary to know more than that an event triggered. This is why some events also produce data. For instance, a pick event provides information about the location of the intersection. To give applications the ability to access this data, Direct Animation for Java calls the notify method of an UntilNotifier with an Object. The event data appropriate to the event that occurred is embedded into that Object. It is the responsibility of the application to cast this Object to the type that it really belongs to in order to retrieve the necessary data. (Note that, in Java, this is a type-safe cast. This means it will produce a runtime error if the object isn't really of the runtime type to which it is is being cast.)
Direct Animation supports the explicit specification of timer events, based upon localtime. For example, the following expression causes a behavior to switch from red to blue 2 seconds after it starts:
until(red, timer(toBvr(2)), blue)
The value given to timer can be any number-valued behavior.
In theory, the timer function for constructing events is not necessary because timer(n) should be equivalent to predicate(gte(localTime, n)). However, due to the implementation of Direct Animation, they are not truly equivalent. This is explained in "Advanced Topic: Event Detection."
Applications often receive events through means other than Direct Animation for Java. Examples include GUI elements and incoming network data. Frequently, these events trigger responses in reactive behaviors. One convenient way of doing this is the "application-triggered event." An application-triggered event is a subclass of the DXMEvent type, and supports an additional trigger method. These events can be constructed via new, placed into reactive behaviors, and triggered at any time by the application. For instance, the following example turns a cube from red to blue upon some external application event:
GeometryBvr cube = importGeometry("cube.wrl"); AppTriggeredEvent appEvent = new AppTriggeredEvent(); ColorBvr col = (ColorBvr)until(red, appEvent, blue); GeometryBvr coloredCube = cube.diffuseColor(col); //... elsewhere, when the application receives the proper event... appEvent.trigger(); //... now coloredCube will turn blue (assuming it's running)
There is also another version of the trigger method that allows event data to be carried with the generated event.
Note that the trigger method is an immediate method, and, in contrast to run and snapshot, doesn't take a time argument. The trigger occurs when the program executes the statement containing the trigger call.
Events can be combined in a variety of simple ways. The method andEvent takes two events and creates a third that occurs only when both constituent events occur simultaneously. The event resulting from orEvent occurs when either of the constituent events occur. And the event resulting from notEvent occurs whenever the constituent event does not occur.
The andEvent method always returns a pair as event data, with each element being the event data from the constituent events. The orEvent method produces the event data of whichever event caused orEvent to trigger. The event data from notEvent is undefined because an event not occuring doesn't produce any information.
DXMEvents support an attachData method that takes an arbitrary object and produces a new event. The new event occurs at the same time as the original event, but its data is now that data that has been specified in the call to attachData. This allows an application to associate arbitrary client data with an event and know that it will be delivered to the notifier when the event occurs.
This method can be used in conjunction with untilNotify and orEvent to allow a behavior to switch to one of several new behaviors, depending on which event occurred. For instance, if we want a behavior that is red until the left mouse button is pressed (in which case it turns green) or the right mouse button is pressed (in which case it turns yellow), we could do the following:
DXMEvent greenLeft = leftButtonDown.attachData(green); DXMEvent yellowRight = rightButtonDown.attachData(yellow); ColorBvr myBvr = (ColorBvr)untilNotify(red, orEvent(greenLeft, yellowRight), notifier);
The notify method would then cast the event data to a Behavior and return it, causing the behavior to become either green or yellow, depending on which button was pressed.
Because attachData can take any object, the application can associate any type of data with events, rather than only behaviors.
The earlier discussion about the predicate method, which creates a DXMEvent from a BooleanBvr, might have given the impression that this event is fired anytime the event occurs. In general, this is not possible, due to the possibilities of temporal aliasing. Therefore, Direct Animation places implementation-specific restrictions on the form of BooleanBvr that will successfully and consistently trigger events. For example, the DXMEvent denoted by predicate(eq(sin(localTime), toBvr(0))) is expected to fire exactly when sin(globalTime) is 0. However, in reality, this event may rarely, if ever, fire. This is because of the sample-driven nature of detecting predicate events. If the implementation doesn't sample at exactly the right time, the event will be missed. Thus, applications should use inequality events, such as predicate(gte(sin(localTime), toBvr(0))) to test for events on continuous behaviors. It is this imprecision in sampling events that is the motivation for the specialized timer event.
There is a subtlety associated with imprecise sampling. If a notifier is invoked using untilNotify waiting for the above event, the event time will not generally be the exact time that the event became true. If precise timing is a requirement, the event time should not be used directly. Instead, the inaccuracy must be compensated for in some application-specific manner.
It is often necessary for an application to determine the instantaneous value of a running behavior at a particular time. For instance, when a collision occurs, an application might need to determine what the instantaneous value of the velocity is at that time in order to reverse and dampen it. Similarly, when a user moves an object with the mouse, and then releases the object, the application typically needs to know the object's position at the time of the release.
It is for these reasons that every running behavior supports a snapshot method. The snapshot method takes a time value and returns a constant behavior that holds the value of that started behavior at the specified simulation time. Snapshot also requires a behavior context to be passed in. This provides a place for state to be maintained on behalf of the snapshot, and should generally be the context that was passed into the notifier.
Note As with run, there are restrictions on the time that can be supplied to snapshot. For purely synthetic behaviors, such as sin(cos(localTime)), any time is valid. However, for reactive behaviors, the currently running behavior is used, and, for input device behaviors, such as mousePosition, there will not be infinite buffering.
It is illegal to call snapshot on a behavior that is not running. An exception is raised if this happens. This is because it is impossible to ask a behavior for its value at a certain time when the behavior has not been started. Using our earlier analogy, this is similar to asking a program to dump its memory state before the program has started.
The snapshot method on an NumberBvr returns a new NumberBvr that is guaranteed to be constant. However, for types that have primitive Java equivalents (NumberBvr, CharBvr, StringBvr, BooleanBvr) a value in the underlying type is often needed. For instance, you might need an actual float in order to call out to an arbitrary Java routine. The extract method exists for these classes of behaviors, takes no arguments, and is expected to be called on a behavior that is actually constant. Calling it on a non-constant behavior results in a run-time error.
The cond construct allows the construction of a behavior out of a boolean behavior and two other behaviors. The value of the resultant behavior at any point in time is equal to the value of one of the two other behaviors. Which of the values chosen is determined by the value of the boolean behavior. Algebraically, this looks like:
cond(bool, trueBranch, falseBranch)(t) == if bool(t) then trueBranch(t) else falseBranch(t)
In other words, this is a "behavior-level" conditional.
The following example creates a behavior, y, which is either another behavior, x, or 1.0 if x is greater than 1.0:
NumberBvr y = (NumberBvr)cond(gte(x, toBvr(1.0)), toBvr(1.0), x);
The methods for responding to discrete events, until and untilNotify, have something in common. They both occur completely within the confines of the Direct Animation model. Events are described or imported into Direct Animation for Java. Changes to behavior values occur within the Direct Animation sampling and event detection processes. These restrictions are often quite tolerable, and account for many of the situations that are likely to occur.
However, there is still a need for a more asynchronous form of modifying behaviors that is less dependent on the control flow of the Direct Animation data structures.The Switcher object supplies this need. Switcher is a Java subclass of Behavior that is instanced with the new operator, is given an initial behavior, and results in a new behavior for the caller to use. It supports a switchTo method, which, when invoked with another behavior, switches to the provided behavior. Here is an example:
// Create a solid color image with a switchable color. The other // arguments are explained further below. Switcher sw = new Switcher(red, ctx); ColorBvr col = (ColorBvr)(sw.getBvr()); ImageBvr im = solidColorImage(col); //... somewhere else in the program ... sw.switchTo(blue);
Now, anywhere that col was used will turn from red to blue. In this example, the switcher contains an untyped Behavior, which the example explicitly casts to a ColorBvr. This is because ColorBvr is the type that was provided as the input to the construction of the switcher. Calls to switchTo must carry arguments of the same runtime behavior type as the one with which the switcher was constructed or runtime exceptions are generated. Finally, the behavior being switched to is started when the switch occurs.
Note also that, like trigger, the switchTo method is an immediate method, and therefore doesn't take a time argument (in contrast to run and snapshot). The switch occurs when the program executes the statement containing the switchTo call.
Atomically Invoking a Series of switchTo Invocations
If, at a given point, an application must perform more than one switchTo invocation, it may run into the problem of a new frame being generated before all the switches have executed. To prevent this, the application can use the Java synchronized keyword on the viewer object to stop the viewer from ticking before everything in the synchronized block executes.
Often, there is a need to construct a behavior that, when an event occurs, cycles through some number of values. The Cycler object solves this problem. Like Switcher, it is also a subclass of Behavior. Here is an example that cycles through the colors red, green, blue, yellow, red, green, and so on, upon each leftButtonDown event:
Behavior behaviors[4] = { red, green, blue, yellow } Cycler cyl = new Cycler(behaviors, leftButtonDown); ColorBvr col = (ColorBvr)(cyl.getBvr());
Note that each component behavior gets started each time there is a transition to it.
The Appendix describes how Cycler is implemented.
A Cycler Subtlety
The behavior returned from getBvr doesn't itself respond to the provided event. That behavior needs to be running before the events have any effect.
There is often a need to reference a behavior before it has been defined. As an example, consider a colored rectangle whose color changes when picked. In this case, the color depends on whether or not the image is picked, but the image is dependent upon the color. This sort of cyclic dependency cannot be easily expressed with the mechanisms we have already discussed. It is for these sorts of situations that we provide an uninitialized behavior. This behavior allows programmers to create a behavior, use it in the definition of other behaviors, but not actually define its contents until some later point. Here is a use of uninitialized behaviors to construct the scenario mentioned above:
ColorBvr col = ColorBvr.newUninitBvr(); ImageBvr im = makeColoredRectangle(col); PickableImageBvr pim = new PickableImageBvr(im); DXMEvent ev = pim.getPickEvent(); col.init( until(red, ev, green) );
This example simply changes the color from red to green the first time the rectangle is passed over by the mouse. Here we initialize the behavior to the color dependent upon the event once we have the event.
If we change the last line to the following:
col.init ( until(red, ev, until(green, notEvent(ev), col) );
the rectangle will be be red whenever we're not over it, and green when we are over it. We do so by making the "col" behavior itself cyclic, going back to red and waiting for the pick after the notEvent(ev) occurs.
When using this feature, one should be aware that the system will generate a run-time error upon attempts to:
A common idiom encountered in constructing behaviors is to loop through some sequence of other behaviors over time. Examples include flipping through pre-rendered images or a list of colors. Direct Animation supports this through the ArrayBvr type. The following code constructs an ArrayBvr out of a Java array, and uses the nth method with a time-varying parameter to cycle through that list.
ColorBvr[] arr = { red, green, blue, yellow, green, cyan, magenta }; ArrayBvr arrBvr = new ArrayBvr(arr); // build an indexer to go from 0 to length &en; 1, then back to 0, etc. // going at a rate of one unit per second. NumberBvr indexer = mod(localTime, toBvr(arr.length &en; 1)); // Use this to index into the ArrayBvr. ColorBvr cyclingCol = (ColorBvr)arrBvr.nth(indexer);
Note that while nth takes a NumberBvr, it uses the greatest integer value less than the number's value to determine the index. The array's index starts at base 0 and any attempt to index beyond its length generates a runtime error.
The previous sections have assumed that the localTime behavior starts at 0 and increases at the rate of one unit per second. This is the default condition, but it can be modified. Direct Animation supports the notion of time substitution. Time substitution creates a new behavior from an existing behavior and a number behavior. In the new behavior, the number behavior replaces all occurrences of localTime that were in the original behavior. This includes behaviors where localTime is implicit, such as imported movies. Even though localTime isn't explicitly used in the code, it can still be substituted with a new time. Time substitution allows behaviors to be time-scaled so that they can, for example, run faster or slower, be time-shifted to start at a different time, or frozen at a particular point in time.
The substituteTime method takes the following form:
NumberBvr timeXf = ...; Behavior newBvr = origBvr. substituteTime(timeXf);
The value of newBvr at time t is found by taking the value of timeXf at time t, and using that time to evaluate the behavior bv.
Here are some simple examples:
//Create an original behavior //A point moving one unit in x per second, starting at 0 origBvr = point2XY(localTime, toBvr(0)); //Create a new behavior moving 0.5 units per second //Do this by replacing localTime with localTime/2 b1 = origBvr.substituteTime(div(localTime, toBvr(2))); //Create a new behavior moving 1 unit per second, starting at 33 //Do this by replacing localTime with localTime + 33 b2 = origBvr.substituteTime(add(localTime, toBvr(33))); //Create new behavior moving 2 units per second, starting at time 33 //Do this by replacing localTime with (localTime * 2) + 33 b3 = origBvr.substituteTime(add(mul(localTime, toBvr(2))), toBvr(33)); //Create new behavior by freezing the original behavior at time 77 b4 = origBvr.substituteTime(toBvr(77)); //Tie the new behavior to the x-component of the mouse b5 = origBvr.substituteTime(mousePosition.getX());
Also note that time substitutions are cumulative. For example, the following series:
c0 = point2XY(localTime, toBvr(0))); c1 = c0.substituteTime(add(localTime, toBvr(33)); c2 = c1.substituteTime(mul(localTime, 2), toBvr(2)));
Could also be expressed as:
c0.substituteTime(add(mul(localTime, 2), toBvr(33))));
Here is another example that shows how to make images of sailboats rock on the water. We first create a simple number behavior:
NumberBvr angle = mul(sin(localTime), toBvr(Math.PI/6));
This behavior begins (at local time zero) as the value zero and then varies with time between +/-p/6 radians (that is, +/-30 degrees). It repeats the behavior every 2 seconds. Assume that we have already constructed (or imported) a behavior representing the geometry of a sail boat, sailBoat0, that is centered at the origin with its long axis aligned along Z. We can use the behavior angle to rock the boat:
Transform3Bvr heel1 = rotate(zVector3, angle); GeometryBvr sailBoat1 = sailboat0.transform(heel1);
The boat is initially upright and then heels from one side to the other, passing through upright (sin(0)=0) approximately every second and a half.
Now, let's create a boat that is rocking more slowly than sailboat1:
Transform3Bvr heel2 = (Transform3Bvr)heel1.substituteTime(div(localTime, toBvr(8))); GeometryBvr sailBoat2 = sailBoat0.transform(heel2);
The second sailboat, sailBoat2, rotates the same amount as sailBoat1 but has a period that is 8 times longer (y = sin1/8x).
Note that, even though the period of rocking is different for each boat, both are initially upright. We will now add a third boat that rocks at the same period as sailBoat2, but is 90 degrees out of phase with it (y = cos 1/8x). We can define this as:
GeometryBvr sailBoat3 = (GeometryBvr) sailBoat2.substituteTime(add(localTime, toBvr(Math.PI/2)));
This boat begins heeled over by p/6 (cos(0)=1) and rocks at the same rate as sailBoat2. Notice how using time substitutions contributes to the modularity of the code. The phase change could be achieved by changing the definition of angle, but this would have also changed the phase of the first two boats as well. The code for angle could have been duplicated and the phase changed, but if angle had a more complicated definition or if the source were not available, this approach would be difficult or impossible.
Time substitutions provide the same modularity benefits in the temporal domain that 2D and 3D transformations provide in the spatial domain. They allow objects to be defined, and then manipulated, from outside to alter their behavior, without needing to know the internals of the objects.
Naturally, there are certain restrictions on the time substitutions that can be applied to behaviors. These are somewhat similar to the restrictions on snapshot. For instance, user-input in general may not be time substituted, and time-substitutiontion will not take one back before event transitions that have already occurred.
Some applications must explicitly run behaviors instead of letting the system do it. For example, consider a scenario where we want to display the number 0 until the user presses the left-mouse button, and then we want to display an increasing clock whose initial value is the amount of time that has elapsed since the behavior started. Think of it as starting a stopwatch, but not looking at that stop watch until the left button is pressed.
An initial attempt at this might yield:
Incorrect Usage
void createModel(double startTime, BehaviorCtx bc) { NumberBvr runningClock = localTime; NumberBvr wrongResult = until(toBvr(0), leftButtonDown, runningClock); ... }
Unfortunately, this doesn't work. That is because the runningClock parameter doesn't get started until after the event, which is not what we want. Thus, we need to call run explicitly to start the clock running:
void createModel(double startTime, BehaviorCtx bc) { NumberBvr runningClock = (NumberBvr)(localTime.run(startTime, bc)); NumberBvr rightResult = until(toBvr(0), leftButtonDown, runningClock); ... }
Here, we explicitly invoke run on localTime to start it at the startTime. Note that run returns a Behavior, so we need to explicitly cast it to the NumberBvr that we know it is.
Now, when until transitions to this, it has already been started.
Certain scenarios call for the application to be able to reference a running behavior once it starts running, but don't require the application to explicitly start that behavior. Consider a scenario where there are two animated, time-varying images (perhaps movies). We want to play the first one, from its beginning, for 10 seconds, then, over the course of the next two seconds, fade from it to the second movie, then continue playing the second movie. Assuming the existence of a fade operation (which can be easily constructed from overlay and opacity), a first attempt might look like this:
Incorrect Usage
ImageBvr wrongResult = (ImageBvr)until(movie1, timer(toBvr(10)), until(fade(div(localTime, toBvr(2)), movie1, movie2), timer(toBvr(2)), movie2);
Unfortunately, this doesn't work. The problem is that, after the first event, the movie1 behavior will be started again, and we will see seconds 1 and 2 of movie1 fading, rather than seconds 11 and 12. Also, after the second event, the movie2 behavior also starts again, replaying seconds 1 and 2, rather than continuing on from the end of 2 seconds.
It is possible to construct the correct version out of a somewhat complex combination of run, untilNotify, and UntilNotifier objects. However, there is an easier way.
This particular scenario calls for a behavior that, once running, is always referred to by the subsequent invocations of run on this behavior. This means that, after the initial run, no new behaviors are created. Another requirement is that, when we first construct this behavior, run isn't invoked automatically. The runOnce() method satisfies both these requirements. With runOnce(), the timeline fader example can be expressed as:
ImageBvr movie1Once = (ImageBvr)movie1.runOnce(); ImageBvr movie2Once = (ImageBvr)movie2.runOnce(); ImageBvr faderMovie = (ImageBvr) until(movie1Once, timer(toBvr(10)), until(fade(div(localTime, toBvr(2)), movie1Once, movie2Once), timer(toBvr(2)), movie2Once);
In this example, movie1Once and movie2Once are constructed as behaviors that are not yet running, but once they are asked to run(), subsequent invocations of run() on these behaviors will result in the running behavior generated by the first run. These semantics meet the need of our fading movie example.
While not replacing all situations in which one would need to combine run() and untilNotify() in complex ways, runOnce() does make certain scenarios significantly simpler to express.
Sometimes an application wants to cause an external action when an event occurs in Direct Animation for Java. For instance, the application may want to display a GUI window when the mouse moves past a certain region of the window. This capability is supported by the registerCallback() method available on events.The registerCallback() method takes an object that implements the EventCallbackObject interface (along with some other parameters), and returns an Object that can later be used to unregister that callback. Whenever that event occurs, the invoke method of the callback is called with the event data produced by that event.
Here is an example:
class MyCallback implements EventCallbackObject { MyCallback(...); public void invoke(Object eventData) { ... pop up my GUI window ... } } //... elsewhere ... ev = predicate(gte(mousePosition.getX(), toBvr(0.05))); Object cookie = ev.registerCallback(new MyCallback(...), startTime, bvrCtx, false); // not a one-shot event // ... from this point, every occurrence of ev will trigger "invoke" ... // at some later point, we unregister the callback unregisterCallback(cookie);
Registering callbacks can be thought of as the complement to AppTriggeredEvent. The former causes an application action to occur upon a Direct Animation event, while the latter is an application action triggering a Direct Animation event.
This section discusses ways of using SoundBvrs. First, consider this familiar example:
until(red, leftButtonDown, blue);
The semantics of this statement are clear. The behavior is red until the left mouse button is pressed. It then turns blue. Now consider this seemingly parallel example:
until(sound1, leftButtonDown, sound2);
In this case, we have sound1 until the left mouse button is pressed, and then we have sound2. But this is, in fact, an incomplete description. What part of sound2 do we hear? This differs from the color example because, in that case, we know exactly what the values are at different times. However, in the sound example, we haven't said what happens for sounds as time progresses. The remainder of this section discusses this problem.
All sounds start at local time 0, and are silent after they have finished (unless they are looped, in which case they don't finish at all). Because until() starts behaviors after the event occurs, we will hear however much of sound1 is played before the button is pressed, and then we will hear sound2.
This is appropriate if you want to start the second sound exactly when the mouse button is pressed. However, if you want to switch to a sound that is already in progress, you must explicitly run it, just as we've discussed in previous examples. An example application might be if sound1 and sound2 are simultaneous tracks of a movie soundtrack in different languages, and you want leftButtonDown to switch between them. The following code fragment constructs this example:
SoundBvr playingSound2 = sound2.run(eventTime, bvrCtx); SoundBvr result = until(sound1, leftButtonDown, playingSound2);
Direct Animation supports explicit construction of the integrals and derivatives of certain types with respect to time. Integration can be performed on values of type NumberBvr, Vector2Bvr, and Vector3Bvr. Derivatives can be taken on these types and also, on Point2Bvr and Point3Bvr. (This creates the corresponding Vector behavior).
When an integral behavior is run(), it starts building a conceptually continuous summation of values from that start time.
For example, , the following code creates a spinning cube that stops and starts on every left mouse button click. When it stops, it stops in place, and when it begins again, it begins from where it left off:
NumberBvr[] zeroAndOne = { toBvr(0), toBvr(1) }; Cycler cyc = new Cycler(zeroAndOne, leftButtonDown); NumberBvr angularVelocity = (NumberBvr)(cyc.getBvr()); NumberBvr angle = integral(angularVelocity); GeometryBvr spinningGeo = geo.transform(rotate(myAxis, angle));
This creates a velocity that cycles between 0 and 1, and an angle that, because it is the integral of that velocity, increases gradually. This angle is then used as the angle by which to rotate the geometry. When the velocity is 1, it rotates at 1 radian per second and, when 0, it rotates at 0 radians per second.
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.