#!/usr/bin/python

# this is GNU code
# Pat Beirne, 2004.........pbeirne@pbeirne.com

# this little demo shows 3 different rendering techniques for 2D Bezier curves,
# and illustrates the difference between quadratic and cubic curves
#
# the basic render is a lookup table to evaluate
#    A(1-t)^3+3B(1-t)^2t+3C(1-t)t^2+Dt^3
#
# the second techniques uses the line splitting method, and recurses
# down
#
# the third method is a curve-of-pursuit. I have not seen this method
# in any literature, so I'm assuming it is my own. See
# http://pbeirne.com/Programming/Beziers.html for more
#
# please let me know if you use this code, or even test it;
# thanks.......... pbeirne@pbeirne.com


from Tkinter import *
from Tkconstants import *
from time import *

# just a helper to build an array of radios
def add_radios(config_array,wnd,v,cmd):
  for (a,b) in config_array:
    Radiobutton(wnd,text=b,variable=v,value=a,command=cmd).grid(sticky=W)
  return              

#-----------------------------------------------------------

class BezierDemo:
  """The main class. Manages the screen, a little state machine, and
  the drawing"""
  def __init__(self,wnd):
    # 3 vars for the radiobuttons
    self._drawLevel = IntVar()
    self._drawLevel.set(3)
    self._drawMode = StringVar()
    self._drawMode.set("lookup")
    self._curve = IntVar()
    self._curve.set(4)

    # window stuff
    self.wnd = wnd

    prompt_wnd              = Label(wnd,anchor=W,bg="white")
    prompt_wnd.pack(side=TOP)
    self.prompt_wnd         = prompt_wnd
    self.prompt_wnd["text"] = "Click anywhere to create the first point"

    canvas_wnd = Canvas(wnd,relief=SUNKEN,bg='white',width=800,height=650)
    canvas_wnd.pack(side=LEFT)
    self.canvas_wnd = canvas_wnd

    f1 = Frame(self.wnd,relief=SUNKEN,border=1)
    
    config = [[3,"Draw quadratic Beziers"],
              [4,"Draw cubic Beziers"]]
    f2 = Frame(f1)
    add_radios(config,f2,self._curve, self.draw)
    f2.grid(row=0,pady=40)

    config = [["lookup","Use lookup table"],
              ["recurse","Use line splitting"],
              ["cop","Use curve of pursuit"]]
    f2 = Frame(f1)
    add_radios(config, f2, self._drawMode, self.draw)
    f2.grid(row=2,pady=40)

    config = [[1,"Draw2Point"],
              [2,"Draw4Point"],
              [3,"Draw8Point"],
              [4,"Draw16Point"],
              [5,"Draw32Point"]]
    f2 = Frame(f1)
    add_radios(config, f2, self._drawLevel, self.draw)
    f2.grid(row=4,pady=40)
    f1.pack()

    
    # state stuff
    self._state = 0
    self._P = [(0,0),(0,0),(0,0),(0,0)]
    canvas_wnd.bind("<Button-1>",self.click)
    
    # draw stuff
    self._line      = None
    self._polyline  = None
    self._polybez   = None
    self._polybez2  = None
    self._nextPoint = 0
    self._rects     = [None, None, None, None]

    # build a lookup table for pre-evaluation of t factors
    i               = range(33)
    t               = map(lambda x: x/32.,i)
    self._bezLut3   = map(lambda x: 3*x*x*(1.-x),t)
    self._bezLut1   = map(lambda x: x*x*x, t)
    self._quadLut   = map(lambda x: x*x,t)
    self._quadLut2  = map(lambda x: 2*x*(1.-x),t)
    
    
  # the states are 0: waiting for another point
  #  1: waiting for an edit
  #  2: moving (in the middle of an edit)
  def click(self, event):
    x  = event.x
    y  = event.y
    rs = 2

    if self._state==0:
      self._P[self._nextPoint]     = (x,y)
      self._rects[self._nextPoint] = \
          self.canvas_wnd.create_rectangle(x-rs,y-rs,x+rs,y+rs,fill="black")
      self.prompt_wnd["text"]      = "Click to define another point, 4 in total"
      #self.canvas_wnd.create_text(10,10*self._nextPoint,
      #    text="point: %d %d" % (x,y),anchor=NW)
      self._nextPoint += 1
      # check to see if we've got enough points
      if self._nextPoint>=4:
        self._state = 1
        self.draw()
        self.prompt_wnd["text"] = "Now you can select a node to move, using the mouse"
      return
    if self._state==1:
      # see if we got a "hit"
      node = self.canvas_wnd.find_overlapping(x,y,x,y)
      if len(node)==0:
        return
      # node is one of the 4 rectangles
      self._movingNode = self._rects.index(node[0])
      self.canvas_wnd.bind("<Motion>",self.move)
      self.canvas_wnd.bind("<ButtonRelease>",self.click)
      self._state = 2
      return
    if self._state==2:
      self._state = 1
      self.canvas_wnd.unbind("<ButtonRelease>")
      self.canvas_wnd.unbind("<Motion>")
      if self._drawMode.get()=="cop":
        self.draw()
      return
            
  def draw(self):
    # don't draw till we have all the points
    if self._state==0:
      return
    #print "draw",self._drawLevel.get(),self._curve.get(),self._drawMode.get()

    # some cleanup of stuff that isn't in all drawing types
    self.canvas_wnd.delete("recurse")
    if self._polybez2:
      self.canvas_wnd.delete(self._polybez3)
      self.canvas_wnd.delete(self._polybez2)

    # copy the points to local vars
    ((x0,y0),(x1,y1),(x2,y2),(x3,y3)) = self._P

    if self._curve.get()==4:
      a = [x0,y0,x1,y1,x2,y2,x3,y3]
      self.canvas_wnd.itemconfig(self._rects[3],fill='black')
    else:
      a = [x0,y0,x1,y1,x2,y2]
      self.canvas_wnd.itemconfig(self._rects[3],fill='')

    # this is a switch statement
    dm = self._drawLevel.get()
    if self._drawMode.get()=="lookup":
      step = 32
      lut = {0:32, 1:16, 2:8, 3:4, 4:2, 5:1}
      if dm >= 0 and dm <= 32:
        step = lut[dm]
        c    = []
        for i in range(0,33,step):
          if self._curve.get()==4:
            c.append((x0*self._bezLut1[i] + x1*self._bezLut3[i] \
                      + x2*self._bezLut3[32-i] + x3*self._bezLut1[32-i]))
            c.append((y0*self._bezLut1[i] + y1*self._bezLut3[i] \
                      + y2*self._bezLut3[32-i] + y3*self._bezLut1[32-i]))
          else:
            c.append((x0*self._quadLut[i] + x1*self._quadLut2[i] \
                      + x2*self._quadLut[32-i]))
            c.append((y0*self._quadLut[i] + y1*self._quadLut2[i] \
                      + y2*self._quadLut[32-i]))
            
      if self._polyline == None:
        self._polyline = self.canvas_wnd.create_line(a,fill='green')
        self._polybez  = self.canvas_wnd.create_line(c,fill='black')
      else:
        self.canvas_wnd.coords(self._polyline, *a)
        self.canvas_wnd.coords(self._polybez, *c)
        
    elif self._drawMode.get()=="recurse":
      if self._polyline == None:
        self._polyline = self.canvas_wnd.create_line(a,fill='green')
      else:
        self.canvas_wnd.coords(self._polyline, *a)
      if self._curve.get()==4:
        self.drawDownCubic(dm, a)
      else:
        self.drawDownQuad(dm, a)

    elif self._drawMode.get()=="cop":
      self.drawCop()
      
  def drawDownQuad(self,levels,points):
    colors = ("","red","blue","purple","red","blue","green")
    mid    = ((points[0]+points[2])/2, \
           (points[1]+points[3])/2, \
           (points[2]+points[4])/2, \
           (points[3]+points[5])/2)
    self.canvas_wnd.create_line(mid[0:4],fill=colors[levels],tags="recurse")
    mid2 = ((mid[0]+mid[2])/2, (mid[1]+mid[3])/2)
    if (levels<=1):
      return
    levels -= 1
    self.drawDownQuad(levels, \
                      (points[0],points[1],mid[0],mid[1],mid2[0],mid2[1]))
    
    self.drawDownQuad(levels, \
                      (mid2[0],mid2[1],mid[2],mid[3],points[4],points[5]))
    return

  def drawDownCubic(self,levels,points):
    colors = ("","red","blue","purple","red","blue","green")
    mid    = ((points[0]+points[2])/2, \
           (points[1]+points[3])/2, \
           (points[2]+points[4])/2, \
           (points[3]+points[5])/2, \
           (points[4]+points[6])/2, \
           (points[5]+points[7])/2)
    self.canvas_wnd.create_line(mid[0:4],fill=colors[levels],tags="recurse")
    self.canvas_wnd.create_line(mid[2:6],fill=colors[levels],tags="recurse")
    mid2 = ((mid[0]+mid[2])/2, (mid[1]+mid[3])/2, \
          (mid[2]+mid[4])/2, (mid[3]+mid[5])/2)
    self.canvas_wnd.create_line(mid2[0:4],fill=colors[levels],tags="recurse")
    if (levels<=1):
      return
    levels -= 1
    self.drawDownCubic(levels, \
                       (points[0],points[1],mid[0],mid[1],mid2[0],mid2[1], \
                        (mid2[0]+mid2[2])/2, (mid2[1]+mid2[3])/2))
    self.drawDownCubic(levels, \
                       (points[6],points[7],mid[4],mid[5],mid2[2],mid2[3], \
                        (mid2[0]+mid2[2])/2, (mid2[1]+mid2[3])/2))
    return

  def drawCop(self):
    ((x0,y0),(x1,y1),(x2,y2),(x3,y3)) = self._P
    a = [x0,y0,x1,y1,x2,y2,x3,y3]

    if self._polyline == None:
      self._polyline = self.canvas_wnd.create_line(a[0:4],fill='green')
    self._polybez2  = self.canvas_wnd.create_line(a[2:6],fill='blue')
    self._polybez3  = self.canvas_wnd.create_line(a[4:8],fill='red')

    # draw slowly
    c_accum = [x2,y2]
    d_accum = [x3,y3]
    c = c_accum
    d = d_accum
    e = 0.01 # step size
    for i in range(100):
      t = i/100.
      T = 1-t
      b = (x1 + (x0-x1)*t , y1 + (y0-y1)*t)
      self.canvas_wnd.coords(self._polyline, x1,y1,b[0],b[1])

      c = (c[0] + (b[0]-c[0])*2*e/T , \
           c[1] + (b[1]-c[1])*2*e/T)
      c_accum.extend(c)
      self.canvas_wnd.coords(self._polybez2, *c_accum)

      d = (d[0] + (c[0]-d[0])*3*e/T, \
           d[1] + (c[1]-d[1])*3*e/T)
      d_accum.extend(d)
      self.canvas_wnd.coords(self._polybez3, *d_accum)
      
      self.canvas_wnd.update()
      sleep(0.03)
    

  def move(self,event):
    rs = 2
    x  = event.x
    y  = event.y
    # uses the self._movingNode to remember which one to move
    self._P[self._movingNode] = (x,y)
    self.canvas_wnd.coords(self._rects[self._movingNode],x-rs,y-rs,x+rs,y+rs)
    if self._drawMode.get() != "cop":
      self.draw()

#-----------------------------------------------------------
 
if __name__ == "__main__":

  root = Tk()
  b1   = BezierDemo(root)
  root.mainloop()
