Analytic dashing of circles with single on/off intervals and butt caps.

Change-Id: If19ac52cb78af57572a102cec0084f5b6c037680
Reviewed-on: https://skia-review.googlesource.com/121882
Auto-Submit: Brian Salomon <bsalomon@google.com>
Reviewed-by: Greg Daniel <egdaniel@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
This commit is contained in:
Brian Salomon 2018-04-20 13:54:11 -04:00 committed by Skia Commit-Bot
parent 2fda63abcd
commit 62e4f3daf7
6 changed files with 712 additions and 24 deletions

View File

@ -108,3 +108,120 @@ private:
};
DEF_GM(return new DashCircleGM; )
class DashCircle2GM : public skiagm::GM {
public:
DashCircle2GM() {}
protected:
SkString onShortName() override { return SkString("dashcircle2"); }
SkISize onISize() override { return SkISize::Make(635, 900); }
void onDraw(SkCanvas* canvas) override {
// These intervals are defined relative to tau.
static constexpr SkScalar kIntervals[][2]{
{0.333f, 0.333f},
{0.015f, 0.015f},
{0.01f , 0.09f },
{0.097f, 0.003f},
{0.02f , 0.04f },
{0.1f , 0.2f },
{0.25f , 0.25f },
{0.6f , 0.7f }, // adds to > 1
{1.2f , 0.8f }, // on is > 1
{0.1f , 1.1f }, // off is > 1*/
};
static constexpr int kN = SK_ARRAY_COUNT(kIntervals);
static constexpr SkScalar kRadius = 20.f;
static constexpr SkScalar kStrokeWidth = 15.f;
static constexpr SkScalar kPad = 5.f;
static constexpr SkRect kCircle = {-kRadius, -kRadius, kRadius, kRadius};
static constexpr SkScalar kThinRadius = kRadius * 1.5;
static constexpr SkRect kThinCircle = {-kThinRadius, -kThinRadius,
kThinRadius, kThinRadius};
static constexpr SkScalar kThinStrokeWidth = 0.4f;
sk_sp<SkPathEffect> deffects[SK_ARRAY_COUNT(kIntervals)];
sk_sp<SkPathEffect> thinDEffects[SK_ARRAY_COUNT(kIntervals)];
for (int i = 0; i < kN; ++i) {
static constexpr SkScalar kTau = 2 * SK_ScalarPI;
static constexpr SkScalar kCircumference = kRadius * kTau;
SkScalar scaledIntervals[2] = {kCircumference * kIntervals[i][0],
kCircumference * kIntervals[i][1]};
deffects[i] = SkDashPathEffect::Make(
scaledIntervals, 2, kCircumference * fPhaseDegrees * kTau / 360.f);
static constexpr SkScalar kThinCircumference = kThinRadius * kTau;
scaledIntervals[0] = kThinCircumference * kIntervals[i][0];
scaledIntervals[1] = kThinCircumference * kIntervals[i][1];
thinDEffects[i] = SkDashPathEffect::Make(
scaledIntervals, 2, kThinCircumference * fPhaseDegrees * kTau / 360.f);
}
SkMatrix rotate;
rotate.setRotate(25.f);
static const SkMatrix kMatrices[]{
SkMatrix::I(),
SkMatrix::MakeScale(1.2f),
SkMatrix::MakeAll(1, 0, 0, 0, -1, 0, 0, 0, 1), // y flipper
SkMatrix::MakeAll(-1, 0, 0, 0, 1, 0, 0, 0, 1), // x flipper
SkMatrix::MakeScale(0.7f),
rotate,
SkMatrix::Concat(
SkMatrix::Concat(SkMatrix::MakeAll(-1, 0, 0, 0, 1, 0, 0, 0, 1), rotate),
rotate)
};
SkPaint paint;
paint.setAntiAlias(true);
paint.setStrokeWidth(kStrokeWidth);
paint.setStyle(SkPaint::kStroke_Style);
// Compute the union of bounds of all of our test cases.
SkRect bounds = SkRect::MakeEmpty();
static const SkRect kBounds = kThinCircle.makeOutset(kThinStrokeWidth / 2.f,
kThinStrokeWidth / 2.f);
for (const auto& m : kMatrices) {
SkRect devBounds;
m.mapRect(&devBounds, kBounds);
bounds.join(devBounds);
}
canvas->save();
canvas->translate(-bounds.fLeft + kPad, -bounds.fTop + kPad);
for (size_t i = 0; i < SK_ARRAY_COUNT(deffects); ++i) {
canvas->save();
for (const auto& m : kMatrices) {
canvas->save();
canvas->concat(m);
paint.setPathEffect(deffects[i]);
paint.setStrokeWidth(kStrokeWidth);
canvas->drawOval(kCircle, paint);
paint.setPathEffect(thinDEffects[i]);
paint.setStrokeWidth(kThinStrokeWidth);
canvas->drawOval(kThinCircle, paint);
canvas->restore();
canvas->translate(bounds.width() + kPad, 0);
}
canvas->restore();
canvas->translate(0, bounds.height() + kPad);
}
canvas->restore();
}
protected:
bool onAnimate(const SkAnimTimer& timer) override {
fPhaseDegrees = timer.secs();
return true;
}
// Init with a non-zero phase for when run as a non-animating GM.
SkScalar fPhaseDegrees = 12.f;
};
DEF_GM(return new DashCircle2GM;)

