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