Animated images


With event-driven animation and object-oriented programming in our tool chest now, let's ramp up and animate multiple objects and use them to play around with images.

All the code files for today: AnimatedImage.java; BlobsDriver.java; BlobsGUI.java; BlobsGUI2.java; PaintedImage.java; SmileGUI.java; WanderingImage.java; WanderingPixel.java

Slides from class

Multiple blobs: lists

It's not a big jump to move from handling a single blob to handling a bunch of them. Java has different ways to handle multiple objects; we'll start with an ArrayList, which is a variable length, random-access representation. The name is confusing, but think of it as just a list (it happens to be implemented via an array, as we'll see in a week). It's a lot like a list in Python. It's different from an array in C or Matlab in that it can grow, and further different from Matlab in that it's 0-indexed rather than 1-indexed (the first element is number 0).

ArrayList, like most classes in Java, has to be imported; it comes from the library java.util. It's just a list of objects (i.e., in order), with methods to add and remove elements. Notably, we can specify what types of objects are in the list, by putting the class name in angle brackets < >; e.g., ArrayList<Blob>. This is called a generic container. The container can hold anything, but we have to specify what kind of anything. Maybe it's a container of Blobs, or of Strings, etc. Knowing what kind of thing it contains leads to safety — we can only put that kind of thing in, and know that whatever we get out is that kind of thing.

The Oracle API docs detail the methods provided by ArrayList; some of the most important are as follows (where "E" indicates the type of thing, in an ArrayList<E>):

For example (BlobsDriver.java):

ArrayList<Blob> blobs = new ArrayList<Blob>();
blobs.add(new Wanderer(10,10));
blobs.add(new Bouncer(20,30,800,600));
blobs.get(0).step(); // => the wanderer
blobs.get(1).step(); // => the bouncer
blobs.size(); // => 2

The golden rule of inheritance is at work here again: due to the is-a nature of the classes, they can live happily together in the same list. And the list is still safe, in that it guarantees it only has blobs, so when we get something from it, we can invoke its step method.

Now we'll set up a GUI for handling a bunch of blobs: BlobsGUI.java. It's a lot like the one for a single blob, but now the instance variable keep all of them in an ArrayList<Blob>. The list is created in the constructor, but initially empty. As always, don't forget to new; else it's not just an empty list, but a non-existent list.

The draw method needs to draw all the blobs in the list. There's a new type of loop here, the "for-each", which iterates over all the objects in a container (like "for-in" in Python). Note that we still have to declare the loop variable's type.

for (Blob blob : blobs) {
  blob.draw(g);
}

Here we're saying "for each blob (of type Blob) in my list, ask it to draw itself". The same thing goes on in handling the timer, asking each blob to take a step.

The mouse press method is modified to either find if some blob contains the mouse press location, or else add a new blob of the appropriate type to the list.

Images

Java lets us load images from files (e.g., jpg or png format) and "draw" them much like we've been drawing ovals. The DrawingGUI class provides a loadImage method to simplify the process of reading an image from a file and creating an instance of class BufferedImage that allows us to manipulate it. See SmileGUI.java. To run this example yourself, save smiley.png in your project, within a folder named "pictures". Here's a screenshot.

The image is loaded in during the main method, and passed to the constructor of the SmileGUI. The draw method then puts the image on the graphics, at coordinate (0,0) (the "null" parameter to "drawImage" is for something we just don't care about).

With that machinery in place, we could also draw a blob as an image instead of an oval: WanderingImage.java. The GUI needs to be modified accordingly, to load in the image and pass it to the constructor: BlobsGUI2.java. You can substitute a more blob-like image by placing it in the correct folder and changing the name of the file being loaded.

Animated images

The following code uses baker.jpg (800x600) and baker-200-150.jpg (a 200x150 version), based on a public domain image in the Dartmouth College category of Wikimedia commons. Save these into your project/pictures folder as with the smiley face.

Now let's actually have the blobs be the image. At the lowest level, images (as well as computer screens) are made up of discrete rectangles called "pixels". If you zoom way in to a picture with an image editor, you can see that. An image of size 800x600 has 800 pixels across by 600 down. And note that the typical graphics coordinate system has the y coordinate increasing from 0 at the top.

012...799
0
1
2
...
599

The BufferedImage class provides getRGB and setRGB to access the pixel value at a coordinate. The value is represented in "RGB" (red, green, blue) color space. We'll decompose that later. For now, we just take that RGB value and create a Color object that we can use with Java graphics. Thus to draw an oval filled with the color for the pixel at (x,y):

Color color = new Color(image.getRGB(x, y));
...
g.setColor(color);
g.fillOval(...);

To keep track of and use a Color, we make yet another Blob subclass, WanderingPixel.java, with an appropriate draw method. Then a GUI AnimatedImage.java loads in an image, creates a bunch of animated pixels, and moves them around. The key differences from our previous BlobsGUI: we create a random number of blobs in the constructor, each at a random position and picking up the color there; in the timer, we only move a random subset of them. Recall that Math.random returns a number between 0 and 1, so we multiply it by the width to get a possible x coordinate, and similarly by the width for y, and by the number of blobs to get the index of a blob. One other thing is that I downsampled the image to be smaller, and then make each of the blobs bigger (radius) to compensate. So a wandering pixel get its colors from (x,y) but is placed at (x*radius, y*radius).

Another way to have blobs interact with an image is for them to trace it out, PaintedImage.java. As they move around, they leave a trail, copying over the colors from the underlying image (okay, so they're changing brushing really quickly, but they are powerful blobs). To do this, we need a result image that starts off blank and is filled in with colors as the blobs move around. The draw method shows this image, while the timer handling copies the colors over. The critical pieces:

original = loadImage(...);
result = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
...
// Pick a random blob, leave a trail where it is, and ask it to move.
Blob blob = blobs.get((int)(Math.random()*blobs.size()));
int x = (int)blob.getX(), y = (int)blob.getY();
// Careful to stay within the image  
if (x>=0 && x<width && y>=0 && y<height) {
  result.setRGB(x, y, original.getRGB(x, y));				
}
blob.step();

Note that a blob can wander outside the width/height of the image, so we have to be careful not to try to do anything with such a blob. Warning: you might encounter something like this in PS-1!

It's set up to work with wanderers, but is generic — switch to bouncers or a custom one you dream up.

Java notes

generics
When a container object doesn't really care what type of thing it holds, that type can be given as a parameter in angle brackets, e.g., ArrayList<Blob>. Then only objects of that type (Blob) are in the container, and when we get one, we know its type. You might come across older Java code (pre-generics) that doesn't specify what's in a container, and the object has to be cast upon getting it. That's considered dangerous.
variable scope
Local variables declared inside a method are only there during that method invocation. In fact, they're only accessible within their containing curly braces. Make good use of local variables to contain temporary information — don't put something as an instance variable unless it needs to persist with the object.
for-each
A special type of loop iterates over members of a collection: for (member : collection) { }.
for loop
Java uses a C-style for-loop for iteratively repeating some statements. The basic structure is for (initialization; condition; step) { statements; }. The initialization is done once, before the loop starts. Then repeat: test the condition, and if it's true, do the statements, then do the step. (And then go back and test the condition again...) If the condition is false, the loop terminates. A common pattern is to use a "loop variable" that is declared and initialized, and then incremented each time through the loop: for (int i=0; i<num; i++) { ... }.