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