teaching machines

Homework 4 – Wireframe

Your objective in this homework is to acquaint yourself with conditional statements and loops, which enable you to write code that diverges and repeats. You will do this in the context of writing an application that produces a GIF of animated wireframe objects.

This assignment is more involved and less easy to test in small chunks than your previous assignments. Plan accordingly.

Wireframes

The essential task in this homework is to read or parse an animation described in a text file, draw each frame of the animation into an image, and collect up the images into an animated GIF. Let’s work through an example Wireframes file and see what animation it produces before we get into the details of the specification.

Here’s a modest Wireframes file that demonstrates all the functionality that your final program will support:

resolution 120 100
delay 1000
loop true

frame
background 230 230 230

object
color 0 128 255
dilation 2
vertex 20 10
vertex 20 90
vertex 100 90
vertex 100 10
endobject

object
color 255 0 255
dilation 5
vertex 40 25
vertex 40 45
vertex 50 45
vertex 50 25
endobject

object
color 255 0 255
dilation 5
vertex 70 25
vertex 70 45
vertex 80 45
vertex 80 25
endobject

object
color 255 128 0
dilation 2
vertex 50 70
vertex 70 70
endobject

endframe

frame
background 230 230 230

object
color 0 128 255
dilation 2
vertex 20 10
vertex 20 90
vertex 100 90
vertex 100 10
endobject

object
color 255 0 255
dilation 5
vertex 40 25
vertex 40 45
vertex 50 45
vertex 50 25
endobject

object
color 128 0 255
dilation 5
vertex 70 35
vertex 80 35
endobject

object
color 255 128 0
dilation 2
vertex 50 70
vertex 73 65
endobject

endframe

Let’s break this down. Consider the first few lines:

resolution 120 100
delay 1000
loop true

From these lines we gather that the animation will be rendered into a GIF that is 120 pixels wide and 100 pixels tall, there will be a delay of 1000 milliseconds between frames, and the animation will loop.

Next in the file is the first frame sandwiched between frame and endframe:

frame
background 230 230 230

object ...

endframe

This first frame has a light gray background. Within the frame are plotted four objects. Let’s examine only the first:

object
color 0 128 255
dilation 2
vertex 20 10
vertex 20 90
vertex 100 90
vertex 100 10
endobject

This object is described with four vertices, which happen to form a square. It is plotted in bluish lines that have been thickened or dilated twice. Though its impossible to determine its semantic meaning through the text alone, this object is a head. In general, objects have one or more vertices and can take on any shape. But only its perimeter, or wireframe, is plotted. The second and third objects are eyes, and the fourth a mouth. Altogether the frame looks like this:

To produce a coherent animation, the second frame retains all of the objects but changes some of them. The head and one of the eyes stay the same, but the other eye winks as the mouth is drawn upward. The two-frame sequence produces this animated GIF:

Helper Classes

There are several classes that will aid you on your journey. Ideally, you should skim their documentation and investigate their usage in books and tutorials. Here we provide just a brief overview of some of their relevant methods.

BufferedImage

The BufferedImage class manages pixel-based images. You can make a new image with this construction:

BufferedImage image = new BufferedImage(480, 320, BufferedImage.TYPE_INT_RGB);

This image has a resolution of 480×320. Each pixel will hold an RGB color. To copy the pixel at column c, row r to its right neighbor, we’d write:

int rgb = image.getRGB(c, r);
image.setRGB(c + 1, r, rgb);

This code will throw an exception if you reach beyond the dimensions of the image.

ImageIO

To test your code in small chunks, it will be helpful to read and write BufferedImages to disk, as we do here:

try {
  BufferedImage image = ImageIO.read(new File("/Users/foobag/Desktop/image.png"));
  ImageIO.write(image, "png", new File("/Users/foobag/Desktop/image.png"));
} catch (Exception e) {
  throw RuntimeException(e);
}

You’ll need to edit the paths to point to your actual destination directory.

When we deal with files, we are likely to encounter FileNotFoundException or IOException. Java forces us to acknowledge these exceptions, and we deal with them here by turning them into a RuntimeException, which lets them bubble up and halt our program.

GifSequenceWriter

Java by itself can’t produce animated GIFs, but a GifSequenceWriter class that can do so has been bundled into the hw4.specchecker package. You can create one like so:

GifSequenceWriter gif = new GifSequenceWriter(new File("/Users/foobag/Desktop/movie.gif"));
gif.setDelay(100);
gif.setLooping(true);
gif.appendFrame(image0);
gif.appendFrame(image1);
gif.appendFrame(image2);
gif.close();

Okay, now we’re ready for the detailed specification!

Requirements

Complete the classes described below. Place all classes in package hw4. Make all methods static.

Main

Write class Main with a main method, which you are encouraged to use to test your code. Nothing in particular is required of it, but it must exist.

DrawingUtilities

