Mercurial > code > home > repos > light9
comparison flax/Timeline.py @ 122:2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
I totally wrecked Timeline so that it can run the show. (I hope it can
at least do that.) Sleepy...
author | dmcc |
---|---|
date | Fri, 13 Jun 2003 15:06:14 +0000 |
parents | 490843093506 |
children | 41c0ec6cd10a |
comparison
equal
deleted
inserted
replaced
121:2f48cb9219ed | 122:2ed9bfd1dd0e |
---|---|
6 """ | 6 """ |
7 Quote of the Build (from Ghostbusters II) | 7 Quote of the Build (from Ghostbusters II) |
8 Dana: Okay, but after dinner, I don't want you putting any of your old cheap | 8 Dana: Okay, but after dinner, I don't want you putting any of your old cheap |
9 moves on me. | 9 moves on me. |
10 Peter: Ohhhh no! I've got all NEW cheap moves. | 10 Peter: Ohhhh no! I've got all NEW cheap moves. |
11 | |
12 Timeline idea | |
13 ============= | |
14 time | 0 1 2 3 4 5 6 | |
15 ---------+----------------------------- | |
16 frame | F F F | |
17 blenders | \ b / \- b ----/ | |
18 | |
19 Update: this is more or less what happened. However, there are | |
20 TimelineTracks as well. FunctionFrames go on their own track. | |
21 LevelFrames must have unique times, FunctionFrames do not. | |
22 | |
23 Level propagation | |
24 ================= | |
25 Cue1 is a CueNode. CueNodes consist of a timeline with any number | |
26 of LevelFrame nodes and LinearBlenders connecting all the frames. | |
27 At time 0, Cue1's timeline has LevelFrame1. At time 5, the timeline | |
28 has NodeFrame1. NodeFrame1 points to Node1, which has it's own sets | |
29 of levels. It could be a Cue, for all Cue1 cares. No matter what, | |
30 we always get down to LevelFrames holding the levels at the bottom, | |
31 then getting combined by Blenders. | |
32 | |
33 /------\ | |
34 | Cue1 | | |
35 |---------------\ | |
36 | Timeline: | | |
37 | 0 ... 5 | | |
38 /--------- LF1 NF1 -------\ | |
39 | | \ / | | | |
40 | | LinearBlender | | | |
41 | \---------------/ | | |
42 | points to | |
43 /---------------\ a port in Cue1, | |
44 | blueleft : 20 | which connects to a Node | |
45 | redwash : 12 | | |
46 | . | | |
47 | : | | |
48 | | | |
49 \---------------/ | |
50 | |
51 PS. blueleft and redwash are other nodes at the bottom of the tree. | |
52 The include their real channel number for the DimmerNode to process. | |
53 | |
54 When Cue1 requests levels, the timeline looks at the current position. | |
55 If it is directly over a Frame (or Frames), that frame is handled. | |
56 If it is LevelFrame, those are the levels that it returns. If there is | |
57 a FunctionFrame, that function is activated. Thus, the order of Frames | |
58 at a specific time is very significant, since the FunctionFrame could | |
59 set the time to 5s in the future. If we are not currently over any | |
60 LevelFrames, we call upon a Blender to determine the value between. | |
61 Say that we are at 2.3s. We use the LinearBlender with 2.3/5.0s = 0.46% | |
62 and it determines that the levels are 1 - 0.46% = 0.54% of LF1 and | |
63 0.46% of NF1. NF1 asks Node9 for its levels and this process starts | |
64 all over. | |
65 | |
66 Graph theory issues (node related issues, should be moved elsewhere) | |
67 ==================================================================== | |
68 1. We need to determine dependencies for updating (topological order). | |
69 2. We need to do cyclicity tests. | |
70 | |
71 Guess who wishes they had brought their theory book home? | |
72 I think we can do both with augmented DFS. An incremental version of both | |
73 would be very nice, though hopefully unnecessary. | |
74 | |
75 """ | 11 """ |
76 | |
77 class InvalidFrameOperation(Exception): | |
78 """You get these when you try to perform some operation on a frame | |
79 that doesn't make sense. The interface is advised to tell the user, | |
80 and indicate that a Blender or FunctionFramea should be disconnected | |
81 or fixed.""" | |
82 pass | |
83 | 12 |
84 class MissingBlender(Exception): | 13 class MissingBlender(Exception): |
85 """Raised when a TimedEvent is missing a blender.""" | 14 """Raised when a TimedEvent is missing a blender.""" |
86 def __init__(self, timedevent): | 15 def __init__(self, timedevent): |
87 make_attributes_from_args('timedevent') | 16 make_attributes_from_args('timedevent') |
91 # these are chosen so we can multiply by -1 to reverse the direction, | 20 # these are chosen so we can multiply by -1 to reverse the direction, |
92 # and multiply direction by the time difference to determine new times. | 21 # and multiply direction by the time difference to determine new times. |
93 FORWARD = 1 | 22 FORWARD = 1 |
94 BACKWARD = -1 | 23 BACKWARD = -1 |
95 | 24 |
96 class Frame: | |
97 """Frame is an event that happens at a specific time. There are two | |
98 types of frames: LevelFrames and FunctionFrames. LevelFrames provide | |
99 levels via their get_levels() function. FunctionFrames alter the | |
100 timeline (e.g. bouncing, looping, speed changes, etc.). They call | |
101 __call__'ed instead.""" | |
102 def __init__(self, name): | |
103 self.name = name | |
104 self.timeline = DummyClass(use_warnings=0, raise_exceptions=0) | |
105 def set_timeline(self, timeline): | |
106 """Tell the Frame who the controlling Timeline is""" | |
107 self.timeline = timeline | |
108 def __mul__(self, percent): | |
109 """Generate a new Frame by multiplying the 'effect' of this frame by | |
110 a percent.""" | |
111 raise InvalidFrameOperation, "Can't use multiply this Frame" | |
112 def __add__(self, otherframe): | |
113 """Combines this frame with another frame, generating a new one.""" | |
114 raise InvalidFrameOperation, "Can't use add on this Frame" | |
115 | |
116 class LevelFrame(Frame): | |
117 """LevelFrames provide levels. They can also be combined with other | |
118 LevelFrames.""" | |
119 def __init__(self, name, levels): | |
120 Frame.__init__(self, name) | |
121 self.levels = levels | |
122 def __mul__(self, percent): | |
123 """Returns a new LevelFrame made by multiplying all levels by a | |
124 percentage. Percent is a float greater than 0.0""" | |
125 newlevels = dict_scale(self.get_levels(), percent) | |
126 return LevelFrame('(%s * %f)' % (self.name, percent), newlevels) | |
127 def __add__(self, otherframe): | |
128 """Combines this LevelFrame with another LevelFrame, generating a new | |
129 one. Values are max() together.""" | |
130 theselevels, otherlevels = self.get_levels(), otherframe.get_levels() | |
131 return LevelFrame('(%s + %s)' % (self.name, otherframe.name), | |
132 dict_max(theselevels, otherlevels)) | |
133 def get_levels(self): | |
134 """This function returns the levels held by this frame.""" | |
135 return self.levels | |
136 def __repr__(self): | |
137 return "<%s %r %r>" % (str(self.__class__), self.name, self.levels) | |
138 | |
139 class EmptyFrame(LevelFrame): | |
140 """An empty LevelFrame, for the purposes of extending the timeline.""" | |
141 def __init__(self, name='Empty Frame'): | |
142 EmptyFrame.__init__(self, name, {}) | |
143 | |
144 class NodeFrame(LevelFrame): | |
145 """A LevelFrame that gets its levels from another Node. This must be | |
146 used from a Timeline that is enclosed in TimelineNode. Node is a string | |
147 describing the node requested.""" | |
148 def __init__(self, name, node): | |
149 LevelFrame.__init__(self, name, {}) | |
150 self.node = node | |
151 def get_levels(self): | |
152 """Ask the node that we point to for its levels""" | |
153 node = self.timeline.get_node(self.node) | |
154 self.levels = node.get_levels() | |
155 return self.levels | |
156 | |
157 class FunctionFrame(Frame): | |
158 def __init__(self, name): | |
159 Frame.__init__(self, name) | |
160 def __call__(self, timeline, timedevent, node): | |
161 """Called when the FunctionFrame is activated. It is given a pointer | |
162 to it's master timeline, the TimedEvent containing it, and Node that | |
163 the timeline is contained in, if available.""" | |
164 pass | |
165 | |
166 # this is kinda broken | |
167 class BounceFunction(FunctionFrame): | |
168 def __call__(self, timeline, timedevent, node): | |
169 """Reverses the direction of play.""" | |
170 timeline.reverse_direction() | |
171 print "boing! new dir:", timeline.direction | |
172 | |
173 # this too | |
174 class LoopFunction(FunctionFrame): | |
175 def __call__(self, timeline, timedevent, node): | |
176 timeline.set_time(0) | |
177 # print 'looped!' | |
178 | |
179 class DoubleTimeFunction(FunctionFrame): | |
180 def __call__(self, timeline, timedevent, node): | |
181 timeline.set_rate(2 * timeline.rate) | |
182 print 'doubled!', timeline.rate | |
183 | |
184 class HalfTimeFunction(FunctionFrame): | |
185 def __call__(self, timeline, timedevent, node): | |
186 timeline.set_rate(0.5 * timeline.rate) | |
187 print 'halved!', timeline.rate | |
188 | |
189 class TimedEvent: | 25 class TimedEvent: |
190 """Container for a Frame which includes a time that it occurs at, | 26 """Container for a Frame which includes a time that it occurs at, |
191 and which blender occurs after it.""" | 27 and which blender occurs after it.""" |
192 def __init__(self, time, frame, blender=None): | 28 def __init__(self, time, frame, blender=None, level=1.0): |
193 make_attributes_from_args('time', 'frame') | 29 make_attributes_from_args('time', 'frame') |
194 self.next_blender = blender | 30 self.next_blender = blender |
31 self.level = level | |
195 def __float__(self): | 32 def __float__(self): |
196 return self.time | 33 return self.time |
197 def __cmp__(self, other): | 34 def __cmp__(self, other): |
198 if type(other) in (float, int): | 35 if type(other) in (float, int): |
199 return cmp(self.time, other) | 36 return cmp(self.time, other) |
200 else: | 37 else: |
201 return cmp(self.time, other.time) | 38 return cmp(self.time, other.time) |
202 def __repr__(self): | 39 def __repr__(self): |
203 return "<TimedEvent %s at %.2f, next blender=%s>" % \ | 40 return "<TimedEvent %s at %.2f, time=%.2f, next blender=%s>" % \ |
204 (self.frame, self.time, self.next_blender) | 41 (self.frame, self.level, self.time, self.next_blender) |
205 def get_levels(self): | 42 def get_level(self): |
206 """Return the Frame's levels. Hopefully frame is a LevelFrame or | 43 return self.level |
207 descendent.""" | |
208 return self.frame.get_levels() | |
209 | 44 |
210 class Blender: | 45 class Blender: |
211 """Blenders are functions that merge the effects of two LevelFrames.""" | 46 """Blenders are functions that merge the effects of two LevelFrames.""" |
212 def __init__(self): | 47 def __init__(self): |
213 pass | 48 pass |
235 blends. blendtime is the percent/100 that the blend should | 70 blends. blendtime is the percent/100 that the blend should |
236 completed. In other words, 0.25 means it should be 0.75 * startframe + | 71 completed. In other words, 0.25 means it should be 0.75 * startframe + |
237 0.25 * endframe. This function is included since many blenders are | 72 0.25 * endframe. This function is included since many blenders are |
238 just functions on the percentage and still combine start and end frames | 73 just functions on the percentage and still combine start and end frames |
239 in this way.""" | 74 in this way.""" |
240 return (startframe * (1.0 - blendtime)) + (endframe * blendtime) | 75 return {startframe : (1.0 - blendtime), endframe : blendtime} |
241 | 76 |
242 class InstantEnd(Blender): | 77 class InstantEnd(Blender): |
243 """Instant change from startframe to endframe at the end. In other words, | 78 """Instant change from startframe to endframe at the end. In other words, |
244 the value returned will be the startframe all the way until the very end | 79 the value returned will be the startframe all the way until the very end |
245 of the blend.""" | 80 of the blend.""" |
246 def __call__(self, startframe, endframe, blendtime): | 81 def __call__(self, startframe, endframe, blendtime): |
247 # "What!?" you say, "Why don't you care about blendtime?" | 82 # "What!?" you say, "Why don't you care about blendtime?" |
248 # This is because Blenders never be asked for blenders at the endpoints | 83 # This is because Blenders never be asked for blenders at the endpoints |
249 # (after all, they wouldn't be blenders if they were). Please see | 84 # (after all, they wouldn't be blenders if they were). Please see |
250 # 'Very important note' in Blender.__doc__ | 85 # 'Very important note' in Blender.__doc__ |
251 return startframe | 86 return {startframe : 1.0} |
252 | 87 |
253 class InstantStart(Blender): | 88 class InstantStart(Blender): |
254 """Instant change from startframe to endframe at the beginning. In other | 89 """Instant change from startframe to endframe at the beginning. In other |
255 words, the value returned will be the startframe at the very beginning | 90 words, the value returned will be the startframe at the very beginning |
256 and then be endframe at all times afterwards.""" | 91 and then be endframe at all times afterwards.""" |
257 def __call__(self, startframe, endframe, blendtime): | 92 def __call__(self, startframe, endframe, blendtime): |
258 # "What!?" you say, "Why don't you care about blendtime?" | 93 # "What!?" you say, "Why don't you care about blendtime?" |
259 # This is because Blenders never be asked for blenders at the endpoints | 94 # This is because Blenders never be asked for blenders at the endpoints |
260 # (after all, they wouldn't be blenders if they were). Please see | 95 # (after all, they wouldn't be blenders if they were). Please see |
261 # 'Very important note' in Blender.__doc__ | 96 # 'Very important note' in Blender.__doc__ |
262 return endframe | 97 return {endframe : 1.0} |
263 | 98 |
264 class LinearBlender(Blender): | 99 class LinearBlender(Blender): |
265 """Linear fade from one frame to another""" | 100 """Linear fade from one frame to another""" |
266 def __call__(self, startframe, endframe, blendtime): | 101 def __call__(self, startframe, endframe, blendtime): |
267 return self.linear_blend(startframe, endframe, blendtime) | 102 return self.linear_blend(startframe, endframe, blendtime) |
268 # return (startframe * (1.0 - blendtime)) + (endframe * blendtime) | |
269 | 103 |
270 class ExponentialBlender(Blender): | 104 class ExponentialBlender(Blender): |
271 """Exponential fade fron one frame to another. You get to specify | 105 """Exponential fade fron one frame to another. You get to specify |
272 the exponent. If my math is correct, exponent=1 means the same thing | 106 the exponent. If my math is correct, exponent=1 means the same thing |
273 as LinearBlender.""" | 107 as LinearBlender.""" |
288 | 122 |
289 class TimelineTrack: | 123 class TimelineTrack: |
290 """TimelineTrack is a single track in a Timeline. It consists of a | 124 """TimelineTrack is a single track in a Timeline. It consists of a |
291 list of TimedEvents and a name. Length is automatically the location | 125 list of TimedEvents and a name. Length is automatically the location |
292 of the last TimedEvent. To extend the Timeline past that, add an | 126 of the last TimedEvent. To extend the Timeline past that, add an |
293 EmptyTimedEvent.""" | 127 EmptyTimedEvent (which doesn't exist :-/).""" |
294 def __init__(self, name, *timedevents): | 128 def __init__(self, name, *timedevents): |
295 self.name = name | 129 self.name = name |
296 self.events = list(timedevents) | 130 self.events = list(timedevents) |
297 self.events.sort() | 131 self.events.sort() |
298 def __str__(self): | 132 def __str__(self): |
350 def get_levels_at_time(self, time): | 184 def get_levels_at_time(self, time): |
351 """Returns a LevelFrame with the levels of this track at that time.""" | 185 """Returns a LevelFrame with the levels of this track at that time.""" |
352 before, after = self.get_surrounding_frames(time) | 186 before, after = self.get_surrounding_frames(time) |
353 | 187 |
354 if before == after: | 188 if before == after: |
355 return before.frame | 189 return {before.frame : 1.0} |
356 else: # we have a blended value | 190 else: # we have a blended value |
357 diff = after.time - before.time | 191 diff = after.time - before.time |
358 elapsed = time - before.time | 192 elapsed = time - before.time |
359 percent = elapsed / diff | 193 percent = elapsed / diff |
360 if not before.next_blender: | 194 if not before.next_blender: |
361 raise MissingBlender, before | 195 raise MissingBlender, before |
362 return before.next_blender(before.frame, after.frame, percent) | 196 return before.next_blender(before.frame, after.frame, percent) |
363 | 197 |
364 class Timeline: | 198 class Timeline: |
365 def __init__(self, tracks, functions, rate=1, direction=FORWARD): | 199 def __init__(self, tracks, rate=1, direction=FORWARD): |
366 """ | 200 """ |
367 Most of this is old: | 201 Most/all of this is old: |
368 | 202 |
369 You can have multiple FunctionFrames at the same time. Their | 203 You can have multiple FunctionFrames at the same time. Their |
370 order is important though, since FunctionFrames will be applied | 204 order is important though, since FunctionFrames will be applied |
371 in the order seen in this list. blenders is a list of Blenders. | 205 in the order seen in this list. blenders is a list of Blenders. |
372 rate is the rate of playback. If set to 1, 1 unit inside the | 206 rate is the rate of playback. If set to 1, 1 unit inside the |
375 the Timeline. Timelines don't have a set length. Their length | 209 the Timeline. Timelines don't have a set length. Their length |
376 is bounded by their last frame. You can put an EmptyFrame at | 210 is bounded by their last frame. You can put an EmptyFrame at |
377 some time if you want to extend a Timeline.""" | 211 some time if you want to extend a Timeline.""" |
378 | 212 |
379 make_attributes_from_args('tracks', 'rate', 'direction') | 213 make_attributes_from_args('tracks', 'rate', 'direction') |
380 # the function track is a special track | |
381 self.fn_track = TimelineTrack('functions', *functions) | |
382 | |
383 self.current_time = 0 | 214 self.current_time = 0 |
384 self.last_clock_time = None | 215 self.last_clock_time = None |
385 self.stopped = 1 | 216 self.stopped = 1 |
386 def length(self): | 217 def length(self): |
387 """Length of the timeline in pseudoseconds. This is determined by | 218 """Length of the timeline in pseudoseconds. This is determined by |
388 finding the length of the longest track.""" | 219 finding the length of the longest track.""" |
389 track_lengths = [track.length() for track in self.tracks] | 220 track_lengths = [track.length() for track in self.tracks] |
390 track_lengths.append(self.fn_track.length()) | |
391 return max(track_lengths) | 221 return max(track_lengths) |
392 def play(self): | 222 def play(self): |
393 """Activates the timeline. Future calls to tick() will advance the | 223 """Activates the timeline. Future calls to tick() will advance the |
394 timeline in the appropriate direction.""" | 224 timeline in the appropriate direction.""" |
395 self.stopped = 0 | 225 self.stopped = 0 |
420 | 250 |
421 # update the time | 251 # update the time |
422 self.last_clock_time = clock_time | 252 self.last_clock_time = clock_time |
423 self.current_time = new_time | 253 self.current_time = new_time |
424 | 254 |
425 # now, find out if we missed any functions | |
426 if self.fn_track.has_events(): | |
427 lower_time, higher_time = last_time, new_time | |
428 if lower_time == higher_time: print "zarg!" | |
429 if lower_time > higher_time: | |
430 lower_time, higher_time = higher_time, lower_time | |
431 | |
432 events_to_process = self.fn_track.get_range(lower_time, | |
433 higher_time, self.direction) | |
434 | |
435 for event in events_to_process: | |
436 # they better be FunctionFrames | |
437 event.frame(self, event, None) # the None should be a Node, | |
438 # but that part is coming later | |
439 | |
440 # now we make sure we're in bounds (we don't do this before, since it | 255 # now we make sure we're in bounds (we don't do this before, since it |
441 # can cause us to skip events that are at boundaries. | 256 # can cause us to skip events that are at boundaries. |
442 self.current_time = max(self.current_time, 0) | 257 self.current_time = max(self.current_time, 0) |
443 self.current_time = min(self.current_time, self.length()) | 258 self.current_time = min(self.current_time, self.length()) |
444 def reverse_direction(self): | 259 def reverse_direction(self): |
454 """Set the time to a new time.""" | 269 """Set the time to a new time.""" |
455 self.current_time = new_time | 270 self.current_time = new_time |
456 def get_levels(self): | 271 def get_levels(self): |
457 """Return the current levels from this timeline. This is done by | 272 """Return the current levels from this timeline. This is done by |
458 adding all the non-functional tracks together.""" | 273 adding all the non-functional tracks together.""" |
459 current_level_frame = LevelFrame('timeline sum', {}) | 274 levels = [t.get_levels_at_time(self.current_time) |
460 for t in self.tracks: | 275 for t in self.tracks] |
461 current_level_frame += t.get_levels_at_time(self.current_time) | 276 return dict_max(*levels) |
462 | |
463 return current_level_frame.get_levels() | |
464 | 277 |
465 if __name__ == '__main__': | 278 if __name__ == '__main__': |
466 scene1 = LevelFrame('scene1', {'red' : 50, 'blue' : 25}) | |
467 scene2 = LevelFrame('scene2', {'red' : 10, 'blue' : 5, 'green' : 70}) | |
468 scene3 = LevelFrame('scene3', {'yellow' : 10, 'blue' : 80, 'purple' : 70}) | |
469 | |
470 T = TimedEvent | 279 T = TimedEvent |
471 | 280 |
472 linear = LinearBlender() | 281 linear = LinearBlender() |
473 quad = ExponentialBlender(2) | 282 quad = ExponentialBlender(2) |
474 invquad = ExponentialBlender(0.5) | 283 invquad = ExponentialBlender(0.5) |
475 smoove = SmoothBlender() | 284 smoove = SmoothBlender() |
476 | 285 |
477 track1 = TimelineTrack('lights', | 286 track1 = TimelineTrack('lights', |
478 T(0, scene1, blender=linear), | 287 T(0, 'red', blender=linear), |
479 T(5, scene2, blender=quad), | 288 T(5, 'blue', blender=quad), |
480 T(10, scene3, blender=smoove), | 289 T(10, 'red', blender=smoove), |
481 T(15, scene2)) # last TimedEvent doesn't need a blender | 290 T(15, 'blue')) # last TimedEvent doesn't need a blender |
482 | 291 |
483 halver = HalfTimeFunction('1/2x') | 292 tl = Timeline([track1]) |
484 doubler = DoubleTimeFunction('2x') | 293 |
485 if 0: | |
486 # bounce is semiworking | |
487 bouncer = BounceFunction('boing') | |
488 tl = Timeline([track1], [T(0, bouncer), | |
489 T(0, halver), | |
490 T(15, bouncer), | |
491 T(15, doubler)]) | |
492 else: | |
493 looper = LoopFunction('loop1') | |
494 tl = Timeline([track1], [T(0, doubler), | |
495 T(5, halver), | |
496 T(14, looper)]) | |
497 tl.play() | 294 tl.play() |
498 | 295 |
499 import Tix | 296 import Tix |
500 root = Tix.Tk() | 297 root = Tix.Tk() |
501 colorscalesframe = Tix.Frame(root) | 298 colorscalesframe = Tix.Frame(root) |
506 # pissed (reason: we need to use classes, not this hacked crap!) | 303 # pissed (reason: we need to use classes, not this hacked crap!) |
507 colors = 'red', 'blue', 'green', 'yellow', 'purple' | 304 colors = 'red', 'blue', 'green', 'yellow', 'purple' |
508 for color in colors: | 305 for color in colors: |
509 sv = Tix.DoubleVar() | 306 sv = Tix.DoubleVar() |
510 scalevars[color] = sv | 307 scalevars[color] = sv |
511 scale = Tix.Scale(colorscalesframe, from_=100, to_=0, bg=color, | 308 scale = Tix.Scale(colorscalesframe, from_=1, to_=0, res=0.01, bg=color, |
512 variable=sv) | 309 variable=sv) |
513 scale.pack(side=Tix.LEFT) | 310 scale.pack(side=Tix.LEFT) |
514 | 311 |
515 def set_timeline_time(time): | 312 def set_timeline_time(time): |
516 tl.set_time(float(time)) | 313 tl.set_time(float(time)) |