Changeset - ec82e1eea3c8
[Not reviewed]
default
0 1 0
dmcc - 22 years ago 2003-05-25 16:25:35

- initial checkin: run Timeline.py for a good time
1 file changed with 243 insertions and 133 deletions:
0 comments (0 inline, 0 general)
flax/Timeline.py
Show inline comments
 
@@ -4,12 +4,86 @@ from time import time
 
from __future__ import division # "I'm sending you back to future!"
 

	
 
"""
 
Quote of the Build (from Ghostbusters II)
 
Dana:  Okay, but after dinner, I don't want you putting any of your old cheap 
 
       moves on me. 
 
Peter: Ohhhh no! I've got all NEW cheap moves.
 
Changelog:
 
Fri May 16 15:17:34 PDT 2003
 
    Project started (roughly).
 

	
 
Mon May 19 17:56:24 PDT 2003
 
    Timeline is more or less working.  Still bugs with skipping
 
    FunctionFrames at random times.
 

	
 
Timeline idea
 
=============
 
 time    | 0   1   2   3   4   5   6
 
---------+-----------------------------
 
frame    | F       F           F
 
blenders |  \  b /  \- b ----/
 

	
 
Update: this is more or less what happened.  However, there are 
 
TimelineTracks as well.  FunctionFrames go on their own track.
 
LevelFrames must have unique times, FunctionFrames do not.
 

	
 
Level propagation
 
=================
 
Cue1 is a CueNode.  CueNodes consist of a timeline with any number
 
of LevelFrame nodes and LinearBlenders connecting all the frames.
 
At time 0, Cue1's timeline has LevelFrame1.  At time 5, the timeline
 
has NodeFrame1.  NodeFrame1 points to Node1, which has it's own sets
 
of levels.  It could be a Cue, for all Cue1 cares.  No matter what,
 
we always get down to LevelFrames holding the levels at the bottom,
 
then getting combined by Blenders.
 

	
 
             /------\
 
             | Cue1 |                                       
 
             |---------------\
 
             |  Timeline:    |                                     
 
             |  0   ... 5    |                                     
 
     /--------- LF1     NF1 -------\                               
 
     |       |   \      /    |     |                          
 
     |       | LinearBlender |     |                                 
 
     |       \---------------/     |                        
 
     |                          points to                  
 
   /---------------\            a port in Cue1,
 
   | blueleft : 20 |            which connects to a Node
 
   | redwash  : 12 |                                                       
 
   |   .           |                                        
 
   |   :           |    
 
   |               |
 
   \---------------/
 
                                                            
 
PS. blueleft and redwash are other nodes at the bottom of the tree.
 
The include their real channel number for the DimmerNode to process.
 

	
 
When Cue1 requests levels, the timeline looks at the current position.
 
If it is directly over a Frame (or Frames), that frame is handled.
 
If it is LevelFrame, those are the levels that it returns.  If there is
 
a FunctionFrame, that function is activated.  Thus, the order of Frames
 
at a specific time is very significant, since the FunctionFrame could
 
set the time to 5s in the future.  If we are not currently over any 
 
LevelFrames, we call upon a Blender to determine the value between.
 
Say that we are at 2.3s.  We use the LinearBlender with 2.3/5.0s = 0.46%
 
and it determines that the levels are 1 - 0.46% = 0.54% of LF1 and
 
0.46% of NF1.  NF1 asks Node9 for its levels and this process starts
 
all over.
 

	
 
Graph theory issues (node related issues, should be moved elsewhere)
 
====================================================================
 
1.  We need to determine dependencies for updating (topological order).
 
2.  We need to do cyclicity tests.
 

	
 
Guess who wishes they had brought their theory book home?
 
I think we can do both with augmented DFS.  An incremental version of both
 
would be very nice, though hopefully unnecessary.
 

	
 
"""
 

	
 
class InvalidFrameOperation(Exception):
 
    """You get these when you try to perform some operation on a frame
 
    that doesn't make sense.  The interface is advised to tell the user,
 
    and indicate that a Blender or FunctionFramea should be disconnected
 
    or fixed."""
 
    pass
 

	
 
