blob: f778752a23246d7ee4e0cd2e9700c43cd071010c [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//////////////////////////////////////////////////////////////////////////////
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);
Tyler Dennistone9ced4f2020-07-22 09:29:42 -040068
69 // Returns squared magnitude of line segments.
70 static float squaredLineLength(const PathSegment& lineSeg);
Tyler Denniston324578b2020-07-17 14:03:42 -040071};
72
73void SkPathStroker2::initForPath(const SkPath& path, const SkPaint& paint) {
74 fRadius = paint.getStrokeWidth() / 2;
75 fCap = paint.getStrokeCap();
76 fJoin = paint.getStrokeJoin();
77 fInnerPath.rewind();
78 fOuterPath.rewind();
79 fInner = &fInnerPath;
80 fOuter = &fOuterPath;
81}
82
83SkPath SkPathStroker2::getFillPath(const SkPath& path, const SkPaint& paint) {
84 initForPath(path, paint);
85
86 // Trace the inner and outer paths simultaneously. Inner will therefore be
87 // recorded in reverse from how we trace the outline.
88 SkPath::Iter it(path, false);
89 PathSegment segment, prevSegment;
90 bool firstSegment = true;
91 while ((segment.fVerb = it.next(segment.fPoints)) != SkPath::kDone_Verb) {
92 // Join to the previous segment
93 if (!firstSegment) {
94 join(prevSegment, segment);
95 }
96
97 // Stroke the current segment
98 switch (segment.fVerb) {
99 case SkPath::kLine_Verb:
100 strokeLine(segment, firstSegment);
101 break;
102 case SkPath::kMove_Verb:
103 // Don't care about multiple contours currently
104 continue;
105 default:
106 SkDebugf("Unhandled path verb %d\n", segment.fVerb);
107 break;
108 }
109
110 std::swap(segment, prevSegment);
111 firstSegment = false;
112 }
113
114 // Open contour => endcap at the end
115 const bool isClosed = path.isLastContourClosed();
116 if (isClosed) {
117 SkDebugf("Unhandled closed contour\n");
118 } else {
119 endcap(CapLocation::End);
120 }
121
122 // Walk inner path in reverse, appending to result
123 appendPathReversed(fInner, fOuter);
124 endcap(CapLocation::Start);
125
126 return fOuterPath;
127}
128
129void SkPathStroker2::strokeLine(const PathSegment& line, bool needsMove) {
130 const SkPoint tangent = line.fPoints[1] - line.fPoints[0];
131 const SkPoint normal = rotate90(tangent);
132 const SkPoint offset = setLength(normal, fRadius);
133 if (needsMove) {
134 fOuter->moveTo(line.fPoints[0] + offset);
135 fInner->moveTo(line.fPoints[0] - offset);
136 }
137 fOuter->lineTo(line.fPoints[1] + offset);
138 fInner->lineTo(line.fPoints[1] - offset);
139}
140
141void SkPathStroker2::endcap(CapLocation loc) {
142 const auto buttCap = [this](CapLocation loc) {
143 if (loc == CapLocation::Start) {
144 // Back at the start of the path: just close the stroked outline
145 fOuter->close();
146 } else {
147 // Inner last pt == first pt when appending in reverse
148 SkPoint innerLastPt;
149 fInner->getLastPt(&innerLastPt);
150 fOuter->lineTo(innerLastPt);
151 }
152 };
153
154 switch (fCap) {
155 case SkPaint::kButt_Cap:
156 buttCap(loc);
157 break;
158 default:
159 SkDebugf("Unhandled endcap %d\n", fCap);
160 buttCap(loc);
161 break;
162 }
163}
164
165void SkPathStroker2::join(const PathSegment& prev, const PathSegment& curr) {
166 const auto miterJoin = [this](const PathSegment& prev, const PathSegment& curr) {
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400167 // Common path endpoint of the two segments is the midpoint of the miter line.
Tyler Denniston324578b2020-07-17 14:03:42 -0400168 const SkPoint miterMidpt = curr.fPoints[0];
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400169
Tyler Denniston324578b2020-07-17 14:03:42 -0400170 SkPoint before = unitNormal(prev, 1);
171 SkPoint after = unitNormal(curr, 0);
172
173 // Check who's inside and who's outside.
174 SkPath *outer = fOuter, *inner = fInner;
175 if (!isClockwise(before, after)) {
176 std::swap(inner, outer);
177 before = rotate180(before);
178 after = rotate180(after);
179 }
180
181 const float cosTheta = before.dot(after);
182 if (SkScalarNearlyZero(1 - cosTheta)) {
183 // Nearly identical normals: don't bother.
184 return;
185 }
186
187 // Before and after have the same origin and magnitude, so before+after is the diagonal of
188 // their rhombus. Origin of this vector is the midpoint of the miter line.
189 SkPoint miterVec = before + after;
190
191 // Note the relationship (draw a right triangle with the miter line as its hypoteneuse):
192 // sin(theta/2) = strokeWidth / miterLength
193 // so miterLength = strokeWidth / sin(theta/2)
194 // where miterLength is the length of the miter from outer point to inner corner.
195 // miterVec's origin is the midpoint of the miter line, so we use strokeWidth/2.
196 // Sqrt is just an application of half-angle identities.
197 const float sinHalfTheta = sqrtf(0.5 * (1 + cosTheta));
198 const float halfMiterLength = fRadius / sinHalfTheta;
199 miterVec.setLength(halfMiterLength); // TODO: miter length limit
200
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400201 // Outer: connect to the miter point, and then to t=0 (on outside stroke) of next segment.
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400202 const SkPoint dest = setLength(after, fRadius);
Tyler Denniston324578b2020-07-17 14:03:42 -0400203 outer->lineTo(miterMidpt + miterVec);
Tyler Dennistonf219aad2020-07-21 16:16:55 -0400204 outer->lineTo(miterMidpt + dest);
205
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400206 // Inner miter is more involved. We're already at t=1 (on inside stroke) of 'prev'.
207 // Check 2 cases to see we can directly connect to the inner miter point
208 // (midpoint - miterVec), or if we need to add extra "loop" geometry.
209 const SkPoint prevUnitTangent = rotate90(before);
210 const float radiusSquared = fRadius * fRadius;
211 // 'alpha' is angle between prev tangent and the curr inwards normal
212 const float cosAlpha = prevUnitTangent.dot(-after);
213 // Solve triangle for len^2: radius^2 = len^2 + (radius * sin(alpha))^2
214 // This is the point at which the inside "corner" of curr at t=0 will lie on a
215 // line connecting the inner and outer corners of prev at t=0. If len is below
216 // this threshold, the inside corner of curr will "poke through" the start of prev,
217 // and we'll need the inner loop geometry.
218 const float threshold1 = radiusSquared * cosAlpha * cosAlpha;
219 // Solve triangle for len^2: halfMiterLen^2 = radius^2 + len^2
220 // This is the point at which the inner miter point will lie on the inner stroke
221 // boundary of the curr segment. If len is below this threshold, the miter point
222 // moves 'inside' of the stroked outline, and we'll need the inner loop geometry.
223 const float threshold2 = halfMiterLength * halfMiterLength - radiusSquared;
224 // If a segment length is smaller than the larger of the two thresholds,
225 // we'll have to add the inner loop geometry.
226 const float maxLenSqd = std::max(threshold1, threshold2);
227 const bool needsInnerLoop =
228 squaredLineLength(prev) < maxLenSqd || squaredLineLength(curr) < maxLenSqd;
229 if (needsInnerLoop) {
230 // Connect to the miter midpoint (common path endpoint of the two segments),
231 // and then to t=0 (on inside) of the next segment. This adds an interior "loop" of
232 // geometry that handles edge cases where segment lengths are shorter than the
233 // stroke width.
234 inner->lineTo(miterMidpt);
235 inner->lineTo(miterMidpt - dest);
236 } else {
237 // Directly connect to inner miter point.
238 inner->setLastPt(miterMidpt - miterVec);
239 }
Tyler Denniston324578b2020-07-17 14:03:42 -0400240 };
241
242 switch (fJoin) {
243 case SkPaint::kMiter_Join:
244 miterJoin(prev, curr);
245 break;
246 default:
247 SkDebugf("Unhandled join %d\n", fJoin);
248 miterJoin(prev, curr);
249 break;
250 }
251}
252
253void SkPathStroker2::appendPathReversed(const SkPath* path, SkPath* result) {
254 const int numVerbs = path->countVerbs();
255 const int numPoints = path->countPoints();
256 std::unique_ptr<uint8_t[]> verbs = std::make_unique<uint8_t[]>(numVerbs);
257 std::unique_ptr<SkPoint[]> points = std::make_unique<SkPoint[]>(numPoints);
258
259 path->getVerbs(verbs.get(), numVerbs);
260 path->getPoints(points.get(), numPoints);
261
262 for (int i = numVerbs - 1, j = numPoints; i >= 0; i--) {
263 auto verb = static_cast<SkPath::Verb>(verbs[i]);
264 switch (verb) {
265 case SkPath::kLine_Verb: {
266 j -= 1;
267 SkASSERT(j >= 1);
268 result->lineTo(points[j - 1]);
269 break;
270 }
271 case SkPath::kMove_Verb:
272 // Ignore
273 break;
274 default:
275 SkASSERT(false);
276 break;
277 }
278 }
279}
280
281SkPoint SkPathStroker2::unitNormal(const PathSegment& seg, float t) {
282 if (seg.fVerb != SkPath::kLine_Verb) {
283 SkDebugf("Unhandled verb for unit normal %d\n", seg.fVerb);
284 }
285
286 (void)t; // Not needed for lines
287 const SkPoint tangent = seg.fPoints[1] - seg.fPoints[0];
288 const SkPoint normal = rotate90(tangent);
289 return setLength(normal, 1);
290}
291
Tyler Dennistone9ced4f2020-07-22 09:29:42 -0400292float SkPathStroker2::squaredLineLength(const PathSegment& lineSeg) {
293 SkASSERT(lineSeg.fVerb == SkPath::kLine_Verb);
294 const SkPoint diff = lineSeg.fPoints[1] - lineSeg.fPoints[0];
295 return diff.dot(diff);
296}
297
Tyler Denniston324578b2020-07-17 14:03:42 -0400298} // namespace
299
300//////////////////////////////////////////////////////////////////////////////
301
302class SimpleStroker : public Sample {
303 bool fShowSkiaStroke, fShowHidden;
304 float fWidth = 175;
305 SkPaint fPtsPaint, fStrokePaint, fNewFillPaint, fHiddenPaint;
306 static constexpr int kN = 3;
307
308public:
309 SkPoint fPts[kN];
310
311 SimpleStroker() : fShowSkiaStroke(true), fShowHidden(false) {
312 fPts[0] = {500, 200};
313 fPts[1] = {300, 200};
314 fPts[2] = {100, 100};
315
316 fPtsPaint.setAntiAlias(true);
317 fPtsPaint.setStrokeWidth(10);
318 fPtsPaint.setStrokeCap(SkPaint::kRound_Cap);
319
320 fStrokePaint.setAntiAlias(true);
321 fStrokePaint.setStyle(SkPaint::kStroke_Style);
322 fStrokePaint.setColor(0x80FF0000);
323
324 fNewFillPaint.setAntiAlias(true);
325 fNewFillPaint.setColor(0x8000FF00);
326
327 fHiddenPaint.setAntiAlias(true);
328 fHiddenPaint.setStyle(SkPaint::kStroke_Style);
329 fHiddenPaint.setColor(0xFF0000FF);
330 }
331
332 void toggle(bool& value) { value = !value; }
333
334protected:
335 SkString name() override { return SkString("SimpleStroker"); }
336
337 bool onChar(SkUnichar uni) override {
338 switch (uni) {
339 case '1':
340 this->toggle(fShowSkiaStroke);
341 return true;
342 case '2':
343 this->toggle(fShowHidden);
344 return true;
345 case '-':
346 fWidth -= 5;
347 return true;
348 case '=':
349 fWidth += 5;
350 return true;
351 default:
352 break;
353 }
354 return false;
355 }
356
357 void makePath(SkPath* path) {
358 path->moveTo(fPts[0]);
359 for (int i = 1; i < kN; ++i) {
360 path->lineTo(fPts[i]);
361 }
362 }
363
364 void onDrawContent(SkCanvas* canvas) override {
365 canvas->drawColor(0xFFEEEEEE);
366
367 SkPath path;
368 this->makePath(&path);
369
370 fStrokePaint.setStrokeWidth(fWidth);
371
372 // The correct result
373 if (fShowSkiaStroke) {
374 canvas->drawPath(path, fStrokePaint);
375 }
376
377 // Simple stroker result
378 SkPathStroker2 stroker;
379 SkPath fillPath = stroker.getFillPath(path, fStrokePaint);
380 canvas->drawPath(fillPath, fNewFillPaint);
381
382 if (fShowHidden) {
383 canvas->drawPath(fillPath, fHiddenPaint);
384 }
385
386 canvas->drawPoints(SkCanvas::kPoints_PointMode, kN, fPts, fPtsPaint);
387 }
388
389 Sample::Click* onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override {
390 const SkScalar tol = 4;
391 const SkRect r = SkRect::MakeXYWH(x - tol, y - tol, tol * 2, tol * 2);
392 for (int i = 0; i < kN; ++i) {
393 if (r.intersects(SkRect::MakeXYWH(fPts[i].fX, fPts[i].fY, 1, 1))) {
394 return new Click([this, i](Click* c) {
395 fPts[i] = c->fCurr;
396 return true;
397 });
398 }
399 }
400 return nullptr;
401 }
402
403private:
404 typedef Sample INHERITED;
405};
406
407DEF_SAMPLE(return new SimpleStroker;)