xref: /aosp_15_r20/external/skia/tools/viewer/SimpleStrokerSlide.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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