blob: 556026379bb15a00617ad7ff40ca8fee0ef42a3c [file] [log] [blame]
/*
* 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/Resources.h"
#include "tools/ToolUtils.h"
// TODO(michaelludwig) - This will be made public within SkImageFilters.h at some point
#include "src/effects/imagefilters/SkCropImageFilter.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;
}
// 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;
}
} 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)),
kRGBA_8888_SkColorType, kPremul_SkAlphaType);
auto surf = SkSurface::MakeRaster(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) {
surf->getCanvas()->clipRect(*contentBounds, SkClipOp::kDifference);
surf->getCanvas()->clear(SK_ColorRED);
}
return surf->makeImageSnapshot();
}
void draw_example(
SkCanvas* canvas,
SkTileMode inputMode, // the tile mode of the input to the crop filter
SkTileMode outputMode, // the tile mode that the crop filter outputs
CropRelation outputRelation, // how crop rect relates to output bounds
CropRelation inputRelation, // how crop rect relates to content bounds
bool hintContent) { // whether or not contentBounds is hinted to saveLayer()
SkASSERT(inputMode == SkTileMode::kDecal && outputMode == SkTileMode::kDecal);
// Determine crop rect for example based on output relation
SkRect outputBounds, cropRect, contentBounds;
get_example_rects(outputRelation, inputRelation, hintContent,
&outputBounds, &cropRect, &contentBounds);
auto image = make_image(canvas, hintContent ? &contentBounds : nullptr);
canvas->save();
canvas->clipRect(kExampleBounds);
// Visualize the image tiled on the content bounds, semi-transparent
{
SkRect clippedContentBounds;
if (clippedContentBounds.intersect(contentBounds, kExampleBounds)) {
auto contentImage = ToolUtils::MakeTextureImage(
canvas, image->makeSubset(clippedContentBounds.roundOut()));
if (contentImage) {
SkPaint tiledPaint;
tiledPaint.setShader(contentImage->makeShader(
inputMode, inputMode, SkSamplingOptions(SkFilterMode::kLinear)));
tiledPaint.setAlphaf(0.15f);
canvas->save();
canvas->translate(clippedContentBounds.fLeft, clippedContentBounds.fTop);
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::Blur(4.f, 4.f, nullptr);
filter = SkMakeCropImageFilter(cropRect, 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::kLinear), nullptr,
SkCanvas::kFast_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();
}
// Draws 2x2 examples for a given input/output tile mode that show 4 relationships between the
// output bounds and the crop rect (intersect, output contains crop, crop contains output, and
// no intersection).
static constexpr SkRect kPaddedTileBounds = {kExampleBounds.fLeft,
kExampleBounds.fTop,
2.f * (kExampleBounds.fRight + 1.f),
2.f * (kExampleBounds.fBottom + 1.f)};
void draw_example_tile(
SkCanvas* canvas,
SkTileMode inputMode,
SkTileMode outputMode,
CropRelation inputRelation,
bool hintContent) {
auto drawQuadrant = [&](int tx, int ty, CropRelation outputRelation) {
canvas->save();
canvas->translate(tx * (kExampleBounds.fRight + 1.f), ty * (kExampleBounds.fBottom + 1.f));
draw_example(canvas, inputMode, outputMode, outputRelation, inputRelation, hintContent);
canvas->restore();
};
// The 4 examples, here Rect refers to the output bounds
drawQuadrant(0, 0, CropRelation::kCropOverlapsRect); // top left
drawQuadrant(1, 0, CropRelation::kRectContainsCrop); // top right
drawQuadrant(0, 1, CropRelation::kCropRectDisjoint); // bot left
drawQuadrant(1, 1, CropRelation::kCropContainsRect); // bot right
// Draw dotted lines in the 1px gap between examples
SkPaint dottedLine;
dottedLine.setColor(SK_ColorGRAY);
dottedLine.setStyle(SkPaint::kStroke_Style);
dottedLine.setStrokeCap(SkPaint::kSquare_Cap);
static const float kDots[2] = {0.f, 5.f};
dottedLine.setPathEffect(SkDashPathEffect::Make(kDots, 2, 0.f));
canvas->drawLine({kPaddedTileBounds.fLeft + 0.5f, kPaddedTileBounds.centerY() - 0.5f},
{kPaddedTileBounds.fRight - 0.5f, kPaddedTileBounds.centerY() - 0.5f},
dottedLine);
canvas->drawLine({kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fTop + 0.5f},
{kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fBottom - 0.5f},
dottedLine);
}
// 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
static constexpr SkRect kPaddedColumnBounds = {kPaddedTileBounds.fLeft,
kPaddedTileBounds.fTop,
kPaddedTileBounds.fRight,
5.f * kPaddedTileBounds.fBottom - 1.f};
void draw_example_column(
SkCanvas* canvas,
SkTileMode inputMode,
SkTileMode outputMode) {
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, outputMode, inputRelation, hintContent);
canvas->translate(0.f, kPaddedTileBounds.fBottom);
}
canvas->restore();
}
// Draw 5x1 grid of examples covering supported input tile modes and crop rect relations
static constexpr int kNumRows = 5;
static constexpr int kNumCols = 1;
static constexpr float kGridWidth = kNumCols * kPaddedColumnBounds.fRight - 1.f;
void draw_example_grid(
SkCanvas* canvas,
SkTileMode outputMode) {
canvas->save();
for (auto inputMode : {SkTileMode::kDecal}) {
draw_example_column(canvas, inputMode, outputMode);
canvas->translate(kPaddedColumnBounds.fRight, 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 * kPaddedTileBounds.fBottom - 0.5f},
{kGridWidth - 0.5f, y * kPaddedTileBounds.fBottom - 0.5f}, dashedLine);
}
for (int x = 1; x < kNumCols; ++x) {
canvas->drawLine({x * kPaddedTileBounds.fRight - 0.5f, 0.5f},
{x * kPaddedTileBounds.fRight - 0.5f, kPaddedColumnBounds.fBottom - 0.5f},
dashedLine);
}
}
} // namespace
namespace skiagm {
class CropImageFilterGM : public GM {
public:
CropImageFilterGM(SkTileMode outputMode) : fOutputMode(outputMode) {}
protected:
SkISize onISize() override {
return {SkScalarRoundToInt(kGridWidth), SkScalarRoundToInt(kPaddedColumnBounds.fBottom)};
}
SkString onShortName() override {
SkString name("crop_imagefilter_");
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;
}
return name;
}
void onDraw(SkCanvas* canvas) override {
draw_example_grid(canvas, fOutputMode);
}
private:
SkTileMode fOutputMode;
};
DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal); )
// TODO(michaelludwig) - will add GM defs for other output tile modes once supported
} // namespace skiagm