git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/branches/WX_2_8_BRANCH@46408 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
372 lines
15 KiB
Python
372 lines
15 KiB
Python
#----------------------------------------------------------------------------
|
|
# Name: clroses.py
|
|
# Purpose: Class definitions for Roses interactive display programs.
|
|
#
|
|
# Author: Ric Werme
|
|
# WWW: http://WermeNH.com/roses
|
|
#
|
|
# Created: June 2007
|
|
# CVS-ID: $Id$
|
|
# Copyright: Public Domain, please give credit where credit is due.
|
|
# License: Sorry, no EULA.
|
|
#----------------------------------------------------------------------------
|
|
|
|
# This is yet another incarnation of an old graphics hack based around
|
|
# misdrawing an analytic geometry curve called a rose. The basic form is
|
|
# simply the polar 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 degrees 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. It offers a "vision" of what an
|
|
# ideal Roses program should be, however, callers are welcome to assert their
|
|
# independence, override defaults, ignore features, etc.
|
|
|
|
from math import sin, cos, pi
|
|
|
|
# Rose class knows about:
|
|
# > Generating points and vectors (returning data as a list of points)
|
|
# > Starting a new rose (e.g. telling user to erase old vectors)
|
|
# > Stepping from one pattern to the next.
|
|
|
|
class rose:
|
|
"Defines everything needed for drawing a rose with timers."
|
|
|
|
# The following data is accessible by callers, but there are set
|
|
# methods for most everything and various method calls to client methods
|
|
# to display current values.
|
|
style = 100 # Angular distance along curve between points
|
|
sincr = -1 # Amount to increment style by in auto mode
|
|
petals = 2 # Lobes on the rose (even values have 2X lobes)
|
|
pincr = 1 # Amount to increment petals by in auto mode
|
|
nvec = 399 # Number of vectors to draw the rose
|
|
minvec = 0 # Minimum number acceptable in automatic mode
|
|
maxvec = 3600 # Maximum number acceptable in automatic mode
|
|
skipvec = 0 # Don't draw this many at the start (cheap animations)
|
|
drawvec = 3600 # Draw only this many (cheap animations)
|
|
step = 20 # Number of vectors to draw each clock tick
|
|
draw_delay = 50 # Time between roselet calls to watch pattern draw
|
|
wait_delay = 2000 # Time between roses in automatic mode
|
|
|
|
# Other variables that the application shouldn't access.
|
|
verbose = 0 # No good way to set this at the moment.
|
|
nextpt = 0 # Next position to draw on next clock tick
|
|
|
|
# Internal states:
|
|
INT_IDLE, INT_DRAW, INT_SEARCH, INT_WAIT, INT_RESIZE = range(5)
|
|
int_state = INT_IDLE
|
|
|
|
# Command states
|
|
CMD_STOP, CMD_GO = range(2)
|
|
cmd_state = CMD_STOP
|
|
|
|
# Return full rose line (a tuple of (x, y) tuples). Not used by interactive
|
|
# clients but still useful for command line and batch clients.
|
|
# This is the "purest" code and doesn't require the App* methods defined
|
|
# by the caller.
|
|
def rose(self, style, petals, vectors):
|
|
self.nvec = vectors
|
|
self.make_tables(vectors)
|
|
line = [(1.0, 0.0)]
|
|
for i in range (1, vectors):
|
|
theta = (style * i) % vectors
|
|
r = self.cos_table[(petals * theta) % vectors]
|
|
line.append((r * self.cos_table[theta], r * self.sin_table[theta]))
|
|
line.append((1.0, 0.0))
|
|
return line
|
|
|
|
# Generate vectors for the next chunk of rose.
|
|
|
|
# This is not meant to be called from an external module, as it is closely
|
|
# coupled to parameters set up within the class and limits set up by
|
|
# restart(). Restart() initializes all data this needs to start drawing a
|
|
# pattern, and clock() calls this to compute the next batch of points and
|
|
# hear if that is the last batch. We maintain all data we need to draw each
|
|
# batch after the first. theta should be 2.0*pi * style*i/self.nvec
|
|
# radians, but we deal in terms of the lookup table so it's just the index
|
|
# that refers to the same spot.
|
|
def roselet(self):
|
|
line = []
|
|
stop = self.nextpt + self.step
|
|
keep_running = True
|
|
if stop > self.endpt:
|
|
stop = self.endpt
|
|
keep_running = False
|
|
for i in range (self.nextpt, stop + 1):
|
|
theta = (self.style * i) % self.nvec
|
|
r = self.cos_table[(self.petals * theta) % self.nvec]
|
|
line.append((r * self.cos_table[theta], r * self.sin_table[theta]))
|
|
self.nextpt = stop
|
|
return line, keep_running
|
|
|
|
# Generate sine and cosine lookup tables. We could create data for just
|
|
# 1/4 of a circle, at least if vectors was a multiple of 4, and share a
|
|
# table for both sine and cosine, but memory is cheaper than it was in
|
|
# PDP-11 days. OTOH, small, shared tables would be more cache friendly,
|
|
# but if we were that concerned, this would be in C.
|
|
def make_tables(self, vectors):
|
|
self.sin_table = [sin(2.0 * pi * i / vectors) for i in range(vectors)]
|
|
self.cos_table = [cos(2.0 * pi * i / vectors) for i in range(vectors)]
|
|
|
|
# Rescale (x,y) data to match our window.
|
|
def rescale(self, line, offset, scale):
|
|
for i in range(len(line)):
|
|
line[i] = (line[i][0] * scale + offset[0], line[i][1] * scale + offset[1])
|
|
return line
|
|
|
|
# Euler's Method for computing the greatest common divisor. Knuth's
|
|
# "The Art of Computer Programming" vol.2 is the standard reference,
|
|
# but the web has several good ones too. Basically this sheds factors
|
|
# that aren't in the GCD and returns when there's nothing left to shed.
|
|
# N.B. Call with a >= b.
|
|
def gcd(self, a, b):
|
|
while b != 0:
|
|
a, b = b, a % b
|
|
return a
|
|
|
|
# Erase any old vectors and start drawing a new rose. When the program
|
|
# starts, the sine and cosine tables don't exist, build them here. (Of
|
|
# course, if an __init__() method is added, move the call there.
|
|
# If we're in automatic mode, check to see if the new pattern has neither
|
|
# too few or too many vectors and skip it if so. Skip by setting up for
|
|
# a one tick wait to let us get back to the main loop so the user can
|
|
# update parameters or stop.
|
|
def restart(self):
|
|
if self.verbose:
|
|
print 'restart: int_state', self.int_state, 'cmd_state', self.cmd_state
|
|
try:
|
|
tmp = self.sin_table[0]
|
|
except:
|
|
self.make_tables(self.nvec)
|
|
|
|
new_state = self.INT_DRAW
|
|
self.takesvec = self.nvec / self.gcd(self.nvec, self.style)
|
|
if not self.takesvec & 1 and self.petals & 1:
|
|
self.takesvec /= 2
|
|
if self.cmd_state == self.CMD_GO:
|
|
if self.minvec > self.takesvec or self.maxvec < self.takesvec:
|
|
new_state = self.INT_SEARCH
|
|
self.AppSetTakesVec(self.takesvec)
|
|
self.AppClear()
|
|
self.nextpt = self.skipvec
|
|
self.endpt = min(self.takesvec, self.skipvec + self.drawvec)
|
|
old_state, self.int_state = self.int_state, new_state
|
|
if old_state == self.INT_IDLE: # Clock not running
|
|
self.clock()
|
|
elif old_state == self.INT_WAIT: # May be long delay, restart
|
|
self.AppCancelTimer()
|
|
self.clock()
|
|
else:
|
|
return 1 # If called by clock(), return and start clock
|
|
return 0 # We're in INT_IDLE or INT_WAIT, clock running
|
|
|
|
# Called from App. Recompute the center and scale values for the subsequent pattern.
|
|
# Force us into INT_RESIZE state if not already there so that in 100 ms we'll start
|
|
# to draw something to give an idea of the new size.
|
|
def resize(self, size, delay):
|
|
xsize, ysize = size
|
|
self.center = (xsize / 2, ysize / 2)
|
|
self.scale = min(xsize, ysize) / 2.1
|
|
self.repaint(delay)
|
|
|
|
# Called from App or above. From App, called with small delay because
|
|
# some window managers will produce a flood of expose events or call us
|
|
# before initialization is done.
|
|
def repaint(self, delay):
|
|
if self.int_state != self.INT_RESIZE:
|
|
# print 'repaint after', delay
|
|
self.int_state = self.INT_RESIZE
|
|
self.AppCancelTimer()
|
|
self.AppAfter(delay, self.clock)
|
|
|
|
# Method that returns the next style and petal values for automatic
|
|
# mode and remembers them internally. Keep things scaled in the
|
|
# range [0:nvec) because there's little reason to exceed that.
|
|
def next(self):
|
|
self.style += self.sincr
|
|
self.petals += self.pincr
|
|
if self.style <= 0 or self.petals < 0:
|
|
self.style, self.petals = \
|
|
abs(self.petals) + 1, abs(self.style)
|
|
if self.style >= self.nvec:
|
|
self.style %= self.nvec # Don't bother defending against 0
|
|
if self.petals >= self.nvec:
|
|
self.petals %= self.nvec
|
|
self.AppSetParam(self.style, self.petals, self.nvec)
|
|
|
|
# Resume pattern drawing with the next one to display.
|
|
def resume(self):
|
|
self.next()
|
|
return self.restart()
|
|
|
|
# Stop/Redraw button.
|
|
def cmd_stop(self):
|
|
if self.cmd_state == self.CMD_STOP:
|
|
self.restart() # Redraw current pattern
|
|
elif self.cmd_state == self.CMD_GO:
|
|
self.cmd_state = self.CMD_STOP
|
|
self.update_labels()
|
|
|
|
# Skip/Forward button. CMD_STOP & CMD_GO both just call resume.
|
|
def cmd_step(self):
|
|
# print 'cmd_step, cmd_state', self.cmd_state
|
|
self.resume() # Draw next pattern
|
|
|
|
# Go/Redraw button
|
|
def cmd_go(self):
|
|
if self.cmd_state == self.CMD_STOP:
|
|
self.cmd_state = self.CMD_GO
|
|
self.update_labels()
|
|
self.resume() # Draw next pattern
|
|
elif self.cmd_state == self.CMD_GO:
|
|
self.restart() # Redraw current pattern
|
|
|
|
# Centralize button naming to share with initialization.
|
|
def update_labels(self):
|
|
if self.cmd_state == self.CMD_STOP:
|
|
self.AppCmdLabels(('Redraw', 'Forward', 'Go', 'Back'))
|
|
else: # Must be in state CMD_GO
|
|
self.AppCmdLabels(('Stop', 'Skip', 'Redraw', 'Reverse'))
|
|
|
|
# Reverse button. Useful for when you see an interesting pattern and want
|
|
# to go back to it. If running, just change direction. If stopped, back
|
|
# up one step. The resume code handles the step, then we change the
|
|
# incrementers back to what they were. (Unless resume changed them too.)
|
|
def cmd_reverse(self):
|
|
self.sincr = -self.sincr
|
|
self.pincr = -self.pincr
|
|
if self.cmd_state == self.CMD_STOP:
|
|
self.resume();
|
|
self.sincr = -self.sincr # Go forward again
|
|
self.pincr = -self.pincr
|
|
else:
|
|
self.AppSetIncrs(self.sincr, self.pincr)
|
|
|
|
# Handler called on each timer event. This handles the metered drawing
|
|
# of a rose and the delays between them. It also registers for the next
|
|
# timer event unless we're idle (rose is done and the delay between
|
|
# roses is 0.)
|
|
def clock(self):
|
|
if self.int_state == self.INT_IDLE:
|
|
# print 'clock called in idle state'
|
|
delay = 0
|
|
elif self.int_state == self.INT_DRAW:
|
|
line, run = self.roselet()
|
|
self.AppCreateLine(self.rescale(line, self.center, self.scale))
|
|
if run:
|
|
delay = self.draw_delay
|
|
else:
|
|
if self.cmd_state == self.CMD_GO:
|
|
self.int_state = self.INT_WAIT
|
|
delay = self.wait_delay
|
|
else:
|
|
self.int_state = self.INT_IDLE
|
|
delay = 0
|
|
elif self.int_state == self.INT_SEARCH:
|
|
delay = self.resume() # May call us to start drawing
|
|
if self.int_state == self.INT_SEARCH:
|
|
delay = self.draw_delay # but not if searching.
|
|
elif self.int_state == self.INT_WAIT:
|
|
if self.cmd_state == self.CMD_GO:
|
|
delay = self.resume() # Calls us to start drawing
|
|
else:
|
|
self.int_state = self.INT_IDLE
|
|
delay = 0
|
|
elif self.int_state == self.INT_RESIZE: # Waiting for resize event stream to settle
|
|
self.AppSetParam(self.style, self.petals, self.nvec)
|
|
self.AppSetIncrs(self.sincr, self.pincr)
|
|
delay = self.restart() # Calls us to start drawing
|
|
|
|
if delay == 0:
|
|
if self.verbose:
|
|
print 'clock: going idle from state', self.int_state
|
|
else:
|
|
self.AppAfter(delay, self.clock)
|
|
|
|
# Methods to allow App to change the parameters on the screen.
|
|
# These expect to be called when the associated paramenter changes,
|
|
# but work reasonably well if several are called at once. (E.g.
|
|
# tkroses.py groups them into things that affect the visual display
|
|
# and warrant a new start, and things that just change and don't affect
|
|
# the ultimate pattern. All parameters within a group are updated
|
|
# at once even if the value hasn't changed.
|
|
|
|
# We restrict the style and petals parameters to the range [0: nvec)
|
|
# since numbers outside of that range aren't interesting. We don't
|
|
# immediately update the value in the application, we probably should.
|
|
|
|
# NW control window - key parameters
|
|
def SetStyle(self, value):
|
|
self.style = value % self.nvec
|
|
self.restart()
|
|
|
|
def SetSincr(self, value):
|
|
self.sincr = value
|
|
|
|
def SetPetals(self, value):
|
|
self.petals = value % self.nvec
|
|
self.restart()
|
|
|
|
def SetPincr(self, value):
|
|
self.pincr = value
|
|
|
|
|
|
# SW control window - vectors
|
|
def SetVectors(self, value):
|
|
self.nvec = value
|
|
self.style %= value
|
|
self.petals %= value
|
|
self.AppSetParam(self.style, self.petals, self.nvec)
|
|
self.make_tables(value)
|
|
self.restart()
|
|
|
|
def SetMinVec(self, value):
|
|
if self.maxvec >= value and self.nvec >= value:
|
|
self.minvec = value
|
|
|
|
def SetMaxVec(self, value):
|
|
if self.minvec < value:
|
|
self.maxvec = value
|
|
|
|
def SetSkipFirst(self, value):
|
|
self.skipvec = value
|
|
self.restart()
|
|
|
|
def SetDrawOnly(self, value):
|
|
self.drawvec = value
|
|
self.restart()
|
|
|
|
|
|
# SE control window - timings
|
|
def SetStep(self, value):
|
|
self.step = value
|
|
|
|
def SetDrawDelay(self, value):
|
|
self.draw_delay = value
|
|
|
|
def SetWaitDelay(self, value):
|
|
self.wait_delay = value
|
|
|
|
# Method for client to use to have us supply our defaults.
|
|
def SupplyControlValues(self):
|
|
self.update_labels()
|
|
self.AppSetParam(self.style, self.petals, self.nvec)
|
|
self.AppSetIncrs(self.sincr, self.pincr)
|
|
self.AppSetVectors(self.nvec, self.minvec, self.maxvec,
|
|
self.skipvec, self.drawvec)
|
|
self.AppSetTiming(self.step, self.draw_delay, self.wait_delay)
|