Write class DrawingUtilities with the following methods:

  • Method lerp that linearly interpolates between two values by some proportion. It accepts three parameters in the following order:
    • a from value of type double
    • a to value of type double
    • a proportion value of type double
    It returns the interpolated value as a double. Calculate the interpolated value based on the pattern you see in these examples:
    • lerp(0, 100, 0.8) → 80 because the total jump between from and to is 100 and we want to move 80% of that jump away from from
    • lerp(80, 90, 0.4) → 94 because the total jump between from and to is 10 and we want to apply 40% of that jump away from from
  • Method lerp that linearly interpolates between two 2D points by some proportion. It accepts three parameters in the following order:
    • a from value of type Point
    • a to value of type Point
    • a proportion value of type double
    It returns the interpolated Point, whose coordinates are calculated by interpolating the x- and y-coordinates of the two points separately and rounding to the nearest int.
  • Method chessboardDistance that calculates the number of steps it takes a king to move from one point to another according to the rules of chess. It accepts two parameters in the following order:
    • a from position of type Point
    • a to position of type Point
    Consider this movement of a king: Here the king requires 5 moves to get between the squares. Because kings can move diagonally, a non-straight path with the same horizontal span can be traveled in just as many steps: But look what happens when the path gets taller than it is wide: This taller-than-wide path requires 7 steps, which is just as many steps as are required this vertically straight path:

    To calculate the chessboard distance, then, we consider just the lengths of the two possible straightened versions of the path. The length of the vertical version is the absolute difference between the y-coordinates. The length of the horizontal version is the absolute difference between the x-coordinates. Whichever is longer is the chessboard distance.

  • Method line that plots a colored line between two points on an image. It accepts four parameters in the following order:
    • an image of type BufferedImage
    • a color of type int
    • a starting pixel of type Point
    • an ending pixel of type Point
    There are many published algorithms for drawing lines. Do not use them, nor any builtin line-drawing methods from the Java library. Instead, implement this pseudocode:
    n = calculate chessboard distance between points
    for each chessboard step
      calculate proportion of steps taken
      lerp between points
      fill lerped pixel with color
    For the loop, consider step 0 (proportion 0) to be at the starting pixel and step n (proportion 1) to be at the ending pixel. Inclusively visit these pixels.

ImageUtilities

Write class ImageUtilities with the following methods:

  • Method clone that creates an independent clone of an image. It accepts a BufferedImage as a parameter, creates a new image of the same size and type, copies over the pixel data, and returns the new image.
  • Method combine that places a transparent overlay on top of a base image. It accepts two parameters in the following order:
    • A base image of type BufferedImage
    • A layer of type BufferedImage
    The images are assumed to be of the same resolution. Each non-transparent pixel of the layer replaces the corresponding pixel in the base image. Thus the base image is changed by this method, but not the layer image.
  • Method hasNeighbor that determines if a pixel has a neighbor of a certain color. It accepts four parameters in the following order:
    • an image of type BufferedImage
    • a column index of type int
    • a row index of type int
    • a color of type int
    It returns true if any of the eight pixels surrounding the pixel specified by the row and column are of the specified color. This can be implemented with a compound logical expression. Note that pixels on the edge won’t have all eight neighbors; be sure to guard against invalid coordinates with short-circuiting.
  • Method createTransparentLayer that creates a completely transparent image. It accepts as its sole parameter a source image of type BufferedImage. It returns a new BufferedImage of the same resolution as the source image, but its type is TYPE_INT_ARGB. A is short for alpha, which is synonomous to opacity in the computer graphics world. Opacity determines how much of the color behind the pixel can be seen. A pixel with opacity 0 is completely transparent. One with opacity 1 blocks all light coming from behind. This method sets all pixels to color 0—no red, no green, no blue, and no opacity.
  • Method dilate that adds a border around pixels of a certain color. It accepts two parameters in the following order:
    • an image of type BufferedImage
    • a color of type int
    It first creates a clone of the image. Then it visits each pixel in the clone. If any of a pixel’s eight neighbors is the specified color, set the corresponding pixel in the clone to that color. This effectively thickens the colored region. It returns the clone.

Wireframe

