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