diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index 584d9f8be..cc5aa14c3 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + common/bezier.h + common/bezier.cpp common/channellayout.h common/clamp.h common/constructors.h diff --git a/app/common/bezier.cpp b/app/common/bezier.cpp new file mode 100644 index 000000000..9e287f9a7 --- /dev/null +++ b/app/common/bezier.cpp @@ -0,0 +1,42 @@ +#include "bezier.h" + +#include + +double Bezier::QuadraticXtoT(double x, double a, double b, double c) +{ + return (a - b + qSqrt(a*x + c*x - 2*b*x + qPow(b, 2) - a*c))/(a - 2*b + c); +} + +double Bezier::QuadraticTtoY(double a, double b, double c, double t) +{ + return qPow(1.0 - t, 2)*a + 2*(1.0 - t)*t*b + qPow(t, 2)*c; +} + +double Bezier::CubicXtoT(double x_target, double a, double b, double c, double d) +{ + double tolerance = 0.0001; + + double lower = 0.0; + double upper = 1.0; + + double percent = 0.5; + double x = CubicTtoY(a, b, c, d, percent); + + while (qAbs(x_target - x) > tolerance) { + if (x_target > x) { + lower = percent; + } else { + upper = percent; + } + + percent = (upper + lower) / 2.0; + x = CubicTtoY(a, b, c, d, percent); + } + + return percent; +} + +double Bezier::CubicTtoY(double a, double b, double c, double d, double t) +{ + return qPow(1.0 - t, 3)*a + 3*qPow(1.0 - t, 2)*t*b + 3*(1.0 - t)*qPow(t, 2)*c + qPow(t, 3)*d; +} diff --git a/app/common/bezier.h b/app/common/bezier.h new file mode 100644 index 000000000..b5991d1bd --- /dev/null +++ b/app/common/bezier.h @@ -0,0 +1,17 @@ +#ifndef BEZIER_H +#define BEZIER_H + + +class Bezier +{ +public: + static double QuadraticXtoT(double x, double a, double b, double c); + + static double QuadraticTtoY(double a, double b, double c, double t); + + static double CubicXtoT(double x_target, double a, double b, double c, double d); + + static double CubicTtoY(double a, double b, double c, double d, double t); +}; + +#endif // BEZIER_H diff --git a/app/node/input.cpp b/app/node/input.cpp index 17222030e..fcb32ed43 100644 --- a/app/node/input.cpp +++ b/app/node/input.cpp @@ -24,6 +24,7 @@ #include #include +#include "common/bezier.h" #include "common/lerp.h" #include "node.h" #include "output.h" @@ -126,11 +127,47 @@ QVariant NodeInput::get_value_at_time(const rational &time) const // We must interpolate between these keyframes if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { - // FIXME: Perform a cubic bezier interpolation - } else if (before->type() == NodeKeyframe::kLinear && after->type() == NodeKeyframe::kBezier) { - // FIXME: Perform a quadratic bezier interpolation with anchors from the AFTER keyframe - } else if (before->type() == NodeKeyframe::kLinear && after->type() == NodeKeyframe::kBezier) { - // FIXME: Perform a quadratic bezier interpolation with anchors from the BEFORE keyframe + // Perform a cubic bezier with two control points + + double t = Bezier::CubicXtoT(time.toDouble(), + before->time().toDouble(), + before->time().toDouble() + before->bezier_control_out().x(), + after->time().toDouble() + after->bezier_control_in().x(), + after->time().toDouble()); + + double y = Bezier::CubicTtoY(before->value().toDouble(), + before->value().toDouble() + before->bezier_control_out().y(), + after->value().toDouble() + after->bezier_control_in().y(), + after->value().toDouble(), + t); + + return y; + + } else if (before->type() == NodeKeyframe::kBezier || after->type() == NodeKeyframe::kBezier) { + // Perform a quadratic bezier with only one control point + + QPointF control_point; + double control_point_time; + double control_point_value; + + if (before->type() == NodeKeyframe::kBezier) { + control_point = before->bezier_control_out(); + control_point_time = before->time().toDouble() + control_point.x(); + control_point_value = before->value().toDouble() + control_point.y(); + } else { + control_point = after->bezier_control_in(); + control_point_time = after->time().toDouble() + control_point.x(); + control_point_value = after->value().toDouble() + control_point.y(); + } + + // Generate T from time values - used to determine bezier progress + double t = Bezier::QuadraticXtoT(time.toDouble(), before->time().toDouble(), control_point_time, after->time().toDouble()); + + // Generate value using T + double y = Bezier::QuadraticTtoY(before->value().toDouble(), control_point_value, after->value().toDouble(), t); + + return y; + } else { // To have arrived here, the keyframes must both be linear qreal period_progress = (time.toDouble() - before->time().toDouble()) / (after->time().toDouble() - before->time().toDouble()); @@ -231,6 +268,8 @@ void NodeInput::insert_keyframe(NodeKeyframePtr key) connect(key.get(), &NodeKeyframe::TimeChanged, this, &NodeInput::KeyframeTimeChanged); connect(key.get(), &NodeKeyframe::ValueChanged, this, &NodeInput::KeyframeValueChanged); connect(key.get(), &NodeKeyframe::TypeChanged, this, &NodeInput::KeyframeTypeChanged); + connect(key.get(), &NodeKeyframe::BezierControlInChanged, this, &NodeInput::KeyframeBezierInChanged); + connect(key.get(), &NodeKeyframe::BezierControlOutChanged, this, &NodeInput::KeyframeBezierOutChanged); emit KeyframeAdded(key); @@ -246,6 +285,8 @@ void NodeInput::remove_keyframe(NodeKeyframePtr key) disconnect(key.get(), &NodeKeyframe::TimeChanged, this, &NodeInput::KeyframeTimeChanged); disconnect(key.get(), &NodeKeyframe::ValueChanged, this, &NodeInput::KeyframeValueChanged); disconnect(key.get(), &NodeKeyframe::TypeChanged, this, &NodeInput::KeyframeTypeChanged); + disconnect(key.get(), &NodeKeyframe::BezierControlInChanged, this, &NodeInput::KeyframeBezierInChanged); + disconnect(key.get(), &NodeKeyframe::BezierControlOutChanged, this, &NodeInput::KeyframeBezierOutChanged); keyframes_.removeOne(key); @@ -299,6 +340,36 @@ void NodeInput::KeyframeTypeChanged() emit_time_range(get_range_around_index(keyframe_index)); } +void NodeInput::KeyframeBezierInChanged() +{ + NodeKeyframe* key = static_cast(sender()); + int keyframe_index = FindIndexOfKeyframeFromRawPtr(key); + + rational start = RATIONAL_MIN; + rational end = key->time(); + + if (keyframe_index > 0) { + start = keyframes_.at(keyframe_index - 1)->time(); + } + + emit ValueChanged(start, end); +} + +void NodeInput::KeyframeBezierOutChanged() +{ + NodeKeyframe* key = static_cast(sender()); + int keyframe_index = FindIndexOfKeyframeFromRawPtr(key); + + rational start = key->time(); + rational end = RATIONAL_MAX; + + if (keyframe_index < keyframes_.size() - 1) { + end = keyframes_.at(keyframe_index + 1)->time(); + } + + emit ValueChanged(start, end); +} + int NodeInput::FindIndexOfKeyframeFromRawPtr(NodeKeyframe *raw_ptr) const { for (int i=0;itool(); - rubberBandSelectionMode(); - if (event->button() == Qt::LeftButton) { QGraphicsView::mousePressEvent(event); @@ -203,8 +201,11 @@ QPointF KeyframeViewBase::GenerateBezierControlPosition(const NodeKeyframe::Bezi return new_bezier_pos; } -void KeyframeViewBase::ProcessBezierDrag(const QPointF& mouse_diff_scaled, bool include_opposing, bool undoable) +void KeyframeViewBase::ProcessBezierDrag(QPointF mouse_diff_scaled, bool include_opposing, bool undoable) { + // Flip the mouse Y because bezier control points are drawn bottom to top, not top to bottom + mouse_diff_scaled.setY(-mouse_diff_scaled.y()); + QPointF new_bezier_pos = GenerateBezierControlPosition(dragging_bezier_point_->mode(), dragging_bezier_point_start_, mouse_diff_scaled); diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 1df11f569..b1d1eaad2 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -38,7 +38,7 @@ private: const QPointF& start_point, const QPointF& scaled_cursor_diff); - void ProcessBezierDrag(const QPointF& mouse_diff_scaled, bool include_opposing, bool undoable); + void ProcessBezierDrag(QPointF mouse_diff_scaled, bool include_opposing, bool undoable); QPointF GetScaledCursorPos(const QPoint& cursor_pos);