diff options
author | Nathan Lovato <nathan@gdquest.com> | 2019-09-05 18:22:37 +0300 |
---|---|---|
committer | Nathan Lovato <nathan@gdquest.com> | 2019-09-05 18:22:52 +0300 |
commit | 61d48c0a4be0ab8f71e6e1d35f0aa99c77fcfd33 (patch) | |
tree | f64d0ea88789184f45112b489598990d549788f2 /power_sequencer/operators/fade_add.py | |
parent | da5a1175e30c347fbce05e49e2f5f895be30bd5b (diff) |
Add the VSE addon Power Sequencer
Diffstat (limited to 'power_sequencer/operators/fade_add.py')
-rw-r--r-- | power_sequencer/operators/fade_add.py | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/power_sequencer/operators/fade_add.py b/power_sequencer/operators/fade_add.py new file mode 100644 index 00000000..d72f034d --- /dev/null +++ b/power_sequencer/operators/fade_add.py @@ -0,0 +1,254 @@ +# +# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors +# +# This file is part of Power Sequencer. +# +# Power Sequencer 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 3 of the +# License, or (at your option) any later version. +# +# Power Sequencer 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 Power Sequencer. If +# not, see <https://www.gnu.org/licenses/>. +# +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 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. + """ + + 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) |