class MissingBlender(Exception):
 
    """Raised when a TimedEvent is missing a blender."""
 
    def __init__(self, timedevent):
 
@@ -22,42 +96,127 @@ class MissingBlender(Exception):
 
FORWARD = 1
 
BACKWARD = -1
 

	
 
MISSING = 'missing'
 
class Frame:
 
    """Frame is an event that happens at a specific time.  There are two
 
    types of frames: LevelFrames and FunctionFrames.  LevelFrames provide
 
    levels via their get_levels() function.  FunctionFrames alter the 
 
    timeline (e.g. bouncing, looping, speed changes, etc.).  They call
 
    __call__'ed instead."""
 
    def __init__(self, name):
 
        self.name = name
 
        self.timeline = DummyClass(use_warnings=0, raise_exceptions=0)
 
    def set_timeline(self, timeline):
 
        """Tell the Frame who the controlling Timeline is"""
 
        self.timeline = timeline
 
    def __mul__(self, percent):
 
        """Generate a new Frame by multiplying the 'effect' of this frame by
 
        a percent."""
 
        raise InvalidFrameOperation, "Can't use multiply this Frame"
 
    def __add__(self, otherframe):
 
        """Combines this frame with another frame, generating a new one."""
 
        raise InvalidFrameOperation, "Can't use add on this Frame"
 

	
 
class LevelFrame(Frame):
 
    """LevelFrames provide levels.  They can also be combined with other
 
    LevelFrames."""
 
    def __init__(self, name, levels):
 
        Frame.__init__(self, name)
 
        self.levels = levels
 
    def __mul__(self, percent):
 
        """Returns a new LevelFrame made by multiplying all levels by a 
 
        percentage.  Percent is a float greater than 0.0"""
 
        newlevels = dict_scale(self.get_levels(), percent)
 
        return LevelFrame('(%s * %f)' % (self.name, percent), newlevels)
 
    def __add__(self, otherframe):
 
        """Combines this LevelFrame with another LevelFrame, generating a new 
 
        one.  Values are max() together."""
 
        theselevels, otherlevels = self.get_levels(), otherframe.get_levels()
 
        return LevelFrame('(%s + %s)' % (self.name, otherframe.name),
 
                          dict_max(theselevels, otherlevels))
 
    def get_levels(self):
 
        """This function returns the levels held by this frame."""
 
        return self.levels
 
    def __repr__(self):
 
        return "<%s %r %r>" % (str(self.__class__), self.name, self.levels)
 

	
 
class EmptyFrame(LevelFrame):
 
    """An empty LevelFrame, for the purposes of extending the timeline."""
 
    def __init__(self, name='Empty Frame'):
 
        EmptyFrame.__init__(self, name, {})
 

	
 
class NodeFrame(LevelFrame):
 
    """A LevelFrame that gets its levels from another Node.  This must be
 
    used from a Timeline that is enclosed in TimelineNode.  Node is a string
 
    describing the node requested."""
 
    def __init__(self, name, node):
 
        LevelFrame.__init__(self, name, {})
 
        self.node = node
 
    def get_levels(self):
 
        """Ask the node that we point to for its levels"""
 
        node = self.timeline.get_node(self.node)
 
        self.levels = node.get_levels()
 
        return self.levels
 

	
 
class FunctionFrame(Frame):
 
    def __init__(self, name):
 
        Frame.__init__(self, name)
 
    def __call__(self, timeline, timedevent, node):
 
        """Called when the FunctionFrame is activated.  It is given a pointer
 
        to it's master timeline, the TimedEvent containing it, and Node that
 
        the timeline is contained in, if available."""
 
        pass
 

	
 
# this is kinda broken
 
class BounceFunction(FunctionFrame):
 
    def __call__(self, timeline, timedevent, node):
 
        """Reverses the direction of play."""
 
        timeline.reverse_direction()
 
        print "boing! new dir:", timeline.direction
 

	
 
# this too
 
class LoopFunction(FunctionFrame):
 
    def __call__(self, timeline, timedevent, node):
 
        timeline.set_time(0)
 
        print 'looped!'
 

	
 
