blob: 534f0abcea301f81b9f5fe3bdcbebcdef7395302 [file] [log] [blame]
Michael Ludwigd20cf332019-01-30 14:38:59 -05001/*
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
Mike Kleinc0bd9f92019-04-23 12:05:21 -05008#include "samplecode/Sample.h"
Michael Ludwigd20cf332019-01-30 14:38:59 -05009
Mike Kleinc0bd9f92019-04-23 12:05:21 -050010#include "include/core/SkCanvas.h"
11#include "include/core/SkColorFilter.h"
12#include "include/core/SkFont.h"
13#include "include/core/SkImage.h"
14#include "include/core/SkPath.h"
15#include "include/core/SkSurface.h"
Michael Ludwigd20cf332019-01-30 14:38:59 -050016
17namespace skiagm {
18
19class ShapeRenderer : public SkRefCntBase {
20public:
21 static constexpr SkScalar kTileWidth = 20.f;
22 static constexpr SkScalar kTileHeight = 20.f;
23
Michael Ludwigd20cf332019-01-30 14:38:59 -050024 // Draw the shape, limited to kTileWidth x kTileHeight. It must apply the local subpixel (tx,
25 // ty) translation and rotation by angle. Prior to these transform adjustments, the SkCanvas
26 // will only have pixel aligned translations (these are separated to make super-sampling
27 // renderers easier).
28 virtual void draw(SkCanvas* canvas, SkPaint* paint,
29 SkScalar tx, SkScalar ty, SkScalar angle) = 0;
30
31 virtual SkString name() = 0;
32
33 virtual sk_sp<ShapeRenderer> toHairline() = 0;
34
35 void applyLocalTransform(SkCanvas* canvas, SkScalar tx, SkScalar ty, SkScalar angle) {
36 canvas->translate(tx, ty);
37 canvas->rotate(angle, kTileWidth / 2.f, kTileHeight / 2.f);
38 }
39};
40
41class RectRenderer : public ShapeRenderer {
42public:
43 static sk_sp<ShapeRenderer> Make() {
44 return sk_sp<ShapeRenderer>(new RectRenderer());
45 }
46
47 SkString name() override { return SkString("rect"); }
48
49 sk_sp<ShapeRenderer> toHairline() override {
50 // Not really available but can't return nullptr
51 return Make();
52 }
53
54 void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
55 SkScalar width = paint->getStrokeWidth();
56 paint->setStyle(SkPaint::kFill_Style);
57
58 this->applyLocalTransform(canvas, tx, ty, angle);
59 canvas->drawRect(SkRect::MakeLTRB(kTileWidth / 2.f - width / 2.f, 2.f,
60 kTileWidth / 2.f + width / 2.f, kTileHeight - 2.f),
61 *paint);
62 }
63
64private:
65 RectRenderer() {}
66
John Stiles7571f9e2020-09-02 22:42:33 -040067 using INHERITED = ShapeRenderer;
Michael Ludwigd20cf332019-01-30 14:38:59 -050068};
69
70class PathRenderer : public ShapeRenderer {
71public:
72 static sk_sp<ShapeRenderer> MakeLine(bool hairline = false) {
73 return MakeCurve(0.f, hairline);
74 }
75
76 static sk_sp<ShapeRenderer> MakeLines(SkScalar depth, bool hairline = false) {
77 return MakeCurve(-depth, hairline);
78 }
79
80 static sk_sp<ShapeRenderer> MakeCurve(SkScalar depth, bool hairline = false) {
81 return sk_sp<ShapeRenderer>(new PathRenderer(depth, hairline));
82 }
83
84 SkString name() override {
85 SkString name;
86 if (fHairline) {
87 name.append("hairline");
88 if (fDepth > 0.f) {
89 name.appendf("-curve-%.2f", fDepth);
90 }
91 } else if (fDepth > 0.f) {
92 name.appendf("curve-%.2f", fDepth);
93 } else if (fDepth < 0.f) {
94 name.appendf("line-%.2f", -fDepth);
95 } else {
96 name.append("line");
97 }
98
99 return name;
100 }
101
102 sk_sp<ShapeRenderer> toHairline() override {
103 return sk_sp<ShapeRenderer>(new PathRenderer(fDepth, true));
104 }
105
106 void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
107 SkPath path;
108 path.moveTo(kTileWidth / 2.f, 2.f);
109
110 if (fDepth > 0.f) {
111 path.quadTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f,
112 kTileWidth / 2.f, kTileHeight - 2.f);
113 } else {
114 if (fDepth < 0.f) {
115 path.lineTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f);
116 }
117 path.lineTo(kTileWidth / 2.f, kTileHeight - 2.f);
118 }
119
120 if (fHairline) {
121 // Fake thinner hairlines by making it transparent, conflating coverage and alpha
122 SkColor4f color = paint->getColor4f();
123 SkScalar width = paint->getStrokeWidth();
124 if (width > 1.f) {
125 // Can't emulate width larger than a pixel
126 return;
127 }
128 paint->setColor4f({color.fR, color.fG, color.fB, width}, nullptr);
129 paint->setStrokeWidth(0.f);
130 }
131
132 // Adding round caps forces Ganesh to use the path renderer for lines instead of converting
133 // them to rectangles (which are already explicitly tested). However, when not curved, the
Michael Ludwig2686d692020-04-17 20:21:37 +0000134 // GrStyledShape will still find a way to turn it into a rrect draw so it doesn't hit the
Michael Ludwigd20cf332019-01-30 14:38:59 -0500135 // path renderer in that condition.
136 paint->setStrokeCap(SkPaint::kRound_Cap);
137 paint->setStrokeJoin(SkPaint::kMiter_Join);
138 paint->setStyle(SkPaint::kStroke_Style);
139
140 this->applyLocalTransform(canvas, tx, ty, angle);
141 canvas->drawPath(path, *paint);
142 }
143
144private:
145 SkScalar fDepth; // 0.f to make a line, otherwise outset of curve from end points
146 bool fHairline;
147
148 PathRenderer(SkScalar depth, bool hairline)
149 : fDepth(depth)
150 , fHairline(hairline) {}
151
John Stiles7571f9e2020-09-02 22:42:33 -0400152 using INHERITED = ShapeRenderer;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500153};
154
155class OffscreenShapeRenderer : public ShapeRenderer {
156public:
157 ~OffscreenShapeRenderer() override = default;
158
159 static sk_sp<OffscreenShapeRenderer> Make(sk_sp<ShapeRenderer> renderer, int supersample,
160 bool forceRaster = false) {
161 SkASSERT(supersample > 0);
162 return sk_sp<OffscreenShapeRenderer>(new OffscreenShapeRenderer(std::move(renderer),
163 supersample, forceRaster));
164 }
165
166 SkString name() override {
167 SkString name = fRenderer->name();
168 if (fSupersampleFactor != 1) {
169 name.prependf("%dx-", fSupersampleFactor * fSupersampleFactor);
170 }
171 return name;
172 }
173
174 sk_sp<ShapeRenderer> toHairline() override {
175 return Make(fRenderer->toHairline(), fSupersampleFactor, fForceRasterBackend);
176 }
177
178 void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
179 // Subpixel translation+angle are applied in the offscreen buffer
180 this->prepareBuffer(canvas, paint, tx, ty, angle);
181 this->redraw(canvas);
182 }
183
184 // Exposed so that it's easy to fill the offscreen buffer, then draw zooms/filters of it before
185 // drawing the original scale back into the canvas.
186 void prepareBuffer(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) {
187 auto info = SkImageInfo::Make(fSupersampleFactor * kTileWidth,
188 fSupersampleFactor * kTileHeight,
189 kRGBA_8888_SkColorType, kPremul_SkAlphaType);
190 auto surface = fForceRasterBackend ? SkSurface::MakeRaster(info)
191 : canvas->makeSurface(info);
192
193 surface->getCanvas()->save();
194 // Make fully transparent so it is easy to determine pixels that are touched by partial cov.
195 surface->getCanvas()->clear(SK_ColorTRANSPARENT);
196 // Set up scaling to fit supersampling amount
197 surface->getCanvas()->scale(fSupersampleFactor, fSupersampleFactor);
198 fRenderer->draw(surface->getCanvas(), paint, tx, ty, angle);
199 surface->getCanvas()->restore();
200
201 // Save image so it can be drawn zoomed in or to visualize touched pixels; only valid until
202 // the next call to draw()
203 fLastRendered = surface->makeImageSnapshot();
204 }
205
206 void redraw(SkCanvas* canvas, SkScalar scale = 1.f, bool debugMode = false) {
207 SkASSERT(fLastRendered);
208 // Use medium quality filter to get mipmaps when drawing smaller, or use nearest filtering
209 // when upscaling
210 SkPaint blit;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500211 if (debugMode) {
212 // Makes anything that's > 1/255 alpha fully opaque and sets color to medium green.
Mike Reede869a1e2019-04-30 12:18:54 -0400213 static constexpr float kFilter[] = {
214 0.f, 0.f, 0.f, 0.f, 16.f/255,
215 0.f, 0.f, 0.f, 0.f, 200.f/255,
216 0.f, 0.f, 0.f, 0.f, 16.f/255,
Michael Ludwigd20cf332019-01-30 14:38:59 -0500217 0.f, 0.f, 0.f, 255.f, 0.f
218 };
219
Mike Reede869a1e2019-04-30 12:18:54 -0400220 blit.setColorFilter(SkColorFilters::Matrix(kFilter));
Michael Ludwigd20cf332019-01-30 14:38:59 -0500221 }
222
Mike Reed2bdf6ed2021-07-15 21:34:48 -0400223 auto sampling = scale > 1 ? SkSamplingOptions(SkFilterMode::kNearest)
224 : SkSamplingOptions(SkFilterMode::kLinear,
225 SkMipmapMode::kLinear);
226
Michael Ludwigd20cf332019-01-30 14:38:59 -0500227 canvas->scale(scale, scale);
Mike Reed839eef32020-12-23 11:18:24 -0500228 canvas->drawImageRect(fLastRendered.get(),
229 SkRect::MakeWH(kTileWidth, kTileHeight),
230 SkRect::MakeWH(kTileWidth, kTileHeight),
Mike Reed2bdf6ed2021-07-15 21:34:48 -0400231 sampling, &blit, SkCanvas::kFast_SrcRectConstraint);
Michael Ludwigd20cf332019-01-30 14:38:59 -0500232 }
233
234private:
235 bool fForceRasterBackend;
236 sk_sp<SkImage> fLastRendered;
237 sk_sp<ShapeRenderer> fRenderer;
238 int fSupersampleFactor;
239
240 OffscreenShapeRenderer(sk_sp<ShapeRenderer> renderer, int supersample, bool forceRaster)
241 : fForceRasterBackend(forceRaster)
242 , fLastRendered(nullptr)
243 , fRenderer(std::move(renderer))
244 , fSupersampleFactor(supersample) { }
245
John Stiles7571f9e2020-09-02 22:42:33 -0400246 using INHERITED = ShapeRenderer;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500247};
248
249class ThinAASample : public Sample {
250public:
251 ThinAASample() {
252 this->setBGColor(0xFFFFFFFF);
253 }
254
255protected:
256 void onOnceBeforeDraw() override {
257 // Setup all base renderers
258 fShapes.push_back(RectRenderer::Make());
259 fShapes.push_back(PathRenderer::MakeLine());
260 fShapes.push_back(PathRenderer::MakeLines(4.f)); // 2 segments
261 fShapes.push_back(PathRenderer::MakeCurve(2.f)); // Shallow curve
262 fShapes.push_back(PathRenderer::MakeCurve(8.f)); // Deep curve
263
264 for (int i = 0; i < fShapes.count(); ++i) {
265 fNative.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1));
266 fRaster.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1, /* raster */ true));
267 fSS4.push_back(OffscreenShapeRenderer::Make(fShapes[i], 4)); // 4x4 -> 16 samples
268 fSS16.push_back(OffscreenShapeRenderer::Make(fShapes[i], 8)); // 8x8 -> 64 samples
269
270 fHairline.push_back(OffscreenShapeRenderer::Make(fRaster[i]->toHairline(), 1));
271 }
272
273 // Start it at something subpixel
274 fStrokeWidth = 0.5f;
275
276 fSubpixelX = 0.f;
277 fSubpixelY = 0.f;
278 fAngle = 0.f;
279
280 fCurrentStage = AnimStage::kMoveLeft;
281 fLastFrameTime = -1.f;
282
283 // Don't animate in the beginning
284 fAnimTranslate = false;
285 fAnimRotate = false;
286 }
287
288 void onDrawContent(SkCanvas* canvas) override {
289 // Move away from screen edge and add instructions
290 SkPaint text;
291 SkFont font(nullptr, 12);
292 canvas->translate(60.f, 20.f);
293 canvas->drawString("Each row features a rendering command under different AA strategies. "
294 "Native refers to the current backend of the viewer, e.g. OpenGL.",
295 0, 0, font, text);
296
297 canvas->drawString(SkStringPrintf("Stroke width: %.2f ('-' to decrease, '=' to increase)",
298 fStrokeWidth), 0, 24, font, text);
299 canvas->drawString(SkStringPrintf("Rotation: %.3f ('r' to animate, 'y' sets to 90, 'u' sets"
300 " to 0, 'space' adds 15)", fAngle), 0, 36, font, text);
301 canvas->drawString(SkStringPrintf("Translation: %.3f, %.3f ('t' to animate)",
302 fSubpixelX, fSubpixelY), 0, 48, font, text);
303
304 canvas->translate(0.f, 100.f);
305
306 // Draw with surface matching current viewer surface type
307 this->drawShapes(canvas, "Native", 0, fNative);
308
309 // Draw with forced raster backend so it's easy to compare side-by-side
310 this->drawShapes(canvas, "Raster", 1, fRaster);
311
312 // Draw paths as hairlines + alpha hack
313 this->drawShapes(canvas, "Hairline", 2, fHairline);
314
315 // Draw at 4x supersampling in bottom left
316 this->drawShapes(canvas, "SSx16", 3, fSS4);
317
318 // And lastly 16x supersampling in bottom right
319 this->drawShapes(canvas, "SSx64", 4, fSS16);
320 }
321
Hal Canary41248072019-07-11 16:32:53 -0400322 bool onAnimate(double nanos) override {
323 SkScalar t = 1e-9 * nanos;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500324 SkScalar dt = fLastFrameTime < 0.f ? 0.f : t - fLastFrameTime;
325 fLastFrameTime = t;
326
327 if (!fAnimRotate && !fAnimTranslate) {
328 // Keep returning true so that the last frame time is tracked
329 fLastFrameTime = -1.f;
330 return false;
331 }
332
333 switch(fCurrentStage) {
334 case AnimStage::kMoveLeft:
335 fSubpixelX += 2.f * dt;
336 if (fSubpixelX >= 1.f) {
337 fSubpixelX = 1.f;
338 fCurrentStage = AnimStage::kMoveDown;
339 }
340 break;
341 case AnimStage::kMoveDown:
342 fSubpixelY += 2.f * dt;
343 if (fSubpixelY >= 1.f) {
344 fSubpixelY = 1.f;
345 fCurrentStage = AnimStage::kMoveRight;
346 }
347 break;
348 case AnimStage::kMoveRight:
349 fSubpixelX -= 2.f * dt;
350 if (fSubpixelX <= -1.f) {
351 fSubpixelX = -1.f;
352 fCurrentStage = AnimStage::kMoveUp;
353 }
354 break;
355 case AnimStage::kMoveUp:
356 fSubpixelY -= 2.f * dt;
357 if (fSubpixelY <= -1.f) {
358 fSubpixelY = -1.f;
359 fCurrentStage = fAnimRotate ? AnimStage::kRotate : AnimStage::kMoveLeft;
360 }
361 break;
362 case AnimStage::kRotate: {
363 SkScalar newAngle = fAngle + dt * 15.f;
364 bool completed = SkScalarMod(newAngle, 15.f) < SkScalarMod(fAngle, 15.f);
365 fAngle = SkScalarMod(newAngle, 360.f);
366 if (completed) {
367 // Make sure we're on a 15 degree boundary
368 fAngle = 15.f * SkScalarRoundToScalar(fAngle / 15.f);
369 if (fAnimTranslate) {
370 fCurrentStage = this->getTranslationStage();
371 }
372 }
373 } break;
374 }
375
376 return true;
377 }
378
Hal Canary8a027312019-07-03 10:55:44 -0400379 SkString name() override { return SkString("Thin-AA"); }
Michael Ludwigd20cf332019-01-30 14:38:59 -0500380
Hal Canary6cc65e12019-07-03 15:53:04 -0400381 bool onChar(SkUnichar key) override {
Michael Ludwigd20cf332019-01-30 14:38:59 -0500382 switch(key) {
383 case 't':
384 // Toggle translation animation.
385 fAnimTranslate = !fAnimTranslate;
386 if (!fAnimTranslate && fAnimRotate && fCurrentStage != AnimStage::kRotate) {
387 // Turned off an active translation so go to rotating
388 fCurrentStage = AnimStage::kRotate;
389 } else if (fAnimTranslate && !fAnimRotate &&
390 fCurrentStage == AnimStage::kRotate) {
391 // Turned on translation, rotation had been paused too, so reset the stage
392 fCurrentStage = this->getTranslationStage();
393 }
394 return true;
395 case 'r':
396 // Toggle rotation animation.
397 fAnimRotate = !fAnimRotate;
398 if (!fAnimRotate && fAnimTranslate && fCurrentStage == AnimStage::kRotate) {
399 // Turned off an active rotation so go back to translation
400 fCurrentStage = this->getTranslationStage();
401 } else if (fAnimRotate && !fAnimTranslate &&
402 fCurrentStage != AnimStage::kRotate) {
403 // Turned on rotation, translation had been paused too, so reset to rotate
404 fCurrentStage = AnimStage::kRotate;
405 }
406 return true;
407 case 'u': fAngle = 0.f; return true;
408 case 'y': fAngle = 90.f; return true;
409 case ' ': fAngle = SkScalarMod(fAngle + 15.f, 360.f); return true;
Brian Osman116b33e2020-02-05 13:34:09 -0500410 case '-': fStrokeWidth = std::max(0.1f, fStrokeWidth - 0.05f); return true;
411 case '=': fStrokeWidth = std::min(1.f, fStrokeWidth + 0.05f); return true;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500412 }
Hal Canary6cc65e12019-07-03 15:53:04 -0400413 return false;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500414 }
415
416private:
417 // Base renderers that get wrapped on the offscreen renderers so that they can be transformed
418 // for visualization, or supersampled.
419 SkTArray<sk_sp<ShapeRenderer>> fShapes;
420
421 SkTArray<sk_sp<OffscreenShapeRenderer>> fNative;
422 SkTArray<sk_sp<OffscreenShapeRenderer>> fRaster;
423 SkTArray<sk_sp<OffscreenShapeRenderer>> fHairline;
424 SkTArray<sk_sp<OffscreenShapeRenderer>> fSS4;
425 SkTArray<sk_sp<OffscreenShapeRenderer>> fSS16;
426
427 SkScalar fStrokeWidth;
428
429 // Animated properties to stress the AA algorithms
430 enum class AnimStage {
431 kMoveRight, kMoveDown, kMoveLeft, kMoveUp, kRotate
432 } fCurrentStage;
433 SkScalar fLastFrameTime;
434 bool fAnimRotate;
435 bool fAnimTranslate;
436
437 // Current frame's animation state
438 SkScalar fSubpixelX;
439 SkScalar fSubpixelY;
440 SkScalar fAngle;
441
442 AnimStage getTranslationStage() {
443 // For paused translations (i.e. fAnimTranslate toggled while translating), the current
444 // stage moves to kRotate, but when restarting the translation animation, we want to
445 // go back to where we were without losing any progress.
446 if (fSubpixelX > -1.f) {
447 if (fSubpixelX >= 1.f) {
448 // Can only be moving down on right edge, given our transition states
449 return AnimStage::kMoveDown;
450 } else if (fSubpixelY > 0.f) {
451 // Can only be moving right along top edge
452 return AnimStage::kMoveRight;
453 } else {
454 // Must be moving left along bottom edge
455 return AnimStage::kMoveLeft;
456 }
457 } else {
458 // Moving up along the left edge, or is at the very top so start moving left
459 return fSubpixelY > -1.f ? AnimStage::kMoveUp : AnimStage::kMoveLeft;
460 }
461 }
462
463 void drawShapes(SkCanvas* canvas, const char* name, int gridX,
464 SkTArray<sk_sp<OffscreenShapeRenderer>> shapes) {
465 SkAutoCanvasRestore autoRestore(canvas, /* save */ true);
466
467 for (int i = 0; i < shapes.count(); ++i) {
468 this->drawShape(canvas, name, gridX, shapes[i].get(), i == 0);
469 // drawShape positions the canvas properly for the next iteration
470 }
471 }
472
473 void drawShape(SkCanvas* canvas, const char* name, int gridX,
474 OffscreenShapeRenderer* shape, bool drawNameLabels) {
475 static constexpr SkScalar kZoomGridWidth = 8 * ShapeRenderer::kTileWidth + 8.f;
476 static constexpr SkRect kTile = SkRect::MakeWH(ShapeRenderer::kTileWidth,
477 ShapeRenderer::kTileHeight);
478 static constexpr SkRect kZoomTile = SkRect::MakeWH(8 * ShapeRenderer::kTileWidth,
479 8 * ShapeRenderer::kTileHeight);
480
481 // Labeling per shape and detailed labeling that isn't per-stroke
482 canvas->save();
483 SkPaint text;
484 SkFont font(nullptr, 12);
485
486 if (gridX == 0) {
487 SkString name = shape->name();
488 SkScalar centering = name.size() * 4.f; // ad-hoc
489
490 canvas->save();
491 canvas->translate(-10.f, 4 * ShapeRenderer::kTileHeight + centering);
492 canvas->rotate(-90.f);
493 canvas->drawString(shape->name(), 0.f, 0.f, font, text);
494 canvas->restore();
495 }
496 if (drawNameLabels) {
497 canvas->drawString(name, gridX * kZoomGridWidth, -10.f, font, text);
498 }
499 canvas->restore();
500
501 // Paints for outlines and actual shapes
502 SkPaint outline;
503 outline.setStyle(SkPaint::kStroke_Style);
504 SkPaint clear;
505 clear.setColor(SK_ColorWHITE);
506
507 SkPaint paint;
508 paint.setAntiAlias(true);
509 paint.setStrokeWidth(fStrokeWidth);
510
511 // Generate a saved image of the correct stroke width, but don't put it into the canvas
512 // yet since we want to draw the "original" size on top of the zoomed in version
513 shape->prepareBuffer(canvas, &paint, fSubpixelX, fSubpixelY, fAngle);
514
515 // Draw it at 8X zoom
516 SkScalar x = gridX * kZoomGridWidth;
517
518 canvas->save();
519 canvas->translate(x, 0.f);
520 canvas->drawRect(kZoomTile, outline);
521 shape->redraw(canvas, 8.0f);
522 canvas->restore();
523
524 // Draw the original
525 canvas->save();
526 canvas->translate(x + 4.f, 4.f);
527 canvas->drawRect(kTile, clear);
528 canvas->drawRect(kTile, outline);
529 shape->redraw(canvas, 1.f);
530 canvas->restore();
531
532 // Now redraw it into the coverage location (just to the right of the original scale)
533 canvas->save();
534 canvas->translate(x + ShapeRenderer::kTileWidth + 8.f, 4.f);
535 canvas->drawRect(kTile, clear);
536 canvas->drawRect(kTile, outline);
537 shape->redraw(canvas, 1.f, /* debug */ true);
538 canvas->restore();
539
540 // Lastly, shift the canvas translation down by 8 * kTH + padding for the next set of shapes
541 canvas->translate(0.f, 8.f * ShapeRenderer::kTileHeight + 20.f);
542 }
543
John Stiles7571f9e2020-09-02 22:42:33 -0400544 using INHERITED = Sample;
Michael Ludwigd20cf332019-01-30 14:38:59 -0500545};
546
547//////////////////////////////////////////////////////////////////////////////
548
549DEF_SAMPLE( return new ThinAASample; )
550
John Stilesa6841be2020-08-06 14:11:56 -0400551} // namespace skiagm