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