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