Mercurial > code > home > repos > light9
annotate flax/Timeline.py @ 2326:e9caffe926df
attribute 'nounlink' to save space
author | drewp@bigasterisk.com |
---|---|
date | Thu, 01 Jun 2023 18:30:25 -0700 |
parents | 851cf44cea40 |
children |
rev | line source |
---|---|
0 | 1 from TLUtility import make_attributes_from_args, dict_scale, dict_max, \ |
2 DummyClass, last_less_than, first_greater_than | |
3 from time import time | |
206 | 4 import random |
0 | 5 from __future__ import division # "I'm sending you back to future!" |
6 | |
7 """ | |
110
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
8 Quote of the Build (from Ghostbusters II) |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
9 Dana: Okay, but after dinner, I don't want you putting any of your old cheap |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
10 moves on me. |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
11 Peter: Ohhhh no! I've got all NEW cheap moves. |
0 | 12 """ |
13 | |
14 class MissingBlender(Exception): | |
15 """Raised when a TimedEvent is missing a blender.""" | |
16 def __init__(self, timedevent): | |
17 make_attributes_from_args('timedevent') | |
18 Exception.__init__(self, "%r is missing a blender." % \ | |
19 self.timedevent) | |
20 | |
21 # these are chosen so we can multiply by -1 to reverse the direction, | |
22 # and multiply direction by the time difference to determine new times. | |
23 FORWARD = 1 | |
24 BACKWARD = -1 | |
25 | |
135 | 26 MISSING = 'missing' |
27 | |
0 | 28 class TimedEvent: |
29 """Container for a Frame which includes a time that it occurs at, | |
30 and which blender occurs after it.""" | |
135 | 31 def __init__(self, time, frame=MISSING, blender=None, level=1.0): |
0 | 32 make_attributes_from_args('time', 'frame') |
33 self.next_blender = blender | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
34 self.level = level |
0 | 35 def __float__(self): |
36 return self.time | |
37 def __cmp__(self, other): | |
135 | 38 if other is None: |
39 raise "I can't compare with a None. I am '%s'" % str(self) | |
0 | 40 if type(other) in (float, int): |
41 return cmp(self.time, other) | |
42 else: | |
43 return cmp(self.time, other.time) | |
44 def __repr__(self): | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
45 return "<TimedEvent %s at %.2f, time=%.2f, next blender=%s>" % \ |
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
46 (self.frame, self.level, self.time, self.next_blender) |
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
47 def get_level(self): |
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
48 return self.level |
123 | 49 def __hash__(self): |
50 return id(self.time) ^ id(self.frame) ^ id(self.next_blender) | |
0 | 51 |
52 class Blender: | |
53 """Blenders are functions that merge the effects of two LevelFrames.""" | |
54 def __init__(self): | |
55 pass | |
135 | 56 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 57 """Return a LevelFrame combining two LevelFrames (startframe and |
58 endframe). blendtime is how much of the blend should be performed | |
59 and will be expressed as a percentage divided by 100, i.e. a float | |
135 | 60 between 0.0 and 1.0. time_since_startframe is the time since the |
61 startframe was on screen in seconds (float). | |
0 | 62 |
63 Very important note: Blenders will *not* be asked for values | |
64 at end points (i.e. blendtime=0.0 and blendtime=1.0). | |
65 The LevelFrames will be allowed to specify the values at | |
66 those times. This is unfortunately for implemementation and | |
67 simplicity purposes. In other words, if we didn't do this, | |
68 we could have two blenders covering the same point in time and | |
69 not know which one to ask for the value. Thus, this saves us | |
70 a lot of messiness with minimal or no sacrifice.""" | |
71 pass | |
72 def __str__(self): | |
73 """As a default, we'll just return the name of the class. Subclasses | |
74 can add parameters on if they want.""" | |
75 return str(self.__class__) | |
76 def linear_blend(self, startframe, endframe, blendtime): | |
77 """Utility function to help you produce linear combinations of two | |
78 blends. blendtime is the percent/100 that the blend should | |
79 completed. In other words, 0.25 means it should be 0.75 * startframe + | |
80 0.25 * endframe. This function is included since many blenders are | |
81 just functions on the percentage and still combine start and end frames | |
82 in this way.""" | |
123 | 83 if startframe.frame == endframe.frame: |
135 | 84 level = startframe.level + (blendtime * \ |
85 (endframe.level - startframe.level)) | |
86 levels = {startframe.frame : level} | |
123 | 87 else: |
88 levels = {startframe.frame : (1.0 - blendtime) * startframe.level, | |
89 endframe.frame : blendtime * endframe.level} | |
90 return levels | |
0 | 91 |
92 class InstantEnd(Blender): | |
93 """Instant change from startframe to endframe at the end. In other words, | |
94 the value returned will be the startframe all the way until the very end | |
95 of the blend.""" | |
135 | 96 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 97 # "What!?" you say, "Why don't you care about blendtime?" |
98 # This is because Blenders never be asked for blenders at the endpoints | |
99 # (after all, they wouldn't be blenders if they were). Please see | |
100 # 'Very important note' in Blender.__doc__ | |
123 | 101 return {startframe.frame : startframe.level} |
0 | 102 |
103 class InstantStart(Blender): | |
104 """Instant change from startframe to endframe at the beginning. In other | |
105 words, the value returned will be the startframe at the very beginning | |
106 and then be endframe at all times afterwards.""" | |
135 | 107 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 108 # "What!?" you say, "Why don't you care about blendtime?" |
109 # This is because Blenders never be asked for blenders at the endpoints | |
110 # (after all, they wouldn't be blenders if they were). Please see | |
111 # 'Very important note' in Blender.__doc__ | |
123 | 112 return {endframe.frame : endframe.level} |
0 | 113 |
114 class LinearBlender(Blender): | |
115 """Linear fade from one frame to another""" | |
135 | 116 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 117 return self.linear_blend(startframe, endframe, blendtime) |
118 | |
119 class ExponentialBlender(Blender): | |
120 """Exponential fade fron one frame to another. You get to specify | |
121 the exponent. If my math is correct, exponent=1 means the same thing | |
122 as LinearBlender.""" | |
123 def __init__(self, exponent): | |
124 self.exponent = exponent | |
135 | 125 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 126 blendtime = blendtime ** self.exponent |
127 return self.linear_blend(startframe, endframe, blendtime) | |
128 | |
129 # 17:02:53 drewp: this makes a big difference for the SmoothBlender | |
130 # (-x*x*(x-1.5)*2) function | |
131 class SmoothBlender(Blender): | |
132 """Drew's "Smoove" Blender function. Hopefully he'll document and | |
133 parametrize it.""" | |
135 | 134 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 135 blendtime = (-1 * blendtime) * blendtime * (blendtime - 1.5) * 2 |
136 return self.linear_blend(startframe, endframe, blendtime) | |
137 | |
135 | 138 class Strobe(Blender): |
139 "Strobes the frame on the right side between offlevel and onlevel." | |
140 def __init__(self, ontime, offtime, onlevel=1, offlevel=0): | |
138 | 141 "times are in seconds (floats)" |
135 | 142 make_attributes_from_args('ontime', 'offtime', 'onlevel', 'offlevel') |
143 self.cycletime = ontime + offtime | |
144 def __call__(self, startframe, endframe, blendtime, time_since_startframe): | |
145 # time into the current period | |
146 period_time = time_since_startframe % self.cycletime | |
147 if period_time <= self.ontime: | |
148 return {endframe.frame : self.onlevel} | |
149 else: | |
150 return {endframe.frame : self.offlevel} | |
151 | |
206 | 152 class Sine(Blender): |
153 "Strobes the frame on the right side between offlevel and onlevel." | |
154 def __init__(self, period, onlevel=1, offlevel=0): | |
155 "times are in seconds (floats)" | |
156 make_attributes_from_args('period', 'onlevel', 'offlevel') | |
157 def __call__(self, startframe, endframe, blendtime, time_since_startframe): | |
158 sin = math.sin(time_since_startframe / self.period * 2 * math.pi) | |
159 zerotoone = (sin / 2) + 0.5 | |
160 level = offlevel + (zerotoone * (onlevel - offlevel)) | |
161 return {endframe.frame : level} | |
162 | |
163 class RandomStrobe(Blender): | |
164 def __init__(self, minwaitlen=0.1, maxwaitlen=0.6, burstlen=0.14, \ | |
165 burstintensity=1, offintensity=0.05, tracklen=500): | |
166 "times are in seconds (floats)" | |
167 make_attributes_from_args('burstlen', 'burstintensity', 'offintensity') | |
168 self.burstintervals = [] | |
169 timecursor = 0 | |
170 | |
171 while timecursor < tracklen: | |
172 waitlen = minwaitlen + (random.random() * (maxwaitlen - minwaitlen)) | |
173 timecursor += waitlen | |
174 self.burstintervals.append((timecursor, timecursor + burstlen)) | |
175 def __call__(self, startframe, endframe, blendtime, time_since_startframe): | |
176 found_burst = 0 | |
177 for intstart, intend in self.burstintervals: | |
178 if intstart <= time_since_startframe <= intend: | |
179 found_burst = 1 | |
180 break | |
181 | |
182 if found_burst: | |
183 return {endframe.frame : self.burstintensity} | |
184 else: | |
185 return {endframe.frame : self.offintensity} | |
186 | |
0 | 187 class TimelineTrack: |
188 """TimelineTrack is a single track in a Timeline. It consists of a | |
189 list of TimedEvents and a name. Length is automatically the location | |
190 of the last TimedEvent. To extend the Timeline past that, add an | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
191 EmptyTimedEvent (which doesn't exist :-/).""" |
135 | 192 def __init__(self, name, *timedevents, **kw): |
193 if kw.get('default_frame'): | |
194 self.default_frame = kw['default_frame'] | |
195 else: | |
196 self.default_frame = None | |
0 | 197 self.name = name |
138 | 198 self.set_events(list(timedevents)) |
199 def set_events(self, events): | |
200 """This is given a list of TimedEvents. They need not be sorted.""" | |
201 self.events = events | |
202 self._cleaup_events() | |
203 def _cleaup_events(self): | |
204 """This makes sure all events are in the right order and have defaults | |
205 filled in if they have missing frames.""" | |
0 | 206 self.events.sort() |
139 | 207 self.fill_in_missing_frames() |
138 | 208 def add_event(self, event): |
209 """Add a TimedEvent object to this TimelineTrack""" | |
210 self.events.append(event) | |
211 self._cleaup_events(self.events) | |
212 def delete_event(self, event): | |
213 """Delete event by TimedEvent object""" | |
214 self.events.remove(event) | |
215 self._cleaup_events(self.events) | |
216 def delete_event_by_name(self, name): | |
217 """Deletes all events matching a certain name""" | |
218 self.events = [e for e in self.events if e.name is not name] | |
219 self._cleaup_events(self.events) | |
220 def delete_event_by_time(self, starttime, endtime=None): | |
221 """Deletes all events within a certain time range, inclusive. endtime | |
222 is optional.""" | |
223 endtime = endtime or starttime | |
224 self.events = [e for e in self.events | |
225 if e.time >= starttime and e.time <= endtime] | |
226 self._cleaup_events(self.events) | |
139 | 227 def fill_in_missing_frames(self): |
138 | 228 """Runs through all events and sets TimedEvent with missing frames to |
229 the default frame.""" | |
135 | 230 for event in self.events: |
231 if event.frame == MISSING: | |
232 event.frame = self.default_frame | |
0 | 233 def __str__(self): |
234 return "<TimelineTrack with events: %r>" % self.events | |
235 def has_events(self): | |
236 """Whether the TimelineTrack has anything in it. In general, | |
237 empty level Tracks should be avoided. However, empty function tracks | |
238 might be common.""" | |
239 return len(self.events) | |
240 def length(self): | |
241 """Returns the length of this track in pseudosecond time units. | |
242 This is done by finding the position of the last TimedEvent.""" | |
243 return float(self.events[-1]) | |
244 def get(self, key, direction=FORWARD): | |
245 """Returns the event at a specific time key. If there is no event | |
246 at that time, a search will be performed in direction. Also note | |
247 that if there are multiple events at one time, only the first will | |
248 be returned. (Probably first in order of adding.) This is not | |
249 a problem at the present since this method is intended for LevelFrames, | |
250 which must exist at unique times.""" | |
251 if direction == BACKWARD: | |
252 func = last_less_than | |
253 else: | |
254 func = first_greater_than | |
255 | |
256 return func(self.events, key) | |
257 def get_range(self, i, j, direction=FORWARD): | |
258 """Returns all events between i and j, exclusively. If direction | |
259 is FORWARD, j will be included. If direction is BACKWARD, i will | |
260 be included. This is because this is used to find FunctionFrames | |
261 and we assume that any function frames at the start point (which | |
262 could be i or j) have been processed.""" | |
110
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
263 return [e for e in self.events if e >= i and e <= j] |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
264 |
0 | 265 if direction == FORWARD: |
266 return [e for e in self.events if e > i and e <= j] | |
267 else: | |
268 return [e for e in self.events if e >= i and e < j] | |
269 def __getitem__(self, key): | |
270 """Returns the event at or after a specific time key. | |
271 For example: timeline[3] will get the first event at time 3. | |
272 | |
273 If you want to get all events at time 3, you are in trouble, but | |
274 you could achieve it with something like: | |
275 timeline.get_range(2.99, 3.01, FORWARD) | |
276 This is hopefully a bogus problem, since you can't have multiple | |
277 LevelFrames at the same time.""" | |
278 return self.get(key, direction=FORWARD) | |
279 def get_surrounding_frames(self, time): | |
280 """Returns frames before and after a specific time. This returns | |
281 a 2-tuple: (previousframe, nextframe). If you have chosen the exact | |
282 time of a frame, it will be both previousframe and nextframe.""" | |
283 return self.get(time, direction=BACKWARD), \ | |
284 self.get(time, direction=FORWARD) | |
285 def get_levels_at_time(self, time): | |
286 """Returns a LevelFrame with the levels of this track at that time.""" | |
287 before, after = self.get_surrounding_frames(time) | |
288 | |
123 | 289 if not after or before == after: |
290 return {before.frame : before.level} | |
0 | 291 else: # we have a blended value |
292 diff = after.time - before.time | |
293 elapsed = time - before.time | |
294 percent = elapsed / diff | |
295 if not before.next_blender: | |
296 raise MissingBlender, before | |
135 | 297 return before.next_blender(before, after, percent, elapsed) |
0 | 298 |
299 class Timeline: | |
135 | 300 def __init__(self, name, tracks, rate=1, direction=FORWARD): |
0 | 301 """ |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
302 Most/all of this is old: |
0 | 303 |
304 You can have multiple FunctionFrames at the same time. Their | |
305 order is important though, since FunctionFrames will be applied | |
306 in the order seen in this list. blenders is a list of Blenders. | |
307 rate is the rate of playback. If set to 1, 1 unit inside the | |
308 Timeline will be 1 second. direction is the initial direction. | |
309 If you want to do have looping, place a LoopFunction at the end of | |
310 the Timeline. Timelines don't have a set length. Their length | |
311 is bounded by their last frame. You can put an EmptyFrame at | |
312 some time if you want to extend a Timeline.""" | |
313 | |
135 | 314 make_attributes_from_args('name', 'tracks', 'rate', 'direction') |
0 | 315 self.current_time = 0 |
316 self.last_clock_time = None | |
317 self.stopped = 1 | |
318 def length(self): | |
319 """Length of the timeline in pseudoseconds. This is determined by | |
320 finding the length of the longest track.""" | |
321 track_lengths = [track.length() for track in self.tracks] | |
322 return max(track_lengths) | |
323 def play(self): | |
324 """Activates the timeline. Future calls to tick() will advance the | |
325 timeline in the appropriate direction.""" | |
326 self.stopped = 0 | |
327 def stop(self): | |
328 """The timeline will no longer continue in either direction, no | |
329 FunctionFrames will be activated.""" | |
330 self.stopped = 1 | |
331 self.last_clock_time = None | |
332 def reset(self): | |
333 """Resets the timeline to 0. Does not change the stoppedness of the | |
334 timeline.""" | |
335 self.current_time = 0 | |
336 def tick(self): | |
337 """Updates the current_time and runs any FunctionFrames that the cursor | |
338 passed over. This call is ignored if the timeline is stopped.""" | |
339 if self.stopped: | |
340 return | |
341 | |
342 last_time = self.current_time | |
343 last_clock = self.last_clock_time | |
344 | |
345 # first, determine new time | |
346 clock_time = time() | |
347 if last_clock is None: | |
348 last_clock = clock_time | |
349 diff = clock_time - last_clock | |
350 new_time = (self.direction * self.rate * diff) + last_time | |
351 | |
352 # update the time | |
353 self.last_clock_time = clock_time | |
354 self.current_time = new_time | |
355 | |
110
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
356 # now we make sure we're in bounds (we don't do this before, since it |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
357 # can cause us to skip events that are at boundaries. |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
358 self.current_time = max(self.current_time, 0) |
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
359 self.current_time = min(self.current_time, self.length()) |
0 | 360 def reverse_direction(self): |
361 """Reverses the direction of play for this node""" | |
362 self.direction = self.direction * -1 | |
363 def set_direction(self, direction): | |
364 """Sets the direction of playback.""" | |
365 self.direction = direction | |
366 def set_rate(self, new_rate): | |
367 """Sets the rate of playback""" | |
368 self.rate = new_rate | |
369 def set_time(self, new_time): | |
370 """Set the time to a new time.""" | |
371 self.current_time = new_time | |
372 def get_levels(self): | |
373 """Return the current levels from this timeline. This is done by | |
374 adding all the non-functional tracks together.""" | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
375 levels = [t.get_levels_at_time(self.current_time) |
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
376 for t in self.tracks] |
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
377 return dict_max(*levels) |
0 | 378 |
379 if __name__ == '__main__': | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
380 def T(*args, **kw): |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
381 """This used to be a synonym for TimedEvent: |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
382 |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
383 T = TimedEvent |
0 | 384 |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
385 It now acts the same way, except that it will fill in a default |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
386 blender if you don't. The default blender is a LinearBlender.""" |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
387 linear = LinearBlender() |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
388 if 'blender' not in kw: |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
389 kw['blender'] = linear |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
390 |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
391 return TimedEvent(*args, **kw) |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
392 |
0 | 393 quad = ExponentialBlender(2) |
394 invquad = ExponentialBlender(0.5) | |
395 smoove = SmoothBlender() | |
396 | |
123 | 397 track1 = TimelineTrack('red track', |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
398 T(0, 'red', level=0), |
123 | 399 T(4, 'red', blender=quad, level=0.5), |
400 T(12, 'red', blender=smoove, level=0.7), | |
401 T(15, 'red', level=0.0)) # last TimedEvent doesn't need a blender | |
402 track2 = TimelineTrack('green track', | |
403 T(0, 'green', blender=invquad, level=0.2), | |
404 T(5, 'green', blender=smoove, level=1), | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
405 T(10, 'green', level=0.8), |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
406 T(15, 'green', level=0.6), |
123 | 407 T(20, 'green', level=0.0)) # last TimedEvent doesn't need a blender |
408 track3 = TimelineTrack('tableau demo', | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
409 T(0, 'blue', level=0.0), |
123 | 410 T(2, 'blue', level=1.0, blender=InstantEnd()), |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
411 T(18, 'blue', level=1.0), |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
412 T(20, 'blue', level=0.0)) |
0 | 413 |
135 | 414 tl = Timeline('test', [track1, track2, track3]) |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
415 |
0 | 416 tl.play() |
417 | |
418 import Tix | |
419 root = Tix.Tk() | |
420 colorscalesframe = Tix.Frame(root) | |
421 scalevars = {} | |
422 # wow, this works out so well, it's almost like I planned it! | |
423 # (actually, it's probably just Tk being as cool as it usually is) | |
424 # ps. if this code ever turns into mainstream code for flax, I'll be | |
425 # pissed (reason: we need to use classes, not this hacked crap!) | |
426 colors = 'red', 'blue', 'green', 'yellow', 'purple' | |
427 for color in colors: | |
428 sv = Tix.DoubleVar() | |
429 scalevars[color] = sv | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
430 scale = Tix.Scale(colorscalesframe, from_=1, to_=0, res=0.01, bg=color, |
0 | 431 variable=sv) |
432 scale.pack(side=Tix.LEFT) | |
433 | |
434 def set_timeline_time(time): | |
435 tl.set_time(float(time)) | |
436 # print 'set_timeline_time', time | |
437 | |
438 def update_scales(): | |
439 levels = tl.get_levels() | |
440 for color in colors: | |
441 scalevars[color].set(levels.get(color, 0)) | |
442 | |
443 colorscalesframe.pack() | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
444 time_scale = Tix.Scale(root, from_=0, to_=tl.length(), |
0 | 445 orient=Tix.HORIZONTAL, res=0.01, command=set_timeline_time) |
446 time_scale.pack(side=Tix.BOTTOM, fill=Tix.X, expand=1) | |
447 | |
448 def play_tl(): | |
449 tl.tick() | |
450 update_scales() | |
451 time_scale.set(tl.current_time) | |
452 # print 'time_scale.set', tl.current_time | |
453 root.after(10, play_tl) | |
454 | |
455 controlwindow = Tix.Toplevel() | |
456 Tix.Button(controlwindow, text='Stop', | |
457 command=lambda: tl.stop()).pack(side=Tix.LEFT) | |
458 Tix.Button(controlwindow, text='Play', | |
459 command=lambda: tl.play()).pack(side=Tix.LEFT) | |
460 Tix.Button(controlwindow, text='Reset', | |
461 command=lambda: time_scale.set(0)).pack(side=Tix.LEFT) | |
462 Tix.Button(controlwindow, text='Flip directions', | |
463 command=lambda: tl.reverse_direction()).pack(side=Tix.LEFT) | |
464 Tix.Button(controlwindow, text='1/2x', | |
465 command=lambda: tl.set_rate(0.5 * tl.rate)).pack(side=Tix.LEFT) | |
466 Tix.Button(controlwindow, text='2x', | |
467 command=lambda: tl.set_rate(2 * tl.rate)).pack(side=Tix.LEFT) | |
468 | |
469 root.after(100, play_tl) | |
470 | |
471 # Timeline.set_time = trace(Timeline.set_time) | |
472 | |
473 Tix.mainloop() |