COMP 310
Fall 2011

Lab04:  Transforming Shapes

Home  Info  Owlspace  Java Resources  Eclipse Resources

Affine Transformations

We learned about the concept of Affine Transformations in lecture.  Today, we are going to use that knowledge to actually transform and draw shapes.

Shape

In homework 1, we created a shape class to allow us to draw shapes on the screen.  This is already a part of Java.  There exists a Shape interface.  There are a few concrete classes that implement the interface, such as Ellipse2D.Double and Rectangle2D.Double.  You can also create an arbitrary shape with a list of points, called a Polygon.

Shapes can be passed to the fill method of a Graphics2D object, which the Graphics object the paintComponent method receives luckily happens to be, and they will be drawn appropriately:

	void paint(Graphics g) {
		Shape ellipse = new Ellipse2D.Double(42, 42, 20.0, 10.0);
		g.setColor(Color.BLUE);
		((Graphics2D) g).fill(ellipse);
	}

AffineTransform

Java also includes an AffineTransform class that can be used to transform shapes.  You can create AffineTransform objects and then set the translation, scale, rotation, and shear to create a transform.  You can then apply the transform to a Shape to create a new Shape.

There are two important things to understand about the AffineTransform class:

  1. The transformations are applied in the reverse order that you add them to the transform.  In other words the last transformation you add will be performed first!
  2. Each type of transformation can be added in two ways, on which begins in "setTo", i.e., "setToTranslation" or "translate".  The "setTo" methods reset the AffineTransform to just have that transformation.  So, you can reuse an AffineTransform object easily by first calling one of the "setTo" methods to install the last transform, then add additional transforms that will be performed earlier.

The following is an example of creating a simple transform that will translate an object by 6 units in the x direction and 12 units in the y direction:

AffineTransform at = new AffineTransform();

at.setToTranslation(6.0, 12.0);

One can then apply this transformation to a shape:

newShape = at.createTransformedShape(originalShape);

Rotation

Given the velocity of our "ball", we need to figure out what angle to rotate our Shape so that it is pointing in the direction that the "ball" is travelling.  Effectively, you have to determine the angle of the velocity vector.  In order to do this, you need to figure out the arctangent of the vector coordinates.  However, the answer is complicated by the fact that the vector could be in one of the four quadrants.  This turns out to be a common operation, so conveniently, Java provides a function for it: Math.atan2(y, x).  The atan2 function returns an angle in radians by properly accounting for the sign of both x and y when calculating the arctangent.  With this rotation angle in hand, the AffineTransform can be set to include a rotation in its transformation.

Note:  AffineTransform has an override of is rotate method, rotate(double vecx, double vecy), that is the equivalent of calling rotate(Math.atan2(vecy,vecx)).    <== Careful, watch the order of the input parameters!

When dealing with rotations, always remember that a rotation is defined about the origin of the coordinate system, not the center of the shape!

Scaling

Scaling will expand or contract the size of an object relative to the origin of the coordinate system.   For instance, if an object is located a distance away from the origin, a scaling operation greater than one will in cause the center of the object to move farther away from the origin as well as increase its size.

 

Adding Shapes to Ballworld

In order to better understand affine transformations and drawing shapes, we are going to add an "EllipseBall" to your Ballworld program.  You can start with any working version of your inheritance-based Ballworld program, but we recommend just continuing to work on the branch you created for last week's lab.  Then you can do the following:

  1. Add another ball class called EllipseBall that is a subclass of ABall.

  2. Your EllipseBall will need to contain both an AffineTransform and a Shape, which you should intialize as an Ellipse2D.Double.

  3. Our ellipses are just going to travel straight, so you can have an empty updateState method.

  4. You will need to override the paint method in order to properly transform and paint your ellipse.  Make sure your ellipse bounces off the walls correctly and always faces in the direction it is travelling.   Think very hard and carefully about what transforms (rotate, translate, scale) you want to perform and in what order you want them applied.   Remember that the AffineTransform adds transformations in reverse order!

When complete, everything should work as before and you should be able to create an EllipseBall that travels straight.  In the next homework, we will use these concepts to enable you to create other shapes that can be combined with your update strategies.

 

 

Command-based Updating

Instead of merely telling the ball to perform an invariant update method (single method to move, paint, bounce, etc) with an invariant parameter (the Graphics object) being passed, the dispatcher sends out a variant command object that the ball invariantly executes.

A command to the ball to do something is defined as such:

package ballworld.model;

/**
 * Interface that represents commands sent through the dispatcher to process the balls
 * 
 */
public abstract interface IBallCmd {
    /**
     * The method run by the ball's update method which is called when the ball is updated by the dispatcher.
     * @param context The ball that is calling this method.   The context under which the command is to be run.
     * @param disp The Dispatcher that sent the command out.
     */	
    public abstract void apply(Ball context, Dispatcher disp);
}

Notice that the command is given a Ball object when it is executed, so that it knows what ball it is working on.

For instance, the dispatcher may send out a command to update the state:

_dispatcher.notifyAll(_updateStateCmd);

All the ball ever does is

 /**
 * The update method called by the main ball Dispatcher to notify all the balls to perform the given command.
 * The given command is executed.
 * @param o The Dispatcher that sent the update request.
 * @param cmd The IBallCmd that will be run.
 */
public void update(Observable o, Object cmd) {
    ((IBallCmd)cmd).apply(this, (Dispatcher) o);
}

With this, a ball can be made to do whatever you want it to do, whenever you want it to do it!

 

Switching Over To Command-based Dispatching

For now, let's just replicate our current behavior but with a command-based dispatching.

  1. Using the constructs above, change the update code of your ball to take an IBallCmd as its input parameter that it then simply "applies".  
  2. Change the code of in the model for the call to the dispatcher's updateAll method.   Use an anonymous inner class to define the IBallCmd to be sent to the balls (Why do we need an anonymous inner class here?)     Where do you think the code that used to be in the ball's update method now resides?

When you are done, your BallWorld system should run identically as before, but with a new door open to even more possibilities!

Did you have to touch your view code at all?   The controller code?   Why or why not?

For the adventurous: By installing a second Timer object, can you decouple the painting process from the updating of the ball state?   In doing so, can you fix the problem of resizing and animated GIFs affecting the behavior of the balls?

 

 

 


© 2011 by Stephen Wong and Scott Rixner