COMP 310
Spring 2019

Lab04:  Transforming Shapes

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

Since HW03 does not require strategy-based painting, the best option to do this lab is to either branch your current HW03 if it is essentially operational, or branch your HW02 into a new Lab04 project.  

It will be easier to test your affine transform operations if you branch HW02 because only one ball, the affine transform-based painting ball you are working on, will get affected.   All other balls will continue to work properly.   If you branch HW03, since there is only one Ball class, all balls will be affected as your work on your affine transform-based painting.

When you get to HW04, you can simply cut and paste the relevant code from Lab04 into it.  There shouldn't be much code to transfer.


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.


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);   // This is NOT a unit-sized shape and is NOT centered at the origin!
		((Graphics2D) g).fill(ellipse);


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();  // Note that the AffineTransform object can be reused because it is being reset each time.

at.setToTranslation(6.0, 12.0);   // This is just an arbitrary location on the screen so you can see the transformed shape somewhere.   

One can then apply this transformation to a shape:

Shape newShape = at.createTransformedShape(originalShape);   // Now you can use the fill() method of the graphics object to paint the newShape 

Remember that the affine transform translation will simply move the shape by the specified amount; it does not place the image at a specific x-y location.    This will be apparent if the original shape is not situated at the origin.


Adding Shapes to Ballworld

(The following assumes you are working on a branch of HW02.   If you are working on a HW03 branch, then the changes will be made to the Ball class or a subclass of Ball if you desire.)

These steps are a summary of the instructions detailed in the subsequent sections.

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.


Create a Unit-sized Prototype Shape

To be used as a prototype, the Ellipse2D.Double shape needs to be of unit size and centered on the origin.   Careful, remember that the Java library defines an ellipse by upper left-hand corner and full width and height! 

Once your prototype is properly created, you can proceed to set the affine transform to match the current characteristics of your ball and thus enable the image to be painted in a manner that reflects the what one expects to see about the ball.


Translating to match the ball

To test your translating, set your prototype to be centered on the origin, but NOT of unit size.   Make it a size that can be easily seen.

Change the values of your affine transform's translation so your prototype will be transformed to be a shape on top of the ball's current location.    Your new ball should now move and bounce on the screen though it may look a little odd since the painted image's size does not yet match the ball's current size.

Get translations working before proceeding on to scaling and rotating.


Scaling to match the ball

To test scaling, be sure that the prototype is now set to be unit size.

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.

Test scaling by

Get scaling working before proceeding on to rotating.


Rotating to match the ball

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.

Recommended:  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!    (Read:  Put the center of your prototype shape at the origin!)

In order to see if your rotations are working, you will need to use something like an ellipse for your prototype where the major and minor axes are not equal.



How big do you make the ellipse so that it appears to bounce as naturally as possible from the walls?   Try a number of widely varying aspect ratios (width vs. height) for the ellipse and try to come up with well-defined formulat for the "radius" of the "ball" that bounces most naturally for a given aspect ratio.   Drawing pictures of the bouncing ellipse will help a lot here.

If you get this far, continue in lab by trying to get an image to paint.   See Lec 12.   Since you don't have paint strategies yet, just hard-code the initialization of the image file into the constructor of your ball.



© 2019 by Stephen Wong