Flutter Engine
The Flutter Engine
SimpleStrokerSlide.cpp
Go to the documentation of this file.
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
10#include "include/core/SkPath.h"
14
15#include "src/core/SkGeometry.h"
16
17#include <vector>
18
19namespace {
20
21//////////////////////////////////////////////////////////////////////////////
22
23static SkPoint rotate90(const SkPoint& p) { return {p.fY, -p.fX}; }
24static SkPoint rotate180(const SkPoint& p) { return p * -1; }
25static SkPoint setLength(SkPoint p, float len) {
26 if (!p.setLength(len)) {
27 SkDebugf("Failed to set point length\n");
28 }
29 return p;
30}
31static 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 */
37class PathRecorder {
38public:
39 SkPath getPath() const {
40 return SkPath::Make(fPoints.data(), fPoints.size(), fVerbs.data(), fVerbs.size(), nullptr,
42 }
43
44 void moveTo(SkPoint p) {
45 fVerbs.push_back(SkPath::kMove_Verb);
46 fPoints.push_back(p);
47 }
48
49 void lineTo(SkPoint p) {
50 fVerbs.push_back(SkPath::kLine_Verb);
51 fPoints.push_back(p);
52 }
53
54 void close() { fVerbs.push_back(SkPath::kClose_Verb); }
55
56 void rewind() {
57 fVerbs.clear();
58 fPoints.clear();
59 }
60
61 int countPoints() const { return fPoints.size(); }
62
63 int countVerbs() const { return fVerbs.size(); }
64
65 bool getLastPt(SkPoint* lastPt) const {
66 if (fPoints.empty()) {
67 return false;
68 }
69 *lastPt = fPoints.back();
70 return true;
71 }
72
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
81 const std::vector<uint8_t>& verbs() const { return fVerbs; }
82
83 const std::vector<SkPoint>& points() const { return fPoints; }
84
85private:
86 std::vector<uint8_t> fVerbs;
87 std::vector<SkPoint> fPoints;
88};
89
90class SkPathStroker2 {
91public:
92 // Returns the fill path
93 SkPath getFillPath(const SkPath& path, const SkPaint& paint);
94
95private:
96 struct PathSegment {
97 SkPath::Verb fVerb;
98 SkPoint fPoints[4];
99 };
100
101 float fRadius;
102 SkPaint::Cap fCap;
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
129void 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
137SkPath 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) {
154 strokeLine(segment, firstSegment);
155 break;
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
183void 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
195void 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) {
210 buttCap(loc);
211 break;
212 default:
213 SkDebugf("Unhandled endcap %d\n", fCap);
214 buttCap(loc);
215 break;
216 }
217}
218
219void 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) {
298 miterJoin(prev, curr);
299 break;
300 default:
301 SkDebugf("Unhandled join %d\n", fJoin);
302 miterJoin(prev, curr);
303 break;
304 }
305}
306
307void 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 }
323 // Ignore
324 break;
325 default:
326 SkASSERT(false);
327 break;
328 }
329 }
330}
331
332SkPoint 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
343float 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
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
360public:
362
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);
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
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
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
456protected:
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
471 bool onClick(ClickHandlerSlide::Click *) override { return false; }
472
473private:
474 void toggle(bool& value) { value = !value; }
475
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
485DEF_SLIDE(return new SimpleStrokerSlide;)
SkPaint::Join fJoin
static bool rewind(EdgeList *activeEdges, Vertex **current, Vertex *dst, const Comparator &c)
static const int points[]
static float prev(float f)
#define SkASSERT(cond)
Definition: SkAssert.h:116
constexpr SkColor SK_ColorRED
Definition: SkColor.h:126
void SK_SPI SkDebugf(const char format[],...) SK_PRINTF_LIKE(1
void swap(sk_sp< T > &a, sk_sp< T > &b)
Definition: SkRefCnt.h:341
static bool SkScalarNearlyZero(SkScalar x, SkScalar tolerance=SK_ScalarNearlyZero)
Definition: SkScalar.h:101
int32_t SkUnichar
Definition: SkTypes.h:175
#define DEF_SLIDE(code)
Definition: Slide.h:25
#define setLength(p, len)
bool onChar(SkUnichar uni) override
void draw(SkCanvas *canvas) override
Click * onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey modi) override
bool onClick(ClickHandlerSlide::Click *) override
void drawPoints(PointMode mode, size_t count, const SkPoint pts[], const SkPaint &paint)
Definition: SkCanvas.cpp:1710
void translate(SkScalar dx, SkScalar dy)
Definition: SkCanvas.cpp:1278
void drawColor(SkColor color, SkBlendMode mode=SkBlendMode::kSrcOver)
Definition: SkCanvas.h:1182
void drawPath(const SkPath &path, const SkPaint &paint)
Definition: SkCanvas.cpp:1747
@ kPoints_PointMode
draw each point separately
Definition: SkCanvas.h:1241
@ kRound_Cap
adds circle
Definition: SkPaint.h:335
@ kButt_Cap
no stroke extension
Definition: SkPaint.h:334
void setStyle(Style style)
Definition: SkPaint.cpp:105
void setColor(SkColor color)
Definition: SkPaint.cpp:119
void setAntiAlias(bool aa)
Definition: SkPaint.h:170
void setStrokeCap(Cap cap)
Definition: SkPaint.cpp:179
@ kStroke_Style
set to stroke geometry
Definition: SkPaint.h:194
@ kMiter_Join
extends to miter limit
Definition: SkPaint.h:359
void setStrokeWidth(SkScalar width)
Definition: SkPaint.cpp:159
Definition: SkPath.h:59
static SkPath Make(const SkPoint[], int pointCount, const uint8_t[], int verbCount, const SkScalar[], int conicWeightCount, SkPathFillType, bool isVolatile=false)
Definition: SkPath.cpp:3569
@ kClose_Verb
Definition: SkPath.h:1471
@ kMove_Verb
Definition: SkPath.h:1466
@ kDone_Verb
Definition: SkPath.h:1472
@ kLine_Verb
Definition: SkPath.h:1467
SkString fName
Definition: Slide.h:54
const Paint & paint
Definition: color_source.cc:38
float SkScalar
Definition: extension.cpp:12
static bool b
struct MyStruct a[10]
uint8_t value
GAsyncResult * result
static float max(float r, float g, float b)
Definition: hsl.cpp:49
double y
double x
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition: switches.h:57
SK_API bool FillPathWithPaint(const SkPath &src, const SkPaint &paint, SkPath *dst, const SkRect *cullRect, SkScalar resScale=1)
Definition: SkPathUtils.cpp:23
ModifierKey
Definition: ModifierKey.h:9
dest
Definition: zip.py:79
static SkString join(const CommandLineFlags::StringArray &)
Definition: skpbench.cpp:741
SeparatedVector2 offset
bool setLength(float length)
Definition: SkPoint.cpp:30
float fX
x-axis value
Definition: SkPoint_impl.h:164
float dot(const SkVector &vec) const
Definition: SkPoint_impl.h:554
float fY
y-axis value
Definition: SkPoint_impl.h:165
static constexpr SkRect MakeXYWH(float x, float y, float w, float h)
Definition: SkRect.h:659
bool intersects(const SkRect &r) const
Definition: SkRect.h:1121