Wednesday, May 12, 2010

Animation in Eclipse RCP Applications - A Bouncing Ball

This article shows how to add animation to an Eclipse RCP Application and builds off of a clean RCP Application with a View. To demonstrate the animation, code is created that makes an image of the moon bounce around the view, using real physics equations to control the acceleration and spin. If you want a more in depth explanation of adding an image or updating the GUI from a worker thread please see the previous articles: Add an Image to an Eclipse RCP Application and Updating a Widget in an Eclipse RCP Application from a Worker Thread.

Step 0: Create a Hello World with Eclipse RCP Application and add a View to it.

Step 1: Add an Image to the view . For this tutorial, an image of the moon with a transparent background called moon.png was prepared and added to the project.





Step 2: Create an inner class that implements Runnable, that updates the contents of the view at a regular interval. Later, during the initialization of the view, this worker thread is started, which calls the update() method of the view every ~15 milliseconds. The speed of the animation can be controlled by the TIMER_INTERVAL variable, which defines how long the thread should sleep before waking up and calling the update() method again.
class AnimatorThread implements Runnable{

// The timer interval in milliseconds
private static final int   TIMER_INTERVAL = 14;

public void go(){

Thread t = new Thread(this);
t.start();

}

public void run() {
try {
while(true){
animate();
Thread.sleep(TIMER_INTERVAL);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


Step 3: Create the view's contents and start the worker thread. In the createPartControl() method of the view, create a Canvas where the image will be painted. Set the canvas's background color and add a paintListener to it with code defining where the image of the moon should be painted. Finally, new up the worker thread class and start it.

public void createPartControl(Composite parent) {

parent.setBackground(new Color(parent.getDisplay(), 205, 38, 38));

// Create the canvas for drawing
canvas = new Canvas(parent, SWT.DOUBLE_BUFFERED);
canvas.setBackground(new Color(parent.getDisplay(), 0,0,0));
canvas.addPaintListener( new PaintListener() {

public void paintControl(PaintEvent e) {

GC gc = e.gc;
Transform trans = new Transform(e.display );
gc.getTransform( trans );
trans.translate( x, y );
trans.translate( IMAGE_WIDTH / 2f, IMAGE_WIDTH / 2f );
trans.rotate( a );
trans.translate( -IMAGE_WIDTH / 2f, -IMAGE_WIDTH / 2f );
gc.setTransform( trans );
trans.dispose();

gc.drawImage( moon, 0, 0, moon.getBounds().width, moon.getBounds().height, 0, 0, IMAGE_WIDTH, IMAGE_WIDTH); // Draw the moon

}
});

AnimatorThread at = new AnimatorThread();
at.go();
}


Step 4: Create the physics for the bouncing of the ball. Add some constants and variables as private members of the view class and an animate() method which calculates the next position of the moon. Last but not least, force a redraw of the canvas at the end of the animate() method. This invokes the code that was defined in the canvas's PaintListener.

public void animate() {

Display.getDefault().asyncExec(new Runnable(){

public void run(){

try{

float left = x;
float top = y;

// Determine the ball's location
directionY += GRAVITY;
x += directionX;
y += directionY;
a += directionA;

// Determine out of bounds
Rectangle rect = canvas.getClientArea();
if ( x > rect.width - IMAGE_WIDTH ) {
x = rect.width - IMAGE_WIDTH;
directionX = -directionX;
directionA -= ( directionY - directionA ) * FRICTION_WALL;
}
if ( x < 0 ) {
x = 0;
directionX = -directionX;
directionA += ( directionY - directionA ) * FRICTION_WALL;
}

if ( y > rect.height - IMAGE_WIDTH ) {
directionY = (int) ( -GRAVITY * Math.sqrt( ( 1 + 8 * ( rect.height - IMAGE_WIDTH ) / GRAVITY ) ) / 2 );
y = rect.height - IMAGE_WIDTH;
directionA += ( directionX - directionA ) * FRICTION_FLOOR;
}

float right = left + IMAGE_WIDTH;
float bottom = top + IMAGE_WIDTH;
if ( x < left )
left = x;
else
right = x + IMAGE_WIDTH;
if ( y < top )
top = y;
else
bottom = y + IMAGE_WIDTH;

// Force a redraw
canvas.redraw( (int) Math.floor( left ) - 1, (int) Math.floor( top ) - 1, (int) ( Math.ceil( right ) - Math.floor( left ) ) + 2, (int) ( Math.ceil( bottom ) - Math.floor( top ) ) + 2, false );

}catch(SWTException e){
//eat it!
}
}
});

}
Step 5: Run the application and test if everything worked. Your application should now have an image of the moon in the view that bounces back and forth across the view. As you resize the view, the moon's boundaries are recalculated. Here's the full code of the view:
package com.eclipsercptutorials.animation;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.ViewPart;

public class MainView extends ViewPart {

public static final String ID = "com.eclipsercptutorials.animation.mainView"; // the ID needs to match the id set in the view's properties

// The image
private Image moon;

// The image width
private static int IMAGE_WIDTH = 85;

// Rate of downward acceleration per frame
private static final float GRAVITY        = .25f;

// Coefficient of friction
private static final float FRICTION_FLOOR = 5f / 9f;
private static final float FRICTION_WALL  = 5f / 11f;

// The location of the "ball"
private float              x              = 0;
private float              y              = 0;
private float              a              = 0;

// The direction the "ball" is moving
private float              directionX     = 4;
private float              directionY     = 0;
private float              directionA     = 0;

// We draw everything on this canvas
private Canvas             canvas;

public MainView() {

moon = Activator.getImageDescriptor("icons/moon.png").createImage();

}

public void createPartControl(Composite parent) {

parent.setBackground(new Color(parent.getDisplay(), 205, 38, 38));

// Create the canvas for drawing
canvas = new Canvas(parent, SWT.DOUBLE_BUFFERED);
canvas.setBackground(new Color(parent.getDisplay(), 0,0,0));
canvas.addPaintListener( new PaintListener() {

public void paintControl(PaintEvent e) {

GC gc = e.gc;
Transform trans = new Transform(e.display );
gc.getTransform( trans );
trans.translate( x, y );
trans.translate( IMAGE_WIDTH / 2f, IMAGE_WIDTH / 2f );
trans.rotate( a );
trans.translate( -IMAGE_WIDTH / 2f, -IMAGE_WIDTH / 2f );
gc.setTransform( trans );
trans.dispose();

gc.drawImage( moon, 0, 0, moon.getBounds().width, moon.getBounds().height, 0, 0, IMAGE_WIDTH, IMAGE_WIDTH); // Draw the moon

}
});

AnimatorThread at = new AnimatorThread();
at.go();
}

public void animate() {

Display.getDefault().asyncExec(new Runnable(){

public void run(){

try{

float left = x;
float top = y;

// Determine the ball's location
directionY += GRAVITY;
x += directionX;
y += directionY;
a += directionA;

// Determine out of bounds
Rectangle rect = canvas.getClientArea();
if ( x > rect.width - IMAGE_WIDTH ) {
x = rect.width - IMAGE_WIDTH;
directionX = -directionX;
directionA -= ( directionY - directionA ) * FRICTION_WALL;
}
if ( x < 0 ) {
x = 0;
directionX = -directionX;
directionA += ( directionY - directionA ) * FRICTION_WALL;
}

if ( y > rect.height - IMAGE_WIDTH ) {
directionY = (int) ( -GRAVITY * Math.sqrt( ( 1 + 8 * ( rect.height - IMAGE_WIDTH ) / GRAVITY ) ) / 2 );
y = rect.height - IMAGE_WIDTH;
directionA += ( directionX - directionA ) * FRICTION_FLOOR;
}

float right = left + IMAGE_WIDTH;
float bottom = top + IMAGE_WIDTH;
if ( x < left )
left = x;
else
right = x + IMAGE_WIDTH;
if ( y < top )
top = y;
else
bottom = y + IMAGE_WIDTH;

// Force a redraw
canvas.redraw( (int) Math.floor( left ) - 1, (int) Math.floor( top ) - 1, (int) ( Math.ceil( right ) - Math.floor( left ) ) + 2, (int) ( Math.ceil( bottom ) - Math.floor( top ) ) + 2, false );

}catch(SWTException e){
//eat it!
}
}
});

}


public void setFocus() {}

class AnimatorThread implements Runnable{

// The timer interval in milliseconds
private static final int   TIMER_INTERVAL = 14;

public void go(){

Thread t = new Thread(this);
t.start();

}

public void run() {
try {
while(true){
animate();
Thread.sleep(TIMER_INTERVAL);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


Piece of Cake!! <--- Previous - Updating a Widget in an Eclipse RCP Application from a Worker Thread ---> Next - Add a Toolbar to a View in an Eclipse RCP Application Also see: Eclipse RCP Tutorial Table of Contents

No comments: