Top

char_draw.canvas module

module that implements a graphics package using block graphics on curses window

# Copyright 2017 James P Goodwin chardraw unicode curses based graphics package
""" module that implements a graphics package using block graphics on curses window """
import locale
locale.setlocale(locale.LC_ALL,"")
import curses
import curses.ascii
import sys
import os
import math

def angle_point(x0,y0,a,radius):
    return (int(x0+(1.5*math.cos(math.radians(a))*radius)), int(y0+math.sin(math.radians(a))*radius))

class Canvas:
    """ primitive drawing surface attached to a curses window """

    to_mask = { 0:1, 1:2, 2:4, 3:8 }
    mask_to_char = {
        0 :'\u2008',
        1 :'\u2598',
        2 :'\u259d',
        3 :'\u2580',
        4 :'\u2596',
        5 :'\u258c',
        6 :'\u259e',
        7 :'\u259b',
        8 :'\u2597',
        9 :'\u259a',
        10:'\u2590',
        11:'\u259c',
        12:'\u2584',
        13:'\u2599',
        14:'\u259f',
        15:'\u2588'
        }

    char_to_mask = {
        '\u2008':0 ,
        '\u2598':1 ,
        '\u259d':2 ,
        '\u2580':3 ,
        '\u2596':4 ,
        '\u258c':5 ,
        '\u259e':6 ,
        '\u259b':7 ,
        '\u2597':8 ,
        '\u259a':9 ,
        '\u2590':10,
        '\u259c':11,
        '\u2584':12,
        '\u2599':13,
        '\u259f':14,
        '\u2588':15
        }

    def __init__(self, win = None ):
        """ constructor, can be initialized with a window to draw on, otherwise window must be set later by set_window """
        self.set_win(win)

    def to_rowcol(self, x, y ):
        """ return character row col for input x,y coordinates """
        return (int(y/2),int(x/2))

    def from_rowcol(self, row, col ):
        """ return the pixel location of a character position, returns upper left pixel in matrix"""
        return (int(row)*2,int(col)*2)

    def round_text_position(self, x, y):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(x,y)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if y1 < y:
            y = y + h/2
        if x1 < x:
            x = x + w/2
        return x, y

    def round_text_x_position(self, x):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(x,0)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if x1 < x:
            x = x + w/2
        return x

    def round_text_y_position(self, y):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(0,y)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if y1 < y:
            y = y + h/2
        return y

    def set_win(self, win ):
        """ point this canvas at a window and initialize things, will blank out the window """
        self.win = win
        self.init_win()

    def init_win(self):
        """ initializes the window and sets up all of the defaults """
        curses.init_pair(5,curses.COLOR_BLACK,curses.COLOR_BLACK)
        curses.init_pair(1,curses.COLOR_GREEN,curses.COLOR_BLACK)
        curses.init_pair(2,curses.COLOR_RED,curses.COLOR_BLACK)
        curses.init_pair(3,curses.COLOR_CYAN,curses.COLOR_BLACK)
        curses.init_pair(4,curses.COLOR_WHITE,curses.COLOR_BLACK)

        self.green = curses.color_pair(1)
        self.red = curses.color_pair(2)
        self.cyan = curses.color_pair(3)
        self.white = curses.color_pair(4)
        self.black = curses.color_pair(5)

        if curses.can_change_color():
            self.color_min = 8
            self.color_max = 256

            red = 0
            green = 100
            blue = 20
            for c in range(self.color_min,self.color_max):
                curses.init_color(c,red,green,blue)
                red += 23
                green += 33
                blue += 53
                red = red % 1000
                green = green % 1000
                blue = blue % 1000

            for cidx in range(self.color_min,self.color_max):
                curses.init_pair(cidx,cidx,curses.COLOR_BLACK)
        else:
            self.color_min = 0
            self.color_max = 8

        if self.win:
            self.max_y,self.max_x = self.win.getmaxyx()
            self.char_map = [[None] * self.max_y for i in range(self.max_x)]
            self.max_y = self.max_y * 2
            self.max_x = self.max_x * 2
        else:
            self.max_y,self.max_x = (0,0)
            self.char_map = None

    def clear( self ):
        """ clear the entire canvas """
        self.char_map = [[None] * self.max_y for i in range(self.max_x)]

    def refresh( self ):
        """ refresh the display after drawing """
        self.win.refresh()

    def get_maxxy( self ):
        """ return the maximum number of x and y pixels that are available in this canvas """
        return (self.max_x,self.max_y)

    def put_pixel( self, x,y, color, set = True ):
        """ turn on a pixel with the color indicated """
        if x < 0 or x >= self.max_x or y < 0 or y >= self.max_y:
            return
        row,col = self.to_rowcol(x,y)
        if row >= self.max_y//2 or col >= self.max_x//2:
            return

        mask = self.to_mask[(int(x)%2)+((int(y)%2)*2)]

        if not self.char_map[col][row]:
            current_mask = 0
        else:
            current_mask = self.char_to_mask[self.char_map[col][row]]

        if set:
            self.char_map[col][row] = self.mask_to_char[ mask | current_mask ]
        else:
            self.char_map[col][row] = self.mask_to_char[ mask ^ current_mask ]
        try:
            self.win.addstr(row,col,self.char_map[col][row].encode('utf_8'),color)
        except:
            pass

    def line(self, x0, y0, x1, y1, color, put_pixel=None ):
        """ draw a line between x0,y0 and x1,y1 in color """
        def set_pixel(x,y,color):
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        dx = abs(x1 - x0)
        dy = abs(y1 - y0)
        x, y = int(x0), int(y0)
        sx = -1 if x0 > x1 else 1
        sy = -1 if y0 > y1 else 1
        if dx > dy:
            err = dx / 2.0
            while x != int(x1):
                set_pixel(x, y,color)
                err -= dy
                if err < 0:
                    y += sy
                    err += dx
                x += sx
        else:
            err = dy / 2.0
            while y != int(y1):
                set_pixel(x, y,color)
                err -= dx
                if err < 0:
                    x += sx
                    err += dy
                y += sy
        set_pixel(x, y, color)

    def intersect( self, seg1, seg2, clip_to_seg = False ):
        """ find the intersection of two segments as tuples (x0,y0,x1,y1) returns tuple (x,y) if no intersection returns None """
        def lineform( seg ):
            """ return A, B, C for the standard line formula Ax + By = C """
            A = float(seg[1]-seg[3])
            B = float(seg[2]-seg[0])
            C = A*seg[0]+B*seg[1]
            return (A,B,C)

        l1 = lineform(seg1)
        l2 = lineform(seg2)

        det = l1[0]*l2[1] - l2[0]*l1[1]
        if det != 0:
            x = (l2[1]*l1[2] - l1[1]*l2[2])/det
            y = (l1[0]*l2[2] - l2[0]*l1[2])/det

            if clip_to_seg:
                if x >= min(seg1[0],seg1[2]) and x <= max(seg1[0],seg1[2]) and y >= min(seg1[1],seg1[3]) and y <= max(seg1[1],seg1[3]):
                    return (int(x),int(y))
            else:
                return (int(x),int(y))
        return None

    def cross_product_length( self, pA,pB,pC ):
        """ compute the cross product of AB x BC """
        BAx = float(pA[0] - pB[0])
        BAy = float(pA[1] - pB[1])
        BCx = float(pC[0] - pB[0])
        BCy = float(pC[1] - pB[1])

        return (BAx * BCy - BAy * BCx)

    def is_convex( self, points ):
        """ take a list of (x,y) tuples representing the vertecies of a polygon in order and return True if it represents a convex polygon, False otherwise """
        got_negative = False
        got_positive = False
        num_points = len(points)
        if num_points <= 3:
            return True


        min_x,min_y,max_x,max_y = self.get_bounds(points)
        if max_x-min_x <= 1.0 or max_y-min_y <= 1.0:
            return True

        for A in range(num_points):
            B = (A+1)%num_points
            C = (B+1)%num_points
            cross_product = self.cross_product_length(points[A],points[B],points[C])
            if cross_product < 0:
                got_negative = True
            elif cross_product > 0:
                got_positive = True

        return not (got_negative and got_positive)

    def get_bounds(self,points):
        """ return tuple (min_x,min_y,max_x,max_y) for list of points """
        min_x = -1
        min_y = -1
        max_x = -1
        max_y = -1
        for x,y in points:
            if min_x < 0 or x < min_x:
                min_x = x
            if min_y < 0 or y < min_y:
                min_y = y
            if max_x < 0 or x > max_x:
                max_x = x
            if max_y < 0 or y > max_y:
                max_y = y
        return (min_x,min_y,max_x,max_y)

    def clip_polygon(self, points, minX, minY, maxX, maxY, dir=-1 ):
        """ clip a polygon against the bounds exressed by minX,minY to maxX,maxY and return either None for nothing inside or the points for the polygon dir is -1 all,0=top,1=right,2=bottom,3=left """
        def inside( p, minX, minY, maxX, maxY, dir ):
            x,y = p
            if dir == 0:
                return(y >= minY)
            elif dir == 1:
                return(x < maxX)
            elif dir == 2:
                return(y < maxY)
            elif dir == 3:
                return(x >= minX)

        def intersect(sp, ep, minX, minY, maxX, maxY, dir ):
            x0,y0 = sp
            x1,y1 = ep
            s1 = (x0,y0,x1,y1)
            if dir == 0:
                s2 = (minX,minY,maxX,minY)
            elif dir == 1:
                s2 = (maxX,minY,maxX,maxY)
            elif dir == 2:
                s2 = (minX,maxY,maxX,maxY)
            elif dir == 3:
                s2 = (minX,minY,minX,maxY)
            return self.intersect(s1,s2,False)

        if dir == -1:
            for d in [0,1,2,3]:
                points = self.clip_polygon(points,minX,minY,maxX,maxY,d)
                if not points:
                    return None
            return points
        else:
            sp = points[-1]
            out_points = []
            for ep in points:
                if inside(ep,minX,minY,maxX,maxY,dir):
                    if inside(sp,minX,minY,maxX,maxY,dir):
                        out_points.append(ep)
                    else:
                        ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                        out_points.append(ip)
                        out_points.append(ep)
                else:
                    if inside(sp,minX,minY,maxX,maxY,dir):
                        ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                        out_points.append(ip)
                sp = ep
            return out_points if out_points else None


    def rasterize( self, points, color, put_pixel=None):
        """ sort points representing the boundary of a filled shape and rasterize by filling lines with color """
        ps = sorted(points,key=lambda x: (x[1],x[0]))
        n_points = len(ps)
        if n_points == 0:
            return
        elif n_points == 1:
            x,y = ps[0]
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        else:
            idx = 1
            x0,y0 = ps[0]
            x1,y1 = x0,y0
            while idx < len(ps):
                xn,yn = ps[idx]
                if yn == y0:
                    x1,y1 = xn,yn
                else:
                    if x0 == x1:
                        if put_pixel:
                            put_pixel(x0,y0,color)
                        else:
                            self.put_pixel(x0,y0,color)
                    else:
                        self.line(x0,y0,x1,y1,color,put_pixel)
                    x0,y0 = xn,yn
                    x1,y1 = x0,y0
                idx += 1
            if x0 == x1:
                if put_pixel:
                    put_pixel(x0,y0,color)
                else:
                    self.put_pixel(x0,y0,color)
            else:
                self.line(x0,y0,x1,y1,color,put_pixel)

    def circle(self, x0, y0, radius, color, fill = False, put_pixel=None ):
        """ draw a circle centered at x0,y0 of radius radius in color """
        points = []

        def circle_point( points, xc,yc,x,y ):
            points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])

        x0 = int(x0)
        y0 = int(y0)
        radius = int(radius)
        x = 0
        y = radius
        d = 3 - 2 * radius
        circle_point(points,x0,y0,x,y)
        while y >= x:
            x += 1
            if d > 0:
                y -= 1
                d = d + 4 * (x - y) + 10
            else:
                d = d + 4 * x + 6
            circle_point(points,x0,y0,x,y)

        for idx in range(len(points)):
            x,y = points[idx]
            x = int(((x-x0)*1.5)+x0)
            points[idx] = (x,y)

        if not fill:
            for x,y in points:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
        else:
            self.rasterize(points,color,put_pixel)

    def arc(self,x0,y0,radius,a0,a1,color,fill=False,put_pixel=None,just_points=False):
        """ draw an arc between a0 degrees to a1 degrees centered at x0,y0 with radius and color """
        points = []

        def circle_point( points, xc,yc,x,y ):
            points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])

        x0 = int(x0)
        y0 = int(y0)
        radius = int(radius)
        x = 0
        y = radius
        d = 3 - 2 * radius
        circle_point(points,x0,y0,x,y)
        while y >= x:
            x += 1
            if d > 0:
                y -= 1
                d = d + 4 * (x - y) + 10
            else:
                d = d + 4 * x + 6
            circle_point(points,x0,y0,x,y)

        xs,ys = angle_point(x0,y0,a0,radius+0.5)
        xe,ye = angle_point(x0,y0,a1,radius+0.5)
        xm,ym = angle_point(x0,y0,(a0+a1)/2,radius+0.5)
        x_min = min(x0,xs,xe,xm)
        y_min = min(y0,ys,ye,ym)
        x_max = max(x0,xs,xe,xm)
        y_max = max(y0,ys,ye,ym)

        # for drawing the previous one was for bounding
        xs,ys = angle_point(x0,y0,a0,radius)
        xe,ye = angle_point(x0,y0,a1,radius)

        filtered_points = []
        for x,y in points:
            px = int(((x-x0)*1.5)+x0)
            if px >= x_min and px <= x_max and y >= y_min and y <= y_max:
                angle = math.degrees(math.atan2(y-y0,x-x0))
                if angle < 0:
                    angle += 360
                if angle >= a0 and angle <= a1:
                    filtered_points.append((px,y))
        points = filtered_points

        if just_points:
            for x,y in points:
                put_pixel(x,y,color)
        else:
            if not fill:
                self.line(x0,y0,xs,ys,color,put_pixel)
                self.line(x0,y0,xe,ye,color,put_pixel)
                for x,y in points:
                    if put_pixel:
                        put_pixel(x,y,color)
                    else:
                        self.put_pixel(x,y,color)
            else:
                def add_pixel( x,y,color ):
                    points.append((x,y))
                self.line(x0,y0,xs,ys,color,add_pixel)
                self.line(x0,y0,xe,ye,color,add_pixel)
                self.rasterize(points,color,put_pixel)

    def rect(self,x0,y0,x1,y1,color,fill=False,put_pixel=None):
        """ draw a rectangle bounding x0,y0, x1,y1, in color == color optionally filling """
        x0 = int(x0)
        x1 = int(x1)
        y0 = int(y0)
        y1 = int(y1)

        if not fill:
            self.line(x0,y0,x0,y1,color)
            self.line(x0,y1,x1,y1,color)
            self.line(x1,y1,x1,y0,color)
            self.line(x1,y0,x0,y0,color)
        else:
            if y1 < y0:
                y=y0
                y0=y1
                y1 = y
            for y in range(y0,y1):
                self.line(x0,y,x1,y,color,put_pixel)

    def textat(self,x,y,color,message):
        """ draw a text message at a coordinate in the color specified """
        x,y = self.round_text_position(x,y)

        height, width = self.from_rowcol(1,len(message))

        if x < 0 or x >self.max_x or y < 0 or y >self.max_y:
            return

        if y + height > self.max_y:
            return

        if x + height > self.max_x:
            clip_height,clip_width = self.to_rowcol(1,(self.max_x-x))
            if clip_width > 0:
                message = message[:clip_width]
            else:
                return

        row,col = self.to_rowcol(x,y)
        self.win.addstr(row,col,message.encode('utf_8'),color)

    def polyline(self,points,color,put_pixel=None):
        """ draw a polyline defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color """
        n_points = len(points)
        if n_points == 0:
            return
        elif n_points == 1:
            x,y = points[0]
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        else:
            for idx in range(n_points-1):
                x0,y0 = points[idx]
                x1,y1 = points[idx+1]
                self.line(x0,y0,x1,y1,color,put_pixel)

    def poly_fill(self,points,color,put_pixel = None):
        """ fill a concave polygon by recursively subdividing until we get a convex polygon """
        clips = []

        minX,minY,maxX,maxY = self.get_bounds(points)

        minX = float(minX)
        minY = float(minY)
        maxX = float(maxX)
        maxY = float(maxY)

        midX = (minX+maxX)/2.0
        midY = (minY+maxY)/2.0

        clips.append((minX,minY,midX,midY))
        clips.append((midX,minY,maxX,midY))
        clips.append((midX,midY,maxX,maxY))
        clips.append((minX,midY,midX,maxY))

        while clips:
            minX,minY,maxX,maxY = clips.pop(0)
            if int(minX)==int(maxX) or int(minY)==int(maxY):
                continue

            p = self.clip_polygon(points,minX,minY,maxX,maxY)
            if p:
                if self.is_convex(p):
                    self.polygon(p,color,True,put_pixel)
                else:
                    midX = (minX+maxX)/2.0
                    midY = (minY+maxY)/2.0
                    if midX - minX < 1.0 or midY - minY < 1.0 or maxX - midX < 1.0 or maxY - midY < 1.0:
                        continue
                    clips.append((minX,minY,midX,midY))
                    clips.append((midX,minY,maxX,midY))
                    clips.append((midX,midY,maxX,maxY))
                    clips.append((minX,midY,midX,maxY))

    def polygon(self,points,color,fill=False,put_pixel=None):
        """ draw a polygon defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color
        the last point will be connected to the first point. polygons can be filled. """

        if not points:
            return

        convex = True
        if fill:
            convex = self.is_convex(points)

        poly_pixels = []
        def put_poly_pixel(x,y,color):
            poly_pixels.append((x,y))

        i = iter(points)
        first = p1 = next(i,None)
        while p1:
            p2 = next(i,None)
            if p2:
                last = p2
                self.line(p1[0],p1[1],p2[0],p2[1],color,put_poly_pixel)
            else:
                last = p1
                put_poly_pixel(p1[0],p1[1],color)
            p1 = p2
        self.line(first[0],first[1],last[0],last[1],color,put_poly_pixel)

        if not fill:
            for x,y in poly_pixels:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
        else:
            if convex:
                self.rasterize( poly_pixels, color, put_pixel)
            else:
                for x,y in poly_pixels:
                    if put_pixel:
                        put_pixel(x,y,color)
                    else:
                        self.put_pixel(x,y,color)
                self.poly_fill(points,color,put_pixel)

