15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations


  1. Import File (eventBasedAnimation.py)
  2. All Examples (eventBasedAnimationNotesExamples.py)
  3. Basic step animation (using drawFn)
  4. Using initFn and stepFn
  5. Custom Title and About Text
  6. Basic non-step (non-parametric) animation (using data fields)
  7. Alternate basic non-step animation
  8. Using mouseFn
  9. Using keyFn
  10. Sweeping ball demo (using mouseFn, keyFn, and stepFn)
  11. More events demo (motion, drag, and release events)
  12. Print data for debugging
  13. Error case #1: creating a new data field in a draw fn
  14. Error case #2: modifying a mutable data field in a draw fn
  15. Error case #3: saving canvas to global and trying to draw in an event fn
  16. Error case #4: add new canvas to data
  17. Polygon demo with undo/redo
  18. 2d grid animation (with viewToModel and modelToView)
  19. Playing card demo (with images)
  20. Background audio demo (and using quitFn)

  1. Import File (eventBasedAnimation.py)
    To run these examples, download eventBasedAnimation.py and save it in the same folder where you are editing and running your animation.

  2. All Examples (eventBasedAnimationNotesExamples.py)
    All these examples are in this file: eventBasedAnimationNotesExamples.py

  3. Basic step animation (using drawFn)
    import eventBasedAnimation
    
    def simpleStepAnimationDrawFn(canvas, data):
        (cx, cy, r) = ((5 * data.step) % data.width, data.height/2, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="blue")
    
    eventBasedAnimation.run(
        drawFn=simpleStepAnimationDrawFn,
        timerDelay=10
        )
    

  4. Using initFn and stepFn
    import eventBasedAnimation
    
    def customStepAnimationInitFn(data):
        data.counter = 1
        data.fill = "blue"
    
    def customStepAnimationStepFn(data):
        data.counter += 1
        if (data.counter % 20 == 0):
            data.fill = "cyan" if (data.fill == "blue") else "blue"
    
    def customStepAnimationDrawFn(canvas, data):
        (cx, cy, r) = ((5 * data.counter) % data.width, data.height/2, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=data.fill)
    
    eventBasedAnimation.run(
        initFn=customStepAnimationInitFn,
        stepFn=customStepAnimationStepFn,
        drawFn=customStepAnimationDrawFn,
        timerDelay=10
        )
    

  5. Custom Title and About Text
    import eventBasedAnimation
    
    def customTitleAndAboutTextInitFn(data):
        data.counter = 1
        data.fill = "blue"
        data.aboutText = "This is the 'about' text!\nPut anything here!"
        data.windowTitle = "Put your window title here!"
    
    def customTitleAndAboutTextStepFn(data):
        data.counter += 1
        if (data.counter % 20 == 0):
            data.fill = "cyan" if (data.fill == "blue") else "blue"
    
    def customTitleAndAboutTextDrawFn(canvas, data):
        (cx, cy, r) = ((5 * data.counter) % data.width, data.height/2, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=data.fill)
        canvas.create_text(data.width/2, 20, text="Check out the window title, and")
        canvas.create_text(data.width/2, 40, text="press ctrl-a for the about box!")
    
    eventBasedAnimation.run(
        initFn=customTitleAndAboutTextInitFn,
        stepFn=customTitleAndAboutTextStepFn,
        drawFn=customTitleAndAboutTextDrawFn,
        timerDelay=10
        )
    

  6. Basic non-step (non-parametric) animation (using data fields)
    import eventBasedAnimation
    
    def nonStepAnimationInitFn(data):
        data.x = 0
        data.dx = +5
        data.aboutText = data.windowTitle = "non-step (non-parametric) animation"
    
    def nonStepAnimationStepFn(data):
        data.x += data.dx
        if (data.x > data.width):
            data.x = data.width
            data.dx = -data.dx
        elif (data.x < 0):
            data.x = 0
            data.dx = -data.dx
    
    def nonStepAnimationDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.height/2, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="orange")
    
    eventBasedAnimation.run(
        initFn=nonStepAnimationInitFn,
        stepFn=nonStepAnimationStepFn,
        drawFn=nonStepAnimationDrawFn,
        timerDelay=10
        )
    

  7. Alternate basic non-step animation
    import eventBasedAnimation
    
    def nonStepAnimationInitFn(data):
        data.x = 0
        data.headingRight = True
        data.speed = 5
        data.aboutText = data.windowTitle = "alternate non-step animation"
    
    def nonStepAnimationStepFn(data):
        if (data.headingRight == True):
            data.x += data.speed
            if (data.x > data.width):
                data.x = data.width
                data.headingRight = False
        else:
            data.x -= data.speed
            if (data.x < 0):
                data.x = 0
                data.headingRight = True
    
    def nonStepAnimationDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.height/2, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="magenta")
    
    eventBasedAnimation.run(
        initFn=nonStepAnimationInitFn,
        stepFn=nonStepAnimationStepFn,
        drawFn=nonStepAnimationDrawFn,
        timerDelay=10
        )
    

  8. Using mouseFn
    import eventBasedAnimation
    
    def mouseDemoInitFn(data):
        (data.x, data.y) = (data.width/2, data.height/2)
        data.aboutText = data.windowTitle = "mouseDemo (click to move ball)"
    
    def mouseDemoMouseFn(event, data):
        (data.x, data.y) = (event.x, event.y)
    
    def mouseDemoDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.y, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="darkGreen")
    
    eventBasedAnimation.run(
        initFn=mouseDemoInitFn,
        mouseFn=mouseDemoMouseFn,
        drawFn=mouseDemoDrawFn,
        )
    

  9. Using keyFn
    import eventBasedAnimation
    
    def keyDemoInitFn(data):
        (data.x, data.y) = (data.width/2, data.height/2)
        data.speed = 20
        data.aboutText = data.windowTitle = "keyDemo (use arrows to move ball)"
    
    def keyDemoKeyFn(event, data):
        if (event.keysym == "Up"):      data.y -= data.speed
        elif (event.keysym == "Down"):  data.y += data.speed
        elif (event.keysym == "Left"):  data.x -= data.speed
        elif (event.keysym == "Right"): data.x += data.speed
    
    def keyDemoDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.y, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="darkGreen")
    
    eventBasedAnimation.run(
        initFn=keyDemoInitFn,
        keyFn=keyDemoKeyFn,
        drawFn=keyDemoDrawFn,
        )
    

  10. Sweeping ball demo (using mouseFn, keyFn, and stepFn)
    import eventBasedAnimation
    
    def sweepingBallInitFn(data):
        (data.x, data.y) = (data.width/2, data.height/2)
        data.speed = 25
        data.aboutText = data.windowTitle = "Sweeping ball demo"
    
    def sweepingBallKeyFn(event, data):
        if (event.keysym == "Up"):
            data.y = (data.y - data.speed) % data.height
        elif (event.keysym == "Down"):
            data.y = (data.y + data.speed) % data.height
    
    def sweepingBallMouseFn(event, data):
        (data.x, data.y) = (event.x, event.y)
    
    def sweepingBallStepFn(data):
        data.x = (data.x + 10) % data.width
    
    def sweepingBallDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.y, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="chartreuse")
        canvas.create_text(data.width/2, 20,
                           text="Try up/down keys + mouse press anywhere")
    
    eventBasedAnimation.run(
        initFn=sweepingBallInitFn,
        stepFn=sweepingBallStepFn,
        mouseFn=sweepingBallMouseFn,
        keyFn=sweepingBallKeyFn,
        drawFn=sweepingBallDrawFn,
        timerDelay=100,
        )
    

  11. More events demo (motion, drag, and release events)
    import eventBasedAnimation
    
    def moreEventsDemoInitFn(data):
        (data.x, data.y) = (data.width/2, data.height/2)
        data.lastAction = "Waiting"
        data.fill = "yellow"
        data.aboutText = data.windowTitle = "moreEventsDemo (use mouse + keys)"
    
    def moreEventsDemoMouseMoveFn(event, data):
        (data.x, data.y) = (event.x, event.y)
        data.lastAction = "Mouse Move at " + str((event.x, event.y))
        data.fill = "red"
    
    def moreEventsDemoMouseFn(event, data):
        (data.x, data.y) = (event.x, event.y)
        data.lastAction = "Mouse Press at " + str((event.x, event.y))
        data.fill = "purple"
    
    def moreEventsDemoMouseDragFn(event, data):
        (data.x, data.y) = (event.x, event.y)
        data.lastAction = "Mouse Drag at " + str((event.x, event.y))
        data.fill = "orange"
    
    def moreEventsDemoMouseReleaseFn(event, data):
        (data.x, data.y) = (event.x, event.y)
        data.lastAction = "Mouse Release at " + str((event.x, event.y))
        data.fill = "cyan"
    
    def moreEventsDemoKeyFn(event, data):
        data.lastAction = "Key Press: " + event.keysym
    
    def moreEventsDemoKeyReleaseFn(event, data):
        data.lastAction = "Key Release: " + event.keysym
    
    def moreEventsDemoDrawFn(canvas, data):
        (cx, cy, r) = (data.x, data.y, 20)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=data.fill)
        canvas.create_text(data.width/2, 20,
                           text=data.lastAction, font="Arial 20 bold")
    
    eventBasedAnimation.run(
        initFn=moreEventsDemoInitFn,
        mouseFn=moreEventsDemoMouseFn,
        mouseMoveFn=moreEventsDemoMouseMoveFn,
        mouseDragFn=moreEventsDemoMouseDragFn,
        mouseReleaseFn=moreEventsDemoMouseReleaseFn,
        keyFn=moreEventsDemoKeyFn,
        keyReleaseFn=moreEventsDemoKeyReleaseFn,
        drawFn=moreEventsDemoDrawFn,
        )
    

  12. Print data for debugging
    import eventBasedAnimation
    
    def debuggingDemoInitFn(data):
        data.foo = "wow"
        data.bar = [1,2,3]
        data.aboutText = data.windowTitle = "print-data-for-debugging demo"
    
    def debuggingDemoKeyFn(event, data):
        print data
    
    def debuggingDemoDrawFn(canvas, data):
        canvas.create_text(data.width/2, data.height/2,
                           text="Press any key to print\ndebugging data to console",
                           font="Arial 20 bold")
    
    eventBasedAnimation.run(
        initFn=debuggingDemoInitFn,
        keyFn=debuggingDemoKeyFn,
        drawFn=debuggingDemoDrawFn
        )
    

  13. Error case #1: creating a new data field in a draw fn
    import eventBasedAnimation
    
    def drawFnWithModelWriteError1(canvas, data):
        data.foo = "this should fail!"
    
    eventBasedAnimation.run(drawFn=drawFnWithModelWriteError1)
    

  14. Error case #2: modifying a mutable data field in a draw fn
    import eventBasedAnimation
    
    def initFn(data):
        data.foo = [42]
    
    def drawFnWithModelWriteError2(canvas, data):
        data.foo[0] = "this should fail!"
    
    eventBasedAnimation.run(initFn=initFn, drawFn=drawFnWithModelWriteError2)
    

  15. Error case #3: saving canvas to global and trying to draw in an event fn
    
    import eventBasedAnimation
    
    canvas_as_global = None
    
    def stepFnCanvasAsGlobalError(data):
        if (canvas_as_global != None):
            msg = "This should fail, using a global canvas from a stepFn!"
            canvas_as_global.create_text(data.width/2, data.height/2, text=msg)
            canvas_as_global.update()
    
    def drawFnCanvasAsGlobalError(canvas, data):
        canvas.create_oval(10, 10, 20, 20, fill="blue")
        global canvas_as_global
        canvas_as_global = canvas
    
    eventBasedAnimation.run(drawFn=drawFnCanvasAsGlobalError,
                            stepFn=stepFnCanvasAsGlobalError)
    

  16. Error case #4: add new canvas to data
    import eventBasedAnimation
    from Tkinter import *
    
    def initFnWithExtraCanvasError(data):
        data.canvas = Canvas() # this should fail!
    
    eventBasedAnimation.run(initFn=initFnWithExtraCanvasError)
    

  17. Polygon demo with undo/redo
    import eventBasedAnimation
    
    def polygonDemoInitFn(data):
        data.points = [ ]
        data.undoList = [ ]
        data.aboutText = data.windowTitle = "Polygon Demo"
    
    def polygonDemoMouseFn(event, data):
        data.points.append((event.x, event.y))
        data.undoList = [ ]
    
    def polygonDemoKeyFn(event, data):
        if (event.keysym == "u"):
            if (len(data.points) > 0):
                data.undoList.append(data.points.pop())
        elif (event.keysym == "r"):
            if (len(data.undoList) > 0):
                data.points.append(data.undoList.pop())
        elif (event.keysym == "c"):
            data.points = [ ]
            data.undoList = [ ]
    
    def polygonDemoDrawFn(canvas, data):
        if (data.points != []):
            canvas.create_polygon(data.points, fill="gold", outline="black")
        canvas.create_text(data.width/2, 20,
                           text="click to add point. u=undo. r=redo. c=clear.")
        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.undoList)) + " point(s) on undoList")
    
    eventBasedAnimation.run(
        initFn=polygonDemoInitFn,
        mouseFn=polygonDemoMouseFn,
        keyFn=polygonDemoKeyFn,
        drawFn=polygonDemoDrawFn,
        )
    

  18. 2d grid animation (with viewToModel and modelToView)
    import eventBasedAnimation
    
    def gridDemoInitFn(data):
        data.rows = 4
        data.cols = 8
        data.margin = 5 # margin around grid
        data.ballRow = data.ballCol = 0
        data.rectRow = data.rectCol = 0
        (data.foodRow, data.foodCol) = (data.rows/4, data.cols/4)
        data.score = 0
        data.aboutText = data.windowTitle = "Grid demo (click to move food)"
    
    def gridDemoStepFn(data):
        # sweep ball left-to-right, top-to-bottom
        if (data.ballRow < data.rows-1):
            data.ballRow += 1
        else:
            data.ballRow = 0
            data.ballCol = (1 + data.ballCol) % data.cols
        # sweep rect clockwise around edges
        if ((data.rectRow == 0) and (data.rectCol < data.cols-1)):
            data.rectCol += 1
        elif ((data.rectCol == data.cols-1) and (data.rectRow < data.rows-1)):
            data.rectRow += 1
        elif ((data.rectRow == data.rows-1) and (data.rectCol > 0)):
            data.rectCol -= 1
        else:
            data.rectRow -= 1
        # check if ball ate food
        if ((data.ballRow == data.foodRow) and (data.ballCol == data.foodCol)):
            data.score += 1
    
    def pointInGrid(x, y, data):
        # return True if (x, y) is inside the grid defined by data.
        # data includes rows, cols, width, height, and margin
        return ((data.margin <= x <= data.width-data.margin) and
                (data.margin <= y <= data.height-data.margin))
    
    def getCell(x, y, data):
        # aka "viewToModel"
        # return the (row, col) in which (x, y) occurred or None if outside grid.
        if (not pointInGrid(x, y, data)):
            return None
        gridWidth = data.width - 2*data.margin
        gridHeight = data.height - 2*data.margin
        row = int(round((y - data.margin) * data.rows / gridHeight))
        col = int(round((x - data.margin) * data.cols / gridWidth))
        # triple-check that we are in bounds
        row = min(data.rows-1, max(0, row))
        col = min(data.cols-1, max(0, col))
        return (row, col)
    
    def getCellBounds(row, col, data):
        # aka "modelToView"
        # data includes rows, cols, width, height, and margin
        # 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 getCellCircleBounds(row, col, data):
        # another "modelToView"
        # Instead of bounding box, return (cx, cy, r) of center
        # of cell, where r is largest radius that still fits in cell
        (x0, y0, x1, y1) = getCellBounds(row, col, data)
        (cx, cy, r) = ((x0+x1)/2, (y0+y1)/2, min(x1-x0,y1-y0)/2)
        return (cx, cy, r)
    
    def gridDemoMouseFn(event, data):
        if (pointInGrid(event.x, event.y, data)):
            print getCell(event.x, event.y, data)
            (data.foodRow, data.foodCol) = getCell(event.x, event.y, data)
    
    def gridDemoDrawFn(canvas, data):
        # draw grid of cells
        for row in xrange(data.rows):
            for col in xrange(data.cols):
                (x0, y0, x1, y1) = getCellBounds(row, col, data)
                canvas.create_rectangle(x0, y0, x1, y1)
        # draw score
        canvas.create_text(data.width/2, data.height/2,
            text=str(data.score), font="Arial 64 bold", fill="darkGray")
        # draw rect
        (x0, y0, x1, y1) = getCellBounds(data.rectRow, data.rectCol, data)
        canvas.create_rectangle(x0, y0, x1, y1, fill="cyan")
        # draw food (before drawing the ball)
        (cx, cy, r) = getCellCircleBounds(data.foodRow, data.foodCol, data)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="salmon")
        # draw ball
        (cx, cy, r) = getCellCircleBounds(data.ballRow, data.ballCol, data)
        canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill="green")
    
    eventBasedAnimation.run(
        initFn=gridDemoInitFn,
        stepFn=gridDemoStepFn,
        mouseFn=gridDemoMouseFn,
        drawFn=gridDemoDrawFn,
        timerDelay=100
        )
    

  19. Playing card demo (with images)
    Note: To run this, you must download playing-card-gifs.zip, and unzip this file, and save the resulting folder (playing-card-gifs) at the same level as your animation code (so images are loaded using the path "playing-card-gifs/cardName.gif". For hw problems that require using card images, we will include that folder in this way, so you do not have to submit the card images with your hw.
    import eventBasedAnimation
    from Tkinter import *
    
    def playingCardDemoInitFn(data):
        data.aboutText = data.windowTitle = "Playing Card Demo"
        loadPlayingCardImages(data) # always load images in init!
    
    def loadPlayingCardImages(data):
        cards = 55 # cards 1-52, back, joker1, joker2
        data.cardImages = [ ]
        for card in xrange(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 playingCardDemoDrawFn(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 xrange(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")
    
    eventBasedAnimation.run(
        initFn=playingCardDemoInitFn,
        drawFn=playingCardDemoDrawFn,
        width=420, height=360, timerDelay=250
        )
    

  20. Background audio demo (and using quitFn)
    Note: To run this, you must download simpleAudio.py, and save it at the same level as your animation code. For hw problems that require using background audio, we will include that file in this way, along with the required audio files, so you do not have to submit these with your submission.

    To run this, you must also download sample.wav, or provide your own sample.wav file in the same folder. This particular sample.wav file is from here.
    import eventBasedAnimation
    import simpleAudio
    
    def backgroundAudioInitFn(data):
        simpleAudio.stopSound() # in case we re-init with ctrl-r
        simpleAudio.startSound("sample.wav", async=True, loop=True)
    
    def backgroundAudioDrawFn(canvas, data):
        canvas.create_text(data.width/2, data.height/2,
                           text="You should hear music now", font="Arial 16 bold")
    
    def backgroundAudioQuitFn(data):
        simpleAudio.stopSound()
    
    eventBasedAnimation.run(
        initFn=backgroundAudioInitFn,
        drawFn=backgroundAudioDrawFn,
        quitFn=backgroundAudioQuitFn,
        )