blob: 28c3b24504e3742a9f0af5e687b93cdfed4e35d7 [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
Tyler Dennistonf219aad2020-07-21 16:16:55 -040020static SkPoint rotate90(const SkPoint& p) { return {p.fY, -p.fX}; }
Tyler Denniston324578b2020-07-17 14:03:42 -040021static 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//////////////////////////////////////////////////////////////////////////////
Tyler Denniston324578b2020-07-17 14:03:42 -040031// Testing ground for a new stroker implementation
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -040032
33/** Helper class for constructing paths, with undo support */
34class PathRecorder {
35public:
36 SkPath getPath() const {
37 return SkPath::Make(fPoints.data(), fPoints.size(), fVerbs.data(), fVerbs.size(), nullptr,
38 0, SkPathFillType::kWinding);
39 }
40
41 void moveTo(SkPoint p) {
42 fVerbs.push_back(SkPath::kMove_Verb);
43 fPoints.push_back(p);
44 }
45
46 void lineTo(SkPoint p) {
47 fVerbs.push_back(SkPath::kLine_Verb);
48 fPoints.push_back(p);
49 }
50
51 void close() { fVerbs.push_back(SkPath::kClose_Verb); }
52
53 void rewind() {
54 fVerbs.clear();
55 fPoints.clear();
56 }
57
58 int countPoints() const { return fPoints.size(); }
59
60 int countVerbs() const { return fVerbs.size(); }
61
62 bool getLastPt(SkPoint* lastPt) const {
63 if (fPoints.empty()) {
64 return false;
65 }
66 *lastPt = fPoints.back();
67 return true;
68 }
69
70 void setLastPt(SkPoint lastPt) {
71 if (fPoints.empty()) {
72 moveTo(lastPt);
73 } else {
74 fPoints.back().set(lastPt.fX, lastPt.fY);
75 }
76 }
77
78 const std::vector<uint8_t>& verbs() const { return fVerbs; }
79
80 const std::vector<SkPoint>& points() const { return fPoints; }
81
82private:
83 std::vector<uint8_t> fVerbs;
84 std::vector<SkPoint> fPoints;
85};
86
Tyler Denniston324578b2020-07-17 14:03:42 -040087class SkPathStroker2 {
88public:
89 // Returns the fill path
90 SkPath getFillPath(const SkPath& path, const SkPaint& paint);
91
92private:
93 struct PathSegment {
94 SkPath::Verb fVerb;
95 SkPoint fPoints[4];
96 };
97
98 float fRadius;
99 SkPaint::Cap fCap;
100 SkPaint::Join fJoin;
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400101 PathRecorder fInner, fOuter;
Tyler Denniston324578b2020-07-17 14:03:42 -0400102
103 // Initialize stroker state
104 void initForPath(const SkPath& path, const SkPaint& paint);
105
106 // Strokes a line segment
107 void strokeLine(const PathSegment& line, bool needsMove);
108
109 // Adds an endcap to fOuter
110 enum class CapLocation { Start, End };
111 void endcap(CapLocation loc);
112
113 // Adds a join between the two segments
114 void join(const PathSegment& prev, const PathSegment& curr);
115
116 // Appends path in reverse to result
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400117 static void appendPathReversed(const PathRecorder& path, PathRecorder* result);
Tyler Denniston324578b2020-07-17 14:03:42 -0400118
119 // Returns the segment unit normal
120 static SkPoint unitNormal(const PathSegment& seg, float t);
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400121
122 // Returns squared magnitude of line segments.
123 static float squaredLineLength(const PathSegment& lineSeg);
Tyler Denniston324578b2020-07-17 14:03:42 -0400124};
125
126void SkPathStroker2::initForPath(const SkPath& path, const SkPaint& paint) {
127 fRadius = paint.getStrokeWidth() / 2;
128 fCap = paint.getStrokeCap();
129 fJoin = paint.getStrokeJoin();
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400130 fInner.rewind();
131 fOuter.rewind();
Tyler Denniston324578b2020-07-17 14:03:42 -0400132}
133
134SkPath SkPathStroker2::getFillPath(const SkPath& path, const SkPaint& paint) {
135 initForPath(path, paint);
136
137 // Trace the inner and outer paths simultaneously. Inner will therefore be
138 // recorded in reverse from how we trace the outline.
139 SkPath::Iter it(path, false);
140 PathSegment segment, prevSegment;
141 bool firstSegment = true;
142 while ((segment.fVerb = it.next(segment.fPoints)) != SkPath::kDone_Verb) {
143 // Join to the previous segment
144 if (!firstSegment) {
145 join(prevSegment, segment);
146 }
147
148 // Stroke the current segment
149 switch (segment.fVerb) {
150 case SkPath::kLine_Verb:
151 strokeLine(segment, firstSegment);
152 break;
153 case SkPath::kMove_Verb:
154 // Don't care about multiple contours currently
155 continue;
156 default:
157 SkDebugf("Unhandled path verb %d\n", segment.fVerb);
158 break;
159 }
160
161 std::swap(segment, prevSegment);
162 firstSegment = false;
163 }
164
165 // Open contour => endcap at the end
166 const bool isClosed = path.isLastContourClosed();
167 if (isClosed) {
168 SkDebugf("Unhandled closed contour\n");
169 } else {
170 endcap(CapLocation::End);
171 }
172
173 // Walk inner path in reverse, appending to result
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400174 appendPathReversed(fInner, &fOuter);
Tyler Denniston324578b2020-07-17 14:03:42 -0400175 endcap(CapLocation::Start);
176
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400177 return fOuter.getPath();
Tyler Denniston324578b2020-07-17 14:03:42 -0400178}
179
180void SkPathStroker2::strokeLine(const PathSegment& line, bool needsMove) {
181 const SkPoint tangent = line.fPoints[1] - line.fPoints[0];
182 const SkPoint normal = rotate90(tangent);
183 const SkPoint offset = setLength(normal, fRadius);
184 if (needsMove) {
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400185 fOuter.moveTo(line.fPoints[0] + offset);
186 fInner.moveTo(line.fPoints[0] - offset);
Tyler Denniston324578b2020-07-17 14:03:42 -0400187 }
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400188 fOuter.lineTo(line.fPoints[1] + offset);
189 fInner.lineTo(line.fPoints[1] - offset);
Tyler Denniston324578b2020-07-17 14:03:42 -0400190}
191
192void SkPathStroker2::endcap(CapLocation loc) {
193 const auto buttCap = [this](CapLocation loc) {
194 if (loc == CapLocation::Start) {
195 // Back at the start of the path: just close the stroked outline
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400196 fOuter.close();
Tyler Denniston324578b2020-07-17 14:03:42 -0400197 } else {
198 // Inner last pt == first pt when appending in reverse
199 SkPoint innerLastPt;
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400200 fInner.getLastPt(&innerLastPt);
201 fOuter.lineTo(innerLastPt);
Tyler Denniston324578b2020-07-17 14:03:42 -0400202 }
203 };
204
205 switch (fCap) {
206 case SkPaint::kButt_Cap:
207 buttCap(loc);
208 break;
209 default:
210 SkDebugf("Unhandled endcap %d\n", fCap);
211 buttCap(loc);
212 break;
213 }
214}
215
216void SkPathStroker2::join(const PathSegment& prev, const PathSegment& curr) {
217 const auto miterJoin = [this](const PathSegment& prev, const PathSegment& curr) {
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400218 // Common path endpoint of the two segments is the midpoint of the miter line.
Tyler Denniston324578b2020-07-17 14:03:42 -0400219 const SkPoint miterMidpt = curr.fPoints[0];
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400220
Tyler Denniston324578b2020-07-17 14:03:42 -0400221 SkPoint before = unitNormal(prev, 1);
222 SkPoint after = unitNormal(curr, 0);
223
224 // Check who's inside and who's outside.
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400225 PathRecorder *outer = &fOuter, *inner = &fInner;
Tyler Denniston324578b2020-07-17 14:03:42 -0400226 if (!isClockwise(before, after)) {
227 std::swap(inner, outer);
228 before = rotate180(before);
229 after = rotate180(after);
230 }
231
232 const float cosTheta = before.dot(after);
233 if (SkScalarNearlyZero(1 - cosTheta)) {
234 // Nearly identical normals: don't bother.
235 return;
236 }
237
238 // Before and after have the same origin and magnitude, so before+after is the diagonal of
239 // their rhombus. Origin of this vector is the midpoint of the miter line.
240 SkPoint miterVec = before + after;
241
242 // Note the relationship (draw a right triangle with the miter line as its hypoteneuse):
243 // sin(theta/2) = strokeWidth / miterLength
244 // so miterLength = strokeWidth / sin(theta/2)
245 // where miterLength is the length of the miter from outer point to inner corner.
246 // miterVec's origin is the midpoint of the miter line, so we use strokeWidth/2.
247 // Sqrt is just an application of half-angle identities.
248 const float sinHalfTheta = sqrtf(0.5 * (1 + cosTheta));
249 const float halfMiterLength = fRadius / sinHalfTheta;
250 miterVec.setLength(halfMiterLength); // TODO: miter length limit
251
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400252 // Outer: connect to the miter point, and then to t=0 (on outside stroke) of next segment.
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400253 const SkPoint dest = setLength(after, fRadius);
Tyler Denniston324578b2020-07-17 14:03:42 -0400254 outer->lineTo(miterMidpt + miterVec);
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400255 outer->lineTo(miterMidpt + dest);
256
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400257 // Inner miter is more involved. We're already at t=1 (on inside stroke) of 'prev'.
258 // Check 2 cases to see we can directly connect to the inner miter point
259 // (midpoint - miterVec), or if we need to add extra "loop" geometry.
260 const SkPoint prevUnitTangent = rotate90(before);
261 const float radiusSquared = fRadius * fRadius;
262 // 'alpha' is angle between prev tangent and the curr inwards normal
263 const float cosAlpha = prevUnitTangent.dot(-after);
264 // Solve triangle for len^2: radius^2 = len^2 + (radius * sin(alpha))^2
265 // This is the point at which the inside "corner" of curr at t=0 will lie on a
266 // line connecting the inner and outer corners of prev at t=0. If len is below
267 // this threshold, the inside corner of curr will "poke through" the start of prev,
268 // and we'll need the inner loop geometry.
269 const float threshold1 = radiusSquared * cosAlpha * cosAlpha;
270 // Solve triangle for len^2: halfMiterLen^2 = radius^2 + len^2
271 // This is the point at which the inner miter point will lie on the inner stroke
272 // boundary of the curr segment. If len is below this threshold, the miter point
273 // moves 'inside' of the stroked outline, and we'll need the inner loop geometry.
274 const float threshold2 = halfMiterLength * halfMiterLength - radiusSquared;
275 // If a segment length is smaller than the larger of the two thresholds,
276 // we'll have to add the inner loop geometry.
277 const float maxLenSqd = std::max(threshold1, threshold2);
278 const bool needsInnerLoop =
279 squaredLineLength(prev) < maxLenSqd || squaredLineLength(curr) < maxLenSqd;
280 if (needsInnerLoop) {
281 // Connect to the miter midpoint (common path endpoint of the two segments),
282 // and then to t=0 (on inside) of the next segment. This adds an interior "loop" of
283 // geometry that handles edge cases where segment lengths are shorter than the
284 // stroke width.
285 inner->lineTo(miterMidpt);
286 inner->lineTo(miterMidpt - dest);
287 } else {
288 // Directly connect to inner miter point.
289 inner->setLastPt(miterMidpt - miterVec);
290 }
Tyler Denniston324578b2020-07-17 14:03:42 -0400291 };
292
293 switch (fJoin) {
294 case SkPaint::kMiter_Join:
295 miterJoin(prev, curr);
296 break;
297 default:
298 SkDebugf("Unhandled join %d\n", fJoin);
299 miterJoin(prev, curr);
300 break;
301 }
302}
303
Tyler Dennistonbaa2a7b2020-07-22 13:46:02 -0400304void SkPathStroker2::appendPathReversed(const PathRecorder& path, PathRecorder* result) {
305 const int numVerbs = path.countVerbs();
306 const int numPoints = path.countPoints();
307 const std::vector<uint8_t>& verbs = path.verbs();
308 const std::vector<SkPoint>& points = path.points();
Tyler Denniston324578b2020-07-17 14:03:42 -0400309
310 for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) {
311 auto verb = static_cast<SkPath::Verb>(verbs[i]);
312 switch (verb) {
313 case SkPath::kLine_Verb: {
314 j -= 1;
315 SkASSERT(j >= 1);
316 result->lineTo(points[j - 1]);
317 break;
318 }
319 case SkPath::kMove_Verb:
320 // Ignore
321 break;
322 default:
323 SkASSERT(false);
324 break;
325 }
326 }
327}
328
329SkPoint SkPathStroker2::unitNormal(const PathSegment& seg, float t) {
330 if (seg.fVerb != SkPath::kLine_Verb) {
331 SkDebugf("Unhandled verb for unit normal %d\n", seg.fVerb);
332 }
333
334 (void)t; // Not needed for lines
335 const SkPoint tangent = seg.fPoints[1] - seg.fPoints[0];
336 const SkPoint normal = rotate90(tangent);
337 return setLength(normal, 1);
338}
339
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400340float SkPathStroker2::squaredLineLength(const PathSegment& lineSeg) {
341 SkASSERT(lineSeg.fVerb == SkPath::kLine_Verb);
342 const SkPoint diff = lineSeg.fPoints[1] - lineSeg.fPoints[0];
343 return diff.dot(diff);
344}
345
Tyler Denniston324578b2020-07-17 14:03:42 -0400346} // namespace
347
348//////////////////////////////////////////////////////////////////////////////
349
350class SimpleStroker : public Sample {
351 bool fShowSkiaStroke, fShowHidden;
352 float fWidth = 175;
353 SkPaint fPtsPaint, fStrokePaint, fNewFillPaint, fHiddenPaint;
354 static constexpr int kN = 3;
355
356public:
357 SkPoint fPts[kN];
358
359 SimpleStroker() : fShowSkiaStroke(true), fShowHidden(false) {
360 fPts[0] = {500, 200};
361 fPts[1] = {300, 200};
362 fPts[2] = {100, 100};
363
364 fPtsPaint.setAntiAlias(true);
365 fPtsPaint.setStrokeWidth(10);
366 fPtsPaint.setStrokeCap(SkPaint::kRound_Cap);
367
368 fStrokePaint.setAntiAlias(true);
369 fStrokePaint.setStyle(SkPaint::kStroke_Style);
370 fStrokePaint.setColor(0x80FF0000);
371
372 fNewFillPaint.setAntiAlias(true);
373 fNewFillPaint.setColor(0x8000FF00);
374
375 fHiddenPaint.setAntiAlias(true);
376 fHiddenPaint.setStyle(SkPaint::kStroke_Style);
377 fHiddenPaint.setColor(0xFF0000FF);
378 }
379
380 void toggle(bool& value) { value = !value; }
381
382protected:
383 SkString name() override { return SkString("SimpleStroker"); }
384
385 bool onChar(SkUnichar uni) override {
386 switch (uni) {
387 case '1':
388 this->toggle(fShowSkiaStroke);
389 return true;
390 case '2':
391 this->toggle(fShowHidden);
392 return true;
393 case '-':
394 fWidth -= 5;
395 return true;
396 case '=':
397 fWidth += 5;
398 return true;
399 default:
400 break;
401 }
402 return false;
403 }
404
405 void makePath(SkPath* path) {
406 path->moveTo(fPts[0]);
407 for (int i = 1; i < kN; ++i) {
408 path->lineTo(fPts[i]);
409 }
410 }
411
412 void onDrawContent(SkCanvas* canvas) override {
413 canvas->drawColor(0xFFEEEEEE);
414
415 SkPath path;
416 this->makePath(&path);
417
418 fStrokePaint.setStrokeWidth(fWidth);
419
420 // The correct result
421 if (fShowSkiaStroke) {
422 canvas->drawPath(path, fStrokePaint);
423 }
424
425 // Simple stroker result
426 SkPathStroker2 stroker;
427 SkPath fillPath = stroker.getFillPath(path, fStrokePaint);
428 canvas->drawPath(fillPath, fNewFillPaint);
429
430 if (fShowHidden) {
431 canvas->drawPath(fillPath, fHiddenPaint);
432 }
433
434 canvas->drawPoints(SkCanvas::kPoints_PointMode, kN, fPts, fPtsPaint);
435 }
436
437 Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override {
438 const SkScalar tol = 4;
439 const SkRect r = SkRect::MakeXYWH(x - tol, y - tol, tol * 2, tol * 2);
440 for (int i = 0; i < kN; ++i) {
441 if (r.intersects(SkRect::MakeXYWH(fPts[i].fX, fPts[i].fY, 1, 1))) {
442 return new Click([this, i](Click* c) {
443 fPts[i] = c->fCurr;
444 return true;
445 });
446 }
447 }
448 return nullptr;
449 }
450
451private:
452 typedef Sample INHERITED;
453};
454
455DEF_SAMPLE(return new SimpleStroker;)