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>
This commit is contained in:
parent
411a4cb67c
commit
50cfbd6112
@ -797,6 +797,13 @@ struct BezierEase : public QEasingCurveFunction
|
|||||||
return t3;
|
return t3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
qreal static inline findTForX(const SingleCubicBezier &singleCubicBezier, qreal x)
|
||||||
{
|
{
|
||||||
const qreal p0 = singleCubicBezier.p0x;
|
const qreal p0 = singleCubicBezier.p0x;
|
||||||
@ -809,6 +816,32 @@ struct BezierEase : public QEasingCurveFunction
|
|||||||
const qreal factorT1 = -3 * p0 + 3 * p1;
|
const qreal factorT1 = -3 * p0 + 3 * p1;
|
||||||
const qreal factorT0 = p0 - x;
|
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 a = factorT2 / factorT3;
|
||||||
const qreal b = factorT1 / factorT3;
|
const qreal b = factorT1 / factorT3;
|
||||||
const qreal c = factorT0 / factorT3;
|
const qreal c = factorT0 / factorT3;
|
||||||
|
@ -54,6 +54,7 @@ private slots:
|
|||||||
void testCbrtDouble();
|
void testCbrtDouble();
|
||||||
void testCbrtFloat();
|
void testCbrtFloat();
|
||||||
void cpp11();
|
void cpp11();
|
||||||
|
void quadraticEquation();
|
||||||
};
|
};
|
||||||
|
|
||||||
void tst_QEasingCurve::type()
|
void tst_QEasingCurve::type()
|
||||||
@ -804,5 +805,74 @@ void tst_QEasingCurve::cpp11()
|
|||||||
#endif
|
#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)
|
QTEST_MAIN(tst_QEasingCurve)
|
||||||
#include "tst_qeasingcurve.moc"
|
#include "tst_qeasingcurve.moc"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user