/* * 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 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 make_cropped_image(sk_sp 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 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 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, 2, 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