class DoubleTimeFunction(FunctionFrame):
 
    def __call__(self, timeline, timedevent, node):
 
        timeline.set_rate(2 * timeline.rate)
 

	
 
class HalfTimeFunction(FunctionFrame):
 
    def __call__(self, timeline, timedevent, node):
 
        timeline.set_rate(0.5 * timeline.rate)
 

	
 
class TimedEvent:
 
    """Container for a Frame which includes a time that it occurs at,
 
    and which blender occurs after it."""
 
    def __init__(self, time, frame=MISSING, blender=None, level=1.0):
 
    def __init__(self, time, frame, blender=None):
 
        make_attributes_from_args('time', 'frame')
 
        self.next_blender = blender
 
        self.level = level
 
    def __float__(self):
 
        return self.time
 
    def __cmp__(self, other):
 
        if other is None:
 
            raise "I can't compare with a None.  I am '%s'" % str(self)
 
        if type(other) in (float, int):
 
            return cmp(self.time, other)
 
        else:
 
            return cmp(self.time, other.time)
 
    def __repr__(self):
 
        return "<TimedEvent %s at %.2f, time=%.2f, next blender=%s>" % \
 
            (self.frame, self.level, self.time, self.next_blender)
 
    def get_level(self):
 
        return self.level
 
    def __hash__(self):
 
        return id(self.time) ^ id(self.frame) ^ id(self.next_blender)
 
        return "<TimedEvent %s at %.2f, next blender=%s>" % \
 
            (self.frame, self.time, self.next_blender)
 
    def get_levels(self):
 
        """Return the Frame's levels.  Hopefully frame is a LevelFrame or
 
        descendent."""
 
        return self.frame.get_levels()
 

	
 
