From 20e3cd2c9fbc049eae8bcedc591c2cc8d4bed656 Mon Sep 17 00:00:00 2001 From: "scroggo@google.com" Date: Tue, 5 Nov 2013 15:54:42 +0000 Subject: Add SkRRect::transform. Much like SkPath::transform, it transforms an SkRRect based on an SkMatrix. Unlike SkPath::transform, it will fail for matrices that contain perspective or skewing. Required by a future change (https://codereview.chromium.org/48623006) to speed up drawing large blurry rounded rectangles by using ninepatches. TODO: This could easily support 90 degree rotations, if desired. BUG=https://b.corp.google.com/issue?id=11174385 R=reed@google.com, robertphillips@google.com Review URL: https://codereview.chromium.org/52703003 git-svn-id: http://skia.googlecode.com/svn/trunk@12132 2bbb7eff-a529-9590-31e7-b0007b416f81 --- include/core/SkRRect.h | 12 +++ src/core/SkRRect.cpp | 80 +++++++++++++++++++ tests/RoundRectTest.cpp | 206 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 296 insertions(+), 2 deletions(-) diff --git a/include/core/SkRRect.h b/include/core/SkRRect.h index 3c6386f1c6..66c433f43b 100644 --- a/include/core/SkRRect.h +++ b/include/core/SkRRect.h @@ -12,6 +12,7 @@ #include "SkPoint.h" class SkPath; +class SkMatrix; // Path forward: // core work @@ -259,6 +260,17 @@ public: */ size_t readFromMemory(const void* buffer, size_t length); + /** + * Transform by the specified matrix, and put the result in dst. + * + * @param matrix SkMatrix specifying the transform. Must only contain + * scale and/or translate, or this call will fail. + * @param dst SkRRect to store the result. It is an error to use this, + * which would make this function no longer const. + * @return true on success, false on failure. If false, dst is unmodified. + */ + bool transform(const SkMatrix& matrix, SkRRect* dst) const; + private: SkRect fRect; // Radii order is UL, UR, LR, LL. Use Corner enum to index into fRadii[] diff --git a/src/core/SkRRect.cpp b/src/core/SkRRect.cpp index bcbf37ec59..e5296d4e3a 100644 --- a/src/core/SkRRect.cpp +++ b/src/core/SkRRect.cpp @@ -6,6 +6,7 @@ */ #include "SkRRect.h" +#include "SkMatrix.h" /////////////////////////////////////////////////////////////////////////////// @@ -233,6 +234,85 @@ void SkRRect::computeType() const { fType = kComplex_Type; } +static bool matrix_only_scale_and_translate(const SkMatrix& matrix) { + const SkMatrix::TypeMask m = (SkMatrix::TypeMask) (SkMatrix::kAffine_Mask + | SkMatrix::kPerspective_Mask); + return (matrix.getType() & m) == 0; +} + +bool SkRRect::transform(const SkMatrix& matrix, SkRRect* dst) const { + if (NULL == dst) { + return false; + } + + // Assert that the caller is not trying to do this in place, which + // would violate const-ness. Do not return false though, so that + // if they know what they're doing and want to violate it they can. + SkASSERT(dst != this); + + if (matrix.isIdentity()) { + *dst = *this; + return true; + } + + // If transform supported 90 degree rotations (which it could), we could + // use SkMatrix::rectStaysRect() to check for a valid transformation. + if (!matrix_only_scale_and_translate(matrix)) { + return false; + } + + SkRect newRect; + if (!matrix.mapRect(&newRect, fRect)) { + return false; + } + + // At this point, this is guaranteed to succeed, so we can modify dst. + dst->fRect = newRect; + + // Now scale each corner + SkScalar xScale = matrix.getScaleX(); + const bool flipX = xScale < 0; + if (flipX) { + xScale = -xScale; + } + SkScalar yScale = matrix.getScaleY(); + const bool flipY = yScale < 0; + if (flipY) { + yScale = -yScale; + } + + // Scale the radii without respecting the flip. + for (int i = 0; i < 4; ++i) { + dst->fRadii[i].fX = SkScalarMul(fRadii[i].fX, xScale); + dst->fRadii[i].fY = SkScalarMul(fRadii[i].fY, yScale); + } + + // Now swap as necessary. + if (flipX) { + if (flipY) { + // Swap with opposite corners + SkTSwap(dst->fRadii[kUpperLeft_Corner], dst->fRadii[kLowerRight_Corner]); + SkTSwap(dst->fRadii[kUpperRight_Corner], dst->fRadii[kLowerLeft_Corner]); + } else { + // Only swap in x + SkTSwap(dst->fRadii[kUpperRight_Corner], dst->fRadii[kUpperLeft_Corner]); + SkTSwap(dst->fRadii[kLowerRight_Corner], dst->fRadii[kLowerLeft_Corner]); + } + } else if (flipY) { + // Only swap in y + SkTSwap(dst->fRadii[kUpperLeft_Corner], dst->fRadii[kLowerLeft_Corner]); + SkTSwap(dst->fRadii[kUpperRight_Corner], dst->fRadii[kLowerRight_Corner]); + } + + // Since the only transforms that were allowed are scale and translate, the type + // remains unchanged. + dst->fType = fType; + + SkDEBUGCODE(dst->validate();) + + return true; +} + /////////////////////////////////////////////////////////////////////////////// void SkRRect::inset(SkScalar dx, SkScalar dy, SkRRect* dst) const { diff --git a/tests/RoundRectTest.cpp b/tests/RoundRectTest.cpp index ec94c33968..95e455d29b 100644 --- a/tests/RoundRectTest.cpp +++ b/tests/RoundRectTest.cpp @@ -6,10 +6,11 @@ */ #include "Test.h" +#include "SkMatrix.h" #include "SkRRect.h" -static const SkScalar kWidth = 100.0f; -static const SkScalar kHeight = 100.0f; +static const SkScalar kWidth = SkFloatToScalar(100.0f); +static const SkScalar kHeight = SkFloatToScalar(100.0f); static void test_inset(skiatest::Reporter* reporter) { SkRRect rr, rr2; @@ -352,6 +353,206 @@ static void test_round_rect_contains_rect(skiatest::Reporter* reporter) { } } +// Called for a matrix that should cause SkRRect::transform to fail. +static void assert_transform_failure(skiatest::Reporter* reporter, const SkRRect& orig, + const SkMatrix& matrix) { + // The test depends on the fact that the original is not empty. + SkASSERT(!orig.isEmpty()); + SkRRect dst; + dst.setEmpty(); + + const SkRRect copyOfDst = dst; + const SkRRect copyOfOrig = orig; + bool success = orig.transform(matrix, &dst); + // This transform should fail. + REPORTER_ASSERT(reporter, !success); + // Since the transform failed, dst should be unchanged. + REPORTER_ASSERT(reporter, copyOfDst == dst); + // original should not be modified. + REPORTER_ASSERT(reporter, copyOfOrig == orig); + REPORTER_ASSERT(reporter, orig != dst); +} + +#define GET_RADII \ + const SkVector& origUL = orig.radii(SkRRect::kUpperLeft_Corner); \ + const SkVector& origUR = orig.radii(SkRRect::kUpperRight_Corner); \ + const SkVector& origLR = orig.radii(SkRRect::kLowerRight_Corner); \ + const SkVector& origLL = orig.radii(SkRRect::kLowerLeft_Corner); \ + const SkVector& dstUL = dst.radii(SkRRect::kUpperLeft_Corner); \ + const SkVector& dstUR = dst.radii(SkRRect::kUpperRight_Corner); \ + const SkVector& dstLR = dst.radii(SkRRect::kLowerRight_Corner); \ + const SkVector& dstLL = dst.radii(SkRRect::kLowerLeft_Corner) + +// Called to test various transforms on a single SkRRect. +static void test_transform_helper(skiatest::Reporter* reporter, const SkRRect& orig) { + SkRRect dst; + dst.setEmpty(); + + // The identity matrix will duplicate the rrect. + bool success = orig.transform(SkMatrix::I(), &dst); + REPORTER_ASSERT(reporter, success); + REPORTER_ASSERT(reporter, orig == dst); + + // Skew and Perspective make transform fail. + SkMatrix matrix; + matrix.reset(); + matrix.setSkewX(SkIntToScalar(2)); + assert_transform_failure(reporter, orig, matrix); + + matrix.reset(); + matrix.setSkewY(SkIntToScalar(3)); + assert_transform_failure(reporter, orig, matrix); + + matrix.reset(); + matrix.setPerspX(SkScalarToPersp(SkIntToScalar(4))); + assert_transform_failure(reporter, orig, matrix); + + matrix.reset(); + matrix.setPerspY(SkScalarToPersp(SkIntToScalar(5))); + assert_transform_failure(reporter, orig, matrix); + + // Rotation fails. + matrix.reset(); + matrix.setRotate(SkIntToScalar(90)); + assert_transform_failure(reporter, orig, matrix); + matrix.setRotate(SkIntToScalar(37)); + assert_transform_failure(reporter, orig, matrix); + + // Translate will keep the rect moved, but otherwise the same. + matrix.reset(); + SkScalar translateX = SkIntToScalar(32); + SkScalar translateY = SkIntToScalar(15); + matrix.setTranslateX(translateX); + matrix.setTranslateY(translateY); + dst.setEmpty(); + success = orig.transform(matrix, &dst); + REPORTER_ASSERT(reporter, success); + for (int i = 0; i < 4; ++i) { + REPORTER_ASSERT(reporter, + orig.radii((SkRRect::Corner) i) == dst.radii((SkRRect::Corner) i)); + } + REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width()); + REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height()); + REPORTER_ASSERT(reporter, dst.rect().left() == orig.rect().left() + translateX); + REPORTER_ASSERT(reporter, dst.rect().top() == orig.rect().top() + translateY); + + // Keeping the translation, but adding skew will make transform fail. + matrix.setSkewY(SkIntToScalar(7)); + assert_transform_failure(reporter, orig, matrix); + + // Scaling in -x will flip the round rect horizontally. + matrix.reset(); + matrix.setScaleX(SkIntToScalar(-1)); + dst.setEmpty(); + success = orig.transform(matrix, &dst); + REPORTER_ASSERT(reporter, success); + { + GET_RADII; + // Radii have swapped in x. + REPORTER_ASSERT(reporter, origUL == dstUR); + REPORTER_ASSERT(reporter, origUR == dstUL); + REPORTER_ASSERT(reporter, origLR == dstLL); + REPORTER_ASSERT(reporter, origLL == dstLR); + } + // Width and height remain the same. + REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width()); + REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height()); + // Right and left have swapped (sort of) + REPORTER_ASSERT(reporter, orig.rect().right() == -dst.rect().left()); + // Top has stayed the same. + REPORTER_ASSERT(reporter, orig.rect().top() == dst.rect().top()); + + // Keeping the scale, but adding a persp will make transform fail. + matrix.setPerspX(SkScalarToPersp(SkIntToScalar(7))); + assert_transform_failure(reporter, orig, matrix); + + // Scaling in -y will flip the round rect vertically. + matrix.reset(); + matrix.setScaleY(SkIntToScalar(-1)); + dst.setEmpty(); + success = orig.transform(matrix, &dst); + REPORTER_ASSERT(reporter, success); + { + GET_RADII; + // Radii have swapped in y. + REPORTER_ASSERT(reporter, origUL == dstLL); + REPORTER_ASSERT(reporter, origUR == dstLR); + REPORTER_ASSERT(reporter, origLR == dstUR); + REPORTER_ASSERT(reporter, origLL == dstUL); + } + // Width and height remain the same. + REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width()); + REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height()); + // Top and bottom have swapped (sort of) + REPORTER_ASSERT(reporter, orig.rect().top() == -dst.rect().bottom()); + // Left has stayed the same. + REPORTER_ASSERT(reporter, orig.rect().left() == dst.rect().left()); + + // Scaling in -x and -y will swap in both directions. + matrix.reset(); + matrix.setScaleY(SkIntToScalar(-1)); + matrix.setScaleX(SkIntToScalar(-1)); + dst.setEmpty(); + success = orig.transform(matrix, &dst); + REPORTER_ASSERT(reporter, success); + { + GET_RADII; + REPORTER_ASSERT(reporter, origUL == dstLR); + REPORTER_ASSERT(reporter, origUR == dstLL); + REPORTER_ASSERT(reporter, origLR == dstUL); + REPORTER_ASSERT(reporter, origLL == dstUR); + } + // Width and height remain the same. + REPORTER_ASSERT(reporter, orig.rect().width() == dst.rect().width()); + REPORTER_ASSERT(reporter, orig.rect().height() == dst.rect().height()); + REPORTER_ASSERT(reporter, orig.rect().top() == -dst.rect().bottom()); + REPORTER_ASSERT(reporter, orig.rect().right() == -dst.rect().left()); + + // Scale in both directions. + SkScalar xScale = SkIntToScalar(3); + SkScalar yScale = SkFloatToScalar(3.2f); + matrix.reset(); + matrix.setScaleX(xScale); + matrix.setScaleY(yScale); + dst.setEmpty(); + success = orig.transform(matrix, &dst); + REPORTER_ASSERT(reporter, success); + // Radii are scaled. + for (int i = 0; i < 4; ++i) { + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.radii((SkRRect::Corner) i).fX, + SkScalarMul(orig.radii((SkRRect::Corner) i).fX, xScale))); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.radii((SkRRect::Corner) i).fY, + SkScalarMul(orig.radii((SkRRect::Corner) i).fY, yScale))); + } + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().width(), + SkScalarMul(orig.rect().width(), xScale))); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().height(), + SkScalarMul(orig.rect().height(), yScale))); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().left(), + SkScalarMul(orig.rect().left(), xScale))); + REPORTER_ASSERT(reporter, SkScalarNearlyEqual(dst.rect().top(), + SkScalarMul(orig.rect().top(), yScale))); +} + +static void test_round_rect_transform(skiatest::Reporter* reporter) { + SkRRect rrect; + { + SkRect r = { 0, 0, kWidth, kHeight }; + rrect.setRectXY(r, SkIntToScalar(4), SkIntToScalar(7)); + test_transform_helper(reporter, rrect); + } + { + SkRect r = { SkIntToScalar(5), SkIntToScalar(15), + SkIntToScalar(27), SkIntToScalar(34) }; + SkVector radii[4] = { { 0, SkIntToScalar(1) }, + { SkIntToScalar(2), SkIntToScalar(3) }, + { SkIntToScalar(4), SkIntToScalar(5) }, + { SkIntToScalar(6), SkIntToScalar(7) } }; + rrect.setRectRadii(r, radii); + test_transform_helper(reporter, rrect); + } +} + static void TestRoundRect(skiatest::Reporter* reporter) { test_round_rect_basic(reporter); test_round_rect_rects(reporter); @@ -360,6 +561,7 @@ static void TestRoundRect(skiatest::Reporter* reporter) { test_round_rect_iffy_parameters(reporter); test_inset(reporter); test_round_rect_contains_rect(reporter); + test_round_rect_transform(reporter); } #include "TestClassDef.h" -- cgit v1.2.3