Functions

def angle_point(

x0, y0, a, radius)

def angle_point(x0,y0,a,radius):
    return (int(x0+(1.5*math.cos(math.radians(a))*radius)), int(y0+math.sin(math.radians(a))*radius))

Classes

class Canvas

primitive drawing surface attached to a curses window

class Canvas:
    """ primitive drawing surface attached to a curses window """

    to_mask = { 0:1, 1:2, 2:4, 3:8 }
    mask_to_char = {
        0 :'\u2008',
        1 :'\u2598',
        2 :'\u259d',
        3 :'\u2580',
        4 :'\u2596',
        5 :'\u258c',
        6 :'\u259e',
        7 :'\u259b',
        8 :'\u2597',
        9 :'\u259a',
        10:'\u2590',
        11:'\u259c',
        12:'\u2584',
        13:'\u2599',
        14:'\u259f',
        15:'\u2588'
        }

    char_to_mask = {
        '\u2008':0 ,
        '\u2598':1 ,
        '\u259d':2 ,
        '\u2580':3 ,
        '\u2596':4 ,
        '\u258c':5 ,
        '\u259e':6 ,
        '\u259b':7 ,
        '\u2597':8 ,
        '\u259a':9 ,
        '\u2590':10,
        '\u259c':11,
        '\u2584':12,
        '\u2599':13,
        '\u259f':14,
        '\u2588':15
        }

    def __init__(self, win = None ):
        """ constructor, can be initialized with a window to draw on, otherwise window must be set later by set_window """
        self.set_win(win)

    def to_rowcol(self, x, y ):
        """ return character row col for input x,y coordinates """
        return (int(y/2),int(x/2))

    def from_rowcol(self, row, col ):
        """ return the pixel location of a character position, returns upper left pixel in matrix"""
        return (int(row)*2,int(col)*2)

    def round_text_position(self, x, y):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(x,y)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if y1 < y:
            y = y + h/2
        if x1 < x:
            x = x + w/2
        return x, y

    def round_text_x_position(self, x):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(x,0)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if x1 < x:
            x = x + w/2
        return x

    def round_text_y_position(self, y):
        """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
        r,c = self.to_rowcol(0,y)
        y1,x1 = self.from_rowcol(r,c)
        h,w = self.from_rowcol(1,1)
        if y1 < y:
            y = y + h/2
        return y

    def set_win(self, win ):
        """ point this canvas at a window and initialize things, will blank out the window """
        self.win = win
        self.init_win()

    def init_win(self):
        """ initializes the window and sets up all of the defaults """
        curses.init_pair(5,curses.COLOR_BLACK,curses.COLOR_BLACK)
        curses.init_pair(1,curses.COLOR_GREEN,curses.COLOR_BLACK)
        curses.init_pair(2,curses.COLOR_RED,curses.COLOR_BLACK)
        curses.init_pair(3,curses.COLOR_CYAN,curses.COLOR_BLACK)
        curses.init_pair(4,curses.COLOR_WHITE,curses.COLOR_BLACK)

        self.green = curses.color_pair(1)
        self.red = curses.color_pair(2)
        self.cyan = curses.color_pair(3)
        self.white = curses.color_pair(4)
        self.black = curses.color_pair(5)

        if curses.can_change_color():
            self.color_min = 8
            self.color_max = 256

            red = 0
            green = 100
            blue = 20
            for c in range(self.color_min,self.color_max):
                curses.init_color(c,red,green,blue)
                red += 23
                green += 33
                blue += 53
                red = red % 1000
                green = green % 1000
                blue = blue % 1000

            for cidx in range(self.color_min,self.color_max):
                curses.init_pair(cidx,cidx,curses.COLOR_BLACK)
        else:
            self.color_min = 0
            self.color_max = 8

        if self.win:
            self.max_y,self.max_x = self.win.getmaxyx()
            self.char_map = [[None] * self.max_y for i in range(self.max_x)]
            self.max_y = self.max_y * 2
            self.max_x = self.max_x * 2
        else:
            self.max_y,self.max_x = (0,0)
            self.char_map = None

    def clear( self ):
        """ clear the entire canvas """
        self.char_map = [[None] * self.max_y for i in range(self.max_x)]

    def refresh( self ):
        """ refresh the display after drawing """
        self.win.refresh()

    def get_maxxy( self ):
        """ return the maximum number of x and y pixels that are available in this canvas """
        return (self.max_x,self.max_y)

    def put_pixel( self, x,y, color, set = True ):
        """ turn on a pixel with the color indicated """
        if x < 0 or x >= self.max_x or y < 0 or y >= self.max_y:
            return
        row,col = self.to_rowcol(x,y)
        if row >= self.max_y//2 or col >= self.max_x//2:
            return

        mask = self.to_mask[(int(x)%2)+((int(y)%2)*2)]

        if not self.char_map[col][row]:
            current_mask = 0
        else:
            current_mask = self.char_to_mask[self.char_map[col][row]]

        if set:
            self.char_map[col][row] = self.mask_to_char[ mask | current_mask ]
        else:
            self.char_map[col][row] = self.mask_to_char[ mask ^ current_mask ]
        try:
            self.win.addstr(row,col,self.char_map[col][row].encode('utf_8'),color)
        except:
            pass

    def line(self, x0, y0, x1, y1, color, put_pixel=None ):
        """ draw a line between x0,y0 and x1,y1 in color """
        def set_pixel(x,y,color):
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        dx = abs(x1 - x0)
        dy = abs(y1 - y0)
        x, y = int(x0), int(y0)
        sx = -1 if x0 > x1 else 1
        sy = -1 if y0 > y1 else 1
        if dx > dy:
            err = dx / 2.0
            while x != int(x1):
                set_pixel(x, y,color)
                err -= dy
                if err < 0:
                    y += sy
                    err += dx
                x += sx
        else:
            err = dy / 2.0
            while y != int(y1):
                set_pixel(x, y,color)
                err -= dx
                if err < 0:
                    x += sx
                    err += dy
                y += sy
        set_pixel(x, y, color)

    def intersect( self, seg1, seg2, clip_to_seg = False ):
        """ find the intersection of two segments as tuples (x0,y0,x1,y1) returns tuple (x,y) if no intersection returns None """
        def lineform( seg ):
            """ return A, B, C for the standard line formula Ax + By = C """
            A = float(seg[1]-seg[3])
            B = float(seg[2]-seg[0])
            C = A*seg[0]+B*seg[1]
            return (A,B,C)

        l1 = lineform(seg1)
        l2 = lineform(seg2)

        det = l1[0]*l2[1] - l2[0]*l1[1]
        if det != 0:
            x = (l2[1]*l1[2] - l1[1]*l2[2])/det
            y = (l1[0]*l2[2] - l2[0]*l1[2])/det

            if clip_to_seg:
                if x >= min(seg1[0],seg1[2]) and x <= max(seg1[0],seg1[2]) and y >= min(seg1[1],seg1[3]) and y <= max(seg1[1],seg1[3]):
                    return (int(x),int(y))
            else:
                return (int(x),int(y))
        return None

    def cross_product_length( self, pA,pB,pC ):
        """ compute the cross product of AB x BC """
        BAx = float(pA[0] - pB[0])
        BAy = float(pA[1] - pB[1])
        BCx = float(pC[0] - pB[0])
        BCy = float(pC[1] - pB[1])

        return (BAx * BCy - BAy * BCx)

    def is_convex( self, points ):
        """ take a list of (x,y) tuples representing the vertecies of a polygon in order and return True if it represents a convex polygon, False otherwise """
        got_negative = False
        got_positive = False
        num_points = len(points)
        if num_points <= 3:
            return True


        min_x,min_y,max_x,max_y = self.get_bounds(points)
        if max_x-min_x <= 1.0 or max_y-min_y <= 1.0:
            return True

        for A in range(num_points):
            B = (A+1)%num_points
            C = (B+1)%num_points
            cross_product = self.cross_product_length(points[A],points[B],points[C])
            if cross_product < 0:
                got_negative = True
            elif cross_product > 0:
                got_positive = True

        return not (got_negative and got_positive)

    def get_bounds(self,points):
        """ return tuple (min_x,min_y,max_x,max_y) for list of points """
        min_x = -1
        min_y = -1
        max_x = -1
        max_y = -1
        for x,y in points:
            if min_x < 0 or x < min_x:
                min_x = x
            if min_y < 0 or y < min_y:
                min_y = y
            if max_x < 0 or x > max_x:
                max_x = x
            if max_y < 0 or y > max_y:
                max_y = y
        return (min_x,min_y,max_x,max_y)

    def clip_polygon(self, points, minX, minY, maxX, maxY, dir=-1 ):
        """ clip a polygon against the bounds exressed by minX,minY to maxX,maxY and return either None for nothing inside or the points for the polygon dir is -1 all,0=top,1=right,2=bottom,3=left """
        def inside( p, minX, minY, maxX, maxY, dir ):
            x,y = p
            if dir == 0:
                return(y >= minY)
            elif dir == 1:
                return(x < maxX)
            elif dir == 2:
                return(y < maxY)
            elif dir == 3:
                return(x >= minX)

        def intersect(sp, ep, minX, minY, maxX, maxY, dir ):
            x0,y0 = sp
            x1,y1 = ep
            s1 = (x0,y0,x1,y1)
            if dir == 0:
                s2 = (minX,minY,maxX,minY)
            elif dir == 1:
                s2 = (maxX,minY,maxX,maxY)
            elif dir == 2:
                s2 = (minX,maxY,maxX,maxY)
            elif dir == 3:
                s2 = (minX,minY,minX,maxY)
            return self.intersect(s1,s2,False)

        if dir == -1:
            for d in [0,1,2,3]:
                points = self.clip_polygon(points,minX,minY,maxX,maxY,d)
                if not points:
                    return None
            return points
        else:
            sp = points[-1]
            out_points = []
            for ep in points:
                if inside(ep,minX,minY,maxX,maxY,dir):
                    if inside(sp,minX,minY,maxX,maxY,dir):
                        out_points.append(ep)
                    else:
                        ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                        out_points.append(ip)
                        out_points.append(ep)
                else:
                    if inside(sp,minX,minY,maxX,maxY,dir):
                        ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                        out_points.append(ip)
                sp = ep
            return out_points if out_points else None


    def rasterize( self, points, color, put_pixel=None):
        """ sort points representing the boundary of a filled shape and rasterize by filling lines with color """
        ps = sorted(points,key=lambda x: (x[1],x[0]))
        n_points = len(ps)
        if n_points == 0:
            return
        elif n_points == 1:
            x,y = ps[0]
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        else:
            idx = 1
            x0,y0 = ps[0]
            x1,y1 = x0,y0
            while idx < len(ps):
                xn,yn = ps[idx]
                if yn == y0:
                    x1,y1 = xn,yn
                else:
                    if x0 == x1:
                        if put_pixel:
                            put_pixel(x0,y0,color)
                        else:
                            self.put_pixel(x0,y0,color)
                    else:
                        self.line(x0,y0,x1,y1,color,put_pixel)
                    x0,y0 = xn,yn
                    x1,y1 = x0,y0
                idx += 1
            if x0 == x1:
                if put_pixel:
                    put_pixel(x0,y0,color)
                else:
                    self.put_pixel(x0,y0,color)
            else:
                self.line(x0,y0,x1,y1,color,put_pixel)

    def circle(self, x0, y0, radius, color, fill = False, put_pixel=None ):
        """ draw a circle centered at x0,y0 of radius radius in color """
        points = []

        def circle_point( points, xc,yc,x,y ):
            points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])

        x0 = int(x0)
        y0 = int(y0)
        radius = int(radius)
        x = 0
        y = radius
        d = 3 - 2 * radius
        circle_point(points,x0,y0,x,y)
        while y >= x:
            x += 1
            if d > 0:
                y -= 1
                d = d + 4 * (x - y) + 10
            else:
                d = d + 4 * x + 6
            circle_point(points,x0,y0,x,y)

        for idx in range(len(points)):
            x,y = points[idx]
            x = int(((x-x0)*1.5)+x0)
            points[idx] = (x,y)

        if not fill:
            for x,y in points:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
        else:
            self.rasterize(points,color,put_pixel)

    def arc(self,x0,y0,radius,a0,a1,color,fill=False,put_pixel=None,just_points=False):
        """ draw an arc between a0 degrees to a1 degrees centered at x0,y0 with radius and color """
        points = []

        def circle_point( points, xc,yc,x,y ):
            points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])

        x0 = int(x0)
        y0 = int(y0)
        radius = int(radius)
        x = 0
        y = radius
        d = 3 - 2 * radius
        circle_point(points,x0,y0,x,y)
        while y >= x:
            x += 1
            if d > 0:
                y -= 1
                d = d + 4 * (x - y) + 10
            else:
                d = d + 4 * x + 6
            circle_point(points,x0,y0,x,y)

        xs,ys = angle_point(x0,y0,a0,radius+0.5)
        xe,ye = angle_point(x0,y0,a1,radius+0.5)
        xm,ym = angle_point(x0,y0,(a0+a1)/2,radius+0.5)
        x_min = min(x0,xs,xe,xm)
        y_min = min(y0,ys,ye,ym)
        x_max = max(x0,xs,xe,xm)
        y_max = max(y0,ys,ye,ym)

        # for drawing the previous one was for bounding
        xs,ys = angle_point(x0,y0,a0,radius)
        xe,ye = angle_point(x0,y0,a1,radius)

        filtered_points = []
        for x,y in points:
            px = int(((x-x0)*1.5)+x0)
            if px >= x_min and px <= x_max and y >= y_min and y <= y_max:
                angle = math.degrees(math.atan2(y-y0,x-x0))
                if angle < 0:
                    angle += 360
                if angle >= a0 and angle <= a1:
                    filtered_points.append((px,y))
        points = filtered_points

        if just_points:
            for x,y in points:
                put_pixel(x,y,color)
        else:
            if not fill:
                self.line(x0,y0,xs,ys,color,put_pixel)
                self.line(x0,y0,xe,ye,color,put_pixel)
                for x,y in points:
                    if put_pixel:
                        put_pixel(x,y,color)
                    else:
                        self.put_pixel(x,y,color)
            else:
                def add_pixel( x,y,color ):
                    points.append((x,y))
                self.line(x0,y0,xs,ys,color,add_pixel)
                self.line(x0,y0,xe,ye,color,add_pixel)
                self.rasterize(points,color,put_pixel)

    def rect(self,x0,y0,x1,y1,color,fill=False,put_pixel=None):
        """ draw a rectangle bounding x0,y0, x1,y1, in color == color optionally filling """
        x0 = int(x0)
        x1 = int(x1)
        y0 = int(y0)
        y1 = int(y1)

        if not fill:
            self.line(x0,y0,x0,y1,color)
            self.line(x0,y1,x1,y1,color)
            self.line(x1,y1,x1,y0,color)
            self.line(x1,y0,x0,y0,color)
        else:
            if y1 < y0:
                y=y0
                y0=y1
                y1 = y
            for y in range(y0,y1):
                self.line(x0,y,x1,y,color,put_pixel)

    def textat(self,x,y,color,message):
        """ draw a text message at a coordinate in the color specified """
        x,y = self.round_text_position(x,y)

        height, width = self.from_rowcol(1,len(message))

        if x < 0 or x >self.max_x or y < 0 or y >self.max_y:
            return

        if y + height > self.max_y:
            return

        if x + height > self.max_x:
            clip_height,clip_width = self.to_rowcol(1,(self.max_x-x))
            if clip_width > 0:
                message = message[:clip_width]
            else:
                return

        row,col = self.to_rowcol(x,y)
        self.win.addstr(row,col,message.encode('utf_8'),color)

    def polyline(self,points,color,put_pixel=None):
        """ draw a polyline defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color """
        n_points = len(points)
        if n_points == 0:
            return
        elif n_points == 1:
            x,y = points[0]
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
        else:
            for idx in range(n_points-1):
                x0,y0 = points[idx]
                x1,y1 = points[idx+1]
                self.line(x0,y0,x1,y1,color,put_pixel)

    def poly_fill(self,points,color,put_pixel = None):
        """ fill a concave polygon by recursively subdividing until we get a convex polygon """
        clips = []

        minX,minY,maxX,maxY = self.get_bounds(points)

        minX = float(minX)
        minY = float(minY)
        maxX = float(maxX)
        maxY = float(maxY)

        midX = (minX+maxX)/2.0
        midY = (minY+maxY)/2.0

        clips.append((minX,minY,midX,midY))
        clips.append((midX,minY,maxX,midY))
        clips.append((midX,midY,maxX,maxY))
        clips.append((minX,midY,midX,maxY))

        while clips:
            minX,minY,maxX,maxY = clips.pop(0)
            if int(minX)==int(maxX) or int(minY)==int(maxY):
                continue

            p = self.clip_polygon(points,minX,minY,maxX,maxY)
            if p:
                if self.is_convex(p):
                    self.polygon(p,color,True,put_pixel)
                else:
                    midX = (minX+maxX)/2.0
                    midY = (minY+maxY)/2.0
                    if midX - minX < 1.0 or midY - minY < 1.0 or maxX - midX < 1.0 or maxY - midY < 1.0:
                        continue
                    clips.append((minX,minY,midX,midY))
                    clips.append((midX,minY,maxX,midY))
                    clips.append((midX,midY,maxX,maxY))
                    clips.append((minX,midY,midX,maxY))

    def polygon(self,points,color,fill=False,put_pixel=None):
        """ draw a polygon defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color
        the last point will be connected to the first point. polygons can be filled. """

        if not points:
            return

        convex = True
        if fill:
            convex = self.is_convex(points)

        poly_pixels = []
        def put_poly_pixel(x,y,color):
            poly_pixels.append((x,y))

        i = iter(points)
        first = p1 = next(i,None)
        while p1:
            p2 = next(i,None)
            if p2:
                last = p2
                self.line(p1[0],p1[1],p2[0],p2[1],color,put_poly_pixel)
            else:
                last = p1
                put_poly_pixel(p1[0],p1[1],color)
            p1 = p2
        self.line(first[0],first[1],last[0],last[1],color,put_poly_pixel)

        if not fill:
            for x,y in poly_pixels:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
        else:
            if convex:
                self.rasterize( poly_pixels, color, put_pixel)
            else:
                for x,y in poly_pixels:
                    if put_pixel:
                        put_pixel(x,y,color)
                    else:
                        self.put_pixel(x,y,color)
                self.poly_fill(points,color,put_pixel)

Ancestors (in MRO)

Class variables

var char_to_mask

var mask_to_char

var to_mask

Static methods

def __init__(

self, win=None)

constructor, can be initialized with a window to draw on, otherwise window must be set later by set_window

def __init__(self, win = None ):
    """ constructor, can be initialized with a window to draw on, otherwise window must be set later by set_window """
    self.set_win(win)

def arc(

self, x0, y0, radius, a0, a1, color, fill=False, put_pixel=None, just_points=False)

draw an arc between a0 degrees to a1 degrees centered at x0,y0 with radius and color

def arc(self,x0,y0,radius,a0,a1,color,fill=False,put_pixel=None,just_points=False):
    """ draw an arc between a0 degrees to a1 degrees centered at x0,y0 with radius and color """
    points = []
    def circle_point( points, xc,yc,x,y ):
        points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])
    x0 = int(x0)
    y0 = int(y0)
    radius = int(radius)
    x = 0
    y = radius
    d = 3 - 2 * radius
    circle_point(points,x0,y0,x,y)
    while y >= x:
        x += 1
        if d > 0:
            y -= 1
            d = d + 4 * (x - y) + 10
        else:
            d = d + 4 * x + 6
        circle_point(points,x0,y0,x,y)
    xs,ys = angle_point(x0,y0,a0,radius+0.5)
    xe,ye = angle_point(x0,y0,a1,radius+0.5)
    xm,ym = angle_point(x0,y0,(a0+a1)/2,radius+0.5)
    x_min = min(x0,xs,xe,xm)
    y_min = min(y0,ys,ye,ym)
    x_max = max(x0,xs,xe,xm)
    y_max = max(y0,ys,ye,ym)
    # for drawing the previous one was for bounding
    xs,ys = angle_point(x0,y0,a0,radius)
    xe,ye = angle_point(x0,y0,a1,radius)
    filtered_points = []
    for x,y in points:
        px = int(((x-x0)*1.5)+x0)
        if px >= x_min and px <= x_max and y >= y_min and y <= y_max:
            angle = math.degrees(math.atan2(y-y0,x-x0))
            if angle < 0:
                angle += 360
            if angle >= a0 and angle <= a1:
                filtered_points.append((px,y))
    points = filtered_points
    if just_points:
        for x,y in points:
            put_pixel(x,y,color)
    else:
        if not fill:
            self.line(x0,y0,xs,ys,color,put_pixel)
            self.line(x0,y0,xe,ye,color,put_pixel)
            for x,y in points:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
        else:
            def add_pixel( x,y,color ):
                points.append((x,y))
            self.line(x0,y0,xs,ys,color,add_pixel)
            self.line(x0,y0,xe,ye,color,add_pixel)
            self.rasterize(points,color,put_pixel)

def circle(

self, x0, y0, radius, color, fill=False, put_pixel=None)

draw a circle centered at x0,y0 of radius radius in color

def circle(self, x0, y0, radius, color, fill = False, put_pixel=None ):
    """ draw a circle centered at x0,y0 of radius radius in color """
    points = []
    def circle_point( points, xc,yc,x,y ):
        points.extend([(xc+x, yc+y),(xc-x, yc+y),(xc+x, yc-y),(xc-x, yc-y),(xc+y, yc+x),(xc-y, yc+x),(xc+y, yc-x),(xc-y, yc-x)])
    x0 = int(x0)
    y0 = int(y0)
    radius = int(radius)
    x = 0
    y = radius
    d = 3 - 2 * radius
    circle_point(points,x0,y0,x,y)
    while y >= x:
        x += 1
        if d > 0:
            y -= 1
            d = d + 4 * (x - y) + 10
        else:
            d = d + 4 * x + 6
        circle_point(points,x0,y0,x,y)
    for idx in range(len(points)):
        x,y = points[idx]
        x = int(((x-x0)*1.5)+x0)
        points[idx] = (x,y)
    if not fill:
        for x,y in points:
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
    else:
        self.rasterize(points,color,put_pixel)

def clear(

self)

clear the entire canvas

def clear( self ):
    """ clear the entire canvas """
    self.char_map = [[None] * self.max_y for i in range(self.max_x)]

def clip_polygon(

self, points, minX, minY, maxX, maxY, dir=-1)

clip a polygon against the bounds exressed by minX,minY to maxX,maxY and return either None for nothing inside or the points for the polygon dir is -1 all,0=top,1=right,2=bottom,3=left

def clip_polygon(self, points, minX, minY, maxX, maxY, dir=-1 ):
    """ clip a polygon against the bounds exressed by minX,minY to maxX,maxY and return either None for nothing inside or the points for the polygon dir is -1 all,0=top,1=right,2=bottom,3=left """
    def inside( p, minX, minY, maxX, maxY, dir ):
        x,y = p
        if dir == 0:
            return(y >= minY)
        elif dir == 1:
            return(x < maxX)
        elif dir == 2:
            return(y < maxY)
        elif dir == 3:
            return(x >= minX)
    def intersect(sp, ep, minX, minY, maxX, maxY, dir ):
        x0,y0 = sp
        x1,y1 = ep
        s1 = (x0,y0,x1,y1)
        if dir == 0:
            s2 = (minX,minY,maxX,minY)
        elif dir == 1:
            s2 = (maxX,minY,maxX,maxY)
        elif dir == 2:
            s2 = (minX,maxY,maxX,maxY)
        elif dir == 3:
            s2 = (minX,minY,minX,maxY)
        return self.intersect(s1,s2,False)
    if dir == -1:
        for d in [0,1,2,3]:
            points = self.clip_polygon(points,minX,minY,maxX,maxY,d)
            if not points:
                return None
        return points
    else:
        sp = points[-1]
        out_points = []
        for ep in points:
            if inside(ep,minX,minY,maxX,maxY,dir):
                if inside(sp,minX,minY,maxX,maxY,dir):
                    out_points.append(ep)
                else:
                    ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                    out_points.append(ip)
                    out_points.append(ep)
            else:
                if inside(sp,minX,minY,maxX,maxY,dir):
                    ip = intersect(sp,ep,minX,minY,maxX,maxY,dir)
                    out_points.append(ip)
            sp = ep
        return out_points if out_points else None

def cross_product_length(

self, pA, pB, pC)

compute the cross product of AB x BC

def cross_product_length( self, pA,pB,pC ):
    """ compute the cross product of AB x BC """
    BAx = float(pA[0] - pB[0])
    BAy = float(pA[1] - pB[1])
    BCx = float(pC[0] - pB[0])
    BCy = float(pC[1] - pB[1])
    return (BAx * BCy - BAy * BCx)

def from_rowcol(

self, row, col)

return the pixel location of a character position, returns upper left pixel in matrix

def from_rowcol(self, row, col ):
    """ return the pixel location of a character position, returns upper left pixel in matrix"""
    return (int(row)*2,int(col)*2)

def get_bounds(

self, points)

return tuple (min_x,min_y,max_x,max_y) for list of points

def get_bounds(self,points):
    """ return tuple (min_x,min_y,max_x,max_y) for list of points """
    min_x = -1
    min_y = -1
    max_x = -1
    max_y = -1
    for x,y in points:
        if min_x < 0 or x < min_x:
            min_x = x
        if min_y < 0 or y < min_y:
            min_y = y
        if max_x < 0 or x > max_x:
            max_x = x
        if max_y < 0 or y > max_y:
            max_y = y
    return (min_x,min_y,max_x,max_y)

def get_maxxy(

self)

return the maximum number of x and y pixels that are available in this canvas

def get_maxxy( self ):
    """ return the maximum number of x and y pixels that are available in this canvas """
    return (self.max_x,self.max_y)

def init_win(

self)

initializes the window and sets up all of the defaults

def init_win(self):
    """ initializes the window and sets up all of the defaults """
    curses.init_pair(5,curses.COLOR_BLACK,curses.COLOR_BLACK)
    curses.init_pair(1,curses.COLOR_GREEN,curses.COLOR_BLACK)
    curses.init_pair(2,curses.COLOR_RED,curses.COLOR_BLACK)
    curses.init_pair(3,curses.COLOR_CYAN,curses.COLOR_BLACK)
    curses.init_pair(4,curses.COLOR_WHITE,curses.COLOR_BLACK)
    self.green = curses.color_pair(1)
    self.red = curses.color_pair(2)
    self.cyan = curses.color_pair(3)
    self.white = curses.color_pair(4)
    self.black = curses.color_pair(5)
    if curses.can_change_color():
        self.color_min = 8
        self.color_max = 256
        red = 0
        green = 100
        blue = 20
        for c in range(self.color_min,self.color_max):
            curses.init_color(c,red,green,blue)
            red += 23
            green += 33
            blue += 53
            red = red % 1000
            green = green % 1000
            blue = blue % 1000
        for cidx in range(self.color_min,self.color_max):
            curses.init_pair(cidx,cidx,curses.COLOR_BLACK)
    else:
        self.color_min = 0
        self.color_max = 8
    if self.win:
        self.max_y,self.max_x = self.win.getmaxyx()
        self.char_map = [[None] * self.max_y for i in range(self.max_x)]
        self.max_y = self.max_y * 2
        self.max_x = self.max_x * 2
    else:
        self.max_y,self.max_x = (0,0)
        self.char_map = None

def intersect(

self, seg1, seg2, clip_to_seg=False)

find the intersection of two segments as tuples (x0,y0,x1,y1) returns tuple (x,y) if no intersection returns None

def intersect( self, seg1, seg2, clip_to_seg = False ):
    """ find the intersection of two segments as tuples (x0,y0,x1,y1) returns tuple (x,y) if no intersection returns None """
    def lineform( seg ):
        """ return A, B, C for the standard line formula Ax + By = C """
        A = float(seg[1]-seg[3])
        B = float(seg[2]-seg[0])
        C = A*seg[0]+B*seg[1]
        return (A,B,C)
    l1 = lineform(seg1)
    l2 = lineform(seg2)
    det = l1[0]*l2[1] - l2[0]*l1[1]
    if det != 0:
        x = (l2[1]*l1[2] - l1[1]*l2[2])/det
        y = (l1[0]*l2[2] - l2[0]*l1[2])/det
        if clip_to_seg:
            if x >= min(seg1[0],seg1[2]) and x <= max(seg1[0],seg1[2]) and y >= min(seg1[1],seg1[3]) and y <= max(seg1[1],seg1[3]):
                return (int(x),int(y))
        else:
            return (int(x),int(y))
    return None

def is_convex(

self, points)

take a list of (x,y) tuples representing the vertecies of a polygon in order and return True if it represents a convex polygon, False otherwise

def is_convex( self, points ):
    """ take a list of (x,y) tuples representing the vertecies of a polygon in order and return True if it represents a convex polygon, False otherwise """
    got_negative = False
    got_positive = False
    num_points = len(points)
    if num_points <= 3:
        return True
    min_x,min_y,max_x,max_y = self.get_bounds(points)
    if max_x-min_x <= 1.0 or max_y-min_y <= 1.0:
        return True
    for A in range(num_points):
        B = (A+1)%num_points
        C = (B+1)%num_points
        cross_product = self.cross_product_length(points[A],points[B],points[C])
        if cross_product < 0:
            got_negative = True
        elif cross_product > 0:
            got_positive = True
    return not (got_negative and got_positive)

def line(

self, x0, y0, x1, y1, color, put_pixel=None)

draw a line between x0,y0 and x1,y1 in color

def line(self, x0, y0, x1, y1, color, put_pixel=None ):
    """ draw a line between x0,y0 and x1,y1 in color """
    def set_pixel(x,y,color):
        if put_pixel:
            put_pixel(x,y,color)
        else:
            self.put_pixel(x,y,color)
    dx = abs(x1 - x0)
    dy = abs(y1 - y0)
    x, y = int(x0), int(y0)
    sx = -1 if x0 > x1 else 1
    sy = -1 if y0 > y1 else 1
    if dx > dy:
        err = dx / 2.0
        while x != int(x1):
            set_pixel(x, y,color)
            err -= dy
            if err < 0:
                y += sy
                err += dx
            x += sx
    else:
        err = dy / 2.0
        while y != int(y1):
            set_pixel(x, y,color)
            err -= dx
            if err < 0:
                x += sx
                err += dy
            y += sy
    set_pixel(x, y, color)

def poly_fill(

self, points, color, put_pixel=None)

fill a concave polygon by recursively subdividing until we get a convex polygon

def poly_fill(self,points,color,put_pixel = None):
    """ fill a concave polygon by recursively subdividing until we get a convex polygon """
    clips = []
    minX,minY,maxX,maxY = self.get_bounds(points)
    minX = float(minX)
    minY = float(minY)
    maxX = float(maxX)
    maxY = float(maxY)
    midX = (minX+maxX)/2.0
    midY = (minY+maxY)/2.0
    clips.append((minX,minY,midX,midY))
    clips.append((midX,minY,maxX,midY))
    clips.append((midX,midY,maxX,maxY))
    clips.append((minX,midY,midX,maxY))
    while clips:
        minX,minY,maxX,maxY = clips.pop(0)
        if int(minX)==int(maxX) or int(minY)==int(maxY):
            continue
        p = self.clip_polygon(points,minX,minY,maxX,maxY)
        if p:
            if self.is_convex(p):
                self.polygon(p,color,True,put_pixel)
            else:
                midX = (minX+maxX)/2.0
                midY = (minY+maxY)/2.0
                if midX - minX < 1.0 or midY - minY < 1.0 or maxX - midX < 1.0 or maxY - midY < 1.0:
                    continue
                clips.append((minX,minY,midX,midY))
                clips.append((midX,minY,maxX,midY))
                clips.append((midX,midY,maxX,maxY))
                clips.append((minX,midY,midX,maxY))

def polygon(

self, points, color, fill=False, put_pixel=None)

draw a polygon defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color the last point will be connected to the first point. polygons can be filled.

def polygon(self,points,color,fill=False,put_pixel=None):
    """ draw a polygon defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color
    the last point will be connected to the first point. polygons can be filled. """
    if not points:
        return
    convex = True
    if fill:
        convex = self.is_convex(points)
    poly_pixels = []
    def put_poly_pixel(x,y,color):
        poly_pixels.append((x,y))
    i = iter(points)
    first = p1 = next(i,None)
    while p1:
        p2 = next(i,None)
        if p2:
            last = p2
            self.line(p1[0],p1[1],p2[0],p2[1],color,put_poly_pixel)
        else:
            last = p1
            put_poly_pixel(p1[0],p1[1],color)
        p1 = p2
    self.line(first[0],first[1],last[0],last[1],color,put_poly_pixel)
    if not fill:
        for x,y in poly_pixels:
            if put_pixel:
                put_pixel(x,y,color)
            else:
                self.put_pixel(x,y,color)
    else:
        if convex:
            self.rasterize( poly_pixels, color, put_pixel)
        else:
            for x,y in poly_pixels:
                if put_pixel:
                    put_pixel(x,y,color)
                else:
                    self.put_pixel(x,y,color)
            self.poly_fill(points,color,put_pixel)

def polyline(

self, points, color, put_pixel=None)

draw a polyline defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color

def polyline(self,points,color,put_pixel=None):
    """ draw a polyline defined by the sequence points which represent a list of (x,y) tuples in the order they should be connected in color """
    n_points = len(points)
    if n_points == 0:
        return
    elif n_points == 1:
        x,y = points[0]
        if put_pixel:
            put_pixel(x,y,color)
        else:
            self.put_pixel(x,y,color)
    else:
        for idx in range(n_points-1):
            x0,y0 = points[idx]
            x1,y1 = points[idx+1]
            self.line(x0,y0,x1,y1,color,put_pixel)

def put_pixel(

self, x, y, color, set=True)

turn on a pixel with the color indicated

def put_pixel( self, x,y, color, set = True ):
    """ turn on a pixel with the color indicated """
    if x < 0 or x >= self.max_x or y < 0 or y >= self.max_y:
        return
    row,col = self.to_rowcol(x,y)
    if row >= self.max_y//2 or col >= self.max_x//2:
        return
    mask = self.to_mask[(int(x)%2)+((int(y)%2)*2)]
    if not self.char_map[col][row]:
        current_mask = 0
    else:
        current_mask = self.char_to_mask[self.char_map[col][row]]
    if set:
        self.char_map[col][row] = self.mask_to_char[ mask | current_mask ]
    else:
        self.char_map[col][row] = self.mask_to_char[ mask ^ current_mask ]
    try:
        self.win.addstr(row,col,self.char_map[col][row].encode('utf_8'),color)
    except:
        pass

def rasterize(

self, points, color, put_pixel=None)

sort points representing the boundary of a filled shape and rasterize by filling lines with color

def rasterize( self, points, color, put_pixel=None):
    """ sort points representing the boundary of a filled shape and rasterize by filling lines with color """
    ps = sorted(points,key=lambda x: (x[1],x[0]))
    n_points = len(ps)
    if n_points == 0:
        return
    elif n_points == 1:
        x,y = ps[0]
        if put_pixel:
            put_pixel(x,y,color)
        else:
            self.put_pixel(x,y,color)
    else:
        idx = 1
        x0,y0 = ps[0]
        x1,y1 = x0,y0
        while idx < len(ps):
            xn,yn = ps[idx]
            if yn == y0:
                x1,y1 = xn,yn
            else:
                if x0 == x1:
                    if put_pixel:
                        put_pixel(x0,y0,color)
                    else:
                        self.put_pixel(x0,y0,color)
                else:
                    self.line(x0,y0,x1,y1,color,put_pixel)
                x0,y0 = xn,yn
                x1,y1 = x0,y0
            idx += 1
        if x0 == x1:
            if put_pixel:
                put_pixel(x0,y0,color)
            else:
                self.put_pixel(x0,y0,color)
        else:
            self.line(x0,y0,x1,y1,color,put_pixel)

def rect(

self, x0, y0, x1, y1, color, fill=False, put_pixel=None)

draw a rectangle bounding x0,y0, x1,y1, in color == color optionally filling

def rect(self,x0,y0,x1,y1,color,fill=False,put_pixel=None):
    """ draw a rectangle bounding x0,y0, x1,y1, in color == color optionally filling """
    x0 = int(x0)
    x1 = int(x1)
    y0 = int(y0)
    y1 = int(y1)
    if not fill:
        self.line(x0,y0,x0,y1,color)
        self.line(x0,y1,x1,y1,color)
        self.line(x1,y1,x1,y0,color)
        self.line(x1,y0,x0,y0,color)
    else:
        if y1 < y0:
            y=y0
            y0=y1
            y1 = y
        for y in range(y0,y1):
            self.line(x0,y,x1,y,color,put_pixel)

def refresh(

self)

refresh the display after drawing

def refresh( self ):
    """ refresh the display after drawing """
    self.win.refresh()

def round_text_position(

self, x, y)

adjust a text position so that it always ends up down and to the right if it is at a half pixel offset

def round_text_position(self, x, y):
    """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
    r,c = self.to_rowcol(x,y)
    y1,x1 = self.from_rowcol(r,c)
    h,w = self.from_rowcol(1,1)
    if y1 < y:
        y = y + h/2
    if x1 < x:
        x = x + w/2
    return x, y

