1 /*
2 * Copyright 2021 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "gm/gm.h"
9
10 #include "include/core/SkBlendMode.h"
11 #include "include/core/SkCanvas.h"
12 #include "include/core/SkColor.h"
13 #include "include/core/SkImageFilter.h"
14 #include "include/core/SkPaint.h"
15 #include "include/core/SkRect.h"
16 #include "include/core/SkSurface.h"
17 #include "include/effects/SkDashPathEffect.h"
18 #include "include/effects/SkGradientShader.h"
19 #include "include/effects/SkImageFilters.h"
20
21 #include "tools/Resources.h"
22
23 // TODO(michaelludwig) - This will be made public within SkImageFilters.h at some point
24 #include "src/effects/imagefilters/SkCropImageFilter.h"
25
26
27 namespace {
28
29 static constexpr SkColor kOutputBoundsColor = SK_ColorRED;
30 static constexpr SkColor kCropRectColor = SK_ColorGREEN;
31 static constexpr SkColor kContentBoundsColor = SK_ColorBLUE;
32
33 static constexpr SkRect kExampleBounds = {0.f, 0.f, 100.f, 100.f};
34
35 // "Crop" refers to the rect passed to the crop image filter, "Rect" refers to some other rect
36 // from context, likely the output bounds or the content bounds.
37 enum class CropRelation {
38 kCropOverlapsRect, // Intersect but doesn't fully contain one way or the other
39 kCropContainsRect,
40 kRectContainsCrop,
41 kCropRectDisjoint,
42 };
43
make_overlap(const SkRect & r,float amountX,float amountY)44 SkRect make_overlap(const SkRect& r, float amountX, float amountY) {
45 return r.makeOffset(r.width() * amountX, r.height() * amountY);
46 }
47
make_inset(const SkRect & r,float amountX,float amountY)48 SkRect make_inset(const SkRect& r, float amountX, float amountY) {
49 return r.makeInset(r.width() * amountX, r.height() * amountY);
50 }
51
make_outset(const SkRect & r,float amountX,float amountY)52 SkRect make_outset(const SkRect& r, float amountX, float amountY) {
53 return r.makeOutset(r.width() * amountX, r.height() * amountY);
54 }
55
make_disjoint(const SkRect & r,float amountX,float amountY)56 SkRect make_disjoint(const SkRect& r, float amountX, float amountY) {
57 float xOffset = (amountX > 0.f ? (r.width() + r.width() * amountX) :
58 (amountX < 0.f ? (-r.width() + r.width() * amountX) : 0.f));
59 float yOffset = (amountY > 0.f ? (r.height() + r.height() * amountY) :
60 (amountY < 0.f ? (-r.height() + r.height() * amountY) : 0.f));
61 return r.makeOffset(xOffset, yOffset);
62 }
63
get_example_rects(CropRelation outputRelation,CropRelation inputRelation,bool hintContent,SkRect * outputBounds,SkRect * cropRect,SkRect * contentBounds)64 void get_example_rects(CropRelation outputRelation, CropRelation inputRelation, bool hintContent,
65 SkRect* outputBounds, SkRect* cropRect, SkRect* contentBounds) {
66 *outputBounds = kExampleBounds.makeInset(20.f, 20.f);
67 switch(outputRelation) {
68 case CropRelation::kCropOverlapsRect:
69 *cropRect = make_overlap(*outputBounds, -0.15f, 0.15f);
70 SkASSERT(cropRect->intersects(*outputBounds) &&
71 !cropRect->contains(*outputBounds) &&
72 !outputBounds->contains(*cropRect));
73 break;
74 case CropRelation::kCropContainsRect:
75 *cropRect = make_outset(*outputBounds, 0.15f, 0.15f);
76 SkASSERT(cropRect->contains(*outputBounds));
77 break;
78 case CropRelation::kRectContainsCrop:
79 *cropRect = make_inset(*outputBounds, 0.15f, 0.15f);
80 SkASSERT(outputBounds->contains(*cropRect));
81 break;
82 case CropRelation::kCropRectDisjoint:
83 *cropRect = make_disjoint(*outputBounds, 0.15f, 0.0f);
84 SkASSERT(!cropRect->intersects(*outputBounds));
85 break;
86 }
87
88 // Determine content bounds for example based on computed crop rect and input relation
89 if (hintContent) {
90 switch(inputRelation) {
91 case CropRelation::kCropOverlapsRect:
92 *contentBounds = make_overlap(*cropRect, 0.075f, -0.75f);
93 SkASSERT(contentBounds->intersects(*cropRect) &&
94 !contentBounds->contains(*cropRect) &&
95 !cropRect->contains(*contentBounds));
96 break;
97 case CropRelation::kCropContainsRect:
98 *contentBounds = make_inset(*cropRect, 0.075f, 0.075f);
99 SkASSERT(cropRect->contains(*contentBounds));
100 break;
101 case CropRelation::kRectContainsCrop:
102 *contentBounds = make_outset(*cropRect, 0.1f, 0.1f);
103 SkASSERT(contentBounds->contains(*cropRect));
104 break;
105 case CropRelation::kCropRectDisjoint:
106 *contentBounds = make_disjoint(*cropRect, 0.0f, 0.075f);
107 SkASSERT(!contentBounds->intersects(*cropRect));
108 break;
109 }
110 } else {
111 *contentBounds = kExampleBounds;
112 }
113 }
114
115 // TODO(michaelludwig) - This is a useful test pattern for tile modes and filtering; should
116 // consolidate it with the similar version in gpu_blur_utils if the GMs remain separate at the end.
make_image(SkCanvas * canvas,const SkRect * contentBounds)117 sk_sp<SkImage> make_image(SkCanvas* canvas, const SkRect* contentBounds) {
118 const float w = kExampleBounds.width();
119 const float h = kExampleBounds.height();
120
121 const auto srcII = SkImageInfo::Make(SkISize::Make(SkScalarCeilToInt(w), SkScalarCeilToInt(h)),
122 kRGBA_8888_SkColorType, kPremul_SkAlphaType);
123 auto surf = SkSurface::MakeRaster(srcII);
124
125 surf->getCanvas()->drawColor(SK_ColorDKGRAY);
126 SkPaint paint;
127 paint.setAntiAlias(true);
128 paint.setStyle(SkPaint::kStroke_Style);
129 // Draw four horizontal lines at 1/4, 3/8, 5/8, 3/4.
130 paint.setStrokeWidth(h/16.f);
131 paint.setColor(SK_ColorRED);
132 surf->getCanvas()->drawLine({0.f, 1.f*h/4.f}, {w, 1.f*h/4.f}, paint);
133 paint.setColor(/* sea foam */ 0xFF71EEB8);
134 surf->getCanvas()->drawLine({0.f, 3.f*h/8.f}, {w, 3.f*h/8.f}, paint);
135 paint.setColor(SK_ColorYELLOW);
136 surf->getCanvas()->drawLine({0.f, 5.f*h/8.f}, {w, 5.f*h/8.f}, paint);
137 paint.setColor(SK_ColorCYAN);
138 surf->getCanvas()->drawLine({0.f, 3.f*h/4.f}, {w, 3.f*h/4.f}, paint);
139
140 // Draw four vertical lines at 1/4, 3/8, 5/8, 3/4.
141 paint.setStrokeWidth(w/16.f);
142 paint.setColor(/* orange */ 0xFFFFA500);
143 surf->getCanvas()->drawLine({1.f*w/4.f, 0.f}, {1.f*h/4.f, h}, paint);
144 paint.setColor(SK_ColorBLUE);
145 surf->getCanvas()->drawLine({3.f*w/8.f, 0.f}, {3.f*h/8.f, h}, paint);
146 paint.setColor(SK_ColorMAGENTA);
147 surf->getCanvas()->drawLine({5.f*w/8.f, 0.f}, {5.f*h/8.f, h}, paint);
148 paint.setColor(SK_ColorGREEN);
149 surf->getCanvas()->drawLine({3.f*w/4.f, 0.f}, {3.f*h/4.f, h}, paint);
150
151 // Fill everything outside of the content bounds with red since it shouldn't be sampled from.
152 if (contentBounds) {
153 surf->getCanvas()->clipRect(*contentBounds, SkClipOp::kDifference);
154 surf->getCanvas()->clear(SK_ColorRED);
155 }
156
157 return surf->makeImageSnapshot();
158 }
159
draw_example(SkCanvas * canvas,SkTileMode inputMode,SkTileMode outputMode,CropRelation outputRelation,CropRelation inputRelation,bool hintContent)160 void draw_example(
161 SkCanvas* canvas,
162 SkTileMode inputMode, // the tile mode of the input to the crop filter
163 SkTileMode outputMode, // the tile mode that the crop filter outputs
164 CropRelation outputRelation, // how crop rect relates to output bounds
165 CropRelation inputRelation, // how crop rect relates to content bounds
166 bool hintContent) { // whether or not contentBounds is hinted to saveLayer()
167 SkASSERT(inputMode == SkTileMode::kDecal && outputMode == SkTileMode::kDecal);
168
169 // Determine crop rect for example based on output relation
170 SkRect outputBounds, cropRect, contentBounds;
171 get_example_rects(outputRelation, inputRelation, hintContent,
172 &outputBounds, &cropRect, &contentBounds);
173
174 auto image = make_image(canvas, hintContent ? &contentBounds : nullptr);
175
176 canvas->save();
177 canvas->clipRect(kExampleBounds);
178 // Visualize the image tiled on the content bounds, semi-transparent
179 {
180 SkRect clippedContentBounds;
181 if (clippedContentBounds.intersect(contentBounds, kExampleBounds)) {
182 auto contentImage = image->makeSubset(clippedContentBounds.roundOut());
183 SkPaint tiledPaint;
184 tiledPaint.setShader(contentImage->makeShader(
185 inputMode, inputMode, SkSamplingOptions(SkFilterMode::kLinear)));
186 tiledPaint.setAlphaf(0.15f);
187
188 canvas->save();
189 canvas->translate(clippedContentBounds.fLeft, clippedContentBounds.fTop);
190 canvas->drawPaint(tiledPaint);
191 canvas->restore();
192 }
193 }
194
195 // Build filter, clip, save layer, draw, restore - the interesting part is in the tile modes
196 // and how the various bounds intersect each other.
197 {
198 sk_sp<SkImageFilter> filter = SkImageFilters::Blur(4.f, 4.f, nullptr);
199 filter = SkMakeCropImageFilter(cropRect, std::move(filter));
200 SkPaint layerPaint;
201 layerPaint.setImageFilter(std::move(filter));
202
203 canvas->save();
204 canvas->clipRect(outputBounds);
205 canvas->saveLayer(hintContent ? &contentBounds : nullptr, &layerPaint);
206
207 canvas->drawImageRect(image.get(), contentBounds, contentBounds,
208 SkSamplingOptions(SkFilterMode::kLinear), nullptr,
209 SkCanvas::kFast_SrcRectConstraint);
210 canvas->restore();
211 canvas->restore();
212 }
213
214 // Visualize bounds after the actual rendering.
215 {
216 SkPaint border;
217 border.setStyle(SkPaint::kStroke_Style);
218
219 border.setColor(kOutputBoundsColor);
220 canvas->drawRect(outputBounds, border);
221
222 border.setColor(kCropRectColor);
223 canvas->drawRect(cropRect, border);
224
225 if (hintContent) {
226 border.setColor(kContentBoundsColor);
227 canvas->drawRect(contentBounds, border);
228 }
229 }
230
231 canvas->restore();
232 }
233
234
235 // Draws 2x2 examples for a given input/output tile mode that show 4 relationships between the
236 // output bounds and the crop rect (intersect, output contains crop, crop contains output, and
237 // no intersection).
238 static constexpr SkRect kPaddedTileBounds = {kExampleBounds.fLeft,
239 kExampleBounds.fTop,
240 2.f * (kExampleBounds.fRight + 1.f),
241 2.f * (kExampleBounds.fBottom + 1.f)};
draw_example_tile(SkCanvas * canvas,SkTileMode inputMode,SkTileMode outputMode,CropRelation inputRelation,bool hintContent)242 void draw_example_tile(
243 SkCanvas* canvas,
244 SkTileMode inputMode,
245 SkTileMode outputMode,
246 CropRelation inputRelation,
247 bool hintContent) {
248 auto drawQuadrant = [&](int tx, int ty, CropRelation outputRelation) {
249 canvas->save();
250 canvas->translate(tx * (kExampleBounds.fRight + 1.f), ty * (kExampleBounds.fBottom + 1.f));
251 draw_example(canvas, inputMode, outputMode, outputRelation, inputRelation, hintContent);
252 canvas->restore();
253 };
254
255 // The 4 examples, here Rect refers to the output bounds
256 drawQuadrant(0, 0, CropRelation::kCropOverlapsRect); // top left
257 drawQuadrant(1, 0, CropRelation::kRectContainsCrop); // top right
258 drawQuadrant(0, 1, CropRelation::kCropRectDisjoint); // bot left
259 drawQuadrant(1, 1, CropRelation::kCropContainsRect); // bot right
260
261 // Draw dotted lines in the 1px gap between examples
262 SkPaint dottedLine;
263 dottedLine.setColor(SK_ColorGRAY);
264 dottedLine.setStyle(SkPaint::kStroke_Style);
265 dottedLine.setStrokeCap(SkPaint::kSquare_Cap);
266 static const float kDots[2] = {0.f, 5.f};
267 dottedLine.setPathEffect(SkDashPathEffect::Make(kDots, 2, 0.f));
268
269 canvas->drawLine({kPaddedTileBounds.fLeft + 0.5f, kPaddedTileBounds.centerY() - 0.5f},
270 {kPaddedTileBounds.fRight - 0.5f, kPaddedTileBounds.centerY() - 0.5f},
271 dottedLine);
272 canvas->drawLine({kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fTop + 0.5f},
273 {kPaddedTileBounds.centerX() - 0.5f, kPaddedTileBounds.fBottom - 0.5f},
274 dottedLine);
275 }
276
277 // Draw 5 example tiles in a column for 5 relationships between content bounds and crop rect:
278 // no content hint, intersect, content contains crop, crop contains content, and no intersection
279 static constexpr SkRect kPaddedColumnBounds = {kPaddedTileBounds.fLeft,
280 kPaddedTileBounds.fTop,
281 kPaddedTileBounds.fRight,
282 5.f * kPaddedTileBounds.fBottom - 1.f};
draw_example_column(SkCanvas * canvas,SkTileMode inputMode,SkTileMode outputMode)283 void draw_example_column(
284 SkCanvas* canvas,
285 SkTileMode inputMode,
286 SkTileMode outputMode) {
287 const std::pair<CropRelation, bool> inputRelations[5] = {
288 { CropRelation::kCropOverlapsRect, false },
289 { CropRelation::kCropOverlapsRect, true },
290 { CropRelation::kCropContainsRect, true },
291 { CropRelation::kRectContainsCrop, true },
292 { CropRelation::kCropRectDisjoint, true }
293 };
294
295 canvas->save();
296 for (auto [inputRelation, hintContent] : inputRelations) {
297 draw_example_tile(canvas, inputMode, outputMode, inputRelation, hintContent);
298 canvas->translate(0.f, kPaddedTileBounds.fBottom);
299 }
300
301 canvas->restore();
302 }
303
304 // Draw 5x1 grid of examples covering supported input tile modes and crop rect relations
305 static constexpr int kNumRows = 5;
306 static constexpr int kNumCols = 1;
307 static constexpr float kGridWidth = kNumCols * kPaddedColumnBounds.fRight - 1.f;
308
draw_example_grid(SkCanvas * canvas,SkTileMode outputMode)309 void draw_example_grid(
310 SkCanvas* canvas,
311 SkTileMode outputMode) {
312 canvas->save();
313 for (auto inputMode : {SkTileMode::kDecal}) {
314 draw_example_column(canvas, inputMode, outputMode);
315 canvas->translate(kPaddedColumnBounds.fRight, 0.f);
316 }
317 canvas->restore();
318
319 // Draw dashed lines between rows and columns
320 SkPaint dashedLine;
321 dashedLine.setColor(SK_ColorGRAY);
322 dashedLine.setStyle(SkPaint::kStroke_Style);
323 dashedLine.setStrokeCap(SkPaint::kSquare_Cap);
324 static const float kDashes[2] = {5.f, 15.f};
325 dashedLine.setPathEffect(SkDashPathEffect::Make(kDashes, 2, 0.f));
326
327 for (int y = 1; y < kNumRows; ++y) {
328 canvas->drawLine({0.5f, y * kPaddedTileBounds.fBottom - 0.5f},
329 {kGridWidth - 0.5f, y * kPaddedTileBounds.fBottom - 0.5f}, dashedLine);
330 }
331 for (int x = 1; x < kNumCols; ++x) {
332 canvas->drawLine({x * kPaddedTileBounds.fRight - 0.5f, 0.5f},
333 {x * kPaddedTileBounds.fRight - 0.5f, kPaddedColumnBounds.fBottom - 0.5f},
334 dashedLine);
335 }
336 }
337
338 } // namespace
339
340 namespace skiagm {
341
342 class CropImageFilterGM : public GM {
343 public:
CropImageFilterGM(SkTileMode outputMode)344 CropImageFilterGM(SkTileMode outputMode) : fOutputMode(outputMode) {}
345
346 protected:
onISize()347 SkISize onISize() override {
348 return {SkScalarRoundToInt(kGridWidth), SkScalarRoundToInt(kPaddedColumnBounds.fBottom)};
349 }
onShortName()350 SkString onShortName() override {
351 SkString name("crop_imagefilter_");
352 switch (fOutputMode) {
353 case SkTileMode::kDecal: name.append("decal"); break;
354 case SkTileMode::kClamp: name.append("clamp"); break;
355 case SkTileMode::kRepeat: name.append("repeat"); break;
356 case SkTileMode::kMirror: name.append("mirror"); break;
357 }
358 return name;
359 }
360
onDraw(SkCanvas * canvas)361 void onDraw(SkCanvas* canvas) override {
362 draw_example_grid(canvas, fOutputMode);
363 }
364
365 private:
366 SkTileMode fOutputMode;
367 };
368
369 DEF_GM( return new CropImageFilterGM(SkTileMode::kDecal); )
370 // TODO(michaelludwig) - will add GM defs for other output tile modes once supported
371
372 } // namespace skiagm
373