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