# This is the first sizable Python program I wrote and dates back to 2001 # and Python V1.5.2. # This is yet another incarnation of an old graphics hack based around # misdrawing an analytic geometry curve calld a rose. The basic form is # simply the polat coordinate function r = cos(a * theta). "a" is the # "order" of the rose, a zero value degenerates to r = 1, a circle. While # this program is happy to draw that, much more interesting things happen when # one or more of the following is in effect: # 1) The "delta theta" between points is large enough to distort the curve, # e.g. 90 degress will draw a square, slightly less will be interesting. # 2) The order of the rose is too large to draw it accurately. # 3) Vectors are drawn at less than full speed. # 4) The program is stepping through different patterns on its own. # While you will be able to predict some aspects of the generated patterns, # a lot of what there is to be found is found at random! # The rose class has all the knowledge to implement generating vector data for # roses and handles all the timing issues. It does not have the user interface # for changing all the drawing parameters. class rose: "Defines everything needed for drawing a rose with timers." # Empty callback for rose completion. Use until caller calls auto, # which he really should do before drawing any rose. XXX. def nil(self): return '' # Initialization needs the canvas widget and the size in pixels. def __init__(self, c, size): print "rose init, size", size self.c = c self.size = size self.done = self.nil nvec = 399 # Number of vectors to draw the rose style = 100 # Angular distance along curve between points petals = 2 # Lobes on the rose (even values have 2X lobes) active = 0 # Non-zero when drawing via clocked callback nextpt = 0 # Next position to draw on next clock tick step = 0 # Number of vectors to draw each clock tick time = 0 # Time between roses in automatic mode # Return full rose line (a tuple of (x, y) tuples). No longer used, # but is the "purest" code. def rose(self, style, petals): self.style = style self.petals = petals line = (1.0, 0.0) for i in range (1, self.nvec): theta = 2.0*pi * style*i/self.nvec r = cos(petals * theta) line = line + (r * cos(theta), r * sin(theta)) line = line + (1.0, 0.0) return line # Generate vectors for the next chunk of rose. # Called by restart() to start a new rose and cont() on clock ticks. def roselet(self, style, petals, start, len): line = () stop = start + len if stop > self.nvec: stop = self.nvec for i in range (start, stop + 1): theta = 2.0*pi * style*i/self.nvec r = cos(petals * theta) line = line + (r * cos(theta), r * sin(theta)) if i and (style * i * (1 + (petals & 1)) % self.nvec == 0): print 'Stopping at', i stop = self.nvec break return line, stop, stop < self.nvec # Rescale (x,y) data to match our window. def scale(self, line, offset, scale): res = () for i in range(0, len(line)): res = res + ( (line[i] + offset) * scale, ) return res # Start drawing a rose, will continue via a callback. def start(self, lstyle, lpetals, nvec, lstep, ltime): self.style = lstyle self.petals = lpetals self.nvec = nvec self.step = lstep self.time = ltime self.restart() # Method to save automatic parameters and callback. We will handle # adjustments (and return adjusted style/petal), user handles delay # between roses. (Does that really make sense?) def auto(self, sincr, pincr, done): self.sincr = sincr self.pincr = pincr self.done = done # Erase any old vectors and start drawing a new rose. def restart(self): self.c.delete("vec") self.nextpt = 0 line, self.nextpt, run = \ self.roselet(self.style, self.petals, 0, self.step) self.c.create_line(self.scale(line, 1.05, self.size/2.1), \ fill="green", tags="vec") print "restart: run", run, self.active if run: if self.active == 0: self.active = 1 Misc.after(win, self.time, self.cont) else: print '*** Already active' else: print 'done on 1st roselet', self.active self.active = 0 self.done() # Callback routine to draw next piece of rose. def cont(self): line, self.nextpt, run = self.roselet(self.style, self.petals, \ self.nextpt, self.step) self.c.create_line(self.scale(line, 1.05, self.size/2.1), \ fill="yellow", tags="vec") if run: win.after(self.time, self.cont) else: print 'done', self.active self.active = 0 self.done() # Method that returns the next style and petal values for automatic # mode and remembers them internally. def next(self): self.style = self.style + self.sincr self.petals = self.petals + self.pincr if self.style <= 0 or self.petals < 0: self.style, self.petals = \ abs(self.petals) + 1, abs(self.style) return self.style, self.petals # Non-class code implement TK window, canvas to draw on, and disply control # panel information to run the show. from Tkinter import * from math import sin, cos, pi import sys # Routine to pick out a number from a string variable without complaint # about ill-formed numbers def get_ent(name): try: val = int(vars[name][2].get()) except ValueError: 'Bad value in', name val = 1 return val # Might as well have one to set things too. def set_ent(name, val): vars[name][2].set(val) # This routine is to set the focus on a particular datum from an alphabetic # accelerator. Needs work - the character makes it into the displayed data # too. def set_focus(event): ch = event.char print 'Focus: ', ch focus[ch].focus() # Number field change callback for values that affect appearance. # Start drawing a new rose. def vis_chg(args): print 'vis_chg', args win.after_cancel(r.after) r.start(get_ent('style'), get_ent('petal'), \ get_ent('vec'), \ get_ent('vec/tick'), get_ent('msec/tick')) # Number field change callback for values that affect automatic stepping. # Update the class's idea of what we want but don't trigger a new rose. def auto_chg(args): print 'auto_chg', args r.auto(get_ent('sincr'), get_ent('pincr'), done) def sel_stop(): sel('stop') def sel_step(): sel('step') def sel_go(): sel('go') # Stoplight button callback. # This keeps track of things based on the current state (stop or go) and # the desired new state (stop, go, or step). The step state isn't a real # state as it isn't saved, but it is a convenient argument. def sel(new_state): state = r.cur_state + new_state print "sel:", state labels = () if state == 'stopstop': r.restart() # redraw, but won't continue elif state == 'stopstep': resume() # do next elif state == 'stopgo': labels = 'stop', 'skip', 'redraw', 'reverse' r.cur_state = 'go' resume() elif state == 'gostop': win.after_cancel(r.after) labels = 'redraw', 'forw', 'go', 'back' r.cur_state = 'stop' elif state == 'gostep': # How about skip win.after_cancel(r.after) resume() elif state == 'gogo': # How about restart win.after_cancel(r.after) r.restart() if labels: b1.configure(text=labels[0]) b2.configure(text=labels[1]) b3.configure(text=labels[2]) b4.configure(text=labels[3]) # reverse button. If running, just change direction. If stopped, back up # one step (don't bother dealing with swapping right) def sel_rev(): sincr = get_ent('sincr') pincr = get_ent('pincr') r.auto(-sincr, -pincr, done) if r.cur_state == 'go': set_ent('sincr', -sincr) set_ent('pincr', -pincr) else: resume(); r.auto(sincr, pincr, done) # Callback when rose is complete def done(): delay = get_ent('delay') if r.cur_state == 'go': if delay: r.after = win.after(delay, resumex) else: resume() def resumex(): # After delay, check to see if should stop if r.cur_state == 'go': resume() def resume(): s, p = r.next() print 'Resume style/petals', s, p set_ent('style', s) set_ent('petal', p) r.restart() win = Tk() size = 700 c = Canvas(win, width = size, height = size, bg="gray20") r = rose(c, size) true, false = 1, 0 r.cur_state = 'stop' r.after = '' vars = {} focus = {} row = 0 # Setup control boxes in the four corners. Each tuple is a command, either # for a new frame ('f') with x, y, and corner or a tuple with # label name, method group name, initial value, and accelerator. for i in [ ('f', 0, 0, NW), ('style', 'vis', 100, 's'), ('sincr', 'auto', -1), \ ('petal', 'vis', 2, 'p'), ('pincr', 'auto', 1), \ ('f', 0, size, SW), ('vec', 'vis', 399, 'v'), ('vmin', 'notyet', 1), \ ('vmax', 'notyet', 399), \ ('f', size, size, SE), ('vec/tick', 'vis', 20, 't'), \ ('msec/tick', 'vis', 50, 'm'), ('delay', '', 2000, 'd') ]: if i[0] == 'f': fw = Frame(win, bg="gray20") c.create_window(i[1], i[2], window=fw, anchor=i[3]) else: il = Label(fw, text=i[0]) iv = StringVar() iv.set(i[2]) ie = Entry(fw, textvariable=iv, width=5) ie.svar = iv if i[1] == 'vis': ie.bind('', vis_chg) elif i[1] == 'auto': ie.bind('', auto_chg) il.grid(row=row, column=0) ie.grid(row=row, column=1) if len(i) >= 4: # Accelerator to move focus to entry widget focus[i[3]] = ie win.bind('<'+i[3]+'>', set_focus) vars[i[0]] = (il, ie, iv) row = row + 1 # Place stoplight buttons in upper right fw = Frame(win, bg="gray20") c.create_window(size, 0, window=fw, anchor=NE) b1 = Button(fw, text='redraw', command=sel_stop, bg='red') b2 = Button(fw, text= 'forw', command=sel_step, bg='yellow') b3 = Button(fw, text= 'go', command=sel_go, bg='green') b4 = Button(fw, text= 'back', command=sel_rev, bg='blue') b1.pack(anchor=E) b2.pack(anchor=E) b3.pack(anchor=E) b4.pack(anchor=E) c.pack() auto_chg('foo') vis_chg('foo') win.mainloop()