Write class Wireframe that handles the parsing of an animation file. Its methods are listed below from independent to dependent order. The last method in the list is what depends on all those above it. You may want to read the list from bottom to top, but we recommend you implement them from top to bottom, testing each one independently to minimize pain and suffering.

  • Method parseColor that reads in the red, green, and blue intensities of an RGB color. It accepts a Scanner parameter, whose next readable chunks of text—or tokens—are the three intensities, each in [0, 255]. The three numbers are returned together as an int with the following byte layout:
    byte 3 byte 2 byte 1 byte 0
    opacity red green blue
    Packing these three numbers together is a common practice to save on storage. The Color class provides a pathway for assembling these numbers together.
  • Method parseBackground that reads in an RGB color and fills an image with that color. It accepts two parameters in the following order:
    • a Scanner whose next tokens are the three RGB intensities
    • an image of type BufferedImage
    It sets every single pixel of the image to the parsed color.
  • Method parseVertex that reads in a two-dimensional coordinate. It accepts a Scanner parameter, whose next tokens are the x- and y-coordinates. It returns the coordinates as a Point.
  • Method parseObject that reads in a single object definition and draws the object. It accepts two parameters in the following order:
    • a Scanner whose next tokens define the object
    • a BufferedImage in which to draw the object
    It reads and responds to one or more commands, stopping when it sees the command endobject. The command color sets the object’s color. The command dilation sets the objects thickness. The command vertex plots a segment of the object’s outline from the previous vertex to this new vertex—unless this is the first vertex. The segment is colored according to the color command. After all commands have been read, the object is closed by connecting a line from the final vertex back to the first. The object’s outline is then dilated the number of times defined by a dilation command. For example, suppose the Scanner is set to read the following object:
    color 255 0 0
    dilation 1
    vertex 10 10
    vertex 10 90
    vertex 90 50
    endobject
    When plotted on a transparent layer, the resulting image looks like this, with the transparent pixels shown in the checkerboard pattern: Do not assume the color, dilation, and vertex commands are in any particular order. You are encouraged to use the following loop structure:
    read command
    while not finished
      if command is option 1
        process option 1
      else if command is option 2
        process option 2
      and so on
      read next command
  • Method parseFrame that reads in a single frame definition. It accepts two parameters in the following order:
    • a Scanner , whose next tokens define the frame
    • a base image of type BufferedImage on which the object layers are combined
    It reads and responds to one or more commands, stopping when it sees the command endframe. The command background fills the image with the specified background color. The command object plots an object to a transparent layer that has the same resolution as the base image. Each object’s layer is combined with the base image. Because each object is drawn onto a transparent image, combining will not erase any previously plotted objects. Follow a loop structure similar to that recommended for parseObject. For example, suppose the Scanner is set to read the following frame:
    background 0 255 255
    
    object
    color 255 128 0
    dilation 2
    vertex 0 0
    vertex 10 10
    vertex 0 10
    endobject
    
    object
    color 0 128 255
    dilation 2
    vertex 0 0
    vertex 10 10
    vertex 0 10
    endobject
    
    endframe
  • Method parseWire that reads in an entire animation file and renders it into an animated GIF. It accepts two parameters in the following order:
    • an animation file of type File
    • a collector of animation frames of type GifSequenceWriter
    It parses the text of the file—using a Scanner so it can hand off work to the helper methods described above. It reads and responds to one or more commands, stopping when there are no more tokens. The command resolution sets the animation’s width and height. The command delay sets the inter-frame delay of the animation. The command loop sets whether or not the animation repeats. The command frame defines a single frame of the animation. Before parsing the frame, it creates a new BufferedImage of the resolution specified by a previous resolution command and with pixel format TYPE_INT_RGB. This is the image into which the frame is rendered. After the frame is complete, it appends the image to the GifSequenceWriter. Follow a loop structure similar to that recommended for parseObject.
  • Method wireToGif that converts a wireframe animation file into an animated GIF. It accepts two parameters in the following order:
    • a source animation file of type File
    • a destination GIF file of type File
    It creates an GIFSequenceWriter for the destination file and parses the source file. To commit the GIF to disk, you must close the GIFSequenceWriter.

Extra

For an extra credit participation point, compose your own animation and share it on Piazza under folder ec4 by the due date. To be eligible, your animation shouldn’t look like it was scrapped together in as little time and creative effort as possible. The submission garnering the most votes will be honored in some way.

Submission

To check your work and submit it for grading:

  1. Run the SpecChecker by selecting hw4 SpecChecker from the run configurations dropdown in IntelliJ IDEA and clicking the run button.
  2. Fix problems until all tests pass.
  3. Commit and push your work to your repository.
  4. Verify on Gitlab that your submission uploaded successfully.

A passing SpecChecker does not guarantee you credit. Your grade is conditioned on a few things:

  • You must meet the requirements described above. The SpecChecker checks some of them, but not all.
  • You must not plagiarize. Write your own code. Talk about code with your classmates. Ask questions of your instructor or TA. Do not look at others’ code. Do not ask questions specific to your homework anywhere online but Piazza. Your instructor employs a vast repertoire of tools to sniff out academic dishonesty, including: drones, CS 1 moles, and a piece of software called MOSS that rigorously compares your code to every other submission. You don’t want to live in a world serviced by those who achieved their credentials by questionable means. For your future self, career, and family, do your own work.
  • Your code must be submitted correctly and on time. Machine and project issues are common—anticipate them. Commit early and often to Git. Extensions will not be granted. If you need more time to make things work, start earlier.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *