# SPDX-FileCopyrightText: 2006-2010 Gregor Lingl for Adafruit Industries # SPDX-FileCopyrightText: 2019 LadyAda for Adafruit Industries # SPDX-FileCopyrightText: 2021 Dave Astels for Adafruit Industries # # SPDX-License-Identifier: MIT """ `adafruit_turtle` ================================================================================ * Originals Author(s): LadyAda and Dave Astels Implementation Notes -------------------- **Hardware:** **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice """ from __future__ import annotations # pylint:disable=too-many-public-methods, too-many-instance-attributes, invalid-name # pylint:disable=too-few-public-methods, too-many-lines, too-many-arguments import gc import math import time import displayio try: from typing import List, Optional, Tuple, Union except ImportError: pass __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_turtle.git" class Color: """Standard colors""" WHITE = 0xFFFFFF """0xFFFFFF :meta hide-value:""" BLACK = 0x000000 """0x000000 :meta hide-value:""" RED = 0xFF0000 """0xFF0000 :meta hide-value:""" ORANGE = 0xFFA500 """0xFFA500 :meta hide-value:""" YELLOW = 0xFFEE00 """0xFFEE00 :meta hide-value:""" GREEN = 0x00C000 """0x00C000 :meta hide-value:""" BLUE = 0x0000FF """0x0000FF :meta hide-value:""" PURPLE = 0x8040C0 """0x8040C0 :meta hide-value:""" PINK = 0xFF40C0 """0xFF40C0 :meta hide-value:""" LIGHT_GRAY = 0xAAAAAA """0xAAAAAA :meta hide-value:""" GRAY = 0x444444 """0x444444 :meta hide-value:""" BROWN = 0xCA801D """0xCA801D :meta hide-value:""" DARK_GREEN = 0x008700 """0x008700 :meta hide-value:""" TURQUOISE = 0x00C0C0 """0x00C0C0 :meta hide-value:""" DARK_BLUE = 0x0000AA """0x0000AA :meta hide-value:""" DARK_RED = 0x800000 """0x800000 :meta hide-value:""" colors = ( BLACK, WHITE, RED, YELLOW, GREEN, ORANGE, BLUE, PURPLE, PINK, GRAY, LIGHT_GRAY, BROWN, DARK_GREEN, TURQUOISE, DARK_BLUE, DARK_RED, ) def __init__(self) -> None: pass class Vec2D: """A 2 dimensional vector class, used as a helper class for implementing turtle graphics. May be useful for turtle graphics programs also. Derived from tuple, so a vector is a tuple! """ # Provides (for a, b vectors, k number): # a+b vector addition # a-b vector subtraction # a*b inner product # k*a and a*k multiplication with scalar # |a| absolute value of a # a.rotate(angle) rotation def __init__(self, x: float, y: float) -> None: self.values = (x, y) def __getitem__(self, index: int) -> float: return self.values[index] def __add__(self, other: Vec2D) -> Vec2D: return Vec2D(self[0] + other[0], self[1] + other[1]) def __mul__(self, other: Union[float, Vec2D]) -> Union[float, Vec2D]: if isinstance(other, Vec2D): return self[0] * other[0] + self[1] * other[1] return Vec2D(self[0] * other, self[1] * other) def __rmul__(self, other: float) -> Optional[Vec2D]: if isinstance(other, (float, int)): return Vec2D(self[0] * other, self[1] * other) return None def __sub__(self, other: Vec2D) -> Vec2D: return Vec2D(self[0] - other[0], self[1] - other[1]) def __neg__(self) -> Vec2D: return Vec2D(-self[0], -self[1]) def __abs__(self) -> float: return (self[0] ** 2 + self[1] ** 2) ** 0.5 def rotate(self, angle: float) -> Vec2D: """Rotate self counterclockwise by angle. :param angle: how much to rotate """ perp = Vec2D(-self[1], self[0]) angle = angle * math.pi / 180.0 c, s = math.cos(angle), math.sin(angle) return Vec2D(self[0] * c + perp[0] * s, self[1] * c + perp[1] * s) def __getnewargs__(self) -> Tuple[float, float]: return (self[0], self[1]) def __repr__(self) -> str: return "({:.2f},{:.2f})".format(self[0], self[1]) class turtle: """A Turtle that can be given commands to draw.""" # pylint:disable=too-many-statements def __init__( self, display: Optional[displayio.Display] = None, scale: float = 1 ) -> None: if display: self._display = display else: try: # pylint: disable=import-outside-toplevel import board self._display = board.DISPLAY except AttributeError as err: raise RuntimeError( "No display available. One must be provided." ) from err self._w: int = self._display.width self._h: int = self._display.height self._x = self._w // (2 * scale) self._y = self._h // (2 * scale) self._speed = 6 self._heading: float = 0 self._fullcircle = 360.0 self._degreesPerAU = 1.0 self._logomode = False self._angleOrient = -1 self._angleOffset: float = self._fullcircle / 4 self._bg_color = 0 self._splash: displayio.Group = displayio.Group() self._bgscale: int = 1 if self._w == self._h: i = 1 while self._bgscale == 1: if self._w / i < 128: self._bg_bitmap = displayio.Bitmap(i, i, 1) self._bgscale = self._w // i i += 1 else: self._bgscale = self._GCD(self._w, self._h) self._bg_bitmap = displayio.Bitmap( self._w // self._bgscale, self._h // self._bgscale, 1 ) self._bg_palette = displayio.Palette(1) self._bg_palette[0] = Color.colors[self._bg_color] self._bg_sprite = displayio.TileGrid( self._bg_bitmap, pixel_shader=self._bg_palette, x=0, y=0 ) self._bg_group = displayio.Group(scale=self._bgscale) self._bg_group.append(self._bg_sprite) self._splash.append(self._bg_group) # group to add background pictures (and/or user-defined stuff) self._bg_addon_group = displayio.Group() self._splash.append(self._bg_addon_group) self._fg_scale: int = int(scale) self._w = self._w // self._fg_scale self._h = self._h // self._fg_scale self._fg_bitmap = displayio.Bitmap(self._w, self._h, len(Color.colors)) self._fg_palette = displayio.Palette(len(Color.colors)) self._fg_palette.make_transparent(self._bg_color) for i, c in enumerate(Color.colors): self._fg_palette[i] = c self._fg_sprite = displayio.TileGrid( self._fg_bitmap, pixel_shader=self._fg_palette, x=0, y=0 ) self._fg_group = displayio.Group(scale=self._fg_scale) self._fg_group.append(self._fg_sprite) self._splash.append(self._fg_group) # group to add text and/or user defined stuff self._fg_addon_group = displayio.Group() self._splash.append(self._fg_addon_group) self._turtle_bitmap = displayio.Bitmap(9, 9, 2) self._turtle_palette = displayio.Palette(2) self._turtle_palette.make_transparent(0) self._turtle_palette[1] = Color.WHITE for i in range(4): self._turtle_bitmap[4 - i, i] = 1 self._turtle_bitmap[i, 4 + i] = 1 self._turtle_bitmap[4 + i, 7 - i] = 1 self._turtle_bitmap[4 + i, i] = 1 self._turtle_sprite = displayio.TileGrid( self._turtle_bitmap, pixel_shader=self._turtle_palette, x=-100, y=-100 ) self._turtle_group = displayio.Group(scale=self._fg_scale) self._turtle_group.append(self._turtle_sprite) self._splash.append(self._turtle_group) self._penstate = False self._pensize = 1 self._pencolor = 1 self.pencolor(Color.WHITE) self._bg_pic = None self._bg_pic_filename = "" self._turtle_pic = None self._turtle_odb = None self._turtle_alt_sprite = None self._drawturtle() self._stamps = {} self._turtle_odb_use = 0 self._turtle_odb_file = None self._odb_tilegrid = None gc.collect() self._display.root_group = self._splash # pylint:enable=too-many-statements def _drawturtle(self) -> None: if self._turtle_pic is None: self._turtle_sprite.x = int(self._x - 4) self._turtle_sprite.y = int(self._y - 4) else: if self._turtle_odb is not None: self._turtle_alt_sprite.x = int(self._x - self._turtle_odb.width // 2) self._turtle_alt_sprite.y = int(self._y - self._turtle_odb.height // 2) else: self._turtle_alt_sprite.x = int(self._x - self._turtle_pic[0] // 2) self._turtle_alt_sprite.y = int(self._y - self._turtle_pic[1] // 2) ########################################################################### # Move and draw def forward(self, distance: float) -> None: """Move the turtle forward by the specified distance, in the direction the turtle is headed. :param distance: how far to move (integer or float) """ p = self.pos() angle = ( self._angleOffset + self._angleOrient * self._heading ) % self._fullcircle x1 = p[0] + math.sin(math.radians(angle)) * distance y1 = p[1] + math.cos(math.radians(angle)) * distance self.goto(x1, y1) fd = forward def backward(self, distance: float) -> None: """Move the turtle backward by distance, opposite to the direction the turtle is headed. Does not change the turtle's heading. :param distance: how far to move (integer or float) """ self.forward(-distance) bk = backward back = backward def right(self, angle: float) -> None: """Turn turtle right by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). :param angle: how much to rotate to the right (integer or float) """ if self._logomode: self._turn(angle) else: self._turn(-angle) rt = right def left(self, angle: float) -> None: """Turn turtle left by angle units. (Units are by default degrees, but can be set via the degrees() and radians() functions.) Angle orientation depends on the turtle mode, see mode(). :param angle: how much to rotate to the left (integer or float) """ if self._logomode: self._turn(-angle) else: self._turn(angle) lt = left # pylint:disable=too-many-branches,too-many-statements def goto( self, x1: Union[float, Vec2D, Tuple[float, float]], y1: Optional[float] = None ) -> None: """If y1 is None, x1 must be a pair of coordinates or an (x, y) tuple Move turtle to an absolute position. If the pen is down, draw line. Does not change the turtle's orientation. :param x1: a number or a pair of numbers :param y1: a number or None """ yn: float = x1[1] if y1 is None else y1 # type: ignore xn: float = x1[0] if y1 is None else x1 # type: ignore xn += self._w // 2 yn = self._h // 2 - yn if not self.isdown(): self._x = xn # woot, we just skip ahead self._y = yn self._drawturtle() return self._do_draw_line(round(self._x), round(self._y), round(xn), round(yn)) self._x = xn self._y = yn def _do_draw_line(self, x0: int, y0: int, xn: int, yn: int): steep = abs(yn - y0) > abs(xn - x0) rev = False dx = xn - x0 if steep: x0, y0 = y0, x0 xn, yn = yn, xn dx = xn - x0 if x0 > xn: rev = True dx = x0 - xn dy = abs(yn - y0) err = dx / 2 ystep = -1 if y0 < yn: ystep = 1 step = 1 if self._speed > 0: ts = ((11 - self._speed) * 0.00020) * (self._speed + 0.5) else: ts = 0 while (not rev and x0 <= xn) or (rev and xn <= x0): if steep: try: self._plot(int(y0), int(x0), self._pencolor) except IndexError: pass else: try: self._plot(int(x0), int(y0), self._pencolor) except IndexError: pass if self._speed > 0: if step >= self._speed: # mark the step step = 1 self._drawturtle() time.sleep(ts) else: step += 1 err -= dy if err < 0: y0 += ystep err += dx if rev: x0 -= 1 else: x0 += 1 self._drawturtle() setpos = goto setposition = goto # pylint:enable=too-many-branches,too-many-statements def setx(self, x: float) -> None: """Set the turtle's first coordinate to x, leave second coordinate unchanged. :param x: new value of the turtle's x coordinate (a number) """ self.goto(x, self.pos()[1]) def sety(self, y: float) -> None: """Set the turtle's second coordinate to y, leave first coordinate unchanged. :param y: new value of the turtle's y coordinate (a number) """ self.goto(self.pos()[0], y) def setheading(self, to_angle: float) -> None: """Set the orientation of the turtle to to_angle. Here are some common directions in degrees: ===== ============= ========== angle standard mode logo mode ===== ============= ========== 0 east north 90 north east 180 west south 270 south west ===== ============= ========== :param to_angle: the new turtle heading """ self._turn(to_angle - self._heading) seth = setheading def home(self) -> None: """Move turtle to the origin - coordinates (0,0) - and set its heading to its start-orientation (which depends on the mode, see mode()). """ self.setheading(90) self.goto(0, 0) # pylint:disable=too-many-locals, too-many-statements, too-many-branches def _plot(self, x: float, y: float, c: int) -> None: if self._pensize == 1: try: self._fg_bitmap[int(x), int(y)] = c return except IndexError: pass r = self._pensize // 2 + 1 angle = ( self._angleOffset + self._angleOrient * self._heading - 90 ) % self._fullcircle sin = math.sin(math.radians(angle)) cos = math.cos(math.radians(angle)) x0 = x + sin * r x1 = x - sin * (self._pensize - r) y0 = y - cos * r y1 = y + cos * (self._pensize - r) coords = [x0, x1, y0, y1] for i, v in enumerate(coords): if v >= 0: coords[i] = math.ceil(v) else: coords[i] = math.floor(v) x0, x1, y0, y1 = coords steep = abs(y1 - y0) > abs(x1 - x0) rev = False dx = x1 - x0 if steep: x0, y0 = y0, x0 x1, y1 = y1, x1 dx = x1 - x0 if x0 > x1: rev = True dx = x0 - x1 dy = abs(y1 - y0) err = dx / 2 ystep = -1 if y0 < y1: ystep = 1 while (not rev and x0 <= x1) or (rev and x1 <= x0): # first row if steep: try: self._fg_bitmap[int(y0), int(x0)] = c except IndexError: pass else: try: self._fg_bitmap[int(x0), int(y0)] = c except IndexError: pass if y0 != y1 and self._heading % 90 != 0: # need a second row to fill the cracks j = -1 if y1 < y0 else 1 if steep: try: self._fg_bitmap[int(y0 + j), int(x0)] = c except IndexError: pass else: try: self._fg_bitmap[int(x0), int(y0 + j)] = c except IndexError: pass err -= dy if err < 0: y0 += ystep err += dx if rev: x0 -= 1 else: x0 += 1 # pylint:enable=too-many-locals, too-many-statements, too-many-branches def circle( self, radius: float, extent: Optional[float] = None, steps: Optional[int] = None ) -> None: """Draw a circle with given radius. The center is radius units left of the turtle; extent - an angle - determines which part of the circle is drawn. If extent is not given, draw the entire circle. If extent is not a full circle, one endpoint of the arc is the current pen position. Draw the arc in counterclockwise direction if radius is positive, otherwise in clockwise direction. Finally the direction of the turtle is changed by the amount of extent. As the circle is approximated by an inscribed regular polygon, steps determines the number of steps to use. If not given, it will be calculated automatically. May be used to draw regular polygons. :param radius: the radius of the circle :param extent: the arc of the circle to be drawn :param steps: how many points along the arc are computed """ # call: circle(radius) # full circle # --or: circle(radius, extent) # arc # --or: circle(radius, extent, steps) # --or: circle(radius, steps=6) # 6-sided polygon pos = self.pos() h = self._heading if extent is None: extent = self._fullcircle if steps is None: frac = abs(extent) / self._fullcircle steps = int(min(3 + abs(radius) / 4.0, 12.0) * frac) * 4 w = extent / steps w2 = 0.5 * w l = radius * math.sin(w * math.pi / 180.0 * self._degreesPerAU) if radius < 0: l, w, w2 = -l, -w, -w2 self.left(w2) for _ in range(steps - 1): self.forward(l) self.left(w) # rounding error correction on the last step self.setheading(self.towards(pos)) # get back to exact same position and heading self.goto(pos) self.setheading(h) # pylint:disable=inconsistent-return-statements def speed(self, speed: Optional[int] = None) -> Optional[int]: """ Set the turtle's speed to an integer value in the range 0..10. If no argument is given, return current speed. If input is a number greater than 10 or smaller than 1, speed is set to 0. Speedstrings are mapped to speedvalues as follows: "fastest": 0 "fast": 10 "normal": 6 "slow": 3 "slowest": 1 Speeds from 1 to 10 enforce increasingly faster animation of line drawing and turtle turning. Attention: speed = 0 means that no animation takes place. forward/back makes turtle jump and likewise left/right make the turtle turn instantly. :param speed: the new turtle speed (0..10) or None """ if speed is None: return self._speed if speed > 10 or speed < 1: self._speed = 0 else: self._speed = speed return None # pylint:enable=inconsistent-return-statements def dot(self, size: Optional[int] = None, color: Optional[int] = None) -> None: """Draw a circular dot with diameter size, using color. If size is not given, the maximum of pensize+4 and 2*pensize is used. :param size: the diameter of the dot :param color: the color of the dot """ if size is None: size = max(self._pensize + 4, self._pensize * 2) if color is None: color = self._pencolor else: color = self._color_to_pencolor(color) pensize = self._pensize pencolor = self._pencolor down = self.isdown() if size > 1: self._pensize = size self._pencolor = color self.pendown() self.right(180) self.right(180) if not down: self.penup() self._pensize = pensize self._pencolor = pencolor else: self._pensize = 1 self._plot(self._x, self._y, color) self._pensize = pensize def stamp( self, bitmap: Optional[displayio.Bitmap] = None, palette: Optional[displayio.Palette] = None, ) -> int: """ Stamp a copy of the turtle shape onto the canvas at the current turtle position. Return a stamp_id for that stamp, which can be used to delete it by calling clearstamp(stamp_id). """ s_id = len(self._stamps) if self._turtle_pic is None: # easy. new_stamp = displayio.TileGrid( self._turtle_bitmap, pixel_shader=self._turtle_palette, x=int(self._x - self._turtle_bitmap.width // 2), y=int(self._y - self._turtle_bitmap.height // 2), ) elif self._turtle_odb is not None: # odb bitmap new_stamp = displayio.TileGrid( self._turtle_odb, pixel_shader=self._turtle_odb.pixel_shader, x=int(self._x - self._turtle_odb.width // 2), y=int(self._y - self._turtle_odb.height // 2), ) self._turtle_odb_use += 1 else: if bitmap is None: raise RuntimeError("a bitmap must be provided") if palette is None: raise RuntimeError("a palette must be provided") new_stamp = displayio.TileGrid( bitmap, pixel_shader=palette, x=int(self._x - bitmap.width // 2), y=int(self._y - bitmap.height // 2), ) self._fg_addon_group.append(new_stamp) if self._turtle_odb is not None: self._stamps[s_id] = (new_stamp, self._turtle_odb_file) else: self._stamps[s_id] = new_stamp return s_id def clearstamp(self, stampid: int) -> None: """ Delete stamp with given stampid. :param stampid: the id of the stamp to be deleted """ if isinstance(stampid, int): if stampid in self._stamps and self._stamps[stampid] is not None: if isinstance(self._stamps[stampid], tuple): self._fg_addon_group.remove(self._stamps[stampid][0]) self._turtle_odb_use -= 1 else: self._fg_addon_group.remove(self._stamps[stampid]) self._stamps[stampid] = None else: return else: raise TypeError("Stamp id must be an int") def clearstamps(self, n: Optional[int] = None) -> None: """ Delete all or first/last n of turtle's stamps. If n is None, delete all stamps, if n > 0 delete first n stamps, else if n < 0 delete last n stamps. :param n: how many stamps to delete (None means delete them all) """ i = 1 for sid in self._stamps: # pylint: disable=consider-using-dict-items if self._stamps[sid] is not None: self.clearstamp(sid) if n is not None and i >= n: return i += 1 ########################################################################### # Tell turtle's state def pos(self) -> Vec2D: """Return the turtle's current location (x,y) (as a Vec2D vector).""" return Vec2D(self._x - self._w // 2, self._h // 2 - self._y) position = pos def towards( self, x1: Union[float, Vec2D, Tuple[float, float]], y1: Optional[float] = None ) -> float: """ Return the angle between the line from turtle position to position specified by (x,y) or the vector. This depends on the turtle's start orientation which depends on the mode - "standard" or "logo"). :param x: a number or a pair/vector of numbers :param y: a number if x is a number, else None """ if y1 is None: y1 = x1[1] x1 = x1[0] x0, y0 = self.pos() result = math.degrees(math.atan2(x1 - x0, y1 - y0)) result /= self._degreesPerAU return (self._angleOffset + self._angleOrient * result) % self._fullcircle def xcor(self) -> float: """Return the turtle's x coordinate.""" return self._x - self._w // 2 def ycor(self) -> float: """Return the turtle's y coordinate.""" return self._h // 2 - self._y def heading(self) -> float: """Return the turtle's current heading (value depends on the turtle mode, see mode()). """ return self._heading def distance( self, x1: Union[float, List[float], Tuple[float, float]], y1: Optional[float] ) -> float: """ Return the distance from the turtle to (x,y) or the vector, in turtle step units. :param x: a number or a pair/vector of numbers :param y: a number if x is a number, else None """ yn: float = x1[1] if y1 is None else y1 # type: ignore xn: float = x1[0] if y1 is None else x1 # type: ignore p = self.pos() x0, y0 = (p[0], p[1]) return math.sqrt((x0 - xn) ** 2 + (y0 - yn) ** 2) ########################################################################### # Setting and measurement def _setDegreesPerAU(self, fullcircle: float) -> None: """Helper function for degrees() and radians()""" self._fullcircle = fullcircle self._degreesPerAU = 360 / fullcircle if self._logomode: self._angleOffset = 0 else: self._angleOffset = -fullcircle / 4 def degrees(self, fullcircle: float = 360) -> None: """Set angle measurement units, i.e. set number of "degrees" for a full circle. Default value is 360 degrees. :param fullcircle: the number of degrees in a full circle """ self._setDegreesPerAU(fullcircle) def radians(self) -> None: """Set the angle measurement units to radians. Equivalent to degrees(2*math.pi).""" self._setDegreesPerAU(2 * math.pi) def mode(self, mode: Optional[str] = None) -> Optional[str]: """ Set turtle mode ("standard" or "logo") and perform reset. If mode is not given, current mode is returned. Mode "standard" is compatible with old turtle. Mode "logo" is compatible with most Logo turtle graphics. :param mode: one of the strings "standard" or "logo" """ if mode == "standard": self._logomode = False self._angleOrient = -1 self._angleOffset = self._fullcircle / 4 elif mode == "logo": self._logomode = True self._angleOrient = 1 self._angleOffset = 0 elif mode is None: if self._logomode: return "logo" return "standard" else: raise RuntimeError("Mode must be 'logo', 'standard', or None") return None def window_height(self) -> float: """ Return the height of the turtle window.""" return self._h def window_width(self) -> float: """ Return the width of the turtle window.""" return self._w ########################################################################### # Drawing state def pendown(self) -> None: """Pull the pen down - drawing when moving.""" self._penstate = True pd = pendown down = pendown def penup(self) -> None: """Pull the pen up - no drawing when moving.""" self._penstate = False pu = penup up = penup def isdown(self) -> bool: """Return True if pen is down, False if it's up.""" return self._penstate def pensize(self, width: Optional[int] = None) -> int: """ Set the line thickness to width or return it. If no argument is given, the current pensize is returned. :param width: - a positive number """ if width is not None: self._pensize = width return self._pensize width = pensize ########################################################################### # Color control # pylint:disable=no-self-use def _color_to_pencolor(self, c: int) -> int: return Color.colors.index(c) # pylint:enable=no-self-use def pencolor(self, c: Optional[int] = None) -> int: """ Return or set the pencolor. pencolor() Return the current pencolor as color specification string or as a tuple (see example). May be used as input to another color/ pencolor/fillcolor call. pencolor(colorvalue) Set pencolor to colorvalue, which is a 24-bit integer such as 0xFF0000. The Color class provides the available values: BLACK, WHITE, RED, YELLOW, ORANGE, GREEN, BLUE, PURPLE, PINK GRAY, LIGHT_GRAY, BROWN, DARK_GREEN, TURQUOISE, DARK_BLUE, DARK_RED """ if c is None: return Color.colors[self._pencolor] if c not in Color.colors: raise RuntimeError("Color must be one of the 'Color' class items") self._pencolor = Color.colors.index(c) self._turtle_palette[1] = c if self._bg_color == self._pencolor: self._turtle_palette.make_transparent(1) else: self._turtle_palette.make_opaque(1) return c def bgcolor(self, c: Optional[int] = None) -> int: """ Return or set the background color. bgcolor() Return the current backgroud color as color specification string. May be used as input to another color/ pencolor/fillcolor call. bgcolor(colorvalue) Set backgroud color to colorvalue, which is a 24-bit integer such as 0xFF0000. The Color class provides the available values: WHITE, BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, PINK """ if c is None: return Color.colors[self._bg_color] if c not in Color.colors: raise RuntimeError("Color must be one of the 'Color' class items") old_color = self._bg_color self._fg_palette.make_opaque(old_color) self._bg_color = Color.colors.index(c) self._bg_palette[0] = c self._fg_palette.make_transparent(self._bg_color) self._turtle_palette[0] = c if self._bg_color == self._pencolor: self._turtle_palette.make_transparent(1) else: self._turtle_palette.make_opaque(1) for h in range(self._h): for w in range(self._w): if self._fg_bitmap[w, h] == old_color: self._fg_bitmap[w, h] = self._bg_color return Color.colors[self._bg_color] # pylint:disable=inconsistent-return-statements def bgpic(self, picname: Optional[str] = None) -> Optional[str]: """Set background image or return name of current backgroundimage. Optional argument: picname -- a string, name of an image file or "nopic". If picname is a filename, set the corresponding image as background. If picname is "nopic", delete backgroundimage, if present. If picname is None, return the filename of the current backgroundimage. """ if picname is None: return self._bg_pic_filename if picname == "nopic": if self._bg_pic is not None: self._bg_addon_group.remove(self._odb_tilegrid) self._odb_tilegrid = None self._bg_pic = None self._bg_pic_filename = "" else: odb = displayio.OnDiskBitmap(picname) self._odb_tilegrid = displayio.TileGrid( odb, pixel_shader=odb.pixel_shader, ) self._bg_addon_group.append(self._odb_tilegrid) self._bg_pic_filename = picname # centered self._odb_tilegrid.y = ((self._h * self._fg_scale) // 2) - (odb.height // 2) self._odb_tilegrid.x = ((self._w * self._fg_scale) // 2) - (odb.width // 2) return None # pylint:enable=inconsistent-return-statements ########################################################################### # More drawing control def reset(self) -> None: """ Delete the turtle's drawings from the screen, re-center the turtle and set variables to the default values.""" self.changeturtle() self.bgpic("nopic") self.bgcolor(Color.BLACK) self.clear() self.penup() self.goto(0, 0) self.setheading(0) self.pensize(1) self.pencolor(Color.WHITE) def clear(self) -> None: """Delete the turtle's drawings from the screen. Do not move turtle.""" self.clearstamps() for w in range(self._w): for h in range(self._h): self._fg_bitmap[w, h] = self._bg_color for i, c in enumerate(Color.colors): self._fg_palette[i] = c ^ 0xFFFFFF for i, c in enumerate(Color.colors): self._fg_palette[i] = c time.sleep(0.1) ########################################################################### # Visibility def showturtle(self) -> None: """ Make the turtle visible.""" if self._turtle_group: return if self._turtle_pic is None: self._turtle_group.append(self._turtle_sprite) else: self._turtle_group.append(self._turtle_alt_sprite) st = showturtle def hideturtle(self) -> None: """ Make the turtle invisible.""" if not self._turtle_group: return self._turtle_group.pop() ht = hideturtle def isvisible(self) -> bool: """ Return True if the Turtle is shown, False if it's hidden.""" if self._turtle_group: return True return False # pylint:disable=too-many-statements, too-many-branches def changeturtle( self, source: Optional[Union[displayio.TileGrid, str]] = None, dimensions: Tuple[int, int] = (12, 12), ) -> None: """ Change the turtle. if a string is provided, its a path to an image opened via OnDiskBitmap if a tilegrid is provided, it replace the default one for the turtle shape. if no argument is provided, the default shape will be restored """ if source is None: if self._turtle_pic is None: return if self._turtle_group: self._turtle_group.remove(self._turtle_alt_sprite) self._turtle_group.append(self._turtle_sprite) self._turtle_alt_sprite = None if self._turtle_odb is not None: self._turtle_odb_use -= 1 self._turtle_odb = None if self._turtle_odb_file is not None: self._turtle_odb_file = None self._turtle_pic = None self._drawturtle() return if isinstance(source, str): visible = self.isvisible() if self._turtle_pic is not None: if self._turtle_group: self._turtle_group.remove(self._turtle_alt_sprite) self._turtle_alt_sprite = None self._turtle_odb = None if not isinstance(self._turtle_pic, tuple): self._turtle_odb_file = None self._turtle_odb_use -= 1 self._turtle_pic = None try: self._turtle_odb = displayio.OnDiskBitmap(source) except: self._turtle_odb_file = None self._turtle_pic = None if visible: self._turtle_group.append(self._turtle_sprite) raise self._turtle_odb_use += 1 self._turtle_pic = True self._turtle_alt_sprite = displayio.TileGrid( self._turtle_odb, pixel_shader=self._turtle_odb.pixel_shader, ) if self._turtle_group: self._turtle_group.pop() if visible: self._turtle_group.append(self._turtle_alt_sprite) self._drawturtle() elif isinstance(source, displayio.TileGrid): if self._turtle_pic is not None: if self._turtle_odb_file is not None: self._turtle_odb_use -= 1 self._turtle_pic = dimensions self._turtle_alt_sprite = source if self._turtle_group: self._turtle_group.pop() self._turtle_group.append(self._turtle_alt_sprite) self._drawturtle() else: raise TypeError( 'Argument must be "str", a "displayio.TileGrid" or nothing.' ) # pylint:enable=too-many-statements, too-many-branches ########################################################################### # Other def _turn(self, angle: float) -> None: if angle % self._fullcircle == 0: return if not self.isdown() or self._pensize == 1: self._heading += angle self._heading %= self._fullcircle # wrap return start_angle = self._heading steps = math.ceil( (self._pensize * 2) * 3.1415 * (abs(angle) / self._fullcircle) ) if steps < 1: d_angle = angle steps = 1 else: d_angle = angle / steps if d_angle > 0: d_angle = math.ceil(d_angle) elif d_angle < 0: d_angle = math.floor(d_angle) else: print("d_angle = 0 !", d_angle, angle, steps) if self._logomode: self._heading += angle else: self._heading -= angle self._heading %= self._fullcircle # wrap return if abs(angle - steps * d_angle) >= abs(d_angle): steps += int(abs(angle - steps * d_angle) // abs(d_angle)) self._plot(self._x, self._y, self._pencolor) for _ in range(steps): self._heading += d_angle self._heading %= self._fullcircle # wrap self._plot(self._x, self._y, self._pencolor) # error correction if self._heading != (start_angle + angle) % self._fullcircle: self._heading = start_angle + angle self._heading %= self._fullcircle self._plot(self._x, self._y, self._pencolor) def _GCD(self, a: int, b: int) -> int: """GCD(a,b): recursive 'Greatest common divisor' calculus for int numbers a and b""" if b == 0: return a r = a % b return self._GCD(b, r)