def round_text_x_position(

self, x)

adjust a text position so that it always ends up down and to the right if it is at a half pixel offset

def round_text_x_position(self, x):
    """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
    r,c = self.to_rowcol(x,0)
    y1,x1 = self.from_rowcol(r,c)
    h,w = self.from_rowcol(1,1)
    if x1 < x:
        x = x + w/2
    return x

def round_text_y_position(

self, y)

adjust a text position so that it always ends up down and to the right if it is at a half pixel offset

def round_text_y_position(self, y):
    """ adjust a text position so that it always ends up down and to the right if it is at a half pixel offset """
    r,c = self.to_rowcol(0,y)
    y1,x1 = self.from_rowcol(r,c)
    h,w = self.from_rowcol(1,1)
    if y1 < y:
        y = y + h/2
    return y

def set_win(

self, win)

point this canvas at a window and initialize things, will blank out the window

def set_win(self, win ):
    """ point this canvas at a window and initialize things, will blank out the window """
    self.win = win
    self.init_win()

def textat(

self, x, y, color, message)

draw a text message at a coordinate in the color specified

def textat(self,x,y,color,message):
    """ draw a text message at a coordinate in the color specified """
    x,y = self.round_text_position(x,y)
    height, width = self.from_rowcol(1,len(message))
    if x < 0 or x >self.max_x or y < 0 or y >self.max_y:
        return
    if y + height > self.max_y:
        return
    if x + height > self.max_x:
        clip_height,clip_width = self.to_rowcol(1,(self.max_x-x))
        if clip_width > 0:
            message = message[:clip_width]
        else:
            return
    row,col = self.to_rowcol(x,y)
    self.win.addstr(row,col,message.encode('utf_8'),color)

def to_rowcol(

self, x, y)

return character row col for input x,y coordinates

def to_rowcol(self, x, y ):
    """ return character row col for input x,y coordinates """
    return (int(y/2),int(x/2))