CS21 Lab 6: Graphics, Using Objects

Due Saturday, October 26, before midnight

Goals

The goals for this lab assignment are:

  • Practice using object-oriented programming

  • Become more comfortable using dot notation to call methods of objects

  • Learn how to draw graphics in python

As you write programs, use good programming practices:

  • Use a comment at the top of the file to describe the purpose of the program (see example).

  • All programs should have a main() function (see example).

  • Use variable names that describe the contents of the variables.

  • Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.

  • Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.

  • Avoid writing any lines of code that exceed 80 columns.

    • Always work in a terminal window that is 80 characters wide (resize it to be this wide)

    • In vscode, at the bottom right in the window, there is an indication of both the line and the column of the cursor.

Function Comments

All functions should have a top-level comment! Please see our function example page if you are confused about writing function comments.

You must work in the lab

The graphics library will only work on the lab machines. Therefore, you can not use your own laptop to work on this week’s lab.

If you have any questions or concerns, please let us know.

The eyes have it

In this lab, we will work up to building a program that fills the screen with a grid of eyeballs that follow the mouse around the screen. We’ll build this program incrementally, starting with a simple grid of circles, moving on to drawing eyeballs, and finally making the eyeballs follow the mouse.

In the eyes.py file, we have provided a partial implementation of the main function, and function stubs for the rest of the functions you will need to write to complete the lab. You will need to write implementations of the functions we have provided as stubs and complete main so that it calls the functions you have written in a way that solves the problem presented below.

Here are all the functions (or function stubs) that are in the eyes.py file, along with a brief description. In the starter code we provided, you will find more extensive function comments and a partial implementation of the main function.

def main():
    """ the main function for this lab """

def get_positive_integer(prompt):
    """ gets a positive integer from the user """

def create_circles(window, gridsize, diameter):
    """ creates a size-by-size grid of circles with the given diameter """

def color_circle(circle):
    """ give a circle a random color """

def move_eyes(eyes, point):
    """ move the eyes to follow the point """

The steps below will guide you through an incremental development of the solution.

get_positive_integer

You will start by asking the user to enter the number of circles that will be in each row and column of the grid and the size of each circle:

$ python3 eyes.py 
Enter the grid size: 6
Enter the diameter: 100

You want to make sure that user only enters positive integers for each of values, repeating the question if they don’t answer correctly:

$ python3 eyes.py 
Enter the grid size: hello
Please enter a positive integer.
Enter the grid size: -3
Please enter a positive integer.
Enter the grid size: 0
Please enter a positive integer.
Enter the grid size: 5
Enter the diameter: 2.5
Please enter a positive integer.
Enter the diameter: 100

To do this, you will implement the function get_positive_integer, which you will use to read each of the values above (the grid size and the diameter). You can use isdigit() method to check if a string is comprised entirely of numbers.

def get_positive_integer(prompt):
    """
    This function prompts the user to enter a positive integer.
    If the user enters anything that isn't a positive integer
    (something that isn't all digits or the number 0 which is not positive),
    ask the user to enter the value again. Repeatedly ask the user until
    they enter a positive integer.

    Args:
        prompt (str): The prompt to show the user

    Returns:
        int: The integer typed by the user
    """
    # TODO: Implement this function

    # TODO: Change this line so it returns the int entered by the user
    return 1

Testing the get_positive_integer function

In main, be sure that you can use the get_positive_integer function to read in the grid size and diameter before proceeding.

creating the graphics window

Now that you know the diameter of each circle and number of circles in each row and column, you can create the GraphWin graphics window with the appropriate size. For example, if the user enters 6 for the grid size and 100 for the diameter, you would create a window that is 600 pixels wide and 600 pixels tall.

Try different values for the grid size and diameter to make sure that the size of the window changes as the user enters different values.

(Note: if the user chooses a small grid size or a small diameter, the "Click to exit" message may not fit entirely in the graphics window, but that’s OK.)

create_circles

Now that the graphics window is set up, you will implement the create_circles function, which creates all of the circles in the grid, accumulates them into a list, and draws them all in the graphics window. The function stub looks like this:

def create_circles(window, grid_size, diameter):
    """
    Given a graphics window, the grid size, and the diameter of each
    circle, create and draw circles to fill the window, returning
    the a list of the circles created.

    Args:
        window (GraphWin): the window where the circles will go
        grid_size (int): the number of circles across and down
        diameter (int): the size of each circle

    Returns:
        list: a list of the circles created
    """

This is a complicated function to write! Let’s try to break it down into smaller steps.

Creating a single circle

Let’s start by creating a single circle with the appropriate diameter in the top left corner of the window. Here’s what the window should look like with a grid size of 6 and a diameter of 100:

a single circle in the top left corner of the window

To create a circle, you need to know its center and its radius. Since you know the diameter of the circle, you can compute the radius by dividing the diameter by 2. But where is the center of the circle? Since the circle is against the left side of the window, the center must be exactly one radius away from the left. And since the circle is also against the top of the window, the center must be exactly one radius away from the top. Since the top left corner of the window is at (0, 0), the center of this circle must be at (radius, radius). Create and draw this circle in the graphics window.