class Blender:
 
    """Blenders are functions that merge the effects of two LevelFrames."""
 
    def __init__(self):
 
        pass
 
    def __call__(self, startframe, endframe, blendtime,  time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        """Return a LevelFrame combining two LevelFrames (startframe and 
 
        endframe).  blendtime is how much of the blend should be performed
 
        and will be expressed as a percentage divided by 100, i.e. a float
 
        between 0.0 and 1.0.  time_since_startframe is the time since the
 
        startframe was on screen in seconds (float).
 
        between 0.0 and 1.0.  
 
        
 
        Very important note: Blenders will *not* be asked for values
 
        at end points (i.e. blendtime=0.0 and blendtime=1.0).
 
@@ -79,41 +238,35 @@ class Blender:
 
        0.25 * endframe.  This function is included since many blenders are
 
        just functions on the percentage and still combine start and end frames
 
        in this way."""
 
        if startframe.frame == endframe.frame:
 
            level = startframe.level + (blendtime * \
 
                (endframe.level - startframe.level))
 
            levels = {startframe.frame : level}
 
        else:
 
            levels = {startframe.frame : (1.0 - blendtime) * startframe.level,
 
                endframe.frame : blendtime * endframe.level}
 
        return levels
 
        return (startframe * (1.0 - blendtime)) + (endframe * blendtime)
 

	
 
class InstantEnd(Blender):
 
    """Instant change from startframe to endframe at the end.  In other words,
 
    the value returned will be the startframe all the way until the very end
 
    of the blend."""
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        # "What!?" you say, "Why don't you care about blendtime?"
 
        # This is because Blenders never be asked for blenders at the endpoints
 
        # (after all, they wouldn't be blenders if they were). Please see
 
        # 'Very important note' in Blender.__doc__
 
        return {startframe.frame : startframe.level}
 
        return startframe
 

	
 
class InstantStart(Blender):
 
    """Instant change from startframe to endframe at the beginning.  In other
 
    words, the value returned will be the startframe at the very beginning
 
    and then be endframe at all times afterwards."""
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        # "What!?" you say, "Why don't you care about blendtime?"
 
        # This is because Blenders never be asked for blenders at the endpoints
 
        # (after all, they wouldn't be blenders if they were). Please see
 
        # 'Very important note' in Blender.__doc__
 
        return {endframe.frame : endframe.level}
 
        return endframe
 

	
 
class LinearBlender(Blender):
 
    """Linear fade from one frame to another"""
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        return self.linear_blend(startframe, endframe, blendtime)
 
        # return (startframe * (1.0 - blendtime)) + (endframe * blendtime)
 

	
 
class ExponentialBlender(Blender):
 
    """Exponential fade fron one frame to another.  You get to specify
 
@@ -121,7 +274,7 @@ class ExponentialBlender(Blender):
 
    as LinearBlender."""
 
    def __init__(self, exponent):
 
        self.exponent = exponent
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        blendtime = blendtime ** self.exponent
 
        return self.linear_blend(startframe, endframe, blendtime)
 

	
 
@@ -130,70 +283,19 @@ class ExponentialBlender(Blender):
 
class SmoothBlender(Blender):
 
    """Drew's "Smoove" Blender function.  Hopefully he'll document and
 
    parametrize it."""
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
    def __call__(self, startframe, endframe, blendtime):
 
        blendtime = (-1 * blendtime) * blendtime * (blendtime - 1.5) * 2
 
        return self.linear_blend(startframe, endframe, blendtime)
 

	
 
class Strobe(Blender):
 
    "Strobes the frame on the right side between offlevel and onlevel."
 
    def __init__(self, ontime, offtime, onlevel=1, offlevel=0):
 
        "times are in seconds (floats)"
 
        make_attributes_from_args('ontime', 'offtime', 'onlevel', 'offlevel')
 
        self.cycletime = ontime + offtime
 
    def __call__(self, startframe, endframe, blendtime, time_since_startframe):
 
        # time into the current period
 
        period_time = time_since_startframe % self.cycletime
 
        if period_time <= self.ontime:
 
            return {endframe.frame : self.onlevel}
 
        else:
 
            return {endframe.frame : self.offlevel}
 

	
 
class TimelineTrack:
 
    """TimelineTrack is a single track in a Timeline.  It consists of a
 
    list of TimedEvents and a name.  Length is automatically the location
 
    of the last TimedEvent.  To extend the Timeline past that, add an
 
    EmptyTimedEvent (which doesn't exist :-/)."""
 
    def __init__(self, name, *timedevents, **kw):
 
        if kw.get('default_frame'):
 
            self.default_frame = kw['default_frame']
 
        else:
 
            self.default_frame = None
 
    EmptyTimedEvent."""
 
    def __init__(self, name, *timedevents):
 
        self.name = name
 
        self.set_events(list(timedevents))
 
    def set_events(self, events):
 
        """This is given a list of TimedEvents.  They need not be sorted."""
 
        self.events = events
 
        self._cleaup_events()
 
    def _cleaup_events(self):
 
        """This makes sure all events are in the right order and have defaults
 
        filled in if they have missing frames."""
 
        self.events = list(timedevents)
 
        self.events.sort()
 
        self.fill_in_missing_frames()
 
    def add_event(self, event):
 
        """Add a TimedEvent object to this TimelineTrack"""
 
        self.events.append(event)
 
        self._cleaup_events(self.events)
 
    def delete_event(self, event):
 
        """Delete event by TimedEvent object"""
 
        self.events.remove(event)
 
        self._cleaup_events(self.events)
 
    def delete_event_by_name(self, name):
 
        """Deletes all events matching a certain name"""
 
        self.events = [e for e in self.events if e.name is not name]
 
        self._cleaup_events(self.events)
 
    def delete_event_by_time(self, starttime, endtime=None):
 
        """Deletes all events within a certain time range, inclusive.  endtime
 
        is optional."""
 
        endtime = endtime or starttime
 
        self.events = [e for e in self.events
 
            if e.time >= starttime and e.time <= endtime]
 
        self._cleaup_events(self.events)
 
    def fill_in_missing_frames(self):
 
        """Runs through all events and sets TimedEvent with missing frames to
 
        the default frame."""
 
        for event in self.events:
 
            if event.frame == MISSING:
 
                event.frame = self.default_frame
 
    def __str__(self):
 
        return "<TimelineTrack with events: %r>" % self.events
 
    def has_events(self):
 
@@ -224,8 +326,6 @@ class TimelineTrack:
 
        be included.  This is because this is used to find FunctionFrames
 
        and we assume that any function frames at the start point (which
 
        could be i or j) have been processed."""
 
        return [e for e in self.events if e >= i and e <= j]
 

	
 
        if direction == FORWARD:
 
            return [e for e in self.events if e > i and e <= j]
 
        else:
 
@@ -250,20 +350,20 @@ class TimelineTrack:
 
        """Returns a LevelFrame with the levels of this track at that time."""
 
        before, after = self.get_surrounding_frames(time)
 
        
 
        if not after or before == after:
 
            return {before.frame : before.level}
 
        if before == after:
 
            return before.frame
 
        else: # we have a blended value
 
            diff = after.time - before.time
 
            elapsed = time - before.time
 
            percent = elapsed / diff
 
            if not before.next_blender:
 
                raise MissingBlender, before
 
            return before.next_blender(before, after, percent, elapsed)
 
            return before.next_blender(before.frame, after.frame, percent)
 

	
 
class Timeline:
 
    def __init__(self, name, tracks, rate=1, direction=FORWARD):
 
    def __init__(self, tracks, functions, rate=1, direction=FORWARD):
 
        """
 
        Most/all of this is old:
 
        Most of this is old:
 

	
 
        You can have multiple FunctionFrames at the same time.  Their
 
        order is important though, since FunctionFrames will be applied
 
@@ -275,7 +375,10 @@ class Timeline:
 
        is bounded by their last frame.  You can put an EmptyFrame at
 
        some time if you want to extend a Timeline."""
 
        
 
        make_attributes_from_args('name', 'tracks', 'rate', 'direction')
 
        make_attributes_from_args('tracks', 'rate', 'direction')
 
        # the function track is a special track
 
        self.fn_track = TimelineTrack('functions', *functions)
 
        
 
        self.current_time = 0
 
        self.last_clock_time = None
 
        self.stopped = 1
 
@@ -283,6 +386,7 @@ class Timeline:
 
        """Length of the timeline in pseudoseconds.  This is determined by
 
        finding the length of the longest track."""
 
        track_lengths = [track.length() for track in self.tracks]
 
        track_lengths.append(self.fn_track.length())
 
        return max(track_lengths)
 
    def play(self):
 
        """Activates the timeline.  Future calls to tick() will advance the 
 
@@ -312,15 +416,26 @@ class Timeline:
 
            last_clock = clock_time
 
        diff = clock_time - last_clock
 
        new_time = (self.direction * self.rate * diff) + last_time
 
        new_time = max(new_time, 0)
 
        new_time = min(new_time, self.length())
 
        
 
        # update the time
 
        self.last_clock_time = clock_time
 
        self.current_time = new_time
 
        
 
        # now we make sure we're in bounds (we don't do this before, since it
 
        # can cause us to skip events that are at boundaries.
 
        self.current_time = max(self.current_time, 0)
 
        self.current_time = min(self.current_time, self.length())
 
        # now, find out if we missed any functions
 
        if self.fn_track.has_events():
 
            lower_time, higher_time = last_time, new_time
 
            if lower_time > higher_time:
 
                lower_time, higher_time = higher_time, lower_time
 
            
 
            events_to_process = self.fn_track.get_range(lower_time, 
 
                higher_time, self.direction)
 
            
 
            for event in events_to_process:
 
                # they better be FunctionFrames
 
                event.frame(self, event, None) # the None should be a Node, 
 
                                               # but that part is coming later
 
    def reverse_direction(self):
 
        """Reverses the direction of play for this node"""
 
        self.direction = self.direction * -1
 
@@ -336,47 +451,42 @@ class Timeline:
 
    def get_levels(self):
 
        """Return the current levels from this timeline.  This is done by
 
        adding all the non-functional tracks together."""
 
        levels = [t.get_levels_at_time(self.current_time)
 
                    for t in self.tracks]
 
        return dict_max(*levels)
 
        current_level_frame = LevelFrame('timeline sum', {})
 
        for t in self.tracks:
 
            current_level_frame += t.get_levels_at_time(self.current_time)
 

	
 
        return current_level_frame.get_levels()
 

	
 
if __name__ == '__main__':
 
    def T(*args, **kw):
 
        """This used to be a synonym for TimedEvent:
 

	
 
        T = TimedEvent
 
    scene1 = LevelFrame('scene1', {'red' : 50, 'blue' : 25})
 
    scene2 = LevelFrame('scene2', {'red' : 10, 'blue' : 5, 'green' : 70})
 
    scene3 = LevelFrame('scene3', {'yellow' : 10, 'blue' : 80, 'purple' : 70})
 

	
 
        It now acts the same way, except that it will fill in a default 
 
        blender if you don't.  The default blender is a LinearBlender."""
 
        linear = LinearBlender()
 
        if 'blender' not in kw:
 
            kw['blender'] = linear
 
    T = TimedEvent
 

	
 
        return TimedEvent(*args, **kw)
 

	
 
    linear = LinearBlender()
 
    quad = ExponentialBlender(2)
 
    invquad = ExponentialBlender(0.5)
 
    smoove = SmoothBlender()
 

	
 
    track1 = TimelineTrack('red track',
 
        T(0, 'red', level=0),
 
        T(4, 'red', blender=quad, level=0.5),
 
        T(12, 'red', blender=smoove, level=0.7),
 
        T(15, 'red', level=0.0)) # last TimedEvent doesn't need a blender
 
    track2 = TimelineTrack('green track',
 
        T(0, 'green', blender=invquad, level=0.2),
 
        T(5, 'green', blender=smoove, level=1),
 
        T(10, 'green', level=0.8),
 
        T(15, 'green', level=0.6),
 
        T(20, 'green', level=0.0)) # last TimedEvent doesn't need a blender
 
    track3 = TimelineTrack('tableau demo',
 
        T(0, 'blue', level=0.0),
 
        T(2, 'blue', level=1.0, blender=InstantEnd()),
 
        T(18, 'blue', level=1.0),
 
        T(20, 'blue', level=0.0))
 
    track1 = TimelineTrack('lights',
 
        T(0, scene1, blender=linear),
 
        T(5, scene2, blender=quad),
 
        T(10, scene3, blender=smoove),
 
        T(15, scene2)) # last TimedEvent doesn't need a blender
 

	
 
    tl = Timeline('test', [track1, track2, track3])
 

	
 
    if 1:
 
        # bounce is semiworking
 
        bouncer = BounceFunction('boing')
 
        halver = HalfTimeFunction('1/2x')
 
        doubler = DoubleTimeFunction('2x')
 
        tl = Timeline([track1], [T(0, bouncer), 
 
                                 T(0, halver),
 
                                 T(15, bouncer),
 
                                 T(15, doubler)])
 
    else:
 
        looper = LoopFunction('loop1')
 
        tl = Timeline([track1], [T(14, looper)])
 
    tl.play()
 

	
 
    import Tix
 
@@ -391,7 +501,7 @@ if __name__ == '__main__':
 
    for color in colors:
 
        sv = Tix.DoubleVar()
 
        scalevars[color] = sv
 
        scale = Tix.Scale(colorscalesframe, from_=1, to_=0, res=0.01, bg=color,
 
        scale = Tix.Scale(colorscalesframe, from_=100, to_=0, bg=color,
 
            variable=sv)
 
        scale.pack(side=Tix.LEFT)
 

	
 
@@ -405,7 +515,7 @@ if __name__ == '__main__':
 
            scalevars[color].set(levels.get(color, 0))
 
    
 
    colorscalesframe.pack()
 
    time_scale = Tix.Scale(root, from_=0, to_=tl.length(), 
 
    time_scale = Tix.Scale(root, from_=0, to_=track1.length(), 
 
        orient=Tix.HORIZONTAL, res=0.01, command=set_timeline_time)
 
    time_scale.pack(side=Tix.BOTTOM, fill=Tix.X, expand=1)
 

	
0 comments (0 inline, 0 general)