| /* | |
| * 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 "AnimTimer.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(SkColorFilters::MatrixRowMajor255(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 AnimTimer& 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; ) | |
| } |