Timeline scenarios in the Trident animation library allow combining multiple timeline scenario actors in a parallel, sequential or custom order. Out of the box, Trident supports timelines, extensions of Runnable class and extensions of SwingWorker class, but the applications can easily create a custom implementation of the TimelineScenarioActor interface.
SwingWorker is an indispensable tool in the arsenal of a Swing programmer, and allows separating the long-running tasks that run on background threads from updating the UI components that must happen on the Event Dispatch Thread. While SWT does not have a direct counterpart, Eclipse core libraries have a very similar concept in Eclipse Jobs. And while the full functionality of Eclipse Jobs allows arbitrary dependencies and fine grained scheduling of interrelated jobs, you can wrap an Eclipse Job as a custom Trident timeline scenario actor and use the TimelineScenario APIs to seamlessly integrate an Eclipse Job in your Trident-powered SWT application.
Here is the complete source code to do this:
import org.eclipse.core.runtime.jobs.*;
import org.pushingpixels.trident.TimelineScenario.TimelineScenarioActor;
public abstract class EclipseJobTimelineScenarioActor extends Job implements
TimelineScenarioActor {
volatile transient boolean isDone = false;
public EclipseJobTimelineScenarioActor(String name) {
super(name);
this.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
isDone = true;
}
});
}
@Override
public boolean isDone() {
return isDone && (this.getState() == Job.NONE);
}
@Override
public void play() {
this.schedule();
}
@Override
public void resetDoneFlag() {
throw new UnsupportedOperationException();
}
@Override
public boolean supportsReplay() {
return false;
}
}
The play() method calls Job.schedule(), scheduling it for immediate execution. The isDone() method is called internally by the Trident engine on every pulse. The implementation registers a JobChangeListener to track the state of the job, and returns the relevant boolean value. Just as with SwingWorkers, the supportsReplay() returns false, and resetDoneFlag() throws an exception.
The timeline scenario below has the following steps which run in a sequential fashion:
Load an image from the specified URL.
Scale it to fit the specified area
Fade it in on the screen
The first step is done using our EclipseJobTimelineScenarioActor (which would be done with a TimelineSwingWorker in a Swing application):
private TimelineScenario getLoadImageScenario(final Item albumItem) {
TimelineScenario loadScenario = new TimelineScenario.Sequence();
// load the image
EclipseJobTimelineScenarioActor imageLoadWorker = new EclipseJobTimelineScenarioActor(
"Load image") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
URL url = new URL(albumItem.getMediumImage().getURL());
image = new Image(Display.getDefault(), url.openStream());
return Status.OK_STATUS;
} catch (Throwable t) {
t.printStackTrace();
return Status.CANCEL_STATUS;
}
}
};
loadScenario.addScenarioActor(imageLoadWorker);
// scale if necessary
TimelineRunnable scaler = new TimelineRunnable() {
@Override
public void run() {
if (image != null) {
float vFactor = (float) OVERVIEW_IMAGE_DIM
/ (float) image.getImageData().height;
float hFactor = (float) OVERVIEW_IMAGE_DIM
/ (float) image.getImageData().width;
float factor = Math.min(1.0f, Math.min(vFactor, hFactor));
if (factor < 1.0f) {
// scaled to fit available area
image = GraniteUtils.getScaledInstance(image,
(int) (factor * image.getImageData().width),
(int) (factor * image.getImageData().height));
}
imageLoadedDone = true;
}
}
};
loadScenario.addScenarioActor(scaler);
// and fade it in
Timeline imageFadeInTimeline = new Timeline(AlbumOverviewComponent.this);
imageFadeInTimeline.addPropertyToInterpolate("imageAlpha", 0.0f, 1.0f);
imageFadeInTimeline.addCallback(new SWTRepaintCallback(
AlbumOverviewComponent.this));
imageFadeInTimeline.setDuration(500);
loadScenario.addScenarioActor(imageFadeInTimeline);
return loadScenario;
}
You will need the following Eclipse libraries in your classpath:
org.eclipse.core.jobs
org.eclipse.equinox.common
org.eclipse.osgi
As you can see, it is quite easy to extend the existing functionality of Trident scenarios by wrapping external modules as custom timeline scenario actors.
Trident animation library allows adding a short fade-out sequence to a Swing / SWT window that is being closed. This can provide an unobtrusive visual feedback to the user, confirming his action and smoothly guiding his eye to the new application state.
Here is a utility method to add a fade-out sequence to disposing a Swing window – this code requires the latest builds of JDK7 that added the Window.setAlpha(float) method:
public static void fadeOutAndDispose(final Window window,
int fadeOutDuration) {
Timeline dispose = new Timeline(window);
dispose.addPropertyToInterpolate("opacity", 1.0f, 0.0f);
dispose.addCallback(new UIThreadTimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
if (newState == TimelineState.DONE) {
window.dispose();
}
}
});
dispose.setDuration(fadeOutDuration);
dispose.play();
}
and the matching method for disposing an SWT shell with a fade-out sequence, which requires SWT 3.4 to run:
public static void fadeOutAndDispose(final Shell shell, int fadeOutDuration) {
Timeline dispose = new Timeline(shell);
dispose.addPropertyToInterpolate("alpha", 255, 0);
dispose.addCallback(new UIThreadTimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
if (newState == TimelineState.DONE) {
shell.dispose();
}
}
});
dispose.setDuration(fadeOutDuration);
dispose.play();
}
Instead of calling Shell.dispose() or Window.dispose(), call the fadeOutAndDispose() method, passing the duration of the fade-out sequence in milliseconds. A previous entry has discussed another option – overriding the Window.dispose() method in your custom Swing class. While this works in Swing, SWT does not allow extending the Shell class outside the org.eclipse.swt.widgets package.
I am thrilled today to announce the availability of the final release for version 1.0 of Trident animation library for Java applications (code-named Acumen). Trident aims to simplify the development of rich animation effects in Java based UI applications, addressing both simple and complex scenarios – and you can read the available documentation in the project Wiki.
The current published API set follows the “simplicity before generality” approach. Trident is a continuation of the internal animation engine that has been part of the Substance look-and-feel for the last two years. Extracting it to a standalone library was accompanied by a significant overhaul of the API facets to:
Provide a shallow learning curve
Address real world use cases
It is very easy to start with Trident. To add animations to your application, simply create a timeline, configure it to change a value of some property and play it. From here, you can go progressively deeper towards the more powerful – and the more complex – Trident APIs:
At each level you get more control over the animations – as you get more comfortable with what Trident can do to address the animation requirements of your application.
During the development of this version i have created a number of simple and more advanced examples using Trident. These examples have driven the current shape of Trident APIs. The simple examples include animating the foreground color of a button, showing an indeterminate progress indication, emulating fireworks and Matrix rain. In addition, Amber and Onyx are more complicated applications that integrate animation scenarios into UIs that fetch and display information from the web-based backend services – such as Digg, Twitter and Amazon. These examples strive to be the blueprints for using Trident in Java applications.
If i had to choose three features that bring the most functionality to interested applications, those would be:
Timeline scenariosthat allow creating progressively complex dependency graphs of timelines, runnables, swing workers and custom application actors
Support for threading rulesof UI toolkits that frees the application code from creating convoluted nested inner classes and prevents it from deadlocking and freezing the UI
The extensibility layer that allows application to extend the existing core functionality to additional property classes and UI toolkits
Going forward, i intend to evolve Trident, and i already have a couple of post-1.0 features in the pipeline. The next major release of Substance will be rewritten to use Trident – further testing the published APIs for usage in real-world scenarios. In addition, the next major release of Flamingo ribbon will add Trident-based animations – where applicable.
Finally, no project is complete without the users trying the different features, pushing the existing APIs, reporting bugs and asking to support additional requirements. Subscribe to the mailinglists and let me know what is missing, and how the existing APIs can be improved. If you find a bug, report it in the issue tracker. If you want to take a look at the code, check out the SVN repository and subscribe to the “commits” mailing list.
Print out one of the PDF markers in the NYArToolkit/Data. For my demo, i’m using pattHiro.pdf
Import all projects under NYArToolkit in your Eclipse workspace. You can ignore the ones for JOGL and QuickTime for this specific demo.
In these projects, tweak the build path to point to the JMF / Java3D jars on your system.
Plug in your web camera.
Run jp.nyatla.nyartoolkit.jmf.sample.NyarToolkitLinkTest class to test that JMF has been installed correctly and can display the captured feed.
Run jp.nyatla.nyartoolkit.java3d.sample.NyARJava3D class to test the tracking capabilities of the NYArToolkit. Once the camera feed is showing, point the camera to the printed marker so that it is fully visible. Once you have it on your screen, a colored cube should be shown. Try rotating, moving and tilting the marker printout – all the while keeping it in the frame – to verify that the cube is properly oriented.
The demo from the video above is available as a part of the new Marble project. Here is how to set it up:
Sync the latest SVN tip of Marble.
Create an Eclipse project for Marble. It should have dependencies on NYArToolkit, NYArToolkit.utils.jmf, NYArToolkit.utils.java3d and Trident. It should also have jmf.jar, vecmath.jar, j3dcore.jar and j3dutils.jar in the build path.
Run the org.pushingpixels.marble.MarbleFireworks3D class. Follow the same instructions as above to point the webcam.
There’s not much to the code in this Marble demo. It follows the NyARJava3D class, but instead of static Java3D content (color cube) it has a dynamic scene that is animated by Trident. For the code below note that i’m definitely not an expert in Java3D and NYArToolkit, so there might as well be a simpler way to do these animations. However, they are enough to get you started in exploring animations in Java-powered augmented reality.
Each explosion volley is implemented by a collection of Explosion3D objects. Each such object models a single explosion “particle”. Here is the constructor of the Explosion3D class:
public Explosion3D(float x, float y, float z, Color color) {
this.x = x;
this.y = y;
this.z = z;
this.color = color;
this.alpha = 1.0f;
this.sphere3DTransformGroup = new TransformGroup();
this.sphere3DTransformGroup
.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
Transform3D mt = new Transform3D();
mt.setTranslation(new Vector3d(this.x, this.y, this.z));
this.sphere3DTransformGroup.setTransform(mt);
this.appearance3D = new Appearance();
this.appearance3D
.setCapability(Appearance.ALLOW_TRANSPARENCY_ATTRIBUTES_WRITE);
this.appearance3D
.setCapability(Appearance.ALLOW_COLORING_ATTRIBUTES_WRITE);
this.appearance3D.setColoringAttributes(new ColoringAttributes(color
.getRed() / 255.0f, color.getGreen() / 255.0f,
color.getBlue() / 255.0f, ColoringAttributes.SHADE_FLAT));
this.appearance3D.setTransparencyAttributes(new TransparencyAttributes(
TransparencyAttributes.BLENDED, 0.0f));
this.sphere3D = new Sphere(0.002f, appearance3D);
this.sphere3DTransformGroup.addChild(this.sphere3D);
this.sphere3DBranchGroup = new BranchGroup();
this.sphere3DBranchGroup.setCapability(BranchGroup.ALLOW_DETACH);
this.sphere3DBranchGroup.addChild(this.sphere3DTransformGroup);
}
Here, we have a bunch of Java3D code to create a group that can be dynamically changed at runtime. This group has only one leaf – the Sphere object.
As we’ll see later, the timeline behind this object changes its coordinates and the alpha (fading it out). Here is the relevant public setter for the alpha property:
Not much here – just updating the transparency of the underlying Java3D Sphere object. The setters for the coordinates are quite similar:
public void setX(float x) {
this.x = x;
Transform3D mt = new Transform3D();
mt.setTranslation(new Vector3d(this.x, this.y, this.z));
this.sphere3DTransformGroup.setTransform(mt);
}
public void setY(float y) {
this.y = y;
Transform3D mt = new Transform3D();
mt.setTranslation(new Vector3d(this.x, this.y, this.z));
this.sphere3DTransformGroup.setTransform(mt);
}
public void setZ(float z) {
this.z = z;
Transform3D mt = new Transform3D();
mt.setTranslation(new Vector3d(this.x, this.y, this.z));
this.sphere3DTransformGroup.setTransform(mt);
}
The main class is MarbleFireworks3D. Its constructor is rather lengthy and has a few major parts. The first part initializes the camera and marker data for the NYArToolkit core:
NyARCode ar_code = new NyARCode(16, 16);
ar_code.loadARPattFromFile(CARCODE_FILE);
ar_param = new J3dNyARParam();
ar_param.loadARParamFromFile(PARAM_FILE);
ar_param.changeScreenSize(WIDTH, HEIGHT);
Following that, there’s a bunch of Java3D code that initializes the universe, locale, platform, body and environment, and creates the main transformation group. The interesting code is the one that creates the main scene group that will hold the dynamic collection of Explosion3D groups:
mainSceneGroup = new TransformGroup();
mainSceneGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
mainSceneGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
mainSceneGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
root.addChild(mainSceneGroup);
nya_behavior = new NyARSingleMarkerBehaviorHolder(ar_param, 30f,
ar_code, 0.08);
nya_behavior.setTransformGroup(mainSceneGroup);
nya_behavior.setBackGround(background);
root.addChild(nya_behavior.getBehavior());
nya_behavior.setUpdateListener(this);
locale.addBranchGraph(root);
The NyARSingleMarkerBehaviorHolder is a helper class from the NYArToolkit.utils.java3d project. It tracks the transformation matrix computed by NYArToolkit based on the current location of the marker and updates the transformation set on the main scene group. As you will see later, there is no explicit handling of the marker tracking in the demo code – only creation, update and deletion of the Explosion3D objects.
Finally, we create a looping thread that adds random firework explosions:
// start adding random explosions
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500 + (int) (Math.random() * 1000));
float x = -1.0f + 2.0f * (float) Math.random();
float y = -1.0f + 2.0f * (float) Math.random();
float z = (float) Math.random();
addFireworkNew(x * 5.0f, y * 5.0f, 5.0f + z * 12.0f);
} catch (Exception exc) {
}
}
}
}.start();
The code in this method computes a 3D uniform distribution of small spheres that originate at the specific location (explosion center) and move outwards. Each Explosion3D object is animated with the matching timeline. The timeline interpolates the alpha property, as well as the coordinates. As you can see, while x and y are interpolated linearly, the interpolation of z takes the gravity into the account – making the explosion particles fall downwards. All the timelines are added to a parallel timeline scenario. Once a timeline starts playing, the matching branch group is added to the main scene graph. Once the timeline scenario is done, all the branch groups are removed from the main scene graph:
private void addFireworkNew(float x, final float y, final float z) {
final TimelineScenario scenario = new TimelineScenario.Parallel();
Set scenarioExplosions = new HashSet();
float R = 6;
int NUMBER = 20;
int r = (int) (255 * Math.random());
int g = (int) (100 + 155 * Math.random());
int b = (int) (50 + 205 * Math.random());
Color color = new Color(r, g, b);
for (double alpha = -Math.PI / 2; alpha <= Math.PI / 2; alpha += 2
* Math.PI / NUMBER) {
final float dy = (float) (R * Math.sin(alpha));
final float yFinal = y + dy;
float rSection = (float) Math.abs(R * Math.cos(alpha));
int expCount = Math.max(0, (int) (NUMBER * rSection / R));
for (int i = 0; i < expCount; i++) {
float xFinal = (float) (x + rSection
* Math.cos(i * 2.0 * Math.PI / expCount));
final float dz = (float)(rSection
* Math.sin(i * 2.0 * Math.PI / expCount));
float zFinal = z + dz;
final Explosion3D explosion = new Explosion3D(x * SCALE, y * SCALE,
z * SCALE, color);
scenarioExplosions.add(explosion);
final Timeline expTimeline = new Timeline(explosion);
expTimeline.addPropertyToInterpolate("alpha", 1.0f, 0.0f);
expTimeline.addPropertyToInterpolate("x", x * SCALE, xFinal
* SCALE);
expTimeline.addPropertyToInterpolate("y", y * SCALE, yFinal
* SCALE);
expTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelinePulse(float durationFraction,
float timelinePosition) {
float t = expTimeline.getTimelinePosition();
float zCurr = (z + dz * t - 10 * t * t) * SCALE;
explosion.setZ(zCurr);
}
});
expTimeline.setDuration(3000);
expTimeline.addCallback(new TimelineCallbackAdapter() {
@Override
public void onTimelineStateChanged(TimelineState oldState,
TimelineState newState, float durationFraction,
float timelinePosition) {
if (newState == TimelineState.PLAYING_FORWARD) {
mainSceneGroup.addChild(explosion
.getSphere3DBranchGroup());
}
}
});
scenario.addScenarioActor(expTimeline);
}
}
synchronized (explosions) {
explosions.put(scenario, scenarioExplosions);
}
scenario.addCallback(new TimelineScenarioCallback() {
@Override
public void onTimelineScenarioDone() {
synchronized (explosions) {
Set ended = explosions.remove(scenario);
for (Explosion3D end : ended) {
mainSceneGroup
.removeChild(end.getSphere3DBranchGroup());
}
}
}
});
scenario.play();
}
The dependent libraries that are used here do the following:
JMF captures the webcam stream and provides the pixels to put on the screen
NYArToolkit processes the webcam stream, locates the marker and computes the matching transformation matrix
Java3D tracks the scene graph objects, handles the depth sorting and the painting of the scene
Trident animates the location and alpha of the explosion particles