Trigonometry is the branch of mathematics that studies triangles and the relationships between their sides and the angles between these sides. The sine, cosine, and tangent trigonometry functions are implemented as programming functions in most languages and given the names sin(), cos(), and tan(). In Python, these functions exist in the
math module. These functions are very handy in games and graphical programs because of the smooth wave-like pattern their return values produce.
This tutorial shows how you can get a variety of neat animation behavior using these functions in your programs. (The animated gifs you see above were taken from the programs in this tutorial.) The code examples in this tutorial should work with both Python 2 and Python 3.
Trig Function Basics
You don't need to know how these functions work. Let's just treat these functions as black boxes: they take one number parameter in, and return a float value out. If you already know the mathematics of sine and cosine, then you can just skim this section.
In the interactive shell, let's see what
math.sin() returns for some values:
>>> import math >>> math.sin(1) 0.8414709848078965 >>> math.sin(2) 0.90929742682568171 >>> math.sin(3) 0.14112000805986721 >>> math.sin(4) -0.7568024953079282 >>> math.sin(5) -0.95892427466313845
It looks like
math.sin() just spits out some random-looking float values. (Actually, it's the length of the opposite side divided by the hypotenuse of a right triangle with the given angle. But you don't need to know or understand this.) But if we graph the return values of the input arguments
10 on a graph, we can see the pattern:
If you figure out the sine values for some more numbers (for example,
1.5 and so on) and then connect the dots with lines, you can see this wave pattern more easily:
In fact, if you kept adding more and more data points to this graph, you would see that the complete sine wave looks like this:
The significant input/output pairs for sine are:
|Math Function||Python Code||Return Value|
|sine of 0||
|sine of π / 2||
|sine of π||
|sine of 3π / 2||
|sine of 2π||
0, then the return value gradually increases until
math.sin(3.14 / 2) (that is, half of pi) returns
1, then it begins to decrease until
0. (The number
3.14 is a special number in mathematics called pi (pronounced the same as delicious "pie".) This value is stored in the variable
pi in the
math module and has the float value
The pattern of return values for
math.cos() looks similar, it just runs a little bit behind the sine wave:
This wavey-looking pattern of return values makes
math.cos() pretty handy for a few graphics and animation programming techniques. The core benefit is that these are functions can take a linearly changing (that is, changes at a set rate) input and give an output that oscillates between -1.0 and 1.0.
Bouncing Animation with Sine and Cosine
Our first programming example shows some simple bouncing animation (though technically not a parabola, but though not physics-precise this bounce is simple to implement):
Here's the main part of the code for the blue bouncing ball's Y position:
yPos = -1 * math.sin(step) * AMPLITUDE ...some other code... step += 0.02
When we call the
math.sin() function, we pass it
step for the argument. The
step variable starts at 0 and is incremented on line 65 by 0.02. So the calls to
math.sin(0.04), and so on.
The return value (that is, the output) is used for the Y coordinate. It is multiplied by
-1 because in Pygame (and most programming languages) the Y coordinate increases going down. This is the opposite direction used in Cartesian coordinates in your math class.
The return value is also multiplied by the value in the
AMPLITUDE variable, so that instead of ranging over
1.0, it will range between
(-1.0 * AMPLITUDE) to
(1.0 * AMPLITUDE). Which is to say, it ranges from
AMPLITUDE. Since we set
100 on line 29 of trig_bounce.py, this causes the Y coordinate to move over a range of 200 pixels (from a Y coordinate of -100 to 100). If we increase
AMPLITUDE, then the ball will move over a larger range (and move faster, since it covers a larger distance in the same amount of time).
Things to note:
AMPLITUDE= Increasing the range that the ball moves over.
AMPLITUDE= Increasing the speed that the ball moves (since it moves over a larger range in the same time as before).
- Increasing the amount
stepincrements by = Increases the speed the ball moves.
On line 55, we pass the red ball's
math.sin() return value to
abs() which makes sure any negative return value is converted to a positive number. This is what causes the red ball to "bounce" instead of oscillate like the blue ball.
Changing the Amplitude and Frequency of the Waves
You can see the return values for the
math.cos() functions (as well as the return value of
math.sin() multiplied by a large amplitude value, and the return value of
math.sin() with a larger input parameter to increase the wave's frequency) in the trig_waves.py program:
As you can see, the cosine wave is exactly the same as the sine wave, except a little further behind it. Take a look at the source code to see how we can adjust the
math.sin() calls to change the shape of the wave.
# sine wave yPos = -1 * math.sin(step) * AMPLITUDE ...some other code... # high amplitude sine wave yPos = -1 * math.sin(step) * (AMPLITUDE + 100) # Note the "+ 100" to the amplitude! ...some other code... # high frequency sine wave yPos = -1 * math.sin(step * 4) * AMPLITUDE # Note the "* 4" to the frequency
Moving in a Circle
The cosine functions can be used for getting the X and Y coordinates of a circle. The trig_circle.py program shows a ball moving in a circle (along with two other balls that are moving only along the X-axis and only along the Y-axis).
In trig_circle.py, these are lines 56 to 60:
# draw blue ball xPos = math.cos(step) * AMPLITUDE yPos = -1 * math.sin(step) * AMPLITUDE #yPos = -1 * abs(math.sin(step) * AMPLITUDE) # uncomment this line to make the ball bounce pygame.draw.circle(DISPLAYSURF, BRIGHTBLUE, (int(xPos) + WIN_CENTERX, int(yPos) + WIN_CENTERY), 20)
Remember, the multiplication by
yPos is because Pygame's Y coordinates increase going down. Note that the
yPos variables are converted to integers and added to
WIN_CENTERY before being passed to
pygame.draw.circle(). While multiplying the input argument will increase the amplitude of the wave, adding a number to the output will move the coordinate (this is called translation). Since we want the ball's X and Y coordinates to be in the center of the window, we add
WIN_CENTERY to them.
You can see the vertical component of this movement with the ball drawn by lines 62 and 63:
# draw vertically moving red ball pygame.draw.circle(DISPLAYSURF, DARKRED, (WINDOWWIDTH - 30, int(yPos) + WIN_CENTERY), 20)
You can see the horizontal component of the circular movement with the ball drawn by lines 65 and 66:
# draw horizontally moving red ball pygame.draw.circle(DISPLAYSURF, DARKRED, (int(xPos) + WIN_CENTERX, WINDOWHEIGHT - 30), 20)
If you uncomment the second
yPos assignment statement, the Y coordinate will be set to the absolute value of the previous calculation. This causes the blue ball to bounce back and forth, since the Y coordinate can never become negative. Try uncommenting this line and re-running the program.
Note: When you use
math.cos() to calculate the X coordinate and
math.sin() to calculate the Y coordinate, using an argument of
0 returns the coordinates of the right edge of the circle and goes counter-clockwise as you increment the argument.
If you use
math.sin() to calculate the X coordinate and
math.cos() to calculate the Y coordinate, passing an argument of
0 returns the coordinates of the top edge of the circle and goes clockwise as you increment the argument.
Making An Analog Clock Program
If you look at the Windows clock (or whatever system clock your OS has), you'll notice that it draws a line from the center of the clock to the numbers that are laid out around the clock. Pygame already provides function that can draw a line if you give it the X and Y coordinates for the two ends of the line.
The numbers of the clock are arranged in a circle, so we can use
math.cos() to get the X and Y coordinates for the other end of the line. You can also use the trig functions to figure out the X and Y coordinates of the numbers.
The main part of the program is the
getTickPosition() function. This returns the X and Y coordinate of a "tick" position of a clock, which we define as starting at 0 at the top of the clock and increasing in a clockwise direction.
# This function retrieves the x, y coordinates based on a "tick" mark, which ranges between 0 and 60 # A "tick" of 0 is at the top of the circle, 30 is at the bottom, 45 is at the "9 o'clock" position, etc. # The "stretch" is how far from the origin the x, y return values will be # "originx" and "originy" will be where the center of the circle is (almost always the center of the window) def getTickPosition(tick, stretch=1.0, originx=WIN_CENTERX, originy=WIN_CENTERY): # uncomment to have a "rotating clock" feature. # This works by pushing the "tick" amount forward #tick += (time.time() % 15) * 4 # The cos() and sin() tick -= 15 # ensure that tick is between 0 and 60 tick = tick % 60 tick = 60 - tick # the argument to sin() or cos() needs to range between 0 and 2 * math.pi # Since tick is always between 0 and 60, (tick / 60.0) will always be between 0.0 and 1.0 # The (tick / 60.0) lets us break up the range between 0 and 2 * math.pi into 60 increments. x = math.cos(2 * math.pi * (tick / 60.0)) y = -1 * math.sin(2 * math.pi * (tick / 60.0)) # "-1 *" because in Pygame, the y coordinates increase going down (the opposite of how they normally go in mathematics) # sin() and cos() return a number between -1.0 and 1.0, so multiply to stretch it out. x *= stretch y *= stretch # Then do the translation (i.e. sliding) of the x and y points. # NOTE: Always do the translation addition AFTER doing the stretch. x += originx y += originy return x, y
Also notice in the source code there are three lines that you can uncomment to apply different animation effects to the clock. Go ahead and experiment with them:
# uncomment to have a "rotating clock" feature. # This works by pushing the "tick" amount forward #tick += (time.time() % 15) * 4 ...some other code... # Uncomment this if you don't want the second hand to move smoothly: #now_second = now ...some other code... # Uncomment this if you want the "pulsing clock" feature: #CLOCKSIZE = originalClockSize + math.sin(2 * math.pi * (time.time() % 1)) * PULSESIZE
Pointing Cannons at a Target
There is another trig function called tangent and its inverse, arctangent. In Python, this is implemented in the
math.atan() functions. One very useful thing that the tangent functions are good for finding the amount of rotation needed to point an object at another, given their X and Y coordinates. The Python function I've implemented for this makes use of the
math.atan2() function (an arctangent function with x and y parameters rather than a single radian parameter) in lines 45 to 52 of trig_cannons.py:
def getAngle(x1, y1, x2, y2): # Return value is 0 for right, 90 for up, 180 for left, and 270 for down (and all values between 0 and 360) rise = y1 - y2 run = x1 - x2 angle = math.atan2(run, rise) # get the angle in radians angle = angle * (180 / math.pi) # convert to degrees angle = (angle + 90) % 360 # adjust for a right-facing sprite return angle
getAngle() function takes in the X and Y coordinates of two points, and returns the number of degrees that the object at the first point should be rotated to be pointing at the second point.
With the code in
getAngle(), we can set up several different cannons on the screen and have them always pointed at the mouse cursor by rotating the cannon's image (on its
Surface object) by the return value of
getAngle(). That is what this code does:
degrees = getAngle(cannonx, cannony, mousex, mousey) # rotate a copy of the cannon image and draw it rotatedSurf = pygame.transform.rotate(cannonSurf, degrees)
Remember to pass the cannon's X and Y coordinates as the first pair of arguments to
getAngle(), otherwise you will get the angle that the mouse cursor would rotate to point to the cannon instead of vice versa.
Red Orbiting Circle
The trig_redcenter.py program is a simplified version of the code that will be in the "following eyes" program next. The basic idea of this program is to get the angle between the mouse cursor and the center of the window (which involves calling the
math.atan2() function). Then a red circle is placed a constant distance away from the center at this angle (which involves
math.sin(), since all the points a constant distance from one point forms a circle.)
# draw the cannons pointed at the mouse cursor mousex, mousey = pygame.mouse.get_pos() degrees = getAngle(WIN_CENTERX, WIN_CENTERY, mousex, mousey) # draw the thin central red circle pygame.draw.circle(DISPLAYSURF, RED, (WIN_CENTERX, WIN_CENTERY), 100, 4) # draw the big circle on the edge xPos = math.cos(degrees * (math.pi / 180)) * 100 yPos = math.sin(degrees * (math.pi / 180)) * 100 pygame.draw.circle(DISPLAYSURF, RED, (int(xPos) + WIN_CENTERX, -1 * int(yPos) + WIN_CENTERY), 40)
This makes it look like the mouse is sliding the big red circle along the thing circle. This effect will be used in the next program.
Eyes Following the Mouse Cursor
The trig_eyes.py program uses this same
getAngle() function to make eyes that will follow the mouse pointer. You can see that the solid black circle of the eyeball has it's center X and Y coordinates set by this code:
degrees = getAngle(eyex, eyey, mousex, mousey) # draw the outline of the eye pygame.draw.circle(DISPLAYSURF, BLACK, (eyex, eyey), 15, 1) # draw the black part of the eye xPos = math.cos(degrees * (math.pi / 180)) * 5 yPos = math.sin(degrees * (math.pi / 180)) * 5 pygame.draw.circle(DISPLAYSURF, BLACK, (int(xPos) + eyex, -1 * int(yPos) + eyey), 10)
To make the eyes "follow" the mouse cursor, we need to have the pupil circle be a constant distance from the center of the eye. We do this with code similar to the code in trig_redcenter.py. We just make sure this distance and the radius of the pupil circles adds up to the radius of the outline circle of the eye. That way the pupil will always be at the edge of the eye outline.
When we put two of these together, they look like a pair of eyes following the mouse circle.
There aren't many times that you need to know complex math in programming. But the basic trig functions that programming languages provide are very helpful for various graphics and animation techniques like the ones outlined here. I hope this tutorial has been helpful. Feel free to ask any further questions in the comments or email me. [email protected]
Learn to program with my books for beginners, free under a Creative Commons license:
Take my Automate the Boring Stuff with Python online Udemy course. Use this link to apply a 60% discount.