Last active
November 27, 2022 23:05
-
-
Save iuriguilherme/196677d2bc95de343ba9f474e701b4ad to your computer and use it in GitHub Desktop.
Fibonacci Word Fractal Colorized
This file contains hidden or 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
| """ | |
| Fibonacci Word Fractal Colorized | |
| Public Domain Fibonacci Day (November 23) 2022 | |
| Copy pasted and adapted from these sources: | |
| https://rosettacode.org/wiki/Fibonacci_word/fractal#Python | |
| https://python-forum.io/thread-25822.html | |
| https://github.com/francofgp/dragon-curve | |
| https://stackoverflow.com/questions/35629520/ | |
| https://stackoverflow.com/questions/73388248/ | |
| https://tkdocs.com/tutorial/canvas.html | |
| https://docs.python.org/3.10/library/turtle.html | |
| https://docs.python.org/3.10/library/tkinter.html | |
| More about what is going on here: | |
| https://en.wikipedia.org/wiki/Fibonacci_word | |
| https://en.wikipedia.org/wiki/Fibonacci_word_fractal | |
| https://mathworld.wolfram.com/FibonacciNumber.html | |
| https://mathworld.wolfram.com/DragonCurve.html | |
| http://hal.archives-ouvertes.fr/docs/00/36/79/72/PDF/The_Fibonacci_word_fracta\ | |
| l.pdf | |
| This code could use async LRU caching but it has not been done because it's | |
| made to use only the standard library from Python 3.10. It assumes, however, | |
| that the user has ImageMagick or another way to convert a TK Frame to | |
| PostScript and furthermore to a Portable Network Graphic image. The package | |
| which does that is: https://pypi.org/project/async-lru/ | |
| """ | |
| import asyncio | |
| import colorama | |
| import functools | |
| import io | |
| import inspect | |
| import logging | |
| import tkinter | |
| import turtle | |
| import random | |
| import sys | |
| from PIL import Image | |
| from typing import Union | |
| name: str = 'coloridacci' | |
| # ~ logging.basicConfig(level = 'INFO') | |
| logging.basicConfig(level = 'DEBUG') | |
| logger: logging.Logger = logging.getLogger(name) | |
| try: | |
| colorama.init() | |
| except Exception as e: | |
| logger.exception(e) | |
| async def startup( | |
| *args, | |
| pen: Union[turtle.RawTurtle, None] = None, | |
| pen_angle: int = 90, | |
| pen_x: int = 0, | |
| pen_y: int = 0, | |
| pen_speed: Union[int, str] = 0, | |
| screen: Union[turtle.TurtleScreen, None] = None, | |
| screen_colormode: int = 255, | |
| screen_tracer: int = 1, | |
| canvas: Union[tkinter.Canvas, None] = None, | |
| canvas_width: int = 1, | |
| canvas_height: int = 1, | |
| root_width: int = 0, | |
| root_height: int = 0, | |
| root: Union[tkinter.Tk, None] = None, | |
| **kwargs, | |
| ) -> tuple[tkinter.Tk, tkinter.Canvas, turtle.TurtleScreen, turtle.RawTurtle]: | |
| """Turtling configuration and definitions""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| if root is None: | |
| root = tkinter.Tk() | |
| root.geometry("x".join([str(root_width), str(root_height)])) | |
| root.update() | |
| if canvas is None: | |
| canvas = tkinter.Canvas( | |
| root, | |
| width = canvas_width, | |
| height = canvas_height, | |
| ) | |
| canvas.pack( | |
| expand = 1, | |
| fill = 'both', | |
| padx = '3', | |
| pady = '3', | |
| ) | |
| # ~ canvas.grid() | |
| if screen is None: | |
| screen = turtle.TurtleScreen(canvas) | |
| screen.mode('world') | |
| screen.tracer(screen_tracer) | |
| screen.colormode(screen_colormode) | |
| if pen is None: | |
| pen = turtle.RawTurtle(screen) | |
| # ~ pen = turtle.Turtle() | |
| # ~ screen = pen.getscreen() | |
| # ~ canvas = screen.getcanvas() | |
| # ~ root = canvas.winfo_toplevel() | |
| pen.hideturtle() | |
| pen.speed(pen_speed) | |
| pen.penup() | |
| pen.setpos(pen_x, pen_y) | |
| pen.pendown() | |
| pen.setheading(pen_angle) | |
| logger.info(f"""Starting at ({pen_x}, {pen_y}) facing {pen_angle}° at \ | |
| {pen.speed()} speed""") | |
| return root, canvas, screen, pen | |
| except Exception as e: | |
| logger.exception(e) | |
| async def generate_image( | |
| canvas: tkinter.Canvas, | |
| *args, | |
| height: Union[int, None] = None, | |
| width: Union[int, None] = None, | |
| **kwargs, | |
| ) -> Union[io.BytesIO, None]: | |
| """Generates PNG Image from current turtle.Screen, returns the bytes \ | |
| buffer.""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| image: io.BytesIO = io.BytesIO() | |
| ## TODO: Receive turtle.Screen (or Canvas / tk.Frame) as argument | |
| with io.BytesIO(canvas.postscript( | |
| # ~ width = width, | |
| # ~ height = height, | |
| ).encode('utf-8')) as _buffer: | |
| with Image.open(_buffer) as eps: | |
| eps.save(image, format = 'PNG') | |
| return image | |
| except Exception as e: | |
| logger.exception(e) | |
| image.close() | |
| return None | |
| async def show_image( | |
| canvas: tkinter.Canvas, | |
| *args, | |
| height: Union[int, None] = None, | |
| width: Union[int, None] = None, | |
| **kwargs, | |
| ) -> None: | |
| """Display Image using Pillow""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| with Image.open(await generate_image( | |
| canvas, | |
| height = height, | |
| width = width, | |
| )) as image: | |
| image.show() | |
| except Exception as e: | |
| logger.exception(e) | |
| async def colorize(number: Union[float, int, str], *args, **kwargs) -> str: | |
| """Returns a color version of a string defined in a map""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| default: tuple[object] = (colorama.Fore.WHITE, colorama.Back.BLACK) | |
| color_map: dict[str, tuple[object]] = { | |
| '0': (colorama.Fore.RED, colorama.Back.BLACK), | |
| '1': (colorama.Fore.GREEN, colorama.Back.BLACK), | |
| '2': (colorama.Fore.YELLOW, colorama.Back.BLACK), | |
| '3': (colorama.Fore.BLUE, colorama.Back.BLACK), | |
| '4': (colorama.Fore.MAGENTA, colorama.Back.BLACK), | |
| '5': (colorama.Fore.CYAN, colorama.Back.BLACK), | |
| '6': (colorama.Fore.RED, colorama.Back.BLACK), | |
| '7': (colorama.Fore.GREEN, colorama.Back.BLACK), | |
| '8': (colorama.Fore.YELLOW, colorama.Back.BLACK), | |
| '9': (colorama.Fore.BLUE, colorama.Back.BLACK), | |
| '.': (colorama.Fore.MAGENTA, colorama.Back.BLACK), | |
| '-': (colorama.Fore.CYAN, colorama.Back.BLACK), | |
| } | |
| return ''.join([''.join([ | |
| colorama.Style.BRIGHT, | |
| color_map.get(n, default)[0], | |
| color_map.get(n, default)[1], | |
| n, | |
| colorama.Style.RESET_ALL, | |
| ]) for n in str(number)]) | |
| except Exception as e: | |
| logger.exception(e) | |
| async def random_rgb() -> tuple[int]: | |
| """Generates a random RGB colorcode""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| return ( | |
| random.randint(0, 255), | |
| random.randint(0, 255), | |
| random.randint(0, 255), | |
| ) | |
| except Exception as e: | |
| logger.exception(e) | |
| return (0,0,0) | |
| async def update_boundaries( | |
| pen: turtle.RawTurtle, | |
| boundaries: dict[str, float], | |
| *args, | |
| **kwargs, | |
| ) -> dict[str, float]: | |
| """Update boundaries according to last RawTurtle position""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| pos: tuple[int] = pen.pos() | |
| boundaries['llx'] = min(boundaries.get('llx'), pos[0]) | |
| boundaries['lly'] = min(boundaries.get('lly'), pos[1]) | |
| boundaries['urx'] = max(boundaries.get('urx'), pos[0]) | |
| boundaries['ury'] = max(boundaries.get('ury'), pos[1]) | |
| return boundaries | |
| except Exception as e: | |
| logger.exception(e) | |
| async def resize_screen( | |
| pen: turtle.RawTurtle, | |
| boundaries: dict[str, float], | |
| *args, | |
| **kwargs, | |
| ) -> None: | |
| """Resize TurtleScreen / Canvas according to drawn boundaries""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| # ~ pen.getscreen().getcanvas().config( | |
| # ~ width = ( | |
| # ~ abs(boundaries.get('urx')) + \ | |
| # ~ abs(boundaries.get('llx')) | |
| # ~ ), | |
| # ~ height = ( | |
| # ~ abs(boundaries.get('ury')) + \ | |
| # ~ abs(boundaries.get('lly')) | |
| # ~ ), | |
| # ~ ) | |
| pen.getscreen().setworldcoordinates(**boundaries) | |
| except Exception as e: | |
| logger.exception(e) | |
| async def turn_0( | |
| pen: turtle.RawTurtle, | |
| angle: int, | |
| iteration: int, | |
| *args, | |
| **kwargs, | |
| ) -> None: | |
| """Rule 2: Rotates Turtle if Fibonacci number is 0; | |
| Rule 3: Rotates left if the current iteration is even; | |
| Rule 4: Rotates right if current iteration is odd; | |
| """ | |
| if iteration % 2: | |
| pen.left(angle) | |
| else: | |
| pen.right(angle) | |
| async def turn_1(*args, **kwargs) -> None: | |
| """Rule 5: If Fibonacci number is 1, do not rotate.""" | |
| pass | |
| turn_map: dict[str, object] = {'0': turn_0, '1': turn_1} | |
| async def fibonacci_fractal( | |
| word: str, | |
| root: tkinter.Tk, | |
| canvas: tkinter.Canvas, | |
| screen: turtle.TurtleScreen, | |
| pen: turtle.RawTurtle, | |
| *args, | |
| step: int = 1, | |
| angle: int = 90, | |
| **kwargs, | |
| ) -> None: | |
| """Generates a Fibonacci Word Fractal""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| boundaries: dict[str, float] = { | |
| 'llx': -1.0, # left | |
| 'lly': -1.0, # down | |
| 'urx': 1.0, # right | |
| 'ury': 1.0, # up | |
| } | |
| for iteration, number in enumerate(word, 1): | |
| await asyncio.sleep(1e-15) | |
| ## Change color of Turtle to a random RGB value, it's coloridacci! | |
| color: tuple[int] = await random_rgb() | |
| pen.pencolor(color) | |
| ## Rule 1: Draw a segment forward | |
| pen.forward(step) | |
| boundaries = await update_boundaries(pen, boundaries) | |
| await resize_screen(pen, boundaries) | |
| await turn_map[number](pen, angle, iteration) | |
| logger.debug(f"""\ | |
| {await colorize(iteration)}/\ | |
| {await colorize(len(word))}\ | |
| ({await colorize(number)}): \ | |
| pos{pen.pos()}\ | |
| {boundaries}, \ | |
| color{color}""") | |
| ## FIXME: https://github.com/tartley/colorama/issues/203 | |
| # ~ logger.info(f"""\ | |
| # ~ {await colorize(iteration)}/\ | |
| # ~ {await colorize(len(word))}\ | |
| # ~ ({await colorize(number)}): \ | |
| # ~ pos{''.join([await colorize(p) for p in pen.pos()])}\ | |
| # ~ { {k: await colorize(v) for k, v in boundaries.items()} }, \ | |
| # ~ color{''.join([await colorize(c) for c in color])}""") | |
| except Exception as e: | |
| logger.exception(e) | |
| n_map: dict = {1: '1', 2: '0'} | |
| # ~ @functools.lru_cache | |
| async def fibonacci_word(n: int, *args, **kwargs) -> str: | |
| """Generates a Fibonacci Word""" | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| assert n > 0 | |
| try: | |
| return n_map[n] | |
| except KeyError: | |
| return await fibonacci_word(n - 1) + await fibonacci_word(n - 2) | |
| except Exception as e: | |
| logger.exception(e) | |
| async def main( | |
| n: int = 20, | |
| pen_step: int = 1, | |
| pen_angle: int = 90, | |
| screen_tracer: int = 300000, | |
| pen_speed: Union[int, str] = 0, | |
| pen_x: int = 0, | |
| pen_y: int = 0, | |
| **kwargs, | |
| ) -> None: | |
| """Generates a Fibonacci Fractal | |
| n: int = Fibonacci Word to use | |
| pen_step: int = Segment length | |
| pen_angle: int = Angle of rotation | |
| screen_tracer: int = Value of n passed to Screen tracer | |
| pen_speed: Union[int, str] = Speed of Turtle | |
| pen_x: int, pen_y: int = Turtle starting position | |
| """ | |
| logger: logging.getLogger = logging.getLogger(inspect.currentframe( | |
| ).f_code.co_name) | |
| try: | |
| word: str = await fibonacci_word(n) | |
| logger.info(f""" | |
| word size = {n} ({len(word)}) | |
| step = {pen_step} | |
| angle = {pen_angle} | |
| tracer = {screen_tracer} | |
| start = ({pen_x}, {pen_y}) | |
| """) | |
| root, canvas, screen, pen = await startup( | |
| pen_angle = pen_angle, | |
| pen_x = pen_x, | |
| pen_y = pen_y, | |
| pen_speed = pen_speed, | |
| screen_tracer = screen_tracer, | |
| canvas_width = 1024, | |
| canvas_height = 768, | |
| root_width = 1024, | |
| root_height = 768, | |
| ) | |
| await fibonacci_fractal( | |
| word, | |
| root = root, | |
| canvas = canvas, | |
| screen = screen, | |
| pen = pen, | |
| step = pen_step, | |
| angle = pen_angle, | |
| ) | |
| await show_image(canvas, None, None) | |
| except Exception as e: | |
| logger.exception(e) | |
| # ~ finally: | |
| # ~ turtle.exitonclick() | |
| # ~ turtle.bye() | |
| if __name__ == '__main__': | |
| __name__ = name | |
| logger: logging.getLogger = logging.getLogger(__name__) | |
| try: | |
| asyncio.run(main(*[int(a) for a in sys.argv[1:]])) | |
| except Exception as e: | |
| logger.exception(e) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment