Skip to content

Instantly share code, notes, and snippets.

@iuriguilherme
Last active November 27, 2022 23:05
Show Gist options
  • Select an option

  • Save iuriguilherme/196677d2bc95de343ba9f474e701b4ad to your computer and use it in GitHub Desktop.

Select an option

Save iuriguilherme/196677d2bc95de343ba9f474e701b4ad to your computer and use it in GitHub Desktop.
Fibonacci Word Fractal Colorized
"""
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