.. _better-graphics: Making Python do the hard work ############################## .. wordcount:: .. vim:ft=rst spell: Last time we showed how to draw basic shapes, but there was one problem with the approach. We had to figure out all those pesky coordinate values by hand. That is a pain, and when something is a pain, we need to find a way to hand that pain off and let the computer do it! Now that we have an introduction to simple expressions, let's see how they can help in our graphics! Coordinates are just numbers **************************** As you saw, the numbers we were entering as parameters to our simple functions were just simple ``integer`` numbers. We use these kinds of numbers since we are working with ``pixels`` and there is no such thing as a fractional pixel. When we want a particular shape to be a certain size, we need to do a bit of simple math. Suppose you want to build a square, but you do not want to figure out all the numbers to place in those function calls. As a review, here is how we might have drawn a square based on what we discussed last time: .. code-block:: python pt1 = Point(100,100) pt2 = Point(200,200) box = Rectangle(pt1, pt2) box.draw(win) If we stick this code into the program framework we set up last time we expect to see a square on the screen. Simple enough. But, what if I want the square to be 128 pixels on a side, how do I change the numbers? Well, we could break out a calculator and do the math ourselves. That violates rule one for all programmers, we are lazy! I am not doing the math myself! What I want to do is to pick a ``reference point`` for the shape and calculate all the other numbers based on that. As an example, let's pick the top-left corner of the square as our reference point. I am going to place that point at an X,Y of 100,100 and I want the sides to be 128 pixels. Here is how we might figure all this out. .. code-block:: python x1 = 100 y2 = 100 width = 128 pt1 = Point(x1, y1) x2 = x1 + width y2 = y1 + width pt2 = Point(x2, y2) box = Rectangle(pt1, pt2) Notice that the only numbers I am really worrying about is where I want my reference point to be on the screen and how big the side of the square is. Everything is worked out from that. Now, that ``reference point`` is just a way of picking any point that is convenient to use, and you might pick a different point to make drawing things easier. Suppose I want to draw a house with a door in the middle and two windows on either side of the door. I want the top of the windows to line up with the top of the door. What would be a good reference point for this project? Drawing the house ================= I am not going to draw a complete house, just a box with a few other boxes for the doors and windows. Here is what I want to draw: .. image:: images/house4.png :align: center Looking closely at this image, I guess picking the top left corner is not a good idea. What if we pick the middle of the bottom of the house, would that be better? Well, I think so, since I can calculate a number of things easily based on that location. Let's create a few variables: .. code-block:: python # location of house bottom mid point X = 250 y = 180 # basic house dimensions width = 200 height = 100 # door and window sizes doorHeight = 80 doorWidth = 45 windowWidth = 50 windowHeight = 50 Phew, that is a lot of stuff. No problem, that is why we use computers, to track all these things. Drawing the basic house ----------------------- We need to remember that drawing has to happen in a certain order. This is just like painting. If you paint something over another object that is already on the drawing, that other object is going to be covered up. Think your way through the drawing from back to front and you will not lose things. First, we will draw the basic house: .. code-block:: python x1 = x - width/2 y1 = y - height x2 = x + width/2 y2 = y pt1 = Point(x1, y2) pt2 = Point(x2, y2) house = Rectangle(pt1,pt2) house.draw(win) look at the ``expressions`` we are using here. We are setting our reference point in the middle of the house, so the first point we want is one half the width of the house back, and at a Y value of ``height`` above the bottom of the house. Do the expressions say that? The other point is at an X value one half the width more than the mid point, and the Y value is at the bottom of the house. Once again, make sure you believe we are doing things right. Or, just try it and see: .. image:: images/house2.png :align: center Looks like we are on the right path! Adding the door --------------- Now, I do not really need all those variables, if I am wiling to put expressions in place of those parameters. Although that seems odd, it is a simple and handy concept. If Python runs into an expression where a parameter is called for, it just ``evaluates`` the expression and figures out a single number, then hands that number to the function troll. Here is what it looks like: .. code-block:: python door_pt1 = Point(x - doorWidth/2, y-doorHeight) door_pt2 = Point(x + doorWidth/2, y) door = Rectangle(door_pt1, door_pt2) door.draw(win) On to the windows ----------------- One more step and we are done with this demo. We have already set up the basic window dimensions as variables, so creating the window rectangles should be easy. Well, we still have to figure out where they go so they are centered in the space on either side of the door. Also, we were told that the windows need to line up with the top of the door. Sounds like we need a bit of math to figure all this out! This will be a bit tedious, and will cause you to think about things in an important way (important in the graphics world, anyway!). To follow along, it might help to draw a few sketches and see if you agree with the calculations. (The drawing will tell you if you are right, by the way!) .. code-block:: python # Where is the top of the window? win_top = y - doorHeight # calculate left window placement win_mid = ((x - doorWidth/2) + (x - width/2))/2 win_left = win_mid - windowWidth/2 win_right = win_left + windowWidth #Left window points win_pt1 = Point(win_left, win_top) win_pt2 = Point(win_right, window_top + window_height) Phew, that ``win_mid`` expression looks complicated! What does it say? Well, we want the window to be centered in the space on each side of the door. We find the middle of that space by figuring out the X value of the left side of the house and the left side of the door and finding the average of those two numbers. You should convince yourself that we have done this correctly in the expression above. Note that we use parentheses to make sure we are doing things right. We can draw the left window now: .. code-block:: python l_win = Rectangle(win_pt1, win_pt2) l_win.draw(win) With any luck you should see the left window nicely centered in its space. The right window code is similar, but we are working on the space to the right of the door: .. code-block:: python # right window win_mid = ((x + doorWidth/2) + (x + width/2))/2 win_left = win_mid - windowWidth/2 win_right = win_left + windowWidth # right window points win_pt1 = Point(win_left, win_top) win_pt2 = Point(win_right, win_top+windowHeight) r_win = Rectangle(win_pt1, win_pt2) r_win.draw(win) .. note:: Some might notice that I am using some variables over again. This is fine since we have already drawn the left window and no longer really need those variables. You could create new variables, but the size of your program gets bigger if you do, and you should think about what variables you really need to keep around. Here is the final house. Looks good, except for the paint. You can figure that out! .. image:: images/house4.png :align: center *************************** What if you want two houses *************************** We have set up a bunch of code that will draw a single house. If you look back at that code you might notice that we have really based everything on those few basic variables at the top. Everything else was calculated ``relative`` to those values. If we want to move the house, all we need to do is change the X and Y values. This situation is ideal for what we will look at next! We will teach a troll how to build a house! Suppose I want to keep the window and door dimensions the same, but create houses with different heights and widths, and specify where to build them. As long as I am reasonable about this, I could teach one of my trolls to do this job with a simple change to the program. We are not going to go into a lot of detail on this, but I will show you a new function that can draw this house for us (and paint it blue). Here is the code: .. literalinclude:: code/house2.py All we did was take all the specific code needed to build a single house, and wrap it up in a function container with a ``def`` line at the top. We indented everything we wanted to move inside the function box. All of the variables defined in the original code are now inside the box and not visible to any code outside of the box. (They are said to be ``private`` to the troll in the box). The variables we want to use to control the placement and size of the house are in the ``parameter list`` we define for this new function. The ``caller`` of this function (who will kick the box to wake up the troll) will need to give us this information before our troll can go to work. This is how we teach our troll a new trick. When we activate the troll, a new house will appear! Look at the bottom of this code. Those two lines at the bottom ``call`` our new function, providing the required information. Here is what we see: .. image:: images/house5.png :align: center Now, that is useful! .. note:: Do you see why the door is blue? Since we did not ``fill`` the door, the inside is blank, not white. The color of the house shows through the door. If we paint the door some other color, that will be on top of the house. That should be enough for this lecture. Make sure you work through this on your own. Once we learn a bit more about loops, adding a picket fence should ba a snap! .. vim:set spell filetype=rst: