I'm trying to debug some odd behavior of a graphics module I've built on my students' old Mac computers. I've tried to copy only the relevant things below, but I imagine that my question is probably going to answer the questions of why it's happening.
So, if you run the code below, you'll see an ellipse bouncing around in a rectangle. Roughly, the way this works is that the ellipse is comprised of around 200 points that basically act as a polygon approximating the ellipse. What's interesting is that on my student's 2016 macbook air there's significant 'drawing' error. When a Shape moves on the screen, it erases the pixels of the Shape currently drawn, then draws the Shape in the new location. I have not personally implemented any threading, so all undrawing should be theoretically be completed before any new drawing takes place. However, when this ellipse bounces around on the screen on their computer, many sections of the lines are not correctly being undrawn, but the ellipse is correctly redrawn in its new location. What's even more interesting is that this only happens on the laptops of 2 students in the class, who both happen to have old macbook airs.
So I don't know what to make of that. My first thought is that, since turtle doesn't implement any threading but is built on top of tkinter, it might be that tkinter uses some degree of threading, but I'm not entirely sure to what degree, and I'm hoping someone might be able to comment on this faster than I could go through the entirety of tkinter's source code to try and get some idea of what might be happening, and what I might be able to do to get around it.
Or, maybe I'm totally wrong and it has nothing to do with that. I'd appreciate any thoughts.
import turtle
from typing import Union
import abc
import time
import math
turtle.tracer(False)
turtle.colormode(255)
class Window:
def __init__(self, title: str, width: int, height: int):
""" The constructor for the Window class.
Creates the turtle._Screen object _screen according to the given
title, width, and height parameters given.
Args:
title (str): A string to title the window.
width (int): An int to specify the window width.
height (int): An int to specify the window height.
"""
self._screen = turtle.Screen()
self._screen.title(title)
self._screen.setup(width+20, height+20)
def setBackground(self, color: Union[str, tuple[float, float, float]]):
""" Sets the background color of the screen.
Args:
color (str or (float, float, float)): The desired color of
the background.
"""
self._screen.bgcolor(color)
class Shape(abc.ABC):
_turt: turtle.Turtle
_drawn: bool
_points: list[tuple[float, float]]
_filled: bool
_center: float
_screen: turtle._Screen
def __init__(self, lineWidth: int,
lineColor: Union[str, tuple[int, int, int]] = None,
fillColor: Union[str, tuple[int, int, int]] = None):
""" This initializes the instance variables common to all shapes.
This includes:
self._turt: The turtle.Turtle object which draws the shape.
It has a pensize of lineWidth, a pencolor of
lineColor, and a fill color of fillColor.
self._drawn: A boolean which is True if and only if the shape
is currently drawn to the screen. It is False by
default.
self._filled: A boolean which is True if and only if the shape
will be filled in when drawn. It is True when
fillColor is given.
self._screen: A Screen which the shape will be drawn to.
self._points: A list of tuples containing the points which
comprise the shape.
Args:
lineWidth (int): The line width of the outer edge
lineColor (str or (int, int, int)): The color of the outer edge.
fillColor (str or (int, int, int)): The color to fill the shape
in.
"""
self._turt = turtle.Turtle()
self._turt.penup()
self._turt.hideturtle()
self._turt.pensize(lineWidth)
self._filled = False
if lineColor:
self._turt.pencolor(lineColor)
if fillColor:
self._turt.fillcolor(fillColor)
self._filled = True
self._drawn = False
self._screen = turtle.Screen()
self._points = []
def setFill(self, color: Union[str, tuple[int, int, int]]):
""" Configures the given shape to be filled with the given parameter
color.
Args:
color (str or (int, int int)): The color to fill in the shape
"""
if color:
self._filled = True
self._turt.fillcolor(color)
if self._drawn:
self.undraw()
self.draw()
@abc.abstractmethod
def _determinePoints(self):
""" Determines the underlying points of the shape.
To be implemented by inherited classes.
"""
def draw(self):
""" Draws the shape to the screen"""
if not self._drawn:
self._turt.goto(self._points[0])
self._turt.pendown()
if self._filled:
self._turt.begin_fill()
for point in self._points:
self._turt.goto(point)
if self._filled:
self._turt.end_fill()
self._turt.penup()
self._screen.update()
self._drawn = True
def undraw(self):
""" Undraws the shape from the screen"""
self._turt.clear()
self._drawn = False
def move(self, dx: float = 0, dy: float = 0):
""" Moves the shape in the x direction by dx and in the y direction by
dy
Args:
dx (float): The amount by which to move the shape to the
right
dy (float): The amount by which to move the shape up """
for i in range(len(self._points)):
self._points[i] = (self._points[i][0] + dx,
self._points[i][1] + dy)
self._center = (self._center[0] + dx, self._center[1] + dy)
if self._drawn:
self.undraw()
self.draw()
def rotate(self, dTheta: float = 0):
""" Rotates the shape counter-clockwise by the angle dTheta measured
in degrees.
Args:
dTheta (float): The number of degrees by which to rotate.
"""
for i in range(len(self._points)):
xOffset = self._points[i][0] - self._center[0]
yOffset = self._points[i][1] - self._center[1]
self._points[i] = (
self._center[0] + xOffset * math.cos(dTheta * 2*math.pi / 360)
- yOffset * math.sin(dTheta * 2*math.pi / 360),
self._center[1] + xOffset * math.sin(dTheta * 2*math.pi / 360)
+ yOffset * math.cos(dTheta * 2*math.pi / 360))
self._turt.left(dTheta)
if self._drawn:
self.undraw()
self.draw()
class Rectangle(Shape):
def __init__(self,
lowerLeftCorner: tuple[int, int], width: int, height: int,
lineWidth: int = 1,
lineColor: Union[str, tuple[int, int, int]] = None,
fillColor: Union[str, tuple[int, int, int]] = None):
""" The constructor for the Rectangle Class.
In addition to the tasks performed by the constructor for the Shape
class, this sets the lowerLeftCorner parameter of the shape, as well
as calling the _determinePoints method to construct the 4 defining
points of the rectangle.
Args:
lowerLeftCorner ((int, int)):
A point denoting the bottom left corner of the
rectangle
width (int): The width of the rectangle
height (int): The height of the rectangle
lineWidth (int): The penwidth of the outer edge
lineColor (str or (float, float, float)): The pencolor of the
outer edge
fillColor (str or (float, float, float)): The color used to fill
the shape """
Shape.__init__(self, lineWidth, lineColor, fillColor)
self.lowerLeftCorner = lowerLeftCorner
self.width = width
self.height = height
self._determinePoints()
def _determinePoints(self):
""" Assembles the 4 defining points of the Rectangle and saves them in
_points.
"""
self._points = [
self.lowerLeftCorner,
(self.lowerLeftCorner[0] + self.width,
self.lowerLeftCorner[1]),
(self.lowerLeftCorner[0] + self.width,
self.lowerLeftCorner[1] + self.height),
(self.lowerLeftCorner[0],
self.lowerLeftCorner[1] + self.height),
self.lowerLeftCorner]
self._center = (self.lowerLeftCorner[0] + self.width/2,
self.lowerLeftCorner[1] + self.height/2)
class Oval(Shape):
def __init__(self, center: tuple[float, float], width: int,
height: int, steps: int = 500, lineWidth: int = 1,
lineColor: Union[str, tuple[int, int, int]] = None,
fillColor: Union[str, tuple[int, int, int]] = None):
""" The constructor for the Oval Class.
In addition to the tasks performed by the constructor for the Shape
class, this sets the center of the shape, as well as calling the
_determinePoints method to construct the points of the ellipse. The
steps parameter specifies the number of points to use for the
construction of the outline.
Args:
center ((float, float)): A point denoting the center of the oval
width (float): The width of the oval
height (float): The height of the oval
steps (int): The number of points to create for the oval
lineWidth (int): The line width of the outer edge
lineColor (str or (int, int, int)): The pencolor of the outer
edge
fillColor (str or (int, int, int)): The color used to fill the
given shape
"""
Shape.__init__(self, lineWidth, lineColor, fillColor)
self._center = center
self._width = width
self._height = height
self._steps = steps
self._determinePoints()
def _determinePoints(self):
""" Populates _points with as many points as specified by steps via the
parametric equations for an ellipse."""
for i in range(self._steps):
self._points.append(
(self._center[0] +
self._width * math.cos(i*2*math.pi/self._steps),
(self._center[1] +
self._height * math.sin(i*2*math.pi/self._steps))))
self._points.append(self._points[0])
if __name__ == "__main__":
window = Window("Tester", 600, 600)
Rectangle((-200, -250), 400, 500, 5, fillColor="Green").draw()
oval = Oval((-50, -50), 100, 50, 200, 3, "Red", "Blue")
oval.draw()
direction = [3, 3]
rotation = 3
def updateDirection():
global direction, rotation
if max([*zip(*oval._points)][0]) >= 200:
direction[0] = -3
if direction[1] > 0:
rotation = -3
else:
rotation = 3
elif min([*zip(*oval._points)][0]) <= -200:
direction[0] = 3
if direction[1] > 0:
rotation = 3
else:
rotation = -3
if max([*zip(*oval._points)][1]) >= 250:
direction[1] = -3
if direction[0] > 0:
rotation = 3
else:
rotation = -3
elif min([*zip(*oval._points)][1]) <= -250:
direction[1] = 3
if direction[0] > 0:
rotation = -3
else:
rotation = 3
time.sleep(1)
while(True):
oval.move(direction[0], direction[1])
updateDirection()
oval.rotate(rotation)
time.sleep(1/65)