This uses quad approximations of the outer and inner paths describing a stroke. Cubics and conics' thick strokes are approximated with quads as well.
The approximation uses a similar error term as the fill scan converter to determine the number of quads to use.
This also updates SampleApp QuadStroker test with conics, ovals, and stroked text.
Review URL: https://codereview.chromium.org/932113002
diff --git a/gyp/common.gypi b/gyp/common.gypi
index ce3fc26..9e48c17 100644
--- a/gyp/common.gypi
+++ b/gyp/common.gypi
@@ -15,6 +15,7 @@
'SK_INTERNAL',
'SK_GAMMA_SRGB',
'SK_GAMMA_APPLY_TO_A8',
+ 'SK_QUAD_STROKE_APPROXIMATION',
'SK_SCALAR_TO_FLOAT_EXCLUDED', # temporary to allow Chrome to call SkFloatToScalar
# 'SK_USE_DISCARDABLE_SCALEDIMAGECACHE', # TODO(reed): Re-enable when tests don't crash with this.
],
diff --git a/samplecode/SampleQuadStroker.cpp b/samplecode/SampleQuadStroker.cpp
index 4fd6e74..a3863a8 100644
--- a/samplecode/SampleQuadStroker.cpp
+++ b/samplecode/SampleQuadStroker.cpp
@@ -85,6 +85,10 @@
bool fEnabled;
};
+struct CircleTypeButton : public StrokeTypeButton {
+ bool fFill;
+};
+
class QuadStrokerView : public SampleView {
enum {
SKELETON_COLOR = 0xFF0000FF,
@@ -92,9 +96,10 @@
};
enum {
- kCount = 10
+ kCount = 15
};
SkPoint fPts[kCount];
+ SkRect fWeightControl;
SkRect fErrorControl;
SkRect fWidthControl;
SkRect fBounds;
@@ -103,11 +108,14 @@
SkAutoTUnref<SkSurface> fMinSurface;
SkAutoTUnref<SkSurface> fMaxSurface;
StrokeTypeButton fCubicButton;
+ StrokeTypeButton fConicButton;
StrokeTypeButton fQuadButton;
StrokeTypeButton fRRectButton;
+ CircleTypeButton fCircleButton;
StrokeTypeButton fTextButton;
SkString fText;
SkScalar fTextSize;
+ SkScalar fWeight;
SkScalar fWidth, fDWidth;
SkScalar fWidthScale;
int fW, fH, fZoom;
@@ -124,32 +132,44 @@
QuadStrokerView() {
this->setBGColor(SK_ColorLTGRAY);
- fPts[0].set(50, 200);
+ fPts[0].set(50, 200); // cubic
fPts[1].set(50, 100);
fPts[2].set(150, 50);
fPts[3].set(300, 50);
- fPts[4].set(350, 200);
+ fPts[4].set(350, 200); // conic
fPts[5].set(350, 100);
fPts[6].set(450, 50);
- fPts[7].set(200, 200);
- fPts[8].set(400, 400);
+ fPts[7].set(150, 300); // quad
+ fPts[8].set(150, 200);
+ fPts[9].set(250, 150);
- fPts[9].set(250, 800);
+ fPts[10].set(200, 200); // rrect
+ fPts[11].set(400, 400);
+
+ fPts[12].set(250, 250); // oval
+ fPts[13].set(450, 450);
+
fText = "a";
fTextSize = 12;
fWidth = 50;
fDWidth = 0.25f;
+ fWeight = 1;
fCubicButton.fLabel = 'C';
fCubicButton.fEnabled = false;
+ fConicButton.fLabel = 'K';
+ fConicButton.fEnabled = true;
fQuadButton.fLabel = 'Q';
fQuadButton.fEnabled = false;
fRRectButton.fLabel = 'R';
fRRectButton.fEnabled = false;
+ fCircleButton.fLabel = 'O';
+ fCircleButton.fEnabled = false;
+ fCircleButton.fFill = false;
fTextButton.fLabel = 'T';
- fTextButton.fEnabled = true;
+ fTextButton.fEnabled = false;
fAnimate = true;
setAsNeeded();
}
@@ -183,12 +203,21 @@
}
void onSizeChange() SK_OVERRIDE {
+ fWeightControl.setXYWH(this->width() - 150, 30, 30, 400);
fErrorControl.setXYWH(this->width() - 100, 30, 30, 400);
fWidthControl.setXYWH(this->width() - 50, 30, 30, 400);
- fCubicButton.fBounds.setXYWH(this->width() - 50, 450, 30, 30);
- fQuadButton.fBounds.setXYWH(this->width() - 50, 500, 30, 30);
- fRRectButton.fBounds.setXYWH(this->width() - 50, 550, 30, 30);
- fTextButton.fBounds.setXYWH(this->width() - 50, 600, 30, 30);
+ int buttonOffset = 450;
+ fCubicButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
+ buttonOffset += 50;
+ fConicButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
+ buttonOffset += 50;
+ fQuadButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
+ buttonOffset += 50;
+ fRRectButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
+ buttonOffset += 50;
+ fCircleButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
+ buttonOffset += 50;
+ fTextButton.fBounds.setXYWH(this->width() - 50, SkIntToScalar(buttonOffset), 30, 30);
this->INHERITED::onSizeChange();
}
@@ -273,25 +302,26 @@
}
}
- void draw_stroke(SkCanvas* canvas, const SkPath& path, SkScalar width, bool drawText) {
+ void draw_stroke(SkCanvas* canvas, const SkPath& path, SkScalar width, SkScalar scale,
+ bool drawText) {
SkRect bounds = path.getBounds();
if (bounds.isEmpty()) {
return;
}
- this->setWHZ(SkScalarCeilToInt(bounds.right()), SkScalarRoundToInt(fTextSize * 3 / 2),
- SkScalarRoundToInt(950.0f / fTextSize));
+ this->setWHZ(SkScalarCeilToInt(bounds.right()), drawText
+ ? SkScalarRoundToInt(scale * 3 / 2) : SkScalarRoundToInt(scale),
+ SkScalarRoundToInt(950.0f / scale));
erase(fMinSurface);
SkPaint paint;
paint.setColor(0x1f1f0f0f);
- fMinSurface->getCanvas()->drawPath(path, paint);
paint.setStyle(SkPaint::kStroke_Style);
- paint.setStrokeWidth(width * fTextSize * fTextSize);
+ paint.setStrokeWidth(width * scale * scale);
paint.setColor(0x3f0f1f3f);
- fMinSurface->getCanvas()->drawPath(path, paint);
-
- this->copyMinToMax();
- fMaxSurface->draw(canvas, 0, 0, NULL);
-
+ if (drawText) {
+ fMinSurface->getCanvas()->drawPath(path, paint);
+ this->copyMinToMax();
+ fMaxSurface->draw(canvas, 0, 0, NULL);
+ }
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(1);
@@ -300,7 +330,7 @@
SkPath scaled;
SkMatrix matrix;
matrix.reset();
- matrix.setScale(950 / fTextSize, 950 / fTextSize);
+ matrix.setScale(950 / scale, 950 / scale);
if (drawText) {
path.transform(matrix, &scaled);
} else {
@@ -317,8 +347,11 @@
SkPaint p;
p.setStyle(SkPaint::kStroke_Style);
- p.setStrokeWidth(width * fTextSize * fTextSize);
-
+ if (drawText) {
+ p.setStrokeWidth(width * scale * scale);
+ } else {
+ p.setStrokeWidth(width);
+ }
p.getFillPath(path, &fill);
SkPath scaledFill;
if (drawText) {
@@ -331,6 +364,33 @@
draw_points(canvas, scaledFill, WIREFRAME_COLOR, false);
}
+ void draw_fill(SkCanvas* canvas, const SkRect& rect, SkScalar width) {
+ if (rect.isEmpty()) {
+ return;
+ }
+ SkPaint paint;
+ paint.setColor(0x1f1f0f0f);
+ paint.setStyle(SkPaint::kStroke_Style);
+ paint.setStrokeWidth(width);
+ SkPath path;
+ SkScalar maxSide = SkTMax(rect.width(), rect.height()) / 2;
+ SkPoint center = { rect.fLeft + maxSide, rect.fTop + maxSide };
+ path.addCircle(center.fX, center.fY, maxSide);
+ canvas->drawPath(path, paint);
+ paint.setStyle(SkPaint::kFill_Style);
+ path.reset();
+ path.addCircle(center.fX, center.fY, maxSide - width / 2);
+ paint.setColor(0x3f0f1f3f);
+ canvas->drawPath(path, paint);
+ path.reset();
+ path.setFillType(SkPath::kEvenOdd_FillType);
+ path.addCircle(center.fX, center.fY, maxSide + width / 2);
+ SkRect outside = SkRect::MakeXYWH(center.fX - maxSide - width, center.fY - maxSide - width,
+ (maxSide + width) * 2, (maxSide + width) * 2);
+ path.addRect(outside);
+ canvas->drawPath(path, paint);
+ }
+
void draw_button(SkCanvas* canvas, const StrokeTypeButton& button) {
SkPaint paint;
paint.setAntiAlias(true);
@@ -377,7 +437,8 @@
}
void setAsNeeded() {
- if (fCubicButton.fEnabled || fQuadButton.fEnabled || fRRectButton.fEnabled) {
+ if (fConicButton.fEnabled || fCubicButton.fEnabled || fQuadButton.fEnabled
+ || fRRectButton.fEnabled || fCircleButton.fEnabled) {
setForGeometry();
} else {
setForText();
@@ -392,27 +453,34 @@
path.moveTo(fPts[0]);
path.cubicTo(fPts[1], fPts[2], fPts[3]);
setForGeometry();
- draw_stroke(canvas, path, width, false);
+ draw_stroke(canvas, path, width, 950, false);
+ }
+
+ if (fConicButton.fEnabled) {
+ path.moveTo(fPts[4]);
+ path.conicTo(fPts[5], fPts[6], fWeight);
+ setForGeometry();
+ draw_stroke(canvas, path, width, 950, false);
}
if (fQuadButton.fEnabled) {
path.reset();
- path.moveTo(fPts[4]);
- path.quadTo(fPts[5], fPts[6]);
+ path.moveTo(fPts[7]);
+ path.quadTo(fPts[8], fPts[9]);
setForGeometry();
- draw_stroke(canvas, path, width, false);
+ draw_stroke(canvas, path, width, 950, false);
}
if (fRRectButton.fEnabled) {
SkScalar rad = 32;
SkRect r;
- r.set(&fPts[7], 2);
+ r.set(&fPts[10], 2);
path.reset();
SkRRect rr;
rr.setRectXY(r, rad, rad);
path.addRRect(rr);
setForGeometry();
- draw_stroke(canvas, path, width, false);
+ draw_stroke(canvas, path, width, 950, false);
path.reset();
SkRRect rr2;
@@ -426,6 +494,19 @@
canvas->drawPath(path, paint);
}
+ if (fCircleButton.fEnabled) {
+ path.reset();
+ SkRect r;
+ r.set(&fPts[12], 2);
+ path.addOval(r);
+ setForGeometry();
+ if (fCircleButton.fFill) {
+ draw_fill(canvas, r, width);
+ } else {
+ draw_stroke(canvas, path, width, 950, false);
+ }
+ }
+
if (fTextButton.fEnabled) {
path.reset();
SkPaint paint;
@@ -433,7 +514,7 @@
paint.setTextSize(fTextSize);
paint.getTextPath(fText.c_str(), fText.size(), 0, fTextSize, &path);
setForText();
- draw_stroke(canvas, path, width * fWidthScale / fTextSize, true);
+ draw_stroke(canvas, path, width * fWidthScale / fTextSize, fTextSize, true);
}
if (fAnimate) {
@@ -445,6 +526,9 @@
}
}
setAsNeeded();
+ if (fConicButton.fEnabled) {
+ draw_control(canvas, fWeightControl, fWeight, 0, 5, "weight");
+ }
#if QUAD_STROKE_APPROXIMATION && defined(SK_DEBUG)
draw_control(canvas, fErrorControl, gDebugStrokerError, kStrokerErrorMin, kStrokerErrorMax,
"error");
@@ -453,7 +537,9 @@
kWidthMax * fWidthScale, "width");
draw_button(canvas, fQuadButton);
draw_button(canvas, fCubicButton);
+ draw_button(canvas, fConicButton);
draw_button(canvas, fRRectButton);
+ draw_button(canvas, fCircleButton);
draw_button(canvas, fTextButton);
this->inval(NULL);
}
@@ -472,9 +558,12 @@
}
}
const SkRect& rectPt = SkRect::MakeXYWH(x, y, 1, 1);
+ if (fWeightControl.contains(rectPt)) {
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 1);
+ }
#if QUAD_STROKE_APPROXIMATION && defined(SK_DEBUG)
if (fErrorControl.contains(rectPt)) {
- return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 1);
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 2);
}
#endif
if (fWidthControl.contains(rectPt)) {
@@ -484,17 +573,27 @@
fCubicButton.fEnabled ^= true;
return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 4);
}
+ if (fConicButton.fBounds.contains(rectPt)) {
+ fConicButton.fEnabled ^= true;
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 5);
+ }
if (fQuadButton.fBounds.contains(rectPt)) {
fQuadButton.fEnabled ^= true;
- return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 5);
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 6);
}
if (fRRectButton.fBounds.contains(rectPt)) {
fRRectButton.fEnabled ^= true;
- return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 6);
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 7);
+ }
+ if (fCircleButton.fBounds.contains(rectPt)) {
+ bool wasEnabled = fCircleButton.fEnabled;
+ fCircleButton.fEnabled = !fCircleButton.fFill;
+ fCircleButton.fFill = wasEnabled && !fCircleButton.fFill;
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 8);
}
if (fTextButton.fBounds.contains(rectPt)) {
fTextButton.fEnabled ^= true;
- return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 7);
+ return new MyClick(this, (int) SK_ARRAY_COUNT(fPts) + 9);
}
return this->INHERITED::onFindClickHandler(x, y, modi);
}
@@ -510,9 +609,11 @@
fPts[index].offset(SkIntToScalar(click->fICurr.fX - click->fIPrev.fX),
SkIntToScalar(click->fICurr.fY - click->fIPrev.fY));
this->inval(NULL);
+ } else if (index == (int) SK_ARRAY_COUNT(fPts) + 1) {
+ fWeight = MapScreenYtoValue(click->fICurr.fY, fWeightControl, 0, 5);
}
#if QUAD_STROKE_APPROXIMATION && defined(SK_DEBUG)
- else if (index == (int) SK_ARRAY_COUNT(fPts) + 1) {
+ else if (index == (int) SK_ARRAY_COUNT(fPts) + 2) {
gDebugStrokerError = SkTMax(FLT_EPSILON, MapScreenYtoValue(click->fICurr.fY,
fErrorControl, kStrokerErrorMin, kStrokerErrorMax));
gDebugStrokerErrorSet = true;
diff --git a/src/core/SkStroke.cpp b/src/core/SkStroke.cpp
index db76caf..1a44a3a 100644
--- a/src/core/SkStroke.cpp
+++ b/src/core/SkStroke.cpp
@@ -9,26 +9,26 @@
#include "SkGeometry.h"
#include "SkPath.h"
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
enum {
kTangent_RecursiveLimit,
kCubic_RecursiveLimit,
+ kConic_RecursiveLimit,
kQuad_RecursiveLimit
};
// quads with extreme widths (e.g. (0,1) (1,6) (0,3) width=5e7) recurse to point of failure
// largest seen for normal cubics : 5, 26
// largest seen for normal quads : 11
- static const int kRecursiveLimits[] = { 5*3, 26*3, 11*3 }; // 3x limits seen in practical tests
+ static const int kRecursiveLimits[] = { 5*3, 26*3, 11*3, 11*3 }; // 3x limits seen in practice
+ SK_COMPILE_ASSERT(0 == kTangent_RecursiveLimit, cubic_stroke_relies_on_tangent_equalling_zero);
+ SK_COMPILE_ASSERT(1 == kCubic_RecursiveLimit, cubic_stroke_relies_on_cubic_equalling_one);
SK_COMPILE_ASSERT(SK_ARRAY_COUNT(kRecursiveLimits) == kQuad_RecursiveLimit + 1,
recursive_limits_mismatch);
- #ifdef SK_DEBUG // enables tweaking these values at runtime from SampleApp
- bool gDebugStrokerErrorSet = false;
- SkScalar gDebugStrokerError;
-
+ #ifdef SK_DEBUG
int gMaxRecursion[SK_ARRAY_COUNT(kRecursiveLimits)] = { 0 };
#endif
#ifndef DEBUG_QUAD_STROKER
@@ -51,14 +51,16 @@
#endif
+#ifndef SK_QUAD_STROKE_APPROXIMATION
#define kMaxQuadSubdivide 5
#define kMaxCubicSubdivide 7
+#endif
static inline bool degenerate_vector(const SkVector& v) {
return !SkPoint::CanNormalize(v.fX, v.fY);
}
-#if !QUAD_STROKE_APPROXIMATION
+#ifndef SK_QUAD_STROKE_APPROXIMATION
static inline bool normals_too_curvy(const SkVector& norm0, SkVector& norm1) {
/* root2/2 is a 45-degree angle
make this constant bigger for more subdivisions (but not >= 1)
@@ -111,7 +113,7 @@
}
///////////////////////////////////////////////////////////////////////////////
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
struct SkQuadConstruct { // The state of the quad stroke under construction.
SkPoint fQuad[3]; // the stroked quad parallel to the original curve
@@ -156,19 +158,16 @@
class SkPathStroker {
public:
-#if QUAD_STROKE_APPROXIMATION
- SkPathStroker(const SkPath& src,
- SkScalar radius, SkScalar miterLimit, SkScalar error, SkPaint::Cap,
- SkPaint::Join, SkScalar resScale);
-#else
SkPathStroker(const SkPath& src,
SkScalar radius, SkScalar miterLimit, SkPaint::Cap,
SkPaint::Join, SkScalar resScale);
-#endif
void moveTo(const SkPoint&);
void lineTo(const SkPoint&);
void quadTo(const SkPoint&, const SkPoint&);
+#ifdef SK_QUAD_STROKE_APPROXIMATION
+ void conicTo(const SkPoint&, const SkPoint&, SkScalar weight);
+#endif
void cubicTo(const SkPoint&, const SkPoint&, const SkPoint&);
void close(bool isLine) { this->finishContour(true, isLine); }
@@ -181,12 +180,13 @@
SkScalar getResScale() const { return fResScale; }
private:
-#if QUAD_STROKE_APPROXIMATION
- SkScalar fError;
-#endif
SkScalar fRadius;
SkScalar fInvMiterLimit;
SkScalar fResScale;
+#ifdef SK_QUAD_STROKE_APPROXIMATION
+ SkScalar fInvResScale;
+ SkScalar fInvResScaleSquared;
+#endif
SkVector fFirstNormal, fPrevNormal, fFirstUnitNormal, fPrevUnitNormal;
SkPoint fFirstPt, fPrevPt; // on original path
@@ -200,7 +200,7 @@
SkPath fInner, fOuter; // outer is our working answer, inner is temp
SkPath fExtra; // added as extra complete contours
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
enum StrokeType {
kOuter_StrokeType = 1, // use sign-opposite values later to flip perpendicular axis
kInner_StrokeType = -1
@@ -231,11 +231,17 @@
bool fFoundTangents; // do less work until tangents meet (cubic)
void addDegenerateLine(const SkQuadConstruct* );
+ ReductionType CheckConicLinear(const SkConic& , SkPoint* reduction);
ReductionType CheckCubicLinear(const SkPoint cubic[4], SkPoint reduction[3],
const SkPoint** tanPtPtr);
ReductionType CheckQuadLinear(const SkPoint quad[3], SkPoint* reduction);
+ ResultType compareQuadConic(const SkConic& , SkQuadConstruct* );
ResultType compareQuadCubic(const SkPoint cubic[4], SkQuadConstruct* );
ResultType compareQuadQuad(const SkPoint quad[3], SkQuadConstruct* );
+ bool conicPerpRay(const SkConic& , SkScalar t, SkPoint* tPt, SkPoint* onPt,
+ SkPoint* tangent) const;
+ bool conicQuadEnds(const SkConic& , SkQuadConstruct* );
+ bool conicStroke(const SkConic& , SkQuadConstruct* );
bool cubicMidOnLine(const SkPoint cubic[4], const SkQuadConstruct* ) const;
bool cubicPerpRay(const SkPoint cubic[4], SkScalar t, SkPoint* tPt, SkPoint* onPt,
SkPoint* tangent) const;
@@ -248,6 +254,9 @@
void quadPerpRay(const SkPoint quad[3], SkScalar t, SkPoint* tPt, SkPoint* onPt,
SkPoint* tangent) const;
bool quadStroke(const SkPoint quad[3], SkQuadConstruct* );
+ void setConicEndNormal(const SkConic& ,
+ const SkVector& normalAB, const SkVector& unitNormalAB,
+ SkVector* normalBC, SkVector* unitNormalBC);
void setCubicEndNormal(const SkPoint cubic[4],
const SkVector& normalAB, const SkVector& unitNormalAB,
SkVector* normalCD, SkVector* unitNormalCD);
@@ -268,7 +277,7 @@
const SkVector& unitNormal);
void line_to(const SkPoint& currPt, const SkVector& normal);
-#if !QUAD_STROKE_APPROXIMATION
+#ifndef SK_QUAD_STROKE_APPROXIMATION
void quad_to(const SkPoint pts[3],
const SkVector& normalAB, const SkVector& unitNormalAB,
SkVector* normalBC, SkVector* unitNormalBC,
@@ -348,16 +357,11 @@
///////////////////////////////////////////////////////////////////////////////
-#if QUAD_STROKE_APPROXIMATION
-SkPathStroker::SkPathStroker(const SkPath& src,
- SkScalar radius, SkScalar miterLimit, SkScalar error,
- SkPaint::Cap cap, SkPaint::Join join, SkScalar resScale)
-#else
SkPathStroker::SkPathStroker(const SkPath& src,
SkScalar radius, SkScalar miterLimit,
SkPaint::Cap cap, SkPaint::Join join, SkScalar resScale)
-#endif
- : fRadius(radius), fResScale(resScale) {
+ : fRadius(radius)
+ , fResScale(resScale) {
/* This is only used when join is miter_join, but we initialize it here
so that it is always defined, to fis valgrind warnings.
@@ -386,15 +390,11 @@
fOuter.setIsVolatile(true);
fInner.incReserve(src.countPoints());
fInner.setIsVolatile(true);
-#if QUAD_STROKE_APPROXIMATION
-#ifdef SK_DEBUG
- if (!gDebugStrokerErrorSet) {
- gDebugStrokerError = error;
- }
- fError = gDebugStrokerError;
-#else
- fError = error;
-#endif
+#ifdef SK_QUAD_STROKE_APPROXIMATION
+ // TODO : write a common error function used by stroking and filling
+ // The '4' below matches the fill scan converter's error term
+ fInvResScale = SkScalarInvert(resScale * 4);
+ fInvResScaleSquared = fInvResScale * fInvResScale;
fRecursionDepth = 0;
#endif
}
@@ -423,7 +423,7 @@
this->postJoinTo(currPt, normal, unitNormal);
}
-#if !QUAD_STROKE_APPROXIMATION
+#ifndef SK_QUAD_STROKE_APPROXIMATION
void SkPathStroker::quad_to(const SkPoint pts[3],
const SkVector& normalAB, const SkVector& unitNormalAB,
SkVector* normalBC, SkVector* unitNormalBC,
@@ -461,7 +461,7 @@
}
#endif
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
void SkPathStroker::setQuadEndNormal(const SkPoint quad[3], const SkVector& normalAB,
const SkVector& unitNormalAB, SkVector* normalBC, SkVector* unitNormalBC) {
if (!set_normal_unitnormal(quad[1], quad[2], fRadius, normalBC, unitNormalBC)) {
@@ -470,6 +470,11 @@
}
}
+void SkPathStroker::setConicEndNormal(const SkConic& conic, const SkVector& normalAB,
+ const SkVector& unitNormalAB, SkVector* normalBC, SkVector* unitNormalBC) {
+ setQuadEndNormal(conic.fPts, normalAB, unitNormalAB, normalBC, unitNormalBC);
+}
+
void SkPathStroker::setCubicEndNormal(const SkPoint cubic[4], const SkVector& normalAB,
const SkVector& unitNormalAB, SkVector* normalCD, SkVector* unitNormalCD) {
SkVector ab = cubic[1] - cubic[0];
@@ -547,7 +552,8 @@
*/
static bool cubic_in_line(const SkPoint cubic[4]) {
SkScalar ptMax = -1;
- int outer1, outer2;
+ int outer1 SK_INIT_TO_AVOID_WARNING;
+ int outer2 SK_INIT_TO_AVOID_WARNING;
for (int index = 0; index < 3; ++index) {
for (int inner = index + 1; inner < 4; ++inner) {
SkVector testDiff = cubic[inner] - cubic[index];
@@ -583,7 +589,8 @@
*/
static bool quad_in_line(const SkPoint quad[3]) {
SkScalar ptMax = -1;
- int outer1, outer2;
+ int outer1 SK_INIT_TO_AVOID_WARNING;
+ int outer2 SK_INIT_TO_AVOID_WARNING;
for (int index = 0; index < 2; ++index) {
for (int inner = index + 1; inner < 3; ++inner) {
SkVector testDiff = quad[inner] - quad[index];
@@ -603,6 +610,10 @@
return pt_to_line(quad[mid], quad[outer1], quad[outer2]) <= lineSlop;
}
+static bool conic_in_line(const SkConic& conic) {
+ return quad_in_line(conic.fPts);
+}
+
SkPathStroker::ReductionType SkPathStroker::CheckCubicLinear(const SkPoint cubic[4],
SkPoint reduction[3], const SkPoint** tangentPtPtr) {
bool degenerateAB = degenerate_vector(cubic[1] - cubic[0]);
@@ -634,6 +645,27 @@
return (ReductionType) (kQuad_ReductionType + count);
}
+SkPathStroker::ReductionType SkPathStroker::CheckConicLinear(const SkConic& conic,
+ SkPoint* reduction) {
+ bool degenerateAB = degenerate_vector(conic.fPts[1] - conic.fPts[0]);
+ bool degenerateBC = degenerate_vector(conic.fPts[2] - conic.fPts[1]);
+ if (degenerateAB & degenerateBC) {
+ return kPoint_ReductionType;
+ }
+ if (degenerateAB | degenerateBC) {
+ return kLine_ReductionType;
+ }
+ if (!conic_in_line(conic)) {
+ return kQuad_ReductionType;
+ }
+ SkScalar t;
+ if (!conic.findMaxCurvature(&t) || 0 == t) {
+ return kLine_ReductionType;
+ }
+ conic.evalAt(t, reduction, NULL);
+ return kDegenerate_ReductionType;
+}
+
SkPathStroker::ReductionType SkPathStroker::CheckQuadLinear(const SkPoint quad[3],
SkPoint* reduction) {
bool degenerateAB = degenerate_vector(quad[1] - quad[0]);
@@ -742,8 +774,45 @@
}
#endif
+#ifdef SK_QUAD_STROKE_APPROXIMATION
+void SkPathStroker::conicTo(const SkPoint& pt1, const SkPoint& pt2, SkScalar weight) {
+ const SkConic conic(fPrevPt, pt1, pt2, weight);
+ SkPoint reduction;
+ ReductionType reductionType = CheckConicLinear(conic, &reduction);
+ if (kPoint_ReductionType == reductionType) {
+ return;
+ }
+ if (kLine_ReductionType == reductionType) {
+ this->lineTo(pt2);
+ return;
+ }
+ if (kDegenerate_ReductionType == reductionType) {
+ this->lineTo(reduction);
+ SkStrokerPriv::JoinProc saveJoiner = fJoiner;
+ fJoiner = SkStrokerPriv::JoinFactory(SkPaint::kRound_Join);
+ this->lineTo(pt2);
+ fJoiner = saveJoiner;
+ return;
+ }
+ SkASSERT(kQuad_ReductionType == reductionType);
+ SkVector normalAB, unitAB, normalBC, unitBC;
+ this->preJoinTo(pt1, &normalAB, &unitAB, false);
+ SkQuadConstruct quadPts;
+ this->init(kOuter_StrokeType, &quadPts, 0, 1);
+ if (!this->conicStroke(conic, &quadPts)) {
+ return;
+ }
+ this->init(kInner_StrokeType, &quadPts, 0, 1);
+ if (!this->conicStroke(conic, &quadPts)) {
+ return;
+ }
+ this->setConicEndNormal(conic, normalAB, unitAB, &normalBC, &unitBC);
+ this->postJoinTo(pt2, normalBC, unitBC);
+}
+#endif
+
void SkPathStroker::quadTo(const SkPoint& pt1, const SkPoint& pt2) {
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
const SkPoint quad[3] = { fPrevPt, pt1, pt2 };
SkPoint reduction;
ReductionType reductionType = CheckQuadLinear(quad, &reduction);
@@ -831,7 +900,7 @@
this->postJoinTo(pt2, normalBC, unitBC);
}
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
// Given a point on the curve and its derivative, scale the derivative by the radius, and
// compute the perpendicular point and its tangent.
void SkPathStroker::setRayPts(const SkPoint& tPt, SkVector* dxy, SkPoint* onPt,
@@ -852,6 +921,41 @@
}
}
+// Given a conic and t, return the point on curve, its perpendicular, and the perpendicular tangent.
+// Returns false if the perpendicular could not be computed (because the derivative collapsed to 0)
+bool SkPathStroker::conicPerpRay(const SkConic& conic, SkScalar t, SkPoint* tPt, SkPoint* onPt,
+ SkPoint* tangent) const {
+ SkVector dxy;
+ conic.evalAt(t, tPt, &dxy);
+ if (dxy.fX == 0 && dxy.fY == 0) {
+ dxy = conic.fPts[2] - conic.fPts[0];
+ }
+ setRayPts(*tPt, &dxy, onPt, tangent);
+ return true;
+}
+
+// Given a conic and a t range, find the start and end if they haven't been found already.
+bool SkPathStroker::conicQuadEnds(const SkConic& conic, SkQuadConstruct* quadPts) {
+ if (!quadPts->fStartSet) {
+ SkPoint conicStartPt;
+ if (!this->conicPerpRay(conic, quadPts->fStartT, &conicStartPt, &quadPts->fQuad[0],
+ &quadPts->fTangentStart)) {
+ return false;
+ }
+ quadPts->fStartSet = true;
+ }
+ if (!quadPts->fEndSet) {
+ SkPoint conicEndPt;
+ if (!this->conicPerpRay(conic, quadPts->fEndT, &conicEndPt, &quadPts->fQuad[2],
+ &quadPts->fTangentEnd)) {
+ return false;
+ }
+ quadPts->fEndSet = true;
+ }
+ return true;
+}
+
+
// Given a cubic and t, return the point on curve, its perpendicular, and the perpendicular tangent.
// Returns false if the perpendicular could not be computed (because the derivative collapsed to 0)
bool SkPathStroker::cubicPerpRay(const SkPoint cubic[4], SkScalar t, SkPoint* tPt, SkPoint* onPt,
@@ -930,10 +1034,6 @@
// are small, a straight line is good enough
SkScalar dist1 = pt_to_line(start, end, quadPts->fTangentEnd);
SkScalar dist2 = pt_to_line(end, start, quadPts->fTangentStart);
- if (SkTMax(dist1, dist2) <= fError * fError) {
- return STROKER_RESULT(kDegenerate_ResultType, depth, quadPts,
- "SkTMax(dist1=%g, dist2=%g) <= fError * fError", dist1, dist2);
- }
if ((numerA >= 0) != (numerB >= 0)) {
if (kCtrlPt_RayType == intersectRayType) {
numerA /= denom;
@@ -944,6 +1044,10 @@
return STROKER_RESULT(kQuad_ResultType, depth, quadPts,
"(numerA=%g >= 0) != (numerB=%g >= 0)", numerA, numerB);
}
+ if (SkTMax(dist1, dist2) <= fInvResScaleSquared) {
+ return STROKER_RESULT(kDegenerate_ResultType, depth, quadPts,
+ "SkTMax(dist1=%g, dist2=%g) <= fInvResScaleSquared", dist1, dist2);
+ }
return STROKER_RESULT(kSplit_ResultType, depth, quadPts,
"(numerA=%g >= 0) == (numerB=%g >= 0)", numerA, numerB);
} else { // if the lines are parallel, straight line is good enough
@@ -979,19 +1083,19 @@
// Return true if the point is close to the bounds of the quad. This is used as a quick reject.
bool SkPathStroker::ptInQuadBounds(const SkPoint quad[3], const SkPoint& pt) const {
SkScalar xMin = SkTMin(SkTMin(quad[0].fX, quad[1].fX), quad[2].fX);
- if (pt.fX + fError < xMin) {
+ if (pt.fX + fInvResScale < xMin) {
return false;
}
SkScalar xMax = SkTMax(SkTMax(quad[0].fX, quad[1].fX), quad[2].fX);
- if (pt.fX - fError > xMax) {
+ if (pt.fX - fInvResScale > xMax) {
return false;
}
SkScalar yMin = SkTMin(SkTMin(quad[0].fY, quad[1].fY), quad[2].fY);
- if (pt.fY + fError < yMin) {
+ if (pt.fY + fInvResScale < yMin) {
return false;
}
SkScalar yMax = SkTMax(SkTMax(quad[0].fY, quad[1].fY), quad[2].fY);
- if (pt.fY - fError > yMax) {
+ if (pt.fY - fInvResScale > yMax) {
return false;
}
return true;
@@ -1022,7 +1126,7 @@
SkPoint strokeMid;
SkEvalQuadAt(stroke, SK_ScalarHalf, &strokeMid);
// measure the distance from the curve to the quad-stroke midpoint, compare to radius
- if (points_within_dist(ray[0], strokeMid, fError)) { // if the difference is small
+ if (points_within_dist(ray[0], strokeMid, fInvResScaleSquared)) { // if the difference is small
if (sharp_angle(quadPts->fQuad)) {
return STROKER_RESULT(kSplit_ResultType, depth, quadPts,
"sharp_angle (1) =%g,%g, %g,%g, %g,%g",
@@ -1031,8 +1135,8 @@
quadPts->fQuad[2].fX, quadPts->fQuad[2].fY);
}
return STROKER_RESULT(kQuad_ResultType, depth, quadPts,
- "points_within_dist(ray[0]=%g,%g, strokeMid=%g,%g, fError)",
- ray[0].fX, ray[0].fY, strokeMid.fX, strokeMid.fY);
+ "points_within_dist(ray[0]=%g,%g, strokeMid=%g,%g, fInvResScaleSquared=%g)",
+ ray[0].fX, ray[0].fY, strokeMid.fX, strokeMid.fY, fInvResScaleSquared);
}
// measure the distance to quad's bounds (quick reject)
// an alternative : look for point in triangle
@@ -1051,7 +1155,7 @@
}
SkPoint quadPt;
SkEvalQuadAt(stroke, roots[0], &quadPt);
- SkScalar error = fError * (SK_Scalar1 - SkScalarAbs(roots[0] - 0.5f) * 2);
+ SkScalar error = fInvResScale * (SK_Scalar1 - SkScalarAbs(roots[0] - 0.5f) * 2);
if (points_within_dist(ray[0], quadPt, error)) { // if the difference is small, we're done
if (sharp_angle(quadPts->fQuad)) {
return STROKER_RESULT(kSplit_ResultType, depth, quadPts,
@@ -1080,14 +1184,32 @@
return resultType;
}
// project a ray from the curve to the stroke
- SkPoint ray[2]; // point near midpoint on quad, midpoint on cubic
+ SkPoint ray[2]; // points near midpoint on quad, midpoint on cubic
if (!this->cubicPerpRay(cubic, quadPts->fMidT, &ray[1], &ray[0], NULL)) {
return kNormalError_ResultType;
}
return strokeCloseEnough(quadPts->fQuad, ray, quadPts STROKER_DEBUG_PARAMS(fRecursionDepth));
}
-// if false is returned, caller splits quadratic approximation
+SkPathStroker::ResultType SkPathStroker::compareQuadConic(const SkConic& conic,
+ SkQuadConstruct* quadPts) {
+ // get the quadratic approximation of the stroke
+ if (!this->conicQuadEnds(conic, quadPts)) {
+ return kNormalError_ResultType;
+ }
+ ResultType resultType = intersectRay(quadPts, kCtrlPt_RayType
+ STROKER_DEBUG_PARAMS(fRecursionDepth) );
+ if (resultType != kQuad_ResultType) {
+ return resultType;
+ }
+ // project a ray from the curve to the stroke
+ SkPoint ray[2]; // points near midpoint on quad, midpoint on conic
+ if (!this->conicPerpRay(conic, quadPts->fMidT, &ray[1], &ray[0], NULL)) {
+ return kNormalError_ResultType;
+ }
+ return strokeCloseEnough(quadPts->fQuad, ray, quadPts STROKER_DEBUG_PARAMS(fRecursionDepth));
+}
+
SkPathStroker::ResultType SkPathStroker::compareQuadQuad(const SkPoint quad[3],
SkQuadConstruct* quadPts) {
// get the quadratic approximation of the stroke
@@ -1126,7 +1248,7 @@
return false;
}
SkScalar dist = pt_to_line(strokeMid, quadPts->fQuad[0], quadPts->fQuad[2]);
- return dist < fError * fError;
+ return dist < fInvResScaleSquared;
}
bool SkPathStroker::cubicStroke(const SkPoint cubic[4], SkQuadConstruct* quadPts) {
@@ -1137,8 +1259,8 @@
return false;
}
if ((kDegenerate_ResultType == resultType
- || points_within_dist(quadPts->fQuad[0], quadPts->fQuad[2], fError))
- && cubicMidOnLine(cubic, quadPts)) {
+ || points_within_dist(quadPts->fQuad[0], quadPts->fQuad[2],
+ fInvResScaleSquared)) && cubicMidOnLine(cubic, quadPts)) {
addDegenerateLine(quadPts);
return true;
}
@@ -1189,6 +1311,36 @@
return true;
}
+bool SkPathStroker::conicStroke(const SkConic& conic, SkQuadConstruct* quadPts) {
+ ResultType resultType = this->compareQuadConic(conic, quadPts);
+ if (kQuad_ResultType == resultType) {
+ const SkPoint* stroke = quadPts->fQuad;
+ SkPath* path = fStrokeType == kOuter_StrokeType ? &fOuter : &fInner;
+ path->quadTo(stroke[1].fX, stroke[1].fY, stroke[2].fX, stroke[2].fY);
+ return true;
+ }
+ if (kDegenerate_ResultType == resultType) {
+ addDegenerateLine(quadPts);
+ return true;
+ }
+ SkDEBUGCODE(gMaxRecursion[kConic_RecursiveLimit] = SkTMax(gMaxRecursion[kConic_RecursiveLimit],
+ fRecursionDepth + 1));
+ if (++fRecursionDepth > kRecursiveLimits[kConic_RecursiveLimit]) {
+ return false; // just abort if projected quad isn't representable
+ }
+ SkQuadConstruct half;
+ (void) half.initWithStart(quadPts);
+ if (!this->conicStroke(conic, &half)) {
+ return false;
+ }
+ (void) half.initWithEnd(quadPts);
+ if (!this->conicStroke(conic, &half)) {
+ return false;
+ }
+ --fRecursionDepth;
+ return true;
+}
+
bool SkPathStroker::quadStroke(const SkPoint quad[3], SkQuadConstruct* quadPts) {
ResultType resultType = this->compareQuadQuad(quad, quadPts);
if (kQuad_ResultType == resultType) {
@@ -1223,7 +1375,7 @@
void SkPathStroker::cubicTo(const SkPoint& pt1, const SkPoint& pt2,
const SkPoint& pt3) {
-#if QUAD_STROKE_APPROXIMATION
+#ifdef SK_QUAD_STROKE_APPROXIMATION
const SkPoint cubic[4] = { fPrevPt, pt1, pt2, pt3 };
SkPoint reduction[3];
const SkPoint* tangentPt;
@@ -1349,13 +1501,6 @@
fDoFill = SkToU8(p.getStyle() == SkPaint::kStrokeAndFill_Style);
}
-#if QUAD_STROKE_APPROXIMATION
-void SkStroke::setError(SkScalar error) {
- SkASSERT(error > 0);
- fError = error;
-}
-#endif
-
void SkStroke::setWidth(SkScalar width) {
SkASSERT(width >= 0);
fWidth = width;
@@ -1432,14 +1577,10 @@
}
SkAutoConicToQuads converter;
+#ifndef SK_QUAD_STROKE_APPROXIMATION
const SkScalar conicTol = SK_Scalar1 / 4 / fResScale;
-
-#if QUAD_STROKE_APPROXIMATION
- SkPathStroker stroker(src, radius, fMiterLimit, fError, this->getCap(),
- this->getJoin(), fResScale);
-#else
- SkPathStroker stroker(src, radius, fMiterLimit, this->getCap(), this->getJoin(), fResScale);
#endif
+ SkPathStroker stroker(src, radius, fMiterLimit, this->getCap(), this->getJoin(), fResScale);
SkPath::Iter iter(src, false);
SkPath::Verb lastSegment = SkPath::kMove_Verb;
@@ -1458,6 +1599,11 @@
lastSegment = SkPath::kQuad_Verb;
break;
case SkPath::kConic_Verb: {
+#ifdef SK_QUAD_STROKE_APPROXIMATION
+ stroker.conicTo(pts[1], pts[2], iter.conicWeight());
+ lastSegment = SkPath::kConic_Verb;
+ break;
+#else
// todo: if we had maxcurvature for conics, perhaps we should
// natively extrude the conic instead of converting to quads.
const SkPoint* quadPts =
@@ -1467,6 +1613,7 @@
quadPts += 2;
}
lastSegment = SkPath::kQuad_Verb;
+#endif
} break;
case SkPath::kCubic_Verb:
stroker.cubicTo(pts[1], pts[2], pts[3]);
diff --git a/src/core/SkStroke.h b/src/core/SkStroke.h
index a5446a4..0574f38 100644
--- a/src/core/SkStroke.h
+++ b/src/core/SkStroke.h
@@ -13,15 +13,9 @@
#include "SkPaint.h"
#include "SkStrokerPriv.h"
-// set to 1 to use experimental outset stroking with quads
-#ifndef QUAD_STROKE_APPROXIMATION
-#define QUAD_STROKE_APPROXIMATION 0
-#endif
-
-#if QUAD_STROKE_APPROXIMATION && defined(SK_DEBUG)
+#if defined SK_QUAD_STROKE_APPROXIMATION && defined SK_DEBUG
extern bool gDebugStrokerErrorSet;
extern SkScalar gDebugStrokerError;
-
extern int gMaxRecursion[];
#endif
@@ -43,9 +37,6 @@
SkPaint::Join getJoin() const { return (SkPaint::Join)fJoin; }
void setJoin(SkPaint::Join);
-#if QUAD_STROKE_APPROXIMATION
- void setError(SkScalar);
-#endif
void setMiterLimit(SkScalar);
void setWidth(SkScalar);
@@ -76,9 +67,6 @@
////////////////////////////////////////////////////////////////
private:
-#if QUAD_STROKE_APPROXIMATION
- SkScalar fError;
-#endif
SkScalar fWidth, fMiterLimit;
SkScalar fResScale;
uint8_t fCap, fJoin;
diff --git a/src/core/SkStrokeRec.cpp b/src/core/SkStrokeRec.cpp
index c25a652..2ca7efd 100644
--- a/src/core/SkStrokeRec.cpp
+++ b/src/core/SkStrokeRec.cpp
@@ -100,6 +100,12 @@
#include "SkStroke.h"
+#if defined SK_QUAD_STROKE_APPROXIMATION && defined SK_DEBUG
+ // enables tweaking these values at runtime from SampleApp
+ bool gDebugStrokerErrorSet = false;
+ SkScalar gDebugStrokerError;
+#endif
+
bool SkStrokeRec::applyToPath(SkPath* dst, const SkPath& src) const {
if (fWidth <= 0) { // hairline or fill
return false;
@@ -111,9 +117,10 @@
stroker.setMiterLimit(fMiterLimit);
stroker.setWidth(fWidth);
stroker.setDoFill(fStrokeAndFill);
+#if defined SK_QUAD_STROKE_APPROXIMATION && defined SK_DEBUG
+ stroker.setResScale(gDebugStrokerErrorSet ? gDebugStrokerError : fResScale);
+#else
stroker.setResScale(fResScale);
-#if QUAD_STROKE_APPROXIMATION
- stroker.setError(1);
#endif
stroker.strokePath(src, dst);
return true;