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