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