xref: /third_party/skia/gm/blurrect.cpp (revision cb93a386)
1/*
2* Copyright 2012 Google Inc.
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 <cmath>
9#include "gm/gm.h"
10#include "include/core/SkBitmap.h"
11#include "include/core/SkBlurTypes.h"
12#include "include/core/SkCanvas.h"
13#include "include/core/SkColor.h"
14#include "include/core/SkColorFilter.h"
15#include "include/core/SkImage.h"
16#include "include/core/SkMaskFilter.h"
17#include "include/core/SkMatrix.h"
18#include "include/core/SkPaint.h"
19#include "include/core/SkPathBuilder.h"
20#include "include/core/SkPoint.h"
21#include "include/core/SkRect.h"
22#include "include/core/SkRefCnt.h"
23#include "include/core/SkScalar.h"
24#include "include/core/SkShader.h"
25#include "include/core/SkSize.h"
26#include "include/core/SkString.h"
27#include "include/core/SkSurface.h"
28#include "include/core/SkTileMode.h"
29#include "include/core/SkTypes.h"
30#include "include/effects/SkGradientShader.h"
31#include "include/gpu/GrRecordingContext.h"
32#include "include/private/SkTo.h"
33#include "src/core/SkBlurMask.h"
34#include "src/core/SkMask.h"
35#include "src/gpu/GrRecordingContextPriv.h"
36#include "tools/timer/TimeUtils.h"
37
38#include <vector>
39
40#define STROKE_WIDTH    SkIntToScalar(10)
41
42typedef void (*Proc)(SkCanvas*, const SkRect&, const SkPaint&);
43
44static void fill_rect(SkCanvas* canvas, const SkRect& r, const SkPaint& p) {
45    canvas->drawRect(r, p);
46}
47
48static void draw_donut(SkCanvas* canvas, const SkRect& r, const SkPaint& p) {
49    SkRect        rect;
50    SkPathBuilder path;
51
52    rect = r;
53    rect.outset(STROKE_WIDTH/2, STROKE_WIDTH/2);
54    path.addRect(rect);
55    rect = r;
56    rect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2);
57
58    path.addRect(rect);
59    path.setFillType(SkPathFillType::kEvenOdd);
60
61    canvas->drawPath(path.detach(), p);
62}
63
64static void draw_donut_skewed(SkCanvas* canvas, const SkRect& r, const SkPaint& p) {
65    SkRect        rect;
66    SkPathBuilder path;
67
68    rect = r;
69    rect.outset(STROKE_WIDTH/2, STROKE_WIDTH/2);
70    path.addRect(rect);
71    rect = r;
72    rect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2);
73
74    rect.offset(7, -7);
75
76    path.addRect(rect);
77    path.setFillType(SkPathFillType::kEvenOdd);
78
79    canvas->drawPath(path.detach(), p);
80}
81
82/*
83 * Spits out an arbitrary gradient to test blur with shader on paint
84 */
85static sk_sp<SkShader> make_radial() {
86    SkPoint pts[2] = {
87        { 0, 0 },
88        { SkIntToScalar(100), SkIntToScalar(100) }
89    };
90    SkTileMode tm = SkTileMode::kClamp;
91    const SkColor colors[] = { SK_ColorRED, SK_ColorGREEN, };
92    const SkScalar pos[] = { SK_Scalar1/4, SK_Scalar1*3/4 };
93    SkMatrix scale;
94    scale.setScale(0.5f, 0.5f);
95    scale.postTranslate(25.f, 25.f);
96    SkPoint center0, center1;
97    center0.set(SkScalarAve(pts[0].fX, pts[1].fX),
98                SkScalarAve(pts[0].fY, pts[1].fY));
99    center1.set(SkScalarInterp(pts[0].fX, pts[1].fX, SkIntToScalar(3)/5),
100                SkScalarInterp(pts[0].fY, pts[1].fY, SkIntToScalar(1)/4));
101    return SkGradientShader::MakeTwoPointConical(center1, (pts[1].fX - pts[0].fX) / 7,
102                                                 center0, (pts[1].fX - pts[0].fX) / 2,
103                                                 colors, pos, SK_ARRAY_COUNT(colors), tm,
104                                                 0, &scale);
105}
106
107typedef void (*PaintProc)(SkPaint*, SkScalar width);
108
109class BlurRectGM : public skiagm::GM {
110public:
111    BlurRectGM(const char name[], U8CPU alpha) : fName(name), fAlpha(SkToU8(alpha)) {}
112
113private:
114    sk_sp<SkMaskFilter> fMaskFilters[kLastEnum_SkBlurStyle + 1];
115    const char* fName;
116    SkAlpha fAlpha;
117
118    void onOnceBeforeDraw() override {
119        for (int i = 0; i <= kLastEnum_SkBlurStyle; ++i) {
120            fMaskFilters[i] = SkMaskFilter::MakeBlur((SkBlurStyle)i,
121                                  SkBlurMask::ConvertRadiusToSigma(SkIntToScalar(STROKE_WIDTH/2)));
122        }
123    }
124
125    SkString onShortName() override { return SkString(fName); }
126
127    SkISize onISize() override { return {860, 820}; }
128
129    void onDraw(SkCanvas* canvas) override {
130        canvas->translate(STROKE_WIDTH*3/2, STROKE_WIDTH*3/2);
131
132        SkRect  r = { 0, 0, 100, 50 };
133        SkScalar scales[] = { SK_Scalar1, 0.6f };
134
135        for (size_t s = 0; s < SK_ARRAY_COUNT(scales); ++s) {
136            canvas->save();
137            for (size_t f = 0; f < SK_ARRAY_COUNT(fMaskFilters); ++f) {
138                SkPaint paint;
139                paint.setMaskFilter(fMaskFilters[f]);
140                paint.setAlpha(fAlpha);
141
142                SkPaint paintWithRadial = paint;
143                paintWithRadial.setShader(make_radial());
144
145                constexpr Proc procs[] = {
146                    fill_rect, draw_donut, draw_donut_skewed
147                };
148
149                canvas->save();
150                canvas->scale(scales[s], scales[s]);
151                this->drawProcs(canvas, r, paint, false, procs, SK_ARRAY_COUNT(procs));
152                canvas->translate(r.width() * 4/3, 0);
153                this->drawProcs(canvas, r, paintWithRadial, false, procs, SK_ARRAY_COUNT(procs));
154                canvas->translate(r.width() * 4/3, 0);
155                this->drawProcs(canvas, r, paint, true, procs, SK_ARRAY_COUNT(procs));
156                canvas->translate(r.width() * 4/3, 0);
157                this->drawProcs(canvas, r, paintWithRadial, true, procs, SK_ARRAY_COUNT(procs));
158                canvas->restore();
159
160                canvas->translate(0, SK_ARRAY_COUNT(procs) * r.height() * 4/3 * scales[s]);
161            }
162            canvas->restore();
163            canvas->translate(4 * r.width() * 4/3 * scales[s], 0);
164        }
165    }
166
167    void drawProcs(SkCanvas* canvas, const SkRect& r, const SkPaint& paint,
168                   bool doClip, const Proc procs[], size_t procsCount) {
169        SkAutoCanvasRestore acr(canvas, true);
170        for (size_t i = 0; i < procsCount; ++i) {
171            if (doClip) {
172                SkRect clipRect(r);
173                clipRect.inset(STROKE_WIDTH/2, STROKE_WIDTH/2);
174                canvas->save();
175                canvas->clipRect(r);
176            }
177            procs[i](canvas, r, paint);
178            if (doClip) {
179                canvas->restore();
180            }
181            canvas->translate(0, r.height() * 4/3);
182        }
183    }
184};
185
186DEF_SIMPLE_GM(blurrect_gallery, canvas, 1200, 1024) {
187        const int fGMWidth = 1200;
188        const int fPadding = 10;
189        const int fMargin = 100;
190
191        const int widths[] = {25, 5, 5, 100, 150, 25};
192        const int heights[] = {100, 100, 5, 25, 150, 25};
193        const SkBlurStyle styles[] = {kNormal_SkBlurStyle, kInner_SkBlurStyle, kOuter_SkBlurStyle};
194        const float radii[] = {20, 5, 10};
195
196        canvas->translate(50,20);
197
198        int cur_x = 0;
199        int cur_y = 0;
200
201        int max_height = 0;
202
203        for (size_t i = 0 ; i < SK_ARRAY_COUNT(widths) ; i++) {
204            int width = widths[i];
205            int height = heights[i];
206            SkRect r;
207            r.setWH(SkIntToScalar(width), SkIntToScalar(height));
208            SkAutoCanvasRestore autoRestore(canvas, true);
209
210            for (size_t j = 0 ; j < SK_ARRAY_COUNT(radii) ; j++) {
211                float radius = radii[j];
212                for (size_t k = 0 ; k < SK_ARRAY_COUNT(styles) ; k++) {
213                    SkBlurStyle style = styles[k];
214
215                    SkMask mask;
216                    if (!SkBlurMask::BlurRect(SkBlurMask::ConvertRadiusToSigma(radius),
217                                              &mask, r, style)) {
218                        continue;
219                    }
220
221                    SkAutoMaskFreeImage amfi(mask.fImage);
222
223                    SkBitmap bm;
224                    bm.installMaskPixels(mask);
225
226                    if (cur_x + bm.width() >= fGMWidth - fMargin) {
227                        cur_x = 0;
228                        cur_y += max_height + fPadding;
229                        max_height = 0;
230                    }
231
232                    canvas->save();
233                    canvas->translate((SkScalar)cur_x, (SkScalar)cur_y);
234                    canvas->translate(-(bm.width() - r.width())/2, -(bm.height()-r.height())/2);
235                    canvas->drawImage(bm.asImage(), 0.f, 0.f);
236                    canvas->restore();
237
238                    cur_x += bm.width() + fPadding;
239                    if (bm.height() > max_height)
240                        max_height = bm.height();
241                }
242            }
243        }
244}
245
246namespace skiagm {
247
248// Compares actual blur rects with reference masks created by the GM. Animates sigma in viewer.
249class BlurRectCompareGM : public GM {
250protected:
251    SkString onShortName() override { return SkString("blurrect_compare"); }
252
253    SkISize onISize() override { return {900, 1220}; }
254
255    void onOnceBeforeDraw() override { this->prepareReferenceMasks(); }
256
257    DrawResult onDraw(SkCanvas* canvas, SkString* errorMsg) override {
258        if (canvas->imageInfo().colorType() == kUnknown_SkColorType ||
259            (canvas->recordingContext() && !canvas->recordingContext()->asDirectContext())) {
260            *errorMsg = "Not supported when recording, relies on canvas->makeSurface()";
261            return DrawResult::kSkip;
262        }
263        int32_t ctxID = canvas->recordingContext() ? canvas->recordingContext()->priv().contextID()
264                                                   : 0;
265        if (fRecalcMasksForAnimation || !fActualMasks[0][0][0] || ctxID != fLastContextUniqueID) {
266            if (fRecalcMasksForAnimation) {
267                // Sigma is changing so references must also be recalculated.
268                this->prepareReferenceMasks();
269            }
270            this->prepareActualMasks(canvas);
271            this->prepareMaskDifferences(canvas);
272            fLastContextUniqueID = ctxID;
273            fRecalcMasksForAnimation = false;
274        }
275        canvas->clear(SK_ColorBLACK);
276        static constexpr float kMargin = 30;
277        float totalW = 0;
278        for (auto w : kSizes) {
279            totalW += w + kMargin;
280        }
281        canvas->translate(kMargin, kMargin);
282        for (int mode = 0; mode < 3; ++mode) {
283            canvas->save();
284            for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) {
285                auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost;
286                for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) {
287                    auto h = kSizes[heightIdx];
288                    canvas->save();
289                    for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) {
290                        auto w = kSizes[widthIdx];
291                        SkPaint paint;
292                        paint.setColor(SK_ColorWHITE);
293                        SkImage* img;
294                        switch (mode) {
295                            case 0:
296                                img = fReferenceMasks[sigmaIdx][heightIdx][widthIdx].get();
297                                break;
298                            case 1:
299                                img = fActualMasks[sigmaIdx][heightIdx][widthIdx].get();
300                                break;
301                            case 2:
302                                img = fMaskDifferences[sigmaIdx][heightIdx][widthIdx].get();
303                                // The error images are opaque, use kPlus so they are additive if
304                                // the overlap between test cases.
305                                paint.setBlendMode(SkBlendMode::kPlus);
306                                break;
307                        }
308                        auto pad = PadForSigma(sigma);
309                        canvas->drawImage(img, -pad, -pad, SkSamplingOptions(), &paint);
310#if 0  // Uncomment to hairline stroke around blurred rect in red on top of the blur result.
311       // The rect is defined at integer coords. We inset by 1/2 pixel so our stroke lies on top
312       // of the edge pixels.
313                        SkPaint stroke;
314                        stroke.setColor(SK_ColorRED);
315                        stroke.setStrokeWidth(0.f);
316                        stroke.setStyle(SkPaint::kStroke_Style);
317                        canvas->drawRect(SkRect::MakeWH(w, h).makeInset(0.5, 0.5), stroke);
318#endif
319                        canvas->translate(w + kMargin, 0.f);
320                    }
321                    canvas->restore();
322                    canvas->translate(0, h + kMargin);
323                }
324            }
325            canvas->restore();
326            canvas->translate(totalW + 2 * kMargin, 0);
327        }
328        return DrawResult::kOk;
329    }
330    bool onAnimate(double nanos) override {
331        fSigmaAnimationBoost = TimeUtils::SineWave(nanos, 5, 2.5f, 0.f, 2.f);
332        fRecalcMasksForAnimation = true;
333        return true;
334    }
335
336private:
337    void prepareReferenceMasks() {
338        auto create_reference_mask = [](int w, int h, float sigma, int numSubpixels) {
339            int pad = PadForSigma(sigma);
340            int maskW = w + 2 * pad;
341            int maskH = h + 2 * pad;
342            // We'll do all our calculations at subpixel resolution, so adjust params
343            w *= numSubpixels;
344            h *= numSubpixels;
345            sigma *= numSubpixels;
346            auto scale = SK_ScalarRoot2Over2 / sigma;
347            auto def_integral_approx = [scale](float a, float b) {
348                return 0.5f * (std::erf(b * scale) - std::erf(a * scale));
349            };
350            // Do the x-pass. Above/below rect are rows of zero. All rows that intersect the rect
351            // are the same. The row is calculated and stored at subpixel resolution.
352            SkASSERT(!(numSubpixels & 0b1));
353            std::unique_ptr<float[]> row(new float[maskW * numSubpixels]);
354            for (int col = 0; col < maskW * numSubpixels; ++col) {
355                // Compute distance to rect left in subpixel units
356                float ldiff = numSubpixels * pad - (col + 0.5f);
357                float rdiff = ldiff + w;
358                row[col] = def_integral_approx(ldiff, rdiff);
359            }
360            // y-pass
361            SkBitmap bmp;
362            bmp.allocPixels(SkImageInfo::MakeA8(maskW, maskH));
363            std::unique_ptr<float[]> accums(new float[maskW]);
364            const float accumScale = 1.f / (numSubpixels * numSubpixels);
365            for (int y = 0; y < maskH; ++y) {
366                // Initialize subpixel accumulation buffer for this row.
367                std::fill_n(accums.get(), maskW, 0);
368                for (int ys = 0; ys < numSubpixels; ++ys) {
369                    // At each subpixel we want to integrate over the kernel centered at the
370                    // subpixel multiplied by the x-pass. The x-pass is zero above and below the
371                    // rect and constant valued from rect top to rect bottom. So we can get the
372                    // integral of just the kernel from rect top to rect bottom and multiply by
373                    // the single x-pass value from our precomputed row.
374                    float tdiff = numSubpixels * pad - (y * numSubpixels + ys + 0.5f);
375                    float bdiff = tdiff + h;
376                    auto integral = def_integral_approx(tdiff, bdiff);
377                    for (int x = 0; x < maskW; ++x) {
378                        for (int xs = 0; xs < numSubpixels; ++xs) {
379                            int rowIdx = x * numSubpixels + xs;
380                            accums[x] += integral * row[rowIdx];
381                        }
382                    }
383                }
384                for (int x = 0; x < maskW; ++x) {
385                    auto result = accums[x] * accumScale;
386                    *bmp.getAddr8(x, y) = SkToU8(sk_float_round2int(255.f * result));
387                }
388            }
389            return bmp.asImage();
390        };
391
392        // Number of times to subsample (in both X and Y). If fRecalcMasksForAnimation is true
393        // then we're animating, don't subsample as much to keep fps higher.
394        const int numSubpixels = fRecalcMasksForAnimation ? 2 : 8;
395
396        for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) {
397            auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost;
398            for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) {
399                auto h = kSizes[heightIdx];
400                for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) {
401                    auto w = kSizes[widthIdx];
402                    fReferenceMasks[sigmaIdx][heightIdx][widthIdx] =
403                            create_reference_mask(w, h, sigma, numSubpixels);
404                }
405            }
406        }
407    }
408
409    void prepareActualMasks(SkCanvas* canvas) {
410        for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) {
411            auto sigma = kSigmas[sigmaIdx] + fSigmaAnimationBoost;
412            for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) {
413                auto h = kSizes[heightIdx];
414                for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) {
415                    auto w = kSizes[widthIdx];
416                    auto pad = PadForSigma(sigma);
417                    auto ii = SkImageInfo::MakeA8(w + 2 * pad, h + 2 * pad);
418                    auto surf = canvas->makeSurface(ii);
419                    if (!surf) {
420                        // Some GPUs don't have renderable A8 :(
421                        surf = canvas->makeSurface(ii.makeColorType(kRGBA_8888_SkColorType));
422                        if (!surf) {
423                            return;
424                        }
425                    }
426                    auto rect = SkRect::MakeXYWH(pad, pad, w, h);
427                    SkPaint paint;
428                    // Color doesn't matter if we're rendering to A8 but does if we promoted to
429                    // RGBA above.
430                    paint.setColor(SK_ColorWHITE);
431                    paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, sigma));
432                    surf->getCanvas()->drawRect(rect, paint);
433                    fActualMasks[sigmaIdx][heightIdx][widthIdx] = surf->makeImageSnapshot();
434                }
435            }
436        }
437    }
438
439    void prepareMaskDifferences(SkCanvas* canvas) {
440        for (size_t sigmaIdx = 0; sigmaIdx < kNumSigmas; ++sigmaIdx) {
441            for (size_t heightIdx = 0; heightIdx < kNumSizes; ++heightIdx) {
442                for (size_t widthIdx = 0; widthIdx < kNumSizes; ++widthIdx) {
443                    const auto& r =  fReferenceMasks[sigmaIdx][heightIdx][widthIdx];
444                    const auto& a =     fActualMasks[sigmaIdx][heightIdx][widthIdx];
445                    auto& d       = fMaskDifferences[sigmaIdx][heightIdx][widthIdx];
446                    // The actual image might not be present if we're on an abandoned GrContext.
447                    if (!a) {
448                        d.reset();
449                        continue;
450                    }
451                    SkASSERT(r->width() == a->width());
452                    SkASSERT(r->height() == a->height());
453                    auto ii = SkImageInfo::Make(r->width(), r->height(),
454                                                kRGBA_8888_SkColorType, kPremul_SkAlphaType);
455                    auto surf = canvas->makeSurface(ii);
456                    if (!surf) {
457                        return;
458                    }
459                    // We visualize the difference by turning both the alpha masks into opaque green
460                    // images (where alpha becomes the green channel) and then perform a
461                    // SkBlendMode::kDifference between them.
462                    SkPaint filterPaint;
463                    filterPaint.setColor(SK_ColorWHITE);
464                    // Actually 8 * alpha becomes green to really highlight differences.
465                    static constexpr float kGreenifyM[] = {0, 0, 0, 0, 0,
466                                                           0, 0, 0, 8, 0,
467                                                           0, 0, 0, 0, 0,
468                                                           0, 0, 0, 0, 1};
469                    auto greenifyCF = SkColorFilters::Matrix(kGreenifyM);
470                    SkPaint paint;
471                    paint.setBlendMode(SkBlendMode::kSrc);
472                    paint.setColorFilter(std::move(greenifyCF));
473                    surf->getCanvas()->drawImage(a, 0, 0, SkSamplingOptions(), &paint);
474                    paint.setBlendMode(SkBlendMode::kDifference);
475                    surf->getCanvas()->drawImage(r, 0, 0, SkSamplingOptions(), &paint);
476                    d = surf->makeImageSnapshot();
477                }
478            }
479        }
480    }
481
482    // Per side padding around mask images for a sigma. Make this overly generous to ensure bugs
483    // related to big blurs are fully visible.
484    static int PadForSigma(float sigma) { return sk_float_ceil2int(4 * sigma); }
485
486    inline static constexpr int kSizes[] = {1, 2, 4, 8, 16, 32};
487    inline static constexpr float kSigmas[] = {0.5f, 1.2f, 2.3f, 3.9f, 7.4f};
488    inline static constexpr size_t kNumSizes = SK_ARRAY_COUNT(kSizes);
489    inline static constexpr size_t kNumSigmas = SK_ARRAY_COUNT(kSigmas);
490
491    sk_sp<SkImage> fReferenceMasks[kNumSigmas][kNumSizes][kNumSizes];
492    sk_sp<SkImage> fActualMasks[kNumSigmas][kNumSizes][kNumSizes];
493    sk_sp<SkImage> fMaskDifferences[kNumSigmas][kNumSizes][kNumSizes];
494    int32_t fLastContextUniqueID;
495    // These are used only when animating.
496    float fSigmaAnimationBoost = 0;
497    bool fRecalcMasksForAnimation = false;
498};
499
500}  // namespace skiagm
501
502//////////////////////////////////////////////////////////////////////////////
503
504DEF_GM(return new BlurRectGM("blurrects", 0xFF);)
505DEF_GM(return new skiagm::BlurRectCompareGM();)
506
507//////////////////////////////////////////////////////////////////////////////
508
509DEF_SIMPLE_GM(blur_matrix_rect, canvas, 650, 685) {
510    static constexpr auto kRect = SkRect::MakeWH(14, 60);
511    static constexpr float kSigmas[] = {0.5f, 1.2f, 2.3f, 3.9f, 7.4f};
512    static constexpr size_t kNumSigmas = SK_ARRAY_COUNT(kSigmas);
513
514    const SkPoint c = {kRect.centerX(), kRect.centerY()};
515
516    std::vector<SkMatrix> matrices;
517
518    matrices.push_back(SkMatrix::RotateDeg(4.f, c));
519
520    matrices.push_back(SkMatrix::RotateDeg(63.f, c));
521
522    matrices.push_back(SkMatrix::RotateDeg(30.f, c));
523    matrices.back().preScale(1.1f, .5f);
524
525    matrices.push_back(SkMatrix::RotateDeg(147.f, c));
526    matrices.back().preScale(3.f, .1f);
527
528    SkMatrix mirror;
529    mirror.setAll(0, 1, 0,
530                  1, 0, 0,
531                  0, 0, 1);
532    matrices.push_back(SkMatrix::Concat(mirror, matrices.back()));
533
534    matrices.push_back(SkMatrix::RotateDeg(197.f, c));
535    matrices.back().preSkew(.3f, -.5f);
536
537    auto bounds = SkRect::MakeEmpty();
538    for (const auto& m : matrices) {
539        SkRect mapped;
540        m.mapRect(&mapped, kRect);
541        bounds.joinNonEmptyArg(mapped.makeSorted());
542    }
543    float blurPad = 2.f*kSigmas[kNumSigmas - 1];
544    bounds.outset(blurPad, blurPad);
545    canvas->translate(-bounds.left(), -bounds.top());
546    for (auto sigma : kSigmas) {
547        SkPaint paint;
548        paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, sigma));
549        canvas->save();
550        for (const auto& m : matrices) {
551            canvas->save();
552            canvas->concat(m);
553            canvas->drawRect(kRect, paint);
554            canvas->restore();
555            canvas->translate(0, bounds.height());
556        }
557        canvas->restore();
558        canvas->translate(bounds.width(), 0);
559    }
560}
561