Conal is a member of the Microsoft Research Graphics Group. He can be contacted at [email protected].
Sidebar: Models versus Presentations
There's no question that computer graphics -- especially interactive graphics -- is an incredibly expressive medium with potential beyond imagination. However, few people are able to create interactive graphics, so what might be a widely shared medium of communication is instead a tool for specialists. The problem is that authors still have to worry about how to get a computer to present content, rather than focus on the nature of the content itself. For instance, behaviors such as motion and growth are generally gradual, continuous phenomena; moreover, many such behaviors go on simultaneously. Computers cannot directly accommodate either of these basic properties, because they do their work in discrete steps rather than continuously, and they only do one thing at a time. Graphics programmers consequently have to bridge the gap between what an animation is and how to present it on a computer.
If the kind of programming in use today (like that described in the accompanying text box "Models versus Presentations" on page 25) is unsuitable for most potential authors, then we need to move toward a different form of programming. Alternative forms must give authors freedom of expression to say what an animation is, while invisibly handling details of discrete, sequential presentation. In other words, these forms must be declarative ("what to be"), rather than imperative ("how to do").
In this article, I present one such approach to declarative programming of interactive content. Fran (short for "functional reactive animation") is a high-level vocabulary that lets you describe the essential nature of an animated model, while omitting details of presentation. And because this vocabulary is embedded in a modern functional programming language (Haskell), the animation models are reusable and composable in powerful ways.
Fran is freely available (with source code) as part of the Hugs implementation of Haskell for Windows 95/NT (http://www .haskell.org/hugs/). Newer versions of Fran may be found at http://www.research.microsoft.com/~conal/Fran/. The underlying ideas form the basis of Microsoft's DirectAnimation, a COM-based programming interface accessible through conventional languages like Java, Visual Basic, JavaScript, VBScript, and C++. DirectAnimation is built into Internet Explorer 4.0, so you may already have it.
There are three ways you can experience this article:
- In this printed version, examples have an accompanying sequence of snapshots. By scanning them from left to right, top to bottom (first row, second row, and so on), you'll get a sense of motion.
- On the Web (http://www.research.microsoft.com/~conal/Fran/ tutorial.htm), examples are illustrated by animated GIFs, showing animation over time, but not interactivity. That version of this article also contains additional discussion and several animations not in the printed version.
- Finally, you can run the examples and interact with or modify them. After installing Hugs (available at http://www .haskell.org/hugs/), double-click on the file tutorial.hs in the subdirectory lib\Fran\demos. At the > prompt, type "main" and press Enter. The examples will begin running. Press Spacebar, "n," or right arrow to advance to the next animation, and "p" or left arrow for the previous one. If you want to display just a single animation (leftRightCharlotte, for instance), then close the animation window and enter "display leftRightCharlotte". You can alter the definition in an editor, save the result, enter ":r" to the Hugs prompt, and "$$" again to display the new version. For 2D examples having a user argument u, use displayU instead of display. Similarly, for 3D examples, use displayG if there is no user argument, and displayGU if there is a user argument.
The First Example
I'll start with the animation in Figure 1 called leftRightCharlotte, which moves Charlotte from side to side. Listing One efines a value called leftRightCharlotte to be the result of applying moveXY to three arguments. (In most other programming languages, you would instead say something like "moveXY(wiggle,0,charlotte)".)
Listing OneleftRightCharlotte = moveXY wiggle 0 charlotte charlotte = importBitmap "../Media/charlotte.bmp" |
The function moveXY takes x and y values and an image, and produces an image moved horizontally by x and vertically by y. All values may be animated. In this example, the x value is given by wiggle, a predefined smoothly animated number. Wiggle starts out at 0, increases to 1, decreases back past 0 to -1, and then increases to 0 again -- all in the course of two seconds, and then it repeats, forever. The second line defines charlotte by importing a bitmap file, making it available for use on the first line as the second argument to moveXY.
Although this example isn't a masterpiece, it is nonetheless a complete animation program in just two short lines of code.
Similarly, Figure 2 and Listing Two define an animation of Patrick moving up and down. To get the vertical movement, I've used a nonzero value for the second argument to moveXY. Rather than using wiggle, you use waggle, which is defined to be just like wiggle, but delayed by half a second.
Listing TwoupDownPat = moveXY 0 waggle pat pat = importBitmap "../Media/pat.bmp" Listing ThreecharlottePatDance = leftRightCharlotte `over` upDownPat |
Figure 3 and Listing Three combine the two previous examples. The over operation glues two animations together, yielding a single animation, with the first one being over the second. Because I used waggle for upDownPat in this combined animation, Pat is at the center when Charlotte is at her extremes (and vice versa).
Composition
Composition is the principle of putting together simple things to make complex ones, then putting these together to make even more complex things, and so on. This building-block principle is crucial for making even moderately complicated constructions; without it, the complexity quickly becomes unmanageable.
Listings One through Three illustrate composition. I first built leftRightCharlotte out of charlotte, wiggle, and moveXY; then upDownPat out of pat, moveXY, and waggle. Finally, I built charlottePatDance out of leftRightCharlotte and upDownPat. A crucial point here is that when you make something out of building blocks, the result is a new building block in itself, and you can forget about how it was constructed.
There is a more powerful version of composition, based on defining functions. Listing Four, for instance, defines hvDance (for "horizontally and vertical dance"), which combines any two images, in the way that charlottePatDance combines charlotte and pat. Now you can give a new definition for the dancing couple that gives exactly the same animation: charlottePatDance = hvDance charlotte pat.
Listing FourhvDance im1 im2 = moveXY wiggle 0 im1 `over` moveXY 0 waggle im2 |
Having defined this generalized dance animation, you can go on to more exotic compositions. For example, you can take an animation produced by hvDance, shrink it, and put the result back into hvDance twice to make it dance with itself. As Figure 4 and Listing Five show, the result is pleasantly surprising. This example gives you a hint of how powerful it is to be able to define new animation functions. For instance, you could try charlottePatDance, stretched by a wiggly amount; see Listing Six(a). To prevent negative scaling, you take the absolute value of wiggle. Next, use hvDance again, but give it wiggly sized charlotte and pat. For visual balance, use wiggle and waggle; see Listing Six(b). Next, put Pat in orbit around a growing and shrinking Charlotte. To get a circular motion, use moveXY, with wiggle for x and waggle for y; see Listing Six(c).
As you may have surmised, wiggle and waggle are related to sine and cosine and defined as:
waggle = cos (pi * time)
wiggle = sin (pi * time)
The animated number time is a commonly used "seed" for animations and has the value t at time t. Thus, for instance, the value of wiggle at time t is equal to sin(t).
Rate-Based Animation
Up to now, the positions of animations have been specified directly. For instance, the definition of leftRightCharlotte says that Charlotte's horizontal position is wiggle.
In the physical universe, objects move as a consequence of forces. As Newton explained, force leads to acceleration, acceleration to velocity, and velocity to position. With computer animation, you have the freedom to ignore the laws of our universe. However, since animations are usually intended to be viewed by and interacted with by inhabitants of our own universe, they are often made to look and feel real by emulating Newtonian laws or simplifications and variations on them.
The key idea underlying Newton's laws and their variations is the notion of an instantaneous rate of change. Fran makes this notion available in animation programs. To illustrate rate-based animation, you can make Becky move from the left edge of the viewing window, toward the right, at a rate of one distance unit per second; see Figure 5 and Listing Seven.
Listing SevenvelBecky u = moveXY x 0 becky where x = -1 + atRate 1 u |
The local definition of x here (introduced as a where clause), follows a style you'll see in the following definitions. To express an animated value that starts out with a value x0 and grows at a rate of r, you say x0 + atRate r u. Here u is a "user", which is a Fran value that contains all user input and display update events. Rate-based animations require a user argument in order to give atRate a way of knowing when to start and how precisely to calculate value from rate. Unlike previous examples, this one can be displayed with displayU. To see this example, enter displayU velBecky.
In Listing Seven, Becky has a constant velocity, but with a little more effort you can give Becky a constant acceleration by providing a constant value for the rate of change of the velocity; see Listing Eight. In the definition of v, the "0 +" is unnecessary, but emphasizes that the initial velocity is zero.
Listing EightaccelBecky u = moveXY x 0 becky where x = -1 + atRate v u v = 0 + atRate 1 u Listing NinemouseVelBecky u = move offset becky where offset = atRate vel u vel = mouseMotion u |
The notion of "rate" is useful not just in one dimension, but in two and three dimensions as well. In Listing Nine, I control Becky's 2D velocity with the mouse. When you hold the mouse cursor at the center of the view window, Becky stays still. As you move away from the center, imagine an arrow from the window's center to the mouse cursor. Becky moves in that direction and her speed will be equal to the arrow's length. This kind of imaginary arrow is referred to as a "vector" and is the same type of quantity as a two- or three-dimensional offset, velocity, or acceleration. In 2D, a vector can be thought of as having horizontal and vertical (X and Y) components, or as having a magnitude (length) and direction. This time, I use move, a variant of moveXY that takes a 2D offset vector. (If a vector v is x units horizontally and y units vertically, then "move v im" is equivalent to "moveXY x y im.") The offset vector starts out as the zero vector, and grows at a rate equal to mouseMotion, which is the offset of the mouse cursor relative to the origin of 2D space (which you see in the center of the view window).
In the real world, the position of an object may affect its speed or acceleration. In Listing Ten, Becky is chasing the mouse cursor. The further away it is, the faster she moves. The only difference from Listing Nine is that the velocity is determined by where the mouse cursor is relative to Becky's own position, as indicated by the vector subtraction.
For fun, you can generalize the beckyChaseMouse function in the same way that hvDance generalized charlottePatDance earlier; see Listing Eleven. Then chaseMouse becky is equivalent to beckyChaseMouse, as you can verify by typing displayU (chaseMouse becky) at the Hugs prompt.
For more fun, try the same, but replace becky with some of the animations that appeared earlier (leftRightCharlotte, charlottePatDance, and patOrbitsCharlotte); see Figure 6 and Listing Twelve.
Next make a chasing animation that acts like it is attached to the mouse cursor by a spring. The definition is similar to beckyChaseMouse. In Listing Thirteen, however, the rate is itself changing at rate accel (acceleration). This acceleration is defined like the velocity was in the previous example, but this time, some drag is also added. This tends to slow down Becky by adding some acceleration in the direction opposite to her movement. (Increasing or decreasing the "drag factor" of 0.5 in Listing Thirteen creates more or less drag.) The operator *^ multiplies a number by a vector, yielding a new vector that has the same direction as the given one but a scaled magnitude.
As usual, these declarative animation programs are straightforward because they say what the motion is, in high-level, continuous terms, without struggling to accommodate the discreteness of the computer used to present them. In contrast, imperative animation programs must explicitly simulate rate-based animation by making lots of discrete steps -- accumulating approximations to the continuously varying forces, accelerations, and velocities -- to approximate motion. Doing an accurate and efficient job of all this approximation work is a tricky task. With systems like Fran, you just describe the continuous motion in terms of continuously varying rates, and trust Fran to do a good job with the approximation. (Not good enough to fly an airplane or control dangerous machinery, but good enough for an effective illustration or game.)
Composition-in-Time
Operations such as over and move support the principle of composition-in-space. Composition-in-time is equally valuable. Figure 7 and Listing Fourteen, for instance, define an orbiting animation, and then combine it with a version of itself delayed by one second. Instead of delaying, you can speed it up; see Listing Fifteen. You can even delay or slow down animations involving user input. In Listing Sixteen, one Jake tracks the mouse cursor, while the other follows the same path, but delayed by one second.
Next you can build an animated sentence, following the mouse's motion path. As a preliminary step, use delayAnims dt anims = overs (zipWith later [0, dt ..] anims) to define a delayAnims function, which takes a time delay dt and a list anims of animations, and yields an animation. Each successive member of the given animation list is delayed by the given amount after the previous member. The definition of delayAnims introduces a few new Fran elements. The Fran overs function is like over, but applies to a list of animations rather than just two. Animations earlier in the list are placed over ones later in the list. The notation [0, dt ...] means the infinite list of numbers 0, dt, 2 dt, 3 dt, and so on. Finally, zipWith applies to a given two-argument function the successive values from two given lists. You use it here to delay the first animation in anims by 0 seconds, the second by dt seconds, the third by 2dt seconds, and so on. Finally, overs combines them into a single animation. Figure 8 and Listing Seventeen present a simple use of delayAnims. Next, use delayAnims (Listing Eighteen) to define mouseTrailWords that makes animated sentences.
The Haskell words function takes a string apart into a list of separate words. The Haskell map function takes a function (moveWord) and a list of values (the separated words) and makes a new list by applying the function to each member of the list. The Fran stringIm function makes a picture of a string. I define the function moveWord locally to be the result of making a picture of the given word, using the Fran stringIm function, and moving it to follow the mouse. delayAnims then causes each of these mouse-following word pictures to be delayed by different amounts. Figure 9 and Listing Nineteen is a use of trailWords following a specified path, while Listing Twenty follows the mouse.
Reactive Animation
The animations presented to this point can be called "nonreactive" since they always do the same thing. A "reactive" animation, on the other hand, involves discrete changes due to events. To illustrate, you can make a circle that starts off red and changes to blue when the left mouse button is pressed.
Listing Twenty-OneredBlue u = buttonMonitor u `over` withColor c circle where c = red `untilB` lbp u -=> blue |
An informal reading of the last line of Listing Twenty-One (also see Figure 10) is that the color c is red until you press the left mouse button, then becomes blue. For a more literal reading, you must understand that there are really two new binary infix operators here -- untilB and -=> -- which can be used separately or together. Implied parentheses are around lbp u -=> blue. The -=> operator, which can be read as "handled by value," takes an event (lbp u) and a value (blue), and yields a new event. In this case, the new event happens when the left button is pressed, and has value blue. The untilB operator takes an animation of any type (the color-valued constant animation red), and an event (lbp u -=> blue), whose occurrence provides a new animation of the same type.
Cyclic Reactivity
To make Figure 10 more interesting, you can switch between red and blue every time the left button is pressed. As Listing Twenty-Two shows, you do this with the help of a cycle function that takes two colors (c1 and c2) and gives an animated color that starts out as c1. When the button is pressed, it swaps c1 and c2 and repeats (using recursion).
Listing Twenty-TworedBlueCycle u = buttonMonitor u `over` withColor (cycle red blue u) circle where cycle c1 c2 u = c1 `untilB` nextUser_ lbp u ==> cycle c2 c1 |
Listing Twenty-Two uses the operator ==>, which is a variant of -=>. This operator (which can be read as "handled with function") takes an event and function f. It works like -=>, but gets event values by applying f to event values from the event given to it. In this case, f is the cycle function applied to just two arguments, leaving the third (a user) to be filled in automatically (using ==>). The nextUser_ function turns lbp into an event whose occurrence information is a new user, corresponding to the remainder of the user u. The color arguments get swapped each time "around the loop."
For variety, Listing Twenty-Three uses three colors, and changes the circle's size smoothly.
Selection
Figure 11 and Listing Twenty-Four present a flower that starts out in the center and moves to the left or right when the left or right mouse button is pressed, returning to the center when the button is released.
The function bSign is defined to be -1 when the left button is down, +1 when the right button is down, and 0 otherwise (thanks to selectLeftRight). You can use bSign to control the rate of growth of an image. In Figure 12 and Listing Twenty-Five, pressing the left (or right) button causes the image to shrink (or grow) until released. Put another way, the rate of growth is 0, -1, or 1, according to bSign. A simple change to the grow function (Listing Twenty-Six) causes the image to grow or shrink at a rate equal to its own size. selectLeftRight, used to define bSign, is also the key ingredient in defining buttonMonitor (Listing Twenty-Seven), which gives button feedback.
stringBIm turns an animated string into an image animation, which here gets enlarged, colored white, and moved down by a little less than half the window height.
selectLeftRight can itself be defined in terms of more basic functions, as in Listing Twenty-Eight. You use the conditional function condB to say that if the left button is down, use the left value, or if the right button is down, use the none value; otherwise use the none (constantB, which turns constants -- nonanimations -- into animations that never change).
Listing Twenty-EightselectLeftRight none left right u = condB (leftButton u) (constantB left) ( condB (rightButton u) (constantB right) ( constantB none )) |
3D Animation
Declarative animation applies to 3D as well, and the 2D operations I've used to this point -- importBMP, moveXY, and stretch -- have 3D counterparts. As a first 3D example, sphere = importX "../Media/sphere2.x" defines a sphere in which the function importX brings in a 3D model in "X-file" format, as used by Microsoft's DirectX. It is just as easy to import a teapot; see Figure 13 and Listing Twenty-Nine. I used stretch3 (a 3D counterpart to stretch) because the imported model was too small. Listing Thirty colors the teapot and makes it spin around the z- (vertical) axis.
Next, you can use the mouse to control the teapot's orientation. To do this, define mouseTurn to turn a given geometry g around the x-axis according the mouse's vertical movement, and around the z-axis according the mouse's horizontal movement, scaled by . Finally, as Figure 14 and Listing Thirty-One show, you apply mouseTurn to a green teapot.
You can also make teapots spin by controlling the rotation angle with the grow function, as in the growing flower examples. First, define spinPot, see Listing Thirty-Two, that takes (animated) color and angle and yields a colored, turning teapot. Then make a pot that spins one way when the left button is pressed, and the other way when the right button is pressed, using the grow function, and giving feedback with buttonMonitor; see Figure 15 and Listing Thirty-Three. renderGeometry, used here with a convenient default camera, turns a 3D animation into a 2D animation.
Additional spinning teapots will all have the general form of using the button monitor and rendering with the default camera. Rather than having to write several definitions, give the pattern a name. In Listing Thirty-Four, withSpinner takes a function as its first argument, and applies that function to the result of the grow function applied to the user argument. With this definition, you can write spin1 more simply; see Listing Thirty-Five. Another use of withSpinner is to make the color vary in hue and use the value from grow to determine the time-varying speed of rotation, so that the mouse buttons cause the turning to accelerate and decelerate (see Listing Thirty-Six).
In addition to visible geometry, you can add lights to a 3D model. In Listing Thirty-Seven, you combine a white sphere, which is visible but does not emit light, and a point light source, which is invisible but emits light. You color the sphere/light pair white, shrink it, and give it motion. For convenience, you express the motion path in terms of spherical coordinates, saying that the distance from the origin of space (which is also the center of the teapot) is always 1.5 units, the longitude is times the elapsed time, and the latitude is twice times the elapsed time. Consequently, you get a motion that meanders about, but maintains a fixed distance from the center of the teapot.
Just for fun, replace the single moving light with five. A simple change suffices, if you add delayAnims3 -- a 3D variant of the 2D delayAnims. As Listing Thirty-Eight shows, the difference is that in the 3D version, you use unionGs instead of overs. With this function, you make a list of five copies of the moving light (see Listing Thirty-Nine), using the predefined Haskell function replicate, stagger them in time with delayAnims3, and combine them with a green teapot. Then slow down the animation to see it more clearly.
In Listing Forty and Figure 16 (a moving trail of colored balls), you define a single ball having a spiral motion, which traces the surface of an unseen sphere of radius 1.5 with a longitude angle changing ten times as fast as the latitude angle (five versus one-half radians per second). From this one moving ball, you make ten balls, each a differently colored version, and then stagger them in time with delayAnims3. The coloring function bColor produces evenly spaced hues.
As a final 3D example, Listing Forty-One presents another spiral. This time you form a static spiral, then turn it about the z-axis.
Related Work
My interest in functional animation originally started with Kavi Arya's "A Functional Approach to Animation," Computer Graphics Forum, 5(4):297-311 (December, 1986). Although elegant, Arya used a discrete model of time. The TBAG system, on the other hand, used a continuous time model, and had a syntactic flavor similar to Fran's; see "TBAG: A High Level Framework for Interactive, Animated 3D Graphics Applications," by Conal Elliott, Greg Schechter, Ricky Yeung, and Salim Abi-Ezzi (Proceedings of SIGGRAPH '94 July, 1994). Unlike Fran, reactivity was handled imperatively. Behaviors were created by means of constraint solving, and updated through constraint assertion and retraction. Concurrent ML introduced a first-class notion of events that can be constructed compositionally; see "CML: A Higher-order Concurrent Language," by John H. Reppy (Proceedings of the ACM SIGPLAN '91 Conference on Programming Language Design and Implementation, 1991). However, those events perform side-effects such as writing to buffers or removing data from buffers. In contrast, Fran event occurrences have associated values -- they help define what an animation is, but do not cause any side effects.
For examples of DirectAnimation, see http://www.microsoft.com/ie/ie40/demos and "Adding Theatrical Effects to Everyday Web Pages with DirectAnimation," by Salim AbiEzzi and Pablo Fernicola (Microsoft Interactive Developer, October 1997).
For background on Haskell, see Introduction to Functional Programming, by Richard Bird and Philip Wadler, (Prentice-Hall, 1987), "A Gentle Introduction to Haskell," by Paul Hudak and Joseph H. Fasel, SIGPLAN Notices, 27(5), May, 1992, and http://haskell.org/tutorial/index.html.
For information on Fran, refer to "Functional Reactive Animation," by Conal Elliott and Paul Hudak, Proceedings of the 1997 ACM SIGPLAN International Conference on Functional Programming (June, 1997), or the Fran web page at http://www.research .microsoft.com/mconal/Fran.
Conclusion
For interactive animation to expand into its potential as a medium of communication, it must become much easier to program. As this article illustrates, one step toward this goal is the replacement of imperative techniques ("how to do") with declarative ones ("what to be").
There are several features I haven't explored here, including sound, smooth flip-book animation, and cropping. There are also many opportunities for improvement: more features for 2D, sound, and 3D; improved efficiency; generation of animation "software components" to integrate with components written in more mainstream programming languages; and support for distributed, multiuser scenarios.
Acknowledgments
Todd Knoblock and Jim Kajiya helped to explore the basic ideas of behaviors and events. Sigbjorn Finne, Anthony Daniels, and Gary Shu Ling helped with the implementation during research internships. Alastair Reid made improvements to the Haskell code, and, along with Paul Hudak and John Peterson, provided helpful discussions about functional animation, how to use Haskell well, and lazy functional programming in general. Becky Elliott cut out the kid pictures, which appear with the kind permission of their owners Patrick, Charlotte, Becky, and Jake.
DDJ
Copyright © 1998, Dr. Dobb's Journal