comparison flax/Timeline.py @ 0:45b12307c695

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