Today: Canvas drawing, draw functions, drawing a grid, function black-box if we have time
Download draw1 Example Project
draw1.zip
Expand the .zip to get a "draw1" folder with .py files in it. Run PyCharm. Use Open... menu to open the ***folder***
Then use the "Terminal" tab at lower left to run by command line as shown below. Command line is the "pro" way to control the computer.
For more detail see guide: Command Line.
Parameters - Look Again
- Look in more detail at how parameters work for a function
- Each parameter can be seen from 2 sides - caller and def
1. bit.paint('blue')
- This is from the "caller" side
- Calling the
paint
function
- Passing in the value
'blue'
as the parameter
- Here we are dictating the value, telling paint() how it's going to be
2. def draw_oval(canvas, x, y, width, height):
- This is the "def" side of the function
- Here writing the code for a draw_ovals() function
- "implementing" this function
- What does
x
and y
mean in this context?
- Here we are taking in
x
and y
- somebody else put values in there
- We just obey whatever values are in there
- We'll work this from both sides in today's example
Drawing on a Canvas
- Have manipulated pixels, each RGB
- Today: draw lines, rectangles, ovals on canvas
- Every computer system has a "canvas" facility
- noun.verb functions on the canvas to draw on it
canvas.draw_line(...)
canvas.fill_rect(...)
canvas upper left is (0, 0)
- Same coordinate system as image pixels
- x grows right, y grows down
Draw Canvas Functions
We'll use the CS106A "DrawCanvas" which supports these functions:
def draw_line(x1, y1, x2, y2):
"""
Draws a black line between points x1,y1 and x2,y2
Optional color='red' parameter can specify a color.
"""
def draw_rect(x, y, width, height):
"""
Draws a 1 pixel rectangle frame with its upper left at x,y
and covering width, height pixels.
Takes optional color='red' parameter.
"""
def fill_rect( x, y, width, height):
"""
Draws a solid black rectangle with its upper left at x,y
and covering width, height pixels.
Takes optional color='red' parameter.
"""
def draw_oval(x, y, width, height):
"""
Draws a 1 pixel oval frame with its upper left bounding rect at x,y
and covering width, height pixels.
Takes optional color='red' parameter.
"""
def fill_oval(x, y, width, height):
"""
Draws a solid black oval with its upper left bounding rect at x,y
and covering width, height pixels.
Takes optional color='red' parameter.
"""
def draw_string(x, y, text):
"""
Draws a black text string with its upper left at x,y
Takes optional color='red' parameter.
"""
Calling Canvas Functions
These are noun.verb functions on a Canvas like this:
# Create a 500x300 canvas, draw a red line on it
canvas = DrawCanvas(500, 300)
canvas.draw_line(0, 0, 100, 100, color='red')
...
Example (a) draw_oval()
The "oval" figure has a black rectangle at its outer boundary. Then a yellow oval inset by 20 pixels on 4 sides. Then 2 red lines making a big X. Running the program draws 2 copies of the figure - one upper-left, one lower-right, which helps with testing.
Thinking about math to position the oval:
def draw_oval(canvas, x, y, width, height):
"""
Given a canvas and x,y and width,height
Draw the "oval" figure inside these bounds.
(this code is complete)
"""
# Draw outer rectangle
canvas.draw_rect(x, y, width, height)
# Draw oval set in by 20 pixels all around
canvas.fill_oval(x + 20, y + 20, width - 40, height - 40, color='yellow')
# Draw red lines
# upper-left to lower-right
canvas.draw_line(x, y, x + width - 1, y + height - 1, color='red')
# lower-left to upper-right
canvas.draw_line(x, y + height - 1, x + width - 1, y, color='red')
# Note: better style would be to have a MARGIN constant = 20, and the oval line
# is as below. We used 20 above to keep the example simple. We'll use constants
# like this on a later homework.
# canvas.fill_oval(x + MARGIN, y + MARGIN, width - 2 * MARGIN, height - 2 * MARGIN, color='yellow')
- Params: here
x y width height
are passed in, use them
- We pass them on, sometimes unchanged, to draw_rect() etc.
- Code in here is complete
- Upper left is x,y
- Drawing should extend to cover width,height
- Draw 3 things:
- 1. Draw rect at boundary
- 2. Draw a yellow oval
Math to give 20 pixel marge: add to x and y. Subtract 40 from width height.
Better style: code uses MARGIN constant
- 3. Draw a red "x" - connecting corners
What are the left, right, top, bottom corner Coordinates?
- Given x,y and width,height...
- leftmost pixel coord is x
- rightmost pixel is x + width - 1
- topmost pixel is y
- bottommost pixel is y + height - 1
Run From Command Line
The main() in this case calls draw_oval() twice, once upper left and once lower right. This tests that the x,y are handled correctly. Open the "terminal" at the lower left of PyCharm. The "$" below is the prompt, the rest you type (on Windows its "python" or "py" instead of "python3".) Close the drawing window when done, this exits the program.
$ python3 draw1.py -oval 300 200
Use The Up Arrow
In the terminal, hit the up arrow. Edit the old command and run it again easily. This is a very productive and fast way to run and vary your program.
Accessibility Screen Zoom In
- "Accessibility" features: help GUI work for people with reduced vision etc.
- Can use the accessibility feature of OS to zoom in and see the exact pixels
- On the Mac: Preferences > Accessibility > Zoom, default keystroke is cmd-option-8 to turn on/off
- On Windows 10: Windows-key and + turns on. Windows-key esc turns off.
Example (b) draw_lines1()
- Lines1 figure
- Given int n, at least 2
- Draw N lines, proportionately spread out
- All start at upper left
- First line goes to upper right corner
- Last line goes to lower right corner
- Rest of lines spread evenly
How To Spread Points Proportionately
- Range loop i:
for i in range(n):
i ranges over 0..n-1
n-1 is the max value seen for i
- Think about where lines end on the right side, at y + y_add, y_add varies per line
- Range of y_add:
At start: 0
At end: height - 1 (call this "max")
- Recipe: (faction 0..1.0) * max
- Fraction is: (i / max_value_of_i)
e.g. i / (n - 1)
- y_add = (i / (n - 1)) * (height - 1)
for i in range(n):
y_add = (i / (n - 1)) * (height - 1)
draw_lines1() Solution
def draw_lines1(canvas, x, y, width, height, n):
"""
Draw the lines1 figure within x,y width,height
"""
canvas.draw_rect(x, y, width, height)
# Figure y_add for each i in the loop
for i in range(n):
y_add = (i / (n - 1)) * (height - 1) # formula: 0..1 fraction * max
canvas.draw_line(x, y, x + width - 1, y + y_add, color='red')
Run from command line
$ python3 draw1.py -lines1 300 200 12
Example (c) draw_lines2()
- (This is either a demo or an exercise)
- An addition to lines1
- See picture below
- Draw N green lines:
Starting at the upper left and going down
Ending at the bottom left and going right
- Compute x_add and y_add in the loop
- Draw the green lines - non-trivial, but very applied math here
Run from the command line:
$ python3 draw1.py -lines2 300 200 12
draw_lines2() Solution Code
def draw_lines2(canvas, x, y, width, height, n):
"""
Draw the lines2 figure within x,y width,height
The code for lines1 is already here
"""
canvas.draw_rect(x, y, width, height)
for i in range(n):
y_add = (i / (n - 1)) * (height - 1) # formula: 0..1 fraction * max
canvas.draw_line(x, y, x + width - 1, y + y_add, color='red')
# loop to draw green "lines2" lines
for i in range(n):
pass
y_add = (i / (n - 1)) * (height - 1)
x_add = (i / (n - 1)) * (width - 1)
canvas.draw_line(x, y + y_add, x + x_add, y + height - 1, color='green')
Example (d) draw_grid1()
- Fill whole canvas with n-by-n grid of rectangles
- Using "row" "col", to distinguish from "x" "y" pixel coords
- In this case, draw a black rect for each grid position
- Issue: how wide is each sub rectangle?
- Divide total with by n, round down
- e.g. 500 pixels width, n = 11
- Use int division to figure an int width for all sub rects
- Each sub rect will be 45 pixels wide
>>> 500 / 11
45.45454545454545 # float division with fraction
>>> 500 // 11 # int division - what we want here
45
For each row/col number, what is its x,y coord?
- What is x left of col 0, col 1, ...
- What is y of row 0, row 1, ...
- The math works out very cleanly
x = col * sub_width
y = row * sub_height
- We'll re-use this "sub_width" pattern
Solution code
def draw_grid1(width, height, n):
"""
Creates a canvas.
Draws a grid1 of n-by-n black rectangles
(this code is complete)
"""
canvas = DrawCanvas(width, height, fast_draw=True)
# Figure sizes for all sub rects
sub_width = width // n
sub_height = height // n
# Loop over row/col
for row in range(n):
for col in range(n):
# Figure upper left of this sub rect
x = col * sub_width
y = row * sub_height
canvas.draw_rect(x, y, sub_width, sub_height)
Run from command line:
$ python3 draw1.py -grid1 600 400 12
Revisit draw_lines2()
Here is the draw_lines2() def first line:
def draw_lines2(canvas, x, y, width, height, n):
"""
Draw the lines2 figure within x,y width,height
The code for lines1 is already here
"""
- What do we have with this function?
- We can call it
- Pass in: canvas and x,y width,height n
- Draws the lines2 figure in there
- So what would the code look like to take grid above
- And fill it with lines2 figures?
Example (e) draw_grid2()
- Write code in draw_grid2()
- For each sub-rect - draw the lines2 figure
- How to make that happen easily?
- Just call the draw_lines2() function!
- Pass it the x,y width,height you want it to draw in
draw_grid2() Solution Code
def draw_grid2(width, height, n):
"""
Creates a canvas.
Add code to draw the lines2 figure in each grid sub rect.
"""
canvas = DrawCanvas(width, height, fast_draw=False)
sub_width = width // n
sub_height = height // n
for row in range(n):
for col in range(n):
pass
# Your code here - draw a lines2 figure in each grid rect
x = col * sub_width
y = row * sub_height
# key line: call the function to draw the figure here
draw_lines2(canvas, x, y, sub_width, sub_height, n)
Looks neat - loops + function-call = power!
Here is the "function black box" material, which can look at today if we have time, or start it next time...
Black Box - Big Picture
- Program made of functions
- Data flows between functions
- How do functions exchange data?
- Function: black box model
data in: parameters
data out: return
- Today 1 function at a time, later build whole picture
Theme: "Black Box" Function Model
Black-box model of a function: function runs with parameters as input, returns a value.
All the complexity of its code is sealed inside. Calling it is relatively simple - provide parameters, get back an answer.
4 Functions Today
(had a tech problem in lecture, now fixed)
>
Black Box Functions
1. Parameters = Input
- Parameters are the inputs of a function
- Each parameter is like a variable with a value already in it
- Where does the value come from...
- (a) (non-answer) Not our concern how, but there's a value in there
- (b) (real answer) The caller code places a value between the parens when calling the function, and this is the value of each parameter
2. return
yields the result of a function
Inside the body of a function, like this:
def foo(n):
...
return n * 10
...
- Return exits the run of foo() immediately
- The run continues at the caller code
- The value, e.g.
n * 10
is the return value of the foo() call
Suppose the caller code looks like
# in caller function
a = foo(6)
b = foo(7)
- The return line within foo() causes 60 to be returned by
foo(6)
essentially a = 60
- This is called the "return value" of the function
- The return value of
foo(7)
is 70, assigned to b
Example 1. "Winnings1" Function
- Say we work for the California lottery
- Writing a function for a game
- Rules: input is an int score 0..10 inclusive
- Output:
score 5 or more, winnings = score * 12
score 4 or less, winnings = score * 10
- We could make a table showing output for each input
- Writing code, we need to be organized thinking about the cases and their outputs
- Code needs to work for all cases, i.e. to be general
- Later: we'll see how to put in formal tests that the code is getting the right output for each input
in out
0 0
1 10
2 20
3 30
4 40
5 60
6 72
7 84
8 96
9 108
10 120
Winnings1 code
- This code works
score
parameter comes in
- if/logic to check for score >= 5
- return is used twice
- "pick off" of cases
- So past the first if.. what is true of score?
- We know score < 5
def winnings1(score):
"""Compute winnings1 described above."""
if score >= 5:
return score * 12
# Run gets here: we know score < 5
return score * 10
Winnings if-return Case Strategy
- The function needs to handle all input cases
- If-logic detects one case .. returns the answer
- The return exits the function immediately with a return value
- Lines below have if-return logic for other cases
- Code is "picking off" the cases one by one
- As we get past if-checks .. input must be a remaining case
- Analogy: coin sorting machine
coin rolls past holes, each a bit larger
dime, penny, nickel, quarter
a dime never makes it to the quarter-hole
The dime is picked-off earlier
- Bug demo: change <= to <
Return None Default
- Demo: cut out the code, just have a "pass"
- If a function does not run a return,
None
is default function result
- If you see
None
in your output - maybe you forgot the return
- Or your if-logic for some case doesn't return