Mercurial > code > home > repos > light9
annotate flax/Timeline.py @ 2405:69ca2b2fc133
overcomplicated attempt at persisting the pane layout in the rdf graph
this was hard because we have to somehow wait for the graph to load before config'ing the panes
author | drewp@bigasterisk.com |
---|---|
date | Fri, 17 May 2024 16:58:26 -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() |