Test your code! You should get a picture similar to the one shown above if you use a grid size of 6 and a diameter of 100. Try other grid sizes and diameters, too!

Creating a row of circles

Now that you can create a single circle, you can create a row of circles across the top of the window using a for loop.

a single row of circles across the top of the window

Notice that the center of each circle is diameter pixels to the right of the center of the previous circle. That means the x coordinate of the center of the next circle gets diameter pixels bigger each time you draw another circle whereas the y coordinate stays the same.

Write this for loop and test your code! You should get a picture similar to the one shown above if you use a grid size of 6 and a diameter of 100. Try other grid sizes and diameters, too!

Creating a grid of circles

Now that you can create a row of circles, you can create a grid of circles. To do this, you will need a nested loop. The outer loop will change the x coordinate (as you did above when you created the row of circles).

a grid of circles filling the window

The inner loop will change the y coordinate. Since you are creating a grid with the same number of rows and columns, the y coordinate will move down by diameter pixels each time you move to the next row.

Write this nest for loop and test your code! You should get a picture similar to the one shown above if you use a grid size of 6 and a diameter of 100. Try other grid sizes and diameters, too!

Returning the circles

Now that you have created all the circles, you should return a list of the circles you created. In order to return a list of the circles you created, you will need to accumulate them into a list as you create them.

After your nested loop has created and drawn all the circles, your list should contain all the circles you created. Return this list from the function.

color_circle

Now that we can create a grid of dots, let’s color them with random colors using the color_circle function:

def color_circle(circle):
    """
    Sets the fill and outline of a single circle to a random color.
    This function does not return anything.
    Args:
        circ (Circle): the circle to color
    """
    # TODO: Implement this function

    # This function has no return value

This function takes a single circle as a parameter and sets the fill and outline of this circle to a random color. This function mutates the circle, so there is no return value.

You can implement this function however you’d like, but you must end up choosing each circle’s color randomly.

  1. One option is to create a list of colors that you can choose from and then randomly select the color from that list.

  2. Another option is to use the function color_rgb(r, g, b), which takes 3 integers as parameters: the red, green, and blue components of the color. Each component color value is between 0 and 255 (inclusive). Using the random library, you can choose random values for each component. You can use the return value of color_rgb just like any other color:

>>> color = color_rgb(138, 206, 0)  # brat green
>>> circle.setFill(color)

After you’ve completed this function, add a call to this function inside your create_circles function just before you draw the circle.

Testing color_circle

Below are a few example runs. Since the colors are random, you should get something similar but of course the colors will not match.

$ python3 eyes.py 
Enter the grid size: 6
Enter the diameter: 100
6 by 6 grid of colored circles with radius 100
$ python3 eyes.py 
Enter the grid size: 10
Enter the diameter: 25
10 by 10 grid of colored circles with radius 25

Eyeball replaces Circle

To make this lab more fun, we will turn the circles into eyeballs. An eyeball is just like a circle, but with a pupil in the center. The pupil is a small black circle that you can move around inside the eyeball.

Let’s first modify your code so that it creates eyeballs instead of circles. To do this, find the line in create_circles where you call the Circle constructor. Change the word Circle to Eyeball and re-run your code.

6 by 6 grid of colored circles with radius 100

One of the really nice things about objects is that two objects that have the same interface can be used interchangeably. This means that you can write a function that takes a Circle object as a parameter and then pass an Eyeball object to that function and it will work just fine!

All of the methods that work on a Circle object will work on an Eyeball object, too. However, there are special methods that only work on Eyeball objects. For example, there is a method called move_pupil that only works on Eyeball objects. That makes sense because Circle objects don’t have pupils. We use the move_pupil method in the next steps so that we can make the pupils follow the mouse around the screen.

move_eyes

The move_eyes function takes a list of eyes and a single point on the screen as parameters and moves the pupil of each eye to move so that it is "looking at" that point. This function mutates each Eyeball in the list, so there is no return value.

def move_eyes(eyes, point):
    """
    Move the pupil of each eye so that it looks at the point.
    There is no return value from this function.

    Args:
        eyes (list): a list of Eyeball objects
        point (Point): a point to have each pupil look at

    """

In main, we will make a call to move_eyes using the list of eyes returned by create_circles and a point where the user clicks. You can get a mouse click from the user with the getMouse function in the graphics library. Assuming your graphics window is called window:

click = window.getMouse()   # click will store the Point where the user clicked

To move a pupil, we use the move_pupil(dx, dy) method of the Eyeball class. Given an Eyeball called eye, eye.move_pupil(15, -10) would move its pupil 15 pixels to the right and 10 pixels up. If you have trouble seeing the pupils moving, you can make those values larger. (Notice that if you make the values too large the pupils will come out of the eyeballs!)

