Mercurial > code > home > repos > light9
annotate flax/Timeline.py @ 123:41c0ec6cd10a
bugfixes, tableau demo
author | dmcc |
---|---|
date | Fri, 13 Jun 2003 15:46:53 +0000 |
parents | 2ed9bfd1dd0e |
children | 8de8a2f467db |
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__': | |
109 | 292 T = TimedEvent |
0 | 293 |
109 | 294 linear = LinearBlender() |
0 | 295 quad = ExponentialBlender(2) |
296 invquad = ExponentialBlender(0.5) | |
297 smoove = SmoothBlender() | |
298 | |
123 | 299 track1 = TimelineTrack('red track', |
300 T(0, 'red', blender=linear, level=0), | |
301 T(4, 'red', blender=quad, level=0.5), | |
302 T(12, 'red', blender=smoove, level=0.7), | |
303 T(15, 'red', level=0.0)) # last TimedEvent doesn't need a blender | |
304 track2 = TimelineTrack('green track', | |
305 T(0, 'green', blender=invquad, level=0.2), | |
306 T(5, 'green', blender=smoove, level=1), | |
307 T(10, 'green', blender=linear, level=0.8), | |
308 T(15, 'green', blender=linear, level=0.6), | |
309 T(20, 'green', level=0.0)) # last TimedEvent doesn't need a blender | |
310 track3 = TimelineTrack('tableau demo', | |
311 T(0, 'blue', level=0.0, blender=linear), | |
312 T(2, 'blue', level=1.0, blender=InstantEnd()), | |
313 T(18, 'blue', level=1.0, blender=linear), | |
314 T(20, 'blue', level=0.0, blender=linear)) | |
0 | 315 |
123 | 316 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
|
317 |
0 | 318 tl.play() |
319 | |
320 import Tix | |
321 root = Tix.Tk() | |
322 colorscalesframe = Tix.Frame(root) | |
323 scalevars = {} | |
324 # wow, this works out so well, it's almost like I planned it! | |
325 # (actually, it's probably just Tk being as cool as it usually is) | |
326 # ps. if this code ever turns into mainstream code for flax, I'll be | |
327 # pissed (reason: we need to use classes, not this hacked crap!) | |
328 colors = 'red', 'blue', 'green', 'yellow', 'purple' | |
329 for color in colors: | |
330 sv = Tix.DoubleVar() | |
331 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
|
332 scale = Tix.Scale(colorscalesframe, from_=1, to_=0, res=0.01, bg=color, |
0 | 333 variable=sv) |
334 scale.pack(side=Tix.LEFT) | |
335 | |
336 def set_timeline_time(time): | |
337 tl.set_time(float(time)) | |
338 # print 'set_timeline_time', time | |
339 | |
340 def update_scales(): | |
341 levels = tl.get_levels() | |
342 for color in colors: | |
343 scalevars[color].set(levels.get(color, 0)) | |
344 | |
345 colorscalesframe.pack() | |
109 | 346 time_scale = Tix.Scale(root, from_=0, to_=track1.length(), |
0 | 347 orient=Tix.HORIZONTAL, res=0.01, command=set_timeline_time) |
348 time_scale.pack(side=Tix.BOTTOM, fill=Tix.X, expand=1) | |
349 | |
350 def play_tl(): | |
351 tl.tick() | |
352 update_scales() | |
353 time_scale.set(tl.current_time) | |
354 # print 'time_scale.set', tl.current_time | |
355 root.after(10, play_tl) | |
356 | |
357 controlwindow = Tix.Toplevel() | |
358 Tix.Button(controlwindow, text='Stop', | |
359 command=lambda: tl.stop()).pack(side=Tix.LEFT) | |
360 Tix.Button(controlwindow, text='Play', | |
361 command=lambda: tl.play()).pack(side=Tix.LEFT) | |
362 Tix.Button(controlwindow, text='Reset', | |
363 command=lambda: time_scale.set(0)).pack(side=Tix.LEFT) | |
364 Tix.Button(controlwindow, text='Flip directions', | |
365 command=lambda: tl.reverse_direction()).pack(side=Tix.LEFT) | |
366 Tix.Button(controlwindow, text='1/2x', | |
367 command=lambda: tl.set_rate(0.5 * tl.rate)).pack(side=Tix.LEFT) | |
368 Tix.Button(controlwindow, text='2x', | |
369 command=lambda: tl.set_rate(2 * tl.rate)).pack(side=Tix.LEFT) | |
370 | |
371 root.after(100, play_tl) | |
372 | |
373 # Timeline.set_time = trace(Timeline.set_time) | |
374 | |
375 Tix.mainloop() |