417 lines
11 KiB
Python
417 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# drawille is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# drawille is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with drawille. If not, see < http://www.gnu.org/licenses/ >.
|
|
#
|
|
# (C) 2014- by Adam Tauber, <asciimoo@gmail.com>
|
|
|
|
import math
|
|
import os
|
|
from sys import version_info
|
|
from collections import defaultdict
|
|
from time import sleep
|
|
import curses
|
|
|
|
IS_PY3 = version_info[0] == 3
|
|
|
|
if IS_PY3:
|
|
unichr = chr
|
|
|
|
"""
|
|
|
|
http://www.alanwood.net/unicode/braille_patterns.html
|
|
|
|
dots:
|
|
,___,
|
|
|1 4|
|
|
|2 5|
|
|
|3 6|
|
|
|7 8|
|
|
`````
|
|
"""
|
|
|
|
pixel_map = ((0x01, 0x08),
|
|
(0x02, 0x10),
|
|
(0x04, 0x20),
|
|
(0x40, 0x80))
|
|
|
|
# braille unicode characters starts at 0x2800
|
|
braille_char_offset = 0x2800
|
|
|
|
|
|
# http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python
|
|
def getTerminalSize():
|
|
"""Returns terminal width, height
|
|
"""
|
|
env = os.environ
|
|
|
|
def ioctl_GWINSZ(fd):
|
|
try:
|
|
import fcntl, termios, struct
|
|
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
|
|
except:
|
|
return
|
|
return cr
|
|
|
|
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
|
|
|
if not cr:
|
|
try:
|
|
fd = os.open(os.ctermid(), os.O_RDONLY)
|
|
cr = ioctl_GWINSZ(fd)
|
|
os.close(fd)
|
|
except:
|
|
pass
|
|
|
|
if not cr:
|
|
cr = (env.get('LINES', 25), env.get('COLUMNS', 80))
|
|
|
|
return int(cr[1]), int(cr[0])
|
|
|
|
|
|
def normalize(coord):
|
|
coord_type = type(coord)
|
|
|
|
if coord_type == int:
|
|
return coord
|
|
elif coord_type == float:
|
|
return int(round(coord))
|
|
else:
|
|
raise TypeError("Unsupported coordinate type <{0}>".format(type(coord)))
|
|
|
|
|
|
def intdefaultdict():
|
|
return defaultdict(int)
|
|
|
|
|
|
def get_pos(x, y):
|
|
"""Convert x, y to cols, rows"""
|
|
return normalize(x) // 2, normalize(y) // 4
|
|
|
|
|
|
class Canvas(object):
|
|
"""This class implements the pixel surface."""
|
|
|
|
def __init__(self, line_ending=os.linesep):
|
|
super(Canvas, self).__init__()
|
|
self.clear()
|
|
self.line_ending = line_ending
|
|
|
|
|
|
def clear(self):
|
|
"""Remove all pixels from the :class:`Canvas` object."""
|
|
self.chars = defaultdict(intdefaultdict)
|
|
|
|
|
|
def set(self, x, y):
|
|
"""Set a pixel of the :class:`Canvas` object.
|
|
|
|
:param x: x coordinate of the pixel
|
|
:param y: y coordinate of the pixel
|
|
"""
|
|
x = normalize(x)
|
|
y = normalize(y)
|
|
col, row = get_pos(x, y)
|
|
|
|
if type(self.chars[row][col]) != int:
|
|
return
|
|
|
|
self.chars[row][col] |= pixel_map[y % 4][x % 2]
|
|
|
|
|
|
def unset(self, x, y):
|
|
"""Unset a pixel of the :class:`Canvas` object.
|
|
|
|
:param x: x coordinate of the pixel
|
|
:param y: y coordinate of the pixel
|
|
"""
|
|
x = normalize(x)
|
|
y = normalize(y)
|
|
col, row = get_pos(x, y)
|
|
|
|
if type(self.chars[row][col]) == int:
|
|
self.chars[row][col] &= ~pixel_map[y % 4][x % 2]
|
|
|
|
if type(self.chars[row][col]) != int or self.chars[row][col] == 0:
|
|
del(self.chars[row][col])
|
|
|
|
if not self.chars.get(row):
|
|
del(self.chars[row])
|
|
|
|
|
|
def toggle(self, x, y):
|
|
"""Toggle a pixel of the :class:`Canvas` object.
|
|
|
|
:param x: x coordinate of the pixel
|
|
:param y: y coordinate of the pixel
|
|
"""
|
|
x = normalize(x)
|
|
y = normalize(y)
|
|
col, row = get_pos(x, y)
|
|
|
|
if type(self.chars[row][col]) != int or self.chars[row][col] & pixel_map[y % 4][x % 2]:
|
|
self.unset(x, y)
|
|
else:
|
|
self.set(x, y)
|
|
|
|
|
|
def set_text(self, x, y, text):
|
|
"""Set text to the given coords.
|
|
|
|
:param x: x coordinate of the text start position
|
|
:param y: y coordinate of the text start position
|
|
"""
|
|
col, row = get_pos(x, y)
|
|
|
|
for i,c in enumerate(text):
|
|
self.chars[row][col+i] = c
|
|
|
|
|
|
def get(self, x, y):
|
|
"""Get the state of a pixel. Returns bool.
|
|
|
|
:param x: x coordinate of the pixel
|
|
:param y: y coordinate of the pixel
|
|
"""
|
|
x = normalize(x)
|
|
y = normalize(y)
|
|
dot_index = pixel_map[y % 4][x % 2]
|
|
col, row = get_pos(x, y)
|
|
char = self.chars.get(row, {}).get(col)
|
|
|
|
if not char:
|
|
return False
|
|
|
|
if type(char) != int:
|
|
return True
|
|
|
|
return bool(char & dot_index)
|
|
|
|
|
|
def rows(self, min_x=None, min_y=None, max_x=None, max_y=None):
|
|
"""Returns a list of the current :class:`Canvas` object lines.
|
|
|
|
:param min_x: (optional) minimum x coordinate of the canvas
|
|
:param min_y: (optional) minimum y coordinate of the canvas
|
|
:param max_x: (optional) maximum x coordinate of the canvas
|
|
:param max_y: (optional) maximum y coordinate of the canvas
|
|
"""
|
|
|
|
if not self.chars.keys():
|
|
return []
|
|
|
|
minrow = min_y // 4 if min_y != None else min(self.chars.keys())
|
|
maxrow = (max_y - 1) // 4 if max_y != None else max(self.chars.keys())
|
|
mincol = min_x // 2 if min_x != None else min(min(x.keys()) for x in self.chars.values())
|
|
maxcol = (max_x - 1) // 2 if max_x != None else max(max(x.keys()) for x in self.chars.values())
|
|
ret = []
|
|
|
|
for rownum in range(minrow, maxrow+1):
|
|
if not rownum in self.chars:
|
|
ret.append('')
|
|
continue
|
|
|
|
maxcol = (max_x - 1) // 2 if max_x != None else max(self.chars[rownum].keys())
|
|
row = []
|
|
|
|
for x in range(mincol, maxcol+1):
|
|
char = self.chars[rownum].get(x)
|
|
|
|
if not char:
|
|
row.append(' ')
|
|
elif type(char) != int:
|
|
row.append(char)
|
|
else:
|
|
row.append(unichr(braille_char_offset+char))
|
|
|
|
ret.append(''.join(row))
|
|
|
|
return ret
|
|
|
|
|
|
def frame(self, min_x=None, min_y=None, max_x=None, max_y=None):
|
|
"""String representation of the current :class:`Canvas` object pixels.
|
|
|
|
:param min_x: (optional) minimum x coordinate of the canvas
|
|
:param min_y: (optional) minimum y coordinate of the canvas
|
|
:param max_x: (optional) maximum x coordinate of the canvas
|
|
:param max_y: (optional) maximum y coordinate of the canvas
|
|
"""
|
|
ret = self.line_ending.join(self.rows(min_x, min_y, max_x, max_y))
|
|
|
|
if IS_PY3:
|
|
return ret
|
|
|
|
return ret.encode('utf-8')
|
|
|
|
|
|
def line(x1, y1, x2, y2):
|
|
"""Returns the coords of the line between (x1, y1), (x2, y2)
|
|
|
|
:param x1: x coordinate of the startpoint
|
|
:param y1: y coordinate of the startpoint
|
|
:param x2: x coordinate of the endpoint
|
|
:param y2: y coordinate of the endpoint
|
|
"""
|
|
|
|
x1 = normalize(x1)
|
|
y1 = normalize(y1)
|
|
x2 = normalize(x2)
|
|
y2 = normalize(y2)
|
|
|
|
xdiff = max(x1, x2) - min(x1, x2)
|
|
ydiff = max(y1, y2) - min(y1, y2)
|
|
xdir = 1 if x1 <= x2 else -1
|
|
ydir = 1 if y1 <= y2 else -1
|
|
|
|
r = max(xdiff, ydiff)
|
|
|
|
for i in range(r+1):
|
|
x = x1
|
|
y = y1
|
|
|
|
if ydiff:
|
|
y += (float(i) * ydiff) / r * ydir
|
|
if xdiff:
|
|
x += (float(i) * xdiff) / r * xdir
|
|
|
|
yield (x, y)
|
|
|
|
|
|
def polygon(center_x=0, center_y=0, sides=4, radius=4):
|
|
degree = float(360) / sides
|
|
|
|
for n in range(sides):
|
|
a = n * degree
|
|
b = (n + 1) * degree
|
|
x1 = (center_x + math.cos(math.radians(a))) * (radius + 1) / 2
|
|
y1 = (center_y + math.sin(math.radians(a))) * (radius + 1) / 2
|
|
x2 = (center_x + math.cos(math.radians(b))) * (radius + 1) / 2
|
|
y2 = (center_y + math.sin(math.radians(b))) * (radius + 1) / 2
|
|
|
|
for x, y in line(x1, y1, x2, y2):
|
|
yield x, y
|
|
|
|
|
|
class Turtle(Canvas):
|
|
"""Turtle graphics interface
|
|
http://en.wikipedia.org/wiki/Turtle_graphics
|
|
"""
|
|
|
|
def __init__(self, pos_x=0, pos_y=0):
|
|
self.pos_x = pos_x
|
|
self.pos_y = pos_y
|
|
self.rotation = 0
|
|
self.brush_on = True
|
|
super(Turtle, self).__init__()
|
|
|
|
|
|
def up(self):
|
|
"""Pull the brush up."""
|
|
self.brush_on = False
|
|
|
|
|
|
def down(self):
|
|
"""Push the brush down."""
|
|
self.brush_on = True
|
|
|
|
|
|
def forward(self, step):
|
|
"""Move the turtle forward.
|
|
|
|
:param step: Integer. Distance to move forward.
|
|
"""
|
|
x = self.pos_x + math.cos(math.radians(self.rotation)) * step
|
|
y = self.pos_y + math.sin(math.radians(self.rotation)) * step
|
|
prev_brush_state = self.brush_on
|
|
self.brush_on = True
|
|
self.move(x, y)
|
|
self.brush_on = prev_brush_state
|
|
|
|
|
|
def move(self, x, y):
|
|
"""Move the turtle to a coordinate.
|
|
|
|
:param x: x coordinate
|
|
:param y: y coordinate
|
|
"""
|
|
if self.brush_on:
|
|
for lx, ly in line(self.pos_x, self.pos_y, x, y):
|
|
self.set(lx, ly)
|
|
|
|
self.pos_x = x
|
|
self.pos_y = y
|
|
|
|
|
|
def right(self, angle):
|
|
"""Rotate the turtle (positive direction).
|
|
|
|
:param angle: Integer. Rotation angle in degrees.
|
|
"""
|
|
self.rotation += angle
|
|
|
|
|
|
def left(self, angle):
|
|
"""Rotate the turtle (negative direction).
|
|
|
|
:param angle: Integer. Rotation angle in degrees.
|
|
"""
|
|
self.rotation -= angle
|
|
|
|
|
|
def back(self, step):
|
|
"""Move the turtle backwards.
|
|
|
|
:param step: Integer. Distance to move backwards.
|
|
"""
|
|
self.forward(-step)
|
|
|
|
|
|
# aliases
|
|
pu = up
|
|
pd = down
|
|
fd = forward
|
|
mv = move
|
|
rt = right
|
|
lt = left
|
|
bk = back
|
|
|
|
|
|
def animate(canvas, fn, delay=1./24, *args, **kwargs):
|
|
"""Animation automatition function
|
|
|
|
:param canvas: :class:`Canvas` object
|
|
:param fn: Callable. Frame coord generator
|
|
:param delay: Float. Delay between frames.
|
|
:param *args, **kwargs: optional fn parameters
|
|
"""
|
|
|
|
# python2 unicode curses fix
|
|
if not IS_PY3:
|
|
import locale
|
|
locale.setlocale(locale.LC_ALL, "")
|
|
|
|
def animation(stdscr):
|
|
|
|
for frame in fn(*args, **kwargs):
|
|
for x,y in frame:
|
|
canvas.set(x,y)
|
|
|
|
f = canvas.frame()
|
|
stdscr.addstr(0, 0, '{0}\n'.format(f))
|
|
stdscr.refresh()
|
|
if delay:
|
|
sleep(delay)
|
|
canvas.clear()
|
|
|
|
curses.wrapper(animation)
|