# CS 151: Lab 5

Project 5
Sprint 2020

### Lab Exercise 5: Object Collections

For this project we will be creating animated scenes. Scenes will be a list of of complex objects, and each complex objects will be a list of simple graphics objects.

We will continue to use the Zelle graphics module, graphicsPlus.py. The graphicsPlus package contains two functions not in the Zelle documentation: getOutline() and getFill(). These methods return the outline and fill colors, respectively, in case you need to access those values. For example, you may want to query an object as to its current color, then change the color for a period of time before having it revert to its original color.

1. Setup

Create a new project5 directory for today's lab, download the graphicsPlus.py file, start up a terminal and cd to your working directory. Open a new python file and call it motion.py. Start by importing the graphics, time, and random packages.

```import time
import random
import graphicsPlus as gr```
2. Define a complex object

We're going to continue to follow the design from last week, where to create a complex object we define an init function that creates the shapes, colors them, and returns them as a list. Define the following function.

`def saucer_init(x, y, scale)` - this function should create a flying saucer from a circle and two ovals and return a list contining the basic shapes.

• Assign to light a Circle object at (x, y-20*scale) with radius 4*scale. Then set its color to (220, 100, 110).
• Assign to body an Oval object with corners at (x-30*scale, y-20*scale) and (x+30*scale, y+20*scale). Set its color to (220, 210, 220).
• Assign to saucer an Oval object with corners at (x-60*scale, y-5*scale) and (x+60*scale, x+5*scale). Set its color to (240, 230, 190).
• Return a list containing light, body, and saucer, in that order.

```# Test code for the flying saucer
def main():

# make a window, add a 4th argument False, which halts automatic updating
win = gr.GraphWin( "Saucer", 400, 400, False )

# create the saucer shape list
saucer = saucer_init(200, 200, 2)

# draw each item in the saucer shape list
for item in saucer:
item.draw(win)

# update the window (draw things) and wait for a mouse click
win.update()
win.getMouse()
win.close()

if __name__ == "__main__":
main()```

Run your motion.py program, which should draw a flying saucer.

3. Make the light change colors

Create a new function `def saucer_animate( shapes, frame_num, win ):`. The first argument to the function, shapes, will be the list of graphics objects making up the saucer. The second argument, frame_num, will be the number of the frame in the animation. The final argument, win, will be the GraphWin window into which the saucer has been drawn.

In saucer_animate, if the frame_num % 20 is equal to 0, set the fill color of the light (the first element in shapes list) to blue, else if the frame_num % 20 is equal to 10, set the light's color to red.

Using the modulo operator is a good method of turning an increasing sequence of numbers into a cyclic series of numbers. In this case, frame_num % 20 will cycle from 0 to 19 as the frame_num value continually increments.

Back in your main function, immediately before the win.update(), begin a for loop that runs 100 times. Inside the loop, call the saucer_animate function, passing in the saucer shape list (saucer), the loop variable, and the GraphWin variable (win). Finish the for loop with calls to win.update() and time.sleep(0.1). The time.sleep(0.1) tells the loop to pause for a tenth of a second, meaning that ten loops is roughly one second of animation time.

If you run your program, the light should flash colors at about a 1s interval.

4. Let the user stop the animation

The GraphWin methods checkMouse() and checkKey() let you see if the user has provided input without halting your program. The checkMouse() method returns a Point object if the user has clicked and None otherwise. The checkKey() method returns a string if the user has typed something, otherwise None.

In the main function, at the start of the main for loop, test if win.checkMouse() is not equal to None. If that is true, then break. After the check for a mouse click, check for a key click. Assign to key the result of calling win.checkKey(). If key is equal to 'q', then break.

To test your code, see if clicking on the window or typing 'q' causes the animation to stop. If you left the original getMouse() at the end of the main function, click a second time to close the window.

5. Write utility functions for complex shapes

If all of your complex objects have the same structure--they are all lists of primitive objects--then we should be able to write some functions to draw them, move them, undraw them, and clone them. Each of these functions needs to loop over the elements in the object list and call the appropriate methods.

To encapsulate this functionality, at the top of your motion.py file create functions that will draw, move, and undraw a list of shapes. The skeletons are below.

```def draw( shapes, win ):
""" Draw all of the objects in shapes in the window (win) """
# for each thing in shapes
# call the draw method on thing with win as the argument

def move( shapes, dx, dy ):
""" Draw all of the objects in shapes by dx in the x-direction
and dy in the y-direction """
# for each item in shapes
# call the move method on item with dx and dy as arguments

def undraw( shapes ):
""" Undraw all of the objects in shapes """
# for each thing in shapes
# call the undraw method on thing```

When you are done creating these functions, go back to the main function and replace the for loop that calls thing.draw() (both lines of code) with a single call to draw( saucer, win ). Test it.

At the start of the saucer_animate function, call move( shapes, 0, -1). Test your code. What does the saucer do?

6. Control the suacer's motion

In saucer_animate, condition the saucer's movement so it moves up (-1) if frame_num is less than 50, and moves down (+1) otherwise. Test your changes.

Can you make the saucer move up, then move down, them move left, and the move right? Try it.

7. Give the flying saucer a beam of light

Let's give the flying saucer a beam of light as it is moving down that comes on and then turns off. To do this, we need to pick which frames should trigger each action and figure out how to create and remove graphics elements.

In the saucer_animate function, begin an if statement that tests if frame_num is equal to 70. Inside the if statement body, do the following.

• Assign to mid the center point of the light, which is the first item in the shapes list. Use the method getCenter().
For example: mid = shapes[0].getCenter()
• Assign to scale the radius divided by 4. Look at the saucer_init method to see why this works.
• Assign to beam a polygon with the points (mid.getX() - scale*4, mid.getY() + scale*40), (mid.getX() + scale*4, mid.getY() + scale*40), (mid.getX() + scale*15, mid.getY() + scale*110), (mid.getX() - scale*15, mid.getY() + scale*110).
• Set the fill color of the polygon to (255, 255, 180).
• Draw the beam into the window.
• Append the beam to the shapes list.

At frame 90, we want to turn off the beam by undrawing the polygon, which is the last item in the shapes list. Begin an if statement that is true when frame_num is equal to 90.

• Assign to beam the result of calling shapes.pop(), which removes the last element of the shapes list and returns it.
• Call the undraw method of the beam object.

Test the new animation and see if the beam turns on and off.

8. Create multiple saucers
• Call saucer_init three times, assigning the return lists to three different variables (e.g. sc1, sc2, sc3).
• Place the saucers in three different locations, giving the first a scale factor of 0.5, the second 0.75, and third 1.
• Make a list scene that contains sc1, sc2, and sc3.
• Loop over the scene list and call the draw utility function you created previously with each sublist.
• Inside the time loop, call the saucer_animate function for each of the saucers.