CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Animation Part 2: Time-Based Animations in Tkinter


 Learning Goal: make time-based animations in Tkinter. In particular:
Note: We will only run animations in Standard Python. These examples will not run in Brython.
  1. Updated Starter Code
  2. Controller: timerFired
  3. Check 5.5
  4. Example: Timer
  5. Example: Changing Colors
  6. Example: Bouncing and Pausing Square
  7. Example: Snake Game

  1. Updated Starter Code
    We have to update run() to add time-based animation. Again, you do not need to understand the run() function! We might get into this briefly on Tuesday. If not and you're curious, ask!
    # Updated Animation Starter Code from tkinter import * #################################### # customize these functions #################################### def init(data): # load data.xyz as appropriate pass def mousePressed(event, data): # use event.x and event.y pass def keyPressed(event, data): # use event.char and event.keysym pass def timerFired(data): pass def redrawAll(canvas, data): # draw in canvas pass #################################### # 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(400, 200)
    Result: (...yes, it's a blank window)


  2. Controller: timerFired
    An important difference between timerFired and keyPressed/mousePressed is that timerFired receives no event.
    Instead, it gets called at regular intervals, based on the data.timerDelay variable defined in run().
    It's up to you to 'create' internal events using this time loop!
    # Timer fired is called every timerDelay milliseconds # We can use this function to effectively loop over time, # thereby simulating movement over time def timerFired(data): data.timerCalls += 1 def init(data): # The variable data.timerDelay is referenced in timerFiredWrapper # Change it to affect how often timerFired is called data.timerDelay = 100 # 100 millisecond == 0.1 seconds data.timerCalls = 0 def mousePressed(event, data): pass def keyPressed(event, data): pass def redrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2, text="Timer Calls: " + str(data.timerCalls))
    Result:



  3. Check 5.5

  4. Note: we'll go over all the following examples in class. You do not need to review them in advance unless you want to.
  5. Example: Timer
    Big idea: Our code doesn't really tell us what time it is, nor how much time has passed. Instead, timerFired(data) runs at regular intervals, like the ticking of a clock. If we want to keep track of passing time, we have to do something like this:
    def init(data): data.timer = 0 data.seconds = 5 def timerFired(data): data.timer += 1 if (data.timer % 10 == 0): # the second clock drops every 10 timerFired calls data.seconds -= 1 if (data.seconds < 0): data.seconds = 5 def redrawAll(canvas, data): canvas.create_text(data.width//2, data.height//2, text="Timer Demo", font="Arial 30") canvas.create_text(data.width//2, data.height//4, text=str(data.seconds), font="Arial 40")
    Result:



  6. Example: Changing Colors
    # Changes the color of the square every second def init(data): data.timerDelay = 1000 # 1 second data.squareColor = "red" def timerFired(data): if data.squareColor == "red": data.squareColor = "green" elif data.squareColor == "green": data.squareColor = "blue" elif data.squareColor == "blue": data.squareColor = "red" def redrawAll(canvas, data): size = 50 canvas.create_rectangle(data.width/2 - size, data.height/2 - size, data.width/2 + size, data.height/2 + size, fill=data.squareColor)
    Result:



  7. Example: Bouncing and Pausing Square
    This code does a lot of things! Adherance to MVC and good style help make it readable, though.
    We'll discuss this in class, but see if you can identify what each function is supposed to do.
    # Draws a bouncing square which can be paused def init(data): data.squareLeft = 50 data.squareTop = 50 data.squareSize = 25 data.dx = 20 data.dy = 20 data.isPaused = False data.timerDelay = 50 def keyPressed(event, data): if (event.char == "p"): data.isPaused = not data.isPaused elif (event.char == "s"): doStep(data) def timerFired(data): if (not data.isPaused): doStep(data) def doStep(data): # Move horizontally data.squareLeft += data.dx # Check if the square has gone out of bounds if data.squareLeft < 0 or data.squareLeft + data.squareSize > data.width: # if so, reverse! data.dx = - data.dx # also, undo the move to avoid moving offscreen data.squareLeft += data.dx # Move vertically the same way data.squareTop += data.dy if data.squareTop < 0 or data.squareTop + data.squareSize > data.height: data.dy = - data.dy data.squareTop += data.dy def redrawAll(canvas, data): # draw the square canvas.create_rectangle(data.squareLeft, data.squareTop, data.squareLeft + data.squareSize, data.squareTop + data.squareSize, fill="yellow") # 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")
    Result:



  8. Example: Snake Game
    This is an example of how you can program a whole game using the animation framework!
    # Snake from tkinter import * import random def init(data): data.rows = 10 data.cols = 10 data.margin = 5 # margin around grid data.snake = [(data.rows/2, data.cols/2)] data.direction = (0, +1) # (drow, dcol) placeFood(data) data.timerDelay = 250 data.gameOver = False data.paused = True # getCellBounds from grid-demo.py def getCellBounds(row, col, data): # aka "modelToView" # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = data.width - 2*data.margin gridHeight = data.height - 2*data.margin x0 = data.margin + gridWidth * col / data.cols x1 = data.margin + gridWidth * (col+1) / data.cols y0 = data.margin + gridHeight * row / data.rows y1 = data.margin + gridHeight * (row+1) / data.rows return (x0, y0, x1, y1) def mousePressed(event, data): data.paused = False def keyPressed(event, data): if (event.keysym == "p"): data.paused = True; return elif (event.keysym == "r"): init(data); return if (data.paused or data.gameOver): return if (event.keysym == "Up"): data.direction = (-1, 0) elif (event.keysym == "Down"): data.direction = (+1, 0) elif (event.keysym == "Left"): data.direction = (0, -1) elif (event.keysym == "Right"): data.direction = (0, +1) # for debugging, take one step on any keypress takeStep(data) def timerFired(data): if (data.paused or data.gameOver): return takeStep(data) def takeStep(data): (drow, dcol) = data.direction (headRow, headCol) = data.snake[0] (newRow, newCol) = (headRow+drow, headCol+dcol) if ((newRow < 0) or (newRow >= data.rows) or (newCol < 0) or (newCol >= data.cols) or ((newRow, newCol) in data.snake)): data.gameOver = True else: data.snake.insert(0, (newRow, newCol)) if (data.foodPosition == (newRow, newCol)): placeFood(data) else: # didn't eat, so remove old tail (slither forward) data.snake.pop() def placeFood(data): data.foodPosition = None row0 = random.randint(0, data.rows-1) col0 = random.randint(0, data.cols-1) for drow in range(data.rows): for dcol in range(data.cols): row = (row0 + drow) % data.rows col = (col0 + dcol) % data.cols if (row,col) not in data.snake: data.foodPosition = (row, col) return def drawBoard(canvas, data): for row in range(data.rows): for col in range(data.cols): (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_rectangle(x0, y0, x1, y1, fill="white") def drawSnake(canvas, data): for (row, col) in data.snake: (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_oval(x0, y0, x1, y1, fill="blue") def drawFood(canvas, data): if (data.foodPosition != None): (row, col) = data.foodPosition (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_oval(x0, y0, x1, y1, fill="green") def drawGameOver(canvas, data): if (data.gameOver): canvas.create_text(data.width/2, data.height/2, text="Game over!", font="Arial 26 bold") def redrawAll(canvas, data): drawBoard(canvas, data) drawSnake(canvas, data) drawFood(canvas, data) drawGameOver(canvas, data)
    Result: