# SPDX-License-Identifier: GPL-3.0-or-later # Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors import bpy from mathutils import Vector from math import floor from .utils.doc import doc_name, doc_idname, doc_brief, doc_description class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator): """*brief* 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 both 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 """ doc = { "name": doc_name(__qualname__), "demo": "https://i.imgur.com/XoUM2vw.gif", "description": doc_description(__doc__), "shortcuts": [ ({"type": "F", "value": "PRESS", "alt": True}, {"type": "OUT"}, "Fade Out"), ({"type": "F", "value": "PRESS", "ctrl": True}, {"type": "IN"}, "Fade In"), ({"type": "F", "value": "PRESS"}, {"type": "IN_OUT"}, "Fade In and Out"), ], "keymap": "Sequencer", } bl_idname = doc_idname(__qualname__) bl_label = doc["name"] bl_description = doc_brief(doc["description"]) 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, or both in and out. Default is both", default="IN_OUT", ) @classmethod def poll(cls, context): return context.selected_sequences 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 = fade_find_or_create_fcurve(context, sequence, animated_property) fades = self.calculate_fades(sequence, fade_fcurve, animated_property, duration) fade_animation_clear(context, fade_fcurve, fades) 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(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(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 keyframe in keyframe_points: for fade in fades: # 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 ReferenceError: pass def fade_animation_create(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)