//
// DirectAnimation SDK sample
//
//
/**
The classic fifteen-puzzle in DirectAnimation. Note that the
underlying image can be *any* image (time-varying, interactive, video-
based, from a geometry, etc), also the puzzle supports a mode where
it is textured on an animate 3D object while it preserves its interactivity.
The main features highlighted in this sample include:
- using a time-varying, interactive image as a texture
- getting events to DirectAnimation from VBScript
- rendering geometry into an image
- overlaying text on video
- using reactive sound as feedback to user input
**/
//
//
// imports
import java.applet.*;
import java.awt.*;
import java.net.*;
import com.ms.dxmedia.*;
public class FifteenPuzzle extends DXMApplet {
public void init() {
super.init() ;
_model = new FifteenPuzzleModel(getCanvas());
setModel(_model);
}
// NOTE: You should use getModel instead of storing the model.
// If you store the model, you need to set it to null at destroy
// to break COM cycles due to current VM implementation of COM.
public void destroy() {
super.destroy();
_model = null;
}
//
//
// These are DXMApplet public methods that are visible to VBScript and are
// used as conduits for VBScript button events to
// triggering AppTriggerEvents in DirectAnimation.
public void toggleGeomProjection() { _model.toggleGeomProjection(); }
public void toggleImageUsed() { _model.toggleImageUsed(); }
public void resetPuzzle() { _model.resetPuzzle(); }
//
//
private FifteenPuzzleModel _model;
}
class FifteenPuzzleModel extends Model {
FifteenPuzzleModel(Viewer viewedOn) {
// Hang onto this for synchronization.
_viewedOn = viewedOn;
}
//
//
// Create a unit-sized image to divide up into the puzzle.
// The image cycles through four images upon toggleImage events.
ImageBvr createUnitImageForPuzzle() {
// Create the different images that will be used in the puzzle:
// time-varying solid-colored image for background
ImageBvr bkgnd = createBackgroundColorCycler();
// image of time-varying elapsed time in seconds
ImageBvr counterImg = createCounterImage();
// first possible image for puzzle, time over colored background
ImageBvr unitIm1 = overlay(counterImg, bkgnd);
// second possibility, based on rendered geometry
ImageBvr unitIm2 = createGeoImage();
// third possibility, based in imported video
ImageBvr unitIm3 = createMovieImage();
// forth possibility, time in second over the video
ImageBvr unitIm4 = overlay(counterImg, unitIm3);
// Have these image behaviors run just once, so
// that they don't get restarted when events happen.
unitIm1 = (ImageBvr)unitIm1.runOnce();
unitIm2 = (ImageBvr)unitIm2.runOnce();
unitIm3 = (ImageBvr)unitIm3.runOnce();
unitIm4 = (ImageBvr)unitIm4.runOnce();
// Cycle between the four images on occurence of this app event.
ImageBvr imageToShow = ImageBvr.newUninitBvr();
_toggleImageUsed = new AppTriggeredEvent();
imageToShow.init( until(unitIm1,
_toggleImageUsed, until(unitIm2,
_toggleImageUsed, until(unitIm3,
_toggleImageUsed, until(unitIm4,
_toggleImageUsed, imageToShow)))));
return imageToShow;
}
//
//
// Constructs one of the puzzle images by rendering
// three intertwined and animate instances of a doughnut.
public ImageBvr createGeoImage() {
// construct a color with a time- varying hue
NumberBvr hue1 = mul(localTime, toBvr(0.2));
ColorBvr col = colorHsl(hue1, toBvr(0.5), toBvr(0.5));
// import a half doughnut in the positive Y half space
GeometryBvr halfDonut =
importGeometry(buildURL(_geoBase,"hlfdonut.x"));
halfDonut = halfDonut.transform(rotate(zVector3, toBvr(Math.PI)));
// Construct a complete doughnut by combining with the second half.
// The resulting doughnut is centered around the origin and is of
// [-1,1] extent in X and Y.
GeometryBvr donut = union(halfDonut,
halfDonut.transform(rotate(xVector3, toBvr(Math.PI))));
// The first doughnut rotates around the Y axis and
// has the time-varying color.
GeometryBvr donut0 =
donut.transform(compose(rotate(yVector3, localTime),
scale3(toBvr(0.3)))).diffuseColor(col);
// The second doughnut is based on the first one scaled down by 25%,
// shifted by 0.2 in positive Z,
// and animated at a 30% faster rate than the first one.
GeometryBvr donut1 = (GeometryBvr)
donut0.transform(compose(translate(0,0,0.2),
scale3(0.75)))
// when the first doughnut progresses by one unit,
// this second doughnut progresses by 1.3 units.
.substituteTime(mul(localTime, toBvr(1.3)));
// The third doughnut is based on the first one scaled down by 25%,
// shifted by 0.2 in negative Z,
// and animated at a 40% slower rate than the first one.
GeometryBvr donut2 = (GeometryBvr)
donut0.transform(compose(translate(0,0,-0.2),
scale3(0.75))).substituteTime(mul(localTime, toBvr(0.6)));
// Construct a 3-D rotational transform with a time-varying
// axis of rotation and a time-varying angle of rotation.
Transform3Bvr xf = rotate(vector3(sin(localTime),
cos(localTime), sin(mul(toBvr(1.34), localTime))),
mul(localTime, toBvr(0.62)));
// The geometry for the image is the aggregation of the
// three doughnuts from above with the animate rotational transform applied.
GeometryBvr donuts = union(donut0, union(donut1, donut2)).transform(xf);
// a camera with projection point at 3 and near plane at 2
CameraBvr cam = perspectiveCamera(toBvr(3),toBvr(2));
GeometryBvr geoWithLight = union(donuts, directionalLight);
ImageBvr im = geoWithLight.render(cam);
// This translates the image so that its lower left corner
// is approximately at the origin. This approximation allows
// the image (doughnuts really) to get clipped slightly.
im = im.transform(translate(0.5,0.5));
return overlay(im, solidColorImage(white));
}
//
//
// Returns an elapsed-seconds counter as an image.
ImageBvr createCounterImage() {
// Convert localTime to a string and ignore fractional part.
FontStyleBvr fs = defaultFont.color(white).bold();
// render into an image
ImageBvr underlyingImage = stringImage(localTime.toString(toBvr(0)),fs);
// Scale into a unit square [0,1]. Note that as the size of the
// the string grows, for example going from 1 digit to 2, the scale
// adapts to fit the resulting image in the unit square.
//underlyingImage = underlyingImage.transform(scale(-1,1));
return underlyingImage.mapToUnitSquare();
}
// This is a time-varying solid-colored image, infinite in extent.
ImageBvr createBackgroundColorCycler() {
NumberBvr hue1 = mul(localTime, toBvr(0.2));
ColorBvr col1 = colorHsl(hue1, toBvr(0.5), toBvr(0.5));
return solidColorImage(col1);
}
//
//
// This returns an imported video as a continually looped image behavior.
ImageBvr createMovieImage() {
SoundBvr [] snd = { null } ;
ImageBvr [] unitIm = { null } ;
NumberBvr len = importMovie(buildURL(_movieBase,"movie.avi"),
unitIm, snd);
// loop it, by using the mod function
unitIm[0] = (ImageBvr)unitIm[0].substituteTime(mod(localTime, len ));
// scale into unit square
//unitIm[0] = unitIm[0].transform(scale(-1,1));
return unitIm[0].mapToUnitSquare();
}
//
//
// Constructs the image of a rotating two-bladed fan, and
// textures the given image on all of its four faces.
public ImageBvr textureOnFan(ImageBvr unitImForTexture) {
// quad is [-1,+1]x[-1,+1] with texture coordinates [0,1]x[0,1]
GeometryBvr faceObj = importGeometry(buildURL(_geoBase,"quad2.x"));
faceObj = faceObj.transform(rotate(zVector3, toBvr(Math.PI/2)));
// Replicate to give a textured backside as well.
faceObj = union(faceObj, faceObj.transform(rotate(yVector3, toBvr(Math.PI))));
// Replicate both sides with a 90 degree rotation to make a fan.
faceObj = union(faceObj,
faceObj.transform(rotate(yVector3, toBvr(Math.PI/2))));
// construct a rotation around the Y axis
Transform3Bvr xf = rotate(yVector3, mul(localTime, toBvr(0.4)));
// construct a rotating fan with the texture
faceObj = faceObj.texture(unitImForTexture.mapToUnitSquare()).transform(xf);
//faceObj = faceObj.transform(rotate(yVector3, toBvr(Math.PI)));
// construct a camera with projection point at 3 and near plane at 2
CameraBvr cam = perspectiveCamera(toBvr(3),toBvr(2));
// use a light and obtain an image
GeometryBvr geoWithLight = union(faceObj, directionalLight);
ImageBvr im = geoWithLight.render(cam);
return im.transform(compose(translate(0.5, 0.5),
scale2(0.4)));
}
//
//
public void createModel(BvrsToRun blst) {
// set the URL bases for the media imports
_mediaBase = getImportBase();
_sndBase = buildURL(_mediaBase,"sound/");
_movieBase = buildURL(_mediaBase,"movie/");
_geoBase = buildURL(_mediaBase,"geometry/");
// create the reactive cycling image for the puzzle
ImageBvr unitIm = createUnitImageForPuzzle();
// tile the image of the puzzle for playability
_puzz = new PuzzleLogic(unitIm, _viewedOn, _mediaBase);
puzzleIm = _puzz.getImage();
// Give the puzzle a black background,
// so the empty square will be black.
puzzleIm = overlay(puzzleIm, solidColorImage(black));
// Establish this as a runOnce bvr, so that when the code
// switches modes, the puzzle doesn't restart.
puzzleIm = (ImageBvr)puzzleIm.runOnce();
// Now, create two images, one based on direct display and
// one based on texturing on the fan:
// Translate the image down to center, and scale it down to 6 cms.
ImageBvr directDisplayIm =
puzzleIm.transform(compose(scale2(0.06),
translate(-0.5,-0.5)));
// Texture the image on a 3-D rotating fan
ImageBvr projectedGeo = textureOnFan(puzzleIm);
projectedGeo =
projectedGeo.transform(compose(scale2(0.07),
translate(-0.5,-0.5)));
projectedGeo = overlay(projectedGeo, solidColorImage(black));
// Cycle through 3-D and 2-D versions upon the toggle geometry event.
ImageBvr imageToShow = ImageBvr.newUninitBvr();
_toggleGeomProjection = new AppTriggeredEvent();
imageToShow.init( until(directDisplayIm,
_toggleGeomProjection, until(projectedGeo,
_toggleGeomProjection, imageToShow)));
// These are the resultant image and sound that constitute the model.
setImage(imageToShow);
setSound(_puzz.getSound());
}
//
public void cleanup() {
super.cleanup();
puzzleIm = null;
_puzz.cleanup();
_puzz=null;
_viewedOn = null;
_toggleGeomProjection = null;
_toggleImageUsed = null;
}
//
// Methods to trigger the AppTriggeredEvents,
// are called from DXMApplet.
public void toggleGeomProjection() { _toggleGeomProjection.trigger(); }
public void toggleImageUsed() { _toggleImageUsed.trigger(); }
public void resetPuzzle() { _puzz.resetImage(); }
// Use this event which originates from VBScript to toggle
// between direct image and fan.
AppTriggeredEvent _toggleGeomProjection;
// Use this event which originates from VBScript to toggle
// between the different images for the puzzle.
AppTriggeredEvent _toggleImageUsed;
PuzzleLogic _puzz;
Viewer _viewedOn;
URL _mediaBase;
URL _sndBase;
URL _movieBase;
URL _geoBase;
ImageBvr puzzleIm;
}
//
//
// Contains the logic for doing the puzzle.
class PuzzleLogic extends Statics {
// Take a unit image and chop it up into the pieces of the puzzle.
public PuzzleLogic(ImageBvr unitSourceIm,
Viewer viewedOn,
URL importBase) {
_viewedOn = viewedOn; // for synchronization
_squares = 3;
_inc = 1.0 / _squares;
_unitSourceIm = unitSourceIm;
// The image is a switcher to facilitate the resetting
// of the image; see the resetImage method below.
ImageBvr initialImage = createImage();
_imageSwitcher = new ModifiableBehavior(initialImage);
_image = (ImageBvr)_imageSwitcher.getBvr();
// will switch to these sounds as appropriate.
_successSound = importSound(buildURL(importBase,
"sound/butin.wav"), null);
_failureSound = importSound(buildURL(importBase,
"sound/deflate.mp2"), null);
// the puzzle sound is a switcher used in setHitSound below.
_soundSwitcher = new ModifiableBehavior(silence);
_sound = (SoundBvr)_soundSwitcher.getBvr();
}
// Just recreate the image and switch to it. Synchronize with
// the viewer so the code doesn't vie for the processor with it.
public void resetImage() {
synchronized (_viewedOn) {
_imageSwitcher.switchTo( createImage() );
}
}
// straightforward hit or fail sounds are generated.
public void setHitSound(boolean success) {
_soundSwitcher.switchTo(success ? _successSound : _failureSound);
}
//
//
// Construct the puzzle image as a composition of its
// distinct parts.
ImageBvr createImage() {
int i;
int j;
double x = 0;
double y = 0;
ImageBvr totalIm = emptyImage;
// build up the cells
for (i = 0; i < _squares; i++, y += _inc) {
x = 0;
for (j = 0; j < _squares; j++, x += _inc) {
// Skip the last one, to leave empty space.
if (i == _squares - 1 && j == _squares - 1) continue;
// Crop to get next cell and move lower left corner to the origin.
ImageBvr croppedIm = _unitSourceIm.
crop(point2(toBvr(x), toBvr(y)),
point2(toBvr(x + _inc), toBvr(y + _inc)));
croppedIm = croppedIm.transform(translate(toBvr(-x), toBvr(-y)));
// make pickable
PickableImage pick = new PickableImage(croppedIm);
ImageBvr pickableCroppedIm = pick.getImageBvr();
// extract probe event and create pick event
DXMEvent probedEvent = pick.getPickEvent();
DXMEvent pickedEvent = andEvent(leftButtonDown, probedEvent);
// Initially translate back to original space.
Transform2Bvr initXf = translate(toBvr(x), toBvr(y));
// Allow transform to change as it's picked.
Transform2Bvr xf = (Transform2Bvr)
untilNotify(initXf, pickedEvent,
new ImagePicker(i, j, pickedEvent, this));
// The entire cell is the transform applied to the cell
// at the origin.
ImageBvr movingIm = pickableCroppedIm.transform(xf);
// accumulate the cells of the puzzle
totalIm = overlay(movingIm, totalIm);
} // first for
} // second for
// Establish initial empty cell.
_emptyRow = _squares - 1;
_emptyCol = _squares - 1;
return totalIm;
}
//
//
public ImageBvr getImage() { return _image; }
public SoundBvr getSound() { return _sound; }
public void cleanup() {
_successSound = _failureSound = _sound = null;
_soundSwitcher = _imageSwitcher = null;
_unitSourceIm = _image = null;
_viewedOn = null;
}
int _squares;
int _emptyRow;
int _emptyCol;
double _inc;
SoundBvr _successSound;
SoundBvr _failureSound;
ModifiableBehavior _soundSwitcher;
ImageBvr _unitSourceIm;
ImageBvr _image;
ModifiableBehavior _imageSwitcher;
SoundBvr _sound;
Viewer _viewedOn;
}
//
//
// This object accepts a user event, and if it's a legal move
// (cell that is adjacent to the empty location), then the cell
// is transitioned and success sound is emitted, else illegal move
// sound is emitted.
class ImagePicker extends Statics implements UntilNotifier {
public ImagePicker(int row,
int col,
DXMEvent ev,
PuzzleLogic puzz) {
_row = row;
_col = col;
_ev = ev;
_puzz = puzz;
}
// invoked through the untilNotify construct
public Behavior notify(Object eventData,
Behavior currentRunningBvr,
BvrsToRun lst) {
Behavior xf = currentRunningBvr;
int r = _puzz._emptyRow;
int c = _puzz._emptyCol;
// check to see if this is a legal move
if (((Math.abs(_row - r) == 1) && _col == c) ||
((Math.abs(_col - c) == 1) && _row == r)) {
// find cell positions before and after the move
Point2Bvr start = point2(toBvr(_col * _puzz._inc),
toBvr(_row * _puzz._inc));
Point2Bvr finish = point2(toBvr(c * _puzz._inc),
toBvr(r * _puzz._inc));
// Build up a transform to move from orig to final in 0.5 second
double transitionTime = 0.5;
// the vector from start to finish
Vector2Bvr delta = sub(finish, start);
// pos begins at start and transitions to finish in transitionTime seconds
Point2Bvr pos = add(start, delta.mul(div(localTime, toBvr(transitionTime))));
// extract a translation from moving position
Transform2Bvr movingXf = translate(sub(pos, origin2));
// Use this transform, and after the elapsed time freeze
// in place using a snapshot event.
xf = untilEx(movingXf,
timer(toBvr(transitionTime)).snapshotEvent(movingXf));
// an alternative is: ,translate(finish.getX(), finish.getY()));
// Set the new location of the empty cell
_puzz._emptyCol = _col;
_puzz._emptyRow = _row;
// Set what position this cell is at now
_col = c;
_row = r;
// emit success sound
_puzz.setHitSound(true);
} else {
// emit illegal move sound
_puzz.setHitSound(false);
}
// Return this new transform, waiting again for the pick event.
return untilNotify(xf, _ev, this);
}
int _row;
int _col;
DXMEvent _ev;
PuzzleLogic _puzz;
}
//