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