VSE: Add operators to add and remove fades
Fades add: Adds or updates a fade animation for either visual or audio strips. Fade options: - In, Out, In and Out create a fade animation of the given duration from the start of the sequence, to the end of the sequence, or on boths sides - From playhead: the fade animation goes from the start of sequences under the playhead to the playhead - To playhead: the fade animation goes from the playhead to the end of sequences under the playhead By default, the duration of the fade is 1 second. Fades clear: Removes fade animation from selected sequences. Removes opacity or volume animation on selected sequences and resets the property to a value of 1.0. Works on all types of sequences. Author: gdquest Reviewed By: ISS Differential Revision: https://developer.blender.org/D5166
This commit is contained in:
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
|
from mathutils import Vector
|
||||||
|
from math import floor
|
||||||
|
|
||||||
from bpy.props import IntProperty
|
from bpy.props import IntProperty
|
||||||
|
|
||||||
@@ -136,8 +138,240 @@ class SequencerDeinterlaceSelectedMovies(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class SequencerFadesClear(Operator):
|
||||||
|
"""Removes fade animation from selected sequences.
|
||||||
|
Removes opacity or volume animation on selected sequences and resets the
|
||||||
|
property to a value of 1.0. Works on all types of sequences.
|
||||||
|
"""
|
||||||
|
bl_idname = "sequencer.fades_clear"
|
||||||
|
bl_label = "Clear Fades"
|
||||||
|
bl_description = "Removes fade animation from selected sequences."
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
fcurves = context.scene.animation_data.action.fcurves
|
||||||
|
|
||||||
|
for sequence in context.selected_sequences:
|
||||||
|
animated_property = 'volume' if hasattr(sequence, 'volume') else 'blend_alpha'
|
||||||
|
for curve in fcurves:
|
||||||
|
if not curve.data_path.endswith(animated_property):
|
||||||
|
continue
|
||||||
|
# Ensure the fcurve corresponds to the selected sequence
|
||||||
|
if sequence == eval("bpy.context.scene." + curve.data_path.replace('.' + animated_property, '')):
|
||||||
|
fcurves.remove(curve)
|
||||||
|
setattr(sequence, animated_property, 1.0)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class SequencerFadesAdd(Operator):
|
||||||
|
"""Adds or updates a fade animation for either visual or audio strips.
|
||||||
|
Fade options:
|
||||||
|
- In, Out, In and Out create a fade animation of the given duration from
|
||||||
|
the start of the sequence, to the end of the sequence, or on boths sides
|
||||||
|
- From playhead: the fade animation goes from the start of sequences under the playhead to the playhead
|
||||||
|
- To playhead: the fade animation goes from the playhead to the end of sequences under the playhead
|
||||||
|
By default, the duration of the fade is 1 second.
|
||||||
|
"""
|
||||||
|
bl_idname = "sequencer.fades_add"
|
||||||
|
bl_label = "Add Fades"
|
||||||
|
bl_description = "Adds or updates a fade animation for either visual or audio strips."
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
duration_seconds: bpy.props.FloatProperty(
|
||||||
|
name="Fade Duration",
|
||||||
|
description="Duration of the fade in seconds",
|
||||||
|
default=1.0,
|
||||||
|
min=0.01)
|
||||||
|
type: bpy.props.EnumProperty(
|
||||||
|
items=[('IN_OUT', 'Fade In And Out', 'Fade selected strips in and out'),
|
||||||
|
('IN', 'Fade In', 'Fade in selected strips'),
|
||||||
|
('OUT', 'Fade Out', 'Fade out selected strips'),
|
||||||
|
('CURSOR_FROM', 'From Playhead', 'Fade from the time cursor to the end of overlapping sequences'),
|
||||||
|
('CURSOR_TO', 'To Playhead', 'Fade from the start of sequences under the time cursor to the current frame')],
|
||||||
|
name="Fade type",
|
||||||
|
description="Fade in, out, both in and out, to, or from the playhead. Default is both in and out.",
|
||||||
|
default='IN_OUT')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
# Can't use context.selected_sequences as it can have an impact on performances
|
||||||
|
return context.scene and context.scene.sequence_editor and context.scene.sequence_editor.active_strip
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# We must create a scene action first if there's none
|
||||||
|
scene = context.scene
|
||||||
|
if not scene.animation_data:
|
||||||
|
scene.animation_data_create()
|
||||||
|
if not scene.animation_data.action:
|
||||||
|
action = bpy.data.actions.new(scene.name + "Action")
|
||||||
|
scene.animation_data.action = action
|
||||||
|
|
||||||
|
sequences = context.selected_sequences
|
||||||
|
if self.type in ['CURSOR_TO', 'CURSOR_FROM']:
|
||||||
|
sequences = [s for s in sequences
|
||||||
|
if s.frame_final_start < context.scene.frame_current < s.frame_final_end]
|
||||||
|
|
||||||
|
max_duration = min(sequences, key=lambda s: s.frame_final_duration).frame_final_duration
|
||||||
|
max_duration = floor(max_duration / 2.0) if self.type == 'IN_OUT' else max_duration
|
||||||
|
|
||||||
|
faded_sequences = []
|
||||||
|
for sequence in sequences:
|
||||||
|
duration = self.calculate_fade_duration(context, sequence)
|
||||||
|
duration = min(duration, max_duration)
|
||||||
|
if not self.is_long_enough(sequence, duration):
|
||||||
|
continue
|
||||||
|
|
||||||
|
animated_property = 'volume' if hasattr(sequence, 'volume') else 'blend_alpha'
|
||||||
|
fade_fcurve = self.fade_find_or_create_fcurve(context, sequence, animated_property)
|
||||||
|
fades = self.calculate_fades(sequence, fade_fcurve, animated_property, duration)
|
||||||
|
self.fade_animation_clear(context, fade_fcurve, fades)
|
||||||
|
self.fade_animation_create(fade_fcurve, fades)
|
||||||
|
faded_sequences.append(sequence)
|
||||||
|
|
||||||
|
sequence_string = "sequence" if len(faded_sequences) == 1 else "sequences"
|
||||||
|
self.report({"INFO"}, "Added fade animation to {} {}.".format(len(faded_sequences), sequence_string))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def calculate_fade_duration(self, context, sequence):
|
||||||
|
frame_current = context.scene.frame_current
|
||||||
|
duration = 0.0
|
||||||
|
if self.type == 'CURSOR_TO':
|
||||||
|
duration = abs(frame_current - sequence.frame_final_start)
|
||||||
|
elif self.type == 'CURSOR_FROM':
|
||||||
|
duration = abs(sequence.frame_final_end - frame_current)
|
||||||
|
else:
|
||||||
|
duration = calculate_duration_frames(context, self.duration_seconds)
|
||||||
|
return max(1, duration)
|
||||||
|
|
||||||
|
def is_long_enough(self, sequence, duration=0.0):
|
||||||
|
minimum_duration = (duration * 2
|
||||||
|
if self.type == 'IN_OUT' else
|
||||||
|
duration)
|
||||||
|
return sequence.frame_final_duration >= minimum_duration
|
||||||
|
|
||||||
|
def calculate_fades(self, sequence, fade_fcurve, animated_property, duration):
|
||||||
|
"""
|
||||||
|
Returns a list of Fade objects
|
||||||
|
"""
|
||||||
|
fades = []
|
||||||
|
if self.type in ['IN', 'IN_OUT', 'CURSOR_TO']:
|
||||||
|
fade = Fade(sequence, fade_fcurve, 'IN', animated_property, duration)
|
||||||
|
fades.append(fade)
|
||||||
|
if self.type in ['OUT', 'IN_OUT', 'CURSOR_FROM']:
|
||||||
|
fade = Fade(sequence, fade_fcurve, 'OUT', animated_property, duration)
|
||||||
|
fades.append(fade)
|
||||||
|
return fades
|
||||||
|
|
||||||
|
def fade_find_or_create_fcurve(self, context, sequence, animated_property):
|
||||||
|
"""
|
||||||
|
Iterates over all the fcurves until it finds an fcurve with a data path
|
||||||
|
that corresponds to the sequence.
|
||||||
|
Returns the matching FCurve or creates a new one if the function can't find a match.
|
||||||
|
"""
|
||||||
|
fade_fcurve = None
|
||||||
|
fcurves = context.scene.animation_data.action.fcurves
|
||||||
|
searched_data_path = sequence.path_from_id(animated_property)
|
||||||
|
for fcurve in fcurves:
|
||||||
|
if fcurve.data_path == searched_data_path:
|
||||||
|
fade_fcurve = fcurve
|
||||||
|
break
|
||||||
|
if not fade_fcurve:
|
||||||
|
fade_fcurve = fcurves.new(data_path=searched_data_path)
|
||||||
|
return fade_fcurve
|
||||||
|
|
||||||
|
def fade_animation_clear(self, context, fade_fcurve, fades):
|
||||||
|
"""
|
||||||
|
Removes existing keyframes in the fades' time range, in fast mode, without
|
||||||
|
updating the fcurve
|
||||||
|
"""
|
||||||
|
keyframe_points = fade_fcurve.keyframe_points
|
||||||
|
for fade in fades:
|
||||||
|
for keyframe in keyframe_points:
|
||||||
|
# The keyframe points list doesn't seem to always update as the
|
||||||
|
# operator re-runs Leading to trying to remove nonexistent keyframes
|
||||||
|
try:
|
||||||
|
if fade.start.x < keyframe.co[0] <= fade.end.x:
|
||||||
|
keyframe_points.remove(keyframe, fast=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
fade_fcurve.update()
|
||||||
|
|
||||||
|
def fade_animation_create(self, fade_fcurve, fades):
|
||||||
|
"""
|
||||||
|
Inserts keyframes in the fade_fcurve in fast mode using the Fade objects.
|
||||||
|
Updates the fcurve after having inserted all keyframes to finish the animation.
|
||||||
|
"""
|
||||||
|
keyframe_points = fade_fcurve.keyframe_points
|
||||||
|
for fade in fades:
|
||||||
|
for point in (fade.start, fade.end):
|
||||||
|
keyframe_points.insert(frame=point.x, value=point.y, options={'FAST'})
|
||||||
|
fade_fcurve.update()
|
||||||
|
# The graph editor and the audio waveforms only redraw upon "moving" a keyframe
|
||||||
|
keyframe_points[-1].co = keyframe_points[-1].co
|
||||||
|
|
||||||
|
|
||||||
|
class Fade:
|
||||||
|
"""
|
||||||
|
Data structure to represent fades
|
||||||
|
"""
|
||||||
|
type = ''
|
||||||
|
animated_property = ''
|
||||||
|
duration = -1
|
||||||
|
max_value = 1.0
|
||||||
|
start, end = Vector((0, 0)), Vector((0, 0))
|
||||||
|
|
||||||
|
def __init__(self, sequence, fade_fcurve, type, animated_property, duration):
|
||||||
|
self.type = type
|
||||||
|
self.animated_property = animated_property
|
||||||
|
self.duration = duration
|
||||||
|
self.max_value = self.calculate_max_value(sequence, fade_fcurve)
|
||||||
|
|
||||||
|
if type == 'IN':
|
||||||
|
self.start = Vector((sequence.frame_final_start, 0.0))
|
||||||
|
self.end = Vector((sequence.frame_final_start + self.duration, self.max_value))
|
||||||
|
elif type == 'OUT':
|
||||||
|
self.start = Vector((sequence.frame_final_end - self.duration, self.max_value))
|
||||||
|
self.end = Vector((sequence.frame_final_end, 0.0))
|
||||||
|
|
||||||
|
def calculate_max_value(self, sequence, fade_fcurve):
|
||||||
|
"""
|
||||||
|
Returns the maximum Y coordinate the fade animation should use for a given sequence
|
||||||
|
Uses either the sequence's value for the animated property, or the next keyframe after the fade
|
||||||
|
"""
|
||||||
|
max_value = 0.0
|
||||||
|
|
||||||
|
if not fade_fcurve.keyframe_points:
|
||||||
|
max_value = getattr(sequence, self.animated_property, 1.0)
|
||||||
|
else:
|
||||||
|
if self.type == 'IN':
|
||||||
|
fade_end = sequence.frame_final_start + self.duration
|
||||||
|
keyframes = (k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end)
|
||||||
|
if self.type == 'OUT':
|
||||||
|
fade_start = sequence.frame_final_end - self.duration
|
||||||
|
keyframes = (k for k in reversed(fade_fcurve.keyframe_points) if k.co[0] <= fade_start)
|
||||||
|
try:
|
||||||
|
max_value = next(keyframes).co[1]
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return max_value if max_value > 0.0 else 1.0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Fade {}: {} to {}".format(self.type, self.start, self.end)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_duration_frames(context, duration_seconds):
|
||||||
|
return round(duration_seconds * context.scene.render.fps / context.scene.render.fps_base)
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SequencerCrossfadeSounds,
|
SequencerCrossfadeSounds,
|
||||||
SequencerCutMulticam,
|
SequencerCutMulticam,
|
||||||
SequencerDeinterlaceSelectedMovies,
|
SequencerDeinterlaceSelectedMovies,
|
||||||
|
SequencerFadesClear,
|
||||||
|
SequencerFadesAdd,
|
||||||
)
|
)
|
||||||
|
@@ -502,6 +502,10 @@ class SEQUENCER_MT_add(Menu):
|
|||||||
col.menu("SEQUENCER_MT_add_transitions", icon='ARROW_LEFTRIGHT')
|
col.menu("SEQUENCER_MT_add_transitions", icon='ARROW_LEFTRIGHT')
|
||||||
col.enabled = selected_sequences_len(context) >= 2
|
col.enabled = selected_sequences_len(context) >= 2
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
col.operator_menu_enum("sequencer.fades_add", "type", text="Fade", icon="IPO_EASE_IN_OUT")
|
||||||
|
col.enabled = selected_sequences_len(context) >= 1
|
||||||
|
|
||||||
|
|
||||||
class SEQUENCER_MT_add_empty(Menu):
|
class SEQUENCER_MT_add_empty(Menu):
|
||||||
bl_label = "Empty"
|
bl_label = "Empty"
|
||||||
@@ -749,26 +753,34 @@ class SEQUENCER_MT_context_menu(Menu):
|
|||||||
layout.operator("sequencer.gap_remove").all = False
|
layout.operator("sequencer.gap_remove").all = False
|
||||||
layout.operator("sequencer.gap_insert")
|
layout.operator("sequencer.gap_insert")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
strip = act_strip(context)
|
strip = act_strip(context)
|
||||||
|
|
||||||
if strip:
|
if strip:
|
||||||
strip_type = strip.type
|
strip_type = strip.type
|
||||||
|
selected_sequences_count = selected_sequences_len(context)
|
||||||
|
|
||||||
if strip_type != 'SOUND':
|
if strip_type != "SOUND":
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator_menu_enum("sequencer.strip_modifier_add", "type", text="Add Modifier")
|
layout.operator_menu_enum("sequencer.strip_modifier_add", "type", text="Add Modifier")
|
||||||
layout.operator("sequencer.strip_modifier_copy", text="Copy Modifiers to Selection")
|
layout.operator("sequencer.strip_modifier_copy", text="Copy Modifiers to Selection")
|
||||||
|
|
||||||
if selected_sequences_len(context) >= 2:
|
if selected_sequences_count >= 2:
|
||||||
layout.separator()
|
layout.separator()
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.menu("SEQUENCER_MT_add_transitions", text="Add Transition")
|
col.menu("SEQUENCER_MT_add_transitions", text="Add Transition")
|
||||||
|
|
||||||
elif selected_sequences_len(context) >= 2:
|
elif selected_sequences_count >= 2:
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("sequencer.crossfade_sounds", text="Crossfade Sounds")
|
layout.operator("sequencer.crossfade_sounds", text="Crossfade Sounds")
|
||||||
|
|
||||||
|
if selected_sequences_count >= 1:
|
||||||
|
col = layout.column()
|
||||||
|
col.operator_menu_enum("sequencer.fades_add", "type", text="Fade")
|
||||||
|
col.enabled = selected_sequences_len(context) >= 1
|
||||||
|
layout.operator("sequencer.fades_clear", text="Clear Fade")
|
||||||
|
|
||||||
if strip_type in {
|
if strip_type in {
|
||||||
'CROSS', 'ADD', 'SUBTRACT', 'ALPHA_OVER', 'ALPHA_UNDER',
|
'CROSS', 'ADD', 'SUBTRACT', 'ALPHA_OVER', 'ALPHA_UNDER',
|
||||||
'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'WIPE', 'GLOW',
|
'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'WIPE', 'GLOW',
|
||||||
|
Reference in New Issue
Block a user