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