Created
May 25, 2023 20:39
-
-
Save AllenDowney/66a18dec4864b1a19c52cd98bba08f73 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from IPython.display import display, HTML | |
import time | |
import math | |
import re | |
# Created at: 23rd October 2018 | |
# by: Tolga Atam | |
# v2.1.0 Updated at: 15th March 2021 | |
# by: Tolga Atam | |
# from https://github.com/tolgaatam/ColabTurtle/blob/master/ColabTurtle/Turtle.py | |
# vX.X.X Updated at by Allen Downey for Think Python 3e | |
# Module for drawing classic Turtle figures on Google Colab notebooks. | |
# It uses html capabilites of IPython library to draw svg shapes inline. | |
# Looks of the figures are inspired from Blockly Games / Turtle (blockly-games.appspot.com/turtle) | |
DEFAULT_WINDOW_SIZE = (250, 150) | |
DEFAULT_SPEED = 4 | |
DEFAULT_TURTLE_VISIBILITY = True | |
DEFAULT_PEN_COLOR = 'darkblue' | |
DEFAULT_TURTLE_COLOR = 'gray' | |
DEFAULT_TURTLE_DEGREE = 0 | |
DEFAULT_BACKGROUND_COLOR = 'white' | |
DEFAULT_IS_PEN_DOWN = True | |
DEFAULT_SVG_LINES_STRING = "" | |
DEFAULT_PEN_WIDTH = 2 | |
# all 140 color names that modern browsers support. taken from https://www.w3schools.com/colors/colors_names.asp | |
VALID_COLORS = ('black', 'navy', 'darkblue', 'mediumblue', 'blue', 'darkgreen', 'green', 'teal', 'darkcyan', 'deepskyblue', 'darkturquoise', 'mediumspringgreen', 'lime', 'springgreen', 'aqua', 'cyan', 'midnightblue', 'dodgerblue', 'lightseagreen', 'forestgreen', 'seagreen', 'darkslategray', 'darkslategrey', 'limegreen', 'mediumseagreen', 'turquoise', 'royalblue', 'steelblue', 'darkslateblue', 'mediumturquoise', 'indigo', 'darkolivegreen', 'cadetblue', 'cornflowerblue', 'rebeccapurple', 'mediumaquamarine', 'dimgray', 'dimgrey', 'slateblue', 'olivedrab', 'slategray', 'slategrey', 'lightslategray', 'lightslategrey', 'mediumslateblue', 'lawngreen', 'chartreuse', 'aquamarine', 'maroon', 'purple', 'olive', 'gray', 'grey', 'skyblue', 'lightskyblue', 'blueviolet', 'darkred', 'darkmagenta', 'saddlebrown', 'darkseagreen', 'lightgreen', 'mediumpurple', 'darkviolet', 'palegreen', 'darkorchid', 'yellowgreen', 'sienna', 'brown', 'darkgray', 'darkgrey', 'lightblue', 'greenyellow', 'paleturquoise', 'lightsteelblue', 'powderblue', 'firebrick', 'darkgoldenrod', 'mediumorchid', 'rosybrown', 'darkkhaki', 'silver', 'mediumvioletred', 'indianred', 'peru', 'chocolate', 'tan', 'lightgray', 'lightgrey', 'thistle', 'orchid', 'goldenrod', 'palevioletred', 'crimson', 'gainsboro', 'plum', 'burlywood', 'lightcyan', 'lavender', 'darksalmon', 'violet', 'palegoldenrod', 'lightcoral', 'khaki', 'aliceblue', 'honeydew', 'azure', 'sandybrown', 'wheat', 'beige', 'whitesmoke', 'mintcream', 'ghostwhite', 'salmon', 'antiquewhite', 'linen', 'lightgoldenrodyellow', 'oldlace', 'red', 'fuchsia', 'magenta', 'deeppink', 'orangered', 'tomato', 'hotpink', 'coral', 'darkorange', 'lightsalmon', 'orange', 'lightpink', 'pink', 'gold', 'peachpuff', 'navajowhite', 'moccasin', 'bisque', 'mistyrose', 'blanchedalmond', 'papayawhip', 'lavenderblush', 'seashell', 'cornsilk', 'lemonchiffon', 'floralwhite', 'snow', 'yellow', 'lightyellow', 'ivory', 'white') | |
VALID_COLORS_SET = set(VALID_COLORS) | |
DEFAULT_TURTLE_SHAPE = 'circle' | |
VALID_TURTLE_SHAPES = ('turtle', 'circle') | |
SVG_TEMPLATE = """ | |
<svg width="{window_width}" height="{window_height}"> | |
<rect width="100%" height="100%" fill="{background_color}"/> | |
{lines} | |
{turtle} | |
</svg> | |
""" | |
TURTLE_TURTLE_SVG_TEMPLATE = """<g visibility={visibility} transform="rotate({degrees},{rotation_x},{rotation_y}) translate({turtle_x}, {turtle_y})"> | |
<path style=" stroke:none;fill-rule:evenodd;fill:{turtle_color};fill-opacity:1;" d="M 18.214844 0.632812 C 16.109375 1.800781 15.011719 4.074219 15.074219 7.132812 L 15.085938 7.652344 L 14.785156 7.496094 C 13.476562 6.824219 11.957031 6.671875 10.40625 7.066406 C 8.46875 7.550781 6.515625 9.15625 4.394531 11.992188 C 3.0625 13.777344 2.679688 14.636719 3.042969 15.027344 L 3.15625 15.152344 L 3.519531 15.152344 C 4.238281 15.152344 4.828125 14.886719 8.1875 13.039062 C 9.386719 12.378906 10.371094 11.839844 10.378906 11.839844 C 10.386719 11.839844 10.355469 11.929688 10.304688 12.035156 C 9.832031 13.09375 9.257812 14.820312 8.96875 16.078125 C 7.914062 20.652344 8.617188 24.53125 11.070312 27.660156 C 11.351562 28.015625 11.363281 27.914062 10.972656 28.382812 C 8.925781 30.84375 7.945312 33.28125 8.238281 35.1875 C 8.289062 35.527344 8.28125 35.523438 8.917969 35.523438 C 10.941406 35.523438 13.074219 34.207031 15.136719 31.6875 C 15.359375 31.417969 15.328125 31.425781 15.5625 31.574219 C 16.292969 32.042969 18.023438 32.964844 18.175781 32.964844 C 18.335938 32.964844 19.941406 32.210938 20.828125 31.71875 C 20.996094 31.625 21.136719 31.554688 21.136719 31.558594 C 21.203125 31.664062 21.898438 32.414062 22.222656 32.730469 C 23.835938 34.300781 25.5625 35.132812 27.582031 35.300781 C 27.90625 35.328125 27.9375 35.308594 28.007812 34.984375 C 28.382812 33.242188 27.625 30.925781 25.863281 28.425781 L 25.542969 27.96875 L 25.699219 27.785156 C 28.945312 23.960938 29.132812 18.699219 26.257812 11.96875 L 26.207031 11.84375 L 27.945312 12.703125 C 31.53125 14.476562 32.316406 14.800781 33.03125 14.800781 C 33.976562 14.800781 33.78125 13.9375 32.472656 12.292969 C 28.519531 7.355469 25.394531 5.925781 21.921875 7.472656 L 21.558594 7.636719 L 21.578125 7.542969 C 21.699219 6.992188 21.761719 5.742188 21.699219 5.164062 C 21.496094 3.296875 20.664062 1.964844 19.003906 0.855469 C 18.480469 0.503906 18.457031 0.5 18.214844 0.632812"/> | |
</g>""" | |
TURTLE_CIRCLE_SVG_TEMPLATE = """ | |
<g visibility={visibility} transform="rotate({degrees},{rotation_x},{rotation_y}) translate({turtle_x}, {turtle_y})"> | |
<circle stroke="{turtle_color}" stroke-width="2" fill="transparent" r="5.5" cx="0" cy="0"/> | |
<polygon points="0,12 2,9 -2,9" style="fill:{turtle_color};stroke:{turtle_color};stroke-width:2"/> | |
</g> | |
""" | |
SPEED_TO_SEC_MAP = {1: 1.5, 2: 0.9, 3: 0.7, 4: 0.5, 5: 0.3, 6: 0.18, 7: 0.12, 8: 0.06, 9: 0.04, 10: 0.02, 11: 0.01, 12: 0.001, 13: 0.0001} | |
# helper function that maps [1,13] speed values to ms delays | |
def _speedToSec(speed): | |
return SPEED_TO_SEC_MAP[speed] | |
turtle_speed = DEFAULT_SPEED | |
is_turtle_visible = DEFAULT_TURTLE_VISIBILITY | |
pen_color = DEFAULT_PEN_COLOR | |
window_size = DEFAULT_WINDOW_SIZE | |
turtle_pos = (DEFAULT_WINDOW_SIZE[0] // 2, DEFAULT_WINDOW_SIZE[1] // 2) | |
turtle_degree = DEFAULT_TURTLE_DEGREE | |
background_color = DEFAULT_BACKGROUND_COLOR | |
is_pen_down = DEFAULT_IS_PEN_DOWN | |
svg_lines_string = DEFAULT_SVG_LINES_STRING | |
pen_width = DEFAULT_PEN_WIDTH | |
turtle_shape = DEFAULT_TURTLE_SHAPE | |
drawing_window = None | |
# construct the display for turtle | |
def make_turtle(speed=DEFAULT_SPEED, width=DEFAULT_WINDOW_SIZE[0], height=DEFAULT_WINDOW_SIZE[1]): | |
global window_size | |
global drawing_window | |
global turtle_speed | |
global is_turtle_visible | |
global pen_color | |
global turtle_color | |
global turtle_pos | |
global turtle_degree | |
global background_color | |
global is_pen_down | |
global svg_lines_string | |
global pen_width | |
global turtle_shape | |
if isinstance(speed,int) == False or speed not in range(1, 14): | |
raise ValueError('speed must be an integer in interval [1,13]') | |
turtle_speed = speed | |
window_size = width, height | |
if not (isinstance(window_size, tuple) and | |
isinstance(window_size[0], int) and | |
isinstance(window_size[1], int)): | |
raise ValueError('window_size must be a tuple of 2 integers') | |
is_turtle_visible = DEFAULT_TURTLE_VISIBILITY | |
pen_color = DEFAULT_PEN_COLOR | |
turtle_color = DEFAULT_TURTLE_COLOR | |
turtle_pos = (window_size[0] // 2, window_size[1] // 2) | |
turtle_degree = DEFAULT_TURTLE_DEGREE | |
background_color = DEFAULT_BACKGROUND_COLOR | |
is_pen_down = DEFAULT_IS_PEN_DOWN | |
svg_lines_string = DEFAULT_SVG_LINES_STRING | |
pen_width = DEFAULT_PEN_WIDTH | |
turtle_shape = DEFAULT_TURTLE_SHAPE | |
drawing_window = display(HTML(_generateSvgDrawing()), display_id=True) | |
# helper function for generating svg string of the turtle | |
def _generateTurtleSvgDrawing(): | |
if is_turtle_visible: | |
vis = 'visible' | |
else: | |
vis = 'hidden' | |
turtle_x = turtle_pos[0] | |
turtle_y = turtle_pos[1] | |
degrees = turtle_degree | |
template = '' | |
if turtle_shape == 'turtle': | |
turtle_x -= 18 | |
turtle_y -= 18 | |
degrees += 90 | |
template = TURTLE_TURTLE_SVG_TEMPLATE | |
else: #circle | |
degrees -= 90 | |
template = TURTLE_CIRCLE_SVG_TEMPLATE | |
return template.format(turtle_color=turtle_color, turtle_x=turtle_x, turtle_y=turtle_y, \ | |
visibility=vis, degrees=degrees, rotation_x=turtle_pos[0], rotation_y=turtle_pos[1]) | |
# helper function for generating the whole svg string | |
def _generateSvgDrawing(): | |
return SVG_TEMPLATE.format(window_width=window_size[0], window_height=window_size[1], | |
background_color=background_color, lines=svg_lines_string, | |
turtle=_generateTurtleSvgDrawing()) | |
# helper functions for updating the screen using the latest positions/angles/lines etc. | |
def _updateDrawing(): | |
if drawing_window == None: | |
raise AttributeError("Display has not been initialized yet. Call initializeTurtle() before using.") | |
time.sleep(_speedToSec(turtle_speed)) | |
drawing_window.update(HTML(_generateSvgDrawing())) | |
# helper function for managing any kind of move to a given 'new_pos' and draw lines if pen is down | |
def _moveToNewPosition(new_pos): | |
global turtle_pos | |
global svg_lines_string | |
# rounding the new_pos to eliminate floating point errors. | |
new_pos = ( round(new_pos[0],3), round(new_pos[1],3) ) | |
start_pos = turtle_pos | |
if is_pen_down: | |
svg_lines_string += """<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke-linecap="round" style="stroke:{pen_color};stroke-width:{pen_width}"/>""".format( | |
x1=start_pos[0], y1=start_pos[1], x2=new_pos[0], y2=new_pos[1], pen_color=pen_color, pen_width=pen_width) | |
turtle_pos = new_pos | |
_updateDrawing() | |
# makes the turtle move forward by 'units' units | |
def forward(units): | |
if not isinstance(units, (int,float)): | |
raise ValueError('units must be a number.') | |
alpha = math.radians(turtle_degree) | |
ending_point = (turtle_pos[0] + units * math.cos(alpha), turtle_pos[1] + units * math.sin(alpha)) | |
_moveToNewPosition(ending_point) | |
fd = forward # alias | |
# makes the turtle move backward by 'units' units | |
def backward(units): | |
if not isinstance(units, (int,float)): | |
raise ValueError('units must be a number.') | |
forward(-1 * units) | |
bk = backward # alias | |
back = backward # alias | |
# makes the turtle move right by 'degrees' degrees (NOT radians) | |
def right(degrees): | |
global turtle_degree | |
if not isinstance(degrees, (int,float)): | |
raise ValueError('degrees must be a number.') | |
turtle_degree = (turtle_degree + degrees) % 360 | |
_updateDrawing() | |
rt = right # alias | |
# makes the turtle face a given direction | |
def face(degrees): | |
global turtle_degree | |
if not isinstance(degrees, (int,float)): | |
raise ValueError('degrees must be a number.') | |
turtle_degree = degrees % 360 | |
_updateDrawing() | |
setheading = face # alias | |
seth = face # alias | |
# makes the turtle move right by 'degrees' degrees (NOT radians, this library does not support radians right now) | |
def left(degrees): | |
if not isinstance(degrees, (int,float)): | |
raise ValueError('degrees must be a number.') | |
right(-1 * degrees) | |
lt = left | |
# raises the pen such that following turtle moves will not cause any drawings | |
def penup(): | |
global is_pen_down | |
is_pen_down = False | |
# TODO: decide if we should put the timout after lifting the pen | |
# _updateDrawing() | |
pu = penup # alias | |
up = penup # alias | |
# lowers the pen such that following turtle moves will now cause drawings | |
def pendown(): | |
global is_pen_down | |
is_pen_down = True | |
# TODO: decide if we should put the timout after releasing the pen | |
# _updateDrawing() | |
pd = pendown # alias | |
down = pendown # alias | |
def isdown(): | |
return is_pen_down | |
# update the speed of the moves, [1,13] | |
# if argument is omitted, it returns the speed. | |
def speed(speed = None): | |
global turtle_speed | |
if speed is None: | |
return turtle_speed | |
if isinstance(speed,int) == False or speed not in range(1, 14): | |
raise ValueError('speed must be an integer in the interval [1,13].') | |
turtle_speed = speed | |
# TODO: decide if we should put the timout after changing the speed | |
# _updateDrawing() | |
# move the turtle to a designated 'x' x-coordinate, y-coordinate stays the same | |
def setx(x): | |
if not isinstance(x, (int,float)): | |
raise ValueError('new x position must be a number.') | |
if x < 0: | |
raise ValueError('new x position must be non-negative.') | |
_moveToNewPosition((x, turtle_pos[1])) | |
# move the turtle to a designated 'y' y-coordinate, x-coordinate stays the same | |
def sety(y): | |
if not isinstance(y, (int,float)): | |
raise ValueError('new y position must be a number.') | |
if y < 0: | |
raise ValueError('new y position must be non-negative.') | |
_moveToNewPosition((turtle_pos[0], y)) | |
def home(): | |
global turtle_degree | |
turtle_degree = DEFAULT_TURTLE_DEGREE | |
_moveToNewPosition( (window_size[0] // 2, window_size[1] // 2) ) # this will handle updating the drawing. | |
# retrieve the turtle's currrent 'x' x-coordinate | |
def getx(): | |
return(turtle_pos[0]) | |
xcor = getx # alias | |
# retrieve the turtle's currrent 'y' y-coordinate | |
def gety(): | |
return(turtle_pos[1]) | |
ycor = gety # alias | |
# retrieve the turtle's current position as a (x,y) tuple vector | |
def position(): | |
return turtle_pos | |
pos = position # alias | |
# retrieve the turtle's current angle | |
def getheading(): | |
return turtle_degree | |
heading = getheading # alias | |
# move the turtle to a designated 'x'-'y' coordinate | |
def goto(x, y=None): | |
if isinstance(x, tuple) and y is None: | |
if len(x) != 2: | |
raise ValueError('the tuple argument must be of length 2.') | |
y = x[1] | |
x = x[0] | |
if not isinstance(x, (int,float)): | |
raise ValueError('new x position must be a number.') | |
if x < 0: | |
raise ValueError('new x position must be non-negative') | |
if not isinstance(y, (int,float)): | |
raise ValueError('new y position must be a number.') | |
if y < 0: | |
raise ValueError('new y position must be non-negative.') | |
_moveToNewPosition((x, y)) | |
setpos = goto # alias | |
setposition = goto # alias | |
# switch turtle visibility to ON | |
def showturtle(): | |
global is_turtle_visible | |
is_turtle_visible = True | |
_updateDrawing() | |
st = showturtle # alias | |
# switch turtle visibility to OFF | |
def hideturtle(): | |
global is_turtle_visible | |
is_turtle_visible = False | |
_updateDrawing() | |
ht = hideturtle # alias | |
def isvisible(): | |
return is_turtle_visible | |
def _validateColorString(color): | |
if color in VALID_COLORS_SET: # 140 predefined html color names | |
return True | |
if re.search("^#(?:[0-9a-fA-F]{3}){1,2}$", color): # 3 or 6 digit hex color code | |
return True | |
if re.search("rgb\(\s*(?:(?:\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5]))\s*,?){3}\)$", color): # rgb color code | |
return True | |
return False | |
def _validateColorTuple(color): | |
if len(color) != 3: | |
return False | |
if not isinstance(color[0], int) or not isinstance(color[1], int) or not isinstance(color[2], int): | |
return False | |
if not 0 <= color[0] <= 255 or not 0 <= color[1] <= 255 or not 0 <= color[2] <= 255: | |
return False | |
return True | |
def _processColor(color): | |
if isinstance(color, str): | |
color = color.lower() | |
if not _validateColorString(color): | |
raise ValueError('color is invalid. it can be a known html color name, 3-6 digit hex string or rgb string.') | |
return color | |
elif isinstance(color, tuple): | |
if not _validateColorTuple(color): | |
raise ValueError('color tuple is invalid. it must be a tuple of three integers, which are in the interval [0,255]') | |
return 'rgb(' + str(color[0]) + ',' + str(color[1]) + ',' + str(color[2]) + ')' | |
else: | |
raise ValueError('the first parameter must be a color string or a tuple') | |
# change the background color of the drawing area | |
# if no params, return the current background color | |
def bgcolor(color = None, c2 = None, c3 = None): | |
global background_color | |
if color is None: | |
return background_color | |
elif c2 is not None: | |
if c3 is None: | |
raise ValueError('if the second argument is set, the third arguments must be set as well to complete the rgb set.') | |
color = (color, c2, c3) | |
background_color = _processColor(color) | |
_updateDrawing() | |
# change the color of the pen | |
# if no params, return the current pen color | |
def color(color = None, c2 = None, c3 = None): | |
global pen_color | |
if color is None: | |
return pen_color | |
elif c2 is not None: | |
if c3 is None: | |
raise ValueError('if the second argument is set, the third arguments must be set as well to complete the rgb set.') | |
color = (color, c2, c3) | |
pen_color = _processColor(color) | |
_updateDrawing() | |
pencolor = color | |
# change the width of the lines drawn by the turtle, in pixels | |
# if the function is called without arguments, it returns the current width | |
def width(width = None): | |
global pen_width | |
if width is None: | |
return pen_width | |
else: | |
if not isinstance(width, int): | |
raise ValueError('new width position must be an integer.') | |
if not width > 0: | |
raise ValueError('new width position must be positive.') | |
pen_width = width | |
# TODO: decide if we should put the timout after changing the pen_width | |
# _updateDrawing() | |
# pensize is an alias for width | |
pensize = width | |
# clear any text or drawing on the screen | |
def clear(): | |
global svg_lines_string | |
svg_lines_string = "" | |
_updateDrawing() | |
def write(obj, **kwargs): | |
global svg_lines_string | |
global turtle_pos | |
text = str(obj) | |
font_size = 12 | |
font_family = 'Arial' | |
font_type = 'normal' | |
align = 'start' | |
if 'align' in kwargs and kwargs['align'] in ('left', 'center', 'right'): | |
if kwargs['align'] == 'left': | |
align = 'start' | |
elif kwargs['align'] == 'center': | |
align = 'middle' | |
else: | |
align = 'end' | |
if "font" in kwargs: | |
font = kwargs["font"] | |
if len(font) != 3 or isinstance(font[0], int) == False or isinstance(font[1], str) == False or font[2] not in {'bold','italic','underline','normal'}: | |
raise ValueError('font parameter must be a triplet consisting of font size (int), font family (str) and font type. font type can be one of {bold, italic, underline, normal}') | |
font_size = font[0] | |
font_family = font[1] | |
font_type = font[2] | |
style_string = "" | |
style_string += "font-size:" + str(font_size) + "px;" | |
style_string += "font-family:'" + font_family + "';" | |
if font_type == 'bold': | |
style_string += "font-weight:bold;" | |
elif font_type == 'italic': | |
style_string += "font-style:italic;" | |
elif font_type == 'underline': | |
style_string += "text-decoration: underline;" | |
svg_lines_string += """<text x="{x}" y="{y}" fill="{fill_color}" text-anchor="{align}" style="{style}">{text}</text>""".format(x=turtle_pos[0], y=turtle_pos[1], text=text, fill_color=pen_color, align=align, style=style_string) | |
_updateDrawing() | |
def shape(shape=None): | |
global turtle_shape | |
if shape is None: | |
return turtle_shape | |
elif shape not in VALID_TURTLE_SHAPES: | |
raise ValueError('shape is invalid. valid options are: ' + str(VALID_TURTLE_SHAPES)) | |
turtle_shape = shape | |
_updateDrawing() | |
# return turtle window width | |
def window_width(): | |
return window_size[0] | |
# return turtle window height | |
def window_height(): | |
return window_size[1] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment