Skip to content
Snippets Groups Projects
animation_motion_trail.py 74.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    # <pep8 compliant>
    
    
    bl_info = {
    
    Campbell Barton's avatar
    Campbell Barton committed
        "name": "Motion Trail",
        "author": "Bart Crouch",
    
        "version": (3, 1, 2),
        "blender": (2, 65, 4),
    
    Campbell Barton's avatar
    Campbell Barton committed
        "location": "View3D > Toolbar > Motion Trail tab",
        "warning": "",
    
        "description": "Display and edit motion trails in the 3D View",
        "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
            "Scripts/Animation/Motion_Trail",
        "tracker_url": "https://developer.blender.org/T26374",
    
    Campbell Barton's avatar
    Campbell Barton committed
        "category": "Animation"}
    
    
    
    import bgl
    import blf
    import bpy
    from bpy_extras import view3d_utils
    import math
    import mathutils
    
    
    # fake fcurve class, used if no fcurve is found for a path
    class fake_fcurve():
        def __init__(self, object, index, rotation=False, scale=False):
            # location
            if not rotation and not scale:
                self.loc = object.location[index]
            # scale
            elif scale:
                self.loc = object.scale[index]
            # rotation
            elif rotation == 'QUATERNION':
                self.loc = object.rotation_quaternion[index]
            elif rotation == 'AXIS_ANGLE':
                self.loc = object.rotation_axis_angle[index]
            else:
                self.loc = object.rotation_euler[index]
            self.keyframe_points = []
    
        def evaluate(self, frame):
            return(self.loc)
    
        def range(self):
            return([])
    
    
    # get location curves of the given object
    def get_curves(object, child=False):
        if object.animation_data and object.animation_data.action:
            action = object.animation_data.action
            if child:
                # posebone
                curves = [fc for fc in action.fcurves if len(fc.data_path)>=14 \
                and fc.data_path[-9:]=='.location' and \
                child.name in fc.data_path.split("\"")]
            else:
                # normal object
                curves = [fc for fc in action.fcurves if \
                fc.data_path == 'location']
        elif object.animation_data and object.animation_data.use_nla:
            curves = []
            strips = []
            for track in object.animation_data.nla_tracks:
                not_handled = [s for s in track.strips]
                while not_handled:
                    current_strip = not_handled.pop(-1)
                    if current_strip.action:
                        strips.append(current_strip)
                    if current_strip.strips:
                        # meta strip
                        not_handled += [s for s in current_strip.strips]
    
            for strip in strips:
                if child:
                    # posebone
                    curves = [fc for fc in strip.action.fcurves if \
                    len(fc.data_path)>=14 and fc.data_path[-9:]=='.location' \
                    and child.name in fc.data_path.split("\"")]
                else:
                    # normal object
                    curves = [fc for fc in strip.action.fcurves if \
                    fc.data_path == 'location']
                if curves:
                    # use first strip with location fcurves
                    break
        else:
            # should not happen?
            curves = []
    
        # ensure we have three curves per object
        fcx = None
        fcy = None
        fcz = None
        for fc in curves:
            if fc.array_index == 0:
                fcx = fc
            elif fc.array_index == 1:
                fcy = fc
            elif fc.array_index == 2:
                fcz = fc
        if fcx == None:
            fcx = fake_fcurve(object, 0)
        if fcy == None:
            fcy = fake_fcurve(object, 1)
        if fcz == None:
            fcz = fake_fcurve(object, 2)
    
        return([fcx, fcy, fcz])
    
    
    # turn screen coordinates (x,y) into world coordinates vector
    def screen_to_world(context, x, y):
        depth_vector = view3d_utils.region_2d_to_vector_3d(\
            context.region, context.region_data, [x,y])
        vector = view3d_utils.region_2d_to_location_3d(\
            context.region, context.region_data, [x,y], depth_vector)
    
        return(vector)
    
    
    # turn 3d world coordinates vector into screen coordinate integers (x,y)
    def world_to_screen(context, vector):
        prj = context.region_data.perspective_matrix * \
            mathutils.Vector((vector[0], vector[1], vector[2], 1.0))
        width_half = context.region.width / 2.0
        height_half = context.region.height / 2.0
    
        x = int(width_half + width_half * (prj.x / prj.w))
        y = int(height_half + height_half * (prj.y / prj.w))
    
        # correction for corner cases in perspective mode
        if prj.w < 0:
            if x < 0:
                x = context.region.width * 2
            else:
                x = context.region.width * -2
            if y < 0:
                y = context.region.height * 2
            else:
                y = context.region.height * -2
    
        return(x, y)
    
    
    # calculate location of display_ob in worldspace
    def get_location(frame, display_ob, offset_ob, curves):
        if offset_ob:
            bpy.context.scene.frame_set(frame)
            display_mat = getattr(display_ob, "matrix", False)
            if not display_mat:
                # posebones have "matrix", objects have "matrix_world"
                display_mat = display_ob.matrix_world
            if offset_ob:
                loc = display_mat.to_translation() + \
                    offset_ob.matrix_world.to_translation()
            else:
                loc = display_mat.to_translation()
        else:
            fcx, fcy, fcz = curves
            locx = fcx.evaluate(frame)
            locy = fcy.evaluate(frame)
            locz = fcz.evaluate(frame)
            loc = mathutils.Vector([locx, locy, locz])
    
        return(loc)
    
    
    # get position of keyframes and handles at the start of dragging
    def get_original_animation_data(context, keyframes):
        keyframes_ori = {}
        handles_ori = {}
    
        if context.active_object and context.active_object.mode == 'POSE':
            armature_ob = context.active_object
            objects = [[armature_ob, pb, armature_ob] for pb in \
                context.selected_pose_bones]
        else:
            objects = [[ob, False, False] for ob in context.selected_objects]
    
        for action_ob, child, offset_ob in objects:
            if not action_ob.animation_data:
                continue
            curves = get_curves(action_ob, child)
            if len(curves) == 0:
                continue
            fcx, fcy, fcz = curves
            if child:
                display_ob = child
            else:
                display_ob = action_ob
    
            # get keyframe positions
            frame_old = context.scene.frame_current
            keyframes_ori[display_ob.name] = {}
            for frame in keyframes[display_ob.name]:
                loc = get_location(frame, display_ob, offset_ob, curves)
                keyframes_ori[display_ob.name][frame] = [frame, loc]
    
            # get handle positions
            handles_ori[display_ob.name] = {}
            for frame in keyframes[display_ob.name]:
                handles_ori[display_ob.name][frame] = {}
                left_x = [frame, fcx.evaluate(frame)]
                right_x = [frame, fcx.evaluate(frame)]
                for kf in fcx.keyframe_points:
                    if kf.co[0] == frame:
                        left_x = kf.handle_left[:]
                        right_x = kf.handle_right[:]
                        break
                left_y = [frame, fcy.evaluate(frame)]
                right_y = [frame, fcy.evaluate(frame)]
                for kf in fcy.keyframe_points:
                    if kf.co[0] == frame:
                        left_y = kf.handle_left[:]
                        right_y = kf.handle_right[:]
                        break
                left_z = [frame, fcz.evaluate(frame)]
                right_z = [frame, fcz.evaluate(frame)]
                for kf in fcz.keyframe_points:
                    if kf.co[0] == frame:
                        left_z = kf.handle_left[:]
                        right_z = kf.handle_right[:]
                        break
                handles_ori[display_ob.name][frame]["left"] = [left_x, left_y,
                    left_z]
                handles_ori[display_ob.name][frame]["right"] = [right_x, right_y,
                    right_z]
    
            if context.scene.frame_current != frame_old:
                context.scene.frame_set(frame_old)
    
        return(keyframes_ori, handles_ori)
    
    
    # callback function that calculates positions of all things that need be drawn
    def calc_callback(self, context):
        if context.active_object and context.active_object.mode == 'POSE':
            armature_ob = context.active_object
            objects = [[armature_ob, pb, armature_ob] for pb in \
                context.selected_pose_bones]
        else:
            objects = [[ob, False, False] for ob in context.selected_objects]
        if objects == self.displayed:
            selection_change = False
        else:
            selection_change = True
    
        if self.lock and not selection_change and \
        context.region_data.perspective_matrix == self.perspective and not \
        context.window_manager.motion_trail.force_update:
            return
    
        # dictionaries with key: objectname
    
        self.paths = {} # value: list of lists with x, y, color
    
        self.keyframes = {} # value: dict with frame as key and [x,y] as value
        self.handles = {} # value: dict of dicts
        self.timebeads = {} # value: dict with frame as key and [x,y] as value
        self.click = {} # value: list of lists with frame, type, loc-vector
        if selection_change:
            # value: editbone inverted rotation matrix or None
            self.edit_bones = {}
        if selection_change or not self.lock or context.window_manager.\
        motion_trail.force_update:
            # contains locations of path, keyframes and timebeads
            self.cached = {"path":{}, "keyframes":{}, "timebeads_timing":{},
                "timebeads_speed":{}}
        if self.cached["path"]:
            use_cache = True
        else:
            use_cache = False
        self.perspective = context.region_data.perspective_matrix.copy()
        self.displayed = objects # store, so it can be checked next time
        context.window_manager.motion_trail.force_update = False
    
        global_undo = context.user_preferences.edit.use_global_undo
        context.user_preferences.edit.use_global_undo = False
    
        for action_ob, child, offset_ob in objects:
            if selection_change:
                if not child:
                    self.edit_bones[action_ob.name] = None
                else:
                    bpy.ops.object.mode_set(mode='EDIT')
                    editbones = action_ob.data.edit_bones
                    mat = editbones[child.name].matrix.copy().to_3x3().inverted()
                    bpy.ops.object.mode_set(mode='POSE')
                    self.edit_bones[child.name] = mat
    
            if not action_ob.animation_data:
                continue
            curves = get_curves(action_ob, child)
            if len(curves) == 0:
                continue
    
            if context.window_manager.motion_trail.path_before == 0:
                range_min = context.scene.frame_start
            else:
                range_min = max(context.scene.frame_start,
                    context.scene.frame_current - \
                    context.window_manager.motion_trail.path_before)
            if context.window_manager.motion_trail.path_after == 0:
                range_max = context.scene.frame_end
            else:
                range_max = min(context.scene.frame_end,
                    context.scene.frame_current + \
                    context.window_manager.motion_trail.path_after)
            fcx, fcy, fcz = curves
            if child:
                display_ob = child
            else:
                display_ob = action_ob
    
            # get location data of motion path
            path = []
            speeds = []
            frame_old = context.scene.frame_current
            step = 11 - context.window_manager.motion_trail.path_resolution
    
            if not use_cache:
                if display_ob.name not in self.cached["path"]:
                    self.cached["path"][display_ob.name] = {}
            if use_cache and range_min-1 in self.cached["path"][display_ob.name]:
                prev_loc = self.cached["path"][display_ob.name][range_min-1]
            else:
                prev_loc = get_location(range_min-1, display_ob, offset_ob, curves)
                self.cached["path"][display_ob.name][range_min-1] = prev_loc
    
            for frame in range(range_min, range_max + 1, step):
                if use_cache and frame in self.cached["path"][display_ob.name]:
                    loc = self.cached["path"][display_ob.name][frame]
                else:
                    loc = get_location(frame, display_ob, offset_ob, curves)
                    self.cached["path"][display_ob.name][frame] = loc
                if not context.region or not context.space_data:
                    continue
                x, y = world_to_screen(context, loc)
                if context.window_manager.motion_trail.path_style == 'simple':
                    path.append([x, y, [0.0, 0.0, 0.0], frame, action_ob, child])
                else:
                    dloc = (loc - prev_loc).length
                    path.append([x, y, dloc, frame, action_ob, child])
                    speeds.append(dloc)
                    prev_loc = loc
    
            # calculate color of path
    
            if context.window_manager.motion_trail.path_style == 'speed':
                speeds.sort()
                min_speed = speeds[0]
                d_speed = speeds[-1] - min_speed
                for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
                    relative_speed = (d_loc - min_speed) / d_speed # 0.0 to 1.0
                    red = min(1.0, 2.0 * relative_speed)
                    blue = min(1.0, 2.0 - (2.0 * relative_speed))
                    path[i][2] = [red, 0.0, blue]
            elif context.window_manager.motion_trail.path_style == 'acceleration':
                accelerations = []
                prev_speed = 0.0
                for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
                    accel = d_loc - prev_speed
                    accelerations.append(accel)
                    path[i][2] = accel
                    prev_speed = d_loc
                accelerations.sort()
                min_accel = accelerations[0]
                max_accel = accelerations[-1]
                for i, [x, y, accel, frame, action_ob, child] in enumerate(path):
                    if accel < 0:
                        relative_accel = accel / min_accel # values from 0.0 to 1.0
                        green = 1.0 - relative_accel
                        path[i][2] = [1.0, green, 0.0]
                    elif accel > 0:
                        relative_accel = accel / max_accel # values from 0.0 to 1.0
                        red = 1.0 - relative_accel
                        path[i][2] = [red, 1.0, 0.0]
                    else:
                        path[i][2] = [1.0, 1.0, 0.0]
            self.paths[display_ob.name] = path
    
            # get keyframes and handles
            keyframes = {}
            handle_difs = {}
            kf_time = []
            click = []
            if not use_cache:
                if display_ob.name not in self.cached["keyframes"]:
                    self.cached["keyframes"][display_ob.name] = {}
    
            for fc in curves:
                for kf in fc.keyframe_points:
                    # handles for location mode
                    if context.window_manager.motion_trail.mode == 'location':
                        if kf.co[0] not in handle_difs:
                            handle_difs[kf.co[0]] = {"left":mathutils.Vector(),
                                "right":mathutils.Vector(), "keyframe_loc":None}
                        handle_difs[kf.co[0]]["left"][fc.array_index] = \
                            (mathutils.Vector(kf.handle_left[:]) - \
                            mathutils.Vector(kf.co[:])).normalized()[1]
                        handle_difs[kf.co[0]]["right"][fc.array_index] = \
                            (mathutils.Vector(kf.handle_right[:]) - \
                            mathutils.Vector(kf.co[:])).normalized()[1]
                    # keyframes
                    if kf.co[0] in kf_time:
                        continue
                    kf_time.append(kf.co[0])
                    co = kf.co[0]
    
                    if use_cache and co in \
                    self.cached["keyframes"][display_ob.name]:
                        loc = self.cached["keyframes"][display_ob.name][co]
                    else:
                        loc = get_location(co, display_ob, offset_ob, curves)
                        self.cached["keyframes"][display_ob.name][co] = loc
                    if handle_difs:
                        handle_difs[co]["keyframe_loc"] = loc
    
                    x, y = world_to_screen(context, loc)
                    keyframes[kf.co[0]] = [x, y]
                    if context.window_manager.motion_trail.mode != 'speed':
                        # can't select keyframes in speed mode
                        click.append([kf.co[0], "keyframe",
                            mathutils.Vector([x,y]), action_ob, child])
            self.keyframes[display_ob.name] = keyframes
    
            # handles are only shown in location-altering mode
            if context.window_manager.motion_trail.mode == 'location' and \
            context.window_manager.motion_trail.handle_display:
                # calculate handle positions
                handles = {}
                for frame, vecs in handle_difs.items():
                    if child:
                        # bone space to world space
                        mat = self.edit_bones[child.name].copy().inverted()
                        vec_left = vecs["left"] * mat
                        vec_right = vecs["right"] * mat
                    else:
                        vec_left = vecs["left"]
                        vec_right = vecs["right"]
                    if vecs["keyframe_loc"] != None:
                        vec_keyframe = vecs["keyframe_loc"]
                    else:
                        vec_keyframe = get_location(frame, display_ob, offset_ob,
                            curves)
                    x_left, y_left = world_to_screen(context, vec_left*2 + \
                        vec_keyframe)
                    x_right, y_right = world_to_screen(context, vec_right*2 + \
                        vec_keyframe)
                    handles[frame] = {"left":[x_left, y_left],
                        "right":[x_right, y_right]}
                    click.append([frame, "handle_left",
                        mathutils.Vector([x_left, y_left]), action_ob, child])
                    click.append([frame, "handle_right",
                        mathutils.Vector([x_right, y_right]), action_ob, child])
                self.handles[display_ob.name] = handles
    
            # calculate timebeads for timing mode
            if context.window_manager.motion_trail.mode == 'timing':
                timebeads = {}
                n = context.window_manager.motion_trail.timebeads * (len(kf_time) \
                    - 1)
                dframe = (range_max - range_min) / (n + 1)
                if not use_cache:
                    if display_ob.name not in self.cached["timebeads_timing"]:
                        self.cached["timebeads_timing"][display_ob.name] = {}
    
                for i in range(1, n+1):
                    frame = range_min + i * dframe
                    if use_cache and frame in \
                    self.cached["timebeads_timing"][display_ob.name]:
                        loc = self.cached["timebeads_timing"][display_ob.name]\
                            [frame]
                    else:
                        loc = get_location(frame, display_ob, offset_ob, curves)
                        self.cached["timebeads_timing"][display_ob.name][frame] = \
                            loc
                    x, y = world_to_screen(context, loc)
                    timebeads[frame] = [x, y]
                    click.append([frame, "timebead", mathutils.Vector([x,y]),
                        action_ob, child])
                self.timebeads[display_ob.name] = timebeads
    
            # calculate timebeads for speed mode
            if context.window_manager.motion_trail.mode == 'speed':
                angles = dict([[kf, {"left":[], "right":[]}] for kf in \
                    self.keyframes[display_ob.name]])
                for fc in curves:
                    for i, kf in enumerate(fc.keyframe_points):
                        if i != 0:
                            angle = mathutils.Vector([-1, 0]).angle(mathutils.\
                                Vector(kf.handle_left) - mathutils.Vector(kf.co),
                                0)
                            if angle != 0:
                                angles[kf.co[0]]["left"].append(angle)
                        if i != len(fc.keyframe_points) - 1:
                            angle = mathutils.Vector([1, 0]).angle(mathutils.\
                                Vector(kf.handle_right) - mathutils.Vector(kf.co),
                                0)
                            if angle != 0:
                                angles[kf.co[0]]["right"].append(angle)
                timebeads = {}
                kf_time.sort()
                if not use_cache:
                    if display_ob.name not in self.cached["timebeads_speed"]:
                        self.cached["timebeads_speed"][display_ob.name] = {}
    
                for frame, sides in angles.items():
                    if sides["left"]:
                        perc = (sum(sides["left"]) / len(sides["left"])) / \
                            (math.pi / 2)
                        perc = max(0.4, min(1, perc * 5))
                        previous = kf_time[kf_time.index(frame) - 1]
                        bead_frame = frame - perc * ((frame - previous - 2) / 2)
                        if use_cache and bead_frame in \
                        self.cached["timebeads_speed"][display_ob.name]:
                            loc = self.cached["timebeads_speed"][display_ob.name]\
                                [bead_frame]
                        else:
                            loc = get_location(bead_frame, display_ob, offset_ob,
                                curves)
                            self.cached["timebeads_speed"][display_ob.name]\
                                [bead_frame] = loc
                        x, y = world_to_screen(context, loc)
                        timebeads[bead_frame] = [x, y]
                        click.append([bead_frame, "timebead", mathutils.\
                            Vector([x,y]), action_ob, child])
                    if sides["right"]:
                        perc = (sum(sides["right"]) / len(sides["right"])) / \
                            (math.pi / 2)
                        perc = max(0.4, min(1, perc * 5))
                        next = kf_time[kf_time.index(frame) + 1]
                        bead_frame = frame + perc * ((next - frame - 2) / 2)
                        if use_cache and bead_frame in \
                        self.cached["timebeads_speed"][display_ob.name]:
                            loc = self.cached["timebeads_speed"][display_ob.name]\
                                [bead_frame]
                        else:
                            loc = get_location(bead_frame, display_ob, offset_ob,
                                curves)
                            self.cached["timebeads_speed"][display_ob.name]\
                                [bead_frame] = loc
                        x, y = world_to_screen(context, loc)
                        timebeads[bead_frame] = [x, y]
                        click.append([bead_frame, "timebead", mathutils.\
                            Vector([x,y]), action_ob, child])
                self.timebeads[display_ob.name] = timebeads
    
            # add frame positions to click-list
            if context.window_manager.motion_trail.frame_display:
                path = self.paths[display_ob.name]
    
                for x, y, color, frame, action_ob, child in path:
    
                    click.append([frame, "frame", mathutils.Vector([x,y]),
                        action_ob, child])
    
            self.click[display_ob.name] = click
    
            if context.scene.frame_current != frame_old:
                context.scene.frame_set(frame_old)
    
        context.user_preferences.edit.use_global_undo = global_undo
    
    
    # draw in 3d-view
    def draw_callback(self, context):
        # polling
    
        if (context.mode not in ('OBJECT', 'POSE') or
            not context.window_manager.motion_trail.enabled):
    
            return
    
        # display limits
        if context.window_manager.motion_trail.path_before != 0:
            limit_min = context.scene.frame_current - \
                context.window_manager.motion_trail.path_before
        else:
            limit_min = -1e6
        if context.window_manager.motion_trail.path_after != 0:
            limit_max = context.scene.frame_current + \
                context.window_manager.motion_trail.path_after
        else:
            limit_max = 1e6
    
        # draw motion path
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glLineWidth(context.window_manager.motion_trail.path_width)
        alpha = 1.0 - (context.window_manager.motion_trail.path_transparency / \
            100.0)
        if context.window_manager.motion_trail.path_style == 'simple':
            bgl.glColor4f(0.0, 0.0, 0.0, alpha)
            for objectname, path in self.paths.items():
                bgl.glBegin(bgl.GL_LINE_STRIP)
    
                for x, y, color, frame, action_ob, child in path:
    
                    if frame < limit_min or frame > limit_max:
                        continue
                    bgl.glVertex2i(x, y)
                bgl.glEnd()
        else:
            for objectname, path in self.paths.items():
    
                for i, [x, y, color, frame, action_ob, child] in enumerate(path):
    
                    if frame < limit_min or frame > limit_max:
                        continue
    
                    r, g, b = color
    
                    if i != 0:
                        prev_path = path[i-1]
                        halfway = [(x + prev_path[0])/2, (y + prev_path[1])/2]
                        bgl.glColor4f(r, g, b, alpha)
                        bgl.glBegin(bgl.GL_LINE_STRIP)
                        bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
                        bgl.glVertex2i(x, y)
                        bgl.glEnd()
                    if i != len(path) - 1:
                        next_path = path[i+1]
                        halfway = [(x + next_path[0])/2, (y + next_path[1])/2]
                        bgl.glColor4f(r, g, b, alpha)
                        bgl.glBegin(bgl.GL_LINE_STRIP)
                        bgl.glVertex2i(x, y)
                        bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
                        bgl.glEnd()
    
        # draw frames
        if context.window_manager.motion_trail.frame_display:
            bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
            bgl.glPointSize(1)
            bgl.glBegin(bgl.GL_POINTS)
            for objectname, path in self.paths.items():
    
                for x, y, color, frame, action_ob, child in path:
    
                    if frame < limit_min or frame > limit_max:
                        continue
                    if self.active_frame and objectname == self.active_frame[0] \
                    and abs(frame - self.active_frame[1]) < 1e-4:
                        bgl.glEnd()
                        bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
                        bgl.glPointSize(3)
                        bgl.glBegin(bgl.GL_POINTS)
                        bgl.glVertex2i(x,y)
                        bgl.glEnd()
                        bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
                        bgl.glPointSize(1)
                        bgl.glBegin(bgl.GL_POINTS)
                    else:
                        bgl.glVertex2i(x,y)
            bgl.glEnd()
    
        # time beads are shown in speed and timing modes
    
        if context.window_manager.motion_trail.mode in ('speed', 'timing'):
    
            bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
            bgl.glPointSize(4)
            bgl.glBegin(bgl.GL_POINTS)
            for objectname, values in self.timebeads.items():
                for frame, coords in values.items():
                    if frame < limit_min or frame > limit_max:
                        continue
                    if self.active_timebead and \
                    objectname == self.active_timebead[0] and \
                    abs(frame - self.active_timebead[1]) < 1e-4:
                        bgl.glEnd()
                        bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
                        bgl.glBegin(bgl.GL_POINTS)
                        bgl.glVertex2i(coords[0], coords[1])
                        bgl.glEnd()
                        bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
                        bgl.glBegin(bgl.GL_POINTS)
                    else:
                        bgl.glVertex2i(coords[0], coords[1])
            bgl.glEnd()
    
        # handles are only shown in location mode
        if context.window_manager.motion_trail.mode == 'location':
            # draw handle-lines
            bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
            bgl.glLineWidth(1)
            bgl.glBegin(bgl.GL_LINES)
            for objectname, values in self.handles.items():
                for frame, sides in values.items():
                    if frame < limit_min or frame > limit_max:
                        continue
                    for side, coords in sides.items():
                        if self.active_handle and \
                        objectname == self.active_handle[0] and \
                        side == self.active_handle[2] and \
                        abs(frame - self.active_handle[1]) < 1e-4:
                            bgl.glEnd()
                            bgl.glColor4f(.75, 0.25, 0.0, 1.0)
                            bgl.glBegin(bgl.GL_LINES)
                            bgl.glVertex2i(self.keyframes[objectname][frame][0],
                                self.keyframes[objectname][frame][1])
                            bgl.glVertex2i(coords[0], coords[1])
                            bgl.glEnd()
                            bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
                            bgl.glBegin(bgl.GL_LINES)
                        else:
                            bgl.glVertex2i(self.keyframes[objectname][frame][0],
                                self.keyframes[objectname][frame][1])
                            bgl.glVertex2i(coords[0], coords[1])
            bgl.glEnd()
    
            # draw handles
            bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
            bgl.glPointSize(4)
            bgl.glBegin(bgl.GL_POINTS)
            for objectname, values in self.handles.items():
                for frame, sides in values.items():
                    if frame < limit_min or frame > limit_max:
                        continue
                    for side, coords in sides.items():
                        if self.active_handle and \
                        objectname == self.active_handle[0] and \
                        side == self.active_handle[2] and \
                        abs(frame - self.active_handle[1]) < 1e-4:
                            bgl.glEnd()
                            bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
                            bgl.glBegin(bgl.GL_POINTS)
                            bgl.glVertex2i(coords[0], coords[1])
                            bgl.glEnd()
                            bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
                            bgl.glBegin(bgl.GL_POINTS)
                        else:
                            bgl.glVertex2i(coords[0], coords[1])
            bgl.glEnd()
    
        # draw keyframes
        bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
        bgl.glPointSize(6)
        bgl.glBegin(bgl.GL_POINTS)
        for objectname, values in self.keyframes.items():
            for frame, coords in values.items():
                if frame < limit_min or frame > limit_max:
                    continue
                if self.active_keyframe and \
                objectname == self.active_keyframe[0] and \
                abs(frame - self.active_keyframe[1]) < 1e-4:
                    bgl.glEnd()
                    bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
                    bgl.glBegin(bgl.GL_POINTS)
                    bgl.glVertex2i(coords[0], coords[1])
                    bgl.glEnd()
                    bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
                    bgl.glBegin(bgl.GL_POINTS)
                else:
                    bgl.glVertex2i(coords[0], coords[1])
        bgl.glEnd()
    
        # draw keyframe-numbers
        if context.window_manager.motion_trail.keyframe_numbers:
            blf.size(0, 12, 72)
            bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
            for objectname, values in self.keyframes.items():
                for frame, coords in values.items():
                    if frame < limit_min or frame > limit_max:
                        continue
                    blf.position(0, coords[0] + 3, coords[1] + 3, 0)
                    text = str(frame).split(".")
                    if len(text) == 1:
                        text = text[0]
                    elif len(text[1]) == 1 and text[1] == "0":
                        text = text[0]
                    else:
                        text = text[0] + "." + text[1][0]
                    if self.active_keyframe and \
                    objectname == self.active_keyframe[0] and \
                    abs(frame - self.active_keyframe[1]) < 1e-4:
                        bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
                        blf.draw(0, text)
                        bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
                    else:
                        blf.draw(0, text)
    
        # restore opengl defaults
        bgl.glLineWidth(1)
        bgl.glDisable(bgl.GL_BLEND)
        bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
        bgl.glPointSize(1)
    
    
    # change data based on mouse movement
    def drag(context, event, drag_mouse_ori, active_keyframe, active_handle,
    active_timebead, keyframes_ori, handles_ori, edit_bones):
        # change 3d-location of keyframe
        if context.window_manager.motion_trail.mode == 'location' and \
        active_keyframe:
            objectname, frame, frame_ori, action_ob, child = active_keyframe
            if child:
                mat = action_ob.matrix_world.copy().inverted() * \
                    edit_bones[child.name].copy().to_4x4()
            else:
                mat = 1
    
            mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
                drag_mouse_ori[1]) * mat
            vector = screen_to_world(context, event.mouse_region_x,
                event.mouse_region_y) * mat
            d = vector - mouse_ori_world
    
            loc_ori_ws = keyframes_ori[objectname][frame][1]
            loc_ori_bs = loc_ori_ws * mat
            new_loc = loc_ori_bs + d
            curves = get_curves(action_ob, child)
    
            for i, curve in enumerate(curves):
                for kf in curve.keyframe_points:
                    if kf.co[0] == frame:
                        kf.co[1] = new_loc[i]
                        kf.handle_left[1] = handles_ori[objectname][frame]\
                            ["left"][i][1] + d[i]
                        kf.handle_right[1] = handles_ori[objectname][frame]\
                            ["right"][i][1] + d[i]
                        break
    
        # change 3d-location of handle
        elif context.window_manager.motion_trail.mode == 'location' and \
        active_handle:
            objectname, frame, side, action_ob, child = active_handle
            if child:
                mat = action_ob.matrix_world.copy().inverted() * \
                    edit_bones[child.name].copy().to_4x4()
            else:
                mat = 1
    
            mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
                drag_mouse_ori[1]) * mat
            vector = screen_to_world(context, event.mouse_region_x,
                event.mouse_region_y) * mat
            d = vector - mouse_ori_world
            curves = get_curves(action_ob, child)
    
            for i, curve in enumerate(curves):
                for kf in curve.keyframe_points:
                    if kf.co[0] == frame:
                        if side == "left":
                            # change handle type, if necessary
    
                            if kf.handle_left_type in (
                                'AUTO',
                                'AUTO_CLAMPED',
                                'ANIM_CLAMPED'):
    
                                kf.handle_left_type = 'ALIGNED'
                            elif kf.handle_left_type == 'VECTOR':
                                kf.handle_left_type = 'FREE'
                            # change handle position(s)
                            kf.handle_left[1] = handles_ori[objectname][frame]\
                                ["left"][i][1] + d[i]
    
                            if kf.handle_left_type in (
                                'ALIGNED',
                                'ANIM_CLAMPED',
                                'AUTO',
                                'AUTO_CLAMPED'):
    
                                dif = (abs(handles_ori[objectname][frame]["right"]\
                                    [i][0] - kf.co[0]) / abs(kf.handle_left[0] - \
                                    kf.co[0])) * d[i]
                                kf.handle_right[1] = handles_ori[objectname]\
                                    [frame]["right"][i][1] - dif
                        elif side == "right":
                            # change handle type, if necessary
    
                            if kf.handle_right_type in (
                                'AUTO',
                                'AUTO_CLAMPED',
                                'ANIM_CLAMPED'):
    
                                kf.handle_left_type = 'ALIGNED'
                                kf.handle_right_type = 'ALIGNED'
                            elif kf.handle_right_type == 'VECTOR':
                                kf.handle_left_type = 'FREE'
                                kf.handle_right_type = 'FREE'
                            # change handle position(s)
                            kf.handle_right[1] = handles_ori[objectname][frame]\
                                ["right"][i][1] + d[i]
    
                            if kf.handle_right_type in (
                                'ALIGNED',
                                'ANIM_CLAMPED',
                                'AUTO',
                                'AUTO_CLAMPED'):
    
                                dif = (abs(handles_ori[objectname][frame]["left"]\
                                    [i][0] - kf.co[0]) / abs(kf.handle_right[0] - \
                                    kf.co[0])) * d[i]
                                kf.handle_left[1] = handles_ori[objectname]\
                                    [frame]["left"][i][1] - dif
                        break
    
        # change position of all keyframes on timeline
        elif context.window_manager.motion_trail.mode == 'timing' and \
        active_timebead:
            objectname, frame, frame_ori, action_ob, child = active_timebead
            curves = get_curves(action_ob, child)
            ranges = [val for c in curves for val in c.range()]
            ranges.sort()
            range_min = round(ranges[0])
            range_max = round(ranges[-1])
            range = range_max - range_min
            dx_screen = -(mathutils.Vector([event.mouse_region_x,
                event.mouse_region_y]) - drag_mouse_ori)[0]
            dx_screen = dx_screen / context.region.width * range
            new_frame = frame + dx_screen
            shift_low = max(1e-4, (new_frame - range_min) / (frame - range_min))
            shift_high = max(1e-4, (range_max - new_frame) / (range_max - frame))
    
            new_mapping = {}
            for i, curve in enumerate(curves):
                for j, kf in enumerate(curve.keyframe_points):
                    frame_map = kf.co[0]
                    if frame_map < range_min + 1e-4 or \
                    frame_map > range_max - 1e-4:
                        continue
                    frame_ori = False
                    for f in keyframes_ori[objectname]:
                        if abs(f - frame_map) < 1e-4:
                            frame_ori = keyframes_ori[objectname][f][0]
                            value_ori = keyframes_ori[objectname][f]
                            break
                    if not frame_ori:
                        continue
                    if frame_ori <= frame:
                        frame_new = (frame_ori - range_min) * shift_low + \
                            range_min
                    else:
                        frame_new = range_max - (range_max - frame_ori) * \
                            shift_high
                    frame_new = max(range_min + j, min(frame_new, range_max - \
                        (len(curve.keyframe_points)-j)+1))
                    d_frame = frame_new - frame_ori
                    if frame_new not in new_mapping:
                        new_mapping[frame_new] = value_ori
                    kf.co[0] = frame_new
                    kf.handle_left[0] = handles_ori[objectname][frame_ori]\
                        ["left"][i][0] + d_frame
                    kf.handle_right[0] = handles_ori[objectname][frame_ori]\
                        ["right"][i][0] + d_frame
            del keyframes_ori[objectname]
            keyframes_ori[objectname] = {}
            for new_frame, value in new_mapping.items():
                keyframes_ori[objectname][new_frame] = value
    
        # change position of active keyframe on the timeline
        elif context.window_manager.motion_trail.mode == 'timing' and \
        active_keyframe:
            objectname, frame, frame_ori, action_ob, child = active_keyframe
            if child:
                mat = action_ob.matrix_world.copy().inverted() * \
                    edit_bones[child.name].copy().to_4x4()
            else:
                mat = action_ob.matrix_world.copy().inverted()
    
            mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
                drag_mouse_ori[1]) * mat
            vector = screen_to_world(context, event.mouse_region_x,
                event.mouse_region_y) * mat
            d = vector - mouse_ori_world
    
            locs_ori = [[f_ori, coords] for f_mapped, [f_ori, coords] in \
                keyframes_ori[objectname].items()]
            locs_ori.sort()
            direction = 1
            range = False
            for i, [f_ori, coords] in enumerate(locs_ori):
                if abs(frame_ori - f_ori) < 1e-4:
                    if i == 0:
                        # first keyframe, nothing before it
                        direction = -1
                        range = [f_ori, locs_ori[i+1][0]]
                    elif i == len(locs_ori) - 1:
                        # last keyframe, nothing after it
                        range = [locs_ori[i-1][0], f_ori]
                    else:
                        current = mathutils.Vector(coords)
                        next = mathutils.Vector(locs_ori[i+1][1])
                        previous = mathutils.Vector(locs_ori[i-1][1])
                        angle_to_next = d.angle(next - current, 0)
                        angle_to_previous = d.angle(previous-current, 0)
                        if angle_to_previous < angle_to_next:
                            # mouse movement is in direction of previous keyframe
                            direction = -1
                        range = [locs_ori[i-1][0], locs_ori[i+1][0]]
                    break
            direction *= -1 # feels more natural in 3d-view
            if not range:
                # keyframe not found, is impossible, but better safe than sorry
                return(active_keyframe, active_timebead, keyframes_ori)
            # calculate strength of movement
            d_screen = mathutils.Vector([event.mouse_region_x,
                event.mouse_region_y]) - drag_mouse_ori
            if d_screen.length != 0:
                d_screen = d_screen.length / (abs(d_screen[0])/d_screen.length*\
                    context.region.width + abs(d_screen[1])/d_screen.length*\
                    context.region.height)