Let’s ignore the point parameter right for now and try this out before continuing. Use the move_eyes method to move every pupil some dx and dy value of your choosing and test this by calling move_eyes in main.

Moving the pupil to look at the mouse click

Calculating where to move the pupil so it looks towards a particular point is somewhat challenging, but we’ve provided a method in the Eyeball class that does most of the hard work for you. Given an Eyeball called eye and a mouse click stored in point, calling eye.new_location(point) will return a new Point which is where the center of the pupil should be. You can use that new pupil center to figure out the values of dx and dy that you need for move_pupil. The new_location method will never move the pupil outside of the eyeball - it will always return the location that is closest to point that is still inside the eyeball.

The eye.get_pupil_center() method returns the current center of the pupil. Use that point, and the point you got back from the new_location method to figure out where to move your pupil.

Test this out before proceeding!

putting it all together

Once all of the functions listed above are working, you can tie everything together in your main function. are convinced this is working, add a loop in main that allows the user to click 5 times. After each click, the pupils should follow the mouse pointer. After the fifth click, the program should display a message (such as "Click to exit") in the graphics window and wait for one additional click before closing the window.

This video shows the final program in action:

Requirements

The code you submit for labs is expected to follow good style practices, and to meet one of the course standards, you’ll need to demonstrate good style on six or more of the lab assignments across the semester. To meet the good style expectations, you should:

  • Write a comment that describes the program as a whole using a comment at the top of the file.

  • All programs should have a main() function.

  • Use descriptive variable names.

  • Write code with readable line length, avoiding writing any lines of code that exceed 80 columns.

  • Add comments to explain complex code segments (as applicable).

  • Write a comment for each function (except main) that describes its purpose, parameters and return value.

Your program should meet the following requirements:

  1. Your program should implement the get_positive_integer function with the same number of parameters and behavior described above.

  2. Your program should implement the create_circles function with the same number of parameters and behavior described above. You should also replace the Circle objects with Eyeball objects in this function as described here.

  3. Your program should implement the color_circle function with the same number of parameters and behavior described above.

  4. Your program should implement the move_eyes function with the same number of parameters and behavior described above.

  5. Your final main function should:

    1. prompt the user for the number of circles and the diameter of the circles (validating the input)

    2. open a graphics window with the appropriate size

    3. create a grid of eyeballs with the given diameter

    4. color each eyeball with a random color. (You can do this in the create_circles function or in main.)

    5. allow the user to click 5 times in the graphics window

    6. after each click, move the pupils of the eyeballs to follow the mouse pointer

Autograder

There is no autograder for Lab 6! The autograder doesn’t work well with graphics. You should be sure to test your functions as you go to make sure that they are working correctly and following the specifications provided.

(Optional) Extra challenges

If you’d like to make this more interesting, here are some fun things to try. These are optional, but if you’re interested in trying them, copy your eyes.py file to a new file named eyes_fun.py and add some (or all) of these fun things to your eyes_fun.py file.

  1. Instead of having the user click 5 times, let the user click as many times as they want. However, you’ll still want the user to tell your program that they are done clicking. Add a special location on the screen that the user can click in to make the program end. For example, you can make your graphics window a little taller and add a "Click to exit" message at the bottom of the window. When the user clicks on that message, the program should end. You’ll have to figure out how to detect when the user clicks on that message.

  1. Instead of creating a grid of eyeballs, allow the user to click in the window to create a new eyeball at that location. Accumulate all the eyeballs in a list. Either create a fixed number of eyeballs (e.g. 8 eyes) or allow the user to click on a message to stop creating new eyeballs. After the user stops creating new eyeballs, allow the user to click to move the pupils of the eyeballs to follow the mouse pointer.

  1. Instead of placing the eyeballs on a blank background, load an image as your background and place the eyeballs on top of that image. Read the documentation for the Image class to learn how to load an image and place it in your graphics window. You can use any image you’d like, but it must be saved in .gif format. The image used in the video below is located on the CS lab machines here: /data/cs21/Ernest_Kilbourne_and_Family.gif (source).

Answer the Questionnaire

After each lab, please complete the short Google Forms questionnaire. Please select the right lab number (Lab 06) from the dropdown menu on the first question.

Once you’re done with that, you should run handin21 again.

Submitting lab assignments

Remember to run handin21 to turn in your lab files! You may run handin21 as many times as you want. Each time it will turn in any new work. We recommend running handin21 after you complete each program or after you complete significant work on any one program.

Logging out

When you’re done working in the lab, you should log out of the computer you’re using.

First quit any applications you are running, including your vscode editor, the browser and the terminal. Then click on the logout icon (logout icon or other logout icon) and choose "log out".

If you plan to leave the lab for just a few minutes, you do not need to log out. It is, however, a good idea to lock your machine while you are gone. You can lock your screen by clicking on the lock xlock icon. PLEASE do not leave a session locked for a long period of time. Power may go out, someone might reboot the machine, etc. You don’t want to lose any work!