Mercurial > code > home > repos > light9
annotate flax/Timeline.py @ 135:5670f66845ce
- results of work from 6.13 rehearsal
author | dmcc |
---|---|
date | Sat, 14 Jun 2003 15:00:47 +0000 |
parents | 8de8a2f467db |
children | 304152488ed7 |
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 | |
135 | 25 MISSING = 'missing' |
26 | |
0 | 27 class TimedEvent: |
28 """Container for a Frame which includes a time that it occurs at, | |
29 and which blender occurs after it.""" | |
135 | 30 def __init__(self, time, frame=MISSING, blender=None, level=1.0): |
0 | 31 make_attributes_from_args('time', 'frame') |
32 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
|
33 self.level = level |
0 | 34 def __float__(self): |
35 return self.time | |
36 def __cmp__(self, other): | |
135 | 37 if other is None: |
38 raise "I can't compare with a None. I am '%s'" % str(self) | |
0 | 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): | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
44 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
|
45 (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
|
46 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
|
47 return self.level |
123 | 48 def __hash__(self): |
49 return id(self.time) ^ id(self.frame) ^ id(self.next_blender) | |
0 | 50 |
51 class Blender: | |
52 """Blenders are functions that merge the effects of two LevelFrames.""" | |
53 def __init__(self): | |
54 pass | |
135 | 55 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 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 | |
135 | 59 between 0.0 and 1.0. time_since_startframe is the time since the |
60 startframe was on screen in seconds (float). | |
0 | 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.""" | |
123 | 82 if startframe.frame == endframe.frame: |
135 | 83 level = startframe.level + (blendtime * \ |
84 (endframe.level - startframe.level)) | |
85 levels = {startframe.frame : level} | |
123 | 86 else: |
87 levels = {startframe.frame : (1.0 - blendtime) * startframe.level, | |
88 endframe.frame : blendtime * endframe.level} | |
89 return levels | |
0 | 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.""" | |
135 | 95 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 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__ | |
123 | 100 return {startframe.frame : startframe.level} |
0 | 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.""" | |
135 | 106 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 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__ | |
123 | 111 return {endframe.frame : endframe.level} |
0 | 112 |
113 class LinearBlender(Blender): | |
114 """Linear fade from one frame to another""" | |
135 | 115 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 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 | |
135 | 124 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 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.""" | |
135 | 133 def __call__(self, startframe, endframe, blendtime, time_since_startframe): |
0 | 134 blendtime = (-1 * blendtime) * blendtime * (blendtime - 1.5) * 2 |
135 return self.linear_blend(startframe, endframe, blendtime) | |
136 | |
135 | 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" | |
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 | |
0 | 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 | |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
155 EmptyTimedEvent (which doesn't exist :-/).""" |
135 | 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 | |
0 | 161 self.name = name |
109 | 162 self.events = list(timedevents) |
0 | 163 self.events.sort() |
135 | 164 self.fill_in_missing_subs() |
165 def fill_in_missing_subs(self): | |
166 for event in self.events: | |
167 if event.frame == MISSING: | |
168 event.frame = self.default_frame | |
0 | 169 def __str__(self): |
170 return "<TimelineTrack with events: %r>" % self.events | |
171 def has_events(self): | |
172 """Whether the TimelineTrack has anything in it. In general, | |
173 empty level Tracks should be avoided. However, empty function tracks | |
174 might be common.""" | |
175 return len(self.events) | |
176 def length(self): | |
177 """Returns the length of this track in pseudosecond time units. | |
178 This is done by finding the position of the last TimedEvent.""" | |
179 return float(self.events[-1]) | |
180 def get(self, key, direction=FORWARD): | |
181 """Returns the event at a specific time key. If there is no event | |
182 at that time, a search will be performed in direction. Also note | |
183 that if there are multiple events at one time, only the first will | |
184 be returned. (Probably first in order of adding.) This is not | |
185 a problem at the present since this method is intended for LevelFrames, | |
186 which must exist at unique times.""" | |
187 if direction == BACKWARD: | |
188 func = last_less_than | |
189 else: | |
190 func = first_greater_than | |
191 | |
192 return func(self.events, key) | |
193 def get_range(self, i, j, direction=FORWARD): | |
194 """Returns all events between i and j, exclusively. If direction | |
195 is FORWARD, j will be included. If direction is BACKWARD, i will | |
196 be included. This is because this is used to find FunctionFrames | |
197 and we assume that any function frames at the start point (which | |
198 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
|
199 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
|
200 |
0 | 201 if direction == FORWARD: |
202 return [e for e in self.events if e > i and e <= j] | |
203 else: | |
204 return [e for e in self.events if e >= i and e < j] | |
205 def __getitem__(self, key): | |
206 """Returns the event at or after a specific time key. | |
207 For example: timeline[3] will get the first event at time 3. | |
208 | |
209 If you want to get all events at time 3, you are in trouble, but | |
210 you could achieve it with something like: | |
211 timeline.get_range(2.99, 3.01, FORWARD) | |
212 This is hopefully a bogus problem, since you can't have multiple | |
213 LevelFrames at the same time.""" | |
214 return self.get(key, direction=FORWARD) | |
215 def get_surrounding_frames(self, time): | |
216 """Returns frames before and after a specific time. This returns | |
217 a 2-tuple: (previousframe, nextframe). If you have chosen the exact | |
218 time of a frame, it will be both previousframe and nextframe.""" | |
219 return self.get(time, direction=BACKWARD), \ | |
220 self.get(time, direction=FORWARD) | |
221 def get_levels_at_time(self, time): | |
222 """Returns a LevelFrame with the levels of this track at that time.""" | |
223 before, after = self.get_surrounding_frames(time) | |
224 | |
123 | 225 if not after or before == after: |
226 return {before.frame : before.level} | |
0 | 227 else: # we have a blended value |
228 diff = after.time - before.time | |
229 elapsed = time - before.time | |
230 percent = elapsed / diff | |
231 if not before.next_blender: | |
232 raise MissingBlender, before | |
135 | 233 return before.next_blender(before, after, percent, elapsed) |
0 | 234 |
235 class Timeline: | |
135 | 236 def __init__(self, name, tracks, rate=1, direction=FORWARD): |
0 | 237 """ |
122
2ed9bfd1dd0e
I totally wrecked Timeline so that it can run the show. (I hope it can
dmcc
parents:
110
diff
changeset
|
238 Most/all of this is old: |
0 | 239 |
240 You can have multiple FunctionFrames at the same time. Their | |
241 order is important though, since FunctionFrames will be applied | |
242 in the order seen in this list. blenders is a list of Blenders. | |
243 rate is the rate of playback. If set to 1, 1 unit inside the | |
244 Timeline will be 1 second. direction is the initial direction. | |
245 If you want to do have looping, place a LoopFunction at the end of | |
246 the Timeline. Timelines don't have a set length. Their length | |
247 is bounded by their last frame. You can put an EmptyFrame at | |
248 some time if you want to extend a Timeline.""" | |
249 | |
135 | 250 make_attributes_from_args('name', 'tracks', 'rate', 'direction') |
0 | 251 self.current_time = 0 |
252 self.last_clock_time = None | |
253 self.stopped = 1 | |
254 def length(self): | |
255 """Length of the timeline in pseudoseconds. This is determined by | |
256 finding the length of the longest track.""" | |
257 track_lengths = [track.length() for track in self.tracks] | |
258 return max(track_lengths) | |
259 def play(self): | |
260 """Activates the timeline. Future calls to tick() will advance the | |
261 timeline in the appropriate direction.""" | |
262 self.stopped = 0 | |
263 def stop(self): | |
264 """The timeline will no longer continue in either direction, no | |
265 FunctionFrames will be activated.""" | |
266 self.stopped = 1 | |
267 self.last_clock_time = None | |
268 def reset(self): | |
269 """Resets the timeline to 0. Does not change the stoppedness of the | |
270 timeline.""" | |
271 self.current_time = 0 | |
272 def tick(self): | |
273 """Updates the current_time and runs any FunctionFrames that the cursor | |
274 passed over. This call is ignored if the timeline is stopped.""" | |
275 if self.stopped: | |
276 return | |
277 | |
278 last_time = self.current_time | |
279 last_clock = self.last_clock_time | |
280 | |
281 # first, determine new time | |
282 clock_time = time() | |
283 if last_clock is None: | |
284 last_clock = clock_time | |
285 diff = clock_time - last_clock | |
286 new_time = (self.direction * self.rate * diff) + last_time | |
287 | |
288 # update the time | |
289 self.last_clock_time = clock_time | |
290 self.current_time = new_time | |
291 | |
110
490843093506
all of this stuff is super rough and not well thought out yet.
dmcc
parents:
109
diff
changeset
|
292 # 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
|
293 # 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
|
294 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
|
295 self.current_time = min(self.current_time, self.length()) |
0 | 296 def reverse_direction(self): |
297 """Reverses the direction of play for this node""" | |
298 self.direction = self.direction * -1 | |
299 def set_direction(self, direction): | |
300 """Sets the direction of playback.""" | |
301 self.direction = direction | |
302 def set_rate(self, new_rate): | |
303 """Sets the rate of playback""" | |
304 self.rate = new_rate | |
305 def set_time(self, new_time): | |
306 """Set the time to a new time.""" | |
307 self.current_time = new_time | |
308 def get_levels(self): | |
309 """Return the current levels from this timeline. This is done by | |
310 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
|
311 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
|
312 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
|
313 return dict_max(*levels) |
0 | 314 |
315 if __name__ == '__main__': | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
316 def T(*args, **kw): |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
317 """This used to be a synonym for TimedEvent: |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
318 |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
319 T = TimedEvent |
0 | 320 |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
321 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
|
322 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
|
323 linear = LinearBlender() |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
324 if 'blender' not in kw: |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
325 kw['blender'] = linear |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
326 |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
327 return TimedEvent(*args, **kw) |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
328 |
0 | 329 quad = ExponentialBlender(2) |
330 invquad = ExponentialBlender(0.5) | |
331 smoove = SmoothBlender() | |
332 | |
123 | 333 track1 = TimelineTrack('red track', |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
334 T(0, 'red', level=0), |
123 | 335 T(4, 'red', blender=quad, level=0.5), |
336 T(12, 'red', blender=smoove, level=0.7), | |
337 T(15, 'red', level=0.0)) # last TimedEvent doesn't need a blender | |
338 track2 = TimelineTrack('green track', | |
339 T(0, 'green', blender=invquad, level=0.2), | |
340 T(5, 'green', blender=smoove, level=1), | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
341 T(10, 'green', level=0.8), |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
342 T(15, 'green', level=0.6), |
123 | 343 T(20, 'green', level=0.0)) # last TimedEvent doesn't need a blender |
344 track3 = TimelineTrack('tableau demo', | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
345 T(0, 'blue', level=0.0), |
123 | 346 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
|
347 T(18, 'blue', level=1.0), |
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
348 T(20, 'blue', level=0.0)) |
0 | 349 |
135 | 350 tl = Timeline('test', [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
|
351 |
0 | 352 tl.play() |
353 | |
354 import Tix | |
355 root = Tix.Tk() | |
356 colorscalesframe = Tix.Frame(root) | |
357 scalevars = {} | |
358 # wow, this works out so well, it's almost like I planned it! | |
359 # (actually, it's probably just Tk being as cool as it usually is) | |
360 # ps. if this code ever turns into mainstream code for flax, I'll be | |
361 # pissed (reason: we need to use classes, not this hacked crap!) | |
362 colors = 'red', 'blue', 'green', 'yellow', 'purple' | |
363 for color in colors: | |
364 sv = Tix.DoubleVar() | |
365 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
|
366 scale = Tix.Scale(colorscalesframe, from_=1, to_=0, res=0.01, bg=color, |
0 | 367 variable=sv) |
368 scale.pack(side=Tix.LEFT) | |
369 | |
370 def set_timeline_time(time): | |
371 tl.set_time(float(time)) | |
372 # print 'set_timeline_time', time | |
373 | |
374 def update_scales(): | |
375 levels = tl.get_levels() | |
376 for color in colors: | |
377 scalevars[color].set(levels.get(color, 0)) | |
378 | |
379 colorscalesframe.pack() | |
124
8de8a2f467db
The "T" function now creates TimedEvents with LinearBlenders for you
dmcc
parents:
123
diff
changeset
|
380 time_scale = Tix.Scale(root, from_=0, to_=tl.length(), |
0 | 381 orient=Tix.HORIZONTAL, res=0.01, command=set_timeline_time) |
382 time_scale.pack(side=Tix.BOTTOM, fill=Tix.X, expand=1) | |
383 | |
384 def play_tl(): | |
385 tl.tick() | |
386 update_scales() | |
387 time_scale.set(tl.current_time) | |
388 # print 'time_scale.set', tl.current_time | |
389 root.after(10, play_tl) | |
390 | |
391 controlwindow = Tix.Toplevel() | |
392 Tix.Button(controlwindow, text='Stop', | |
393 command=lambda: tl.stop()).pack(side=Tix.LEFT) | |
394 Tix.Button(controlwindow, text='Play', | |
395 command=lambda: tl.play()).pack(side=Tix.LEFT) | |
396 Tix.Button(controlwindow, text='Reset', | |
397 command=lambda: time_scale.set(0)).pack(side=Tix.LEFT) | |
398 Tix.Button(controlwindow, text='Flip directions', | |
399 command=lambda: tl.reverse_direction()).pack(side=Tix.LEFT) | |
400 Tix.Button(controlwindow, text='1/2x', | |
401 command=lambda: tl.set_rate(0.5 * tl.rate)).pack(side=Tix.LEFT) | |
402 Tix.Button(controlwindow, text='2x', | |
403 command=lambda: tl.set_rate(2 * tl.rate)).pack(side=Tix.LEFT) | |
404 | |
405 root.after(100, play_tl) | |
406 | |
407 # Timeline.set_time = trace(Timeline.set_time) | |
408 | |
409 Tix.mainloop() |