View File

@ -68,6 +68,7 @@ public:
enum ClassID {
kBigKeyProcessor_ClassID,
kBlockInputFragmentProcessor_ClassID,
kButtCapStrokedCircleGeometryProcessor_ClassID,
kCircleGeometryProcessor_ClassID,
kCircularRRectEffect_ClassID,
kColorMatrixEffect_ClassID,

View File

@ -1162,7 +1162,8 @@ bool GrRenderTargetContext::drawFilledDRRect(const GrClip& clip,
SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle);
stroke.setStrokeStyle(outerR - innerR);
auto op = GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, circleBounds,
stroke, this->caps()->shaderCaps());
GrStyle(stroke, nullptr),
this->caps()->shaderCaps());
if (op) {
this->addDrawOp(clip, std::move(op));
return true;
@ -1294,21 +1295,17 @@ void GrRenderTargetContext::drawOval(const GrClip& clip,
SkDEBUGCODE(this->validate();)
GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "drawOval", fContext);
if (oval.isEmpty()) {
return;
if (oval.isEmpty() && !style.pathEffect()) {
return;
}
SkASSERT(!style.pathEffect()); // this should've been devolved to a path in SkGpuDevice
AutoCheckFlush acf(this->drawingManager());
const SkStrokeRec& stroke = style.strokeRec();
GrAAType aaType = this->chooseAAType(aa, GrAllowMixedSamples::kNo);
if (GrAAType::kCoverage == aaType) {
const GrShaderCaps* shaderCaps = fContext->caps()->shaderCaps();
std::unique_ptr<GrDrawOp> op =
GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, oval, stroke, shaderCaps);
if (op) {
if (auto op = GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, oval, style,
shaderCaps)) {
this->addDrawOp(clip, std::move(op));
return;
}

View File

@ -509,14 +509,6 @@ void SkGpuDevice::drawRegion(const SkRegion& region, const SkPaint& paint) {
void SkGpuDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
ASSERT_SINGLE_OWNER
GR_CREATE_TRACE_MARKER_CONTEXT("SkGpuDevice", "drawOval", fContext.get());
// Presumably the path effect warps this to something other than an oval
if (paint.getPathEffect()) {
SkPath path;
path.setIsVolatile(true);
path.addOval(oval);
this->drawPath(path, paint, nullptr, true);
return;
}
if (paint.getMaskFilter()) {
// The RRect path can handle special case blurring

View File

@ -264,6 +264,240 @@ sk_sp<GrGeometryProcessor> CircleGeometryProcessor::TestCreate(GrProcessorTestDa
}
#endif
class ButtCapDashedCircleGeometryProcessor : public GrGeometryProcessor {
public:
ButtCapDashedCircleGeometryProcessor(const SkMatrix& localMatrix)
: INHERITED(kButtCapStrokedCircleGeometryProcessor_ClassID), fLocalMatrix(localMatrix) {
fInPosition = &this->addVertexAttrib("inPosition", kFloat2_GrVertexAttribType);
fInColor = &this->addVertexAttrib("inColor", kUByte4_norm_GrVertexAttribType);
fInCircleEdge = &this->addVertexAttrib("inCircleEdge", kFloat4_GrVertexAttribType);
fInDashParams = &this->addVertexAttrib("inDashParams", kFloat4_GrVertexAttribType);
}
~ButtCapDashedCircleGeometryProcessor() override {}
const char* name() const override { return "ButtCapDashedCircleGeometryProcessor"; }
void getGLSLProcessorKey(const GrShaderCaps& caps, GrProcessorKeyBuilder* b) const override {
GLSLProcessor::GenKey(*this, caps, b);
}
GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const override {
return new GLSLProcessor();
}
private:
class GLSLProcessor : public GrGLSLGeometryProcessor {
public:
GLSLProcessor() {}
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
const ButtCapDashedCircleGeometryProcessor& bcscgp =
args.fGP.cast<ButtCapDashedCircleGeometryProcessor>();
GrGLSLVertexBuilder* vertBuilder = args.fVertBuilder;
GrGLSLVaryingHandler* varyingHandler = args.fVaryingHandler;
GrGLSLUniformHandler* uniformHandler = args.fUniformHandler;
GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder;
// emit attributes
varyingHandler->emitAttributes(bcscgp);
fragBuilder->codeAppend("float4 circleEdge;");
varyingHandler->addPassThroughAttribute(bcscgp.fInCircleEdge, "circleEdge");
fragBuilder->codeAppend("float4 dashParams;");
varyingHandler->addPassThroughAttribute(
bcscgp.fInDashParams, "dashParams",
GrGLSLVaryingHandler::Interpolation::kCanBeFlat);
GrGLSLVarying wrapDashes(kHalf4_GrSLType);
varyingHandler->addVarying("wrapDashes", &wrapDashes,
GrGLSLVaryingHandler::Interpolation::kCanBeFlat);
GrGLSLVarying lastIntervalLength(kHalf_GrSLType);
varyingHandler->addVarying("lastIntervalLength", &lastIntervalLength,
GrGLSLVaryingHandler::Interpolation::kCanBeFlat);
vertBuilder->codeAppendf("float4 dashParams = %s;", bcscgp.fInDashParams->fName);
// Our fragment shader works in on/off intervals as specified by dashParams.xy:
// x = length of on interval, y = length of on + off.
// There are two other parameters in dashParams.zw:
// z = start angle in radians, w = phase offset in radians in range -y/2..y/2.
// Each interval has a "corresponding" dash which may be shifted partially or
// fully out of its interval by the phase. So there may be up to two "visual"
// dashes in an interval.
// When computing coverage in an interval we look at three dashes. These are the
// "corresponding" dashes from the current, previous, and next intervals. Any of these
// may be phase shifted into our interval or even when phase=0 they may be within half a
// pixel distance of a pixel center in the interval.
// When in the first interval we need to check the dash from the last interval. And
// similarly when in the last interval we need to check the dash from the first
// interval. When 2pi is not perfectly divisible dashParams.y this is a boundary case.
// We compute the dash begin/end angles in the vertex shader and apply them in the
// fragment shader when we detect we're in the first/last interval.
vertBuilder->codeAppend(R"(
// The two boundary dash intervals are stored in wrapDashes.xy and .zw and fed
// to the fragment shader as a varying.
float4 wrapDashes;
half lastIntervalLength = mod(6.28318530718, dashParams.y);
// We can happen to be perfectly divisible.
if (0 == lastIntervalLength) {
lastIntervalLength = dashParams.y;
}
// Let 'l' be the last interval before reaching 2 pi.
// Based on the phase determine whether (l-1)th, l-th, or (l+1)th interval's
// "corresponding" dash appears in the l-th interval and is closest to the 0-th
// interval.
half offset = 0;
if (-dashParams.w >= lastIntervalLength) {
offset = -dashParams.y;
} else if (dashParams.w > dashParams.y - lastIntervalLength) {
offset = dashParams.y;
}
wrapDashes.x = -lastIntervalLength + offset - dashParams.w;
// The end of this dash may be beyond the 2 pi and therefore clipped. Hence the
// min.
wrapDashes.y = min(wrapDashes.x + dashParams.x, 0);
// Based on the phase determine whether the -1st, 0th, or 1st interval's
// "corresponding" dash appears in the 0th interval and is closest to l.
offset = 0;
if (dashParams.w >= dashParams.x) {
offset = dashParams.y;
} else if (-dashParams.w > dashParams.y - dashParams.x) {
offset = -dashParams.y;
}
wrapDashes.z = lastIntervalLength + offset - dashParams.w;
wrapDashes.w = wrapDashes.z + dashParams.x;
// The start of the dash we're considering may be clipped by the start of the
// circle.
wrapDashes.z = max(wrapDashes.z, lastIntervalLength);
)");
vertBuilder->codeAppendf("%s = wrapDashes;", wrapDashes.vsOut());
vertBuilder->codeAppendf("%s = lastIntervalLength;", lastIntervalLength.vsOut());
fragBuilder->codeAppendf("half4 wrapDashes = %s;", wrapDashes.fsIn());
fragBuilder->codeAppendf("half lastIntervalLength = %s;", lastIntervalLength.fsIn());
// setup pass through color
varyingHandler->addPassThroughAttribute(
bcscgp.fInColor, args.fOutputColor,
GrGLSLVaryingHandler::Interpolation::kCanBeFlat);
// Setup position
this->writeOutputPosition(vertBuilder, gpArgs, bcscgp.fInPosition->fName);
// emit transforms
this->emitTransforms(vertBuilder,
varyingHandler,
uniformHandler,
bcscgp.fInPosition->asShaderVar(),
bcscgp.fLocalMatrix,
args.fFPCoordTransformHandler);
GrShaderVar fnArgs[] = {
GrShaderVar("angleToEdge", kFloat_GrSLType),
GrShaderVar("diameter", kFloat_GrSLType),
};
SkString fnName;
fragBuilder->emitFunction(kFloat_GrSLType, "coverage_from_dash_edge",
SK_ARRAY_COUNT(fnArgs), fnArgs, R"(
float linearDist;
angleToEdge = clamp(angleToEdge, -3.1415, 3.1415);
linearDist = diameter * sin(angleToEdge / 2);
return clamp(linearDist + 0.5, 0, 1);
)",
&fnName);
fragBuilder->codeAppend(R"(
float d = length(circleEdge.xy) * circleEdge.z;
// Compute coverage from outer/inner edges of the stroke.
half distanceToOuterEdge = circleEdge.z - d;
half edgeAlpha = clamp(distanceToOuterEdge, 0.0, 1.0);
half distanceToInnerEdge = d - circleEdge.z * circleEdge.w;
half innerAlpha = clamp(distanceToInnerEdge, 0.0, 1.0);
edgeAlpha *= innerAlpha;
half angleFromStart = atan(circleEdge.y, circleEdge.x) - dashParams.z;
angleFromStart = mod(angleFromStart, 6.28318530718);
float x = mod(angleFromStart, dashParams.y);
// Convert the radial distance from center to pixel into a diameter.
d *= 2;
half2 currDash = half2(-dashParams.w, dashParams.x - dashParams.w);
half2 nextDash = half2(dashParams.y - dashParams.w,
dashParams.y + dashParams.x - dashParams.w);
half2 prevDash = half2(-dashParams.y - dashParams.w,
-dashParams.y + dashParams.x - dashParams.w);
half dashAlpha = 0;
)");
fragBuilder->codeAppendf(R"(
if (angleFromStart - x + dashParams.y >= 6.28318530718) {
dashAlpha += %s(x - wrapDashes.z, d) * %s(wrapDashes.w - x, d);
currDash.y = min(currDash.y, lastIntervalLength);
if (nextDash.x >= lastIntervalLength) {
// The next dash is outside the 0..2pi range, throw it away
nextDash.xy = half2(1000);
} else {
// Clip the end of the next dash to the end of the circle
nextDash.y = min(nextDash.y, lastIntervalLength);
}
}
)", fnName.c_str(), fnName.c_str());
fragBuilder->codeAppendf(R"(
if (angleFromStart - x - dashParams.y < -0.01) {
dashAlpha += %s(x - wrapDashes.x, d) * %s(wrapDashes.y - x, d);
currDash.x = max(currDash.x, 0);
if (prevDash.y <= 0) {
// The previous dash is outside the 0..2pi range, throw it away
prevDash.xy = half2(1000);
} else {
// Clip the start previous dash to the start of the circle
prevDash.x = max(prevDash.x, 0);
}
}
)", fnName.c_str(), fnName.c_str());
fragBuilder->codeAppendf(R"(
dashAlpha += %s(x - currDash.x, d) * %s(currDash.y - x, d);
dashAlpha += %s(x - nextDash.x, d) * %s(nextDash.y - x, d);
dashAlpha += %s(x - prevDash.x, d) * %s(prevDash.y - x, d);
dashAlpha = min(dashAlpha, 1);
edgeAlpha *= dashAlpha;
)", fnName.c_str(), fnName.c_str(), fnName.c_str(), fnName.c_str(), fnName.c_str(),
fnName.c_str());
fragBuilder->codeAppendf("%s = half4(edgeAlpha);", args.fOutputCoverage);
}
static void GenKey(const GrGeometryProcessor& gp,
const GrShaderCaps&,
GrProcessorKeyBuilder* b) {
const ButtCapDashedCircleGeometryProcessor& bcscgp =
gp.cast<ButtCapDashedCircleGeometryProcessor>();
b->add32(bcscgp.fLocalMatrix.hasPerspective());
}
void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc,
FPCoordTransformIter&& transformIter) override {
this->setTransformDataHelper(
primProc.cast<ButtCapDashedCircleGeometryProcessor>().fLocalMatrix, pdman,
&transformIter);
}
private:
typedef GrGLSLGeometryProcessor INHERITED;
};
SkMatrix fLocalMatrix;
const Attribute* fInPosition;
const Attribute* fInColor;
const Attribute* fInCircleEdge;
const Attribute* fInDashParams;
GR_DECLARE_GEOMETRY_PROCESSOR_TEST
typedef GrGeometryProcessor INHERITED;
};
#if GR_TEST_UTILS
sk_sp<GrGeometryProcessor> ButtCapDashedCircleGeometryProcessor::TestCreate(GrProcessorTestData* d) {
const SkMatrix& matrix = GrTest::TestMatrix(d->fRandom);
return sk_sp<GrGeometryProcessor>(new ButtCapDashedCircleGeometryProcessor(matrix));
}
#endif
///////////////////////////////////////////////////////////////////////////////
/**
@ -1240,6 +1474,301 @@ private:
typedef GrMeshDrawOp INHERITED;
};
class ButtCapDashedCircleOp final : public GrMeshDrawOp {
private:
using Helper = GrSimpleMeshDrawOpHelper;
public:
DEFINE_OP_CLASS_ID
static std::unique_ptr<GrDrawOp> Make(GrPaint&& paint, const SkMatrix& viewMatrix,
SkPoint center, SkScalar radius, SkScalar strokeWidth,
SkScalar startAngle, SkScalar onAngle, SkScalar offAngle,
SkScalar phaseAngle) {
SkASSERT(circle_stays_circle(viewMatrix));
SkASSERT(strokeWidth < 2 * radius);
return Helper::FactoryHelper<ButtCapDashedCircleOp>(std::move(paint), viewMatrix, center,
radius, strokeWidth, startAngle,
onAngle, offAngle, phaseAngle);
}
ButtCapDashedCircleOp(const Helper::MakeArgs& helperArgs, GrColor color,
const SkMatrix& viewMatrix, SkPoint center, SkScalar radius,
SkScalar strokeWidth, SkScalar startAngle, SkScalar onAngle,
SkScalar offAngle, SkScalar phaseAngle)
: GrMeshDrawOp(ClassID()), fHelper(helperArgs, GrAAType::kCoverage) {
SkASSERT(circle_stays_circle(viewMatrix));
viewMatrix.mapPoints(&center, 1);
radius = viewMatrix.mapRadius(radius);
strokeWidth = viewMatrix.mapRadius(strokeWidth);
// Determine the angle where the circle starts in device space and whether its orientation
// has been reversed.
SkVector start;
bool reflection;
if (!startAngle) {
start = {1, 0};
} else {
start.fY = SkScalarSinCos(startAngle, &start.fX);
}
viewMatrix.mapVectors(&start, 1);
startAngle = SkScalarATan2(start.fY, start.fX);
reflection = (viewMatrix.getScaleX() * viewMatrix.getScaleY() -
viewMatrix.getSkewX() * viewMatrix.getSkewY()) < 0;
auto totalAngle = onAngle + offAngle;
phaseAngle = SkScalarMod(phaseAngle + totalAngle / 2, totalAngle) - totalAngle / 2;
SkScalar halfWidth = 0;
if (SkScalarNearlyZero(strokeWidth)) {
halfWidth = SK_ScalarHalf;
} else {
halfWidth = SkScalarHalf(strokeWidth);
}
SkScalar outerRadius = radius + halfWidth;
SkScalar innerRadius = radius - halfWidth;
// The radii are outset for two reasons. First, it allows the shader to simply perform
// simpler computation because the computed alpha is zero, rather than 50%, at the radius.
// Second, the outer radius is used to compute the verts of the bounding box that is
// rendered and the outset ensures the box will cover all partially covered by the circle.
outerRadius += SK_ScalarHalf;
innerRadius -= SK_ScalarHalf;
fViewMatrixIfUsingLocalCoords = viewMatrix;
SkRect devBounds = SkRect::MakeLTRB(center.fX - outerRadius, center.fY - outerRadius,
center.fX + outerRadius, center.fY + outerRadius);
// We store whether there is a reflection as a negative total angle.
if (reflection) {
totalAngle = -totalAngle;
}
fCircles.push_back(Circle{
color,
outerRadius,
innerRadius,
onAngle,
totalAngle,
startAngle,
phaseAngle,
devBounds
});
// Use the original radius and stroke radius for the bounds so that it does not include the
// AA bloat.
radius += halfWidth;
this->setBounds(
{center.fX - radius, center.fY - radius, center.fX + radius, center.fY + radius},
HasAABloat::kYes, IsZeroArea::kNo);
fVertCount = circle_type_to_vert_count(true);
fIndexCount = circle_type_to_index_count(true);
}
const char* name() const override { return "ButtCappedDashedCircleOp"; }
void visitProxies(const VisitProxyFunc& func) const override { fHelper.visitProxies(func); }
SkString dumpInfo() const override {
SkString string;
for (int i = 0; i < fCircles.count(); ++i) {
string.appendf(
"Color: 0x%08x Rect [L: %.2f, T: %.2f, R: %.2f, B: %.2f],"
"InnerRad: %.2f, OuterRad: %.2f, OnAngle: %.2f, TotalAngle: %.2f, "
"Phase: %.2f\n",
fCircles[i].fColor, fCircles[i].fDevBounds.fLeft, fCircles[i].fDevBounds.fTop,
fCircles[i].fDevBounds.fRight, fCircles[i].fDevBounds.fBottom,
fCircles[i].fInnerRadius, fCircles[i].fOuterRadius, fCircles[i].fOnAngle,
fCircles[i].fTotalAngle, fCircles[i].fPhaseAngle);
}
string += fHelper.dumpInfo();
string += INHERITED::dumpInfo();
return string;
}
RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip* clip,
GrPixelConfigIsClamped dstIsClamped) override {
GrColor* color = &fCircles.front().fColor;
return fHelper.xpRequiresDstTexture(caps, clip, dstIsClamped,
GrProcessorAnalysisCoverage::kSingleChannel, color);
}
FixedFunctionFlags fixedFunctionFlags() const override { return fHelper.fixedFunctionFlags(); }
private:
void onPrepareDraws(Target* target) override {
SkMatrix localMatrix;
if (!fViewMatrixIfUsingLocalCoords.invert(&localMatrix)) {
return;
}
// Setup geometry processor
sk_sp<GrGeometryProcessor> gp(new ButtCapDashedCircleGeometryProcessor(localMatrix));
struct CircleVertex {
SkPoint fPos;
GrColor fColor;
SkPoint fOffset;
SkScalar fOuterRadius;
SkScalar fInnerRadius;
SkScalar fOnAngle;
SkScalar fTotalAngle;
SkScalar fStartAngle;
SkScalar fPhaseAngle;
};
size_t vertexStride = gp->getVertexStride();
SkASSERT(vertexStride == sizeof(CircleVertex));
const GrBuffer* vertexBuffer;
int firstVertex;
char* vertices = (char*)target->makeVertexSpace(vertexStride, fVertCount, &vertexBuffer,
&firstVertex);
if (!vertices) {
SkDebugf("Could not allocate vertices\n");
return;
}
const GrBuffer* indexBuffer = nullptr;
int firstIndex = 0;
uint16_t* indices = target->makeIndexSpace(fIndexCount, &indexBuffer, &firstIndex);
if (!indices) {
SkDebugf("Could not allocate indices\n");
return;
}
int currStartVertex = 0;
for (const auto& circle : fCircles) {
// The inner radius in the vertex data must be specified in normalized space so that
// length() can be called with smaller values to avoid precision issues with half
// floats.
auto normInnerRadius = circle.fInnerRadius / circle.fOuterRadius;
const SkRect& bounds = circle.fDevBounds;
bool reflect = false;
SkScalar totalAngle = circle.fTotalAngle;
if (totalAngle < 0) {
reflect = true;
totalAngle = -totalAngle;
}
// The bounding geometry for the circle is composed of an outer bounding octagon and
// an inner bounded octagon.
// Initializes the attributes that are the same at each vertex. Also applies reflection.
auto init_const_attrs_and_reflect = [&](CircleVertex* v) {
v->fColor = circle.fColor;
v->fOuterRadius = circle.fOuterRadius;
v->fInnerRadius = normInnerRadius;
v->fOnAngle = circle.fOnAngle;
v->fTotalAngle = totalAngle;
v->fStartAngle = circle.fStartAngle;
v->fPhaseAngle = circle.fPhaseAngle;
if (reflect) {
v->fStartAngle = -v->fStartAngle;
v->fOffset.fY = -v->fOffset.fY;
}
};
// Compute the vertices of the outer octagon.
SkPoint center = SkPoint::Make(bounds.centerX(), bounds.centerY());
SkScalar halfWidth = 0.5f * bounds.width();
auto init_outer_vertex = [&](int idx, SkScalar x, SkScalar y) {
CircleVertex* v = reinterpret_cast<CircleVertex*>(vertices + idx * vertexStride);
v->fPos = center + SkPoint{x * halfWidth, y * halfWidth};
v->fOffset = {x, y};
init_const_attrs_and_reflect(v);
};
static constexpr SkScalar kOctOffset = 0.41421356237f; // sqrt(2) - 1
init_outer_vertex(0, -kOctOffset, -1);
init_outer_vertex(1, kOctOffset, -1);
init_outer_vertex(2, 1, -kOctOffset);
init_outer_vertex(3, 1, kOctOffset);
init_outer_vertex(4, kOctOffset, 1);
init_outer_vertex(5, -kOctOffset, 1);
init_outer_vertex(6, -1, kOctOffset);
init_outer_vertex(7, -1, -kOctOffset);
// Compute the vertices of the inner octagon.
auto init_inner_vertex = [&](int idx, SkScalar x, SkScalar y) {
CircleVertex* v =
reinterpret_cast<CircleVertex*>(vertices + (idx + 8) * vertexStride);
v->fPos = center + SkPoint{x * circle.fInnerRadius, y * circle.fInnerRadius};
v->fOffset = {x * normInnerRadius, y * normInnerRadius};
init_const_attrs_and_reflect(v);
};
// cosine and sine of pi/8
static constexpr SkScalar kCos = 0.923579533f;
static constexpr SkScalar kSin = 0.382683432f;
init_inner_vertex(0, -kSin, -kCos);
init_inner_vertex(1, kSin, -kCos);
init_inner_vertex(2, kCos, -kSin);
init_inner_vertex(3, kCos, kSin);
init_inner_vertex(4, kSin, kCos);
init_inner_vertex(5, -kSin, kCos);
init_inner_vertex(6, -kCos, kSin);
init_inner_vertex(7, -kCos, -kSin);
const uint16_t* primIndices = circle_type_to_indices(true);
const int primIndexCount = circle_type_to_index_count(true);
for (int i = 0; i < primIndexCount; ++i) {
*indices++ = primIndices[i] + currStartVertex;
}
currStartVertex += circle_type_to_vert_count(true);
vertices += circle_type_to_vert_count(true) * vertexStride;
}
GrMesh mesh(GrPrimitiveType::kTriangles);
mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1);
mesh.setVertexData(vertexBuffer, firstVertex);
target->draw(gp.get(), fHelper.makePipeline(target), mesh);
}
bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
ButtCapDashedCircleOp* that = t->cast<ButtCapDashedCircleOp>();
// can only represent 65535 unique vertices with 16-bit indices
if (fVertCount + that->fVertCount > 65536) {
return false;
}
if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
return false;
}
if (fHelper.usesLocalCoords() &&
!fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
return false;
}
fCircles.push_back_n(that->fCircles.count(), that->fCircles.begin());
this->joinBounds(*that);
fVertCount += that->fVertCount;
fIndexCount += that->fIndexCount;
return true;
}
struct Circle {
GrColor fColor;
SkScalar fOuterRadius;
SkScalar fInnerRadius;
SkScalar fOnAngle;
SkScalar fTotalAngle;
SkScalar fStartAngle;
SkScalar fPhaseAngle;
SkRect fDevBounds;
};
SkMatrix fViewMatrixIfUsingLocalCoords;
Helper fHelper;
SkSTArray<1, Circle, true> fCircles;
int fVertCount;
int fIndexCount;
typedef GrMeshDrawOp INHERITED;
};
///////////////////////////////////////////////////////////////////////////////
class EllipseOp : public GrMeshDrawOp {
@ -2487,7 +3016,8 @@ std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeRRectOp(GrPaint&& paint,
const SkStrokeRec& stroke,
const GrShaderCaps* shaderCaps) {
if (rrect.isOval()) {
return MakeOvalOp(std::move(paint), viewMatrix, rrect.getBounds(), stroke, shaderCaps);
return MakeOvalOp(std::move(paint), viewMatrix, rrect.getBounds(), GrStyle(stroke, nullptr),
shaderCaps);
}
if (!viewMatrix.rectStaysRect() || !rrect.isSimple()) {
@ -2502,25 +3032,55 @@ std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeRRectOp(GrPaint&& paint,
std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeOvalOp(GrPaint&& paint,
const SkMatrix& viewMatrix,
const SkRect& oval,
const SkStrokeRec& stroke,
const GrStyle& style,
const GrShaderCaps* shaderCaps) {
// we can draw circles
SkScalar width = oval.width();
if (width > SK_ScalarNearlyZero && SkScalarNearlyEqual(width, oval.height()) &&
circle_stays_circle(viewMatrix)) {
auto r = width / 2.f;
SkPoint center = {oval.centerX(), oval.centerY()};
return CircleOp::Make(std::move(paint), viewMatrix, center, width / 2.f,
GrStyle(stroke, nullptr));
if (style.hasNonDashPathEffect()) {
return nullptr;
} else if (style.isDashed()) {
if (style.strokeRec().getCap() != SkPaint::kButt_Cap ||
style.dashIntervalCnt() != 2 || style.strokeRec().getWidth() >= width) {
return nullptr;
}
auto onInterval = style.dashIntervals()[0];
auto offInterval = style.dashIntervals()[1];
if (offInterval == 0) {
GrStyle strokeStyle(style.strokeRec(), nullptr);
return MakeOvalOp(std::move(paint), viewMatrix, oval, strokeStyle, shaderCaps);
} else if (onInterval == 0) {
// There is nothing to draw but we have no way to indicate that here.
return nullptr;
}
auto angularOnInterval = onInterval / r;
auto angularOffInterval = offInterval / r;
auto phaseAngle = style.dashPhase() / r;
// Currently this function doesn't accept ovals with different start angles, though
// it could.
static const SkScalar kStartAngle = 0.f;
return ButtCapDashedCircleOp::Make(std::move(paint), viewMatrix, center, r,
style.strokeRec().getWidth(), kStartAngle,
angularOnInterval, angularOffInterval, phaseAngle);
}
return CircleOp::Make(std::move(paint), viewMatrix, center, r, style);
}
if (style.pathEffect()) {
return nullptr;
}
// prefer the device space ellipse op for batchability
if (viewMatrix.rectStaysRect()) {
return EllipseOp::Make(std::move(paint), viewMatrix, oval, stroke);
return EllipseOp::Make(std::move(paint), viewMatrix, oval, style.strokeRec());
}
// Otherwise, if we have shader derivative support, render as device-independent
if (shaderCaps->shaderDerivativeSupport()) {
return DIEllipseOp::Make(std::move(paint), viewMatrix, oval, stroke);
return DIEllipseOp::Make(std::move(paint), viewMatrix, oval, style.strokeRec());
}
return nullptr;
@ -2582,6 +3142,27 @@ GR_DRAW_OP_TEST_DEFINE(CircleOp) {
} while (true);
}
GR_DRAW_OP_TEST_DEFINE(ButtCapDashedCircleOp) {
SkScalar rotate = random->nextSScalar1() * 360.f;
SkScalar translateX = random->nextSScalar1() * 1000.f;
SkScalar translateY = random->nextSScalar1() * 1000.f;
SkScalar scale = random->nextSScalar1() * 100.f;
SkMatrix viewMatrix;
viewMatrix.setRotate(rotate);
viewMatrix.postTranslate(translateX, translateY);
viewMatrix.postScale(scale, scale);
SkRect circle = GrTest::TestSquare(random);
SkPoint center = {circle.centerX(), circle.centerY()};
SkScalar radius = circle.width() / 2.f;
SkScalar strokeWidth = random->nextRangeScalar(0.001f * radius, 1.8f * radius);
SkScalar onAngle = random->nextRangeScalar(0.01f, 1000.f);
SkScalar offAngle = random->nextRangeScalar(0.01f, 1000.f);
SkScalar startAngle = random->nextRangeScalar(-1000.f, 1000.f);
SkScalar phase = random->nextRangeScalar(-1000.f, 1000.f);
return ButtCapDashedCircleOp::Make(std::move(paint), viewMatrix, center, radius, strokeWidth,
startAngle, onAngle, offAngle, phase);
}
GR_DRAW_OP_TEST_DEFINE(EllipseOp) {
SkMatrix viewMatrix = GrTest::TestMatrixRectStaysRect(random);
SkRect ellipse = GrTest::TestSquare(random);

View File

@ -28,7 +28,7 @@ public:
static std::unique_ptr<GrDrawOp> MakeOvalOp(GrPaint&&,
const SkMatrix&,
const SkRect& oval,
const SkStrokeRec&,
const GrStyle& style,
const GrShaderCaps*);
static std::unique_ptr<GrDrawOp> MakeRRectOp(GrPaint&&,