diff --git a/app/app.pro b/app/app.pro
index 327028e6d8..1be96e368c 100644
--- a/app/app.pro
+++ b/app/app.pro
@@ -73,7 +73,8 @@ HEADERS += \
src/doubleprogressdialog.h \
src/colorslider.h \
src/checkupdatesdialog.h \
- src/presetdialog.h
+ src/presetdialog.h \
+ src/retimedialog.h
SOURCES += \
src/importlayersdialog.cpp \
@@ -109,7 +110,8 @@ SOURCES += \
src/colorslider.cpp \
src/checkupdatesdialog.cpp \
src/presetdialog.cpp \
- src/app_util.cpp
+ src/app_util.cpp \
+ src/retimedialog.cpp
FORMS += \
ui/importimageseqpreview.ui \
@@ -137,7 +139,8 @@ FORMS += \
ui/filespage.ui \
ui/toolspage.ui \
ui/toolboxwidget.ui \
- ui/presetdialog.ui
+ ui/presetdialog.ui \
+ ui/retimedialog.ui
diff --git a/app/src/actioncommands.cpp b/app/src/actioncommands.cpp
index cbbc143b69..665a6d4c1d 100644
--- a/app/src/actioncommands.cpp
+++ b/app/src/actioncommands.cpp
@@ -56,6 +56,7 @@ GNU General Public License for more details.
#include "doubleprogressdialog.h"
#include "checkupdatesdialog.h"
#include "errordialog.h"
+#include "retimedialog.h"
ActionCommands::ActionCommands(QWidget* parent) : QObject(parent)
@@ -760,6 +761,18 @@ void ActionCommands::moveFrameBackward()
}
}
+void ActionCommands::retime()
+{
+ auto dialog = new RetimeDialog(mParent);
+ dialog->setOrigFps(mEditor->fps());
+ dialog->exec();
+
+ if (dialog->result() == QDialog::Accepted)
+ {
+ mEditor->retime(dialog->getNewFps(), dialog->getNewSpeed());
+ }
+}
+
Status ActionCommands::addNewBitmapLayer()
{
bool ok;
diff --git a/app/src/actioncommands.h b/app/src/actioncommands.h
index e6603762fb..d0688a09c1 100644
--- a/app/src/actioncommands.h
+++ b/app/src/actioncommands.h
@@ -70,6 +70,7 @@ class ActionCommands : public QObject
void duplicateKey();
void moveFrameForward();
void moveFrameBackward();
+ void retime();
// Layer
Status addNewBitmapLayer();
diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp
index a2d63e955c..0579e1f90c 100644
--- a/app/src/mainwindow2.cpp
+++ b/app/src/mainwindow2.cpp
@@ -352,6 +352,7 @@ void MainWindow2::createMenus()
connect(ui->actionDuplicate_Frame, &QAction::triggered, mCommands, &ActionCommands::duplicateKey);
connect(ui->actionMove_Frame_Forward, &QAction::triggered, mCommands, &ActionCommands::moveFrameForward);
connect(ui->actionMove_Frame_Backward, &QAction::triggered, mCommands, &ActionCommands::moveFrameBackward);
+ connect(ui->actionRetime, &QAction::triggered, mCommands, &ActionCommands::retime);
//--- Tool Menu ---
connect(ui->actionMove, &QAction::triggered, mToolBox, &ToolBoxWidget::moveOn);
@@ -1243,6 +1244,7 @@ void MainWindow2::setupKeyboardShortcuts()
ui->actionRemove_Frame->setShortcut(cmdKeySeq(CMD_REMOVE_FRAME));
ui->actionMove_Frame_Backward->setShortcut(cmdKeySeq(CMD_MOVE_FRAME_BACKWARD));
ui->actionMove_Frame_Forward->setShortcut(cmdKeySeq(CMD_MOVE_FRAME_FORWARD));
+ ui->actionRetime->setShortcut(cmdKeySeq(CMD_RETIME));
ui->actionFlip_inbetween->setShortcut(cmdKeySeq(CMD_FLIP_INBETWEEN));
ui->actionFlip_rolling->setShortcut(cmdKeySeq(CMD_FLIP_ROLLING));
@@ -1452,6 +1454,7 @@ void MainWindow2::makeConnections(Editor* pEditor, TimeLine* pTimeline)
connect(pTimeline, &TimeLine::newCameraLayer, mCommands, &ActionCommands::addNewCameraLayer);
connect(mTimeLine, &TimeLine::playButtonTriggered, mCommands, &ActionCommands::PlayStop);
+ connect(mTimeLine, &TimeLine::retimeTriggered, mCommands, &ActionCommands::retime);
connect(pEditor->layers(), &LayerManager::currentLayerChanged, pTimeline, &TimeLine::updateUI);
connect(pEditor->layers(), &LayerManager::layerCountChanged, pTimeline, &TimeLine::updateUI);
@@ -1461,6 +1464,8 @@ void MainWindow2::makeConnections(Editor* pEditor, TimeLine* pTimeline)
connect(pEditor, &Editor::objectLoaded, pTimeline, &TimeLine::onObjectLoaded);
connect(pEditor, &Editor::updateTimeLine, pTimeline, &TimeLine::updateUI);
+ connect(pEditor, &Editor::retimed, pTimeline, &TimeLine::updateFps);
+
connect(pEditor->layers(), &LayerManager::currentLayerChanged, mToolOptions, &ToolOptionWidget::updateUI);
}
diff --git a/app/src/retimedialog.cpp b/app/src/retimedialog.cpp
new file mode 100644
index 0000000000..d6e1bcd4ce
--- /dev/null
+++ b/app/src/retimedialog.cpp
@@ -0,0 +1,30 @@
+#include "retimedialog.h"
+#include "ui_retimedialog.h"
+
+RetimeDialog::RetimeDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::RetimeDialog)
+{
+ ui->setupUi(this);
+}
+
+RetimeDialog::~RetimeDialog()
+{
+ delete ui;
+}
+
+void RetimeDialog::setOrigFps(int origFps)
+{
+ ui->origFps->setText(QString("%1 %2").arg(origFps).arg(tr("fps")));
+ ui->newFpsBox->setValue(origFps);
+}
+
+int RetimeDialog::getNewFps()
+{
+ return ui->newFpsBox->value();
+}
+
+qreal RetimeDialog::getNewSpeed()
+{
+ return ui->newSpeedBox->value();
+}
diff --git a/app/src/retimedialog.h b/app/src/retimedialog.h
new file mode 100644
index 0000000000..dc78d0b9e4
--- /dev/null
+++ b/app/src/retimedialog.h
@@ -0,0 +1,27 @@
+#ifndef RETIMEDIALOG_H
+#define RETIMEDIALOG_H
+
+#include
+
+namespace Ui {
+class RetimeDialog;
+}
+
+class RetimeDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit RetimeDialog(QWidget *parent = nullptr);
+ ~RetimeDialog();
+
+ void setOrigFps(int origFps);
+ int getNewFps();
+
+ qreal getNewSpeed();
+
+private:
+ Ui::RetimeDialog *ui;
+};
+
+#endif // RETIMEDIALOG_H
diff --git a/app/src/shortcutspage.cpp b/app/src/shortcutspage.cpp
index 355f78549b..82733255fe 100644
--- a/app/src/shortcutspage.cpp
+++ b/app/src/shortcutspage.cpp
@@ -330,6 +330,7 @@ static QString getHumanReadableShortcutName(const QString& cmdName)
{CMD_LOOP, QObject::tr("Toggle Loop", "Shortcut")},
{CMD_MOVE_FRAME_BACKWARD, QObject::tr("Move Frame Backward", "Shortcut")},
{CMD_MOVE_FRAME_FORWARD, QObject::tr("Move Frame Forward", "Shortcut")},
+ {CMD_RETIME, QObject::tr("Retime Animation", "Shortcut")},
{CMD_NEW_BITMAP_LAYER, QObject::tr("New Bitmap Layer", "Shortcut")},
{CMD_NEW_CAMERA_LAYER, QObject::tr("New Camera Layer", "Shortcut")},
{CMD_NEW_FILE, QObject::tr("New File", "Shortcut")},
diff --git a/app/ui/mainwindow2.ui b/app/ui/mainwindow2.ui
index 3e312785d2..ae41f6e776 100644
--- a/app/ui/mainwindow2.ui
+++ b/app/ui/mainwindow2.ui
@@ -49,7 +49,7 @@
0
0
831
- 30
+ 24
diff --git a/app/ui/retimedialog.ui b/app/ui/retimedialog.ui
new file mode 100644
index 0000000000..04a105729c
--- /dev/null
+++ b/app/ui/retimedialog.ui
@@ -0,0 +1,190 @@
+
+
+ RetimeDialog
+
+
+
+ 0
+ 0
+ 456
+ 335
+
+
+
+ Retime Animation
+
+
+ -
+
+
+ <h1>Retime Animation</h1>
+
+
+
+ -
+
+
+ Retiming allows you to adjust the FPS and playback speed of your project independently. It accomplishes this by moving the frames closer together or further apart to reach the desired speed and frame rate.
+
+
+ true
+
+
+
+ -
+
+
+ <span style=" text-decoration: underline;">Frame Rate</span>
+
+
+
+ -
+
+
-
+
+
+ Original:
+
+
+
+ -
+
+
+ <span style=" font-weight:600;">12 fps</span>
+
+
+
+ -
+
+
+ New:
+
+
+
+ -
+
+
+ fps
+
+
+ 1
+
+
+ 90
+
+
+ 12
+
+
+
+
+
+ -
+
+
+ <span style=" text-decoration: underline;">Speed</span>
+
+
+
+ -
+
+
-
+
+
+ Original:
+
+
+
+ -
+
+
+ <span style=" font-weight:600;">1.00x</span>
+
+
+
+ -
+
+
+ New:
+
+
+
+ -
+
+
+ x
+
+
+ 0.010000000000000
+
+
+ 90.000000000000000
+
+
+ 0.100000000000000
+
+
+ 1.000000000000000
+
+
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" color:red;">Warning: Retiming to a lower fps or higher speed may result in the loss of frames. This action cannot be undone.</span></p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ RetimeDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ RetimeDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/core_lib/data/resources/kb.ini b/core_lib/data/resources/kb.ini
index 40e0ac7e3e..45a507f42f 100644
--- a/core_lib/data/resources/kb.ini
+++ b/core_lib/data/resources/kb.ini
@@ -50,6 +50,7 @@ CmdGotoNextKeyFrame=Alt+.
CmdGotoPreviousKeyFrame=Alt+","
CmdMoveFrameForward=Ctrl+.
CmdMoveFrameBackward=ctrl+","
+CmdRetime=
CmdAddFrame=F7
CmdDuplicateFrame=F6
CmdRemoveFrame=Shift+F5
diff --git a/core_lib/src/interface/editor.cpp b/core_lib/src/interface/editor.cpp
index 1b38db6361..d45b309611 100644
--- a/core_lib/src/interface/editor.cpp
+++ b/core_lib/src/interface/editor.cpp
@@ -126,10 +126,83 @@ void Editor::setFps(int fps)
mPreferenceManager->set(SETTING::FPS, fps);
}
+void Editor::retime(int newFps, qreal speed)
+{
+ const int origFps = fps();
+ const qreal multiplier = (static_cast(newFps) / origFps) * (1 / speed);
+ if (qFuzzyCompare(multiplier, 1)) return;
+
+ for (int i = 0; i < mObject->getLayerCount(); i++)
+ {
+ Layer* layer = mObject->getLayer(i);
+ QMap frameRemap; // New frame -> old frame
+ QList deleteFrames;
+ for (int origPos = layer->firstKeyFramePosition(); origPos <= layer->getMaxKeyFramePosition(); origPos++)
+ {
+ if (layer->keyExists(origPos))
+ {
+ // Scale the current position to the new fps
+ // The offset is to make sure that frame 1 stays at frame 1
+ int newPos = qRound((origPos-1)*multiplier)+1;
+ if (frameRemap.contains(newPos))
+ {
+ // If two frames get mapped to the same new frame, choose the closer of them
+ if (qAbs((frameRemap.value(newPos)-1)*multiplier+1-newPos) > qAbs((origPos-1)*multiplier+1-newPos))
+ {
+ deleteFrames.append(frameRemap.value(newPos));
+ frameRemap.insert(newPos, origPos);
+ }
+ else
+ {
+ deleteFrames.append(origPos);
+ }
+ }
+ else
+ {
+ frameRemap.insert(newPos, origPos);
+ }
+ }
+ }
+
+ foreach (const int frame, deleteFrames)
+ {
+ layer->removeKeyFrame(frame);
+ }
+
+ if (multiplier < 1)
+ {
+ for (auto iter = frameRemap.constBegin(); iter != frameRemap.constEnd(); iter++)
+ {
+ if (iter.value() != iter.key())
+ {
+ layer->swapKeyFrames(iter.value(), iter.key());
+ }
+ }
+ }
+ else
+ {
+ for (auto iter = frameRemap.keys().crbegin(); iter != frameRemap.keys().crend(); iter++)
+ {
+ if (frameRemap.value(*iter) != *iter)
+ {
+ layer->swapKeyFrames(frameRemap.value(*iter), *iter);
+ }
+ }
+ }
+ }
+
+ setFps(newFps);
+ layers()->notifyAnimationLengthChanged();
+ emit retimed(newFps);
+ emit updateTimeLine();
+}
+
void Editor::makeConnections()
{
connect(mPreferenceManager, &PreferenceManager::optionChanged, this, &Editor::settingUpdated);
connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &Editor::clipboardChanged);
+
+ connect(this, &Editor::retimed, playback(), &PlaybackManager::setFps);
}
void Editor::dragEnterEvent(QDragEnterEvent* event)
diff --git a/core_lib/src/interface/editor.h b/core_lib/src/interface/editor.h
index 6f9dd72a6d..cc0c8a9ff4 100644
--- a/core_lib/src/interface/editor.h
+++ b/core_lib/src/interface/editor.h
@@ -91,6 +91,7 @@ class Editor : public QObject
int currentFrame();
int fps();
void setFps(int fps);
+ void retime(int newFps, qreal speed);
int currentLayerIndex() { return mCurrentLayerIndex; }
void setCurrentLayerIndex(int i);
@@ -131,6 +132,8 @@ class Editor : public QObject
void needDisplayInfo(const QString& title, const QString& body);
void needDisplayInfoNoTitle(const QString& body);
+ void retimed(int newFps);
+
public: //slots
void clearCurrentFrame();
diff --git a/core_lib/src/interface/timecontrols.cpp b/core_lib/src/interface/timecontrols.cpp
index fffd6e0aeb..989418123a 100644
--- a/core_lib/src/interface/timecontrols.cpp
+++ b/core_lib/src/interface/timecontrols.cpp
@@ -18,6 +18,7 @@ GNU General Public License for more details.
#include "timecontrols.h"
#include
+#include
#include
#include "editor.h"
@@ -46,6 +47,11 @@ void TimeControls::initUI()
mFpsBox->setSuffix(tr(" fps"));
mFpsBox->setToolTip(tr("Frames per second"));
mFpsBox->setFocusPolicy(Qt::WheelFocus);
+ mFpsBox->setContextMenuPolicy(Qt::ActionsContextMenu);
+ QMenu *mFpsMenu = new QMenu(mFpsBox);
+ QAction *retimeAction = new QAction(tr("Retime"), mFpsMenu);
+ connect(retimeAction, &QAction::triggered, this, &TimeControls::retimeTriggered);
+ mFpsBox->addAction(retimeAction);
mLoopStartSpinBox = new QSpinBox(this);
mLoopStartSpinBox->setFixedHeight(24);
@@ -209,6 +215,12 @@ void TimeControls::updatePlayState()
}
}
+void TimeControls::updateFps(int fps)
+{
+ QSignalBlocker b(mFpsBox);
+ mFpsBox->setValue(fps);
+}
+
void TimeControls::jumpToStartButtonClicked()
{
if (mPlaybackRangeCheckBox->isChecked())
diff --git a/core_lib/src/interface/timecontrols.h b/core_lib/src/interface/timecontrols.h
index 0c15c7138a..bf33033bd1 100644
--- a/core_lib/src/interface/timecontrols.h
+++ b/core_lib/src/interface/timecontrols.h
@@ -52,8 +52,10 @@ class TimeControls : public QToolBar
void soundScrubToggled(bool);
void fpsChanged(int);
void playButtonTriggered();
+ void retimeTriggered();
public slots:
+ void updateFps(int fps);
/// Work-around in case the FPS spin-box "valueChanged" signal doesn't work.
void onFpsEditingFinished();
diff --git a/core_lib/src/interface/timeline.cpp b/core_lib/src/interface/timeline.cpp
index af9f7d6a1c..8ffa2a6c78 100644
--- a/core_lib/src/interface/timeline.cpp
+++ b/core_lib/src/interface/timeline.cpp
@@ -209,6 +209,7 @@ void TimeLine::initUI()
connect(mTimeControls, &TimeControls::fpsChanged, this, &TimeLine::fpsChanged);
connect(mTimeControls, &TimeControls::fpsChanged, this, &TimeLine::updateLength);
connect(mTimeControls, &TimeControls::playButtonTriggered, this, &TimeLine::playButtonTriggered);
+ connect(mTimeControls, &TimeControls::retimeTriggered, this, &TimeLine::retimeTriggered);
connect(newBitmapLayerAct, &QAction::triggered, this, &TimeLine::newBitmapLayer);
connect(newVectorLayerAct, &QAction::triggered, this, &TimeLine::newVectorLayer);
@@ -222,6 +223,8 @@ void TimeLine::initUI()
connect(editor(), &Editor::currentFrameChanged, this, &TimeLine::updateFrame);
+ connect(this, &TimeLine::updateFps, mTimeControls, &TimeControls::updateFps);
+
LayerManager* layer = editor()->layers();
connect(layer, &LayerManager::layerCountChanged, this, &TimeLine::updateLayerNumber);
mNumLayers = layer->count();
diff --git a/core_lib/src/interface/timeline.h b/core_lib/src/interface/timeline.h
index c457bb8cc8..9ed5db14a0 100644
--- a/core_lib/src/interface/timeline.h
+++ b/core_lib/src/interface/timeline.h
@@ -70,6 +70,8 @@ class TimeLine : public BaseDockWidget
void onionPrevClick();
void onionNextClick();
void playButtonTriggered();
+ void retimeTriggered();
+ void updateFps(int fps);
public:
bool scrubbing = false;
diff --git a/core_lib/src/util/pencildef.h b/core_lib/src/util/pencildef.h
index f638ebbd12..8879524518 100644
--- a/core_lib/src/util/pencildef.h
+++ b/core_lib/src/util/pencildef.h
@@ -151,6 +151,7 @@ const static int MaxFramesBound = 9999;
#define CMD_REMOVE_FRAME "CmdRemoveFrame"
#define CMD_MOVE_FRAME_BACKWARD "CmdMoveFrameBackward"
#define CMD_MOVE_FRAME_FORWARD "CmdMoveFrameForward"
+#define CMD_RETIME "CmdRetime"
#define CMD_TOOL_MOVE "CmdToolMove"
#define CMD_TOOL_SELECT "CmdToolSelect"
#define CMD_TOOL_BRUSH "CmdToolBrush"