diff options
author | Svenn-Arne Dragly <svenn-arne.dragly@qt.io> | 2018-03-14 16:36:09 +0100 |
---|---|---|
committer | Svenn-Arne Dragly <svenn-arne.dragly@qt.io> | 2018-03-26 15:53:10 +0000 |
commit | 50cfbd6112a2682228cdf34cd72b5abae967cdb2 (patch) | |
tree | 369c97df16b15c5f3bd86cc32e888571b1f3cf53 | |
parent | 411a4cb67cd3d976ddbd94b37a0ce936bfb223e5 (diff) |
Animation: Fix case where QEasingCurve::valueForProgress returns nan
Previously, we would divide by zero in BezierEase::findTForX if factorT3
was zero when solving the cubic equation.
This change fixes the problem by adding solutions for the special cases
where the cubic equation can be reduced to a quadratic or linear
equation.
This change also adds tests that cover cases where the equation becomes
quadratic, linear or invalid.
Task-number: QTBUG-67061
Change-Id: I2b59f7e0392eb807663c3c8927509fd8b226ebc7
Reviewed-by: Christian Stromme <christian.stromme@qt.io>
-rw-r--r-- | src/corelib/tools/qeasingcurve.cpp | 75 | ||||
-rw-r--r-- | tests/auto/corelib/tools/qeasingcurve/tst_qeasingcurve.cpp | 70 |
2 files changed, 124 insertions, 21 deletions
diff --git a/src/corelib/tools/qeasingcurve.cpp b/src/corelib/tools/qeasingcurve.cpp index 0b8fa4ca74..e66db58ed7 100644 --- a/src/corelib/tools/qeasingcurve.cpp +++ b/src/corelib/tools/qeasingcurve.cpp @@ -797,27 +797,60 @@ struct BezierEase : public QEasingCurveFunction return t3; } - qreal static inline findTForX(const SingleCubicBezier &singleCubicBezier, qreal x) - { - const qreal p0 = singleCubicBezier.p0x; - const qreal p1 = singleCubicBezier.p1x; - const qreal p2 = singleCubicBezier.p2x; - const qreal p3 = singleCubicBezier.p3x; - - const qreal factorT3 = p3 - p0 + 3 * p1 - 3 * p2; - const qreal factorT2 = 3 * p0 - 6 * p1 + 3 * p2; - const qreal factorT1 = -3 * p0 + 3 * p1; - const qreal factorT0 = p0 - x; - - const qreal a = factorT2 / factorT3; - const qreal b = factorT1 / factorT3; - const qreal c = factorT0 / factorT3; - - return singleRealSolutionForCubic(a, b, c); - - //one new iteration to increase numeric stability - //return newtonIteration(singleCubicBezier, t, x); - } + bool static inline almostZero(qreal value) + { + // 1e-3 might seem excessively fuzzy, but any smaller value will make the + // factors a, b, and c large enough to knock out the cubic solver. + return value > -1e-3 && value < 1e-3; + } + + qreal static inline findTForX(const SingleCubicBezier &singleCubicBezier, qreal x) + { + const qreal p0 = singleCubicBezier.p0x; + const qreal p1 = singleCubicBezier.p1x; + const qreal p2 = singleCubicBezier.p2x; + const qreal p3 = singleCubicBezier.p3x; + + const qreal factorT3 = p3 - p0 + 3 * p1 - 3 * p2; + const qreal factorT2 = 3 * p0 - 6 * p1 + 3 * p2; + const qreal factorT1 = -3 * p0 + 3 * p1; + const qreal factorT0 = p0 - x; + + // Cases for quadratic, linear and invalid equations + if (almostZero(factorT3)) { + if (almostZero(factorT2)) { + if (almostZero(factorT1)) + return 0.0; + + return -factorT0 / factorT1; + } + const qreal discriminant = factorT1 * factorT1 - 4.0 * factorT2 * factorT0; + if (discriminant < 0.0) + return 0.0; + + if (discriminant == 0.0) + return -factorT1 / (2.0 * factorT2); + + const qreal solution1 = (-factorT1 + std::sqrt(discriminant)) / (2.0 * factorT2); + if (solution1 >= 0.0 && solution1 <= 1.0) + return solution1; + + const qreal solution2 = (-factorT1 - std::sqrt(discriminant)) / (2.0 * factorT2); + if (solution2 >= 0.0 && solution2 <= 1.0) + return solution2; + + return 0.0; + } + + const qreal a = factorT2 / factorT3; + const qreal b = factorT1 / factorT3; + const qreal c = factorT0 / factorT3; + + return singleRealSolutionForCubic(a, b, c); + + //one new iteration to increase numeric stability + //return newtonIteration(singleCubicBezier, t, x); + } }; struct TCBEase : public BezierEase diff --git a/tests/auto/corelib/tools/qeasingcurve/tst_qeasingcurve.cpp b/tests/auto/corelib/tools/qeasingcurve/tst_qeasingcurve.cpp index 0714883855..79309f960d 100644 --- a/tests/auto/corelib/tools/qeasingcurve/tst_qeasingcurve.cpp +++ b/tests/auto/corelib/tools/qeasingcurve/tst_qeasingcurve.cpp @@ -54,6 +54,7 @@ private slots: void testCbrtDouble(); void testCbrtFloat(); void cpp11(); + void quadraticEquation(); }; void tst_QEasingCurve::type() @@ -804,5 +805,74 @@ void tst_QEasingCurve::cpp11() #endif } +void tst_QEasingCurve::quadraticEquation() { + // We find the value for a given time by solving a cubic equation. + // ax^3 + bx^2 + cx + d = 0 + // However, the solver also needs to take care of cases where a = 0, + // b = 0 or c = 0, and the equation becomes quadratic, linear or invalid. + // A naive cubic solver might divide by zero and return nan, even + // when the solution is a real number. + // This test should triggers those cases. + + { + // If the control points are spaced 1/3 apart of the distance of the + // start- and endpoint, the equation becomes linear. + QEasingCurve test(QEasingCurve::BezierSpline); + const qreal p1 = 1.0 / 3.0; + const qreal p2 = 1.0 - 1.0 / 3.0; + const qreal p3 = 1.0; + + test.addCubicBezierSegment(QPointF(p1, 0.0), QPointF(p2, 1.0), QPointF(p3, 1.0)); + QVERIFY(qAbs(test.valueForProgress(0.25) - 0.15625) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.5) - 0.5) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.75) - 0.84375) < 1e-6); + } + + { + // If both the start point and the first control point + // are placed a 0.0, and the second control point is + // placed at 1/3, we get a case where a = 0 and b != 0 + // i.e. a quadratic equation. + QEasingCurve test(QEasingCurve::BezierSpline); + const qreal p1 = 0.0; + const qreal p2 = 1.0 / 3.0; + const qreal p3 = 1.0; + test.addCubicBezierSegment(QPointF(p1, 0.0), QPointF(p2, 1.0), QPointF(p3, 1.0)); + QVERIFY(qAbs(test.valueForProgress(0.25) - 0.5) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.5) - 0.792893) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.75) - 0.950962) < 1e-6); + } + + { + // If both the start point and the first control point + // are placed a 0.0, and the second control point is + // placed close to 1/3, we get a case where a = ~0 and b != 0. + // It's not truly a quadratic equation, but should be treated + // as one, because it causes some cubic solvers to fail. + QEasingCurve test(QEasingCurve::BezierSpline); + const qreal p1 = 0.0; + const qreal p2 = 1.0 / 3.0 + 1e-6; + const qreal p3 = 1.0; + test.addCubicBezierSegment(QPointF(p1, 0.0), QPointF(p2, 1.0), QPointF(p3, 1.0)); + QVERIFY(qAbs(test.valueForProgress(0.25) - 0.499999) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.5) - 0.792892) < 1e-6); + QVERIFY(qAbs(test.valueForProgress(0.75) - 0.950961) < 1e-6); + } + + { + // A bad case, where the segment is of zero length. + // However, it might still happen in user code, + // and we should return a sensible answer. + QEasingCurve test(QEasingCurve::BezierSpline); + const qreal p0 = 0.0; + const qreal p1 = p0; + const qreal p2 = p0; + const qreal p3 = p0; + test.addCubicBezierSegment(QPointF(p1, 0.0), QPointF(p2, 1.0), QPointF(p3, 1.0)); + test.addCubicBezierSegment(QPointF(p3, 1.0), QPointF(1.0, 1.0), QPointF(1.0, 1.0)); + QCOMPARE(test.valueForProgress(0.0), 0.0); + } +} + QTEST_MAIN(tst_QEasingCurve) #include "tst_qeasingcurve.moc" |