blob: eb35189019e25631e5177761025e119a0e694375 [file] [log] [blame]
Tyler Denniston324578b2020-07-17 14:03:42 -04001/*
2 * Copyright 2020 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "include/core/SkBitmap.h"
9#include "include/core/SkCanvas.h"
10#include "include/core/SkPath.h"
11#include "include/utils/SkParsePath.h"
12#include "samplecode/Sample.h"
13
14#include "src/core/SkGeometry.h"
15
16namespace {
17
18//////////////////////////////////////////////////////////////////////////////
19
20static SkPoint rotate90(const SkPoint& p) { return {-p.fY, p.fX}; }
21static SkPoint rotate180(const SkPoint& p) { return p * -1; }
22static SkPoint setLength(SkPoint p, float len) {
23 if (!p.setLength(len)) {
24 SkDebugf("Failed to set point length\n");
25 }
26 return p;
27}
28static bool isClockwise(const SkPoint& a, const SkPoint& b) { return a.cross(b) > 0; }
29
30//////////////////////////////////////////////////////////////////////////////
31
32// Testing ground for a new stroker implementation
33class SkPathStroker2 {
34public:
35 // Returns the fill path
36 SkPath getFillPath(const SkPath& path, const SkPaint& paint);
37
38private:
39 struct PathSegment {
40 SkPath::Verb fVerb;
41 SkPoint fPoints[4];
42 };
43
44 float fRadius;
45 SkPaint::Cap fCap;
46 SkPaint::Join fJoin;
47 SkPath fInnerPath, fOuterPath;
48 SkPath *fInner = &fInnerPath, *fOuter = &fOuterPath;
49
50 // Initialize stroker state
51 void initForPath(const SkPath& path, const SkPaint& paint);
52
53 // Strokes a line segment
54 void strokeLine(const PathSegment& line, bool needsMove);
55
56 // Adds an endcap to fOuter
57 enum class CapLocation { Start, End };
58 void endcap(CapLocation loc);
59
60 // Adds a join between the two segments
61 void join(const PathSegment& prev, const PathSegment& curr);
62
63 // Appends path in reverse to result
64 static void appendPathReversed(const SkPath* path, SkPath* result);
65
66 // Returns the segment unit normal
67 static SkPoint unitNormal(const PathSegment& seg, float t);
68};
69
70void SkPathStroker2::initForPath(const SkPath& path, const SkPaint& paint) {
71 fRadius = paint.getStrokeWidth() / 2;
72 fCap = paint.getStrokeCap();
73 fJoin = paint.getStrokeJoin();
74 fInnerPath.rewind();
75 fOuterPath.rewind();
76 fInner = &fInnerPath;
77 fOuter = &fOuterPath;
78}
79
80SkPath SkPathStroker2::getFillPath(const SkPath& path, const SkPaint& paint) {
81 initForPath(path, paint);
82
83 // Trace the inner and outer paths simultaneously. Inner will therefore be
84 // recorded in reverse from how we trace the outline.
85 SkPath::Iter it(path, false);
86 PathSegment segment, prevSegment;
87 bool firstSegment = true;
88 while ((segment.fVerb = it.next(segment.fPoints)) != SkPath::kDone_Verb) {
89 // Join to the previous segment
90 if (!firstSegment) {
91 join(prevSegment, segment);
92 }
93
94 // Stroke the current segment
95 switch (segment.fVerb) {
96 case SkPath::kLine_Verb:
97 strokeLine(segment, firstSegment);
98 break;
99 case SkPath::kMove_Verb:
100 // Don't care about multiple contours currently
101 continue;
102 default:
103 SkDebugf("Unhandled path verb %d\n", segment.fVerb);
104 break;
105 }
106
107 std::swap(segment, prevSegment);
108 firstSegment = false;
109 }
110
111 // Open contour => endcap at the end
112 const bool isClosed = path.isLastContourClosed();
113 if (isClosed) {
114 SkDebugf("Unhandled closed contour\n");
115 } else {
116 endcap(CapLocation::End);
117 }
118
119 // Walk inner path in reverse, appending to result
120 appendPathReversed(fInner, fOuter);
121 endcap(CapLocation::Start);
122
123 return fOuterPath;
124}
125
126void SkPathStroker2::strokeLine(const PathSegment& line, bool needsMove) {
127 const SkPoint tangent = line.fPoints[1] - line.fPoints[0];
128 const SkPoint normal = rotate90(tangent);
129 const SkPoint offset = setLength(normal, fRadius);
130 if (needsMove) {
131 fOuter->moveTo(line.fPoints[0] + offset);
132 fInner->moveTo(line.fPoints[0] - offset);
133 }
134 fOuter->lineTo(line.fPoints[1] + offset);
135 fInner->lineTo(line.fPoints[1] - offset);
136}
137
138void SkPathStroker2::endcap(CapLocation loc) {
139 const auto buttCap = [this](CapLocation loc) {
140 if (loc == CapLocation::Start) {
141 // Back at the start of the path: just close the stroked outline
142 fOuter->close();
143 } else {
144 // Inner last pt == first pt when appending in reverse
145 SkPoint innerLastPt;
146 fInner->getLastPt(&innerLastPt);
147 fOuter->lineTo(innerLastPt);
148 }
149 };
150
151 switch (fCap) {
152 case SkPaint::kButt_Cap:
153 buttCap(loc);
154 break;
155 default:
156 SkDebugf("Unhandled endcap %d\n", fCap);
157 buttCap(loc);
158 break;
159 }
160}
161
162void SkPathStroker2::join(const PathSegment& prev, const PathSegment& curr) {
163 const auto miterJoin = [this](const PathSegment& prev, const PathSegment& curr) {
164 const SkPoint miterMidpt = curr.fPoints[0];
165 SkPoint before = unitNormal(prev, 1);
166 SkPoint after = unitNormal(curr, 0);
167
168 // Check who's inside and who's outside.
169 SkPath *outer = fOuter, *inner = fInner;
170 if (!isClockwise(before, after)) {
171 std::swap(inner, outer);
172 before = rotate180(before);
173 after = rotate180(after);
174 }
175
176 const float cosTheta = before.dot(after);
177 if (SkScalarNearlyZero(1 - cosTheta)) {
178 // Nearly identical normals: don't bother.
179 return;
180 }
181
182 // Before and after have the same origin and magnitude, so before+after is the diagonal of
183 // their rhombus. Origin of this vector is the midpoint of the miter line.
184 SkPoint miterVec = before + after;
185
186 // Note the relationship (draw a right triangle with the miter line as its hypoteneuse):
187 // sin(theta/2) = strokeWidth / miterLength
188 // so miterLength = strokeWidth / sin(theta/2)
189 // where miterLength is the length of the miter from outer point to inner corner.
190 // miterVec's origin is the midpoint of the miter line, so we use strokeWidth/2.
191 // Sqrt is just an application of half-angle identities.
192 const float sinHalfTheta = sqrtf(0.5 * (1 + cosTheta));
193 const float halfMiterLength = fRadius / sinHalfTheta;
194 miterVec.setLength(halfMiterLength); // TODO: miter length limit
195
196 outer->lineTo(miterMidpt + miterVec);
197 inner->lineTo(miterMidpt - miterVec);
198 };
199
200 switch (fJoin) {
201 case SkPaint::kMiter_Join:
202 miterJoin(prev, curr);
203 break;
204 default:
205 SkDebugf("Unhandled join %d\n", fJoin);
206 miterJoin(prev, curr);
207 break;
208 }
209}
210
211void SkPathStroker2::appendPathReversed(const SkPath* path, SkPath* result) {
212 const int numVerbs = path->countVerbs();
213 const int numPoints = path->countPoints();
214 std::unique_ptr<uint8_t[]> verbs = std::make_unique<uint8_t[]>(numVerbs);
215 std::unique_ptr<SkPoint[]> points = std::make_unique<SkPoint[]>(numPoints);
216
217 path->getVerbs(verbs.get(), numVerbs);
218 path->getPoints(points.get(), numPoints);
219
220 for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) {
221 auto verb = static_cast<SkPath::Verb>(verbs[i]);
222 switch (verb) {
223 case SkPath::kLine_Verb: {
224 j -= 1;
225 SkASSERT(j >= 1);
226 result->lineTo(points[j - 1]);
227 break;
228 }
229 case SkPath::kMove_Verb:
230 // Ignore
231 break;
232 default:
233 SkASSERT(false);
234 break;
235 }
236 }
237}
238
239SkPoint SkPathStroker2::unitNormal(const PathSegment& seg, float t) {
240 if (seg.fVerb != SkPath::kLine_Verb) {
241 SkDebugf("Unhandled verb for unit normal %d\n", seg.fVerb);
242 }
243
244 (void)t; // Not needed for lines
245 const SkPoint tangent = seg.fPoints[1] - seg.fPoints[0];
246 const SkPoint normal = rotate90(tangent);
247 return setLength(normal, 1);
248}
249
250} // namespace
251
252//////////////////////////////////////////////////////////////////////////////
253
254class SimpleStroker : public Sample {
255 bool fShowSkiaStroke, fShowHidden;
256 float fWidth = 175;
257 SkPaint fPtsPaint, fStrokePaint, fNewFillPaint, fHiddenPaint;
258 static constexpr int kN = 3;
259
260public:
261 SkPoint fPts[kN];
262
263 SimpleStroker() : fShowSkiaStroke(true), fShowHidden(false) {
264 fPts[0] = {500, 200};
265 fPts[1] = {300, 200};
266 fPts[2] = {100, 100};
267
268 fPtsPaint.setAntiAlias(true);
269 fPtsPaint.setStrokeWidth(10);
270 fPtsPaint.setStrokeCap(SkPaint::kRound_Cap);
271
272 fStrokePaint.setAntiAlias(true);
273 fStrokePaint.setStyle(SkPaint::kStroke_Style);
274 fStrokePaint.setColor(0x80FF0000);
275
276 fNewFillPaint.setAntiAlias(true);
277 fNewFillPaint.setColor(0x8000FF00);
278
279 fHiddenPaint.setAntiAlias(true);
280 fHiddenPaint.setStyle(SkPaint::kStroke_Style);
281 fHiddenPaint.setColor(0xFF0000FF);
282 }
283
284 void toggle(bool& value) { value = !value; }
285
286protected:
287 SkString name() override { return SkString("SimpleStroker"); }
288
289 bool onChar(SkUnichar uni) override {
290 switch (uni) {
291 case '1':
292 this->toggle(fShowSkiaStroke);
293 return true;
294 case '2':
295 this->toggle(fShowHidden);
296 return true;
297 case '-':
298 fWidth -= 5;
299 return true;
300 case '=':
301 fWidth += 5;
302 return true;
303 default:
304 break;
305 }
306 return false;
307 }
308
309 void makePath(SkPath* path) {
310 path->moveTo(fPts[0]);
311 for (int i = 1; i < kN; ++i) {
312 path->lineTo(fPts[i]);
313 }
314 }
315
316 void onDrawContent(SkCanvas* canvas) override {
317 canvas->drawColor(0xFFEEEEEE);
318
319 SkPath path;
320 this->makePath(&path);
321
322 fStrokePaint.setStrokeWidth(fWidth);
323
324 // The correct result
325 if (fShowSkiaStroke) {
326 canvas->drawPath(path, fStrokePaint);
327 }
328
329 // Simple stroker result
330 SkPathStroker2 stroker;
331 SkPath fillPath = stroker.getFillPath(path, fStrokePaint);
332 canvas->drawPath(fillPath, fNewFillPaint);
333
334 if (fShowHidden) {
335 canvas->drawPath(fillPath, fHiddenPaint);
336 }
337
338 canvas->drawPoints(SkCanvas::kPoints_PointMode, kN, fPts, fPtsPaint);
339 }
340
341 Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override {
342 const SkScalar tol = 4;
343 const SkRect r = SkRect::MakeXYWH(x - tol, y - tol, tol * 2, tol * 2);
344 for (int i = 0; i < kN; ++i) {
345 if (r.intersects(SkRect::MakeXYWH(fPts[i].fX, fPts[i].fY, 1, 1))) {
346 return new Click([this, i](Click* c) {
347 fPts[i] = c->fCurr;
348 return true;
349 });
350 }
351 }
352 return nullptr;
353 }
354
355private:
356 typedef Sample INHERITED;
357};
358
359DEF_SAMPLE(return new SimpleStroker;)