• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "samplecode/Sample.h"
9 
10 #include "src/gpu/geometry/GrQuad.h"
11 #include "src/gpu/ops/QuadPerEdgeAA.h"
12 
13 #include "include/core/SkCanvas.h"
14 #include "include/core/SkPaint.h"
15 #include "include/effects/SkDashPathEffect.h"
16 #include "include/pathops/SkPathOps.h"
17 #include "include/private/SkTPin.h"
18 
19 using VertexSpec = skgpu::v1::QuadPerEdgeAA::VertexSpec;
20 using ColorType = skgpu::v1::QuadPerEdgeAA::ColorType;
21 using Subset = skgpu::v1::QuadPerEdgeAA::Subset;
22 using IndexBufferOption = skgpu::v1::QuadPerEdgeAA::IndexBufferOption;
23 
24 // 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)25 static void draw_extended_line(SkCanvas* canvas, const SkPaint paint,
26                               const SkPoint& p0, const SkPoint& p1) {
27     SkVector v = p1 - p0;
28     v.setLength(v.length() + 3.f);
29     canvas->drawLine(p1 - v, p0 + v, paint);
30 
31     // Draw normal vector too
32     SkPaint normalPaint = paint;
33     normalPaint.setPathEffect(nullptr);
34     normalPaint.setStrokeWidth(paint.getStrokeWidth() / 4.f);
35 
36     SkVector n = {v.fY, -v.fX};
37     n.setLength(.25f);
38     SkPoint m = (p0 + p1) * 0.5f;
39     canvas->drawLine(m, m + n, normalPaint);
40 }
41 
make_aa_line(const SkPoint & p0,const SkPoint & p1,bool aaOn,bool outset,SkPoint line[2])42 static void make_aa_line(const SkPoint& p0, const SkPoint& p1, bool aaOn,
43                          bool outset, SkPoint line[2]) {
44     SkVector n = {0.f, 0.f};
45     if (aaOn) {
46         SkVector v = p1 - p0;
47         n = outset ? SkVector::Make(v.fY, -v.fX) : SkVector::Make(-v.fY, v.fX);
48         n.setLength(0.5f);
49     }
50 
51     line[0] = p0 + n;
52     line[1] = p1 + n;
53 }
54 
55 // 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)56 static SkScalar signed_distance(const SkPoint& p, const SkPoint& l0, const SkPoint& l1) {
57     SkVector v = l1 - l0;
58     v.normalize();
59     SkVector n = {v.fY, -v.fX};
60     SkScalar c = -n.dot(l0);
61     return n.dot(p) + c;
62 }
63 
get_area_coverage(const bool edgeAA[4],const SkPoint corners[4],const SkPoint & point)64 static SkScalar get_area_coverage(const bool edgeAA[4], const SkPoint corners[4],
65                                   const SkPoint& point) {
66     SkPath shape;
67     shape.addPoly(corners, 4, true);
68     SkPath pixel;
69     pixel.addRect(SkRect::MakeXYWH(point.fX - 0.5f, point.fY - 0.5f, 1.f, 1.f));
70 
71     SkPath intersection;
72     if (!Op(shape, pixel, kIntersect_SkPathOp, &intersection) || intersection.isEmpty()) {
73         return 0.f;
74     }
75 
76     // Calculate area of the convex polygon
77     SkScalar area = 0.f;
78     for (int i = 0; i < intersection.countPoints(); ++i) {
79         SkPoint p0 = intersection.getPoint(i);
80         SkPoint p1 = intersection.getPoint((i + 1) % intersection.countPoints());
81         SkScalar det = p0.fX * p1.fY - p1.fX * p0.fY;
82         area += det;
83     }
84 
85     // Scale by 1/2, then take abs value (this area formula is signed based on point winding, but
86     // since it's convex, just make it positive).
87     area = SkScalarAbs(0.5f * area);
88 
89     // Now account for the edge AA. If the pixel center is outside of a non-AA edge, turn of its
90     // coverage. If the pixel only intersects non-AA edges, then set coverage to 1.
91     bool needsNonAA = false;
92     SkScalar edgeD[4];
93     for (int i = 0; i < 4; ++i) {
94         SkPoint e0 = corners[i];
95         SkPoint e1 = corners[(i + 1) % 4];
96         edgeD[i] = -signed_distance(point, e0, e1);
97         if (!edgeAA[i]) {
98             if (edgeD[i] < -1e-4f) {
99                 return 0.f; // Outside of non-AA line
100             }
101             needsNonAA = true;
102         }
103     }
104     // Otherwise inside the shape, so check if any AA edge exerts influence over nonAA
105     if (needsNonAA) {
106         for (int i = 0; i < 4; i++) {
107             if (edgeAA[i] && edgeD[i] < 0.5f) {
108                 needsNonAA = false;
109                 break;
110             }
111         }
112     }
113     return needsNonAA ? 1.f : area;
114 }
115 
116 // 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)117 static SkScalar get_edge_dist_coverage(const bool edgeAA[4], const SkPoint corners[4],
118                                        const SkPoint outsetLines[8], const SkPoint insetLines[8],
119                                        const SkPoint& point) {
120     bool flip = false;
121     // If the quad has been inverted, the original corners will not all be on the negative side of
122     // every outset line. When that happens, calculate coverage using the "inset" lines and flip
123     // the signed distance
124     for (int i = 0; i < 4; ++i) {
125         for (int j = 0; j < 4; ++j) {
126             SkScalar d = signed_distance(corners[i], outsetLines[j * 2], outsetLines[j * 2 + 1]);
127             if (d > 1e-4f) {
128                 flip = true;
129                 break;
130             }
131         }
132         if (flip) {
133             break;
134         }
135     }
136 
137     const SkPoint* lines = flip ? insetLines : outsetLines;
138 
139     SkScalar minCoverage = 1.f;
140     for (int i = 0; i < 4; ++i) {
141         // Multiply by negative 1 so that outside points have negative distances
142         SkScalar d = (flip ? 1 : -1) * signed_distance(point, lines[i * 2], lines[i * 2 + 1]);
143         if (!edgeAA[i] && d >= -1e-4f) {
144             d = 1.f;
145         }
146         if (d < minCoverage) {
147             minCoverage = d;
148             if (minCoverage < 0.f) {
149                 break; // Outside the shape
150             }
151         }
152     }
153     return minCoverage < 0.f ? 0.f : minCoverage;
154 }
155 
inside_triangle(const SkPoint & point,const SkPoint & t0,const SkPoint & t1,const SkPoint & t2,SkScalar bary[3])156 static bool inside_triangle(const SkPoint& point, const SkPoint& t0, const SkPoint& t1,
157                             const SkPoint& t2, SkScalar bary[3]) {
158     // Check sign of t0 to (t1,t2). If it is positive, that means the normals point into the
159     // triangle otherwise the normals point outside the triangle so update edge distances as
160     // necessary
161     bool flip = signed_distance(t0, t1, t2) < 0.f;
162 
163     SkScalar d0 = (flip ? -1 : 1) * signed_distance(point, t0, t1);
164     SkScalar d1 = (flip ? -1 : 1) * signed_distance(point, t1, t2);
165     SkScalar d2 = (flip ? -1 : 1) * signed_distance(point, t2, t0);
166     // Be a little forgiving
167     if (d0 < -1e-4f || d1 < -1e-4f || d2 < -1e-4f) {
168         return false;
169     }
170 
171     // Inside, so calculate barycentric coords from the sideline distances
172     SkScalar d01 = (t0 - t1).length();
173     SkScalar d12 = (t1 - t2).length();
174     SkScalar d20 = (t2 - t0).length();
175 
176     if (SkScalarNearlyZero(d12) || SkScalarNearlyZero(d20) || SkScalarNearlyZero(d01)) {
177         // Empty degenerate triangle
178         return false;
179     }
180 
181     // Coordinates for a vertex use distances to the opposite edge
182     bary[0] = d1 * d12;
183     bary[1] = d2 * d20;
184     bary[2] = d0 * d01;
185     // And normalize
186     SkScalar sum = bary[0] + bary[1] + bary[2];
187     bary[0] /= sum;
188     bary[1] /= sum;
189     bary[2] /= sum;
190 
191     return true;
192 }
193 
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)194 static SkScalar get_framed_coverage(const SkPoint outer[4], const SkScalar outerCoverages[4],
195                                     const SkPoint inner[4], const SkScalar innerCoverages[4],
196                                     const SkRect& geomDomain, const SkPoint& point) {
197     // Triangles are ordered clock wise. Indices >= 4 refer to inner[i - 4]. Otherwise its outer[i].
198     static const int kFrameTris[] = {
199         0, 1, 4,   4, 1, 5,
200         1, 2, 5,   5, 2, 6,
201         2, 3, 6,   6, 3, 7,
202         3, 0, 7,   7, 0, 4,
203         4, 5, 7,   7, 5, 6
204     };
205     static const int kNumTris = 10;
206 
207     SkScalar bary[3];
208     for (int i = 0; i < kNumTris; ++i) {
209         int i0 = kFrameTris[i * 3];
210         int i1 = kFrameTris[i * 3 + 1];
211         int i2 = kFrameTris[i * 3 + 2];
212 
213         SkPoint t0 = i0 >= 4 ? inner[i0 - 4] : outer[i0];
214         SkPoint t1 = i1 >= 4 ? inner[i1 - 4] : outer[i1];
215         SkPoint t2 = i2 >= 4 ? inner[i2 - 4] : outer[i2];
216         if (inside_triangle(point, t0, t1, t2, bary)) {
217             // Calculate coverage by barycentric interpolation of coverages
218             SkScalar c0 = i0 >= 4 ? innerCoverages[i0 - 4] : outerCoverages[i0];
219             SkScalar c1 = i1 >= 4 ? innerCoverages[i1 - 4] : outerCoverages[i1];
220             SkScalar c2 = i2 >= 4 ? innerCoverages[i2 - 4] : outerCoverages[i2];
221 
222             SkScalar coverage = bary[0] * c0 + bary[1] * c1 + bary[2] * c2;
223             if (coverage < 0.5f) {
224                 // Check distances to domain
225                 SkScalar l = SkTPin(point.fX - geomDomain.fLeft, 0.f, 1.f);
226                 SkScalar t = SkTPin(point.fY - geomDomain.fTop, 0.f, 1.f);
227                 SkScalar r = SkTPin(geomDomain.fRight - point.fX, 0.f, 1.f);
228                 SkScalar b = SkTPin(geomDomain.fBottom - point.fY, 0.f, 1.f);
229                 coverage = std::min(coverage, l * t * r * b);
230             }
231             return coverage;
232         }
233     }
234     // Not inside any triangle
235     return 0.f;
236 }
237 
238 static constexpr SkScalar kViewScale = 100.f;
239 static constexpr SkScalar kViewOffset = 200.f;
240 
241 class DegenerateQuadSample : public Sample {
242 public:
DegenerateQuadSample(const SkRect & rect)243     DegenerateQuadSample(const SkRect& rect)
244             : fOuterRect(rect)
245             , fCoverageMode(CoverageMode::kArea) {
246         fOuterRect.toQuad(fCorners);
247         for (int i = 0; i < 4; ++i) {
248             fEdgeAA[i] = true;
249         }
250     }
251 
onDrawContent(SkCanvas * canvas)252     void onDrawContent(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     Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) override;
390     bool onClick(Sample::Click*) override;
391     bool onChar(SkUnichar) override;
name()392     SkString name() override { return SkString("DegenerateQuad"); }
393 
394 private:
395     class Click;
396 
397     enum class CoverageMode {
398         kArea, kEdgeDistance, kGPUMesh
399     };
400 
401     const SkRect fOuterRect;
402     SkPoint fCorners[4]; // TL, TR, BR, BL
403     bool fEdgeAA[4]; // T, R, B, L
404     CoverageMode fCoverageMode;
405 
isValid() const406     bool isValid() const {
407         SkPath path;
408         path.addPoly(fCorners, 4, true);
409         return path.isConvex();
410     }
411 
getTessellatedPoints(SkPoint inset[4],SkScalar insetCoverage[4],SkPoint outset[4],SkScalar outsetCoverage[4],SkRect * domain) const412     void getTessellatedPoints(SkPoint inset[4], SkScalar insetCoverage[4], SkPoint outset[4],
413                               SkScalar outsetCoverage[4], SkRect* domain) const {
414         // Fixed vertex spec for extracting the picture frame geometry
415         static const VertexSpec kSpec =
416             {GrQuad::Type::kGeneral, ColorType::kNone,
417              GrQuad::Type::kAxisAligned, false, Subset::kNo,
418              GrAAType::kCoverage, false, IndexBufferOption::kPictureFramed};
419         static const GrQuad kIgnored(SkRect::MakeEmpty());
420 
421         GrQuadAAFlags flags = GrQuadAAFlags::kNone;
422         flags |= fEdgeAA[0] ? GrQuadAAFlags::kTop : GrQuadAAFlags::kNone;
423         flags |= fEdgeAA[1] ? GrQuadAAFlags::kRight : GrQuadAAFlags::kNone;
424         flags |= fEdgeAA[2] ? GrQuadAAFlags::kBottom : GrQuadAAFlags::kNone;
425         flags |= fEdgeAA[3] ? GrQuadAAFlags::kLeft : GrQuadAAFlags::kNone;
426 
427         GrQuad quad = GrQuad::MakeFromSkQuad(fCorners, SkMatrix::I());
428 
429         float vertices[56]; // 2 quads, with x, y, coverage, and geometry domain (7 floats x 8 vert)
430         skgpu::v1::QuadPerEdgeAA::Tessellator tessellator(kSpec, (char*) vertices);
431         tessellator.append(&quad, nullptr, {1.f, 1.f, 1.f, 1.f},
432                            SkRect::MakeEmpty(), flags);
433 
434         // The first quad in vertices is the inset, then the outset, but they
435         // are ordered TL, BL, TR, BR so un-interleave coverage and re-arrange
436         inset[0] = {vertices[0], vertices[1]}; // TL
437         insetCoverage[0] = vertices[2];
438         inset[3] = {vertices[7], vertices[8]}; // BL
439         insetCoverage[3] = vertices[9];
440         inset[1] = {vertices[14], vertices[15]}; // TR
441         insetCoverage[1] = vertices[16];
442         inset[2] = {vertices[21], vertices[22]}; // BR
443         insetCoverage[2] = vertices[23];
444 
445         outset[0] = {vertices[28], vertices[29]}; // TL
446         outsetCoverage[0] = vertices[30];
447         outset[3] = {vertices[35], vertices[36]}; // BL
448         outsetCoverage[3] = vertices[37];
449         outset[1] = {vertices[42], vertices[43]}; // TR
450         outsetCoverage[1] = vertices[44];
451         outset[2] = {vertices[49], vertices[50]}; // BR
452         outsetCoverage[2] = vertices[51];
453 
454         *domain = {vertices[52], vertices[53], vertices[54], vertices[55]};
455     }
456 
457     using INHERITED = Sample;
458 };
459 
460 class DegenerateQuadSample::Click : public Sample::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 Sample::Click* DegenerateQuadSample::onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) {
489     SkPoint inCTM = SkPoint::Make((x - kViewOffset) / kViewScale, (y - kViewOffset) / kViewScale);
490     for (int i = 0; i < 4; ++i) {
491         if ((fCorners[i] - inCTM).length() < 10.f / kViewScale) {
492             return new Click(fOuterRect, i);
493         }
494     }
495     return new Click(fOuterRect, -1);
496 }
497 
onClick(Sample::Click * click)498 bool DegenerateQuadSample::onClick(Sample::Click* click) {
499     Click* myClick = (Click*) click;
500     myClick->doClick(fCorners);
501     return true;
502 }
503 
onChar(SkUnichar code)504 bool DegenerateQuadSample::onChar(SkUnichar code) {
505         switch(code) {
506             case '1':
507                 fEdgeAA[0] = !fEdgeAA[0];
508                 return true;
509             case '2':
510                 fEdgeAA[1] = !fEdgeAA[1];
511                 return true;
512             case '3':
513                 fEdgeAA[2] = !fEdgeAA[2];
514                 return true;
515             case '4':
516                 fEdgeAA[3] = !fEdgeAA[3];
517                 return true;
518             case 'q':
519                 fCoverageMode = CoverageMode::kArea;
520                 return true;
521             case 'w':
522                 fCoverageMode = CoverageMode::kEdgeDistance;
523                 return true;
524             case 'e':
525                 fCoverageMode = CoverageMode::kGPUMesh;
526                 return true;
527         }
528         return false;
529 }
530 
531 DEF_SAMPLE(return new DegenerateQuadSample(SkRect::MakeWH(4.f, 4.f));)
532