| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/gfx/transform_util.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/logging.h" |
| #include "base/strings/stringprintf.h" |
| #include "ui/gfx/point.h" |
| #include "ui/gfx/point3_f.h" |
| #include "ui/gfx/rect.h" |
| |
| namespace gfx { |
| |
| namespace { |
| |
| SkMScalar Length3(SkMScalar v[3]) { |
| double vd[3] = {SkMScalarToDouble(v[0]), SkMScalarToDouble(v[1]), |
| SkMScalarToDouble(v[2])}; |
| return SkDoubleToMScalar( |
| std::sqrt(vd[0] * vd[0] + vd[1] * vd[1] + vd[2] * vd[2])); |
| } |
| |
| void Scale3(SkMScalar v[3], SkMScalar scale) { |
| for (int i = 0; i < 3; ++i) |
| v[i] *= scale; |
| } |
| |
| template <int n> |
| SkMScalar Dot(const SkMScalar* a, const SkMScalar* b) { |
| double total = 0.0; |
| for (int i = 0; i < n; ++i) |
| total += a[i] * b[i]; |
| return SkDoubleToMScalar(total); |
| } |
| |
| template <int n> |
| void Combine(SkMScalar* out, |
| const SkMScalar* a, |
| const SkMScalar* b, |
| double scale_a, |
| double scale_b) { |
| for (int i = 0; i < n; ++i) |
| out[i] = SkDoubleToMScalar(a[i] * scale_a + b[i] * scale_b); |
| } |
| |
| void Cross3(SkMScalar out[3], SkMScalar a[3], SkMScalar b[3]) { |
| SkMScalar x = a[1] * b[2] - a[2] * b[1]; |
| SkMScalar y = a[2] * b[0] - a[0] * b[2]; |
| SkMScalar z = a[0] * b[1] - a[1] * b[0]; |
| out[0] = x; |
| out[1] = y; |
| out[2] = z; |
| } |
| |
| SkMScalar Round(SkMScalar n) { |
| return SkDoubleToMScalar(std::floor(SkMScalarToDouble(n) + 0.5)); |
| } |
| |
| // Taken from http://www.w3.org/TR/css3-transforms/. |
| bool Slerp(SkMScalar out[4], |
| const SkMScalar q1[4], |
| const SkMScalar 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); |
| |
| // Interpolate angles along the shortest path. For example, to interpolate |
| // between a 175 degree angle and a 185 degree angle, interpolate along the |
| // 10 degree path from 175 to 185, rather than along the 350 degree path in |
| // the opposite direction. This matches WebKit's implementation but not |
| // the current W3C spec. Fixing the spec to match this approach is discussed |
| // at: |
| // http://lists.w3.org/Archives/Public/www-style/2013May/0131.html |
| double scale1 = 1.0; |
| if (product < 0) { |
| product = -product; |
| scale1 = -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; |
| } |
| |
| double denom = std::sqrt(1.0 - product * product); |
| double theta = std::acos(product); |
| double w = std::sin(progress * theta) * (1.0 / denom); |
| |
| 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 (m.get(3, 3) == 0.0) |
| // Cannot normalize. |
| return false; |
| |
| SkMScalar scale = 1.0 / m.get(3, 3); |
| for (int i = 0; i < 4; i++) |
| for (int j = 0; j < 4; j++) |
| m.set(i, j, m.get(i, j) * scale); |
| |
| return true; |
| } |
| |
| SkMatrix44 BuildPerspectiveMatrix(const DecomposedTransform& decomp) { |
| SkMatrix44 matrix(SkMatrix44::kIdentity_Constructor); |
| |
| for (int i = 0; i < 4; i++) |
| matrix.setDouble(3, i, decomp.perspective[i]); |
| return matrix; |
| } |
| |
| SkMatrix44 BuildTranslationMatrix(const DecomposedTransform& decomp) { |
| SkMatrix44 matrix(SkMatrix44::kUninitialized_Constructor); |
| // Implicitly calls matrix.setIdentity() |
| matrix.setTranslate(SkDoubleToMScalar(decomp.translate[0]), |
| SkDoubleToMScalar(decomp.translate[1]), |
| SkDoubleToMScalar(decomp.translate[2])); |
| return matrix; |
| } |
| |
| SkMatrix44 BuildSnappedTranslationMatrix(DecomposedTransform decomp) { |
| decomp.translate[0] = Round(decomp.translate[0]); |
| decomp.translate[1] = Round(decomp.translate[1]); |
| decomp.translate[2] = Round(decomp.translate[2]); |
| return BuildTranslationMatrix(decomp); |
| } |
| |
| SkMatrix44 BuildRotationMatrix(const DecomposedTransform& decomp) { |
| double x = decomp.quaternion[0]; |
| double y = decomp.quaternion[1]; |
| double z = decomp.quaternion[2]; |
| double w = decomp.quaternion[3]; |
| |
| SkMatrix44 matrix(SkMatrix44::kUninitialized_Constructor); |
| |
| // Implicitly calls matrix.setIdentity() |
| matrix.set3x3(1.0 - 2.0 * (y * y + z * z), |
| 2.0 * (x * y + z * w), |
| 2.0 * (x * z - y * w), |
| 2.0 * (x * y - z * w), |
| 1.0 - 2.0 * (x * x + z * z), |
| 2.0 * (y * z + x * w), |
| 2.0 * (x * z + y * w), |
| 2.0 * (y * z - x * w), |
| 1.0 - 2.0 * (x * x + y * y)); |
| return matrix; |
| } |
| |
| SkMatrix44 BuildSnappedRotationMatrix(const DecomposedTransform& decomp) { |
| // Create snapped rotation. |
| SkMatrix44 rotation_matrix = BuildRotationMatrix(decomp); |
| for (int i = 0; i < 3; ++i) { |
| for (int j = 0; j < 3; ++j) { |
| SkMScalar value = rotation_matrix.get(i, j); |
| // Snap values to -1, 0 or 1. |
| if (value < -0.5f) { |
| value = -1.0f; |
| } else if (value > 0.5f) { |
| value = 1.0f; |
| } else { |
| value = 0.0f; |
| } |
| rotation_matrix.set(i, j, value); |
| } |
| } |
| return rotation_matrix; |
| } |
| |
| SkMatrix44 BuildSkewMatrix(const DecomposedTransform& decomp) { |
| SkMatrix44 matrix(SkMatrix44::kIdentity_Constructor); |
| |
| SkMatrix44 temp(SkMatrix44::kIdentity_Constructor); |
| if (decomp.skew[2]) { |
| temp.setDouble(1, 2, decomp.skew[2]); |
| matrix.preConcat(temp); |
| } |
| |
| if (decomp.skew[1]) { |
| temp.setDouble(1, 2, 0); |
| temp.setDouble(0, 2, decomp.skew[1]); |
| matrix.preConcat(temp); |
| } |
| |
| if (decomp.skew[0]) { |
| temp.setDouble(0, 2, 0); |
| temp.setDouble(0, 1, decomp.skew[0]); |
| matrix.preConcat(temp); |
| } |
| return matrix; |
| } |
| |
| SkMatrix44 BuildScaleMatrix(const DecomposedTransform& decomp) { |
| SkMatrix44 matrix(SkMatrix44::kUninitialized_Constructor); |
| matrix.setScale(SkDoubleToMScalar(decomp.scale[0]), |
| SkDoubleToMScalar(decomp.scale[1]), |
| SkDoubleToMScalar(decomp.scale[2])); |
| return matrix; |
| } |
| |
| SkMatrix44 BuildSnappedScaleMatrix(DecomposedTransform decomp) { |
| decomp.scale[0] = Round(decomp.scale[0]); |
| decomp.scale[1] = Round(decomp.scale[1]); |
| decomp.scale[2] = Round(decomp.scale[2]); |
| return BuildScaleMatrix(decomp); |
| } |
| |
| Transform ComposeTransform(const SkMatrix44& perspective, |
| const SkMatrix44& translation, |
| const SkMatrix44& rotation, |
| const SkMatrix44& skew, |
| const SkMatrix44& scale) { |
| SkMatrix44 matrix(SkMatrix44::kIdentity_Constructor); |
| |
| matrix.preConcat(perspective); |
| matrix.preConcat(translation); |
| matrix.preConcat(rotation); |
| matrix.preConcat(skew); |
| matrix.preConcat(scale); |
| |
| Transform to_return; |
| to_return.matrix() = matrix; |
| return to_return; |
| } |
| |
| bool CheckViewportPointMapsWithinOnePixel(const Point& point, |
| const Transform& transform) { |
| Point3F point_original(point); |
| Point3F point_transformed(point); |
| |
| // Can't use TransformRect here since it would give us the axis-aligned |
| // bounding rect of the 4 points in the initial rectable which is not what we |
| // want. |
| transform.TransformPoint(&point_transformed); |
| |
| if ((point_transformed - point_original).Length() > 1.f) { |
| // The changed distance should not be more than 1 pixel. |
| return false; |
| } |
| return true; |
| } |
| |
| bool CheckTransformsMapsIntViewportWithinOnePixel(const Rect& viewport, |
| const Transform& original, |
| const Transform& snapped) { |
| |
| Transform original_inv(Transform::kSkipInitialization); |
| bool invertible = true; |
| invertible &= original.GetInverse(&original_inv); |
| DCHECK(invertible) << "Non-invertible transform, cannot snap."; |
| |
| Transform combined = snapped * original_inv; |
| |
| return CheckViewportPointMapsWithinOnePixel(viewport.origin(), combined) && |
| CheckViewportPointMapsWithinOnePixel(viewport.top_right(), combined) && |
| CheckViewportPointMapsWithinOnePixel(viewport.bottom_left(), |
| combined) && |
| CheckViewportPointMapsWithinOnePixel(viewport.bottom_right(), |
| combined); |
| } |
| |
| } // namespace |
| |
| Transform GetScaleTransform(const Point& anchor, float scale) { |
| Transform transform; |
| transform.Translate(anchor.x() * (1 - scale), |
| anchor.y() * (1 - scale)); |
| transform.Scale(scale, 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; |
| |
| SkMatrix44 perspectiveMatrix = matrix; |
| |
| for (int i = 0; i < 3; ++i) |
| perspectiveMatrix.set(3, i, 0.0); |
| |
| perspectiveMatrix.set(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 (matrix.get(3, 0) != 0.0 || matrix.get(3, 1) != 0.0 || |
| matrix.get(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(SkMatrix44::kUninitialized_Constructor); |
| if (!perspectiveMatrix.invert(&inversePerspectiveMatrix)) |
| return false; |
| |
| SkMatrix44 transposedInversePerspectiveMatrix = |
| inversePerspectiveMatrix; |
| |
| transposedInversePerspectiveMatrix.transpose(); |
| 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] = matrix.get(i, 3); |
| |
| SkMScalar row[3][3]; |
| for (int i = 0; i < 3; i++) |
| for (int j = 0; j < 3; ++j) |
| row[i][j] = matrix.get(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. |
| SkMScalar 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 perspective = BuildPerspectiveMatrix(decomp); |
| SkMatrix44 translation = BuildTranslationMatrix(decomp); |
| SkMatrix44 rotation = BuildRotationMatrix(decomp); |
| SkMatrix44 skew = BuildSkewMatrix(decomp); |
| SkMatrix44 scale = BuildScaleMatrix(decomp); |
| |
| return ComposeTransform(perspective, translation, rotation, skew, scale); |
| } |
| |
| bool SnapTransform(Transform* out, |
| const Transform& transform, |
| const Rect& viewport) { |
| DecomposedTransform decomp; |
| DecomposeTransform(&decomp, transform); |
| |
| SkMatrix44 rotation_matrix = BuildSnappedRotationMatrix(decomp); |
| SkMatrix44 translation = BuildSnappedTranslationMatrix(decomp); |
| SkMatrix44 scale = BuildSnappedScaleMatrix(decomp); |
| |
| // Rebuild matrices for other unchanged components. |
| SkMatrix44 perspective = BuildPerspectiveMatrix(decomp); |
| |
| // Completely ignore the skew. |
| SkMatrix44 skew(SkMatrix44::kIdentity_Constructor); |
| |
| // Get full tranform |
| Transform snapped = |
| ComposeTransform(perspective, translation, rotation_matrix, skew, scale); |
| |
| // Verify that viewport is not moved unnaturally. |
| bool snappable = |
| CheckTransformsMapsIntViewportWithinOnePixel(viewport, transform, snapped); |
| if (snappable) { |
| *out = snapped; |
| } |
| return snappable; |
| } |
| |
| std::string DecomposedTransform::ToString() const { |
| return base::StringPrintf( |
| "translate: %+0.4f %+0.4f %+0.4f\n" |
| "scale: %+0.4f %+0.4f %+0.4f\n" |
| "skew: %+0.4f %+0.4f %+0.4f\n" |
| "perspective: %+0.4f %+0.4f %+0.4f %+0.4f\n" |
| "quaternion: %+0.4f %+0.4f %+0.4f %+0.4f\n", |
| translate[0], |
| translate[1], |
| translate[2], |
| scale[0], |
| scale[1], |
| scale[2], |
| skew[0], |
| skew[1], |
| skew[2], |
| perspective[0], |
| perspective[1], |
| perspective[2], |
| perspective[3], |
| quaternion[0], |
| quaternion[1], |
| quaternion[2], |
| quaternion[3]); |
| } |
| |
| float MatrixDistance(const Transform& a, const Transform& b) { |
| double sum = 0.0; |
| |
| const SkMatrix44& a_data = a.matrix(); |
| const SkMatrix44& b_data = b.matrix(); |
| |
| for (int row = 0; row < 4; ++row) { |
| for (int col = 0; col < 4; ++col) { |
| double diff = a_data.get(row, col) - b_data.get(row, col); |
| sum += diff * diff; |
| } |
| } |
| |
| return static_cast<float>(std::sqrt(sum)); |
| } |
| |
| |
| } // namespace ui |