Skip to content

Instantly share code, notes, and snippets.

@gumblex
Created May 5, 2016 05:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gumblex/a59114068ed9826ae100eadf84465b31 to your computer and use it in GitHub Desktop.
Save gumblex/a59114068ed9826ae100eadf84465b31 to your computer and use it in GitHub Desktop.
Mandelbrot ASCII art from PyPy (independent version)
import os
import sys
import colorsys
"""
Black 0;30 Dark Gray 1;30
Blue 0;34 Light Blue 1;34
Green 0;32 Light Green 1;32
Cyan 0;36 Light Cyan 1;36
Red 0;31 Light Red 1;31
Purple 0;35 Light Purple 1;35
Brown 0;33 Yellow 1;33
Light Gray 0;37 White 1;37
"""
# used for debugging/finding new coordinates
# How to:
# 1. Set DEBUG to True
# 2. Add a new coordinate to coordinates with a high distance and high max colour (e.g. 300)
# 3. Run, pick an interesting coordinate from the shown list and replace the newly added
# coordinate by it.
# 4. Rerun to see the max colour, insert this max colour where you put the high max colour.
# 5. Set DEBUG to False
DEBUG = False
try:
from io import StringIO
except ImportError:
from StringIO import StringIO
try:
callable = callable
except NameError:
def callable(obj):
return hasattr(obj, "__call__")
if sys.version_info >= (3, 0):
text = str
bytes = bytes
TextIO = StringIO
exec ("print_ = print")
else:
text = unicode
bytes = str
next = lambda it: it.next()
class TextIO(StringIO):
def write(self, data):
if not isinstance(data, unicode):
data = unicode(data, getattr(self, '_encoding', 'UTF-8'), 'replace')
StringIO.write(self, data)
def print_(*args, **kwargs):
""" minimal backport of py3k print statement. """
sep = ' '
if 'sep' in kwargs:
sep = kwargs.pop('sep')
end = '\n'
if 'end' in kwargs:
end = kwargs.pop('end')
file = 'file' in kwargs and kwargs.pop('file') or sys.stdout
if kwargs:
args = ", ".join([str(x) for x in kwargs])
raise TypeError("invalid keyword arguments: %s" % args)
at_start = True
for x in args:
if not at_start:
file.write(sep)
file.write(str(x))
at_start = False
file.write(end)
win32_and_ctypes = False
if sys.platform == "win32":
try:
import ctypes
win32_and_ctypes = True
except ImportError:
pass
def hsv2ansi(h, s, v):
# h: 0..1, s/v: 0..1
if s < 0.1:
return int(v * 23) + 232
r, g, b = [int(x * 5) for x in colorsys.hsv_to_rgb(h, s, v)]
return 16 + (r * 36) + (g * 6) + b
def ramp_idx(i, num):
assert num > 0
i0 = float(i) / num
h = 0.57 + i0
s = 1 - pow(i0,3)
v = 1
return hsv2ansi(h, s, v)
def ansi_ramp(num):
return [ramp_idx(i, num) for i in range(num)]
if os.environ.get('TERM', 'dumb').find('256') > 0:
palette = ["38;5;%d" % x for x in ansi_ramp(80)]
else:
palette = [39, 34, 35, 36, 31, 33, 32, 37]
def _getdimensions():
import termios,fcntl,struct
call = fcntl.ioctl(1,termios.TIOCGWINSZ,"\000"*8)
height,width = struct.unpack( "hhhh", call ) [:2]
return height, width
def get_terminal_width():
try:
height, width = _getdimensions()
except (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit):
raise
except:
# FALLBACK
width = int(os.environ.get('COLUMNS', 80))
else:
# XXX the windows getdimensions may be bogus, let's sanify a bit
if width < 40:
width = 80
return width
# XXX unify with _escaped func below
def ansi_print(text, esc, file=None, newline=True, flush=False):
if file is None:
file = sys.stderr
text = text.rstrip()
if esc and not isinstance(esc, tuple):
esc = (esc,)
if esc and sys.platform != "win32" and file.isatty():
text = (''.join(['\x1b[%sm' % cod for cod in esc]) +
text +
'\x1b[0m') # ANSI color code "reset"
if newline:
text += '\n'
if esc and win32_and_ctypes and file.isatty():
if 1 in esc:
bold = True
esc = tuple([x for x in esc if x != 1])
else:
bold = False
esctable = {() : FOREGROUND_WHITE, # normal
(31,): FOREGROUND_RED, # red
(32,): FOREGROUND_GREEN, # green
(33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow
(34,): FOREGROUND_BLUE, # blue
(35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple
(36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan
(37,): FOREGROUND_WHITE, # white
(39,): FOREGROUND_WHITE, # reset
}
attr = esctable.get(esc, FOREGROUND_WHITE)
if bold:
attr |= FOREGROUND_INTENSITY
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
if file is sys.stderr:
handle = GetStdHandle(STD_ERROR_HANDLE)
else:
handle = GetStdHandle(STD_OUTPUT_HANDLE)
oldcolors = GetConsoleInfo(handle).wAttributes
attr |= (oldcolors & 0x0f0)
SetConsoleTextAttribute(handle, attr)
while len(text) > 32768:
file.write(text[:32768])
text = text[32768:]
if text:
file.write(text)
SetConsoleTextAttribute(handle, oldcolors)
else:
file.write(text)
if flush:
file.flush()
def should_do_markup(file):
if os.environ.get('PY_COLORS') == '1':
return True
if os.environ.get('PY_COLORS') == '0':
return False
return hasattr(file, 'isatty') and file.isatty() \
and os.environ.get('TERM') != 'dumb' \
and not (sys.platform.startswith('java') and os._name == 'nt')
if win32_and_ctypes:
import ctypes
from ctypes import wintypes
# ctypes access to the Windows console
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
FOREGROUND_BLACK = 0x0000 # black text
FOREGROUND_BLUE = 0x0001 # text color contains blue.
FOREGROUND_GREEN = 0x0002 # text color contains green.
FOREGROUND_RED = 0x0004 # text color contains red.
FOREGROUND_WHITE = 0x0007
FOREGROUND_INTENSITY = 0x0008 # text color is intensified.
BACKGROUND_BLACK = 0x0000 # background color black
BACKGROUND_BLUE = 0x0010 # background color contains blue.
BACKGROUND_GREEN = 0x0020 # background color contains green.
BACKGROUND_RED = 0x0040 # background color contains red.
BACKGROUND_WHITE = 0x0070
BACKGROUND_INTENSITY = 0x0080 # background color is intensified.
SHORT = ctypes.c_short
class COORD(ctypes.Structure):
_fields_ = [('X', SHORT),
('Y', SHORT)]
class SMALL_RECT(ctypes.Structure):
_fields_ = [('Left', SHORT),
('Top', SHORT),
('Right', SHORT),
('Bottom', SHORT)]
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
_fields_ = [('dwSize', COORD),
('dwCursorPosition', COORD),
('wAttributes', wintypes.WORD),
('srWindow', SMALL_RECT),
('dwMaximumWindowSize', COORD)]
_GetStdHandle = ctypes.windll.kernel32.GetStdHandle
_GetStdHandle.argtypes = [wintypes.DWORD]
_GetStdHandle.restype = wintypes.HANDLE
def GetStdHandle(kind):
return _GetStdHandle(kind)
SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute
SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD]
SetConsoleTextAttribute.restype = wintypes.BOOL
_GetConsoleScreenBufferInfo = \
ctypes.windll.kernel32.GetConsoleScreenBufferInfo
_GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE,
ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)]
_GetConsoleScreenBufferInfo.restype = wintypes.BOOL
def GetConsoleInfo(handle):
info = CONSOLE_SCREEN_BUFFER_INFO()
_GetConsoleScreenBufferInfo(handle, ctypes.byref(info))
return info
def _getdimensions():
handle = GetStdHandle(STD_OUTPUT_HANDLE)
info = GetConsoleInfo(handle)
# Substract one from the width, otherwise the cursor wraps
# and the ending \n causes an empty line to display.
return info.dwSize.Y, info.dwSize.X - 1
class Mandelbrot:
def __init__ (self, width=100, height=28, x_pos=-0.5, y_pos=0, distance=6.75):
self.xpos = x_pos
self.ypos = y_pos
aspect_ratio = 1/3.
factor = float(distance) / width # lowering the distance will zoom in
self.xscale = factor * aspect_ratio
self.yscale = factor
self.iterations = 170
self.x = width
self.y = height
self.z0 = complex(0, 0)
def init(self):
self.reset_lines = False
xmin = self.xpos - self.xscale * self.x / 2
ymin = self.ypos - self.yscale * self.y / 2
self.x_range = [xmin + self.xscale * ix for ix in range(self.x)]
self.y_range = [ymin + self.yscale * iy for iy in range(self.y)]
def reset(self, cnt):
self.reset_lines = cnt
def generate(self):
self.reset_lines = False
iy = 0
while iy < self.y:
ix = 0
while ix < self.x:
c = complex(self.x_range[ix], self.y_range[iy])
z = self.z0
colour = 0
mind = 2
for i in range(self.iterations):
z = z * z + c
d = abs(z)
if d >= 2:
colour = min(int(mind / 0.007), 254) + 1
break
else:
mind = min(d, mind)
yield ix, iy, colour
if self.reset_lines is not False: # jump to the beginning of the line
iy += self.reset_lines
do_break = bool(self.reset_lines)
self.reset_lines = False
if do_break:
break
ix = 0
else:
ix += 1
iy += 1
class Driver(object):
zoom_locations = [
# x, y, "distance", max color range
(0.37865401, 0.669227668, 0.04, 111),
(-1.2693, -0.4145, 0.2, 105),
(-1.2693, -0.4145, 0.05, 97),
(-1.2642, -0.4185, 0.01, 95),
(-1.15, -0.28, 0.9, 94),
(-1.15, -0.28, 0.3, 58),
(-1.15, -0.28, 0.05, 26),
]
def __init__(self, **kwargs):
self.kwargs = kwargs
self.zoom_location = -1
self.max_colour = 256
self.colour_range = None
self.invert = True
self.interesting_coordinates = []
self.init()
def init(self):
self.width = get_terminal_width() or 80 # in some envs, the py lib doesnt default the width correctly
self.mandelbrot = Mandelbrot(width=(self.width or 1), **self.kwargs)
self.mandelbrot.init()
self.gen = self.mandelbrot.generate()
def reset(self, cnt=0):
""" Resets to the beginning of the line and drops cnt lines internally. """
self.mandelbrot.reset(cnt)
def restart(self):
""" Restarts the current generator. """
print_(file=sys.stderr)
self.init()
def dot(self):
""" Emits a colourful character. """
x = c = 0
try:
x, y, c = next(self.gen)
if x == 0:
width = get_terminal_width()
if width != self.width:
self.init()
except StopIteration:
if DEBUG and self.interesting_coordinates:
print_("Interesting coordinates:", self.interesting_coordinates, file=sys.stderr)
self.interesting_coordinates = []
kwargs = self.kwargs
self.zoom_location += 1
self.zoom_location %= len(self.zoom_locations)
loc = self.zoom_locations[self.zoom_location]
kwargs.update({"x_pos": loc[0], "y_pos": loc[1], "distance": loc[2]})
self.max_colour = loc[3]
if DEBUG:
# Only used for debugging new locations:
print_("Colour range", self.colour_range)
self.colour_range = None
self.restart()
return
if self.print_pixel(c, self.invert):
self.interesting_coordinates.append(dict(x=(x, self.mandelbrot.x_range[x]),
y=(y, self.mandelbrot.y_range[y])))
if x == self.width - 1:
print_(file=sys.stderr)
def print_pixel(self, colour, invert=1):
chars = [".", ".", "+", "*", "%", "#"]
idx = lambda chars: (colour+1) * (len(chars) - 1) // self.max_colour
if invert:
idx = lambda chars, idx=idx:len(chars) - 1 - idx(chars)
char = chars[idx(chars)]
ansi_colour = palette[idx(palette)]
ansi_print(char, ansi_colour, newline=False, flush=True)
if DEBUG:
if self.colour_range is None:
self.colour_range = [colour, colour]
else:
old_colour_range = self.colour_range
self.colour_range = [min(self.colour_range[0], colour), max(self.colour_range[1], colour)]
if old_colour_range[0] - colour > 3 or colour - old_colour_range[1] > 3:
return True
if __name__ == '__main__':
import random
from time import sleep
d = Driver()
while True:
sleep(random.random() / 800)
d.dot()
if 0 and random.random() < 0.01:
string = "WARNING! " * 3
d.jump(len(string))
print_(string, end=' ')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment