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