551 lines
21 KiB
C++
551 lines
21 KiB
C++
/*
|
|
* Copyright 2019 Google Inc.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "samplecode/Sample.h"
|
|
|
|
#include "include/core/SkCanvas.h"
|
|
#include "include/core/SkColorFilter.h"
|
|
#include "include/core/SkFont.h"
|
|
#include "include/core/SkImage.h"
|
|
#include "include/core/SkPath.h"
|
|
#include "include/core/SkSurface.h"
|
|
|
|
namespace skiagm {
|
|
|
|
class ShapeRenderer : public SkRefCntBase {
|
|
public:
|
|
static constexpr SkScalar kTileWidth = 20.f;
|
|
static constexpr SkScalar kTileHeight = 20.f;
|
|
|
|
// Draw the shape, limited to kTileWidth x kTileHeight. It must apply the local subpixel (tx,
|
|
// ty) translation and rotation by angle. Prior to these transform adjustments, the SkCanvas
|
|
// will only have pixel aligned translations (these are separated to make super-sampling
|
|
// renderers easier).
|
|
virtual void draw(SkCanvas* canvas, SkPaint* paint,
|
|
SkScalar tx, SkScalar ty, SkScalar angle) = 0;
|
|
|
|
virtual SkString name() = 0;
|
|
|
|
virtual sk_sp<ShapeRenderer> toHairline() = 0;
|
|
|
|
void applyLocalTransform(SkCanvas* canvas, SkScalar tx, SkScalar ty, SkScalar angle) {
|
|
canvas->translate(tx, ty);
|
|
canvas->rotate(angle, kTileWidth / 2.f, kTileHeight / 2.f);
|
|
}
|
|
};
|
|
|
|
class RectRenderer : public ShapeRenderer {
|
|
public:
|
|
static sk_sp<ShapeRenderer> Make() {
|
|
return sk_sp<ShapeRenderer>(new RectRenderer());
|
|
}
|
|
|
|
SkString name() override { return SkString("rect"); }
|
|
|
|
sk_sp<ShapeRenderer> toHairline() override {
|
|
// Not really available but can't return nullptr
|
|
return Make();
|
|
}
|
|
|
|
void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
|
|
SkScalar width = paint->getStrokeWidth();
|
|
paint->setStyle(SkPaint::kFill_Style);
|
|
|
|
this->applyLocalTransform(canvas, tx, ty, angle);
|
|
canvas->drawRect(SkRect::MakeLTRB(kTileWidth / 2.f - width / 2.f, 2.f,
|
|
kTileWidth / 2.f + width / 2.f, kTileHeight - 2.f),
|
|
*paint);
|
|
}
|
|
|
|
private:
|
|
RectRenderer() {}
|
|
|
|
using INHERITED = ShapeRenderer;
|
|
};
|
|
|
|
class PathRenderer : public ShapeRenderer {
|
|
public:
|
|
static sk_sp<ShapeRenderer> MakeLine(bool hairline = false) {
|
|
return MakeCurve(0.f, hairline);
|
|
}
|
|
|
|
static sk_sp<ShapeRenderer> MakeLines(SkScalar depth, bool hairline = false) {
|
|
return MakeCurve(-depth, hairline);
|
|
}
|
|
|
|
static sk_sp<ShapeRenderer> MakeCurve(SkScalar depth, bool hairline = false) {
|
|
return sk_sp<ShapeRenderer>(new PathRenderer(depth, hairline));
|
|
}
|
|
|
|
SkString name() override {
|
|
SkString name;
|
|
if (fHairline) {
|
|
name.append("hairline");
|
|
if (fDepth > 0.f) {
|
|
name.appendf("-curve-%.2f", fDepth);
|
|
}
|
|
} else if (fDepth > 0.f) {
|
|
name.appendf("curve-%.2f", fDepth);
|
|
} else if (fDepth < 0.f) {
|
|
name.appendf("line-%.2f", -fDepth);
|
|
} else {
|
|
name.append("line");
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
sk_sp<ShapeRenderer> toHairline() override {
|
|
return sk_sp<ShapeRenderer>(new PathRenderer(fDepth, true));
|
|
}
|
|
|
|
void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
|
|
SkPath path;
|
|
path.moveTo(kTileWidth / 2.f, 2.f);
|
|
|
|
if (fDepth > 0.f) {
|
|
path.quadTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f,
|
|
kTileWidth / 2.f, kTileHeight - 2.f);
|
|
} else {
|
|
if (fDepth < 0.f) {
|
|
path.lineTo(kTileWidth / 2.f + fDepth, kTileHeight / 2.f);
|
|
}
|
|
path.lineTo(kTileWidth / 2.f, kTileHeight - 2.f);
|
|
}
|
|
|
|
if (fHairline) {
|
|
// Fake thinner hairlines by making it transparent, conflating coverage and alpha
|
|
SkColor4f color = paint->getColor4f();
|
|
SkScalar width = paint->getStrokeWidth();
|
|
if (width > 1.f) {
|
|
// Can't emulate width larger than a pixel
|
|
return;
|
|
}
|
|
paint->setColor4f({color.fR, color.fG, color.fB, width}, nullptr);
|
|
paint->setStrokeWidth(0.f);
|
|
}
|
|
|
|
// Adding round caps forces Ganesh to use the path renderer for lines instead of converting
|
|
// them to rectangles (which are already explicitly tested). However, when not curved, the
|
|
// GrStyledShape will still find a way to turn it into a rrect draw so it doesn't hit the
|
|
// path renderer in that condition.
|
|
paint->setStrokeCap(SkPaint::kRound_Cap);
|
|
paint->setStrokeJoin(SkPaint::kMiter_Join);
|
|
paint->setStyle(SkPaint::kStroke_Style);
|
|
|
|
this->applyLocalTransform(canvas, tx, ty, angle);
|
|
canvas->drawPath(path, *paint);
|
|
}
|
|
|
|
private:
|
|
SkScalar fDepth; // 0.f to make a line, otherwise outset of curve from end points
|
|
bool fHairline;
|
|
|
|
PathRenderer(SkScalar depth, bool hairline)
|
|
: fDepth(depth)
|
|
, fHairline(hairline) {}
|
|
|
|
using INHERITED = ShapeRenderer;
|
|
};
|
|
|
|
class OffscreenShapeRenderer : public ShapeRenderer {
|
|
public:
|
|
~OffscreenShapeRenderer() override = default;
|
|
|
|
static sk_sp<OffscreenShapeRenderer> Make(sk_sp<ShapeRenderer> renderer, int supersample,
|
|
bool forceRaster = false) {
|
|
SkASSERT(supersample > 0);
|
|
return sk_sp<OffscreenShapeRenderer>(new OffscreenShapeRenderer(std::move(renderer),
|
|
supersample, forceRaster));
|
|
}
|
|
|
|
SkString name() override {
|
|
SkString name = fRenderer->name();
|
|
if (fSupersampleFactor != 1) {
|
|
name.prependf("%dx-", fSupersampleFactor * fSupersampleFactor);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
sk_sp<ShapeRenderer> toHairline() override {
|
|
return Make(fRenderer->toHairline(), fSupersampleFactor, fForceRasterBackend);
|
|
}
|
|
|
|
void draw(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) override {
|
|
// Subpixel translation+angle are applied in the offscreen buffer
|
|
this->prepareBuffer(canvas, paint, tx, ty, angle);
|
|
this->redraw(canvas);
|
|
}
|
|
|
|
// Exposed so that it's easy to fill the offscreen buffer, then draw zooms/filters of it before
|
|
// drawing the original scale back into the canvas.
|
|
void prepareBuffer(SkCanvas* canvas, SkPaint* paint, SkScalar tx, SkScalar ty, SkScalar angle) {
|
|
auto info = SkImageInfo::Make(fSupersampleFactor * kTileWidth,
|
|
fSupersampleFactor * kTileHeight,
|
|
kRGBA_8888_SkColorType, kPremul_SkAlphaType);
|
|
auto surface = fForceRasterBackend ? SkSurface::MakeRaster(info)
|
|
: canvas->makeSurface(info);
|
|
|
|
surface->getCanvas()->save();
|
|
// Make fully transparent so it is easy to determine pixels that are touched by partial cov.
|
|
surface->getCanvas()->clear(SK_ColorTRANSPARENT);
|
|
// Set up scaling to fit supersampling amount
|
|
surface->getCanvas()->scale(fSupersampleFactor, fSupersampleFactor);
|
|
fRenderer->draw(surface->getCanvas(), paint, tx, ty, angle);
|
|
surface->getCanvas()->restore();
|
|
|
|
// Save image so it can be drawn zoomed in or to visualize touched pixels; only valid until
|
|
// the next call to draw()
|
|
fLastRendered = surface->makeImageSnapshot();
|
|
}
|
|
|
|
void redraw(SkCanvas* canvas, SkScalar scale = 1.f, bool debugMode = false) {
|
|
SkASSERT(fLastRendered);
|
|
// Use medium quality filter to get mipmaps when drawing smaller, or use nearest filtering
|
|
// when upscaling
|
|
SkPaint blit;
|
|
if (debugMode) {
|
|
// Makes anything that's > 1/255 alpha fully opaque and sets color to medium green.
|
|
static constexpr float kFilter[] = {
|
|
0.f, 0.f, 0.f, 0.f, 16.f/255,
|
|
0.f, 0.f, 0.f, 0.f, 200.f/255,
|
|
0.f, 0.f, 0.f, 0.f, 16.f/255,
|
|
0.f, 0.f, 0.f, 255.f, 0.f
|
|
};
|
|
|
|
blit.setColorFilter(SkColorFilters::Matrix(kFilter));
|
|
}
|
|
|
|
canvas->scale(scale, scale);
|
|
canvas->drawImageRect(fLastRendered.get(),
|
|
SkRect::MakeWH(kTileWidth, kTileHeight),
|
|
SkRect::MakeWH(kTileWidth, kTileHeight),
|
|
SkSamplingOptions(scale > 1.f ? kNone_SkFilterQuality
|
|
: kMedium_SkFilterQuality),
|
|
&blit,
|
|
SkCanvas::kFast_SrcRectConstraint);
|
|
}
|
|
|
|
private:
|
|
bool fForceRasterBackend;
|
|
sk_sp<SkImage> fLastRendered;
|
|
sk_sp<ShapeRenderer> fRenderer;
|
|
int fSupersampleFactor;
|
|
|
|
OffscreenShapeRenderer(sk_sp<ShapeRenderer> renderer, int supersample, bool forceRaster)
|
|
: fForceRasterBackend(forceRaster)
|
|
, fLastRendered(nullptr)
|
|
, fRenderer(std::move(renderer))
|
|
, fSupersampleFactor(supersample) { }
|
|
|
|
using INHERITED = ShapeRenderer;
|
|
};
|
|
|
|
class ThinAASample : public Sample {
|
|
public:
|
|
ThinAASample() {
|
|
this->setBGColor(0xFFFFFFFF);
|
|
}
|
|
|
|
protected:
|
|
void onOnceBeforeDraw() override {
|
|
// Setup all base renderers
|
|
fShapes.push_back(RectRenderer::Make());
|
|
fShapes.push_back(PathRenderer::MakeLine());
|
|
fShapes.push_back(PathRenderer::MakeLines(4.f)); // 2 segments
|
|
fShapes.push_back(PathRenderer::MakeCurve(2.f)); // Shallow curve
|
|
fShapes.push_back(PathRenderer::MakeCurve(8.f)); // Deep curve
|
|
|
|
for (int i = 0; i < fShapes.count(); ++i) {
|
|
fNative.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1));
|
|
fRaster.push_back(OffscreenShapeRenderer::Make(fShapes[i], 1, /* raster */ true));
|
|
fSS4.push_back(OffscreenShapeRenderer::Make(fShapes[i], 4)); // 4x4 -> 16 samples
|
|
fSS16.push_back(OffscreenShapeRenderer::Make(fShapes[i], 8)); // 8x8 -> 64 samples
|
|
|
|
fHairline.push_back(OffscreenShapeRenderer::Make(fRaster[i]->toHairline(), 1));
|
|
}
|
|
|
|
// Start it at something subpixel
|
|
fStrokeWidth = 0.5f;
|
|
|
|
fSubpixelX = 0.f;
|
|
fSubpixelY = 0.f;
|
|
fAngle = 0.f;
|
|
|
|
fCurrentStage = AnimStage::kMoveLeft;
|
|
fLastFrameTime = -1.f;
|
|
|
|
// Don't animate in the beginning
|
|
fAnimTranslate = false;
|
|
fAnimRotate = false;
|
|
}
|
|
|
|
void onDrawContent(SkCanvas* canvas) override {
|
|
// Move away from screen edge and add instructions
|
|
SkPaint text;
|
|
SkFont font(nullptr, 12);
|
|
canvas->translate(60.f, 20.f);
|
|
canvas->drawString("Each row features a rendering command under different AA strategies. "
|
|
"Native refers to the current backend of the viewer, e.g. OpenGL.",
|
|
0, 0, font, text);
|
|
|
|
canvas->drawString(SkStringPrintf("Stroke width: %.2f ('-' to decrease, '=' to increase)",
|
|
fStrokeWidth), 0, 24, font, text);
|
|
canvas->drawString(SkStringPrintf("Rotation: %.3f ('r' to animate, 'y' sets to 90, 'u' sets"
|
|
" to 0, 'space' adds 15)", fAngle), 0, 36, font, text);
|
|
canvas->drawString(SkStringPrintf("Translation: %.3f, %.3f ('t' to animate)",
|
|
fSubpixelX, fSubpixelY), 0, 48, font, text);
|
|
|
|
canvas->translate(0.f, 100.f);
|
|
|
|
// Draw with surface matching current viewer surface type
|
|
this->drawShapes(canvas, "Native", 0, fNative);
|
|
|
|
// Draw with forced raster backend so it's easy to compare side-by-side
|
|
this->drawShapes(canvas, "Raster", 1, fRaster);
|
|
|
|
// Draw paths as hairlines + alpha hack
|
|
this->drawShapes(canvas, "Hairline", 2, fHairline);
|
|
|
|
// Draw at 4x supersampling in bottom left
|
|
this->drawShapes(canvas, "SSx16", 3, fSS4);
|
|
|
|
// And lastly 16x supersampling in bottom right
|
|
this->drawShapes(canvas, "SSx64", 4, fSS16);
|
|
}
|
|
|
|
bool onAnimate(double nanos) override {
|
|
SkScalar t = 1e-9 * nanos;
|
|
SkScalar dt = fLastFrameTime < 0.f ? 0.f : t - fLastFrameTime;
|
|
fLastFrameTime = t;
|
|
|
|
if (!fAnimRotate && !fAnimTranslate) {
|
|
// Keep returning true so that the last frame time is tracked
|
|
fLastFrameTime = -1.f;
|
|
return false;
|
|
}
|
|
|
|
switch(fCurrentStage) {
|
|
case AnimStage::kMoveLeft:
|
|
fSubpixelX += 2.f * dt;
|
|
if (fSubpixelX >= 1.f) {
|
|
fSubpixelX = 1.f;
|
|
fCurrentStage = AnimStage::kMoveDown;
|
|
}
|
|
break;
|
|
case AnimStage::kMoveDown:
|
|
fSubpixelY += 2.f * dt;
|
|
if (fSubpixelY >= 1.f) {
|
|
fSubpixelY = 1.f;
|
|
fCurrentStage = AnimStage::kMoveRight;
|
|
}
|
|
break;
|
|
case AnimStage::kMoveRight:
|
|
fSubpixelX -= 2.f * dt;
|
|
if (fSubpixelX <= -1.f) {
|
|
fSubpixelX = -1.f;
|
|
fCurrentStage = AnimStage::kMoveUp;
|
|
}
|
|
break;
|
|
case AnimStage::kMoveUp:
|
|
fSubpixelY -= 2.f * dt;
|
|
if (fSubpixelY <= -1.f) {
|
|
fSubpixelY = -1.f;
|
|
fCurrentStage = fAnimRotate ? AnimStage::kRotate : AnimStage::kMoveLeft;
|
|
}
|
|
break;
|
|
case AnimStage::kRotate: {
|
|
SkScalar newAngle = fAngle + dt * 15.f;
|
|
bool completed = SkScalarMod(newAngle, 15.f) < SkScalarMod(fAngle, 15.f);
|
|
fAngle = SkScalarMod(newAngle, 360.f);
|
|
if (completed) {
|
|
// Make sure we're on a 15 degree boundary
|
|
fAngle = 15.f * SkScalarRoundToScalar(fAngle / 15.f);
|
|
if (fAnimTranslate) {
|
|
fCurrentStage = this->getTranslationStage();
|
|
}
|
|
}
|
|
} break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
SkString name() override { return SkString("Thin-AA"); }
|
|
|
|
bool onChar(SkUnichar key) override {
|
|
switch(key) {
|
|
case 't':
|
|
// Toggle translation animation.
|
|
fAnimTranslate = !fAnimTranslate;
|
|
if (!fAnimTranslate && fAnimRotate && fCurrentStage != AnimStage::kRotate) {
|
|
// Turned off an active translation so go to rotating
|
|
fCurrentStage = AnimStage::kRotate;
|
|
} else if (fAnimTranslate && !fAnimRotate &&
|
|
fCurrentStage == AnimStage::kRotate) {
|
|
// Turned on translation, rotation had been paused too, so reset the stage
|
|
fCurrentStage = this->getTranslationStage();
|
|
}
|
|
return true;
|
|
case 'r':
|
|
// Toggle rotation animation.
|
|
fAnimRotate = !fAnimRotate;
|
|
if (!fAnimRotate && fAnimTranslate && fCurrentStage == AnimStage::kRotate) {
|
|
// Turned off an active rotation so go back to translation
|
|
fCurrentStage = this->getTranslationStage();
|
|
} else if (fAnimRotate && !fAnimTranslate &&
|
|
fCurrentStage != AnimStage::kRotate) {
|
|
// Turned on rotation, translation had been paused too, so reset to rotate
|
|
fCurrentStage = AnimStage::kRotate;
|
|
}
|
|
return true;
|
|
case 'u': fAngle = 0.f; return true;
|
|
case 'y': fAngle = 90.f; return true;
|
|
case ' ': fAngle = SkScalarMod(fAngle + 15.f, 360.f); return true;
|
|
case '-': fStrokeWidth = std::max(0.1f, fStrokeWidth - 0.05f); return true;
|
|
case '=': fStrokeWidth = std::min(1.f, fStrokeWidth + 0.05f); return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
// Base renderers that get wrapped on the offscreen renderers so that they can be transformed
|
|
// for visualization, or supersampled.
|
|
SkTArray<sk_sp<ShapeRenderer>> fShapes;
|
|
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> fNative;
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> fRaster;
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> fHairline;
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> fSS4;
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> fSS16;
|
|
|
|
SkScalar fStrokeWidth;
|
|
|
|
// Animated properties to stress the AA algorithms
|
|
enum class AnimStage {
|
|
kMoveRight, kMoveDown, kMoveLeft, kMoveUp, kRotate
|
|
} fCurrentStage;
|
|
SkScalar fLastFrameTime;
|
|
bool fAnimRotate;
|
|
bool fAnimTranslate;
|
|
|
|
// Current frame's animation state
|
|
SkScalar fSubpixelX;
|
|
SkScalar fSubpixelY;
|
|
SkScalar fAngle;
|
|
|
|
AnimStage getTranslationStage() {
|
|
// For paused translations (i.e. fAnimTranslate toggled while translating), the current
|
|
// stage moves to kRotate, but when restarting the translation animation, we want to
|
|
// go back to where we were without losing any progress.
|
|
if (fSubpixelX > -1.f) {
|
|
if (fSubpixelX >= 1.f) {
|
|
// Can only be moving down on right edge, given our transition states
|
|
return AnimStage::kMoveDown;
|
|
} else if (fSubpixelY > 0.f) {
|
|
// Can only be moving right along top edge
|
|
return AnimStage::kMoveRight;
|
|
} else {
|
|
// Must be moving left along bottom edge
|
|
return AnimStage::kMoveLeft;
|
|
}
|
|
} else {
|
|
// Moving up along the left edge, or is at the very top so start moving left
|
|
return fSubpixelY > -1.f ? AnimStage::kMoveUp : AnimStage::kMoveLeft;
|
|
}
|
|
}
|
|
|
|
void drawShapes(SkCanvas* canvas, const char* name, int gridX,
|
|
SkTArray<sk_sp<OffscreenShapeRenderer>> shapes) {
|
|
SkAutoCanvasRestore autoRestore(canvas, /* save */ true);
|
|
|
|
for (int i = 0; i < shapes.count(); ++i) {
|
|
this->drawShape(canvas, name, gridX, shapes[i].get(), i == 0);
|
|
// drawShape positions the canvas properly for the next iteration
|
|
}
|
|
}
|
|
|
|
void drawShape(SkCanvas* canvas, const char* name, int gridX,
|
|
OffscreenShapeRenderer* shape, bool drawNameLabels) {
|
|
static constexpr SkScalar kZoomGridWidth = 8 * ShapeRenderer::kTileWidth + 8.f;
|
|
static constexpr SkRect kTile = SkRect::MakeWH(ShapeRenderer::kTileWidth,
|
|
ShapeRenderer::kTileHeight);
|
|
static constexpr SkRect kZoomTile = SkRect::MakeWH(8 * ShapeRenderer::kTileWidth,
|
|
8 * ShapeRenderer::kTileHeight);
|
|
|
|
// Labeling per shape and detailed labeling that isn't per-stroke
|
|
canvas->save();
|
|
SkPaint text;
|
|
SkFont font(nullptr, 12);
|
|
|
|
if (gridX == 0) {
|
|
SkString name = shape->name();
|
|
SkScalar centering = name.size() * 4.f; // ad-hoc
|
|
|
|
canvas->save();
|
|
canvas->translate(-10.f, 4 * ShapeRenderer::kTileHeight + centering);
|
|
canvas->rotate(-90.f);
|
|
canvas->drawString(shape->name(), 0.f, 0.f, font, text);
|
|
canvas->restore();
|
|
}
|
|
if (drawNameLabels) {
|
|
canvas->drawString(name, gridX * kZoomGridWidth, -10.f, font, text);
|
|
}
|
|
canvas->restore();
|
|
|
|
// Paints for outlines and actual shapes
|
|
SkPaint outline;
|
|
outline.setStyle(SkPaint::kStroke_Style);
|
|
SkPaint clear;
|
|
clear.setColor(SK_ColorWHITE);
|
|
|
|
SkPaint paint;
|
|
paint.setAntiAlias(true);
|
|
paint.setStrokeWidth(fStrokeWidth);
|
|
|
|
// Generate a saved image of the correct stroke width, but don't put it into the canvas
|
|
// yet since we want to draw the "original" size on top of the zoomed in version
|
|
shape->prepareBuffer(canvas, &paint, fSubpixelX, fSubpixelY, fAngle);
|
|
|
|
// Draw it at 8X zoom
|
|
SkScalar x = gridX * kZoomGridWidth;
|
|
|
|
canvas->save();
|
|
canvas->translate(x, 0.f);
|
|
canvas->drawRect(kZoomTile, outline);
|
|
shape->redraw(canvas, 8.0f);
|
|
canvas->restore();
|
|
|
|
// Draw the original
|
|
canvas->save();
|
|
canvas->translate(x + 4.f, 4.f);
|
|
canvas->drawRect(kTile, clear);
|
|
canvas->drawRect(kTile, outline);
|
|
shape->redraw(canvas, 1.f);
|
|
canvas->restore();
|
|
|
|
// Now redraw it into the coverage location (just to the right of the original scale)
|
|
canvas->save();
|
|
canvas->translate(x + ShapeRenderer::kTileWidth + 8.f, 4.f);
|
|
canvas->drawRect(kTile, clear);
|
|
canvas->drawRect(kTile, outline);
|
|
shape->redraw(canvas, 1.f, /* debug */ true);
|
|
canvas->restore();
|
|
|
|
// Lastly, shift the canvas translation down by 8 * kTH + padding for the next set of shapes
|
|
canvas->translate(0.f, 8.f * ShapeRenderer::kTileHeight + 20.f);
|
|
}
|
|
|
|
using INHERITED = Sample;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
DEF_SAMPLE( return new ThinAASample; )
|
|
|
|
} // namespace skiagm
|