diff options
author | Richard Antalik <richardantalik@gmail.com> | 2019-09-14 03:24:02 +0300 |
---|---|---|
committer | Richard Antalik <richardantalik@gmail.com> | 2019-09-14 03:24:42 +0300 |
commit | 2ec025d7be3ca9f3fd2fe0ac844ef70c69e7af55 (patch) | |
tree | 472b1b1a6c4e399cc86390abf07628ffa5288819 | |
parent | a960dc451930796ca310cc88f00fb701374b2f7f (diff) |
VSE: Add operators to add and remove fadestemp-npr-smooth-contour
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
-rw-r--r-- | release/scripts/startup/bl_operators/sequencer.py | 234 | ||||
-rw-r--r-- | release/scripts/startup/bl_ui/space_sequencer.py | 20 |
2 files changed, 250 insertions, 4 deletions
diff --git a/release/scripts/startup/bl_operators/sequencer.py b/release/scripts/startup/bl_operators/sequencer.py index b1152157cf8..1da96834a5a 100644 --- a/release/scripts/startup/bl_operators/sequencer.py +++ b/release/scripts/startup/bl_operators/sequencer.py @@ -20,6 +20,8 @@ import bpy from bpy.types import Operator +from mathutils import Vector +from math import floor from bpy.props import IntProperty @@ -136,8 +138,240 @@ class SequencerDeinterlaceSelectedMovies(Operator): 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 = ( SequencerCrossfadeSounds, SequencerCutMulticam, SequencerDeinterlaceSelectedMovies, + SequencerFadesClear, + SequencerFadesAdd, ) diff --git a/release/scripts/startup/bl_ui/space_sequencer.py b/release/scripts/startup/bl_ui/space_sequencer.py index 66ce7496869..e6ab9d1e9de 100644 --- a/release/scripts/startup/bl_ui/space_sequencer.py +++ b/release/scripts/startup/bl_ui/space_sequencer.py @@ -502,6 +502,10 @@ class SEQUENCER_MT_add(Menu): col.menu("SEQUENCER_MT_add_transitions", icon='ARROW_LEFTRIGHT') 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): bl_label = "Empty" @@ -749,26 +753,34 @@ class SEQUENCER_MT_context_menu(Menu): layout.operator("sequencer.gap_remove").all = False layout.operator("sequencer.gap_insert") + layout.separator() + strip = act_strip(context) if strip: strip_type = strip.type + selected_sequences_count = selected_sequences_len(context) - if strip_type != 'SOUND': - + if strip_type != "SOUND": layout.separator() layout.operator_menu_enum("sequencer.strip_modifier_add", "type", text="Add Modifier") layout.operator("sequencer.strip_modifier_copy", text="Copy Modifiers to Selection") - if selected_sequences_len(context) >= 2: + if selected_sequences_count >= 2: layout.separator() col = layout.column() col.menu("SEQUENCER_MT_add_transitions", text="Add Transition") - elif selected_sequences_len(context) >= 2: + elif selected_sequences_count >= 2: layout.separator() 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 { 'CROSS', 'ADD', 'SUBTRACT', 'ALPHA_OVER', 'ALPHA_UNDER', 'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'WIPE', 'GLOW', |