// // // /** Album3D imports images with corresponding labels and presents the images as a photo album in 3-D. It also allows the interactive flipping of pages and rotation of the view point, the former by using the left and right arrows, and the latter by using the up and down arrows. The flipping pages have corresponding stereo sound effects. The key feature of this sample is a state machine used to control and coordinate the animation of the album and the corresponding sound effects. The state machine has five states and is used to control and coordinate four behaviors, including: indexes to the left and right pages of the album (NumberBvrs), the actual center page (GeometryBvr), and the sound effect (SoundBvr). The state machine flows between the different states when specific events occur, and it switches the above four behaviors into new behaviors, accordingly. This is done through the two forms of the "until" method. The example suggests a systematic way to implement state machines in DirectAnmiation, based on mixed media behaviors. In addition, the approach lends itself to automatic generation of behavior-controlling state machines through visual authoring tools. **/ // // // imports import com.ms.dxmedia.*; // all DirectAnimation classes import java.awt.*; // Dimension class import java.net.*; // URL class // An applet that instantiates the Album3D model public class Album3D extends DXMApplet { public void init() { super.init() ; setModel (new A3DM(this)) ; } } class A3DM extends Model { A3DM(DXMApplet dxma) { Dimension dim = dxma.getSize(); // The base unit of measure in DirectAnimation is the meter. // Convert the size into meters by multiplying it with the pixelBvr. _halfHeight = mul(toBvr(dim.height*0.5),pixelBvr); _halfWidth = mul(toBvr(dim.width*0.5),pixelBvr); } // // // Takes an index to a page and a direction of motion. // Returns the given page animated in the proper direction, // both left and right page flips are supported. public static GeometryBvr flip(NumberBvr ind, int dir) { // orientation of page flip depends on dir Transform3Bvr flipTrans = rotate(yVector3, mul(toBvr(dir), mul(toBvr(flipRate), localTime))); // starting angle of page depends on dir GeometryBvr page = ((GeometryBvr)picFrames.nth(ind)).transform( (Transform3Bvr)cond(toBvr(dir == +1), leftTrans, rightTrans)); return page.transform(flipTrans); } // Flipping a page commences with startSnd and transitions to flipSound, // stereo sound is used to reflect the direction of the page flip. public static SoundBvr flippingSnd(int dir) { double panRate = 2/period; // in period time it goes from 0 to 2 return (SoundBvr)until(startSnd.pan(-1 * dir), // in -dir speaker //timer(startLen[0]), timer(toBvr(1.0)), // substitute for startLen flipSnd.loop().pan(mul(toBvr(dir), // varries from -dir to +dir. sub(mul(toBvr(panRate), localTime), toBvr(1))))); } // Pastes a picture on a unit-sized page while preserving its // aspect ratio. The picture is left-centered. The extra // space is filled with paper texture. public static ImageBvr normalPic(ImageBvr pic) { Bbox2Bvr picBbox = pic.boundingBox(); Point2Bvr ur = picBbox.getMax(); NumberBvr dim = maxNumBvr(ur.getX(), ur.getY()); ImageBvr croppedTex = paperTex.tile().crop(point2(neg(dim), neg(dim)), point2(dim, dim)); return overlay(pic.transform(scale(toBvr(-1),toBvr(1))), croppedTex).mapToUnitSquare(); } // Constructs a number behavior which is a certain number // or 0 depending on if the given key is pressed or not. static NumberBvr keyIntegral(int key) { NumberBvr cAngleRate = toBvr(Math.PI/8); // rate of motion of camera angle NumberBvr factor = (NumberBvr)cond(keyState(key), cAngleRate, toBvr(0)); return integral(factor); } // Takes a page of the album and renders it into an image // based on consistent camera and lights. Animates the camera // based on the up and down arrow keys. static ImageBvr renderPage(GeometryBvr page) { // create an interactive angle to animate the camera NumberBvr initialAngle = toBvr(Math.PI/20); NumberBvr cameraAngle = add(initialAngle, sub(keyIntegral(Event.UP), keyIntegral(Event.DOWN))); // use the angle to rotate the camera Transform3Bvr cameraTrans = rotate(xVector3, cameraAngle); CameraBvr camera = perspectiveCamera(toBvr(5), toBvr(4)).transform(cameraTrans); // scale the resulting image, map 1 unit to 50 pixels Transform2Bvr scaleTrans = scale2(mul(toBvr(PAGESIZE),pixelBvr)); GeometryBvr scene = union(ambientLight, page); return scene.render(camera).transform(scaleTrans); } // // render a string into an image, at a specific location and color public static ImageBvr renderStr(StringBvr str, ColorBvr clr, NumberBvr xPos, NumberBvr yPos) { Transform2Bvr pos = translate(xPos, yPos); FontStyleBvr fs = defaultFont.bold().color(clr); ImageBvr textIm = stringImage(str, fs).transform(pos); return textIm; } // returns max(x, y) public static NumberBvr maxNumBvr(NumberBvr x, NumberBvr y) { return (NumberBvr) cond(gt(y, x), y, x); } // /** The state machine has five states controlled by two events, one for left page flips and one for right flips. The machine controls and coordinates the pages of the album and the sound effects of the album. The sample uses notification objects because of the cyclic dependency in the finite state machine (not because of event data, which is the second purpose for using notification objects). Notification objects, by providing a delayed execution construct, make it possible to construct cyclic calling dependencies without infinite looping. The sample also uses the until2 method instead of untilNotify since it allows the aggregatation of two notification events each with a different notification method; see the opened state below. Note that both cyclic dependency and branching from one state into two or more states upon different events are commonly occuring patterns in general state machines. Hence the example below has the building blocks that are needed for typical state machines. **/ // This state is the entry point into the state machine // and is also returned to upon flipping the // front page back (effectively going back to the start state). // It sets the left page to blank, the right page to // the front page of the album, and it emits the page-flip completion sound // through the right speaker. // The state waits on the leftEventNotify event upon which it // invokes the transLeft notification object which implements the // "transition left" state. public static TupleBvr frontState () { NumberBvr leftInd = toBvr(0); Behavior newBvrs[] = { leftInd, add(leftInd, toBvr(1)), emptyGeometry, endSnd.pan(+1)}; TupleBvr tuple = new TupleBvr(newBvrs); return (TupleBvr)untilEx(tuple, leftEventNotify); } // This state is entered upon the end of a page flip // that causes the album to be properly opened // (i.e., not in a boundary condition). It waits on either a left or a right // event, at which point it invokes the transLeft or transRight // notification objects, respectively, which carry it through either // the left transition or to right // transition state. It accepts an index to the // left page plus the side to which the previous page was flipped to, // in order to emit the page flip termination sound. public static TupleBvr opened (NumberBvr leftInd, int side) { Behavior newBvrs[] = {leftInd , add(leftInd, toBvr(1)), emptyGeometry, endSnd.pan(toBvr(side))}; TupleBvr tuple = new TupleBvr(newBvrs); return (TupleBvr)untilEx(tuple, orEvent(rightEventNotify, leftEventNotify)); } // symmetric to frontState public static TupleBvr backState () { NumberBvr leftInd = toBvr(A3DM.numPics); Behavior newBvrs[] = {leftInd, add(leftInd, toBvr(1)), emptyGeometry, endSnd.pan(-1)}; TupleBvr tuple = new TupleBvr(newBvrs); return (TupleBvr)untilEx(tuple, rightEventNotify); } // // // Here we create the Album3D model. public void createModel(BvrsToRun blst) { // // // Build URL bases for importing images, geometries, and sounds. URL mediaBase = getImportBase(); URL imgBase = buildURL(mediaBase,"image/"); URL geoBase = buildURL(mediaBase,"geometry/"); URL sndBase = buildURL(mediaBase,"sound/"); // Load the sound files that will be used for sound effects startSnd = importSound(buildURL(sndBase,"inflate.mp2"), startLen); flipSnd = importSound(buildURL(sndBase,"clock1.mp2"), null); endSnd = importSound(buildURL(sndBase,"deflate.mp2"), null); // This image will be tiled into the paper texture. paperTex = importImage(buildURL(imgBase,"sandbg1.jpg")); // Synthetically construct the front and back cover pages of the Album. ColorBvr labelColor = red; ImageBvr front1 = renderStr(toBvr("Outdoors"), labelColor, toBvr(0), toBvr(0.003)); ImageBvr front2 = renderStr(toBvr("Album"), labelColor, toBvr(0), toBvr(-0.003)); ImageBvr front = overlay(front1, front2); ImageBvr back = renderStr(toBvr("The End"), labelColor, toBvr(0), toBvr(0)); // Import a list of images, some of these could have been .gif images. // We add the front and back covers appropriately, // and we add empty images to pair with them. ImageBvr pics[] = {emptyImage, front, importImage(buildURL(imgBase,"phantom.jpg")), importImage(buildURL(imgBase,"hiddenBeachSS.jpg")), importImage(buildURL(imgBase,"hiddenBeachSG.jpg")), importImage(buildURL(imgBase,"jotunheimen1.jpg")), importImage(buildURL(imgBase,"redWoodCar.jpg")), importImage(buildURL(imgBase,"wenatcheeCascades.jpg")), importImage(buildURL(imgBase,"tulipsHol1.jpg")), importImage(buildURL(imgBase,"tulipsHol2.jpg")), importImage(buildURL(imgBase,"yosemiteCreek.jpg")), importImage(buildURL(imgBase,"kidsUtah.jpg")), importImage(buildURL(imgBase,"foliageUtah.jpg")), back, emptyImage}; // Ditto for corresponding labels. No labels for cover pages nor for empty ones. StringBvr lbls[] = {toBvr(""), toBvr(""), toBvr("Crater Lk., Oregon"), toBvr("Klamath, California"), toBvr("Klamath, California"), toBvr("Jotenheimen, Norway"), toBvr("Redwoods N.P., California"), toBvr("Cascades, Washington"), toBvr("Tulip Garden, Holland"), toBvr("Tulip Garden, Holland"), toBvr("Yosemite, California"), toBvr("Centerville, Utah"), toBvr("Bountiful, Utah"), toBvr(""), toBvr("")}; ArrayBvr labels = new ArrayBvr(lbls); numPics = lbls.length - 2; // don't count the emptyImages // a [-1, 1] square in X and Y with [0, 1] texture coordinates for // both faces. GeometryBvr square = importGeometry(buildURL(geoBase,"square.x")); square = square.transform(scale(toBvr(1),toBvr(-1),toBvr(1))); //square = square.transform(rotate(xVector3, toBvr(Math.PI))); // frame is a [0, 2]x[-1, 1] square, on right hand side of Y axis GeometryBvr frame = square.transform(translate(1,0,0)); // creates an array for the pages that are counter part to the pictures. GeometryBvr frames[] = new GeometryBvr[numPics + 2]; // first and last correspond to the empty pictures frames[0] = emptyGeometry; frames[numPics + 1] = emptyGeometry; // the rest correspond to the pictures for (int i=1; i<=numPics; i++) frames[i] = frame.texture(normalPic(pics[i])); // Construct a Behavior Array of the pages, so we can use NumberBvr indices picFrames = new ArrayBvr(frames); // demarcates the line around which pages pivot NumberBvr lineThickness = toBvr(6 * mm); // 3% of page size // Make the square very thin in the X direction. // We use a square to represent the line in 3D space. GeometryBvr pivotLine = square.diffuseColor(red). transform(scale(lineThickness, toBvr(1), toBvr(1))); // // // Kick-off the state machine. // The "state" is a tuple with two number behaviors, one each for the left and // right page indexes, a geometry behavior for the center page, // and a sound behavior for the audio effects. TupleBvr state = frontState(); // extract the individual behaviors from the tuple leftInd = (NumberBvr) state.nth(0); rightInd = (NumberBvr) state.nth(1); centerPage = (GeometryBvr) state.nth(2); snd = (SoundBvr) state.nth(3); // extract the left and right pages and labels from their indices GeometryBvr leftPage = ((GeometryBvr)picFrames.nth(leftInd)).transform(leftTrans); GeometryBvr rightPage = ((GeometryBvr)picFrames.nth(rightInd)).transform(rightTrans); StringBvr lPageLabel = (StringBvr)labels.nth(leftInd); StringBvr rPageLabel = (StringBvr)labels.nth(rightInd); // now construct the album from the pages and the pivot line ImageBvr album = overlay(renderPage(pivotLine), overlay(renderPage(centerPage), overlay(renderPage(rightPage), renderPage(leftPage)))); // Here, construct the two lables and position them under the album. // Position them relative to the viewport to be at 12.5% from the bottom. ImageBvr lLabel = renderStr(lPageLabel, labelColor, neg(div(_halfWidth,toBvr(2))), mul(toBvr(-0.75),_halfHeight)); ImageBvr rLabel = renderStr(rPageLabel, labelColor, div(_halfWidth,toBvr(2)), mul(toBvr(-0.75),_halfHeight)); // The model is the album with the lables and a blue background. ImageBvr model = overlay(album, overlay(lLabel, overlay(rLabel, solidColorImage(blue)))); setSound(snd); setImage(model); // // } // stores the height of the applet viewport in the member variable. private NumberBvr _halfHeight; private NumberBvr _halfWidth; // // static final int PAGESIZE = 60; // Album page size in pixels static final double period = 3; // duration of a page flip in seconds static final double tiltAngle = Math.PI / 4; // angle at which resting pages sit static final double flipRate = (Math.PI - 2*tiltAngle)/period; // the rate of a page flip // album pages are slightly off horizontal // resting position of left page static final Transform3Bvr leftTrans = rotate(yVector3, toBvr(-Math.PI + tiltAngle)); // resting position of right page static final Transform3Bvr rightTrans = rotate(yVector3, toBvr(-tiltAngle)); static int numPics; // the number of pages in the album // the three sound files that will be imported // the length of the start sound static NumberBvr startLen[] = new NumberBvr[1]; // at the begining of a page flip static SoundBvr startSnd; // during a page flip static SoundBvr flipSnd; // at the end of a page flip static SoundBvr endSnd; // the texture used for the album paper static ImageBvr paperTex; static ArrayBvr picFrames; // the pages of the album // The state of the album consists of four behaviors static NumberBvr leftInd; // index to left page static NumberBvr rightInd; // index to right page static GeometryBvr centerPage; // the actual center page static SoundBvr snd; // sound associated with page flipping // // // interaction events // Right and left arrows to flip pages. static final DXMEvent rightEvent = keyDown(Event.RIGHT); static final DXMEvent leftEvent = keyDown(Event.LEFT); // signals the conclusion of a page flip animation static final DXMEvent endFlip = timer(toBvr(period)); // instantiate the notifier objects static final TransRight transRight = new TransRight(); static final TransLeft transLeft = new TransLeft(); // Define notify events based on the previous objects, // these events get used in the state machine. static final DXMEvent rightEventNotify = rightEvent.notifyEvent(transRight); static final DXMEvent leftEventNotify = leftEvent.notifyEvent(transLeft); // } // // This notifier object flips the center page to the left side until the // completion of the page flip. At that point if we've flipped // the last page then we go to the back state, else we go to // the opened state, which is the nominal one. // Notice how we extract the present left page index from the "previous" // parameter of the "notify" method. class TransLeft extends Statics implements UntilNotifier { public Behavior notify(Object eventData, Behavior previous, BvrsToRun blst) { NumberBvr leftInd = (NumberBvr)((TupleBvr)previous).nth(0); Behavior newBvrs[] = {leftInd, add(leftInd, toBvr(2)), A3DM.flip(add(leftInd, toBvr(1)), -1), A3DM.flippingSnd(-1)}; return (TupleBvr) until(new TupleBvr(newBvrs), A3DM.endFlip, cond(eq(leftInd, toBvr(A3DM.numPics - 1)), A3DM.backState(), A3DM.opened(add(leftInd, toBvr(1)), -1))); } } // symmetric to TransLeft class TransRight extends Statics implements UntilNotifier { public Behavior notify(Object eventData, Behavior previous, BvrsToRun blst) { NumberBvr leftInd = sub((NumberBvr)((TupleBvr)previous).nth(0), toBvr(1)); Behavior newBvrs[] = {leftInd, add(leftInd, toBvr(2)), A3DM.flip(add(leftInd, toBvr(1)), +1), A3DM.flippingSnd(+1)}; return (TupleBvr) until(new TupleBvr(newBvrs), A3DM.endFlip, cond(eq(leftInd, toBvr(0)), A3DM.frontState(), A3DM.opened(leftInd, +1))); } } //