Adafruit_CircuitPython_turtle/adafruit_turtle.py
2024-12-02 16:06:44 -06:00

1256 lines
40 KiB
Python

# 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._logomode = True
self._fullcircle = 360.0
self._degreesPerAU = 1.0
self._angleOrient = 1
self._angleOffset: float = 0
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
x0 = self._x
y0 = self._y
if not self.isdown():
self._x = xn # woot, we just skip ahead
self._y = yn
self._drawturtle()
return
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
self._x = y0
self._y = x0
else:
try:
self._plot(int(x0), int(y0), self._pencolor)
except IndexError:
pass
self._x = x0
self._y = y0
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)