• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 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 "samplecode/Sample.h"
9 
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkColor.h"
12 #include "include/core/SkColorFilter.h"
13 #include "include/core/SkFont.h"
14 #include "include/core/SkImage.h"
15 #include "include/core/SkImageFilter.h"
16 #include "include/core/SkImageInfo.h"
17 #include "include/core/SkPaint.h"
18 #include "include/core/SkPoint.h"
19 #include "include/core/SkRect.h"
20 #include "include/core/SkSurface.h"
21 
22 #include "include/effects/SkDashPathEffect.h"
23 #include "include/effects/SkGradientShader.h"
24 #include "include/effects/SkImageFilters.h"
25 
26 #include "src/core/SkImageFilter_Base.h"
27 #include "src/core/SkSpecialImage.h"
28 
29 #include "tools/ToolUtils.h"
30 
31 namespace {
32 
33 struct FilterNode {
34     // Pointer to the actual filter in the DAG, so it still contains its input filters and
35     // may be used as an input in an earlier node. Null when this represents the "source" input
36     sk_sp<SkImageFilter> fFilter;
37 
38     // FilterNodes wrapping each of fFilter's inputs. Leaf node when fInputNodes is empty.
39     SkTArray<FilterNode> fInputNodes;
40 
41     // Distance from root filter
42     int fDepth;
43 
44     // The source content rect (this is the same for all nodes, but is stored here for convenience)
45     SkRect fContent;
46     // The portion of the original CTM that is kept as the local matrix/ctm when filtering
47     SkMatrix fLocalCTM;
48     // The portion of the original CTM that the results should be drawn with (or given current
49     // canvas impl., the portion of the CTM that is baked into a new DAG)
50     SkMatrix fRemainingCTM;
51 
52     // Cached reverse bounds using device-space clip bounds (e.g. SkCanvas::clipRectBounds with
53     // null first argument). This represents the layer calculated in SkCanvas for the filtering.
54     // FIXME: SkCanvas (and this sample), this is seeded with the device-space clip bounds so that
55     // the implicit matrix node's reverse bounds are updated appropriately when it recurses to the
56     // original root node.
57     SkIRect fLayerBounds;
58 
59     // Cached reverse bounds using the local draw bounds (e.g. SkCanvas::clipRectBounds with the
60     // draw bounds provided as first argument). For intermediate nodes in a DAG, this is calculated
61     // to match what the filter would compute when being evaluated as part of the original DAG
62     // (i.e. if the implicit matrix filter node were not inserted at the beginning).
63     // fReverseLocalIsolatedBounds is the same, except it represents what would be calculated if
64     // only this node were being applied as the image filter.
65     SkIRect fReverseLocalBounds;
66     SkIRect fReverseLocalIsolatedBounds;
67 
68     // Cached forward bounds based on local draw bounds. For intermediate nodes in a DAG, this is
69     // calculated to match what the filter computes as part of the whole DAG. fForwardIsolatedBounds
70     // is the same but represents what would be calculated if only this node were applied.
71     SkIRect fForwardBounds;
72     SkIRect fForwardIsolatedBounds;
73 
74     // Should be called after the input nodes have been created since this will complete the
75     // entire tree.
computeBounds__anon4ad2fd810111::FilterNode76     void computeBounds() {
77         // In normal usage, forward bounds are filter-space bounds of the geometry content, so
78         // fContent mapped by the local matrix, since we assume the layer content is made by
79         // concat(localCTM) -> clipRect(content) -> drawRect(content).
80         // Similarly, in normal usage, reverse bounds are the filter-space bounds of the space to
81         // be filled by image filter results. Since the clip rect is set to the same as the content,
82         // it's the same bounds forward or reverse in this contrived case.
83         SkIRect inputRect;
84         fLocalCTM.mapRect(fContent).roundOut(&inputRect);
85 
86         this->computeForwardBounds(inputRect);
87 
88         // The layer bounds (matching what SkCanvas computes), use the content rect mapped by the
89         // entire CTM as its input rect. If this is an implicit matrix node, the computeReverseX
90         // functions will switch to using the local-mapped bounds for children in order to simulate
91         // what would happen if the last step were done as a draw. When there's no implicit matrix
92         // node, this calculated rectangle is the same as inputRect.
93         SkIRect deviceRect;
94         SkMatrix ctm = SkMatrix::Concat(fRemainingCTM, fLocalCTM);
95         ctm.mapRect(fContent).roundOut(&deviceRect);
96 
97         SkASSERT(this->isImplicitMatrixNode() || inputRect == deviceRect);
98         this->computeReverseLocalIsolatedBounds(deviceRect);
99         this->computeReverseBounds(deviceRect, false);
100         // Unlike the above two calls, calculating layer bounds will keep the device bounds for
101         // intermediate nodes to show the current SkCanvas behavior vs. the ideal
102         this->computeReverseBounds(deviceRect, true);
103     }
104 
isImplicitMatrixNode__anon4ad2fd810111::FilterNode105     bool isImplicitMatrixNode() const {
106         // In the future we wish to replace the implicit matrix node with direct draws to the final
107         // destination (instead of using an SkMatrixImageFilter). Visualizing the DAG correctly
108         // requires handling these nodes differently since it has part of the canvas CTM built in.
109         return fDepth == 1 && !fRemainingCTM.isIdentity();
110     }
111 
112 private:
computeForwardBounds__anon4ad2fd810111::FilterNode113     void computeForwardBounds(const SkIRect srcRect) {
114         if (fFilter) {
115             // For forward filtering, the leaves of the DAG are evaluated first and are then
116             // propagated to the root. This means that every filter's filterBounds() function sees
117             // the original src rect. It is never dependent on the parent node (unlike reverse
118             // filtering), so calling filterBounds() on an intermediate node gives us the correct
119             // intermediate values.
120             fForwardBounds = fFilter->filterBounds(
121                     srcRect, fLocalCTM, SkImageFilter::kForward_MapDirection, nullptr);
122 
123             // For isolated forward filtering, it uses the same input but should not be propagated
124             // to the inputs, so get the filter node bounds directly.
125             fForwardIsolatedBounds = as_IFB(fFilter)->filterNodeBounds(
126                     srcRect, fLocalCTM, SkImageFilter::kForward_MapDirection, nullptr);
127         } else {
128             fForwardBounds = srcRect;
129             fForwardIsolatedBounds = srcRect;
130         }
131 
132         // Fill in children
133         for (int i = 0; i < fInputNodes.count(); ++i) {
134             fInputNodes[i].computeForwardBounds(srcRect);
135         }
136     }
137 
computeReverseLocalIsolatedBounds__anon4ad2fd810111::FilterNode138     void computeReverseLocalIsolatedBounds(const SkIRect& srcRect) {
139         if (fFilter) {
140             fReverseLocalIsolatedBounds = as_IFB(fFilter)->filterNodeBounds(
141                     srcRect, fLocalCTM, SkImageFilter::kReverse_MapDirection, &srcRect);
142         } else {
143             fReverseLocalIsolatedBounds = srcRect;
144         }
145 
146         SkIRect childSrcRect = srcRect;
147         if (this->isImplicitMatrixNode()) {
148             // Switch srcRect from the device-space bounds to what would be used when the draw is
149             // the final step of filtering, as if the implicit node weren't needed
150             fLocalCTM.mapRect(fContent).roundOut(&childSrcRect);
151         }
152 
153         // Fill in children. Unlike regular reverse bounds mapping, the input nodes see the original
154         // bounds. Normally, the bounds that the child nodes see have already been mapped processed
155         // by this node.
156         for (int i = 0; i < fInputNodes.count(); ++i) {
157             fInputNodes[i].computeReverseLocalIsolatedBounds(childSrcRect);
158         }
159     }
160 
161     // fReverseLocalBounds and fLayerBounds are computed the same, except they differ in what the
162     // initial bounding rectangle was. It is assumed that the 'srcRect' has already been processed
163     // by the parent node's onFilterNodeBounds() function, as in SkImageFilter::filterBounds().
computeReverseBounds__anon4ad2fd810111::FilterNode164     void computeReverseBounds(const SkIRect& srcRect, bool writeToLayerBounds) {
165         SkIRect reverseBounds = srcRect;
166 
167         if (fFilter) {
168             // Since srcRect has been through parent's onFilterNodeBounds(), calling filterBounds()
169             // directly on this node will calculate the same rectangle that this filter would report
170             // during the parent node's onFilterBounds() recursion.
171             reverseBounds = fFilter->filterBounds(
172                     srcRect, fLocalCTM, SkImageFilter::kReverse_MapDirection, &srcRect);
173 
174             SkIRect nextSrcRect;
175             if (this->isImplicitMatrixNode() && !writeToLayerBounds) {
176                 // When not writing to the layer bounds, and we're the implicit matrix node
177                 // we reset the src rect to be what it should be if no implicit node was necessary.
178                 fLocalCTM.mapRect(fContent).roundOut(&nextSrcRect);
179             } else {
180                 // To calculate the appropriate intermediate reverse bounds for the children, we
181                 // need this node's onFilterNodeBounds() results based on its parents' bounds (the
182                 // current 'srcRect').
183                 nextSrcRect = as_IFB(fFilter)->filterNodeBounds(
184                     srcRect, fLocalCTM, SkImageFilter::kReverse_MapDirection, &srcRect);
185             }
186 
187             // Fill in the children. The union of these bounds should equal the value calculated
188             // for reverseBounds already.
189             SkDEBUGCODE(SkIRect netReverseBounds = SkIRect::MakeEmpty();)
190             for (int i = 0; i < fInputNodes.count(); ++i) {
191                 fInputNodes[i].computeReverseBounds(nextSrcRect, writeToLayerBounds);
192                 SkDEBUGCODE(netReverseBounds.join(
193                         writeToLayerBounds ? fInputNodes[i].fLayerBounds
194                                            : fInputNodes[i].fReverseLocalBounds);)
195             }
196             // Because of the resetting done when not computing layer bounds for the implicit
197             // matrix node, this assertion doesn't hold in that particular scenario.
198             SkASSERT(netReverseBounds == reverseBounds ||
199                      (this->isImplicitMatrixNode() && !writeToLayerBounds));
200         }
201 
202         if (writeToLayerBounds) {
203             fLayerBounds = reverseBounds;
204         } else {
205             fReverseLocalBounds = reverseBounds;
206         }
207     }
208 };
209 
210 } // anonymous namespace
211 
build_dag(const SkMatrix & local,const SkMatrix & remainder,const SkRect & rect,const SkImageFilter * filter,int depth)212 static FilterNode build_dag(const SkMatrix& local, const SkMatrix& remainder, const SkRect& rect,
213                             const SkImageFilter* filter, int depth) {
214     FilterNode node;
215     node.fFilter = sk_ref_sp(filter);
216     node.fDepth = depth;
217     node.fContent = rect;
218 
219     node.fLocalCTM = local;
220     node.fRemainingCTM = remainder;
221 
222     if (node.fFilter) {
223         if (depth > 0) {
224             // We don't visit children when at the root because the real child filters are replaced
225             // with the internalSaveLayer decomposition emulation, which then cycles back to the
226             // original filter but with an updated matrix (and then we process the children).
227             node.fInputNodes.reserve(node.fFilter->countInputs());
228             for (int i = 0; i < node.fFilter->countInputs(); ++i) {
229                 node.fInputNodes.push_back() =
230                         build_dag(local, remainder, rect, node.fFilter->getInput(i), depth + 1);
231             }
232         }
233     }
234 
235     return node;
236 }
237 
build_dag(const SkMatrix & ctm,const SkRect & rect,const SkImageFilter * rootFilter)238 static FilterNode build_dag(const SkMatrix& ctm, const SkRect& rect,
239                             const SkImageFilter* rootFilter) {
240     // Emulate SkCanvas::internalSaveLayer's decomposition of the CTM.
241     SkMatrix local;
242     sk_sp<SkImageFilter> finalFilter = as_IFB(rootFilter)->applyCTM(ctm, &local);
243 
244     // In ApplyCTMToFilter, the CTM is decomposed such that CTM = remainder * local. The matrix
245     // that is embedded in 'finalFilter' is actually local^-1*remainder*local to account for
246     // how SkMatrixImageFilter is specified, but we want the true remainder since it is what should
247     // transform the results to put in the correct place after filtering.
248     SkMatrix invLocal, remaining;
249     if (as_IFB(rootFilter)->uniqueID() != as_IFB(finalFilter)->uniqueID()) {
250         remaining = SkMatrix::Concat(ctm, invLocal);
251     } else {
252         remaining = SkMatrix::I();
253     }
254 
255     // Create a root node that represents the full result
256     FilterNode rootNode = build_dag(ctm, SkMatrix::I(), rect, rootFilter, 0);
257     // Set its only child as the modified DAG that handles the CTM decomposition
258     rootNode.fInputNodes.push_back() =
259             build_dag(local, remaining, rect, finalFilter.get(), 1);
260     // Fill in bounds information that requires the entire node DAG to have been extracted first.
261     rootNode.fInputNodes[0].computeBounds();
262     return rootNode;
263 }
264 
draw_node(SkCanvas * canvas,const FilterNode & node)265 static void draw_node(SkCanvas* canvas, const FilterNode& node) {
266     canvas->clear(SK_ColorTRANSPARENT);
267 
268     SkPaint filterPaint;
269     filterPaint.setImageFilter(node.fFilter);
270 
271     SkPaint paint;
272     static const SkColor kColors[2] = {SK_ColorGREEN, SK_ColorWHITE};
273     SkPoint points[2] = { {node.fContent.fLeft + 15.f, node.fContent.fTop + 15.f},
274                           {node.fContent.fRight - 15.f, node.fContent.fBottom - 15.f} };
275     paint.setShader(SkGradientShader::MakeLinear(points, kColors, nullptr, SK_ARRAY_COUNT(kColors),
276                                                  SkTileMode::kRepeat));
277 
278     SkPaint line;
279     line.setStrokeWidth(0.f);
280     line.setStyle(SkPaint::kStroke_Style);
281 
282     if (node.fDepth == 0) {
283         // The root node, so draw this one the canonical way through SkCanvas to show current
284         // net behavior. Will not include bounds visualization.
285         canvas->save();
286         canvas->concat(node.fLocalCTM);
287         SkASSERT(node.fRemainingCTM.isIdentity());
288 
289         canvas->clipRect(node.fContent, /* aa */ true);
290         canvas->saveLayer(nullptr, &filterPaint);
291         canvas->drawRect(node.fContent, paint);
292         canvas->restore(); // Completes the image filter
293         canvas->restore(); // Undoes matrix and clip
294 
295         // Draw content rect (no clipping)
296         canvas->save();
297         canvas->concat(node.fLocalCTM);
298         line.setColor(SK_ColorBLACK);
299         canvas->drawRect(node.fContent, line);
300         canvas->restore();
301     } else {
302         canvas->save();
303         if (!node.isImplicitMatrixNode()) {
304             canvas->concat(node.fRemainingCTM);
305         }
306         canvas->concat(node.fLocalCTM);
307 
308         canvas->saveLayer(nullptr, &filterPaint);
309         canvas->drawRect(node.fContent, paint);
310         canvas->restore(); // Completes the image filter
311 
312         // Draw content-rect bounds
313         line.setColor(SK_ColorBLACK);
314         if (node.isImplicitMatrixNode()) {
315             canvas->setMatrix(SkMatrix::Concat(node.fRemainingCTM, node.fLocalCTM));
316         }
317         canvas->drawRect(node.fContent, line);
318         canvas->restore(); // Undoes the matrix
319 
320         // Bounding boxes have all been mapped by the local matrix already, so drawing them with
321         // the remaining CTM should align everything to the already drawn filter outputs. The
322         // exception is forward bounds of the implicit matrix node, which also have been mapped
323         // by the remainder matrix.
324         canvas->save();
325         canvas->concat(node.fRemainingCTM);
326 
327         // The bounds of the layer saved for the filtering as currently implemented
328         line.setColor(SK_ColorRED);
329         canvas->drawRect(SkRect::Make(node.fLayerBounds).makeOutset(5.f, 5.f), line);
330         // The bounds of the layer that could be saved if the last step were a draw
331         line.setColor(SK_ColorMAGENTA);
332         canvas->drawRect(SkRect::Make(node.fReverseLocalBounds).makeOutset(4.f, 4.f), line);
333 
334         // Dashed lines for the isolated shapes
335         static const SkScalar kDashParams[] = {6.f, 12.f};
336         line.setPathEffect(SkDashPathEffect::Make(kDashParams, 2, 0.f));
337         // The bounds of the layer if it were the only filter in the DAG
338         canvas->drawRect(SkRect::Make(node.fReverseLocalIsolatedBounds).makeOutset(3.f, 3.f), line);
339 
340         if (node.isImplicitMatrixNode()) {
341             canvas->resetMatrix();
342         }
343         // The output bounds calculated as if the node were the only filter in the DAG
344         line.setColor(SK_ColorBLUE);
345         canvas->drawRect(SkRect::Make(node.fForwardIsolatedBounds).makeOutset(1.f, 1.f), line);
346 
347         // The output bounds calculated for the node
348         line.setPathEffect(nullptr);
349         canvas->drawRect(SkRect::Make(node.fForwardBounds).makeOutset(2.f, 2.f), line);
350 
351         canvas->restore();
352     }
353 }
354 
355 static constexpr float kLineHeight = 16.f;
356 static constexpr float kLineInset = 8.f;
357 
print_matrix(SkCanvas * canvas,const char * prefix,const SkMatrix & matrix,float x,float y,const SkFont & font,const SkPaint & paint)358 static float print_matrix(SkCanvas* canvas, const char* prefix, const SkMatrix& matrix,
359                          float x, float y, const SkFont& font, const SkPaint& paint) {
360     canvas->drawString(prefix, x, y, font, paint);
361     y += kLineHeight;
362     for (int i = 0; i < 3; ++i) {
363         SkString row;
364         row.appendf("[%.2f %.2f %.2f]",
365                     matrix.get(i * 3), matrix.get(i * 3 + 1), matrix.get(i * 3 + 2));
366         canvas->drawString(row, x, y, font, paint);
367         y += kLineHeight;
368     }
369     return y;
370 }
371 
print_size(SkCanvas * canvas,const char * prefix,const SkIRect & rect,float x,float y,const SkFont & font,const SkPaint & paint)372 static float print_size(SkCanvas* canvas, const char* prefix, const SkIRect& rect,
373                        float x, float y, const SkFont& font, const SkPaint& paint) {
374     canvas->drawString(prefix, x, y, font, paint);
375     y += kLineHeight;
376     SkString sz;
377     sz.appendf("%d x %d", rect.width(), rect.height());
378     canvas->drawString(sz, x, y, font, paint);
379     return y + kLineHeight;
380 }
381 
print_info(SkCanvas * canvas,const FilterNode & node)382 static float print_info(SkCanvas* canvas, const FilterNode& node) {
383     SkFont font(nullptr, 12);
384     SkPaint text;
385     text.setAntiAlias(true);
386 
387     float y = kLineHeight;
388     if (node.fDepth == 0) {
389         canvas->drawString("Final Results", kLineInset, y, font, text);
390         // The actual interesting matrices are in the root node's first child
391         y = print_matrix(canvas, "Local", node.fInputNodes[0].fLocalCTM,
392                      kLineInset, y + kLineHeight, font, text);
393         y = print_matrix(canvas, "Embedded", node.fInputNodes[0].fRemainingCTM,
394                      kLineInset, y, font, text);
395     } else if (node.fFilter) {
396         canvas->drawString(node.fFilter->getTypeName(), kLineInset, y, font, text);
397         print_size(canvas, "Layer Size", node.fLayerBounds, kLineInset, y + kLineHeight,
398                    font, text);
399         y = print_size(canvas, "Ideal Size", node.fReverseLocalBounds, 10 * kLineInset,
400                        y + kLineHeight, font, text);
401     } else {
402         canvas->drawString("Source Input", kLineInset, kLineHeight, font, text);
403         y += kLineHeight;
404     }
405 
406     return y;
407 }
408 
409 // Returns bottom edge in pixels that the subtree reached in canvas
draw_dag(SkCanvas * canvas,SkSurface * nodeSurface,const FilterNode & node)410 static float draw_dag(SkCanvas* canvas, SkSurface* nodeSurface, const FilterNode& node) {
411     // First capture the results of the node, into nodeSurface
412     draw_node(nodeSurface->getCanvas(), node);
413     sk_sp<SkImage> nodeResults = nodeSurface->makeImageSnapshot();
414 
415     // Fill in background of the filter node with a checkerboard
416     canvas->save();
417     canvas->clipRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height()));
418     ToolUtils::draw_checkerboard(canvas, SK_ColorGRAY, SK_ColorLTGRAY, 10);
419     canvas->restore();
420 
421     // Display filtered results in current canvas' location (assumed CTM is set for this node)
422     canvas->drawImage(nodeResults, 0, 0);
423 
424     SkPaint line;
425     line.setAntiAlias(true);
426     line.setStyle(SkPaint::kStroke_Style);
427     line.setStrokeWidth(3.f);
428 
429     // Text info
430     canvas->save();
431     canvas->translate(0, nodeResults->height());
432     float textHeight = print_info(canvas, node);
433     canvas->restore();
434 
435     // Border around filtered results + text info
436     canvas->drawRect(SkRect::MakeWH(nodeResults->width(), nodeResults->height() + textHeight),
437                      line);
438 
439     static const float kPad = 20.f;
440     float x = nodeResults->width() + kPad;
441     float y = 0;
442     for (int i = 0; i < node.fInputNodes.count(); ++i) {
443         // Line connecting this node to its child
444         canvas->drawLine(nodeResults->width(), 0.5f * nodeResults->height(), // right of node
445                          x, y + 0.5f * nodeResults->height(), line);         // left of child
446         canvas->save();
447         canvas->translate(x, y);
448         y = draw_dag(canvas, nodeSurface, node.fInputNodes[i]);
449         canvas->restore();
450     }
451     return SkMaxScalar(y, nodeResults->height() + textHeight + kPad);
452 }
453 
draw_dag(SkCanvas * canvas,sk_sp<SkImageFilter> filter,const SkRect & rect,const SkISize & surfaceSize)454 static void draw_dag(SkCanvas* canvas, sk_sp<SkImageFilter> filter,
455                      const SkRect& rect, const SkISize& surfaceSize) {
456     // Get the current CTM, which includes all the viewer's UI modifications, which we want to
457     // pass into our mock canvases for each DAG node.
458     SkMatrix ctm = canvas->getTotalMatrix();
459 
460     canvas->save();
461     // Reset the matrix so that the DAG layout and instructional text is fixed to the window.
462     canvas->resetMatrix();
463 
464     // Process the image filter DAG to display intermediate results later on, which will apply the
465     // provided CTM during draw_node calls.
466     FilterNode dag = build_dag(ctm, rect, filter.get());
467 
468     sk_sp<SkSurface> nodeSurface = canvas->makeSurface(
469             canvas->imageInfo().makeWH(surfaceSize.width(), surfaceSize.height()));
470     draw_dag(canvas, nodeSurface.get(), dag);
471 
472     canvas->restore();
473 }
474 
475 class ImageFilterDAGSample : public Sample {
476 public:
ImageFilterDAGSample()477     ImageFilterDAGSample() {}
478 
onDrawContent(SkCanvas * canvas)479     void onDrawContent(SkCanvas* canvas) override {
480         static const SkRect kFilterRect = SkRect::MakeXYWH(20.f, 20.f, 60.f, 60.f);
481         static const SkISize kFilterSurfaceSize = SkISize::Make(
482                 2 * (kFilterRect.fRight + kFilterRect.fLeft),
483                 2 * (kFilterRect.fBottom + kFilterRect.fTop));
484 
485         // Somewhat clunky, but we want to use the viewer calculated CTM in the mini surfaces used
486         // per DAG node. The rotation matrix viewer calculates is based on the sample size so trick
487         // it into calculating the right matrix for us w/ 1 frame latency.
488         this->setSize(kFilterSurfaceSize.width(), kFilterSurfaceSize.height());
489 
490         // Make a large DAG
491         //        /--- Color Filter <---- Blur <--- Offset
492         // Merge <
493         //        \--- Blur <--- Drop Shadow
494         sk_sp<SkImageFilter> drop2 = SkImageFilters::DropShadow(
495                 10.f, 5.f, 3.f, 3.f, SK_ColorBLACK, nullptr);
496         sk_sp<SkImageFilter> blur1 = SkImageFilters::Blur(2.f, 2.f, std::move(drop2));
497 
498         sk_sp<SkImageFilter> offset3 = SkImageFilters::Offset(-5.f, -5.f, nullptr);
499         sk_sp<SkImageFilter> blur2 = SkImageFilters::Blur(4.f, 4.f, std::move(offset3));
500         sk_sp<SkImageFilter> cf1 = SkImageFilters::ColorFilter(
501                 SkColorFilters::Blend(SK_ColorGRAY, SkBlendMode::kModulate), std::move(blur2));
502 
503         sk_sp<SkImageFilter> merge0 = SkImageFilters::Merge(std::move(blur1), std::move(cf1));
504 
505         draw_dag(canvas, std::move(merge0), kFilterRect, kFilterSurfaceSize);
506     }
507 
name()508     SkString name() override { return SkString("ImageFilterDAG"); }
509 
510 private:
511 
512     typedef Sample INHERITED;
513 };
514 
515 DEF_SAMPLE(return new ImageFilterDAGSample();)
516