Skip to content

Commit

Permalink
Merge pull request #3781 from ferdnyc/new-scaler
Browse files Browse the repository at this point in the history
project_data: New keyframe scaler implementation
  • Loading branch information
jonoomph authored Jan 29, 2021
2 parents 8cdb471 + 3ad93c7 commit 544fda3
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 56 deletions.
85 changes: 85 additions & 0 deletions src/classes/keyframe_scaler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
@file
@brief Process project data, scaling keyframe X coordinates by the given factor
@author Jonathan Thomas <jonathan@openshot.org>
@author FeRD (Frank Dana) <ferdnyc@gmail.com>
@section LICENSE
Copyright (c) 2008-2020 OpenShot Studios, LLC
(http://www.openshotstudios.com). This file is part of
OpenShot Video Editor (http://www.openshot.org), an open-source project
dedicated to delivering high quality video editing and animation solutions
to the world.
OpenShot Video Editor 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.
OpenShot Video Editor 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 OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
"""


class KeyframeScaler:
"""This factory class produces scaler objects which, when called,
will apply the assigned scaling factor to the keyframe points
in a project data dictionary. Keyframe X coordinate values are
multiplied by the scaling factor, except X=1 (because the first
frame never changes)"""

def _scale_x_value(self, value: float) -> int:
"""Scale value by some factor, except for 1 (leave that alone)"""
if value == 1.0:
return value
# Round to nearest INT
return round(value * self._scale_factor)

def _update_prop(self, prop: dict):
"""Find the keyframe points in a property and scale"""
# Create a list of lists of keyframe points for this prop
if "red" in prop:
# It's a color, one list of points for each channel
keyframes = [prop[color].get("Points", []) for color in prop]
else:
# Not a color, just a single list of points
keyframes = [prop.get("Points", [])]
for k in keyframes:
# Scale the X coordinate (frame #) by the stored factor
[point["co"].update({
"X": self._scale_x_value(point["co"].get("X", 0.0))
})
for point in k if "co" in point]

def _process_item(self, item: dict):
"""Process all the dict sub-members of the current dict"""
dict_props = [
item[prop] for prop in item
if isinstance(item[prop], dict)
]
for prop in dict_props:
self._update_prop(prop)

def __call__(self, data: dict) -> dict:
"""Apply the stored scaling factor to a project data dict"""
# Look for keyframe objects in clips
for clip in data.get('clips', []):
self._process_item(clip)
# Also update any effects applied to the clip
for effect in clip.get("effects", []):
self._process_item(effect)
# Look for keyframe objects in project effects (transitions)
for effect in data.get('effects', []):
self._process_item(effect)
# return the scaled project data
return data

def __init__(self, factor: float):
"""Store the scale factor assigned to this instance"""
self._scale_factor = factor
65 changes: 9 additions & 56 deletions src/classes/project_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
from classes.assets import get_assets_path
from windows.views.find_file import find_missing_file

from .keyframe_scaler import KeyframeScaler


class ProjectDataStore(JsonDataStore, UpdateInterface):
""" This class allows advanced searching of data structure, implements changes interface """
Expand Down Expand Up @@ -394,65 +396,16 @@ def load(self, file_path, clear_thumbnails=True):
from classes.app import get_app
get_app().updates.load(self._data)

def scale_keyframe_value(self, original_value, scale_factor):
"""Scale keyframe X coordinate by some factor, except for 1 (leave that alone)"""
if original_value == 1.0:
# This represents the first frame of a clip (so we want to maintain that)
return original_value
else:
# Round to nearest INT
return round(original_value * scale_factor)

def rescale_keyframes(self, scale_factor):
"""Adjust all keyframe coordinates from previous FPS to new FPS (using a scale factor)
and return scaled project data without modifing the current project."""
log.info('Scale all keyframes by a factor of %s' % scale_factor)

# Create copy of active project data
data = copy.deepcopy(self._data)

# Rescale the the copied project data
# Loop through all clips (and look for Keyframe objects)
# Scale the X coordinate by factor (which represents the frame #)
for clip in data.get('clips', []):
for attribute in clip:
if type(clip.get(attribute)) == dict and "Points" in clip.get(attribute):
for point in clip.get(attribute).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)
if type(clip.get(attribute)) == dict and "red" in clip.get(attribute):
for color in clip.get(attribute):
for point in clip.get(attribute).get(color).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)
for effect in clip.get("effects", []):
for attribute in effect:
if type(effect.get(attribute)) == dict and "Points" in effect.get(attribute):
for point in effect.get(attribute).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)
if type(effect.get(attribute)) == dict and "red" in effect.get(attribute):
for color in effect.get(attribute):
for point in effect.get(attribute).get(color).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)

# Loop through all effects/transitions (and look for Keyframe objects)
# Scale the X coordinate by factor (which represents the frame #)
for effect in data.get('effects', []):
for attribute in effect:
if type(effect.get(attribute)) == dict and "Points" in effect.get(attribute):
for point in effect.get(attribute).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)
if type(effect.get(attribute)) == dict and "red" in effect.get(attribute):
for color in effect.get(attribute):
for point in effect.get(attribute).get(color).get("Points"):
if "co" in point:
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)

# return the copied and scaled project data
return data
#
log.info('Scale all keyframes by a factor of %s', scale_factor)
# Create a scaler instance
scaler = KeyframeScaler(factor=scale_factor)
# Create copy of active project data and scale
scaled = scaler(copy.deepcopy(self._data))
return scaled

def read_legacy_project_file(self, file_path):
"""Attempt to read a legacy version 1.x openshot project file"""
Expand Down

0 comments on commit 544fda3

Please sign in to comment.