Tuesday, July 8, 2008

Motion Madness - Physics Gone Wild


Our LWUIT lists and transitions move according to various physics based algorithms allowing the motion to be very fluid and smooth. This is enabled by the Motion class which encapsulates physical motion properties, however to someone who never dealt with physics based animations this might seem like a very odd class.

First lets explain some animation basics, every animation has a state this might seem obvious but isn't as clear cut. The state indicates how we draw the animation but isn't the actual animation frame, e.g. say we want to move a ball across the screen. The state can be its X/Y coordinate, if we would update the state in the paint method the ball will move fast on a fast device and slow in a slow device. Worse, it will have linear or erratic motion rather than a smooth motion.

LWUIT solves this by providing the animate() method which is invoked in fixed intervals allowing you to update animation state. If animation state hasn't changed just return false from animate and no repaint will occur. How does all this fit with Motion?

Think of motion as a physics equation that allows you to calculate acceleration and deceleration of an animation on a single axis. So if we have a list scrolling and we want the scroll to accelerate/decelerate as it moves we can just create a spline motion (representing well known equation for such physical movement) and rather than immediately paint the updated position we can use animate to update the position based on the motion.

To demonstrate this I created a simple demo allowing you to view an image larger than the screen and move it using the motion class. Moving with the arrow keys will produce a smooth motion effect, you can also "flick" your finger to physically move the image with some friction using velocity.
/**
* A component that allows us to drag an image file with a physical drag motion
* effect.
*
* @author Shai Almog
*/

public class MotionComponent extends Component {
private Image img;
private int positionX;
private int positionY;
private Motion motionX;
private Motion motionY;
private int destX;
private int destY;
private static final int TIME = 800;
private static final int DISTANCE_X = Display.getInstance().getDisplayWidth() / 3;
private static final int DISTANCE_Y = Display.getInstance().getDisplayHeight() / 3;
private int dragBeginX = -1;
private int dragBeginY = -1;
private int dragCount = 0;

public MotionComponent(Image img) {
this.img = img;
}

protected Dimension calcPreferredSize() {
Style s = getStyle();
return new Dimension(img.getWidth() + s.getPadding(LEFT) + s.getPadding(RIGHT),
img.getHeight() + s.getPadding(TOP) + s.getPadding(BOTTOM));
}

public void initComponent() {
getComponentForm().registerAnimated(this);
}

public void paint(Graphics g) {
Style s = getStyle();
g.drawImage(img, getX() - positionX + s.getPadding(LEFT), getY() - positionY + s.getPadding(TOP));
}

public void keyPressed(int keyCode) {
super.keyPressed(keyCode);
switch(Display.getInstance().getGameAction(keyCode)) {
case Display.GAME_DOWN:
destY = Math.min(destY + DISTANCE_Y, img.getHeight() - Display.getInstance().getDisplayHeight());
motionY = Motion.createSplineMotion(positionY, destY, TIME);
motionY.start();
break;
case Display.GAME_UP:
destY = Math.max(destY - DISTANCE_Y, 0);
motionY = Motion.createSplineMotion(positionY, destY, TIME);
motionY.start();
break;
case Display.GAME_LEFT:
destX = Math.max(destX - DISTANCE_X, 0);
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
break;
case Display.GAME_RIGHT:
destX = Math.min(destX + DISTANCE_X, img.getWidth() - Display.getInstance().getDisplayWidth());
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
break;
default:
return;
}
}

public void pointerDragged(int x, int y) {
if(dragBeginX == -1) {
dragBeginX = x;
dragBeginY = y;
}
positionX = Math.max(0, Math.min(positionX + x - dragBeginX, img.getWidth() - Display.getInstance().getDisplayWidth()));
positionY = Math.max(0, Math.min(positionY + y - dragBeginY, img.getHeight() - Display.getInstance().getDisplayHeight()));
dragCount++;
}

public void pointerReleased(int x, int y) {
// this is a result of a more significant drag operation, some VM's always
// send a pointerDragged so we should ignore too few drag events
if(dragCount > 4) {
float velocity = -0.2f;
if(dragBeginX < x) {
velocity = 0.2f;
}
motionX = Motion.createFrictionMotion(positionX, velocity, 0.0004f);
motionX.start();

if(dragBeginY < y) {
velocity = 0.2f;
} else {
velocity = -0.2f;
}
motionY = Motion.createFrictionMotion(positionY, velocity, 0.0004f);
motionY.start();
}
dragCount = 0;
dragBeginX = -1;
dragBeginY = -1;
}

public boolean animate() {
boolean val = false;
if(motionX != null) {
positionX = motionX.getValue();
if(motionX.isFinished()) {
motionX = null;
}
// velocity might exceed image bounds
positionX = Math.max(0, Math.min(positionX, img.getWidth() - Display.getInstance().getDisplayWidth()));
val = true;
}
if(motionY != null) {
positionY = motionY.getValue();
if(motionY.isFinished()) {
motionY = null;
}
positionY = Math.max(0, Math.min(positionY, img.getHeight() - Display.getInstance().getDisplayHeight()));
val = true;
}
return val;
}
}
To use this component just use this code:
Form motionDrag = new Form("Motion");
motionDrag.setLayout(new BorderLayout());
motionDrag.addComponent(BorderLayout.CENTER, new MotionComponent(Image.createImage("/setu_bandhasana.jpg")));
motionDrag.show();
How does this work?

Look at this snippet from keyPressed:
case Display.GAME_RIGHT:
destX = Math.min(destX + DISTANCE_X, img.getWidth() - Display.getInstance().getDisplayWidth());
motionX = Motion.createSplineMotion(positionX, destX, TIME);
motionX.start();
We update the destX variable which is never used to draw, the positionX is the position where we actually draw the image. Then we create and start a spline motion over time.
Now in the animate method we have the following:
positionX = motionX.getValue();
if(motionX.isFinished()) {
motionX = null;
}
// velocity might exceed image bounds
positionX = Math.max(0, Math.min(positionX, img.getWidth() - Display.getInstance().getDisplayWidth()));
val = true;
We extract the current value from the motion (based on the current time) and update the position drawn on the screen accordingly. We make sure to repaint our changes by returning true from animate.

Motion for touch screens includes velocity and other complexities I don't want to get into (find a physicist...), but the general idea is similar. Most of the code above relates to the fact that we can move in 4 directions using both the touch screen and the keyboard.

0 comments:

Post a Comment