Skip to content
Snippets Groups Projects
animation_motion_trail.py 77.2 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 #####
    
    
    
    bl_info = {
    
    Campbell Barton's avatar
    Campbell Barton committed
        "name": "Motion Trail",
        "author": "Bart Crouch",
    
        "version": (3, 1, 3),
    
        "blender": (2, 80, 0),
    
    Campbell Barton's avatar
    Campbell Barton committed
        "location": "View3D > Toolbar > Motion Trail tab",
    
        "warning": "Needs bgl draw update",
    
        "description": "Display and edit motion trails in the 3D View",
    
        "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
                   "Scripts/Animation/Motion_Trail",
    
        "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
    
    
    
    import bgl
    import blf
    import bpy
    from bpy_extras import view3d_utils
    import math
    import mathutils
    
    from bpy.props import (
            BoolProperty,
            EnumProperty,
            FloatProperty,
            IntProperty,
            StringProperty,
            PointerProperty,
            )
    
    
    
    # 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
    
            fcx = fake_fcurve(object, 0)
    
            fcy = fake_fcurve(object, 1)
    
            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.preferences.edit.use_global_undo
            context.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"] is not 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.preferences.edit.use_global_undo = global_undo
    
    
        except:
            # restore global undo in case of failure (see T52524)
    
            context.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
    
                                    '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]
    
                                    '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
    
                                    '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]
    
                                    '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