jupyter-turtle 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Mike Matera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-turtle
3
+ Version: 1.0.1
4
+ Summary: Simplified Turtle graphics for the Jupyter Notebook
5
+ Author-email: Mike Matera <michael.matera@cabrillo.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mike-matera/jupyter-turtle
8
+ Project-URL: Issues, https://github.com/mike-matera/jupyter-turtle/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: ipycanvas
15
+ Requires-Dist: jupyterlab
16
+ Dynamic: license-file
17
+
18
+ # Jupyter Turtle
19
+
20
+ This is an implementation of simplified Turtle graphics to support my [Python
21
+ for Everyone](https://github.com/mike-matera/python-for-everyone) course.
22
+ [Logo](https://en.wikipedia.org/wiki/Logo_\(programming_language\)) was my first
23
+ programming language. The purpose of this implementation is to give beginners a
24
+ visual experience to help them understand algorithmic reasoning. The module
25
+ contains a `Turtle` class implementation with a full API and a simplified API
26
+ that I use in class.
27
+
@@ -0,0 +1,10 @@
1
+ # Jupyter Turtle
2
+
3
+ This is an implementation of simplified Turtle graphics to support my [Python
4
+ for Everyone](https://github.com/mike-matera/python-for-everyone) course.
5
+ [Logo](https://en.wikipedia.org/wiki/Logo_\(programming_language\)) was my first
6
+ programming language. The purpose of this implementation is to give beginners a
7
+ visual experience to help them understand algorithmic reasoning. The module
8
+ contains a `Turtle` class implementation with a full API and a simplified API
9
+ that I use in class.
10
+
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "jupyter-turtle"
3
+ authors = [
4
+ {name = "Mike Matera", email = "michael.matera@cabrillo.edu"},
5
+ ]
6
+ description = "Simplified Turtle graphics for the Jupyter Notebook"
7
+ readme = "README.md"
8
+ requires-python = ">=3.9"
9
+ version = "1.0.1"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICEN[CS]E*"]
16
+
17
+ dependencies = [
18
+ "ipycanvas",
19
+ "jupyterlab",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/mike-matera/jupyter-turtle"
24
+ Issues = "https://github.com/mike-matera/jupyter-turtle/issues"
25
+
26
+ [tool.setuptools.package-data]
27
+ jupyter_turtle = ["*.png"]
28
+
29
+ [build-system]
30
+ requires = ["setuptools"]
31
+ build-backend = "setuptools.build_meta"
32
+
33
+ [tool.ruff]
34
+ line-length = 79
35
+
36
+ [tool.ruff.format]
37
+ quote-style = "double"
38
+ indent-style = "space"
39
+ docstring-code-format = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,201 @@
1
+ import contextlib
2
+ from typing import Sequence, Tuple, Union
3
+
4
+ from IPython.display import display
5
+
6
+ from .turtle import DimPoint, Turtle
7
+
8
+ _turtle = None
9
+
10
+ # The simplified API.
11
+
12
+
13
+ def _check_turtle():
14
+ global _turtle
15
+ if _turtle is None:
16
+ _turtle = Turtle()
17
+ _turtle.stats = {
18
+ "moves": 0,
19
+ "jumps": 0,
20
+ "strokes": 0,
21
+ "turns": 0,
22
+ "words": [],
23
+ "_last_down": 0,
24
+ }
25
+ display(_turtle)
26
+ return _turtle
27
+
28
+
29
+ def clear():
30
+ """Clear the canvas."""
31
+ tu = _check_turtle()
32
+ _turtle.stats = {
33
+ "moves": 0,
34
+ "jumps": 0,
35
+ "strokes": 0,
36
+ "turns": 0,
37
+ "words": [],
38
+ "_last_down": 0,
39
+ }
40
+ tu.clear()
41
+
42
+
43
+ def move(distance: float):
44
+ """Move the turtle by distance pixels."""
45
+ tu = _check_turtle()
46
+ tu.stats["moves"] += 1
47
+ tu.move(distance)
48
+
49
+
50
+ def turn(degrees: float):
51
+ """Turn the pen by degrees. Positive numbers turn left, negative numbers
52
+ turn right."""
53
+ tu = _check_turtle()
54
+ tu.stats["turns"] += 1
55
+ tu.turn(degrees)
56
+
57
+
58
+ def pen_up():
59
+ """Pick the pen up. Movements won't make lines."""
60
+ tu = _check_turtle()
61
+ if tu.stats["moves"] > tu.stats["_last_down"]:
62
+ tu.stats["strokes"] += 1
63
+ tu.pen_up()
64
+
65
+
66
+ def pen_down():
67
+ """Put the pen down. Movements will make lines."""
68
+ tu = _check_turtle()
69
+ tu.stats["_last_down"] = tu.stats["moves"]
70
+ tu.pen_down()
71
+
72
+
73
+ def show_turtle():
74
+ """Show the turtle in the scene."""
75
+ tu = _check_turtle()
76
+ tu.show()
77
+
78
+
79
+ def hide_turtle():
80
+ """Hide the turtle in the scene."""
81
+ tu = _check_turtle()
82
+ tu.hide()
83
+
84
+
85
+ def write(
86
+ text: str, font: str = "24px sans-serif", text_align: str = "center"
87
+ ):
88
+ """Write text.
89
+
90
+ Arguments:
91
+
92
+ text: The text to write
93
+ font: The HTML font specification
94
+ text_align: The alignment of the text relative to the turtle
95
+ """
96
+ tu = _check_turtle()
97
+ tu.stats["words"].append(text)
98
+ tu.write(text, font, text_align)
99
+
100
+
101
+ def goto(*place: Union[Tuple[int, int], Sequence[int], DimPoint]):
102
+ """Jump to a point"""
103
+ tu = _check_turtle()
104
+ tu.stats["moves"] += 1
105
+ tu.stats["jumps"] += 1
106
+ tu.pos = place
107
+
108
+
109
+ def set_background(filename: str):
110
+ """Set the background image"""
111
+ tu = _check_turtle()
112
+ tu.background(filename)
113
+
114
+
115
+ def set_heading(heading: float):
116
+ """Set the pen to face heading in degrees."""
117
+ tu = _check_turtle()
118
+ tu.stats["turns"] += 1
119
+ tu.heading = heading
120
+
121
+
122
+ def set_color(color: Union[str, int]):
123
+ """Set the pen color using HTML color notation."""
124
+ tu = _check_turtle()
125
+ tu.color = color
126
+
127
+
128
+ def set_width(width: int):
129
+ """Set the line thickness."""
130
+ tu = _check_turtle()
131
+ tu.width = width
132
+
133
+
134
+ def set_size(width: int, height: int):
135
+ """Set the size of the arena."""
136
+ tu = _check_turtle()
137
+ tu.size = (width, height)
138
+
139
+
140
+ @contextlib.contextmanager
141
+ def polygon(
142
+ *,
143
+ color: str | None = None,
144
+ width: int | None = None,
145
+ fill: str | None = None,
146
+ ):
147
+ """
148
+ Draw a polygon by connecting moves together.
149
+
150
+ Example:
151
+
152
+ with tu.polygon():
153
+ tu.move(30)
154
+ tu.turn(45)
155
+ tu.move(30)
156
+ tu.turn(45)
157
+ tu.move(30)
158
+ """
159
+ tu = _check_turtle()
160
+ old_color = tu.color
161
+ old_width = tu.width
162
+ old_fill = tu.fill
163
+ try:
164
+ if color is not None:
165
+ tu.color = color
166
+ if fill is not None:
167
+ tu.fill = fill
168
+ if width is not None:
169
+ tu.width = width
170
+
171
+ with tu.polygon():
172
+ yield
173
+
174
+ finally:
175
+ tu.color = old_color
176
+ tu.fill = old_fill
177
+ tu.width = old_width
178
+
179
+
180
+ def pre_run_cell(info):
181
+ """
182
+ Callback before a cell is run.
183
+ """
184
+ global _turtle
185
+ _turtle = None
186
+
187
+
188
+ def post_run_cell(result):
189
+ """
190
+ Callback after a cell has run.
191
+ """
192
+ global _turtle
193
+ result.turtle = _turtle
194
+ if _turtle is not None:
195
+ if _turtle.stats["moves"] > _turtle.stats["_last_down"]:
196
+ _turtle.stats["strokes"] += 1
197
+
198
+
199
+ def load_ipython_extension(ipython):
200
+ ipython.events.register("pre_run_cell", pre_run_cell)
201
+ ipython.events.register("post_run_cell", post_run_cell)
@@ -0,0 +1,290 @@
1
+ """
2
+ A simple implementation of turtle graphics for teaching algorithms.
3
+
4
+ Author: Mike Matera
5
+ """
6
+
7
+ import colorsys
8
+ import contextlib
9
+ import math
10
+ import pathlib
11
+ from collections import namedtuple
12
+ from collections.abc import Sequence
13
+
14
+ import numpy
15
+ import PIL
16
+ from ipycanvas import Canvas, MultiCanvas, hold_canvas
17
+ from IPython.display import display
18
+
19
+ DimPoint = namedtuple("DimPoint", ["x", "y"])
20
+
21
+
22
+ class Turtle:
23
+ def __init__(self, size: tuple[int, int] = (600, 300), **kwargs):
24
+ """Create a Turtle drawing canvas.
25
+
26
+ Arguments:
27
+ size: Set the size of the canvas.
28
+ """
29
+ # Load the Turtle image
30
+ turtle = numpy.array(
31
+ PIL.Image.open(pathlib.Path(__file__).parent / "turtle.png")
32
+ )
33
+ self._turtle = Canvas(width=turtle.shape[0], height=turtle.shape[1])
34
+ self._turtle.put_image_data(turtle)
35
+
36
+ # Create a new Canvas
37
+ self._size = DimPoint(size[0], size[1])
38
+ self._canvas = MultiCanvas(
39
+ n_canvases=3,
40
+ width=self._size.x,
41
+ height=self._size.y,
42
+ sync_image_data=True,
43
+ **kwargs,
44
+ )
45
+
46
+ # Initialize properties
47
+ self._current = self._to_native(DimPoint(0, 0))
48
+ self._cur_heading = (3 * math.pi) / 2 # in Canvas Y is negative.
49
+ self._pendown = True
50
+ self._show = True
51
+ self._fill = None
52
+ self._image = None
53
+ self._polygon = False
54
+
55
+ # Render the turtle
56
+ with self._do_draw():
57
+ self._canvas.clear()
58
+
59
+ @contextlib.contextmanager
60
+ def _do_draw(self):
61
+ """Context manager that combines all drawing operations and re-renders
62
+ the turtle."""
63
+ with hold_canvas(self._canvas):
64
+ yield
65
+ self._canvas[2].clear()
66
+ if self._show:
67
+ # Redraw the Turtle
68
+ self._canvas[2].save()
69
+ self._canvas[2].translate(self._current.x, self._current.y)
70
+ self._canvas[2].rotate(self._cur_heading + math.pi / 2)
71
+ self._canvas[2].draw_image(
72
+ self._turtle, x=-15, y=-15, width=30, height=30
73
+ )
74
+ self._canvas[2].restore()
75
+
76
+ def _to_native(self, point: tuple[int, int]) -> DimPoint:
77
+ """Convert Turtle coordinates to native ones."""
78
+ return DimPoint(
79
+ x=self._size.x // 2 + point[0], y=self._size.y // 2 - point[1]
80
+ )
81
+
82
+ def _to_turtle(self, point: DimPoint) -> tuple[int, int]:
83
+ """Convert Turtle coordinates to native ones."""
84
+ return (point[0] - self._size.x // 2, self._size.y // 2 - point[1])
85
+
86
+ def _ipython_display_(self):
87
+ display(self._canvas)
88
+
89
+ def _move(self, to: DimPoint):
90
+ """Low-level move operation."""
91
+ start = self._current
92
+ self._current = to
93
+ with self._do_draw():
94
+ if self._polygon:
95
+ if self._pendown:
96
+ self._canvas[1].line_to(*self._current)
97
+ else:
98
+ self._canvas[1].move_to(*start)
99
+ else:
100
+ if self._pendown:
101
+ self._canvas[1].begin_path()
102
+ self._canvas[1].move_to(*start)
103
+ self._canvas[1].line_to(*self._current)
104
+ self._canvas[1].stroke()
105
+
106
+ def clear(self):
107
+ """Clear all drawing."""
108
+ self._canvas[1].clear()
109
+
110
+ def move(self, distance: float):
111
+ """Move the turtle by distance pixels."""
112
+ self._move(
113
+ DimPoint(
114
+ x=self._current.x + math.cos(self._cur_heading) * distance,
115
+ y=self._current.y + math.sin(self._cur_heading) * distance,
116
+ )
117
+ )
118
+
119
+ def turn(self, degrees: float):
120
+ """Turn the pen by degrees."""
121
+ with self._do_draw():
122
+ self._cur_heading = (self._cur_heading - math.radians(degrees)) % (
123
+ math.pi * 2
124
+ )
125
+
126
+ def pen_up(self):
127
+ """Pick the pen up. Movements won't make lines."""
128
+ self._pendown = False
129
+
130
+ def pen_down(self):
131
+ """Put the pen down. Movements will make lines."""
132
+ self._pendown = True
133
+
134
+ def show(self):
135
+ """Show the turtle in the scene."""
136
+ with self._do_draw():
137
+ self._show = True
138
+
139
+ def hide(self):
140
+ """Hide the turtle in the scene."""
141
+ with self._do_draw():
142
+ self._show = False
143
+
144
+ def background(self, filename: str):
145
+ """Set the background image"""
146
+ self._image = numpy.array(PIL.Image.open(filename))
147
+ self.size = (self._image.shape[1], self._image.shape[0])
148
+ self._canvas[0].put_image_data(self._image)
149
+
150
+ @contextlib.contextmanager
151
+ def polygon(self):
152
+ """Context manager for drawing a polygon."""
153
+ self._polygon = True
154
+ self._canvas[1].begin_path()
155
+ self._canvas[1].move_to(*self._current)
156
+ try:
157
+ yield
158
+ finally:
159
+ self._canvas[1].close_path()
160
+ self._canvas[1].stroke()
161
+ if self._fill is not None:
162
+ self._canvas[1].fill()
163
+ self._polygon = False
164
+
165
+ def write(
166
+ self,
167
+ text: str,
168
+ font: str = "24px sans-serif",
169
+ text_align: str = "center",
170
+ ):
171
+ """Write text.
172
+
173
+ Arguments:
174
+
175
+ text: The text to write
176
+ font: The HTML font specification
177
+ text_align: The alignment of the text relative to the turtle
178
+ line_color: The color of the outline of the text (defaults to the pen color)
179
+ fill_color: The color of the fill of the text (defaults to the pen color)
180
+ """
181
+ with self._do_draw():
182
+ self._canvas[1].translate(self._current.x, self._current.y)
183
+ self._canvas[1].rotate(self._cur_heading + math.pi / 2)
184
+ self._canvas[1].font = font
185
+ self._canvas[1].text_align = text_align
186
+ self._canvas[1].fill_text(text, 0, 0)
187
+ self._canvas[1].stroke_text(text, 0, 0)
188
+ self._canvas[1].reset_transform()
189
+
190
+ @property
191
+ def size(self) -> tuple[int, int]:
192
+ """Get the size of the canvas' color buffer (not layout size)."""
193
+ return (self._size.x, self._size.y)
194
+
195
+ @size.setter
196
+ def size(self, newsize: tuple[int, int]):
197
+ """Resize the canvas element and adjust they layout size."""
198
+ self._size = DimPoint(x=newsize[0], y=newsize[1])
199
+ with self._do_draw():
200
+ self._canvas.width = self._size.x
201
+ self._canvas.height = self._size.y
202
+
203
+ # Move the turtle to the center
204
+ self._current = self._to_native(DimPoint(0, 0))
205
+ self._cur_heading = (3 * math.pi) / 2 # in Canvas Y is negative.
206
+
207
+ if self._size.x >= 800:
208
+ self._canvas.layout.width = "90%"
209
+ self._canvas.layout.max_width = f"{self._size.x}px"
210
+ self._canvas.layout.min_width = "800px"
211
+ else:
212
+ self._canvas.layout.width = "auto"
213
+ self._canvas.layout.max_width = "auto"
214
+ self._canvas.layout.min_width = "auto"
215
+
216
+ @property
217
+ def pos(self) -> tuple[int, int]:
218
+ """Get the current location of the Turtle."""
219
+ return self._to_turtle(self._current)
220
+
221
+ @pos.setter
222
+ def pos(self, *place: tuple[int, int] | Sequence[int] | DimPoint):
223
+ """Goto a point in the coordinate space."""
224
+ if len(place) == 0:
225
+ raise ValueError("Goto where?")
226
+ elif isinstance(place[0], DimPoint):
227
+ p = place[0]
228
+ elif isinstance(place[0], tuple):
229
+ p = DimPoint._make(*place)
230
+ else:
231
+ p = DimPoint._make(place)
232
+
233
+ self._move(self._to_native(p))
234
+
235
+ @property
236
+ def heading(self) -> float:
237
+ """Get the current heading."""
238
+ return -math.degrees(self._cur_heading + math.pi / 2) % 360
239
+
240
+ @heading.setter
241
+ def heading(self, heading: float):
242
+ """Set the pen to face heading in degrees."""
243
+ with self._do_draw():
244
+ self._cur_heading = math.radians(-heading - 90) % (math.pi * 2)
245
+
246
+ def _hue_to_html(self, hue: int):
247
+ """Convert a color number from 0 to 365 to HTML"""
248
+ rgb = colorsys.hsv_to_rgb((hue % 365) / 364, 1, 1)
249
+ return f"#{round(rgb[0] * 255):02x}{round(rgb[1] * 255):02x}{round(rgb[2] * 255):02x}"
250
+
251
+ @property
252
+ def color(self) -> str:
253
+ """Set the pen color using HTML color notation."""
254
+ return self._canvas[1].stroke_style
255
+
256
+ @color.setter
257
+ def color(self, color: str | int):
258
+ """Set the pen color using HTML color notation."""
259
+ if color.__class__ in [int, float]:
260
+ color = self._hue_to_html(color)
261
+ self._canvas[1].stroke_style = color
262
+
263
+ @property
264
+ def fill(self) -> str:
265
+ """Set the pen color using HTML color notation."""
266
+ return self._fill
267
+
268
+ @fill.setter
269
+ def fill(self, color: str):
270
+ """Set the pen color using HTML color notation."""
271
+ if color.__class__ in [int, float]:
272
+ color = self._hue_to_html(color)
273
+ self._fill = color
274
+ if color is not None:
275
+ self._canvas[1].fill_style = color
276
+
277
+ @property
278
+ def width(self) -> int:
279
+ """The line thickness."""
280
+ return self._canvas[1].line_width
281
+
282
+ @width.setter
283
+ def width(self, width: int):
284
+ """Set the line thickness."""
285
+ self._canvas[1].line_width = width
286
+
287
+ @property
288
+ def image(self) -> PIL.Image:
289
+ """Return an image of the Canvas."""
290
+ return PIL.Image.fromarray(self._image)
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-turtle
3
+ Version: 1.0.1
4
+ Summary: Simplified Turtle graphics for the Jupyter Notebook
5
+ Author-email: Mike Matera <michael.matera@cabrillo.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mike-matera/jupyter-turtle
8
+ Project-URL: Issues, https://github.com/mike-matera/jupyter-turtle/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: ipycanvas
15
+ Requires-Dist: jupyterlab
16
+ Dynamic: license-file
17
+
18
+ # Jupyter Turtle
19
+
20
+ This is an implementation of simplified Turtle graphics to support my [Python
21
+ for Everyone](https://github.com/mike-matera/python-for-everyone) course.
22
+ [Logo](https://en.wikipedia.org/wiki/Logo_\(programming_language\)) was my first
23
+ programming language. The purpose of this implementation is to give beginners a
24
+ visual experience to help them understand algorithmic reasoning. The module
25
+ contains a `Turtle` class implementation with a full API and a simplified API
26
+ that I use in class.
27
+
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/jupyter_turtle/__init__.py
5
+ src/jupyter_turtle/turtle.png
6
+ src/jupyter_turtle/turtle.py
7
+ src/jupyter_turtle.egg-info/PKG-INFO
8
+ src/jupyter_turtle.egg-info/SOURCES.txt
9
+ src/jupyter_turtle.egg-info/dependency_links.txt
10
+ src/jupyter_turtle.egg-info/requires.txt
11
+ src/jupyter_turtle.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ ipycanvas
2
+ jupyterlab
@@ -0,0 +1 @@
1
+ jupyter_turtle