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