/* * 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 "Sample.h" #include "SkAnimTimer.h" #include "SkCanvas.h" #include "SkColorFilter.h" #include "SkFont.h" #include "SkImage.h" #include "SkPath.h" #include "SkSurface.h" namespace skiagm { class ShapeRenderer : public SkRefCntBase { public: static constexpr SkScalar kTileWidth = 20.f; static constexpr SkScalar kTileHeight = 20.f; virtual ~ShapeRenderer() {} // 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() {} typedef ShapeRenderer INHERITED; }; 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 // GrShape 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) {} typedef ShapeRenderer INHERITED; }; 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; blit.setFilterQuality(scale > 1.f ? kNone_SkFilterQuality : kMedium_SkFilterQuality); if (debugMode) { // Makes anything that's > 1/255 alpha fully opaque and sets color to medium green. static constexpr SkScalar kFilter[] = { 0.f, 0.f, 0.f, 0.f, 16.f, 0.f, 0.f, 0.f, 0.f, 200.f, 0.f, 0.f, 0.f, 0.f, 16.f, 0.f, 0.f, 0.f, 255.f, 0.f }; blit.setColorFilter(SkColorFilter::MakeMatrixFilterRowMajor255(kFilter)); } canvas->scale(scale, scale); canvas->drawImageRect(fLastRendered, SkRect::MakeWH(kTileWidth, kTileHeight), &blit); } 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) { } typedef ShapeRenderer INHERITED; }; 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(const SkAnimTimer& timer) override { SkScalar t = timer.secs(); 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; } bool onQuery(Sample::Event* evt) override { if (Sample::TitleQ(*evt)) { Sample::TitleR(evt, "Thin-AA"); return true; } SkUnichar key; if (Sample::CharQ(*evt, &key)) { 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 = SkMaxScalar(0.1f, fStrokeWidth - 0.05f); return true; case '=': fStrokeWidth = SkMinScalar(1.f, fStrokeWidth + 0.05f); return true; } } return this->INHERITED::onQuery(evt); } 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); } typedef Sample INHERITED; }; ////////////////////////////////////////////////////////////////////////////// DEF_SAMPLE( return new ThinAASample; ) }