CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Animation Demos
Learning Goal: recognize common useful things we can do in animations. In particular:
- How to create multiple screens in a single animation.
- How to undo & redo actions.
- How to add images to an animation.
Note: We will only run animations in Standard Python. These examples will not run in Brython.
- Mode Demo
This demo shows how you can create a "mode dispatcher" to implement different screens in your code# Mode Demo from tkinter import * #################################### # init #################################### def init(data): # There is only one init, not one-per-mode data.mode = "splashScreen" data.score = 0 #################################### # mode dispatcher #################################### def mousePressed(event, data): if (data.mode == "splashScreen"): splashScreenMousePressed(event, data) elif (data.mode == "playGame"): playGameMousePressed(event, data) elif (data.mode == "help"): helpMousePressed(event, data) def keyPressed(event, data): if (data.mode == "splashScreen"): splashScreenKeyPressed(event, data) elif (data.mode == "playGame"): playGameKeyPressed(event, data) elif (data.mode == "help"): helpKeyPressed(event, data) def timerFired(data): if (data.mode == "splashScreen"): splashScreenTimerFired(data) elif (data.mode == "playGame"): playGameTimerFired(data) elif (data.mode == "help"): helpTimerFired(data) def redrawAll(canvas, data): if (data.mode == "splashScreen"): splashScreenRedrawAll(canvas, data) elif (data.mode == "playGame"): playGameRedrawAll(canvas, data) elif (data.mode == "help"): helpRedrawAll(canvas, data) #################################### # splashScreen mode #################################### def splashScreenMousePressed(event, data): pass def splashScreenKeyPressed(event, data): data.mode = "playGame" def splashScreenTimerFired(data): pass def splashScreenRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-20, text="This is a splash screen!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2+20, text="Press any key to play!", font="Arial 20") #################################### # help mode #################################### def helpMousePressed(event, data): pass def helpKeyPressed(event, data): data.mode = "playGame" def helpTimerFired(data): pass def helpRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-40, text="This is help mode!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2-10, text="How to play:", font="Arial 20") canvas.create_text(data.width/2, data.height/2+15, text="Do nothing and score points!", font="Arial 20") canvas.create_text(data.width/2, data.height/2+40, text="Press any key to keep playing!", font="Arial 20") #################################### # playGame mode #################################### def playGameMousePressed(event, data): data.score = 0 def playGameKeyPressed(event, data): if (event.keysym == 'h'): data.mode = "help" def playGameTimerFired(data): data.score += 1 def playGameRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-40, text="This is a fun game!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2-10, text="Score = " + str(data.score), font="Arial 20") canvas.create_text(data.width/2, data.height/2+15, text="Click anywhere to reset score", font="Arial 20") canvas.create_text(data.width/2, data.height/2+40, text="Press 'h' for help!", font="Arial 20") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) canvas.create_rectangle(0, 0, data.width, data.height, fill='white', width=0) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds root = Tk() root.resizable(width=False, height=False) # prevents resizing window init(data) # create the root and the canvas canvas = Canvas(root, width=data.width, height=data.height) canvas.configure(bd=0, highlightthickness=0) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)Result:
- Undo/Redo/Save/Load Demo
This demo shows how to implement a basic undo/redo system, and also shows how data can be saved and loaded from a text file.
That means you can have data that persists even if you exit your code!# Undo/Redo/Save/Load demo from tkinter import * import copy import os #Some helper functions to read and write to files def readFile(path): with open(path, "rt") as f: return f.read() def writeFile(path, contents): with open(path, "wt") as f: f.write(contents) #################################### # customize these functions #################################### def init(data): # The list of polygon points data.points=[] # The list of "undone" moves data.history=[] def mousePressed(event, data): # use event.x and event.y to append new polygon points data.points.append((event.x,event.y)) # let's clear the undo buffer when a new point is added data.history=[] def keyPressed(event, data): # use u and r to undo and redo actions if event.keysym=="u": data.history.append(data.points.pop()) elif event.keysym=="r": data.points.append(data.history.pop()) # Save by writing the polygon points as strings to a file elif event.keysym=="s": s="" for point in data.points: s+=str(point[0])+"@"+str(point[1])+"\n" writeFile("save.txt",s) # Load by reading the file and parsing the resulting string elif event.keysym=="l": s=readFile("save.txt") data.points=[] for line in s.splitlines(): tokens = line.split("@") print(tokens) p1=int(tokens[0]) p2=int(tokens[1]) data.points.append((p1,p2)) # Note that while this save/load strategy is general, you may need to store # very different things! If you need to save a very complex state, you # might consider researching the "pickle" module def timerFired(data): pass def redrawAll(canvas, data): if len(data.points)!=0: canvas.create_polygon(data.points, fill="green", outline="black") canvas.create_text(data.width/2, 20, text="click to add point. u=undo. r=redo. s=save, l=load.") canvas.create_text(data.width/2, 40, text=str(len(data.points)) + " point(s) in polygon") canvas.create_text(data.width/2, 60, text=str(len(data.history)) + " point(s) on undoList") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) canvas.create_rectangle(0, 0, data.width, data.height, fill='white', width=0) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds root = Tk() root.resizable(width=False, height=False) # prevents resizing window init(data) # create the root and the canvas canvas = Canvas(root, width=data.width, height=data.height) canvas.configure(bd=0, highlightthickness=0) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(800, 800)Result (without save/load):
- Images Demo
There is a lot of code here that just deals with organizing the cards and images.
The most important parts relating to images are creating PhotoImage data types and calling canvas.create_image!
Remember that tkinter can only handle .gifs so you'll want to check out PIL or pillow if you want .jpgs, .pngs etc.
To run this demo, first download playing-card-gifs.zip and unzip that file, so the folder playing-card-gifs is at the same level as this code.# Images Demo from tkinter import * def init(data): data.step = 0 loadPlayingCardImages(data) # always load images in init! def loadPlayingCardImages(data): cards = 55 # cards 1-52, back, joker1, joker2 data.cardImages = [ ] for card in range(cards): rank = (card%13)+1 suit = "cdhsx"[card//13] filename = "playing-card-gifs/%s%d.gif" % (suit, rank) data.cardImages.append(PhotoImage(file=filename)) def getPlayingCardImage(data, rank, suitName): suitName = suitName[0].lower() # only car about first letter suitNames = "cdhsx" assert(1 <= rank <= 13) assert(suitName in suitNames) suit = suitNames.index(suitName) return data.cardImages[13*suit + rank - 1] def getSpecialPlayingCardImage(data, name): specialNames = ["back", "joker1", "joker2"] return getPlayingCardImage(data, specialNames.index(name)+1, "x") def mousePressed(event, data): pass def keyPressed(event, data): pass def timerFired(data): data.step += 1 def redrawAll(canvas, data): suitNames = ["Clubs", "Diamonds", "Hearts", "Spades", "Xtras"] suit = (data.step//10) % len(suitNames) suitName = suitNames[suit] cards = 3 if (suitName == "Xtras") else 13 margin = 10 (left, top) = (margin, 40) for rank in range(1,cards+1): image = getPlayingCardImage(data, rank, suitName) if (left + image.width() > data.width): (left, top) = (margin, top + image.height() + margin) canvas.create_image(left, top, anchor=NW, image=image) left += image.width() + margin canvas.create_text(data.width/2, 20, text=suitName, font="Arial 28 bold") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) canvas.create_rectangle(0, 0, data.width, data.height, fill='white', width=0) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 250 # milliseconds root = Tk() root.resizable(width=False, height=False) # prevents resizing window init(data) # create the root and the canvas canvas = Canvas(root, width=data.width, height=data.height) canvas.configure(bd=0, highlightthickness=0) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(420, 360)Result:
- "Physics" demo (images, gravity, etc.)
This demo adds some features to the basic bouncing ball animation.
It may also help you to understand how to draw single images easier than the card demo.
To run this demo, first download kirby.gif and put it in the same folder as your python file.# Bouncing ball with gravity, mouse reaction (and Kirby) from tkinter import * #################################### # customize these functions #################################### import random # Draws a bouncing ball which can be paused def init(data): data.ballSize = 100 data.gravity = 1 data.ballX = random.randint(data.ballSize,data.width-data.ballSize) data.ballY = random.randint(data.ballSize,data.height-data.ballSize) data.dx = random.randint(1,4) data.dy = random.randint(1,4) data.isPaused = False data.timerDelay = 10 #Add a dampening effect data.boing = 0.95 #Add mouse attraction data.attract = 0.05 #Set up kirby image data.kirby = PhotoImage(file="kirby.gif") data.drawKirb = False def keyPressed(event, data): if (event.char == "p"): data.isPaused = not data.isPaused elif (event.char == "s"): doStep(data) if (event.char == "k"): data.drawKirb = not data.drawKirb # Click to ATTRACT ball to a given position def mousePressed(event, data): data.dx += data.attract * (event.x - data.ballX) data.dy += data.attract * (event.y - data.ballY) def timerFired(data): if (not data.isPaused): doStep(data) def doStep(data): # Move horizontally data.ballX += data.dx # Check if the ball has gone out of bounds if data.ballX-data.ballSize < 0 or data.ballX + data.ballSize > data.width: # if so, reverse! data.dx = - data.dx # also, undo the move to avoid moving offscreen data.ballX += data.dx # and steal some kinetic energy data.dx *= data.boing # Move vertically the same way data.ballY += data.dy if data.ballY-data.ballSize < 0 or data.ballY + data.ballSize > data.height: data.dy = - data.dy data.ballY += data.dy data.dy *= data.boing # Change vertical speed using gravity data.dy+=data.gravity def redrawAll(canvas, data): # draw the ball canvas.create_oval(data.ballX-data.ballSize, data.ballY-data.ballSize, data.ballX + data.ballSize, data.ballY + data.ballSize, fill="red") if data.drawKirb: canvas.create_image(data.ballX, data.ballY, image=data.kirby) # draw the text canvas.create_text(data.width/2, 20, text="Pressing 'p' pauses/unpauses timer") canvas.create_text(data.width/2, 40, text="Pressing 's' steps the timer once") canvas.create_text(data.width/2, 60, text="Pressing 'k' adds Kirby") canvas.create_text(data.width/2, 80, text="Click for boing!") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) canvas.create_rectangle(0, 0, data.width, data.height, fill='white', width=0) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds root = Tk() root.resizable(width=False, height=False) # prevents resizing window init(data) # create the root and the canvas canvas = Canvas(root, width=data.width, height=data.height) canvas.configure(bd=0, highlightthickness=0) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(800, 800)