Kept needing reversed versions of animations, like a put-down from a pick-up, a reversed reload for an "undo" interaction, that kind of thing. Blender doesn't have a built-in reverse action operator that actually works cleanly, and the manual approach (scale by -1 in the graph editor) messes up your frame range and leaves you with negative frame numbers.
Wrote a script that does it properly. The core idea: remap each keyframe's time from [start, end] to [end, start]. That part is obvious. What's less obvious is that you also have to swap and negate the Bezier handle offsets, otherwise your easing curves end up mirrored wrong and slow-outs become slow-ins.
import bpy
def reverse_action(action, start_frame=None, end_frame=None):
if not action or not action.fcurves:
return
all_times = [kp.co.x for fc in action.fcurves for kp in fc.keyframe_points]
if not all_times:
return
t_start = start_frame if start_frame is not None else min(all_times)
t_end = end_frame if end_frame is not None else max(all_times)
t_range = t_end - t_start
for fcurve in action.fcurves:
for kp in fcurve.keyframe_points:
orig_t = kp.co.x
new_t = t_start + (t_range - (orig_t - t_start))
lh_offset = kp.handle_left.x - orig_t
rh_offset = kp.handle_right.x - orig_t
kp.co.x = new_t
kp.handle_left.x = new_t - rh_offset
kp.handle_right.x = new_t - lh_offset
fcurve.keyframe_points.sort()
fcurve.update()
# Usage
action = bpy.data.actions.get("MyAction")
if action:
reverse_action(action)
A few caveats: modifies the action in-place, so duplicate it first if you want to keep the original. It's purely reversing time, not values, which is almost always what you want. Quaternion rotation channels reverse cleanly with this. Euler channels can develop flips if the original already had discontinuities, but that's a pre-existing problem, not something the script introduces.
Works well enough that I've wired it into my sidebar panel as a one-click button. Curious if anyone's hit edge cases, particularly on rigs that mix Euler and quaternion bones in the same action, which I haven't fully stress-tested.