/* * Copyright 2011 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "gm/gm.h" #include "include/core/SkCanvas.h" #include "include/core/SkClipOp.h" #include "include/core/SkColor.h" #include "include/core/SkColorFilter.h" #include "include/core/SkFont.h" #include "include/core/SkFontTypes.h" #include "include/core/SkPaint.h" #include "include/core/SkPathBuilder.h" #include "include/core/SkRect.h" #include "include/core/SkScalar.h" #include "include/core/SkSize.h" #include "include/core/SkString.h" #include "include/core/SkTypeface.h" #include "include/core/SkTypes.h" #include "include/effects/SkGradientShader.h" #include "tools/Resources.h" #include "tools/ToolUtils.h" #include namespace skiagm { constexpr SkColor gPathColor = SK_ColorBLACK; constexpr SkColor gClipAColor = SK_ColorBLUE; constexpr SkColor gClipBColor = SK_ColorRED; class ComplexClipGM : public GM { public: ComplexClipGM(bool aaclip, bool saveLayer, bool invertDraw) : fDoAAClip(aaclip) , fDoSaveLayer(saveLayer) , fInvertDraw(invertDraw) { this->setBGColor(0xFFDEDFDE); } protected: SkString onShortName() override { SkString str; str.printf("complexclip_%s%s%s", fDoAAClip ? "aa" : "bw", fDoSaveLayer ? "_layer" : "", fInvertDraw ? "_invert" : ""); return str; } SkISize onISize() override { return SkISize::Make(388, 780); } void onDraw(SkCanvas* canvas) override { SkPath path = SkPathBuilder() .moveTo(0, 50) .quadTo(0, 0, 50, 0) .lineTo(175, 0) .quadTo(200, 0, 200, 25) .lineTo(200, 150) .quadTo(200, 200, 150, 200) .lineTo(0, 200) .close() .moveTo(50, 50) .lineTo(150, 50) .lineTo(150, 125) .quadTo(150, 150, 125, 150) .lineTo(50, 150) .close() .detach(); if (fInvertDraw) { path.setFillType(SkPathFillType::kInverseEvenOdd); } else { path.setFillType(SkPathFillType::kEvenOdd); } SkPaint pathPaint; pathPaint.setAntiAlias(true); pathPaint.setColor(gPathColor); SkPath clipA = SkPath::Polygon({{10, 20}, {165, 22}, {70, 105}, {165, 177}, {-5, 180}}, true); SkPath clipB = SkPath::Polygon({{40, 10}, {190, 15}, {195, 190}, {40, 185}, {155, 100}}, true); SkFont font(ToolUtils::create_portable_typeface(), 20); constexpr struct { SkClipOp fOp; const char* fName; } gOps[] = { //extra spaces in names for measureText {SkClipOp::kIntersect, "Isect "}, {SkClipOp::kDifference, "Diff " }, }; canvas->translate(20, 20); canvas->scale(3 * SK_Scalar1 / 4, 3 * SK_Scalar1 / 4); if (fDoSaveLayer) { // We want the layer to appear symmetric relative to actual // device boundaries so we need to "undo" the effect of the // scale and translate SkRect bounds = SkRect::MakeLTRB( 4.0f/3.0f * -20, 4.0f/3.0f * -20, 4.0f/3.0f * (this->getISize().fWidth - 20), 4.0f/3.0f * (this->getISize().fHeight - 20)); bounds.inset(100, 100); SkPaint boundPaint; boundPaint.setColor(SK_ColorRED); boundPaint.setStyle(SkPaint::kStroke_Style); canvas->drawRect(bounds, boundPaint); canvas->clipRect(bounds); canvas->saveLayer(&bounds, nullptr); } for (int invBits = 0; invBits < 4; ++invBits) { canvas->save(); for (size_t op = 0; op < SK_ARRAY_COUNT(gOps); ++op) { this->drawHairlines(canvas, path, clipA, clipB); bool doInvA = SkToBool(invBits & 1); bool doInvB = SkToBool(invBits & 2); canvas->save(); // set clip clipA.setFillType(doInvA ? SkPathFillType::kInverseEvenOdd : SkPathFillType::kEvenOdd); clipB.setFillType(doInvB ? SkPathFillType::kInverseEvenOdd : SkPathFillType::kEvenOdd); canvas->clipPath(clipA, fDoAAClip); canvas->clipPath(clipB, gOps[op].fOp, fDoAAClip); // In the inverse case we need to prevent the draw from covering the whole // canvas. if (fInvertDraw) { SkRect rectClip = clipA.getBounds(); rectClip.join(path.getBounds()); rectClip.join(path.getBounds()); rectClip.outset(5, 5); canvas->clipRect(rectClip); } // draw path clipped canvas->drawPath(path, pathPaint); canvas->restore(); SkPaint paint; SkScalar txtX = 45; paint.setColor(gClipAColor); const char* aTxt = doInvA ? "InvA " : "A "; canvas->drawSimpleText(aTxt, strlen(aTxt), SkTextEncoding::kUTF8, txtX, 220, font, paint); txtX += font.measureText(aTxt, strlen(aTxt), SkTextEncoding::kUTF8); paint.setColor(SK_ColorBLACK); canvas->drawSimpleText(gOps[op].fName, strlen(gOps[op].fName), SkTextEncoding::kUTF8, txtX, 220, font, paint); txtX += font.measureText(gOps[op].fName, strlen(gOps[op].fName), SkTextEncoding::kUTF8); paint.setColor(gClipBColor); const char* bTxt = doInvB ? "InvB " : "B "; canvas->drawSimpleText(bTxt, strlen(bTxt), SkTextEncoding::kUTF8, txtX, 220, font, paint); canvas->translate(250,0); } canvas->restore(); canvas->translate(0, 250); } if (fDoSaveLayer) { canvas->restore(); } } private: void drawHairlines(SkCanvas* canvas, const SkPath& path, const SkPath& clipA, const SkPath& clipB) { SkPaint paint; paint.setAntiAlias(true); paint.setStyle(SkPaint::kStroke_Style); const SkAlpha fade = 0x33; // draw path in hairline paint.setColor(gPathColor); paint.setAlpha(fade); canvas->drawPath(path, paint); // draw clips in hair line paint.setColor(gClipAColor); paint.setAlpha(fade); canvas->drawPath(clipA, paint); paint.setColor(gClipBColor); paint.setAlpha(fade); canvas->drawPath(clipB, paint); } bool fDoAAClip; bool fDoSaveLayer; bool fInvertDraw; using INHERITED = GM; }; ////////////////////////////////////////////////////////////////////////////// DEF_GM(return new ComplexClipGM(false, false, false);) DEF_GM(return new ComplexClipGM(false, false, true);) DEF_GM(return new ComplexClipGM(false, true, false);) DEF_GM(return new ComplexClipGM(false, true, true);) DEF_GM(return new ComplexClipGM(true, false, false);) DEF_GM(return new ComplexClipGM(true, false, true);) DEF_GM(return new ComplexClipGM(true, true, false);) DEF_GM(return new ComplexClipGM(true, true, true);) } // namespace skiagm DEF_SIMPLE_GM(clip_shader, canvas, 840, 650) { auto img = GetResourceAsImage("images/yellow_rose.png"); auto sh = img->makeShader(SkSamplingOptions()); SkRect r = SkRect::MakeIWH(img->width(), img->height()); SkPaint p; canvas->translate(10, 10); canvas->drawImage(img, 0, 0); canvas->save(); canvas->translate(img->width() + 10, 0); canvas->clipShader(sh, SkClipOp::kIntersect); p.setColor(SK_ColorRED); canvas->drawRect(r, p); canvas->restore(); canvas->save(); canvas->translate(0, img->height() + 10); canvas->clipShader(sh, SkClipOp::kDifference); p.setColor(SK_ColorGREEN); canvas->drawRect(r, p); canvas->restore(); canvas->save(); canvas->translate(img->width() + 10, img->height() + 10); canvas->clipShader(sh, SkClipOp::kIntersect); canvas->save(); SkMatrix lm = SkMatrix::Scale(1.0f/5, 1.0f/5); canvas->clipShader(img->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions(), lm)); canvas->drawImage(img, 0, 0); canvas->restore(); canvas->restore(); } DEF_SIMPLE_GM(clip_shader_layer, canvas, 430, 320) { auto img = GetResourceAsImage("images/yellow_rose.png"); auto sh = img->makeShader(SkSamplingOptions()); SkRect r = SkRect::MakeIWH(img->width(), img->height()); canvas->translate(10, 10); // now add the cool clip canvas->clipRect(r); canvas->clipShader(sh); // now draw a layer with the same image, and watch it get restored w/ the clip canvas->saveLayer(&r, nullptr); canvas->drawColor(0xFFFF0000); canvas->restore(); } DEF_SIMPLE_GM(clip_shader_nested, canvas, 256, 256) { float w = 64.f; float h = 64.f; const SkColor gradColors[] = {SK_ColorBLACK, SkColorSetARGB(128, 128, 128, 128)}; auto s = SkGradientShader::MakeRadial({0.5f * w, 0.5f * h}, 0.1f * w, gradColors, nullptr, 2, SkTileMode::kRepeat, 0, nullptr); SkPaint p; // A large black rect affected by two gradient clips canvas->save(); canvas->clipShader(s); canvas->scale(2.f, 2.f); canvas->clipShader(s); canvas->drawRect(SkRect::MakeWH(w, h), p); canvas->restore(); canvas->translate(0.f, 2.f * h); // A small red rect, with no clipping canvas->save(); p.setColor(SK_ColorRED); canvas->drawRect(SkRect::MakeWH(w, h), p); canvas->restore(); } namespace { // Where is canvas->concat(persp) called relative to the clipShader calls. enum ConcatPerspective { kConcatBeforeClips, kConcatAfterClips, kConcatBetweenClips }; // Order in which clipShader(image) and clipShader(gradient) are specified; only meaningful // when CanvasPerspective is kConcatBetweenClips. enum ClipOrder { kClipImageFirst, kClipGradientFirst, kDoesntMatter = kClipImageFirst }; // Which shaders have perspective applied as a local matrix. enum LocalMatrix { kNoLocalMat, kImageWithLocalMat, kGradientWithLocalMat, kBothWithLocalMat }; struct Config { ConcatPerspective fConcat; ClipOrder fOrder; LocalMatrix fLM; }; static void draw_banner(SkCanvas* canvas, Config config) { SkString banner; banner.append("Persp: "); if (config.fConcat == kConcatBeforeClips || config.fLM == kBothWithLocalMat) { banner.append("Both Clips"); } else { SkASSERT((config.fConcat == kConcatBetweenClips && config.fLM == kNoLocalMat) || (config.fConcat == kConcatAfterClips && (config.fLM == kImageWithLocalMat || config.fLM == kGradientWithLocalMat))); if ((config.fConcat == kConcatBetweenClips && config.fOrder == kClipImageFirst) || config.fLM == kGradientWithLocalMat) { banner.append("Gradient"); } else { SkASSERT(config.fOrder == kClipGradientFirst || config.fLM == kImageWithLocalMat); banner.append("Image"); } } if (config.fLM != kNoLocalMat) { banner.append(" (w/ LM, should equal top row)"); } static const SkFont kFont(ToolUtils::create_portable_typeface(), 12); canvas->drawString(banner.c_str(), 20.f, -30.f, kFont, SkPaint()); }; } // namespace DEF_SIMPLE_GM(clip_shader_persp, canvas, 1370, 1030) { // Each draw has a clipShader(image-shader), a clipShader(gradient-shader), a concat(persp-mat), // and each shader may or may not be wrapped with a perspective local matrix. // Pairs of configs that should match in appearance where first config doesn't use a local // matrix (top row of GM) and the second does (bottom row of GM). Config matches[][2] = { // Everything has perspective {{kConcatBeforeClips, kDoesntMatter, kNoLocalMat}, {kConcatAfterClips, kDoesntMatter, kBothWithLocalMat}}, // Image shader has perspective {{kConcatBetweenClips, kClipGradientFirst, kNoLocalMat}, {kConcatAfterClips, kDoesntMatter, kImageWithLocalMat}}, // Gradient shader has perspective {{kConcatBetweenClips, kClipImageFirst, kNoLocalMat}, {kConcatAfterClips, kDoesntMatter, kGradientWithLocalMat}} }; // The image that is drawn auto img = GetResourceAsImage("images/yellow_rose.png"); // Scale factor always applied to the image shader so that it tiles SkMatrix scale = SkMatrix::Scale(1.f / 4.f, 1.f / 4.f); // The perspective matrix applied wherever needed SkPoint src[4]; SkRect::Make(img->dimensions()).toQuad(src); SkPoint dst[4] = {{0, 80.f}, {img->width() + 28.f, -100.f}, {img->width() - 28.f, img->height() + 100.f}, {0.f, img->height() - 80.f}}; SkMatrix persp; SkAssertResult(persp.setPolyToPoly(src, dst, 4)); SkMatrix perspScale = SkMatrix::Concat(persp, scale); auto drawConfig = [&](Config config) { canvas->save(); draw_banner(canvas, config); // Make clipShaders (possibly with local matrices) bool gradLM = config.fLM == kGradientWithLocalMat || config.fLM == kBothWithLocalMat; const SkColor gradColors[] = {SK_ColorBLACK, SkColorSetARGB(128, 128, 128, 128)}; auto gradShader = SkGradientShader::MakeRadial({0.5f * img->width(), 0.5f * img->height()}, 0.1f * img->width(), gradColors, nullptr, 2, SkTileMode::kRepeat, 0, gradLM ? &persp : nullptr); bool imageLM = config.fLM == kImageWithLocalMat || config.fLM == kBothWithLocalMat; auto imgShader = img->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions(), imageLM ? perspScale : scale); // Perspective before any clipShader if (config.fConcat == kConcatBeforeClips) { canvas->concat(persp); } // First clipshader canvas->clipShader(config.fOrder == kClipImageFirst ? imgShader : gradShader); // Perspective between clipShader if (config.fConcat == kConcatBetweenClips) { canvas->concat(persp); } // Second clipShader canvas->clipShader(config.fOrder == kClipImageFirst ? gradShader : imgShader); // Perspective after clipShader if (config.fConcat == kConcatAfterClips) { canvas->concat(persp); } // Actual draw and clip boundary are the same for all configs canvas->clipIRect(img->bounds()); canvas->clear(SK_ColorBLACK); canvas->drawImage(img, 0, 0); canvas->restore(); }; SkIRect grid = persp.mapRect(SkRect::Make(img->dimensions())).roundOut(); grid.fLeft -= 20; // manual adjust to look nicer canvas->translate(10.f, 10.f); for (size_t i = 0; i < SK_ARRAY_COUNT(matches); ++i) { canvas->save(); canvas->translate(-grid.fLeft, -grid.fTop); drawConfig(matches[i][0]); canvas->translate(0.f, grid.height()); drawConfig(matches[i][1]); canvas->restore(); canvas->translate(grid.width(), 0.f); } } DEF_SIMPLE_GM(clip_shader_difference, canvas, 512, 512) { auto image = GetResourceAsImage("images/yellow_rose.png"); canvas->clear(SK_ColorGRAY); SkRect rect = SkRect::MakeWH(256, 256); SkMatrix local = SkMatrix::RectToRect(SkRect::MakeWH(image->width(), image->height()), SkRect::MakeWH(64, 64)); auto shader = image->makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions(), &local); SkPaint paint; paint.setColor(SK_ColorRED); paint.setAntiAlias(true); // TL: A rectangle { canvas->save(); canvas->translate(0, 0); canvas->clipShader(shader, SkClipOp::kDifference); canvas->drawRect(rect, paint); canvas->restore(); } // TR: A round rectangle { canvas->save(); canvas->translate(256, 0); canvas->clipShader(shader, SkClipOp::kDifference); canvas->drawRRect(SkRRect::MakeRectXY(rect, 64.f, 64.f), paint); canvas->restore(); } // BL: A path { canvas->save(); canvas->translate(0, 256); canvas->clipShader(shader, SkClipOp::kDifference); SkPath path; path.moveTo(0.f, 128.f); path.lineTo(128.f, 256.f); path.lineTo(256.f, 128.f); path.lineTo(128.f, 0.f); SkScalar d = 64.f * SK_ScalarSqrt2; path.moveTo(128.f - d, 128.f - d); path.lineTo(128.f - d, 128.f + d); path.lineTo(128.f + d, 128.f + d); path.lineTo(128.f + d, 128.f - d); canvas->drawPath(path, paint); canvas->restore(); } // BR: Text { canvas->save(); canvas->translate(256, 256); canvas->clipShader(shader, SkClipOp::kDifference); for (int y = 0; y < 4; ++y) { canvas->drawString("Hello", 32.f, y * 64.f, SkFont(nullptr, 64.f), paint); } canvas->restore(); } }