diff options
author | vollick@chromium.org <vollick@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-15 01:12:55 +0000 |
---|---|---|
committer | vollick@chromium.org <vollick@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-15 01:12:55 +0000 |
commit | 2fcafa0b3526619bbb10f0aebd7e6a22ee9f965b (patch) | |
tree | 36b374f9fe0318987049f6d257eee4a3d5443070 /ui/gfx | |
parent | a6b0810ce805601284fe04881600ad4625de22ab (diff) | |
download | chromium_src-2fcafa0b3526619bbb10f0aebd7e6a22ee9f965b.zip chromium_src-2fcafa0b3526619bbb10f0aebd7e6a22ee9f965b.tar.gz chromium_src-2fcafa0b3526619bbb10f0aebd7e6a22ee9f965b.tar.bz2 |
Add support for blending matrices.
This CL adds matrix blending functionality as outlined here:
http://www.w3.org/TR/css3-3d-transforms/
BUG=159972
R=sky
Review URL: https://codereview.chromium.org/11293199
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@167809 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/gfx')
-rw-r--r-- | ui/gfx/transform.cc | 203 | ||||
-rw-r--r-- | ui/gfx/transform.h | 52 | ||||
-rw-r--r-- | ui/gfx/transform_unittest.cc | 697 | ||||
-rw-r--r-- | ui/gfx/transform_util.cc | 314 | ||||
-rw-r--r-- | ui/gfx/transform_util.h | 36 |
5 files changed, 1245 insertions, 57 deletions
diff --git a/ui/gfx/transform.cc b/ui/gfx/transform.cc index 1ca2021..e1d77c3 100644 --- a/ui/gfx/transform.cc +++ b/ui/gfx/transform.cc @@ -2,16 +2,35 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// MSVC++ requires this to be set before any other includes to get M_PI. +#define _USE_MATH_DEFINES + #include "ui/gfx/transform.h" +#include <cmath> + #include "ui/gfx/point.h" #include "ui/gfx/point3_f.h" -#include "ui/gfx/rect_f.h" +#include "ui/gfx/vector3d_f.h" +#include "ui/gfx/rect.h" #include "ui/gfx/safe_integer_conversions.h" #include "ui/gfx/skia_util.h" +#include "ui/gfx/transform_util.h" namespace gfx { +namespace { + +#define MGET(m, row, col) SkMScalarToDouble(m.get(row, col)) +#define MSET(m, row, col, value) m.set(row, col, SkMScalarToDouble(value)) + +double TanDegrees(double degrees) { + double radians = degrees * M_PI / 180; + return std::tan(radians); +} + +} // namespace + Transform::Transform() { matrix_.reset(); } @@ -41,11 +60,15 @@ void Transform::SetRotateAbout(const Point3F& axis, double degree) { } void Transform::SetScaleX(double x) { - matrix_.set(0, 0, SkDoubleToMScalar(x)); + MSET(matrix_, 0, 0, x); } void Transform::SetScaleY(double y) { - matrix_.set(1, 1, SkDoubleToMScalar(y)); + MSET(matrix_, 1, 1, y); +} + +void Transform::SetScaleZ(double z) { + MSET(matrix_, 2, 2, z); } void Transform::SetScale(double x, double y) { @@ -54,12 +77,22 @@ void Transform::SetScale(double x, double y) { matrix_.get(2, 2)); } +void Transform::SetScale3d(double x, double y, double z) { + matrix_.setScale(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); +} + void Transform::SetTranslateX(double x) { - matrix_.set(0, 3, SkDoubleToMScalar(x)); + MSET(matrix_, 0, 3, x); } void Transform::SetTranslateY(double y) { - matrix_.set(1, 3, SkDoubleToMScalar(y)); + MSET(matrix_, 1, 3, y); +} + +void Transform::SetTranslateZ(double z) { + MSET(matrix_, 2, 3, z); } void Transform::SetTranslate(double x, double y) { @@ -68,9 +101,23 @@ void Transform::SetTranslate(double x, double y) { matrix_.get(2, 3)); } +void Transform::SetTranslate3d(double x, double y, double z) { + matrix_.setTranslate(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); +} + +void Transform::SetSkewX(double angle) { + MSET(matrix_, 0, 1, TanDegrees(angle)); +} + +void Transform::SetSkewY(double angle) { + MSET(matrix_, 1, 0, TanDegrees(angle)); +} + void Transform::SetPerspectiveDepth(double depth) { SkMatrix44 m; - m.set(3, 2, SkDoubleToMScalar(-1 / depth)); + MSET(m, 3, 2, -1.0 / depth); matrix_ = m; } @@ -100,6 +147,14 @@ void Transform::ConcatScale(double x, double y) { matrix_.postConcat(scale); } +void Transform::ConcatScale3d(double x, double y, double z) { + SkMatrix44 scale; + scale.setScale(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); + matrix_.postConcat(scale); +} + void Transform::ConcatTranslate(double x, double y) { SkMatrix44 translate; translate.setTranslate(SkDoubleToMScalar(x), @@ -108,12 +163,100 @@ void Transform::ConcatTranslate(double x, double y) { matrix_.postConcat(translate); } +void Transform::ConcatTranslate3d(double x, double y, double z) { + SkMatrix44 translate; + translate.setTranslate(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); + matrix_.postConcat(translate); +} + +void Transform::ConcatSkewX(double angle_x) { + Transform t; + t.SetSkewX(angle_x); + matrix_.postConcat(t.matrix_); +} + +void Transform::ConcatSkewY(double angle_y) { + Transform t; + t.SetSkewY(angle_y); + matrix_.postConcat(t.matrix_); +} + void Transform::ConcatPerspectiveDepth(double depth) { SkMatrix44 m; - m.set(3, 2, SkDoubleToMScalar(-1 / depth)); + MSET(m, 3, 2, -1.0 / depth); matrix_.postConcat(m); } +void Transform::PreconcatRotate(double degree) { + SkMatrix44 rot; + rot.setRotateDegreesAbout(SkDoubleToMScalar(0), + SkDoubleToMScalar(0), + SkDoubleToMScalar(1), + SkDoubleToMScalar(degree)); + matrix_.preConcat(rot); +} + +void Transform::PreconcatRotateAbout(const Point3F& axis, double degree) { + SkMatrix44 rot; + rot.setRotateDegreesAbout(SkDoubleToMScalar(axis.x()), + SkDoubleToMScalar(axis.y()), + SkDoubleToMScalar(axis.z()), + SkDoubleToMScalar(degree)); + matrix_.preConcat(rot); +} + +void Transform::PreconcatScale(double x, double y) { + SkMatrix44 scale; + scale.setScale(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(1)); + matrix_.preConcat(scale); +} + +void Transform::PreconcatScale3d(double x, double y, double z) { + SkMatrix44 scale; + scale.setScale(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); + matrix_.preConcat(scale); +} + +void Transform::PreconcatTranslate(double x, double y) { + SkMatrix44 translate; + translate.setTranslate(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(0)); + matrix_.preConcat(translate); +} + +void Transform::PreconcatTranslate3d(double x, double y, double z) { + SkMatrix44 translate; + translate.setTranslate(SkDoubleToMScalar(x), + SkDoubleToMScalar(y), + SkDoubleToMScalar(z)); + matrix_.preConcat(translate); +} + +void Transform::PreconcatSkewX(double angle_x) { + Transform t; + t.SetSkewX(angle_x); + matrix_.preConcat(t.matrix_); +} + +void Transform::PreconcatSkewY(double angle_y) { + Transform t; + t.SetSkewY(angle_y); + matrix_.preConcat(t.matrix_); +} + +void Transform::PreconcatPerspectiveDepth(double depth) { + SkMatrix44 m; + MSET(m, 3, 2, -1.0 / depth); + matrix_.preConcat(m); +} + void Transform::PreconcatTransform(const Transform& transform) { if (!transform.matrix_.isIdentity()) { matrix_.preConcat(transform.matrix_); @@ -180,16 +323,38 @@ bool Transform::TransformRectReverse(RectF* rect) const { return true; } +bool Transform::Blend(const Transform& from, double progress) { + if (progress <= 0.0) { + *this = from; + return true; + } + + if (progress >= 1.0) + return true; + + DecomposedTransform to_decomp; + DecomposedTransform from_decomp; + if (!DecomposeTransform(&to_decomp, *this) || + !DecomposeTransform(&from_decomp, from)) + return false; + + if (!BlendDecomposedTransforms(&to_decomp, to_decomp, from_decomp, progress)) + return false; + + matrix_ = ComposeTransform(to_decomp).matrix(); + return true; +} + void Transform::TransformPointInternal(const SkMatrix44& xform, Point3F& point) const { - SkScalar p[4] = { - SkDoubleToScalar(point.x()), - SkDoubleToScalar(point.y()), - SkDoubleToScalar(point.z()), - SkDoubleToScalar(1) + SkMScalar p[4] = { + SkDoubleToMScalar(point.x()), + SkDoubleToMScalar(point.y()), + SkDoubleToMScalar(point.z()), + SkDoubleToMScalar(1) }; - xform.map(p); + xform.mapMScalars(p); if (p[3] != 1 && abs(p[3]) > 0) { point.SetPoint(p[0] / p[3], p[1] / p[3], p[2]/ p[3]); @@ -200,14 +365,14 @@ void Transform::TransformPointInternal(const SkMatrix44& xform, void Transform::TransformPointInternal(const SkMatrix44& xform, Point& point) const { - SkScalar p[4] = { - SkIntToScalar(point.x()), - SkIntToScalar(point.y()), - SkIntToScalar(0), - SkIntToScalar(1) + SkMScalar p[4] = { + SkDoubleToMScalar(point.x()), + SkDoubleToMScalar(point.y()), + SkDoubleToMScalar(0), + SkDoubleToMScalar(1) }; - xform.map(p); + xform.mapMScalars(p); point.SetPoint(ToRoundedInt(p[0]), ToRoundedInt(p[1])); } diff --git a/ui/gfx/transform.h b/ui/gfx/transform.h index 57fa903..45bc4da 100644 --- a/ui/gfx/transform.h +++ b/ui/gfx/transform.h @@ -42,12 +42,20 @@ class UI_EXPORT Transform { // Sets the scaling parameters. void SetScaleX(double x); void SetScaleY(double y); + void SetScaleZ(double z); void SetScale(double x, double y); + void SetScale3d(double x, double y, double z); // Sets the translation parameters. void SetTranslateX(double x); void SetTranslateY(double y); + void SetTranslateZ(double z); void SetTranslate(double x, double y); + void SetTranslate3d(double x, double y, double z); + + // Sets the skew parameters. + void SetSkewX(double angle); + void SetSkewY(double angle); // Creates a perspective matrix. // Based on the 'perspective' operation from @@ -62,13 +70,46 @@ class UI_EXPORT Transform { // Applies scaling on current transform. void ConcatScale(double x, double y); + void ConcatScale3d(double x, double y, double z); // Applies translation on current transform. void ConcatTranslate(double x, double y); + void ConcatTranslate3d(double x, double y, double z); // Applies a perspective on current transform. void ConcatPerspectiveDepth(double depth); + // Applies a skew on the current transform. + void ConcatSkewX(double angle_x); + void ConcatSkewY(double angle_y); + + // Applies the current transformation on a rotation and assigns the result + // to |this|. + void PreconcatRotate(double degree); + + // Applies the current transformation on an axis-angle rotation and assigns + // the result to |this|. + void PreconcatRotateAbout(const Point3F& point, double degree); + + // Applies the current transformation on a scaling and assigns the result + // to |this|. + void PreconcatScale(double x, double y); + void PreconcatScale3d(double x, double y, double z); + + // Applies the current transformation on a translation and assigns the result + // to |this|. + void PreconcatTranslate(double x, double y); + void PreconcatTranslate3d(double x, double y, double z); + + // Applies the current transformation on a skew and assigns the result + // to |this|. + void PreconcatSkewX(double angle_x); + void PreconcatSkewY(double angle_y); + + // Applies the current transformation on a perspective transform and assigns + // the result to |this|. + void PreconcatPerspectiveDepth(double depth); + // Applies a transformation on the current transformation // (i.e. 'this = this * transform;'). void PreconcatTransform(const Transform& transform); @@ -110,6 +151,17 @@ class UI_EXPORT Transform { // transformed rect. bool TransformRectReverse(RectF* rect) const; + // Decomposes |this| and |from|, interpolates the decomposed values, and + // sets |this| to the reconstituted result. Returns false if either matrix + // can't be decomposed. Uses routines described in this spec: + // http://www.w3.org/TR/css3-3d-transforms/. + // + // Note: this call is expensive since we need to decompose the transform. If + // you're going to be calling this rapidly (e.g., in an animation) you should + // decompose once using gfx::DecomposeTransforms and reuse your + // DecomposedTransform. + bool Blend(const Transform& from, double progress); + // Returns the underlying matrix. const SkMatrix44& matrix() const { return matrix_; } SkMatrix44& matrix() { return matrix_; } diff --git a/ui/gfx/transform_unittest.cc b/ui/gfx/transform_unittest.cc index 52bb1c6..9518c66 100644 --- a/ui/gfx/transform_unittest.cc +++ b/ui/gfx/transform_unittest.cc @@ -2,8 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// MSVC++ requires this to be set before any other includes to get M_PI. +#define _USE_MATH_DEFINES + #include "ui/gfx/transform.h" +#include <cmath> #include <ostream> #include <limits> @@ -11,17 +15,85 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/gfx/point.h" #include "ui/gfx/point3_f.h" +#include "ui/gfx/transform_util.h" + +namespace gfx { namespace { -bool PointsAreNearlyEqual(const gfx::Point3F& lhs, - const gfx::Point3F& rhs) { +bool PointsAreNearlyEqual(const Point3F& lhs, + const Point3F& rhs) { float epsilon = 0.0001f; return lhs.SquaredDistanceTo(rhs) < epsilon; } +bool MatricesAreNearlyEqual(const Transform& lhs, + const Transform& rhs) { + float epsilon = 0.0001f; + for (int row = 0; row < 4; ++row) { + for (int col = 0; col < 4; ++col) { + if (std::abs(lhs.matrix().get(row, col) - + rhs.matrix().get(row, col)) > epsilon) + return false; + } + } + return true; +} + +#define EXPECT_ROW1_EQ(a, b, c, d, transform) \ + EXPECT_FLOAT_EQ((a), (transform).matrix().get(0, 0)); \ + EXPECT_FLOAT_EQ((b), (transform).matrix().get(0, 1)); \ + EXPECT_FLOAT_EQ((c), (transform).matrix().get(0, 2)); \ + EXPECT_FLOAT_EQ((d), (transform).matrix().get(0, 3)); + +#define EXPECT_ROW2_EQ(a, b, c, d, transform) \ + EXPECT_FLOAT_EQ((a), (transform).matrix().get(1, 0)); \ + EXPECT_FLOAT_EQ((b), (transform).matrix().get(1, 1)); \ + EXPECT_FLOAT_EQ((c), (transform).matrix().get(1, 2)); \ + EXPECT_FLOAT_EQ((d), (transform).matrix().get(1, 3)); + +#define EXPECT_ROW3_EQ(a, b, c, d, transform) \ + EXPECT_FLOAT_EQ((a), (transform).matrix().get(2, 0)); \ + EXPECT_FLOAT_EQ((b), (transform).matrix().get(2, 1)); \ + EXPECT_FLOAT_EQ((c), (transform).matrix().get(2, 2)); \ + EXPECT_FLOAT_EQ((d), (transform).matrix().get(2, 3)); + +#define EXPECT_ROW4_EQ(a, b, c, d, transform) \ + EXPECT_FLOAT_EQ((a), (transform).matrix().get(3, 0)); \ + EXPECT_FLOAT_EQ((b), (transform).matrix().get(3, 1)); \ + EXPECT_FLOAT_EQ((c), (transform).matrix().get(3, 2)); \ + EXPECT_FLOAT_EQ((d), (transform).matrix().get(3, 3)); \ + +// Checking float values for equality close to zero is not robust using +// EXPECT_FLOAT_EQ (see gtest documentation). So, to verify rotation matrices, +// we must use a looser absolute error threshold in some places. +#define EXPECT_ROW1_NEAR(a, b, c, d, transform, errorThreshold) \ + EXPECT_NEAR((a), (transform).matrix().get(0, 0), (errorThreshold)); \ + EXPECT_NEAR((b), (transform).matrix().get(0, 1), (errorThreshold)); \ + EXPECT_NEAR((c), (transform).matrix().get(0, 2), (errorThreshold)); \ + EXPECT_NEAR((d), (transform).matrix().get(0, 3), (errorThreshold)); + +#define EXPECT_ROW2_NEAR(a, b, c, d, transform, errorThreshold) \ + EXPECT_NEAR((a), (transform).matrix().get(1, 0), (errorThreshold)); \ + EXPECT_NEAR((b), (transform).matrix().get(1, 1), (errorThreshold)); \ + EXPECT_NEAR((c), (transform).matrix().get(1, 2), (errorThreshold)); \ + EXPECT_NEAR((d), (transform).matrix().get(1, 3), (errorThreshold)); + +#define EXPECT_ROW3_NEAR(a, b, c, d, transform, errorThreshold) \ + EXPECT_NEAR((a), (transform).matrix().get(2, 0), (errorThreshold)); \ + EXPECT_NEAR((b), (transform).matrix().get(2, 1), (errorThreshold)); \ + EXPECT_NEAR((c), (transform).matrix().get(2, 2), (errorThreshold)); \ + EXPECT_NEAR((d), (transform).matrix().get(2, 3), (errorThreshold)); + +#ifdef SK_MSCALAR_IS_DOUBLE +#define ERROR_THRESHOLD 1e-14 +#else +#define ERROR_THRESHOLD 1e-7 +#endif +#define LOOSE_ERROR_THRESHOLD 1e-7 + TEST(XFormTest, Equality) { - gfx::Transform lhs, rhs, interpolated; + Transform lhs, rhs, interpolated; rhs.matrix().set3x3(1, 2, 3, 4, 5, 6, 7, 8, 9); @@ -41,8 +113,8 @@ TEST(XFormTest, Equality) { EXPECT_TRUE(rhs != interpolated); } } - lhs = gfx::Transform(); - rhs = gfx::Transform(); + lhs = Transform(); + rhs = Transform(); for (int i = 1; i < 100; ++i) { lhs.SetTranslate(i, i); rhs.SetTranslate(-i, -i); @@ -70,12 +142,12 @@ TEST(XFormTest, ConcatTranslate) { 10, 20 }, }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatTranslate(value.tx, value.ty); - gfx::Point3F p1(value.x1, value.y1, 0); - gfx::Point3F p2(value.x2, value.y2, 0); + Point3F p1(value.x1, value.y1, 0); + Point3F p2(value.x2, value.y2, 0); xform.TransformPoint(p1); if (value.tx == value.tx && value.ty == value.ty) { @@ -97,12 +169,12 @@ TEST(XFormTest, ConcatScale) { { 1, std::numeric_limits<float>::quiet_NaN(), 1 } }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatScale(value.scale, value.scale); - gfx::Point3F p1(value.before, value.before, 0); - gfx::Point3F p2(value.after, value.after, 0); + Point3F p1(value.before, value.before, 0); + Point3F p2(value.after, value.after, 0); xform.TransformPoint(p1); if (value.scale == value.scale) { EXPECT_TRUE(PointsAreNearlyEqual(p1, p2)); @@ -126,12 +198,12 @@ TEST(XFormTest, ConcatRotate) { { 1, 0, std::numeric_limits<float>::quiet_NaN(), 1, 0 } }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatRotate(value.degrees); - gfx::Point3F p1(value.x1, value.y1, 0); - gfx::Point3F p2(value.x2, value.y2, 0); + Point3F p1(value.x1, value.y1, 0); + Point3F p2(value.x2, value.y2, 0); xform.TransformPoint(p1); if (value.degrees == value.degrees) { EXPECT_TRUE(PointsAreNearlyEqual(p1, p2)); @@ -157,8 +229,8 @@ TEST(XFormTest, SetTranslate) { for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; for (int k = 0; k < 3; ++k) { - gfx::Point3F p0, p1, p2; - gfx::Transform xform; + Point3F p0, p1, p2; + Transform xform; switch (k) { case 0: p1.SetPoint(value.x1, 0, 0); @@ -204,8 +276,8 @@ TEST(XFormTest, SetScale) { for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; for (int k = 0; k < 3; ++k) { - gfx::Point3F p0, p1, p2; - gfx::Transform xform; + Point3F p0, p1, p2; + Transform xform; switch (k) { case 0: p1.SetPoint(value.before, 0, 0); @@ -256,11 +328,11 @@ TEST(XFormTest, SetRotate) { for (size_t i = 0; i < ARRAYSIZE_UNSAFE(set_rotate_cases); ++i) { const SetRotateCase& value = set_rotate_cases[i]; - gfx::Point3F p0; - gfx::Point3F p1(value.x, value.y, 0); - gfx::Point3F p2(value.xprime, value.yprime, 0); + Point3F p0; + Point3F p1(value.x, value.y, 0); + Point3F p2(value.xprime, value.yprime, 0); p0 = p1; - gfx::Transform xform; + Transform xform; xform.SetRotate(value.degree); // just want to make sure that we don't crash in the case of NaN. if (value.degree == value.degree) { @@ -291,12 +363,12 @@ TEST(XFormTest, ConcatTranslate2D) { 10, 20}, }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatTranslate(value.tx, value.ty); - gfx::Point p1(value.x1, value.y1); - gfx::Point p2(value.x2, value.y2); + Point p1(value.x1, value.y1); + Point p2(value.x2, value.y2); xform.TransformPoint(p1); if (value.tx == value.tx && value.ty == value.ty) { @@ -319,12 +391,12 @@ TEST(XFormTest, ConcatScale2D) { { 1, std::numeric_limits<float>::quiet_NaN(), 1} }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatScale(value.scale, value.scale); - gfx::Point p1(value.before, value.before); - gfx::Point p2(value.after, value.after); + Point p1(value.before, value.before); + Point p2(value.after, value.after); xform.TransformPoint(p1); if (value.scale == value.scale) { EXPECT_EQ(p1.x(), p2.x()); @@ -349,12 +421,12 @@ TEST(XFormTest, ConcatRotate2D) { { 1, 0, std::numeric_limits<float>::quiet_NaN(), 1, 0} }; - gfx::Transform xform; + Transform xform; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_cases); ++i) { const TestCase& value = test_cases[i]; xform.ConcatRotate(value.degrees); - gfx::Point p1(value.x1, value.y1); - gfx::Point p2(value.x2, value.y2); + Point p1(value.x1, value.y1); + Point p2(value.x2, value.y2); xform.TransformPoint(p1); if (value.degrees == value.degrees) { EXPECT_EQ(p1.x(), p2.x()); @@ -383,8 +455,8 @@ TEST(XFormTest, SetTranslate2D) { for (int j = -1; j < 2; ++j) { for (int k = 0; k < 3; ++k) { float epsilon = 0.0001f; - gfx::Point p0, p1, p2; - gfx::Transform xform; + Point p0, p1, p2; + Transform xform; switch (k) { case 0: p1.SetPoint(value.x1, 0); @@ -436,8 +508,8 @@ TEST(XFormTest, SetScale2D) { for (int j = -1; j < 2; ++j) { for (int k = 0; k < 3; ++k) { float epsilon = 0.0001f; - gfx::Point p0, p1, p2; - gfx::Transform xform; + Point p0, p1, p2; + Transform xform; switch (k) { case 0: p1.SetPoint(value.before, 0); @@ -496,8 +568,8 @@ TEST(XFormTest, SetRotate2D) { const SetRotateCase& value = set_rotate_cases[i]; for (int j = 1; j >= -1; --j) { float epsilon = 0.1f; - gfx::Point pt(value.x, value.y); - gfx::Transform xform; + Point pt(value.x, value.y); + Transform xform; // should be invariant to small floating point errors. xform.SetRotate(value.degree + j * epsilon); // just want to make sure that we don't crash in the case of NaN. @@ -513,4 +585,553 @@ TEST(XFormTest, SetRotate2D) { } } -} // namespace +TEST(XFormTest, BlendTranslate) { + Transform from; + for (int i = 0; i < 10; ++i) { + Transform to; + to.SetTranslate3d(1, 1, 1); + double t = i / 9.0; + EXPECT_TRUE(to.Blend(from, t)); + EXPECT_FLOAT_EQ(t, to.matrix().get(0, 3)); + EXPECT_FLOAT_EQ(t, to.matrix().get(1, 3)); + EXPECT_FLOAT_EQ(t, to.matrix().get(2, 3)); + } +} + +TEST(XFormTest, BlendRotate) { + Point3F axes[] = { + Point3F(1, 0, 0), + Point3F(0, 1, 0), + Point3F(0, 0, 1), + Point3F(1, 1, 1) + }; + Transform from; + for (size_t index = 0; index < ARRAYSIZE_UNSAFE(axes); ++index) { + for (int i = 0; i < 10; ++i) { + Transform to; + to.SetRotateAbout(axes[index], 90); + double t = i / 9.0; + EXPECT_TRUE(to.Blend(from, t)); + + Transform expected; + expected.SetRotateAbout(axes[index], 90 * t); + + EXPECT_TRUE(MatricesAreNearlyEqual(expected, to)); + } + } +} + +TEST(XFormTest, CannotBlend180DegreeRotation) { + Point3F axes[] = { + Point3F(1, 0, 0), + Point3F(0, 1, 0), + Point3F(0, 0, 1), + Point3F(1, 1, 1) + }; + Transform from; + for (size_t index = 0; index < ARRAYSIZE_UNSAFE(axes); ++index) { + Transform to; + to.SetRotateAbout(axes[index], 180); + EXPECT_FALSE(to.Blend(from, 0.5)); + } +} + +TEST(XFormTest, BlendScale) { + Transform from; + for (int i = 0; i < 10; ++i) { + Transform to; + to.SetScale3d(5, 4, 3); + double t = i / 9.0; + EXPECT_TRUE(to.Blend(from, t)); + EXPECT_FLOAT_EQ(t * 4 + 1, to.matrix().get(0, 0)); + EXPECT_FLOAT_EQ(t * 3 + 1, to.matrix().get(1, 1)); + EXPECT_FLOAT_EQ(t * 2 + 1, to.matrix().get(2, 2)); + } +} + +TEST(XFormTest, BlendSkew) { + Transform from; + for (int i = 0; i < 2; ++i) { + Transform to; + to.SetSkewX(20); + to.PreconcatSkewY(10); + double t = i; + Transform expected; + expected.SetSkewX(t * 20); + expected.PreconcatSkewY(t * 10); + EXPECT_TRUE(to.Blend(from, t)); + EXPECT_TRUE(MatricesAreNearlyEqual(expected, to)); + } +} + +TEST(XFormTest, BlendPerspective) { + Transform from; + from.SetPerspectiveDepth(200); + for (int i = 0; i < 2; ++i) { + Transform to; + to.SetPerspectiveDepth(800); + double t = i; + Transform expected; + expected.SetPerspectiveDepth(t * 600 + 200); + EXPECT_TRUE(to.Blend(from, t)); + EXPECT_TRUE(MatricesAreNearlyEqual(expected, to)); + } +} + +TEST(XFormTest, BlendIdentity) { + Transform from; + Transform to; + EXPECT_TRUE(to.Blend(from, 0.5)); + EXPECT_EQ(to, from); +} + +TEST(XFormTest, CannotBlendSingularMatrix) { + Transform from; + Transform to; + to.matrix().set(1, 1, SkDoubleToMScalar(0)); + EXPECT_FALSE(to.Blend(from, 0.5)); +} + +TEST(XFormTest, VerifyBlendForTranslation) +{ + Transform from; + from.PreconcatTranslate3d(100, 200, 100); + + Transform to; + + to.PreconcatTranslate3d(200, 100, 300); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + to = Transform(); + to.PreconcatTranslate3d(200, 100, 300); + to.Blend(from, 0.25); + EXPECT_ROW1_EQ(1, 0, 0, 125, to); + EXPECT_ROW2_EQ(0, 1, 0, 175, to); + EXPECT_ROW3_EQ(0, 0, 1, 150, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatTranslate3d(200, 100, 300); + to.Blend(from, 0.5); + EXPECT_ROW1_EQ(1, 0, 0, 150, to); + EXPECT_ROW2_EQ(0, 1, 0, 150, to); + EXPECT_ROW3_EQ(0, 0, 1, 200, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatTranslate3d(200, 100, 300); + to.Blend(from, 1); + EXPECT_ROW1_EQ(1, 0, 0, 200, to); + EXPECT_ROW2_EQ(0, 1, 0, 100, to); + EXPECT_ROW3_EQ(0, 0, 1, 300, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForScale) +{ + Transform from; + from.PreconcatScale3d(100, 200, 100); + + Transform to; + + to.PreconcatScale3d(200, 100, 300); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + to = Transform(); + to.PreconcatScale3d(200, 100, 300); + to.Blend(from, 0.25); + EXPECT_ROW1_EQ(125, 0, 0, 0, to); + EXPECT_ROW2_EQ(0, 175, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 150, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatScale3d(200, 100, 300); + to.Blend(from, 0.5); + EXPECT_ROW1_EQ(150, 0, 0, 0, to); + EXPECT_ROW2_EQ(0, 150, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 200, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatScale3d(200, 100, 300); + to.Blend(from, 1); + EXPECT_ROW1_EQ(200, 0, 0, 0, to); + EXPECT_ROW2_EQ(0, 100, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 300, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForSkewX) +{ + Transform from; + from.PreconcatSkewX(0); + + Transform to; + + to.PreconcatSkewX(45); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + to = Transform(); + to.PreconcatSkewX(45); + to.Blend(from, 0.5); + EXPECT_ROW1_EQ(1, 0.5, 0, 0, to); + EXPECT_ROW2_EQ(0, 1, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatSkewX(45); + to.Blend(from, 0.25); + EXPECT_ROW1_EQ(1, 0.25, 0, 0, to); + EXPECT_ROW2_EQ(0, 1, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatSkewX(45); + to.Blend(from, 1); + EXPECT_ROW1_EQ(1, 1, 0, 0, to); + EXPECT_ROW2_EQ(0, 1, 0, 0, to); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForSkewY) +{ + // NOTE CAREFULLY: Decomposition of skew and rotation terms of the matrix + // is inherently underconstrained, and so it does not always compute the + // originally intended skew parameters. The current implementation uses QR + // decomposition, which decomposes the shear into a rotation + non-uniform + // scale. + // + // It is unlikely that the decomposition implementation will need to change + // very often, so to get any test coverage, the compromise is to verify the + // exact matrix that the.Blend() operation produces. + // + // This problem also potentially exists for skewX, but the current QR + // decomposition implementation just happens to decompose those test + // matrices intuitively. + // + // Unfortunately, this case suffers from uncomfortably large precision + // error. + + Transform from; + from.PreconcatSkewY(0); + + Transform to; + + to.PreconcatSkewY(45); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + to = Transform(); + to.PreconcatSkewY(45); + to.Blend(from, 0.25); + EXPECT_ROW1_NEAR(1.0823489449280947471976333, + 0.0464370719145053845178239, + 0, + 0, + to, + LOOSE_ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0.2152925909665224513123150, + 0.9541702441750861130032035, + 0, + 0, + to, + LOOSE_ERROR_THRESHOLD); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatSkewY(45); + to.Blend(from, 0.5); + EXPECT_ROW1_NEAR(1.1152212925809066312865525, + 0.0676495144007326631996335, + 0, + 0, + to, + LOOSE_ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0.4619397844342648662419037, + 0.9519009045724774464858342, + 0, + 0, + to, + LOOSE_ERROR_THRESHOLD); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatSkewY(45); + to.Blend(from, 1); + EXPECT_ROW1_NEAR(1, 0, 0, 0, to, LOOSE_ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(1, 1, 0, 0, to, LOOSE_ERROR_THRESHOLD); + EXPECT_ROW3_EQ(0, 0, 1, 0, to); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForRotationAboutX) +{ + // Even though.Blending uses quaternions, axis-aligned rotations should. + // Blend the same with quaternions or Euler angles. So we can test + // rotation.Blending by comparing against manually specified matrices from + // Euler angles. + + Transform from; + from.PreconcatRotateAbout(Point3F(1, 0, 0), 0); + + Transform to; + + to.PreconcatRotateAbout(Point3F(1, 0, 0), 90); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + double expectedRotationAngle = 22.5 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(1, 0, 0), 90); + to.Blend(from, 0.25); + EXPECT_ROW1_NEAR(1, 0, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, + std::cos(expectedRotationAngle), + -std::sin(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, + std::sin(expectedRotationAngle), + std::cos(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + expectedRotationAngle = 45 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(1, 0, 0), 90); + to.Blend(from, 0.5); + EXPECT_ROW1_NEAR(1, 0, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, + std::cos(expectedRotationAngle), + -std::sin(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, + std::sin(expectedRotationAngle), + std::cos(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatRotateAbout(Point3F(1, 0, 0), 90); + to.Blend(from, 1); + EXPECT_ROW1_NEAR(1, 0, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, 0, -1, 0, to, ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, 1, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForRotationAboutY) +{ + Transform from; + from.PreconcatRotateAbout(Point3F(0, 1, 0), 0); + + Transform to; + + to.PreconcatRotateAbout(Point3F(0, 1, 0), 90); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + double expectedRotationAngle = 22.5 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 1, 0), 90); + to.Blend(from, 0.25); + EXPECT_ROW1_NEAR(std::cos(expectedRotationAngle), + 0, + std::sin(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, 1, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(-std::sin(expectedRotationAngle), + 0, + std::cos(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + expectedRotationAngle = 45 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 1, 0), 90); + to.Blend(from, 0.5); + EXPECT_ROW1_NEAR(std::cos(expectedRotationAngle), + 0, + std::sin(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, 1, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(-std::sin(expectedRotationAngle), + 0, + std::cos(expectedRotationAngle), + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 1, 0), 90); + to.Blend(from, 1); + EXPECT_ROW1_NEAR(0, 0, 1, 0, to, ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(0, 1, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(-1, 0, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForRotationAboutZ) +{ + Transform from; + from.PreconcatRotateAbout(Point3F(0, 0, 1), 0); + + Transform to; + + to.PreconcatRotateAbout(Point3F(0, 0, 1), 90); + to.Blend(from, 0); + EXPECT_EQ(from, to); + + double expectedRotationAngle = 22.5 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 0, 1), 90); + to.Blend(from, 0.25); + EXPECT_ROW1_NEAR(std::cos(expectedRotationAngle), + -std::sin(expectedRotationAngle), + 0, + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(std::sin(expectedRotationAngle), + std::cos(expectedRotationAngle), + 0, + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, 0, 1, 0, to, ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + expectedRotationAngle = 45 * M_PI / 180.0; + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 0, 1), 90); + to.Blend(from, 0.5); + EXPECT_ROW1_NEAR(std::cos(expectedRotationAngle), + -std::sin(expectedRotationAngle), + 0, + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(std::sin(expectedRotationAngle), + std::cos(expectedRotationAngle), + 0, + 0, + to, + ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, 0, 1, 0, to, ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); + + to = Transform(); + to.PreconcatRotateAbout(Point3F(0, 0, 1), 90); + to.Blend(from, 1); + EXPECT_ROW1_NEAR(0, -1, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW2_NEAR(1, 0, 0, 0, to, ERROR_THRESHOLD); + EXPECT_ROW3_NEAR(0, 0, 1, 0, to, ERROR_THRESHOLD); + EXPECT_ROW4_EQ(0, 0, 0, 1, to); +} + +TEST(XFormTest, VerifyBlendForCompositeTransform) +{ + // Verify that the.Blending was done with a decomposition in correct order + // by blending a composite transform. Using matrix x vector notation + // (Ax = b, where x is column vector), the ordering should be: + // perspective * translation * rotation * skew * scale + // + // It is not as important (or meaningful) to check intermediate + // interpolations; order of operations will be tested well enough by the + // end cases that are easier to specify. + + Transform from; + Transform to; + + Transform expectedEndOfAnimation; + expectedEndOfAnimation.PreconcatPerspectiveDepth(1); + expectedEndOfAnimation.PreconcatTranslate3d(10, 20, 30); + expectedEndOfAnimation.PreconcatRotateAbout(Point3F(0, 0, 1), 25); + expectedEndOfAnimation.PreconcatSkewY(45); + expectedEndOfAnimation.PreconcatScale3d(6, 7, 8); + + to = expectedEndOfAnimation; + to.Blend(from, 0); + EXPECT_EQ(from, to); + + to = expectedEndOfAnimation; + // We short circuit if blend is >= 1, so to check the numerics, we will + // check that we get close to what we expect when we're nearly done + // interpolating. + to.Blend(from, .99999); + + // Recomposing the matrix results in a normalized matrix, so to verify we + // need to normalize the expectedEndOfAnimation before comparing elements. + // Normalizing means dividing everything by expectedEndOfAnimation.m44(). + Transform normalizedExpectedEndOfAnimation = expectedEndOfAnimation; + Transform normalizationMatrix; + normalizationMatrix.matrix().set( + 0, 0, SkDoubleToMScalar(1 / expectedEndOfAnimation.matrix().get(3, 3))); + normalizationMatrix.matrix().set( + 1, 1, SkDoubleToMScalar(1 / expectedEndOfAnimation.matrix().get(3, 3))); + normalizationMatrix.matrix().set( + 2, 2, SkDoubleToMScalar(1 / expectedEndOfAnimation.matrix().get(3, 3))); + normalizationMatrix.matrix().set( + 3, 3, SkDoubleToMScalar(1 / expectedEndOfAnimation.matrix().get(3, 3))); + normalizedExpectedEndOfAnimation.PreconcatTransform(normalizationMatrix); + + EXPECT_TRUE(MatricesAreNearlyEqual(normalizedExpectedEndOfAnimation, to)); +} + +TEST(XFormTest, SetScaleZ) +{ + Transform scaled; + scaled.SetScaleZ(2); + EXPECT_FLOAT_EQ(2, scaled.matrix().get(2, 2)); +} + +TEST(XFormTest, SetTranslateZ) +{ + Transform translated; + translated.SetTranslateZ(2); + EXPECT_FLOAT_EQ(2, translated.matrix().get(2, 3)); +} + +TEST(XFormTest, DecomposedTransformCtor) +{ + DecomposedTransform decomp; + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(0.0, decomp.translate[i]); + EXPECT_EQ(1.0, decomp.scale[i]); + EXPECT_EQ(0.0, decomp.skew[i]); + EXPECT_EQ(0.0, decomp.quaternion[i]); + EXPECT_EQ(0.0, decomp.perspective[i]); + } + EXPECT_EQ(1.0, decomp.quaternion[3]); + EXPECT_EQ(1.0, decomp.perspective[3]); + Transform identity; + Transform composed = ComposeTransform(decomp); + EXPECT_TRUE(MatricesAreNearlyEqual(identity, composed)); +} + +} // namespace + +} // namespace gfx diff --git a/ui/gfx/transform_util.cc b/ui/gfx/transform_util.cc index cae1124..9a1551a 100644 --- a/ui/gfx/transform_util.cc +++ b/ui/gfx/transform_util.cc @@ -4,10 +4,111 @@ #include "ui/gfx/transform_util.h" +#include <cmath> + #include "ui/gfx/point.h" namespace gfx { +namespace { + +#define MGET(m, row, col) SkMScalarToDouble(m.get(row, col)) +#define MSET(m, row, col, value) m.set(row, col, SkMScalarToDouble(value)) + +// TODO(vollick): Remove this once SkMatrix44::transpose has landed. +SkMatrix44 Transpose(const SkMatrix44& m) { + SkMatrix44 to_return; + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 4; ++j) + to_return.set(i, j, m.get(j, i)); + return to_return; +} + +double Length3(double v[3]) { + return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); +} + +void Scale3(double v[3], double scale) { + for (int i = 0; i < 3; ++i) + v[i] *= scale; +} + +template <int n> +double Dot(const double* a, const double* b) { + double toReturn = 0; + for (int i = 0; i < n; ++i) + toReturn += a[i] * b[i]; + return toReturn; +} + +template <int n> +void Combine(double* out, + const double* a, + const double* b, + double scale_a, + double scale_b) { + for (int i = 0; i < n; ++i) + out[i] = a[i] * scale_a + b[i] * scale_b; +} + +void Cross3(double out[3], double a[3], double b[3]) { + double x = a[1] * b[2] - a[2] * b[1]; + double y = a[2] * b[0] - a[0] * b[2]; + double z = a[0] * b[1] - a[1] * b[0]; + out[0] = x; + out[1] = y; + out[2] = z; +} + +// Taken from http://www.w3.org/TR/css3-transforms/. +bool Slerp(double out[4], + const double q1[4], + const double q2[4], + double progress) { + double product = Dot<4>(q1, q2); + + // Clamp product to -1.0 <= product <= 1.0. + product = std::min(std::max(product, -1.0), 1.0); + + const double epsilon = 1e-5; + if (std::abs(product - 1.0) < epsilon) { + for (int i = 0; i < 4; ++i) + out[i] = q1[i]; + return true; + } + + if (std::abs(product) < epsilon) { + // Rotation by 180 degrees. We'll fail. It's ambiguous how to interpolate. + return false; + } + + double denom = std::sqrt(1 - product * product); + double theta = std::acos(product); + double w = std::sin(progress * theta) * (1 / denom); + + double scale1 = std::cos(progress * theta) - product * w; + double scale2 = w; + Combine<4>(out, q1, q2, scale1, scale2); + + return true; +} + +// Returns false if the matrix cannot be normalized. +bool Normalize(SkMatrix44& m) { + if (MGET(m, 3, 3) == 0.0) + // Cannot normalize. + return false; + + double scale = 1.0 / MGET(m, 3, 3); + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + MSET(m, i, j, MGET(m, i, j) * scale); + + return true; +} + +} // namespace + Transform GetScaleTransform(const Point& anchor, float scale) { Transform transform; transform.ConcatScale(scale, scale); @@ -16,4 +117,217 @@ Transform GetScaleTransform(const Point& anchor, float scale) { return transform; } +DecomposedTransform::DecomposedTransform() { + translate[0] = translate[1] = translate[2] = 0.0; + scale[0] = scale[1] = scale[2] = 1.0; + skew[0] = skew[1] = skew[2] = 0.0; + perspective[0] = perspective[1] = perspective[2] = 0.0; + quaternion[0] = quaternion[1] = quaternion[2] = 0.0; + perspective[3] = quaternion[3] = 1.0; +} + +bool BlendDecomposedTransforms(DecomposedTransform* out, + const DecomposedTransform& to, + const DecomposedTransform& from, + double progress) { + double scalea = progress; + double scaleb = 1.0 - progress; + Combine<3>(out->translate, to.translate, from.translate, scalea, scaleb); + Combine<3>(out->scale, to.scale, from.scale, scalea, scaleb); + Combine<3>(out->skew, to.skew, from.skew, scalea, scaleb); + Combine<4>( + out->perspective, to.perspective, from.perspective, scalea, scaleb); + return Slerp(out->quaternion, from.quaternion, to.quaternion, progress); +} + +// Taken from http://www.w3.org/TR/css3-transforms/. +bool DecomposeTransform(DecomposedTransform* decomp, + const Transform& transform) { + if (!decomp) + return false; + + // We'll operate on a copy of the matrix. + SkMatrix44 matrix = transform.matrix(); + + // If we cannot normalize the matrix, then bail early as we cannot decompose. + if (!Normalize(matrix)) + return false; + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + MSET(matrix, i, j, MGET(matrix, i, j) / MGET(matrix, 3, 3)); + + SkMatrix44 perspectiveMatrix = matrix; + + for (int i = 0; i < 3; ++i) + MSET(perspectiveMatrix, 3, i, 0.0); + + MSET(perspectiveMatrix, 3, 3, 1.0); + + // If the perspective matrix is not invertible, we are also unable to + // decompose, so we'll bail early. Constant taken from SkMatrix44::invert. + if (std::abs(perspectiveMatrix.determinant()) < 1e-8) + return false; + + if (MGET(matrix, 3, 0) != 0.0 || + MGET(matrix, 3, 1) != 0.0 || + MGET(matrix, 3, 2) != 0.0) { + // rhs is the right hand side of the equation. + SkMScalar rhs[4] = { + matrix.get(3, 0), + matrix.get(3, 1), + matrix.get(3, 2), + matrix.get(3, 3) + }; + + // Solve the equation by inverting perspectiveMatrix and multiplying + // rhs by the inverse. + SkMatrix44 inversePerspectiveMatrix; + if (!perspectiveMatrix.invert(&inversePerspectiveMatrix)) + return false; + + SkMatrix44 transposedInversePerspectiveMatrix = + Transpose(inversePerspectiveMatrix); + + transposedInversePerspectiveMatrix.mapMScalars(rhs); + + for (int i = 0; i < 4; ++i) + decomp->perspective[i] = rhs[i]; + + } else { + // No perspective. + for (int i = 0; i < 3; ++i) + decomp->perspective[i] = 0.0; + decomp->perspective[3] = 1.0; + } + + for (int i = 0; i < 3; i++) + decomp->translate[i] = MGET(matrix, i, 3); + + double row[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; ++j) + row[i][j] = MGET(matrix, j, i); + + // Compute X scale factor and normalize first row. + decomp->scale[0] = Length3(row[0]); + if (decomp->scale[0] != 0.0) + Scale3(row[0], 1.0 / decomp->scale[0]); + + // Compute XY shear factor and make 2nd row orthogonal to 1st. + decomp->skew[0] = Dot<3>(row[0], row[1]); + Combine<3>(row[1], row[1], row[0], 1.0, -decomp->skew[0]); + + // Now, compute Y scale and normalize 2nd row. + decomp->scale[1] = Length3(row[1]); + if (decomp->scale[1] != 0.0) + Scale3(row[1], 1.0 / decomp->scale[1]); + + decomp->skew[0] /= decomp->scale[1]; + + // Compute XZ and YZ shears, orthogonalize 3rd row + decomp->skew[1] = Dot<3>(row[0], row[2]); + Combine<3>(row[2], row[2], row[0], 1.0, -decomp->skew[1]); + decomp->skew[2] = Dot<3>(row[1], row[2]); + Combine<3>(row[2], row[2], row[1], 1.0, -decomp->skew[2]); + + // Next, get Z scale and normalize 3rd row. + decomp->scale[2] = Length3(row[2]); + if (decomp->scale[2] != 0.0) + Scale3(row[2], 1.0 / decomp->scale[2]); + + decomp->skew[1] /= decomp->scale[2]; + decomp->skew[2] /= decomp->scale[2]; + + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + double pdum3[3]; + Cross3(pdum3, row[1], row[2]); + if (Dot<3>(row[0], pdum3) < 0) { + for (int i = 0; i < 3; i++) { + decomp->scale[i] *= -1.0; + for (int j = 0; j < 3; ++j) + row[i][j] *= -1.0; + } + } + + decomp->quaternion[0] = + 0.5 * std::sqrt(std::max(1.0 + row[0][0] - row[1][1] - row[2][2], 0.0)); + decomp->quaternion[1] = + 0.5 * std::sqrt(std::max(1.0 - row[0][0] + row[1][1] - row[2][2], 0.0)); + decomp->quaternion[2] = + 0.5 * std::sqrt(std::max(1.0 - row[0][0] - row[1][1] + row[2][2], 0.0)); + decomp->quaternion[3] = + 0.5 * std::sqrt(std::max(1.0 + row[0][0] + row[1][1] + row[2][2], 0.0)); + + if (row[2][1] > row[1][2]) + decomp->quaternion[0] = -decomp->quaternion[0]; + if (row[0][2] > row[2][0]) + decomp->quaternion[1] = -decomp->quaternion[1]; + if (row[1][0] > row[0][1]) + decomp->quaternion[2] = -decomp->quaternion[2]; + + return true; +} + +// Taken from http://www.w3.org/TR/css3-transforms/. +Transform ComposeTransform(const DecomposedTransform& decomp) { + SkMatrix44 matrix; + for (int i = 0; i < 4; i++) + MSET(matrix, 3, i, decomp.perspective[i]); + + SkMatrix44 tempTranslation; + tempTranslation.setTranslate(SkDoubleToMScalar(decomp.translate[0]), + SkDoubleToMScalar(decomp.translate[1]), + SkDoubleToMScalar(decomp.translate[2])); + matrix.preConcat(tempTranslation); + + double x = decomp.quaternion[0]; + double y = decomp.quaternion[1]; + double z = decomp.quaternion[2]; + double w = decomp.quaternion[3]; + + SkMatrix44 rotation_matrix; + MSET(rotation_matrix, 0, 0, 1.0 - 2.0 * (y * y + z * z)); + MSET(rotation_matrix, 0, 1, 2.0 * (x * y - z * w)); + MSET(rotation_matrix, 0, 2, 2.0 * (x * z + y * w)); + MSET(rotation_matrix, 1, 0, 2.0 * (x * y + z * w)); + MSET(rotation_matrix, 1, 1, 1.0 - 2.0 * (x * x + z * z)); + MSET(rotation_matrix, 1, 2, 2.0 * (y * z - x * w)); + MSET(rotation_matrix, 2, 0, 2.0 * (x * z - y * w)); + MSET(rotation_matrix, 2, 1, 2.0 * (y * z + x * w)); + MSET(rotation_matrix, 2, 2, 1.0 - 2.0 * (x * x + y * y)); + + matrix.preConcat(rotation_matrix); + + SkMatrix44 temp; + if (decomp.skew[2]) { + MSET(temp, 1, 2, decomp.skew[2]); + matrix.preConcat(temp); + } + + if (decomp.skew[1]) { + MSET(temp, 1, 2, 0); + MSET(temp, 0, 2, decomp.skew[1]); + matrix.preConcat(temp); + } + + if (decomp.skew[0]) { + MSET(temp, 0, 2, 0); + MSET(temp, 0, 1, decomp.skew[0]); + matrix.preConcat(temp); + } + + SkMatrix44 tempScale; + tempScale.setScale(SkDoubleToMScalar(decomp.scale[0]), + SkDoubleToMScalar(decomp.scale[1]), + SkDoubleToMScalar(decomp.scale[2])); + matrix.preConcat(tempScale); + + Transform to_return; + to_return.matrix() = matrix; + return to_return; +} + } // namespace ui diff --git a/ui/gfx/transform_util.h b/ui/gfx/transform_util.h index e898d70..b6da7b0 100644 --- a/ui/gfx/transform_util.h +++ b/ui/gfx/transform_util.h @@ -15,6 +15,42 @@ class Point; // Returns a scale transform at |anchor| point. UI_EXPORT Transform GetScaleTransform(const Point& anchor, float scale); +// Contains the components of a factored transform. These components may be +// blended and recomposed. +struct UI_EXPORT DecomposedTransform { + // The default constructor initializes the components in such a way that + // if used with Compose below, will produce the identity transform. + DecomposedTransform(); + + double translate[3]; + double scale[3]; + double skew[3]; + double perspective[4]; + double quaternion[4]; + + // Copy and assign are allowed. +}; + +// Interpolates the decomposed components |to| with |from| using the +// routines described in http://www.w3.org/TR/css3-3d-transform/. +// |progress| is in the range [0, 1] (0 leaves |out| unchanged, and 1 +// assigns |from| to |out|). +UI_EXPORT bool BlendDecomposedTransforms(DecomposedTransform* out, + const DecomposedTransform& to, + const DecomposedTransform& from, + double progress); + +// Decomposes this transform into its translation, scale, skew, perspective, +// and rotation components following the routines detailed in this spec: +// http://www.w3.org/TR/css3-3d-transforms/. +UI_EXPORT bool DecomposeTransform(DecomposedTransform* out, + const Transform& transform); + +// Composes a transform from the given translation, scale, skew, prespective, +// and rotation components following the routines detailed in this spec: +// http://www.w3.org/TR/css3-3d-transforms/. +UI_EXPORT Transform ComposeTransform(const DecomposedTransform& decomp); + } // namespace gfx #endif // UI_GFX_TRANSFORM_UTIL_H_ |