import turtle from typing import Union import time import abc 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. Enables mouse click tracking in accordance with the getMouse() method. 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, height) self._screen.cv.bind("", self._onClick) self._mouseX = None self._mouseY = None def _onClick(self, e): """ Internal function to store the mouse position. """ self.mouseX = e.x self.mouseY = e.y 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) def close(self): """ Closes the screen. """ self._screen.bye() def getMouse(self) -> tuple[int, int]: """ Wait for mouse click and return the point representing the click Returns: (int, int): The point where the mouse was clicked. """ self._screen.cv.update() self.mouseX = None self.mouseY = None while self.mouseX == None or self.mouseY == None: self._screen.cv.update() time.sleep(.1) (x, y) = (self.mouseX, self.mouseY) self.mouseX = None self.mouseY = None return (x - self._screen.window_width()/2 - 2, self._screen.window_height()/2 - y + 3) class Shape(abc.ABC): _turt: turtle.Turtle _drawn: bool _points: list[tuple[float, float]] _filled: bool _center: float _screen: turtle._Screen _label: 'Text' 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. self._label: An optional Text object that stays tied to 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 = [] self._label = None def containsPoint(self, point: tuple[float, float]) -> bool: """ Returns True if the given point is contained within the boundary of this Shape. See https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html#Explanation Returns: bool: True if the given point is contained within the boundary ofthis Shape """ currentBool = False if point in self._points: return True for i in range(len(self._points)): j = i - 1 % len(self._points) if (((self._points[i][1] > point[1]) != (self._points[j][1] > point[1])) and (point[0] < (self._points[j][0] - self._points[i][0]) * (point[1]-self._points[i][1]) / (self._points[j][1]-self._points[i][1]) + self._points[i][0])): currentBool = not currentBool return currentBool def setLabel(self, text: str, size: int = 12, font: str = 'Helvetica', color: Union[str, tuple[int, int, int]] = "Black", offSet: tuple[float, float] = (0, 0)): """ Adds an optional Text label which stays tied to the shape. If a label has already been created, this replaces it. Args: text (str): The text of the label. size (int): The font size of the text. font (str): The font name of the text. color (str or (int, int, int)): The color of the text. offSet (float, float): The off set from the center of the shape to tie the label to. """ if self._label: self._label.undraw() self._label = Text(text, (self._center[0] + offSet[0], self._center[1] + offSet[1]), size, font, color) if self._drawn: self._label.draw() def removeLabel(self): """ Removes the label from the shape. """ if self._label: self._label.undraw() self._label = None 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() def setLineColor(self, color: Union[str, tuple[int, int, int]]): """ Configures the given shape to have lines colored by the given color Args: color (str or (int, int int)): The color to set the lines of the given shape. """ if color: self._turt.pencolor(color) if self._drawn: self.undraw() self.draw() def setLineWidth(self, width: int): """ Configures the given shape to have lines of the given width Args: width (int): The width to create the lines of the shape """ self._turt.pensize(width) 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() if self._label: self._label.draw() self._screen.update() self._drawn = True def undraw(self): """ Undraws the shape from the screen""" self._turt.clear() if self._label: self._label.undraw() 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._label: self._label.move(dx, 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() def getPoints(self) -> list[tuple[float, float]]: """ Returns the list of points defining this shape. Returns: [(float, float), ...]: The list of points defining this shape.""" return self._points[:] 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]) class Polygon(Shape): def __init__(self, points: list[tuple[float, float]], lineWidth: int = 1, lineColor: Union[str, tuple[int, int, int]] = None, fillColor: Union[str, tuple[int, int, int]] = None): """ The constructor for the Polygon Class. In addition to the tasks performed by the constructor for the Shape class, this calls the _determinePoints method to determine the center of the polygon. Args: points ([(int, int), ...]): A list of points forming the outline of the polygon 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 used to fill the shape """ Shape.__init__(self, lineWidth, lineColor, fillColor) self._points = points self._points.append(self._points[0]) self._determinePoints() def _determinePoints(self): """ This function determines the center of the polygon via the formula given at https://en.wikipedia.org/wiki/Centroid#Of_a_polygon """ centerX = 0 centerY = 0 area = 0 for i in range(len(self._points)-1): x_i = self._points[i][0] x_ip1 = self._points[i+1][0] y_i = self._points[i][1] y_ip1 = self._points[i+1][1] centerX += (x_i + x_ip1) * (x_i*y_ip1 - x_ip1*y_i) centerY += (y_i + y_ip1) * (x_i*y_ip1 - x_ip1*y_i) area += (x_i*y_ip1 - x_ip1*y_i) area = area / 2 centerX = centerX / (6 * area) centerY = centerY / (6 * area) self._center = (centerX, centerY) class Text(Shape): def __init__(self, text: str, pos: tuple[int, int], size: int = 12, font: str = "Helvetica", fontColor: Union[str, tuple[int, int, int]] = None): Shape.__init__(self, size, fontColor) """ The constructor for the Text Class. Args: text (str): The text to be written. pos ((float, float)): The position to draw the text. size (int): The font size of the text. fontColor (str or (int, int, int)): The color to write the text. """ self._text = text self._size = size self._font = font self._center = pos def _determinePoints(self): pass def setFill(self, color: Union[str, tuple[int, int, int]]): """ This function is unimplemented for Texts. """ def setLineColor(self, color: Union[str, tuple[int, int, int]]): """ This function is unimplemented for Texts. """ def setLineWidth(self, width: int): """ This function is unimplemented for Texts. """ def draw(self): """ Writes the text to the screen. """ if not self._drawn: self._turt.goto(self._center) self._turt.write(self._text, align="center", font = (self._font, self._size, 'normal')) self._drawn = True if __name__ == "__main__": window = Window("Tester", 600, 600) Rectangle((-200, -250), 400, 500, 5, fillColor="Green").draw() # sqr = Rectangle((-50, -50), 100, 100, 3, "Red", "Blue") # sqr.draw() sqr = Oval((-50, -50), 100, 50, 500, 3, "Red", "Blue") sqr.draw() # listOfPoints = [] # for i in range(5): # listOfPoints.append((50*math.cos(i * 2 * math.pi / 5), # 50*math.sin(i * 2 * math.pi / 5))) # sqr = Polygon(listOfPoints, 3, "Red", "Blue") # sqr.draw() sqr.setLabel('Label', 10, color='Green', offSet=(0, -8)) direction = [3, 3] rotation = 3 def updateDirection(): global direction, rotation if max([*zip(*sqr._points)][0]) >= 200: direction[0] = -3 if direction[1] > 0: rotation = -3 else: rotation = 3 elif min([*zip(*sqr._points)][0]) <= -200: direction[0] = 3 if direction[1] > 0: rotation = 3 else: rotation = -3 if max([*zip(*sqr._points)][1]) >= 250: direction[1] = -3 if direction[0] > 0: rotation = 3 else: rotation = -3 elif min([*zip(*sqr._points)][1]) <= -250: direction[1] = 3 if direction[0] > 0: rotation = -3 else: rotation = 3 time.sleep(1) while(True): sqr.move(direction[0], direction[1]) updateDirection() sqr.rotate(rotation) time.sleep(1/65)