Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Lovato <nathan@gdquest.com>2019-09-05 18:22:37 +0300
committerNathan Lovato <nathan@gdquest.com>2019-09-05 18:22:52 +0300
commit61d48c0a4be0ab8f71e6e1d35f0aa99c77fcfd33 (patch)
treef64d0ea88789184f45112b489598990d549788f2 /power_sequencer/operators/fade_add.py
parentda5a1175e30c347fbce05e49e2f5f895be30bd5b (diff)
Add the VSE addon Power Sequencer
Diffstat (limited to 'power_sequencer/operators/fade_add.py')
-rw-r--r--power_sequencer/operators/fade_add.py254
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)