Jed Rembold
October 13, 2025
When the function to the right is run, what does the screen look like just after 1 second has passed?
def rev_q():
def step():
rect.move(1, 1)
def once():
rect.set_filled(True)
gw = GWindow(200, 200)
rect = GRect(0, 0, 25, 25)
gw.add(rect)
gw.set_interval(step, 20)
gw.set_timeout(once, 1000)
Many would probably try to approach this doing something like as follows:
def growing_circles():
gw = GWindow(WIDTH, HEIGHT)
for i in range(NUM_CIRCLES):
|||Create a new circle|||
|||Animate the circle to grow it|||
|||Wait for the animation to complete|||The problem here is that there is no clear way to “wait” for an animation to complete
Instead, we need an event callback that takes care of both circle creation (when needed) and growing animations
Need to keep track of what the program should be doing, and then have the timer callback function handle whatever is needed
Conceptually, for these circles, might look more like this:
def step():
if |||there is a circle needing growing|||
|||then increase its size|||
elif |||a new circle needs to be created|||
|||then create one|||
else:
|||stop the madness by stopping the timer!|||from pgl import GWindow, GOval
import random
GWIDTH = 500
GHEIGHT = 400
N_CIRCLES = 20
MIN_RADIUS = 15
MAX_RADIUS = 100
DELTA_TIME = 10
DELTA_SIZE = 1
def random_color():
color = "#"
for i in range(6):
color += random.choice("0123456789ABCDEF")
return color
def create_filled_circle(x, y, r, color="black"):
circ = Goval(x-r, y-r, 2*r, 2*r)
circ.set_filled(True)
circ.set_color(color)
return circ
def growing_circles():
def start_new_circle():
r = random.uniform(MIN_RADIUS, MAX_RADIUS)
x = random.uniform(r, GWIDTH - r)
y = random.uniform(r, GHEIGHT - r)
gw.circle = create_filled_circle(
x, y,
0, random_color()
)
gw.desired_size = 2 * r
gw.current_size = 0
gw.circles_created += 1
return gw.circle
def step():
# Grow a circle if needed
if gw.current_size < gw.desired_size:
gw.current_size += DELTA_SIZE
x = gw.circle.get_x() - DELTA_SIZE / 2
y = gw.circle.get_y() - DELTA_SIZE / 2
gw.circle.set_bounds(
x, y,
gw.current_size,
gw.current_size
)
# or add a circle if you can
elif gw.circles_created < N_CIRCLES:
gw.add(start_new_circle())
# or stop
else:
timer.stop()
gw = GWindow(GWIDTH, GHEIGHT)
gw.circles_created = 0
gw.current_size = 0
gw.desired_size = 0
timer = gw.set_interval(step, DELTA_TIME)
from pgl import GWindow, GOval, GLine
from pgl_tools import create_filled_circle
def two_body():
def step():
# Compute forces and accelerations
dx = planet1.get_x() - planet2.get_x()
dy = planet1.get_y() - planet2.get_y()
r3 = (dx ** 2 + dy ** 2) ** (3 / 2)
ax = 1000 / r3 * dx
ay = 1000 / r3 * dy
# Update velocities
gw.vx1 += -ax
gw.vy1 += -ay
gw.vx2 += ax
gw.vy2 += ay
# Augment history paths
path1 = GLine(
planet1.get_x() + 10,
planet1.get_y() + 10,
planet1.get_x() + 10 + gw.vx1,
planet1.get_y() + 10 + gw.vy1,
)
path1.set_color("red")
path1.set_line_width(3)
path2 = GLine(
planet2.get_x() + 10,
planet2.get_y() + 10,
planet2.get_x() + 10 + gw.vx2,
planet2.get_y() + 10 + gw.vy2,
)
path2.set_color("cyan")
path2.set_line_width(3)
# Move planets
planet1.move(gw.vx1, gw.vy1)
planet2.move(gw.vx2, gw.vy2)
gw.add(path1)
gw.add(path2)
gw = GWindow(600, 600)
# Defining state variables
gw.vx1, gw.vy1 = 0, 1
gw.vx2, gw.vy2 = 0, -1
planet1 = create_filled_circle(200, 200, 10, "red")
planet2 = create_filled_circle(400, 200, 10, "cyan")
gw.add(planet1)
gw.add(planet2)
gw.set_interval(step, 30)
if __name__ == '__main__':
two_body()
GArc class represents an arc formed
by taking a section of the perimeter of an oval.GArc class is a
GFillableObject, and so you can call
.set_filled() on a
GArc objectdef filled_arc():
gw = GWindow(400, 400)
arc = GArc(50, 50,
350, 350,
90, 135)
arc.set_color("orange")
arc.set_filled(True)
gw.add(arc)

GPolygon classGPolygons are
GFillableObjects, so they can be filledGPolygon function creates an
empty polygon, to which you then can add vertexes.add_vertex(x,y) on the
GPolygon object
x and y
measured relative to the reference point.add_vertex(x,y) adds another new vertex
relative to the reference point.add_edge(dx,dy) adds a new vertex
relative to the preceding vertex.add_polar_edge(r, theta) adds a new
vertex relative to the previous using polar coordinatesdef triangle_by_vertex():
def create_triangle(b, h):
tri = GPolygon()
tri.add_vertex(-b / 2, h / 2)
tri.add_vertex(b / 2, h / 2)
tri.add_vertex(0, -h / 2)
return tri
gw = GWindow(500, 500)
triangle = create_triangle(200, 200)
triangle.set_filled(True)
triangle.set_color("red")
gw.add(triangle, 250, 250)
def triangle_by_polar_edge():
def create_eq_triangle(side):
tri = GPolygon()
tri.add_vertex(0, 0)
for i in range(0, 360, 120):
tri.add_polar_edge(side, i)
return tri
gw = GWindow(500, 500)
triangle = create_eq_triangle(100)
triangle.set_filled(True)
triangle.set_color("green")
gw.add(triangle, 250, 250)
GCompound class makes it possible to
combine several graphical objects so that the entire structure behaves
as a single objectGWindow and
GObject
GCompound,
you place them relative to the reference pointGCompound to a canvas,
you set the location of the reference pointdef my_axe():
def create_axe():
axe = GCompound()
shaft = GRect(-15, 0, 30, 300)
shaft.set_filled(True)
shaft.set_color("brown")
axe.add(shaft)
blade = GPolygon()
blade.add_vertex(0, 0)
blade.add_vertex(200, -50)
blade.add_vertex(200, 50)
blade.set_filled(True)
blade.set_color("gray")
axe.add(blade, -80, 50)
return axe
gw = GWindow(500, 500)
axe = create_axe()
gw.add(axe, 250, 100)