• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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