- Installing required modules (PIL/Pillow and Requests)
To use cmu_112_graphics.py, you need
to have some modules installed. If they are not installed, you will see
a message like this (or a similar one for "requests" instead of "PIL"):
**********************************************************
** Cannot import PIL -- it seems you need to install pillow
** This may result in limited functionality or even a runtime error.
**********************************************************
You can try to use 'pip' to install the missing modules, but it can be
complicated making sure you are installing these modules for the same version
of Python that you are running. Here are some more-reliable steps
that should work for you:
- For Windows:
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
import sys
print(f'"{sys.executable}" -m pip install pillow')
print(f'"{sys.executable}" -m pip install requests')
- Open Command Prompt as an administrator user (right click - run as administrator)
- Copy-paste each of the two commands printed in step 1 into the
command prompt you opened in step 2.
- Close the command prompt and close Python.
- Re-open Python, and you're set (hopefully)!
- For Mac or Linux:
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
import sys
print(f'sudo "{sys.executable}" -m pip install pillow')
print(f'sudo "{sys.executable}" -m pip install requests')
- Open Terminal
- Copy-paste each of the two commands printed in step 1 into the
command prompt you opened in step 2.
- If you see a lock and a password is requested, type in the same password that you use to log into your computer.
- Close the terminal and close Python.
- Re-open Python, and you're set (hopefully)!
If these steps do not work for you, please go to OH and we will be happy
to assist.
- Keyboard Shortcuts
from cmu_112_graphics import *
def appStarted(app):
app.counter = 0
def timerFired(app):
app.counter += 1
def redrawAll(app, canvas):
canvas.create_text(200, 50,
text='Keyboard Shortcut Demo', fill='black')
canvas.create_text(200, 100,
text='Press control-p to pause/unpause', fill='black')
canvas.create_text(200, 150,
text='Press control-s to save a snapshot', fill='black')
canvas.create_text(200, 200,
text='Press control-q to quit', fill='black')
canvas.create_text(200, 250,
text='Press control-x to hard exit', fill='black')
canvas.create_text(200, 300, text=f'{app.counter}', fill='black')
runApp(width=400, height=400) # quit still runs next one, exit does not
runApp(width=600, height=600)
- Events
from cmu_112_graphics import *
def appStarted(app):
app.messages = ['appStarted']
def appStopped(app):
app.messages.append('appStopped')
print('appStopped!')
def keyPressed(app, event):
app.messages.append('keyPressed: ' + event.key)
def keyReleased(app, event):
app.messages.append('keyReleased: ' + event.key)
def mousePressed(app, event):
app.messages.append(f'mousePressed at {(event.x, event.y)}')
def mouseReleased(app, event):
app.messages.append(f'mouseReleased at {(event.x, event.y)}')
def mouseMoved(app, event):
app.messages.append(f'mouseMoved at {(event.x, event.y)}')
def mouseDragged(app, event):
app.messages.append(f'mouseDragged at {(event.x, event.y)}')
def sizeChanged(app):
app.messages.append(f'sizeChanged to {(app.width, app.height)}')
def redrawAll(app, canvas):
font = 'Arial 20 bold'
canvas.create_text(app.width/2, 30, text='Events Demo',
font=font, fill='black')
n = min(10, len(app.messages))
i0 = len(app.messages)-n
for i in range(i0, len(app.messages)):
canvas.create_text(app.width/2, 100+50*(i-i0),
text=f'#{i}: {app.messages[i]}',
font=font, fill='black')
runApp(width=600, height=600)
- Input and Output Methods
# This demos app.getUserInput(prompt) and app.showMessage(message)
from cmu_112_graphics import *
def appStarted(app):
app.message = 'Click the mouse to enter your name!'
def mousePressed(app, event):
name = app.getUserInput('What is your name?')
if (name == None):
app.message = 'You canceled!'
else:
app.showMessage('You entered: ' + name)
app.message = f'Hi, {name}!'
def redrawAll(app, canvas):
font = 'Arial 24 bold'
canvas.create_text(app.width/2, app.height/2,
text=app.message, font=font, fill='black')
runApp(width=500, height=300)
- Image Methods
- loadImage and scaleImage (using url)
# This demos loadImage and scaleImage from a url
from cmu_112_graphics import *
def appStarted(app):
url = 'https://tinyurl.com/great-pitch-gif'
app.image1 = app.loadImage(url)
app.image2 = app.scaleImage(app.image1, 2/3)
def redrawAll(app, canvas):
canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1))
canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2))
runApp(width=700, height=600)
- loadImage and scaleImage (using local file)
Let's do that again, only this time using an image stored locally.
To run this version, you must first download this image
(testImage2.gif)
and save it in the same folder as your Python code:
# This demos loadImage and scaleImage from a local file
from cmu_112_graphics import *
def appStarted(app):
app.image1 = app.loadImage('testImage2.gif')
app.image2 = app.scaleImage(app.image1, 2/3)
def redrawAll(app, canvas):
canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1))
canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2))
runApp(width=700, height=600)
- Using image.size
# This demos using image.size
from cmu_112_graphics import *
def appStarted(app):
url = 'https://tinyurl.com/great-pitch-gif'
app.image1 = app.loadImage(url)
app.image2 = app.scaleImage(app.image1, 2/3)
def drawImageWithSizeBelowIt(app, canvas, image, cx, cy):
canvas.create_image(cx, cy, image=ImageTk.PhotoImage(image))
imageWidth, imageHeight = image.size
msg = f'Image size: {imageWidth} x {imageHeight}'
canvas.create_text(cx, cy + imageHeight/2 + 20,
text=msg, font='Arial 20 bold', fill='black')
def redrawAll(app, canvas):
drawImageWithSizeBelowIt(app, canvas, app.image1, 200, 300)
drawImageWithSizeBelowIt(app, canvas, app.image2, 500, 300)
runApp(width=700, height=600)
- Using transpose to flip an image
# This demos using transpose to flip an image
from cmu_112_graphics import *
def appStarted(app):
url = 'https://tinyurl.com/great-pitch-gif'
app.image1 = app.loadImage(url)
app.image2 = app.image1.transpose(Image.FLIP_LEFT_RIGHT)
def redrawAll(app, canvas):
canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1))
canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2))
runApp(width=700, height=600)
- Using getpixel and putpixel
# This demos using getpixel and putpixel
from cmu_112_graphics import *
def appStarted(app):
url = 'https://tinyurl.com/great-pitch-gif'
app.image1 = app.loadImage(url)
# now let's make a copy that only uses the red part of each rgb pixel:
app.image1 = app.image1.convert('RGB')
app.image2 = Image.new(mode='RGB', size=app.image1.size)
for x in range(app.image2.width):
for y in range(app.image2.height):
r,g,b = app.image1.getpixel((x,y))
app.image2.putpixel((x,y),(r,0,0))
def redrawAll(app, canvas):
canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1))
canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2))
runApp(width=700, height=600)
- Making new images and using ImageDraw
Now let's look see how to create a new image (rather than load an image),
and to use the ImageDraw module to draw (lines, etc) on an image:
# This demos creating a new blank image and using PIL ImageDraw
from cmu_112_graphics import *
def appStarted(app):
imageWidth, imageHeight = app.width//3, app.height//2
bgColor = (0, 255, 255) # cyan
app.image1 = Image.new('RGB', (imageWidth, imageHeight), bgColor)
# Now that we created the image, let's use ImageDraw to draw in it
# See https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html
draw = ImageDraw.Draw(app.image1)
draw.line((0, 0, imageWidth, imageHeight), width=10, fill=(255, 0, 0))
draw.line((0, imageHeight, imageWidth, 0), width=10, fill=(0, 0, 255))
# And now we will create a scaled copy to show this is a normal image
app.image2 = app.scaleImage(app.image1, 2/3)
def redrawAll(app, canvas):
canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1))
canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2))
runApp(width=700, height=600)
- getSnapshot and saveSnapshot
Now let's look at getSnapshot and saveSnapshot:
Note: this feature does not work especially well on some platforms.
# This demos getSnapshot and saveSnapshot
from cmu_112_graphics import *
def appStarted(app):
app.image = None
def keyPressed(app, event):
if (event.key == 'g'):
snapshotImage = app.getSnapshot()
app.image = app.scaleImage(snapshotImage, 0.4)
elif (event.key == 's'):
app.saveSnapshot()
def redrawAll(app, canvas):
canvas.create_text(350, 20, text='Press g to getSnapshot', fill='black')
canvas.create_text(350, 40, text='Press s to saveSnapshot', fill='black')
canvas.create_rectangle(50, 100, 250, 500, fill='cyan')
if (app.image != None):
canvas.create_image(525, 300, image=ImageTk.PhotoImage(app.image))
runApp(width=700, height=600)
- Spritesheets using Pillow/PIL methods (such as image.crop)
We can use
Pillow/PIL methods
such
as image.crop() (among many others!), which we use here to use a spritestrip (a kind
of spritesheet) by cropping each sub-image from this single image:
# This demos sprites using Pillow/PIL images
# See here for more details:
# https://pillow.readthedocs.io/en/stable/reference/Image.html
# This uses a spritestrip from this tutorial:
# https://www.codeandweb.com/texturepacker/tutorials/how-to-create-a-sprite-sheet
from cmu_112_graphics import *
def appStarted(app):
url = 'http://www.cs.cmu.edu/~112/notes/sample-spritestrip.png'
spritestrip = app.loadImage(url)
app.sprites = [ ]
for i in range(6):
sprite = spritestrip.crop((30+260*i, 30, 230+260*i, 250))
app.sprites.append(sprite)
app.spriteCounter = 0
def timerFired(app):
app.spriteCounter = (1 + app.spriteCounter) % len(app.sprites)
def redrawAll(app, canvas):
sprite = app.sprites[app.spriteCounter]
canvas.create_image(200, 200, image=ImageTk.PhotoImage(sprite))
runApp(width=400, height=400)
- Animated Gifs
We can use animated gifs, too! For example, download this animated gif:
Then run this:
from cmu_112_graphics import *
def appStarted(app):
app.spritePhotoImages = loadAnimatedGif('sample-animatedGif.gif')
app.spriteCounter = 0
def loadAnimatedGif(path):
# load first sprite outside of try/except to raise file-related exceptions
spritePhotoImages = [ PhotoImage(file=path, format='gif -index 0') ]
i = 1
while True:
try:
spritePhotoImages.append(PhotoImage(file=path,
format=f'gif -index {i}'))
i += 1
except Exception as e:
return spritePhotoImages
def timerFired(app):
app.spriteCounter = (1 + app.spriteCounter) % len(app.spritePhotoImages)
def redrawAll(app, canvas):
photoImage = app.spritePhotoImages[app.spriteCounter]
canvas.create_image(200, 200, image=photoImage)
runApp(width=400, height=400)
- Caching PhotoImages for increased speed
If you are using a lot of images, then the call to ImageTk.PhotoImage(image)
can slow things down, so we can cache the results of that, like so:
# This demos caching PhotoImages for increased speed
# when using a LOT of images (2500 here)
from cmu_112_graphics import *
import time
def make2dList(rows, cols):
return [ ([0] * cols) for row in range(rows) ]
def appStarted(app):
url = 'https://tinyurl.com/great-pitch-gif'
app.image1 = app.loadImage(url)
app.margin = 20
app.rows = app.cols = 50
app.images = make2dList(app.rows, app.cols)
for row in range(app.rows):
for col in range(app.cols):
app.images[row][col] = app.scaleImage(app.image1, 0.1)
app.counter = 0
app.timerDelay = 1
app.timerResult = 'Counting to 10...'
app.useCachedImages = False
resetTimer(app)
def resetTimer(app):
app.time0 = time.time()
app.counter = 0
def timerFired(app):
app.counter += 1
if (app.counter == 10):
duration = time.time() - app.time0
app.timerResult = f'Last time to 10: {round(duration,1)}s'
app.useCachedImages = not app.useCachedImages
resetTimer(app)
# from www.cs.cmu.edu/~112/notes/notes-animations-part1.html#exampleGrids
def getCellBounds(app, row, col):
# aka "modelToView"
# returns (x0, y0, x1, y1) corners/bounding box of given cell in grid
gridWidth = app.width - 2*app.margin
gridHeight = app.height - 2*app.margin
columnWidth = gridWidth / app.cols
rowHeight = gridHeight / app.rows
x0 = app.margin + col * columnWidth
x1 = app.margin + (col+1) * columnWidth
y0 = app.margin + row * rowHeight
y1 = app.margin + (row+1) * rowHeight
return (x0, y0, x1, y1)
def getCachedPhotoImage(app, image):
# stores a cached version of the PhotoImage in the PIL/Pillow image
if ('cachedPhotoImage' not in image.__dict__):
image.cachedPhotoImage = ImageTk.PhotoImage(image)
return image.cachedPhotoImage
def redrawAll(app, canvas):
for row in range(app.rows):
for col in range(app.cols):
(x0, y0, x1, y1) = getCellBounds(app, row, col)
cx, cy = (x0 + x1)/2, (y0 + y1)/2
image = app.images[row][col]
if (app.useCachedImages):
photoImage = getCachedPhotoImage(app, image)
else:
photoImage = ImageTk.PhotoImage(image)
canvas.create_image(cx, cy, image=photoImage)
canvas.create_rectangle(app.width/2-250, app.height/2-100,
app.width/2+250, app.height/2+100,
fill='lightYellow')
canvas.create_text(app.width/2, app.height/2-50,
text=f'Using cached images = {app.useCachedImages}',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2,
text=app.timerResult,
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2+50,
text=str(app.counter),
font='Arial 30 bold', fill='black')
runApp(width=700, height=600)
- Using Modes (aka Screens)
# This demos using modes (aka screens).
from cmu_112_graphics import *
import random
##########################################
# Splash Screen Mode
##########################################
def splashScreenMode_redrawAll(app, canvas):
font = 'Arial 26 bold'
canvas.create_text(app.width/2, 150, text='This demos a ModalApp!',
font=font, fill='black')
canvas.create_text(app.width/2, 200, text='This is a modal splash screen!',
font=font, fill='black')
canvas.create_text(app.width/2, 250, text='Press any key for the game!',
font=font, fill='black')
def splashScreenMode_keyPressed(app, event):
app.mode = 'gameMode'
##########################################
# Game Mode
##########################################
def gameMode_redrawAll(app, canvas):
font = 'Arial 26 bold'
canvas.create_text(app.width/2, 20, text=f'Score: {app.score}',
font=font, fill='black')
canvas.create_text(app.width/2, 60, text='Click on the dot!',
font=font, fill='black')
canvas.create_text(app.width/2, 100, text='Press h for help screen!',
font=font, fill='black')
canvas.create_text(app.width/2, 140, text='Press v for an MVC Violation!',
font=font, fill='black')
canvas.create_oval(app.x-app.r, app.y-app.r, app.x+app.r, app.y+app.r,
fill=app.color)
if app.makeAnMVCViolation:
app.ohNo = 'This is an MVC Violation!'
def gameMode_timerFired(app):
moveDot(app)
def gameMode_mousePressed(app, event):
d = ((app.x - event.x)**2 + (app.y - event.y)**2)**0.5
if (d <= app.r):
app.score += 1
randomizeDot(app)
elif (app.score > 0):
app.score -= 1
def gameMode_keyPressed(app, event):
if (event.key == 'h'):
app.mode = 'helpMode'
elif (event.key == 'v'):
app.makeAnMVCViolation = True
##########################################
# Help Mode
##########################################
def helpMode_redrawAll(app, canvas):
font = 'Arial 26 bold'
canvas.create_text(app.width/2, 150, text='This is the help screen!',
font=font, fill='black')
canvas.create_text(app.width/2, 250, text='(Insert helpful message here)',
font=font, fill='black')
canvas.create_text(app.width/2, 350, text='Press any key to return to the game!',
font=font, fill='black')
def helpMode_keyPressed(app, event):
app.mode = 'gameMode'
##########################################
# Main App
##########################################
def appStarted(app):
app.mode = 'splashScreenMode'
app.score = 0
app.timerDelay = 50
app.makeAnMVCViolation = False
randomizeDot(app)
def randomizeDot(app):
app.x = random.randint(20, app.width-20)
app.y = random.randint(20, app.height-20)
app.r = random.randint(10, 20)
app.color = random.choice(['red', 'orange', 'yellow', 'green', 'blue'])
app.dx = random.choice([+1,-1])*random.randint(3,6)
app.dy = random.choice([+1,-1])*random.randint(3,6)
def moveDot(app):
app.x += app.dx
if (app.x < 0) or (app.x > app.width): app.dx = -app.dx
app.y += app.dy
if (app.y < 0) or (app.y > app.height): app.dy = -app.dy
runApp(width=600, height=500)
- Example: Sidescrollers 1-3
- SideScroller1
# SideScroller1:
from cmu_112_graphics import *
import random
def appStarted(app):
app.scrollX = 0
app.dots = [(random.randrange(app.width),
random.randrange(60, app.height)) for _ in range(50)]
def keyPressed(app, event):
if (event.key == "Left"): app.scrollX -= 5
elif (event.key == "Right"): app.scrollX += 5
def redrawAll(app, canvas):
# draw the player fixed to the center of the scrolled canvas
cx, cy, r = app.width/2, app.height/2, 10
canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan')
# draw the dots, shifted by the scrollX offset
for (cx, cy) in app.dots:
cx -= app.scrollX # <-- This is where we scroll each dot!!!
canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen')
# draw the x and y axes
x = app.width/2 - app.scrollX # <-- This is where we scroll the axis!
y = app.height/2
canvas.create_line(x, 0, x, app.height)
canvas.create_line(0, y, app.width, y)
# draw the instructions and the current scrollX
x = app.width/2
canvas.create_text(x, 20, text='Use arrows to move left or right',
fill='black')
canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}',
fill='black')
runApp(width=300, height=300)
- SideScroller2
# SideScroller2:
# Now with a scroll margin, so player does not stay fixed
# at the center of the scrolled canvas, and we only scroll
# if the player's center (in this case) gets closer than the
# margin to the left or right edge of the canvas.
from cmu_112_graphics import *
import random
def appStarted(app):
app.scrollX = 0
app.scrollMargin = 50
app.playerX = app.width//2 # player's center
app.dots = [(random.randrange(app.width),
random.randrange(60, app.height)) for _ in range(50)]
def makePlayerVisible(app):
# scroll to make player visible as needed
if (app.playerX < app.scrollX + app.scrollMargin):
app.scrollX = app.playerX - app.scrollMargin
if (app.playerX > app.scrollX + app.width - app.scrollMargin):
app.scrollX = app.playerX - app.width + app.scrollMargin
def movePlayer(app, dx, dy):
app.playerX += dx
makePlayerVisible(app)
def keyPressed(app, event):
if (event.key == "Left"): movePlayer(app, -5, 0)
elif (event.key == "Right"): movePlayer(app, +5, 0)
def redrawAll(app, canvas):
# draw the player, shifted by the scrollX offset
cx, cy, r = app.playerX, app.height/2, 10
cx -= app.scrollX # <-- This is where we scroll the player!!!
canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan')
# draw the dots, shifted by the scrollX offset
for (cx, cy) in app.dots:
cx -= app.scrollX # <-- This is where we scroll each dot!!!
canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen')
# draw the x and y axes
x = app.width/2 - app.scrollX # <-- This is where we scroll the axis!
y = app.height/2
canvas.create_line(x, 0, x, app.height)
canvas.create_line(0, y, app.width, y)
# draw the instructions and the current scrollX
x = app.width/2
canvas.create_text(x, 20, text='Use arrows to move left or right',
fill='black')
canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}',
fill='black')
runApp(width=300, height=300)
- SideScroller3
# SideScroller3:
# Now with walls that track when you run into them (but
# ignore while you are still crossing them).
from cmu_112_graphics import *
def appStarted(app):
app.scrollX = 0
app.scrollMargin = 50
app.playerX = app.scrollMargin
app.playerY = 0
app.playerWidth = 10
app.playerHeight = 20
app.walls = 5
app.wallPoints = [0]*app.walls
app.wallWidth = 20
app.wallHeight = 40
app.wallSpacing = 90 # wall left edges are at 90, 180, 270,...
app.currentWallHit = -1 # start out not hitting a wall
def getPlayerBounds(app):
# returns absolute bounds, not taking scrollX into account
(x0, y1) = (app.playerX, app.height/2 - app.playerY)
(x1, y0) = (x0 + app.playerWidth, y1 - app.playerHeight)
return (x0, y0, x1, y1)
def getWallBounds(app, wall):
# returns absolute bounds, not taking scrollX into account
(x0, y1) = ((1+wall) * app.wallSpacing, app.height/2)
(x1, y0) = (x0 + app.wallWidth, y1 - app.wallHeight)
return (x0, y0, x1, y1)
def getWallHit(app):
# return wall that player is currently hitting
# note: this should be optimized to only check the walls that are visible
# or even just directly compute the wall without a loop
playerBounds = getPlayerBounds(app)
for wall in range(app.walls):
wallBounds = getWallBounds(app, wall)
if (boundsIntersect(app, playerBounds, wallBounds) == True):
return wall
return -1
def boundsIntersect(app, boundsA, boundsB):
# return l2<=r1 and t2<=b1 and l1<=r2 and t1<=b2
(ax0, ay0, ax1, ay1) = boundsA
(bx0, by0, bx1, by1) = boundsB
return ((ax1 >= bx0) and (bx1 >= ax0) and
(ay1 >= by0) and (by1 >= ay0))
def checkForNewWallHit(app):
# check if we are hitting a new wall for the first time
wall = getWallHit(app)
if (wall != app.currentWallHit):
app.currentWallHit = wall
if (wall >= 0):
app.wallPoints[wall] += 1
def makePlayerVisible(app):
# scroll to make player visible as needed
if (app.playerX < app.scrollX + app.scrollMargin):
app.scrollX = app.playerX - app.scrollMargin
if (app.playerX > app.scrollX + app.width - app.scrollMargin):
app.scrollX = app.playerX - app.width + app.scrollMargin
def movePlayer(app, dx, dy):
app.playerX += dx
app.playerY += dy
makePlayerVisible(app)
checkForNewWallHit(app)
def sizeChanged(app):
makePlayerVisible(app)
def mousePressed(app, event):
app.playerX = event.x + app.scrollX
checkForNewWallHit(app)
def keyPressed(app, event):
if (event.key == "Left"): movePlayer(app, -5, 0)
elif (event.key == "Right"): movePlayer(app, +5, 0)
elif (event.key == "Up"): movePlayer(app, 0, +5)
elif (event.key == "Down"): movePlayer(app, 0, -5)
def redrawAll(app, canvas):
# draw the base line
lineY = app.height/2
lineHeight = 5
canvas.create_rectangle(0, lineY, app.width, lineY+lineHeight,fill="black")
# draw the walls
# (Note: should optimize to only consider walls that can be visible now!)
sx = app.scrollX
for wall in range(app.walls):
(x0, y0, x1, y1) = getWallBounds(app, wall)
fill = "orange" if (wall == app.currentWallHit) else "pink"
canvas.create_rectangle(x0-sx, y0, x1-sx, y1, fill=fill)
(cx, cy) = ((x0+x1)/2 - sx, (y0 + y1)/2)
canvas.create_text(cx, cy, text=str(app.wallPoints[wall]), fill='black')
cy = lineY + 5
canvas.create_text(cx, cy, text=str(wall), anchor=N, fill='black')
# draw the player
(x0, y0, x1, y1) = getPlayerBounds(app)
canvas.create_oval(x0 - sx, y0, x1 - sx, y1, fill="cyan")
# draw the instructions
msg = "Use arrows to move, hit walls to score"
canvas.create_text(app.width/2, 20, text=msg, fill='black')
runApp(width=300, height=300)
- Playing Sounds
We will wrap this up with two ways to play sounds. There are more sophisticated
ways to play sounds. This just meant to be a quick way to quickly get some
sounds in your app.
In order to run these examples, first download
button.mp3
- Playing Sounds with Pygame
Note that you may not use Pygame in general before MVP, but you may
use Pygame even before then if it is strictly used for sounds in this way:
# This demos playing sounds using Pygame:
from cmu_112_graphics import *
import pygame
class Sound(object):
def __init__(self, path):
self.path = path
self.loops = 1
pygame.mixer.music.load(path)
# Returns True if the sound is currently playing
def isPlaying(self):
return bool(pygame.mixer.music.get_busy())
# Loops = number of times to loop the sound.
# If loops = 1 or 1, play it once.
# If loops > 1, play it loops + 1 times.
# If loops = -1, loop forever.
def start(self, loops=1):
self.loops = loops
pygame.mixer.music.play(loops=loops)
# Stops the current sound from playing
def stop(self):
pygame.mixer.music.stop()
def appStarted(app):
pygame.mixer.init()
app.sound = Sound("button.mp3")
def appStopped(app):
app.sound.stop()
def keyPressed(app, event):
if (event.key == 's'):
if app.sound.isPlaying(): app.sound.stop()
else: app.sound.start()
elif (event.key == 'l'):
app.sound.start(loops=-1)
elif event.key.isdigit():
app.sound.start(loops=int(event.key))
def timerFired(app):
pass
def redrawAll(app, canvas):
canvas.create_text(app.width/2, app.height/2-60,
text=f'{app.sound.path} (loops = {app.sound.loops})',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2-20,
text=f'sound is playing = {app.sound.isPlaying()}',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2+20,
text='Press s to start/stop sound',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2+60,
text='Press l to loop sound',
font='Arial 30 bold', fill='black')
runApp(width=600, height=200)
- Playing Sounds with afplay (Mac only)
If Pygame does not work for you, here is an alternative that only runs on Macs
and is a bit slower, but runs without Pygame:
# This demos playing sounds using afplay (Mac only)
import subprocess, threading, time
from cmu_112_graphics import *
class Sound(object):
def __init__(self, path):
self.path = path
self.process = None
self.loop = False
def isPlaying(self):
return (self.process is not None)
def checkProcess(self):
# This method is run inside a separate thread
# so the main thread does not hang while this runs.
while self.process is not None:
if (self.process.poll() is not None):
self.process = None
else:
time.sleep(0.2)
if self.loop:
self.start(loop=True)
def start(self, loop=False):
self.stop()
self.loop = loop
self.process = subprocess.Popen(['afplay', self.path])
threading.Thread(target=self.checkProcess).start()
def stop(self):
process = self.process
self.loop = False
self.process = None
if (process is not None):
try: process.kill()
except: pass
def appStarted(app):
app.sound = Sound('button.mp3')
def appStopped(app):
app.sound.stop()
def keyPressed(app, event):
if (event.key == 's'):
if app.sound.isPlaying(): app.sound.stop()
else: app.sound.start()
elif (event.key == 'l'):
app.sound.start(loop=True)
def redrawAll(app, canvas):
canvas.create_text(app.width/2, app.height/2-60,
text=f'{app.sound.path} (loop = {app.sound.loop})',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2-20,
text=f'sound is playing = {app.sound.isPlaying()}',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2+20,
text='Press s to start/stop sound',
font='Arial 30 bold', fill='black')
canvas.create_text(app.width/2, app.height/2+60,
text='Press l to loop sound',
font='Arial 30 bold', fill='black')
runApp(width=600, height=200)