|  | /* | 
|  | * Copyright 2021 Google LLC | 
|  | * | 
|  | * Use of this source code is governed by a BSD-style license that can be | 
|  | * found in the LICENSE file. | 
|  | */ | 
|  |  | 
|  | #include "gm/gm.h" | 
|  |  | 
|  | #include "include/core/SkBlendMode.h" | 
|  | #include "include/core/SkCanvas.h" | 
|  | #include "include/core/SkColor.h" | 
|  | #include "include/core/SkImageFilter.h" | 
|  | #include "include/core/SkPaint.h" | 
|  | #include "include/core/SkPathEffect.h" | 
|  | #include "include/core/SkRect.h" | 
|  | #include "include/core/SkSurface.h" | 
|  | #include "include/effects/SkDashPathEffect.h" | 
|  | #include "include/effects/SkGradientShader.h" | 
|  | #include "include/effects/SkImageFilters.h" | 
|  | #include "tools/GpuToolUtils.h" | 
|  | #include "tools/Resources.h" | 
|  | #include "tools/ToolUtils.h" | 
|  |  | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | static constexpr SkColor kOutputBoundsColor  = SK_ColorRED; | 
|  | static constexpr SkColor kCropRectColor      = SK_ColorGREEN; | 
|  | static constexpr SkColor kContentBoundsColor = SK_ColorBLUE; | 
|  |  | 
|  | static constexpr SkRect kExampleBounds = {0.f, 0.f, 100.f, 100.f}; | 
|  |  | 
|  | // "Crop" refers to the rect passed to the crop image filter, "Rect" refers to some other rect | 
|  | // from context, likely the output bounds or the content bounds. | 
|  | enum class CropRelation { | 
|  | kCropOverlapsRect, // Intersect but doesn't fully contain one way or the other | 
|  | kCropContainsRect, | 
|  | kRectContainsCrop, | 
|  | kCropRectDisjoint, | 
|  | }; | 
|  |  | 
|  | SkRect make_overlap(const SkRect& r, float amountX, float amountY) { | 
|  | return r.makeOffset(r.width() * amountX, r.height() * amountY); | 
|  | } | 
|  |  | 
|  | SkRect make_inset(const SkRect& r, float amountX, float amountY) { | 
|  | return r.makeInset(r.width() * amountX, r.height() * amountY); | 
|  | } | 
|  |  | 
|  | SkRect make_outset(const SkRect& r, float amountX, float amountY) { | 
|  | return r.makeOutset(r.width() * amountX, r.height() * amountY); | 
|  | } | 
|  |  | 
|  | SkRect make_disjoint(const SkRect& r, float amountX, float amountY) { | 
|  | float xOffset = (amountX > 0.f ? (r.width() + r.width() * amountX) : | 
|  | (amountX < 0.f ? (-r.width() + r.width() * amountX) : 0.f)); | 
|  | float yOffset = (amountY > 0.f ? (r.height() + r.height() * amountY) : | 
|  | (amountY < 0.f ? (-r.height() + r.height() * amountY) : 0.f)); | 
|  | return r.makeOffset(xOffset, yOffset); | 
|  | } | 
|  |  | 
|  | void get_example_rects(CropRelation outputRelation, CropRelation inputRelation, bool hintContent, | 
|  | SkRect* outputBounds, SkRect* cropRect, SkRect* contentBounds) { | 
|  | *outputBounds = kExampleBounds.makeInset(20.f, 20.f); | 
|  | switch(outputRelation) { | 
|  | case CropRelation::kCropOverlapsRect: | 
|  | *cropRect = make_overlap(*outputBounds, -0.15f, 0.15f); | 
|  | SkASSERT(cropRect->intersects(*outputBounds) && | 
|  | !cropRect->contains(*outputBounds) && | 
|  | !outputBounds->contains(*cropRect)); | 
|  | break; | 
|  | case CropRelation::kCropContainsRect: | 
|  | *cropRect = make_outset(*outputBounds, 0.15f, 0.15f); | 
|  | SkASSERT(cropRect->contains(*outputBounds)); | 
|  | break; | 
|  | case CropRelation::kRectContainsCrop: | 
|  | *cropRect = make_inset(*outputBounds, 0.15f, 0.15f); | 
|  | SkASSERT(outputBounds->contains(*cropRect)); | 
|  | break; | 
|  | case CropRelation::kCropRectDisjoint: | 
|  | *cropRect = make_disjoint(*outputBounds, 0.15f, 0.0f); | 
|  | SkASSERT(!cropRect->intersects(*outputBounds)); | 
|  | break; | 
|  | } | 
|  |  | 
|  | SkAssertResult(cropRect->intersect(kExampleBounds)); | 
|  |  | 
|  | // Determine content bounds for example based on computed crop rect and input relation | 
|  | if (hintContent) { | 
|  | switch(inputRelation) { | 
|  | case CropRelation::kCropOverlapsRect: | 
|  | *contentBounds = make_overlap(*cropRect, 0.075f, -0.75f); | 
|  | SkASSERT(contentBounds->intersects(*cropRect) && | 
|  | !contentBounds->contains(*cropRect) && | 
|  | !cropRect->contains(*contentBounds)); | 
|  | break; | 
|  | case CropRelation::kCropContainsRect: | 
|  | *contentBounds = make_inset(*cropRect, 0.075f, 0.075f); | 
|  | SkASSERT(cropRect->contains(*contentBounds)); | 
|  | break; | 
|  | case CropRelation::kRectContainsCrop: | 
|  | *contentBounds = make_outset(*cropRect, 0.1f, 0.1f); | 
|  | SkASSERT(contentBounds->contains(*cropRect)); | 
|  | break; | 
|  | case CropRelation::kCropRectDisjoint: | 
|  | *contentBounds = make_disjoint(*cropRect, 0.0f, 0.075f); | 
|  | SkASSERT(!contentBounds->intersects(*cropRect)); | 
|  | break; | 
|  | } | 
|  |  | 
|  | SkAssertResult(contentBounds->intersect(kExampleBounds)); | 
|  | } else { | 
|  | *contentBounds = kExampleBounds; | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO(michaelludwig) - This is a useful test pattern for tile modes and filtering; should | 
|  | // consolidate it with the similar version in gpu_blur_utils if the GMs remain separate at the end. | 
|  | sk_sp<SkImage> make_image(SkCanvas* canvas, const SkRect* contentBounds) { | 
|  | const float w = kExampleBounds.width(); | 
|  | const float h = kExampleBounds.height(); | 
|  |  | 
|  | const auto srcII = SkImageInfo::Make(SkISize::Make(SkScalarCeilToInt(w), SkScalarCeilToInt(h)), | 
|  | kN32_SkColorType, kPremul_SkAlphaType); | 
|  | auto surf = SkSurfaces::Raster(srcII); | 
|  |  | 
|  | surf->getCanvas()->drawColor(SK_ColorDKGRAY); | 
|  | SkPaint paint; | 
|  | paint.setAntiAlias(true); | 
|  | paint.setStyle(SkPaint::kStroke_Style); | 
|  | // Draw four horizontal lines at 1/4, 3/8, 5/8, 3/4. | 
|  | paint.setStrokeWidth(h/16.f); | 
|  | paint.setColor(SK_ColorRED); | 
|  | surf->getCanvas()->drawLine({0.f, 1.f*h/4.f}, {w, 1.f*h/4.f}, paint); | 
|  | paint.setColor(/* sea foam */ 0xFF71EEB8); | 
|  | surf->getCanvas()->drawLine({0.f, 3.f*h/8.f}, {w, 3.f*h/8.f}, paint); | 
|  | paint.setColor(SK_ColorYELLOW); | 
|  | surf->getCanvas()->drawLine({0.f, 5.f*h/8.f}, {w, 5.f*h/8.f}, paint); | 
|  | paint.setColor(SK_ColorCYAN); | 
|  | surf->getCanvas()->drawLine({0.f, 3.f*h/4.f}, {w, 3.f*h/4.f}, paint); | 
|  |  | 
|  | // Draw four vertical lines at 1/4, 3/8, 5/8, 3/4. | 
|  | paint.setStrokeWidth(w/16.f); | 
|  | paint.setColor(/* orange */ 0xFFFFA500); | 
|  | surf->getCanvas()->drawLine({1.f*w/4.f, 0.f}, {1.f*h/4.f, h}, paint); | 
|  | paint.setColor(SK_ColorBLUE); | 
|  | surf->getCanvas()->drawLine({3.f*w/8.f, 0.f}, {3.f*h/8.f, h}, paint); | 
|  | paint.setColor(SK_ColorMAGENTA); | 
|  | surf->getCanvas()->drawLine({5.f*w/8.f, 0.f}, {5.f*h/8.f, h}, paint); | 
|  | paint.setColor(SK_ColorGREEN); | 
|  | surf->getCanvas()->drawLine({3.f*w/4.f, 0.f}, {3.f*h/4.f, h}, paint); | 
|  |  | 
|  | // Fill everything outside of the content bounds with red since it shouldn't be sampled from. | 
|  | if (contentBounds) { | 
|  | SkRect buffer = contentBounds->makeOutset(1.f, 1.f); | 
|  | surf->getCanvas()->clipRect(buffer, SkClipOp::kDifference); | 
|  | surf->getCanvas()->clear(SK_ColorRED); | 
|  | } | 
|  |  | 
|  | return surf->makeImageSnapshot(); | 
|  | } | 
|  |  | 
|  | // Subset 'image' to contentBounds, apply 'contentTile' mode to fill 'cropRect'-sized image. | 
|  | sk_sp<SkImage> make_cropped_image(sk_sp<SkImage> image, | 
|  | const SkRect& contentBounds, | 
|  | SkTileMode contentTile, | 
|  | const SkRect& cropRect) { | 
|  | auto surface = SkSurfaces::Raster( | 
|  | image->imageInfo().makeWH(SkScalarCeilToInt(cropRect.width()), | 
|  | SkScalarCeilToInt(cropRect.height()))); | 
|  | auto content = image->makeSubset( | 
|  | nullptr, | 
|  | contentTile == SkTileMode::kDecal ? contentBounds.roundOut() : contentBounds.roundIn(), | 
|  | {}); | 
|  | if (!content || !surface) { | 
|  | return nullptr; | 
|  | } | 
|  | SkPaint tiledContent; | 
|  | tiledContent.setShader(content->makeShader(contentTile, contentTile, | 
|  | SkFilterMode::kNearest, | 
|  | SkMatrix::Translate(contentBounds.left(), | 
|  | contentBounds.top()))); | 
|  | surface->getCanvas()->translate(-cropRect.left(), -cropRect.top()); | 
|  | surface->getCanvas()->drawPaint(tiledContent); | 
|  | return surface->makeImageSnapshot(); | 
|  | } | 
|  |  | 
|  | void draw_example_tile( | 
|  | SkCanvas* canvas, | 
|  | SkTileMode inputMode,         // the tile mode applied to content bounds | 
|  | CropRelation inputRelation,   // how crop rect relates to content bounds | 
|  | bool hintContent,             // whether or not contentBounds is hinted to saveLayer() | 
|  | SkTileMode outputMode,        // the tile mode applied to the crop rect output | 
|  | CropRelation outputRelation) {// how crop rect relates to output bounds (clip pre-saveLayer) | 
|  |  | 
|  | // Determine crop rect for example based on output relation | 
|  | SkRect outputBounds, cropRect, contentBounds; | 
|  | get_example_rects(outputRelation, inputRelation, hintContent, | 
|  | &outputBounds, &cropRect, &contentBounds); | 
|  | SkASSERT(kExampleBounds.contains(outputBounds) && | 
|  | kExampleBounds.contains(cropRect) && | 
|  | kExampleBounds.contains(contentBounds)); | 
|  |  | 
|  | auto image = make_image(canvas, hintContent ? &contentBounds : nullptr); | 
|  |  | 
|  | canvas->save(); | 
|  | // Visualize the image tiled on the content bounds (blue border) and then tiled on the crop | 
|  | // rect (green) border, semi-transparent | 
|  | { | 
|  | auto cropImage = ToolUtils::MakeTextureImage( | 
|  | canvas, make_cropped_image(image, contentBounds, inputMode, cropRect)); | 
|  | if (cropImage) { | 
|  | SkPaint tiledPaint; | 
|  | tiledPaint.setShader(cropImage->makeShader(outputMode, outputMode, | 
|  | SkFilterMode::kNearest, | 
|  | SkMatrix::Translate(cropRect.left(), | 
|  | cropRect.top()))); | 
|  | tiledPaint.setAlphaf(0.25f); | 
|  |  | 
|  | canvas->save(); | 
|  | canvas->clipRect(kExampleBounds); | 
|  | canvas->drawPaint(tiledPaint); | 
|  | canvas->restore(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Build filter, clip, save layer, draw, restore - the interesting part is in the tile modes | 
|  | // and how the various bounds intersect each other. | 
|  | { | 
|  | sk_sp<SkImageFilter> filter = SkImageFilters::Crop(contentBounds, inputMode, nullptr); | 
|  | filter = SkImageFilters::Blur(4.f, 4.f, std::move(filter)); | 
|  | filter = SkImageFilters::Crop(cropRect, outputMode, std::move(filter)); | 
|  | SkPaint layerPaint; | 
|  | layerPaint.setImageFilter(std::move(filter)); | 
|  |  | 
|  | canvas->save(); | 
|  | canvas->clipRect(outputBounds); | 
|  | canvas->saveLayer(hintContent ? &contentBounds : nullptr, &layerPaint); | 
|  |  | 
|  | auto tmp = ToolUtils::MakeTextureImage(canvas, image); | 
|  | canvas->drawImageRect(tmp, contentBounds, contentBounds, | 
|  | SkSamplingOptions(SkFilterMode::kNearest), nullptr, | 
|  | SkCanvas::kStrict_SrcRectConstraint); | 
|  | canvas->restore(); | 
|  | canvas->restore(); | 
|  | } | 
|  |  | 
|  | // Visualize bounds after the actual rendering. | 
|  | { | 
|  | SkPaint border; | 
|  | border.setStyle(SkPaint::kStroke_Style); | 
|  |  | 
|  | border.setColor(kOutputBoundsColor); | 
|  | canvas->drawRect(outputBounds, border); | 
|  |  | 
|  | border.setColor(kCropRectColor); | 
|  | canvas->drawRect(cropRect, border); | 
|  |  | 
|  | if (hintContent) { | 
|  | border.setColor(kContentBoundsColor); | 
|  | canvas->drawRect(contentBounds, border); | 
|  | } | 
|  | } | 
|  |  | 
|  | canvas->restore(); | 
|  | } | 
|  |  | 
|  | // Draw 5 example tiles in a column for 5 relationships between content bounds and crop rect: | 
|  | // no content hint, intersect, content contains crop, crop contains content, and no intersection | 
|  | void draw_example_column( | 
|  | SkCanvas* canvas, | 
|  | SkTileMode inputMode, | 
|  | SkTileMode outputMode, | 
|  | CropRelation outputRelation) { | 
|  | const std::pair<CropRelation, bool> inputRelations[5] = { | 
|  | { CropRelation::kCropOverlapsRect, false }, | 
|  | { CropRelation::kCropOverlapsRect, true }, | 
|  | { CropRelation::kCropContainsRect, true }, | 
|  | { CropRelation::kRectContainsCrop, true }, | 
|  | { CropRelation::kCropRectDisjoint, true } | 
|  | }; | 
|  |  | 
|  | canvas->save(); | 
|  | for (auto [inputRelation, hintContent] : inputRelations) { | 
|  | draw_example_tile(canvas, inputMode, inputRelation, hintContent, | 
|  | outputMode, outputRelation); | 
|  | canvas->translate(0.f, kExampleBounds.fBottom + 1.f); | 
|  | } | 
|  |  | 
|  | canvas->restore(); | 
|  | } | 
|  |  | 
|  | // Draw 5x4 grid of examples covering supported input tile modes and crop rect relations | 
|  | static constexpr int kNumRows = 5; | 
|  | static constexpr int kNumCols = 4; | 
|  | static constexpr float kGridWidth = kNumCols * (kExampleBounds.fRight+1.f) - 1.f; | 
|  | static constexpr float kGridHeight = kNumRows * (kExampleBounds.fBottom+1.f) - 1.f; | 
|  |  | 
|  | void draw_example_grid( | 
|  | SkCanvas* canvas, | 
|  | SkTileMode inputMode, | 
|  | SkTileMode outputMode) { | 
|  | canvas->save(); | 
|  | for (auto outputRelation : { CropRelation::kCropOverlapsRect, | 
|  | CropRelation::kCropContainsRect, | 
|  | CropRelation::kRectContainsCrop, | 
|  | CropRelation::kCropRectDisjoint }) { | 
|  | draw_example_column(canvas, inputMode, outputMode, outputRelation); | 
|  | canvas->translate(kExampleBounds.fRight + 1.f, 0.f); | 
|  | } | 
|  | canvas->restore(); | 
|  |  | 
|  | // Draw dashed lines between rows and columns | 
|  | SkPaint dashedLine; | 
|  | dashedLine.setColor(SK_ColorGRAY); | 
|  | dashedLine.setStyle(SkPaint::kStroke_Style); | 
|  | dashedLine.setStrokeCap(SkPaint::kSquare_Cap); | 
|  | static const float kDashes[2] = {5.f, 15.f}; | 
|  | dashedLine.setPathEffect(SkDashPathEffect::Make(kDashes, 0.f)); | 
|  |  | 
|  | for (int y = 1; y < kNumRows; ++y) { | 
|  | canvas->drawLine({0.5f, y * (kExampleBounds.fBottom+1.f) - 0.5f}, | 
|  | {kGridWidth - 0.5f, y * (kExampleBounds.fBottom+1.f) - 0.5f}, | 
|  | dashedLine); | 
|  | } | 
|  | for (int x = 1; x < kNumCols; ++x) { | 
|  | canvas->drawLine({x * (kExampleBounds.fRight+1.f) - 0.5f, 0.5f}, | 
|  | {x * (kExampleBounds.fRight+1.f) - 0.5f, kGridHeight - 0.5f}, | 
|  | dashedLine); | 
|  | } | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace skiagm { | 
|  |  | 
|  | class CropImageFilterGM : public GM { | 
|  | public: | 
|  | CropImageFilterGM(SkTileMode inputMode, SkTileMode outputMode) | 
|  | : fInputMode(inputMode) | 
|  | , fOutputMode(outputMode) {} | 
|  |  | 
|  | protected: | 
|  | SkISize getISize() override { | 
|  | return {SkScalarRoundToInt(4.f * (kExampleBounds.fRight + 1.f) - 1.f), | 
|  | SkScalarRoundToInt(5.f * (kExampleBounds.fBottom + 1.f) - 1.f)}; | 
|  | } | 
|  | SkString getName() const override { | 
|  | SkString name("crop_imagefilter_"); | 
|  | switch(fInputMode) { | 
|  | case SkTileMode::kDecal:  name.append("decal");  break; | 
|  | case SkTileMode::kClamp:  name.append("clamp");  break; | 
|  | case SkTileMode::kRepeat: name.append("repeat"); break; | 
|  | case SkTileMode::kMirror: name.append("mirror"); break; | 
|  | } | 
|  | name.append("-in_"); | 
|  |  | 
|  | switch (fOutputMode) { | 
|  | case SkTileMode::kDecal:  name.append("decal");  break; | 
|  | case SkTileMode::kClamp:  name.append("clamp");  break; | 
|  | case SkTileMode::kRepeat: name.append("repeat"); break; | 
|  | case SkTileMode::kMirror: name.append("mirror"); break; | 
|  | } | 
|  | name.append("-out"); | 
|  | return name; | 
|  | } | 
|  |  | 
|  | void onDraw(SkCanvas* canvas) override { | 
|  | draw_example_grid(canvas, fInputMode, fOutputMode); | 
|  | } | 
|  |  | 
|  | private: | 
|  | SkTileMode fInputMode; | 
|  | SkTileMode fOutputMode; | 
|  | }; | 
|  |  | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal, SkTileMode::kDecal); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal, SkTileMode::kClamp); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal, SkTileMode::kRepeat); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal, SkTileMode::kMirror); ) | 
|  |  | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kClamp, SkTileMode::kDecal); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kClamp, SkTileMode::kClamp); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kClamp, SkTileMode::kRepeat); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kClamp, SkTileMode::kMirror); ) | 
|  |  | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kRepeat, SkTileMode::kDecal); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kRepeat, SkTileMode::kClamp); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kRepeat, SkTileMode::kRepeat); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kRepeat, SkTileMode::kMirror); ) | 
|  |  | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kMirror, SkTileMode::kDecal); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kMirror, SkTileMode::kClamp); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kMirror, SkTileMode::kRepeat); ) | 
|  | DEF_GM( return new CropImageFilterGM(SkTileMode::kMirror, SkTileMode::kMirror); ) | 
|  |  | 
|  | }  // namespace skiagm |