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