-
Notifications
You must be signed in to change notification settings - Fork 727
/
Turtle.py
515 lines (399 loc) · 19.6 KB
/
Turtle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
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 = (300, 150)
DEFAULT_SPEED = 6
DEFAULT_TURTLE_VISIBILITY = True
DEFAULT_PEN_COLOR = '#663399'
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 make_turtle() 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 moveto(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))
goto = moveto # alias
setpos = moveto # alias
setposition = moveto # alias
# jump to a given location without leaving a trail
def jumpto(x, y=None):
flag = is_pen_down
penup()
goto(x, y)
if flag:
pendown()
# 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]