File: clroses.py

package info (click to toggle)
wxpython4.0 4.2.3%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 221,752 kB
  • sloc: cpp: 962,555; python: 230,573; ansic: 170,731; makefile: 51,756; sh: 9,342; perl: 1,564; javascript: 584; php: 326; xml: 200
file content (372 lines) | stat: -rw-r--r-- 15,803 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#----------------------------------------------------------------------------
# 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, int(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.  Note the negative scaling in the
    # Y direction, this compensates for Y moving down the screen, but up on
    # graph paper.
    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 int(self.takesvec) & 1 and int(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()

    # Go/Stop button.
    def cmd_go_stop(self):
        if self.cmd_state == self.CMD_STOP:
            self.cmd_state = self.CMD_GO
            self.resume()               # Draw next pattern
        elif self.cmd_state == self.CMD_GO:
            self.cmd_state = self.CMD_STOP
        self.update_labels()

    # Centralize button naming to share with initialization.
    # Leave colors to the application (assuming it cares), we can't guess
    # what's available.
    def update_labels(self):
        if self.cmd_state == self.CMD_STOP:
            self.AppCmdLabels(('Go', 'Redraw', 'Backward', 'Forward'))
        else:           # Must be in state CMD_GO
            self.AppCmdLabels(('Stop', 'Redraw', 'Reverse', 'Skip'))

    # Redraw/Redraw button
    def cmd_redraw(self):
        self.restart()          # Redraw current pattern

    # Backward/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_backward(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)

    # Forward/Skip button.  CMD_STOP & CMD_GO both just call resume.
    def cmd_step(self):
        self.resume()           # Draw next pattern

    # 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)