xref: /aosp_15_r20/external/skia/tools/viewer/DegenerateQuadsSlide.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2019 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 "include/core/SkCanvas.h"
9 #include "include/core/SkPaint.h"
10 #include "include/core/SkPathEffect.h"
11 #include "include/effects/SkDashPathEffect.h"
12 #include "include/pathops/SkPathOps.h"
13 #include "include/private/base/SkTPin.h"
14 #include "src/gpu/ganesh/geometry/GrQuad.h"
15 #include "src/gpu/ganesh/ops/QuadPerEdgeAA.h"
16 #include "tools/viewer/ClickHandlerSlide.h"
17 
18 using VertexSpec = skgpu::ganesh::QuadPerEdgeAA::VertexSpec;
19 using ColorType = skgpu::ganesh::QuadPerEdgeAA::ColorType;
20 using Subset = skgpu::ganesh::QuadPerEdgeAA::Subset;
21 using IndexBufferOption = skgpu::ganesh::QuadPerEdgeAA::IndexBufferOption;
22 
23 // Draw a line through the two points, outset by a fixed length in screen space
draw_extended_line(SkCanvas * canvas,const SkPaint & paint,const SkPoint & p0,const SkPoint & p1)24 static void draw_extended_line(SkCanvas* canvas, const SkPaint& paint,
25                                const SkPoint& p0, const SkPoint& p1) {
26     SkVector v = p1 - p0;
27     v.setLength(v.length() + 3.f);
28     canvas->drawLine(p1 - v, p0 + v, paint);
29 
30     // Draw normal vector too
31     SkPaint normalPaint = paint;
32     normalPaint.setPathEffect(nullptr);
33     normalPaint.setStrokeWidth(paint.getStrokeWidth() / 4.f);
34 
35     SkVector n = {v.fY, -v.fX};
36     n.setLength(.25f);
37     SkPoint m = (p0 + p1) * 0.5f;
38     canvas->drawLine(m, m + n, normalPaint);
39 }
40 
make_aa_line(const SkPoint & p0,const SkPoint & p1,bool aaOn,bool outset,SkPoint line[2])41 static void make_aa_line(const SkPoint& p0, const SkPoint& p1, bool aaOn,
42                          bool outset, SkPoint line[2]) {
43     SkVector n = {0.f, 0.f};
44     if (aaOn) {
45         SkVector v = p1 - p0;
46         n = outset ? SkVector::Make(v.fY, -v.fX) : SkVector::Make(-v.fY, v.fX);
47         n.setLength(0.5f);
48     }
49 
50     line[0] = p0 + n;
51     line[1] = p1 + n;
52 }
53 
54 // To the line through l0-l1, not capped at the end points of the segment
signed_distance(const SkPoint & p,const SkPoint & l0,const SkPoint & l1)55 static SkScalar signed_distance(const SkPoint& p, const SkPoint& l0, const SkPoint& l1) {
56     SkVector v = l1 - l0;
57     v.normalize();
58     SkVector n = {v.fY, -v.fX};
59     SkScalar c = -n.dot(l0);
60     return n.dot(p) + c;
61 }
62 
get_area_coverage(const bool edgeAA[4],const SkPoint corners[4],const SkPoint & point)63 static SkScalar get_area_coverage(const bool edgeAA[4], const SkPoint corners[4],
64                                   const SkPoint& point) {
65     SkPath shape;
66     shape.addPoly(corners, 4, true);
67     SkPath pixel;
68     pixel.addRect(SkRect::MakeXYWH(point.fX - 0.5f, point.fY - 0.5f, 1.f, 1.f));
69 
70     SkPath intersection;
71     if (!Op(shape, pixel, kIntersect_SkPathOp, &intersection) || intersection.isEmpty()) {
72         return 0.f;
73     }
74 
75     // Calculate area of the convex polygon
76     SkScalar area = 0.f;
77     for (int i = 0; i < intersection.countPoints(); ++i) {
78         SkPoint p0 = intersection.getPoint(i);
79         SkPoint p1 = intersection.getPoint((i + 1) % intersection.countPoints());
80         SkScalar det = p0.fX * p1.fY - p1.fX * p0.fY;
81         area += det;
82     }
83 
84     // Scale by 1/2, then take abs value (this area formula is signed based on point winding, but
85     // since it's convex, just make it positive).
86     area = SkScalarAbs(0.5f * area);
87 
88     // Now account for the edge AA. If the pixel center is outside of a non-AA edge, turn of its
89     // coverage. If the pixel only intersects non-AA edges, then set coverage to 1.
90     bool needsNonAA = false;
91     SkScalar edgeD[4];
92     for (int i = 0; i < 4; ++i) {
93         SkPoint e0 = corners[i];
94         SkPoint e1 = corners[(i + 1) % 4];
95         edgeD[i] = -signed_distance(point, e0, e1);
96         if (!edgeAA[i]) {
97             if (edgeD[i] < -1e-4f) {
98                 return 0.f; // Outside of non-AA line
99             }
100             needsNonAA = true;
101         }
102     }
103     // Otherwise inside the shape, so check if any AA edge exerts influence over nonAA
104     if (needsNonAA) {
105         for (int i = 0; i < 4; i++) {
106             if (edgeAA[i] && edgeD[i] < 0.5f) {
107                 needsNonAA = false;
108                 break;
109             }
110         }
111     }
112     return needsNonAA ? 1.f : area;
113 }
114 
115 // FIXME take into account max coverage properly,
get_edge_dist_coverage(const bool edgeAA[4],const SkPoint corners[4],const SkPoint outsetLines[8],const SkPoint insetLines[8],const SkPoint & point)116 static SkScalar get_edge_dist_coverage(const bool edgeAA[4], const SkPoint corners[4],
117                                        const SkPoint outsetLines[8], const SkPoint insetLines[8],
118                                        const SkPoint& point) {
119     bool flip = false;
120     // If the quad has been inverted, the original corners will not all be on the negative side of
121     // every outset line. When that happens, calculate coverage using the "inset" lines and flip
122     // the signed distance
123     for (int i = 0; i < 4; ++i) {
124         for (int j = 0; j < 4; ++j) {
125             SkScalar d = signed_distance(corners[i], outsetLines[j * 2], outsetLines[j * 2 + 1]);
126             if (d > 1e-4f) {
127                 flip = true;
128                 break;
129             }
130         }
131         if (flip) {
132             break;
133         }
134     }
135 
136     const SkPoint* lines = flip ? insetLines : outsetLines;
137 
138     SkScalar minCoverage = 1.f;
139     for (int i = 0; i < 4; ++i) {
140         // Multiply by negative 1 so that outside points have negative distances
141         SkScalar d = (flip ? 1 : -1) * signed_distance(point, lines[i * 2], lines[i * 2 + 1]);
142         if (!edgeAA[i] && d >= -1e-4f) {
143             d = 1.f;
144         }
145         if (d < minCoverage) {
146             minCoverage = d;
147             if (minCoverage < 0.f) {
148                 break; // Outside the shape
149             }
150         }
151     }
152     return minCoverage < 0.f ? 0.f : minCoverage;
153 }
154 
inside_triangle(const SkPoint & point,const SkPoint & t0,const SkPoint & t1,const SkPoint & t2,SkScalar bary[3])155 static bool inside_triangle(const SkPoint& point, const SkPoint& t0, const SkPoint& t1,
156                             const SkPoint& t2, SkScalar bary[3]) {
157     // Check sign of t0 to (t1,t2). If it is positive, that means the normals point into the
158     // triangle otherwise the normals point outside the triangle so update edge distances as
159     // necessary
160     bool flip = signed_distance(t0, t1, t2) < 0.f;
161 
162     SkScalar d0 = (flip ? -1 : 1) * signed_distance(point, t0, t1);
163     SkScalar d1 = (flip ? -1 : 1) * signed_distance(point, t1, t2);
164     SkScalar d2 = (flip ? -1 : 1) * signed_distance(point, t2, t0);
165     // Be a little forgiving
166     if (d0 < -1e-4f || d1 < -1e-4f || d2 < -1e-4f) {
167         return false;
168     }
169 
170     // Inside, so calculate barycentric coords from the sideline distances
171     SkScalar d01 = (t0 - t1).length();
172     SkScalar d12 = (t1 - t2).length();
173     SkScalar d20 = (t2 - t0).length();
174 
175     if (SkScalarNearlyZero(d12) || SkScalarNearlyZero(d20) || SkScalarNearlyZero(d01)) {
176         // Empty degenerate triangle
177         return false;
178     }
179 
180     // Coordinates for a vertex use distances to the opposite edge
181     bary[0] = d1 * d12;
182     bary[1] = d2 * d20;
183     bary[2] = d0 * d01;
184     // And normalize
185     SkScalar sum = bary[0] + bary[1] + bary[2];
186     bary[0] /= sum;
187     bary[1] /= sum;
188     bary[2] /= sum;
189 
190     return true;
191 }
192 
get_framed_coverage(const SkPoint outer[4],const SkScalar outerCoverages[4],const SkPoint inner[4],const SkScalar innerCoverages[4],const SkRect & geomDomain,const SkPoint & point)193 static SkScalar get_framed_coverage(const SkPoint outer[4], const SkScalar outerCoverages[4],
194                                     const SkPoint inner[4], const SkScalar innerCoverages[4],
195                                     const SkRect& geomDomain, const SkPoint& point) {
196     // Triangles are ordered clock wise. Indices >= 4 refer to inner[i - 4]. Otherwise its outer[i].
197     static const int kFrameTris[] = {
198         0, 1, 4,   4, 1, 5,
199         1, 2, 5,   5, 2, 6,
200         2, 3, 6,   6, 3, 7,
201         3, 0, 7,   7, 0, 4,
202         4, 5, 7,   7, 5, 6
203     };
204     static const int kNumTris = 10;
205 
206     SkScalar bary[3];
207     for (int i = 0; i < kNumTris; ++i) {
208         int i0 = kFrameTris[i * 3];
209         int i1 = kFrameTris[i * 3 + 1];
210         int i2 = kFrameTris[i * 3 + 2];
211 
212         SkPoint t0 = i0 >= 4 ? inner[i0 - 4] : outer[i0];
213         SkPoint t1 = i1 >= 4 ? inner[i1 - 4] : outer[i1];
214         SkPoint t2 = i2 >= 4 ? inner[i2 - 4] : outer[i2];
215         if (inside_triangle(point, t0, t1, t2, bary)) {
216             // Calculate coverage by barycentric interpolation of coverages
217             SkScalar c0 = i0 >= 4 ? innerCoverages[i0 - 4] : outerCoverages[i0];
218             SkScalar c1 = i1 >= 4 ? innerCoverages[i1 - 4] : outerCoverages[i1];
219             SkScalar c2 = i2 >= 4 ? innerCoverages[i2 - 4] : outerCoverages[i2];
220 
221             SkScalar coverage = bary[0] * c0 + bary[1] * c1 + bary[2] * c2;
222             if (coverage < 0.5f) {
223                 // Check distances to domain
224                 SkScalar l = SkTPin(point.fX - geomDomain.fLeft, 0.f, 1.f);
225                 SkScalar t = SkTPin(point.fY - geomDomain.fTop, 0.f, 1.f);
226                 SkScalar r = SkTPin(geomDomain.fRight - point.fX, 0.f, 1.f);
227                 SkScalar b = SkTPin(geomDomain.fBottom - point.fY, 0.f, 1.f);
228                 coverage = std::min(coverage, l * t * r * b);
229             }
230             return coverage;
231         }
232     }
233     // Not inside any triangle
234     return 0.f;
235 }
236 
237 static constexpr SkScalar kViewScale = 100.f;
238 static constexpr SkScalar kViewOffset = 200.f;
239 
240 class DegenerateQuadSlide : public ClickHandlerSlide {
241 public:
DegenerateQuadSlide(const SkRect & rect)242     DegenerateQuadSlide(const SkRect& rect)
243             : fOuterRect(rect)
244             , fCoverageMode(CoverageMode::kArea) {
245         fOuterRect.toQuad(fCorners);
246         for (int i = 0; i < 4; ++i) {
247             fEdgeAA[i] = true;
248         }
249         fName = "DegenerateQuad";
250     }
251 
draw(SkCanvas * canvas)252     void draw(SkCanvas* canvas) override {
253         static const SkScalar kDotParams[2] = {1.f / kViewScale, 12.f / kViewScale};
254         sk_sp<SkPathEffect> dots = SkDashPathEffect::Make(kDotParams, 2, 0.f);
255         static const SkScalar kDashParams[2] = {8.f / kViewScale, 12.f / kViewScale};
256         sk_sp<SkPathEffect> dashes = SkDashPathEffect::Make(kDashParams, 2, 0.f);
257 
258         SkPaint circlePaint;
259         circlePaint.setAntiAlias(true);
260 
261         SkPaint linePaint;
262         linePaint.setAntiAlias(true);
263         linePaint.setStyle(SkPaint::kStroke_Style);
264         linePaint.setStrokeWidth(4.f / kViewScale);
265         linePaint.setStrokeJoin(SkPaint::kRound_Join);
266         linePaint.setStrokeCap(SkPaint::kRound_Cap);
267 
268         canvas->translate(kViewOffset, kViewOffset);
269         canvas->scale(kViewScale, kViewScale);
270 
271         // Draw the outer rectangle as a dotted line
272         linePaint.setPathEffect(dots);
273         canvas->drawRect(fOuterRect, linePaint);
274 
275         bool valid = this->isValid();
276 
277         if (valid) {
278             SkPoint outsets[8];
279             SkPoint insets[8];
280             // Calculate inset and outset lines for edge-distance visualization
281             for (int i = 0; i < 4; ++i) {
282                 make_aa_line(fCorners[i], fCorners[(i + 1) % 4], fEdgeAA[i], true, outsets + i * 2);
283                 make_aa_line(fCorners[i], fCorners[(i + 1) % 4], fEdgeAA[i], false, insets + i * 2);
284             }
285 
286             // Calculate inner and outer meshes for GPU visualization
287             SkPoint gpuOutset[4];
288             SkScalar gpuOutsetCoverage[4];
289             SkPoint gpuInset[4];
290             SkScalar gpuInsetCoverage[4];
291             SkRect gpuDomain;
292             this->getTessellatedPoints(gpuInset, gpuInsetCoverage, gpuOutset, gpuOutsetCoverage,
293                                        &gpuDomain);
294 
295             // Visualize the coverage values across the clamping rectangle, but test pixels outside
296             // of the "outer" rect since some quad edges can be outset extra far.
297             SkPaint pixelPaint;
298             pixelPaint.setAntiAlias(true);
299             SkRect covRect = fOuterRect.makeOutset(2.f, 2.f);
300             for (SkScalar py = covRect.fTop; py < covRect.fBottom; py += 1.f) {
301                 for (SkScalar px = covRect.fLeft; px < covRect.fRight; px += 1.f) {
302                     // px and py are the top-left corner of the current pixel, so get center's
303                     // coordinate
304                     SkPoint pixelCenter = {px + 0.5f, py + 0.5f};
305                     SkScalar coverage;
306                     if (fCoverageMode == CoverageMode::kArea) {
307                         coverage = get_area_coverage(fEdgeAA, fCorners, pixelCenter);
308                     } else if (fCoverageMode == CoverageMode::kEdgeDistance) {
309                         coverage = get_edge_dist_coverage(fEdgeAA, fCorners, outsets, insets,
310                                                           pixelCenter);
311                     } else {
312                         SkASSERT(fCoverageMode == CoverageMode::kGPUMesh);
313                         coverage = get_framed_coverage(gpuOutset, gpuOutsetCoverage,
314                                                        gpuInset, gpuInsetCoverage, gpuDomain,
315                                                        pixelCenter);
316                     }
317 
318                     SkRect pixelRect = SkRect::MakeXYWH(px, py, 1.f, 1.f);
319                     pixelRect.inset(0.1f, 0.1f);
320 
321                     SkScalar a = 1.f - 0.5f * coverage;
322                     pixelPaint.setColor4f({a, a, a, 1.f}, nullptr);
323                     canvas->drawRect(pixelRect, pixelPaint);
324 
325                     pixelPaint.setColor(coverage > 0.f ? SK_ColorGREEN : SK_ColorRED);
326                     pixelRect.inset(0.38f, 0.38f);
327                     canvas->drawRect(pixelRect, pixelPaint);
328                 }
329             }
330 
331             linePaint.setPathEffect(dashes);
332             // Draw the inset/outset "infinite" lines
333             if (fCoverageMode == CoverageMode::kEdgeDistance) {
334                 for (int i = 0; i < 4; ++i) {
335                     if (fEdgeAA[i]) {
336                         linePaint.setColor(SK_ColorBLUE);
337                         draw_extended_line(canvas, linePaint, outsets[i * 2], outsets[i * 2 + 1]);
338                         linePaint.setColor(SK_ColorGREEN);
339                         draw_extended_line(canvas, linePaint, insets[i * 2], insets[i * 2 + 1]);
340                     } else {
341                         // Both outset and inset are the same line, so only draw one in cyan
342                         linePaint.setColor(SK_ColorCYAN);
343                         draw_extended_line(canvas, linePaint, outsets[i * 2], outsets[i * 2 + 1]);
344                     }
345                 }
346             }
347 
348             linePaint.setPathEffect(nullptr);
349             // What is tessellated using GrQuadPerEdgeAA
350             if (fCoverageMode == CoverageMode::kGPUMesh) {
351                 SkPath outsetPath;
352                 outsetPath.addPoly(gpuOutset, 4, true);
353                 linePaint.setColor(SK_ColorBLUE);
354                 canvas->drawPath(outsetPath, linePaint);
355 
356                 SkPath insetPath;
357                 insetPath.addPoly(gpuInset, 4, true);
358                 linePaint.setColor(SK_ColorGREEN);
359                 canvas->drawPath(insetPath, linePaint);
360 
361                 SkPaint domainPaint = linePaint;
362                 domainPaint.setStrokeWidth(2.f / kViewScale);
363                 domainPaint.setPathEffect(dashes);
364                 domainPaint.setColor(SK_ColorMAGENTA);
365                 canvas->drawRect(gpuDomain, domainPaint);
366             }
367 
368             // Draw the edges of the true quad as a solid line
369             SkPath path;
370             path.addPoly(fCorners, 4, true);
371             linePaint.setColor(SK_ColorBLACK);
372             canvas->drawPath(path, linePaint);
373         } else {
374             // Draw the edges of the true quad as a solid *red* line
375             SkPath path;
376             path.addPoly(fCorners, 4, true);
377             linePaint.setColor(SK_ColorRED);
378             linePaint.setPathEffect(nullptr);
379             canvas->drawPath(path, linePaint);
380         }
381 
382         // Draw the four clickable corners as circles
383         circlePaint.setColor(valid ? SK_ColorBLACK : SK_ColorRED);
384         for (int i = 0; i < 4; ++i) {
385             canvas->drawCircle(fCorners[i], 5.f / kViewScale, circlePaint);
386         }
387     }
388 
389     bool onChar(SkUnichar) override;
390 
391 
392 protected:
393     Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
394     bool onClick(Click*) override;
395 
396 private:
397     class Click;
398 
399     enum class CoverageMode {
400         kArea, kEdgeDistance, kGPUMesh
401     };
402 
403     const SkRect fOuterRect;
404     SkPoint fCorners[4]; // TL, TR, BR, BL
405     bool fEdgeAA[4]; // T, R, B, L
406     CoverageMode fCoverageMode;
407 
isValid() const408     bool isValid() const {
409         SkPath path;
410         path.addPoly(fCorners, 4, true);
411         return path.isConvex();
412     }
413 
getTessellatedPoints(SkPoint inset[4],SkScalar insetCoverage[4],SkPoint outset[4],SkScalar outsetCoverage[4],SkRect * domain) const414     void getTessellatedPoints(SkPoint inset[4], SkScalar insetCoverage[4], SkPoint outset[4],
415                               SkScalar outsetCoverage[4], SkRect* domain) const {
416         // Fixed vertex spec for extracting the picture frame geometry
417         static const VertexSpec kSpec =
418             {GrQuad::Type::kGeneral, ColorType::kNone,
419              GrQuad::Type::kAxisAligned, false, Subset::kNo,
420              GrAAType::kCoverage, false, IndexBufferOption::kPictureFramed};
421         static const GrQuad kIgnored(SkRect::MakeEmpty());
422 
423         GrQuadAAFlags flags = GrQuadAAFlags::kNone;
424         flags |= fEdgeAA[0] ? GrQuadAAFlags::kTop : GrQuadAAFlags::kNone;
425         flags |= fEdgeAA[1] ? GrQuadAAFlags::kRight : GrQuadAAFlags::kNone;
426         flags |= fEdgeAA[2] ? GrQuadAAFlags::kBottom : GrQuadAAFlags::kNone;
427         flags |= fEdgeAA[3] ? GrQuadAAFlags::kLeft : GrQuadAAFlags::kNone;
428 
429         GrQuad quad = GrQuad::MakeFromSkQuad(fCorners, SkMatrix::I());
430 
431         float vertices[56]; // 2 quads, with x, y, coverage, and geometry domain (7 floats x 8 vert)
432         skgpu::ganesh::QuadPerEdgeAA::Tessellator tessellator(kSpec, (char*)vertices);
433         tessellator.append(&quad, nullptr, {1.f, 1.f, 1.f, 1.f},
434                            SkRect::MakeEmpty(), flags);
435 
436         // The first quad in vertices is the inset, then the outset, but they
437         // are ordered TL, BL, TR, BR so un-interleave coverage and re-arrange
438         inset[0] = {vertices[0], vertices[1]}; // TL
439         insetCoverage[0] = vertices[2];
440         inset[3] = {vertices[7], vertices[8]}; // BL
441         insetCoverage[3] = vertices[9];
442         inset[1] = {vertices[14], vertices[15]}; // TR
443         insetCoverage[1] = vertices[16];
444         inset[2] = {vertices[21], vertices[22]}; // BR
445         insetCoverage[2] = vertices[23];
446 
447         outset[0] = {vertices[28], vertices[29]}; // TL
448         outsetCoverage[0] = vertices[30];
449         outset[3] = {vertices[35], vertices[36]}; // BL
450         outsetCoverage[3] = vertices[37];
451         outset[1] = {vertices[42], vertices[43]}; // TR
452         outsetCoverage[1] = vertices[44];
453         outset[2] = {vertices[49], vertices[50]}; // BR
454         outsetCoverage[2] = vertices[51];
455 
456         *domain = {vertices[52], vertices[53], vertices[54], vertices[55]};
457     }
458 };
459 
460 class DegenerateQuadSlide::Click : public ClickHandlerSlide::Click {
461 public:
Click(const SkRect & clamp,int index)462     Click(const SkRect& clamp, int index)
463             : fOuterRect(clamp)
464             , fIndex(index) {}
465 
doClick(SkPoint points[4])466     void doClick(SkPoint points[4]) {
467         if (fIndex >= 0) {
468             this->drag(&points[fIndex]);
469         } else {
470             for (int i = 0; i < 4; ++i) {
471                 this->drag(&points[i]);
472             }
473         }
474     }
475 
476 private:
477     SkRect fOuterRect;
478     int fIndex;
479 
drag(SkPoint * point)480     void drag(SkPoint* point) {
481         SkPoint delta = fCurr - fPrev;
482         *point += SkPoint::Make(delta.x() / kViewScale, delta.y() / kViewScale);
483         point->fX = std::min(fOuterRect.fRight, std::max(point->fX, fOuterRect.fLeft));
484         point->fY = std::min(fOuterRect.fBottom, std::max(point->fY, fOuterRect.fTop));
485     }
486 };
487 
onFindClickHandler(SkScalar x,SkScalar y,skui::ModifierKey)488 ClickHandlerSlide::Click* DegenerateQuadSlide::onFindClickHandler(SkScalar x, SkScalar y,
489                                                                   skui::ModifierKey) {
490     SkPoint inCTM = SkPoint::Make((x - kViewOffset) / kViewScale, (y - kViewOffset) / kViewScale);
491     for (int i = 0; i < 4; ++i) {
492         if ((fCorners[i] - inCTM).length() < 10.f / kViewScale) {
493             return new Click(fOuterRect, i);
494         }
495     }
496     return new Click(fOuterRect, -1);
497 }
498 
onClick(ClickHandlerSlide::Click * click)499 bool DegenerateQuadSlide::onClick(ClickHandlerSlide::Click* click) {
500     Click* myClick = (Click*) click;
501     myClick->doClick(fCorners);
502     return true;
503 }
504 
onChar(SkUnichar code)505 bool DegenerateQuadSlide::onChar(SkUnichar code) {
506         switch(code) {
507             case '1':
508                 fEdgeAA[0] = !fEdgeAA[0];
509                 return true;
510             case '2':
511                 fEdgeAA[1] = !fEdgeAA[1];
512                 return true;
513             case '3':
514                 fEdgeAA[2] = !fEdgeAA[2];
515                 return true;
516             case '4':
517                 fEdgeAA[3] = !fEdgeAA[3];
518                 return true;
519             case 'q':
520                 fCoverageMode = CoverageMode::kArea;
521                 return true;
522             case 'w':
523                 fCoverageMode = CoverageMode::kEdgeDistance;
524                 return true;
525             case 'e':
526                 fCoverageMode = CoverageMode::kGPUMesh;
527                 return true;
528         }
529         return false;
530 }
531 
532 DEF_SLIDE(return new DegenerateQuadSlide(SkRect::MakeWH(4.f, 4.f));)
533