Visitar URL original
Dashed paths (#54) · flutter/packages@8184de9 · GitHub
Skip to content

Commit 8184de9

Browse files
authored
Dashed paths (#54)
1 parent 424198b commit 8184de9

9 files changed

Lines changed: 810 additions & 19 deletions

File tree

‎packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:math' as math;
22
import 'package:meta/meta.dart';
33

4+
import '../util.dart';
5+
46
/// An immutable position in two-dimensional space.
57
///
68
/// This class is roughly compatible with dart:ui's Offset.
@@ -12,6 +14,28 @@ class Point {
1214
/// The point at the origin of coordinate space.
1315
static const Point zero = Point(0, 0);
1416

17+
/// Linearly interpolate between two points.
18+
///
19+
/// The [t] argument represents a position on the timeline, with 0.0 meaning
20+
/// interpolation has not started and 1.0 meaning interpolation has finished.
21+
///
22+
/// At the start the returned value equals [a], and at the end it equals [b]. As
23+
/// the number advances from 0 to 1 it returns a value closer to a or b
24+
/// respectively.
25+
static Point lerp(Point a, Point b, double t) {
26+
return Point(
27+
lerpDouble(a.x, b.x, t),
28+
lerpDouble(a.y, b.y, t),
29+
);
30+
}
31+
32+
/// The distance between points [a] and [b].
33+
static double distance(Point a, Point b) {
34+
final double x = (a.x - b.x);
35+
final double y = (a.y - b.y);
36+
return math.sqrt((x * x) + (y * y));
37+
}
38+
1539
/// The offset along the x-axis of this point.
1640
final double x;
1741

‎packages/vector_graphics_compiler/lib/src/geometry/path.dart‎

Lines changed: 234 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ class CubicToCommand extends PathCommand {
151151
const CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3)
152152
: super._(PathCommandType.cubic);
153153

154+
/// Creates a cubic command from the current point to [end] using [control1]
155+
/// and [control2] as control points.
156+
CubicToCommand.fromPoints(Point control1, Point control2, Point end)
157+
: this(control1.x, control1.y, control2.x, control2.y, end.x, end.y);
158+
159+
factory CubicToCommand._fromIterablePoints(Iterable<Point> points) {
160+
final List<Point> list = points.toList();
161+
assert(list.length == 3);
162+
return CubicToCommand.fromPoints(list[0], list[1], list[2]);
163+
}
164+
154165
/// The absolute offset of the first control point for this path from the x
155166
/// axis.
156167
final double x1;
@@ -159,6 +170,9 @@ class CubicToCommand extends PathCommand {
159170
/// axis.
160171
final double y1;
161172

173+
/// A [Point] representation of [x1],[y1], the first control point.
174+
Point get controlPoint1 => Point(x1, y1);
175+
162176
/// The absolute offset of the second control point for this path from the x
163177
/// axis.
164178
final double x2;
@@ -167,6 +181,9 @@ class CubicToCommand extends PathCommand {
167181
/// axis.
168182
final double y2;
169183

184+
/// A [Point] representation of [x2],[y2], the second control point.
185+
Point get controlPoint2 => Point(x2, y2);
186+
170187
/// The absolute offset of the destination point for this path from the x
171188
/// axis.
172189
final double x3;
@@ -175,6 +192,83 @@ class CubicToCommand extends PathCommand {
175192
/// axis.
176193
final double y3;
177194

195+
/// A [Point] representation of [x3],[y3], the end point of the curve.
196+
Point get endPoint => Point(x3, y3);
197+
198+
/// Subdivides the cubic curve described by [start], [control1], [control2],
199+
/// [end].
200+
///
201+
/// The returned list describes two cubics, where elements `0, 1, 2, 3` are
202+
/// the start, cp1, cp2, and end points of the first cubic and `3, 4, 5, 6`
203+
/// are the start, cp1, cp2, and end points of the second cubic.
204+
static List<Point> subdivide(
205+
Point start,
206+
Point control1,
207+
Point control2,
208+
Point end,
209+
double t,
210+
) {
211+
final Point ab = Point.lerp(start, control1, t);
212+
final Point bc = Point.lerp(control1, control2, t);
213+
final Point cd = Point.lerp(control2, end, t);
214+
final Point abc = Point.lerp(ab, bc, t);
215+
final Point bcd = Point.lerp(bc, cd, t);
216+
final Point abcd = Point.lerp(abc, bcd, t);
217+
return <Point>[
218+
start,
219+
ab,
220+
abc,
221+
abcd,
222+
bcd,
223+
cd,
224+
end,
225+
];
226+
}
227+
228+
/// Precisions > 1 increase precision at cost of extra computation time.
229+
double computeLength(Point start) {
230+
// Mike Reed just made this up! The nerve of him.
231+
// One difference from Skia is just setting a default tolerance of 3. This
232+
// is good enough for a particular test SVG that has this curve:
233+
// M65 33c0 17.673-14.326 32-32 32S1 50.673 1 33C1 15.327 15.326 1 33 1s32 14.327 32 32z
234+
// Lower values end up getting the end points wrong when dashing a path.
235+
const double tolerance = 1 / 2 * 3;
236+
237+
double _compute(
238+
Point p1,
239+
Point cp1,
240+
Point cp2,
241+
Point p2,
242+
double distance,
243+
) {
244+
// If it's "too curvy," cut it in half
245+
if (Point.distance(cp1, Point.lerp(p1, p2, 1 / 3)) > tolerance ||
246+
Point.distance(cp2, Point.lerp(p1, p2, 2 / 3)) > tolerance) {
247+
final List<Point> points = subdivide(p1, cp1, cp2, p2, .5);
248+
distance = _compute(
249+
points[0],
250+
points[1],
251+
points[2],
252+
points[3],
253+
distance,
254+
);
255+
distance = _compute(
256+
points[3],
257+
points[4],
258+
points[5],
259+
points[6],
260+
distance,
261+
);
262+
} else {
263+
// It's collinear enough to just treat as a line.
264+
distance += Point.distance(p1, p2);
265+
}
266+
return distance;
267+
}
268+
269+
return _compute(start, Point(x1, y1), Point(x2, y2), Point(x3, y3), 0);
270+
}
271+
178272
@override
179273
CubicToCommand transformed(AffineMatrix matrix) {
180274
final Point xy1 = matrix.transformPoint(Point(x1, y1));
@@ -244,16 +338,9 @@ class PathBuilder implements PathProxy {
244338

245339
final List<PathCommand> _commands = <PathCommand>[];
246340

247-
Point _currentSubPathPoint = Point.zero;
248-
249-
/// The last destination point used by this builder.
250-
Point get currentPoint => _currentPoint;
251-
Point _currentPoint = Point.zero;
252-
253341
@override
254342
PathBuilder close() {
255343
_commands.add(const CloseCommand());
256-
_currentPoint = _currentSubPathPoint;
257344
return this;
258345
}
259346

@@ -267,21 +354,18 @@ class PathBuilder implements PathProxy {
267354
double y3,
268355
) {
269356
_commands.add(CubicToCommand(x1, y1, x2, y2, x3, y3));
270-
_currentPoint = Point(x3, y3);
271357
return this;
272358
}
273359

274360
@override
275361
PathBuilder lineTo(double x, double y) {
276362
_commands.add(LineToCommand(x, y));
277-
_currentPoint = Point(x, y);
278363
return this;
279364
}
280365

281366
@override
282367
PathBuilder moveTo(double x, double y) {
283368
_commands.add(MoveToCommand(x, y));
284-
_currentPoint = _currentSubPathPoint = Point(x, y);
285369
return this;
286370
}
287371

@@ -323,10 +407,10 @@ class PathBuilder implements PathProxy {
323407

324408
/// Adds a rectangle to the new path.
325409
PathBuilder addRect(Rect rect) {
326-
moveTo(rect.right, rect.top);
410+
moveTo(rect.left, rect.top);
411+
lineTo(rect.right, rect.top);
327412
lineTo(rect.right, rect.bottom);
328413
lineTo(rect.left, rect.bottom);
329-
lineTo(rect.left, rect.top);
330414
close();
331415
return this;
332416
}
@@ -462,6 +546,20 @@ class Path {
462546
other.fillType == fillType;
463547
}
464548

549+
/// Creates a dashed version of this path.
550+
///
551+
/// The interval list is read in a circular fashion, such that the first
552+
/// interval is used to dash and the second to move. If the list is an odd
553+
/// number of elements, it is effectively the same as if it were repeated
554+
/// twice.
555+
///
556+
/// Callers are responsible for not passing interval lists consisting entirely
557+
/// of `0`.
558+
Path dashed(List<double> intervals) {
559+
final _PathDasher dasher = _PathDasher(intervals);
560+
return dasher.dash(this);
561+
}
562+
465563
/// Compute the bounding box for the given path segment.
466564
Rect bounds() {
467565
if (_commands.isEmpty) {
@@ -549,3 +647,127 @@ Path parseSvgPathData(String svg, [PathFillType type = PathFillType.nonZero]) {
549647
}
550648
return pathBuilder.toPath();
551649
}
650+
651+
class _CircularIntervalList {
652+
_CircularIntervalList(this._vals)
653+
: assert(_vals.isNotEmpty),
654+
assert(!_vals.every((double val) => val == 0));
655+
656+
final List<double> _vals;
657+
int _idx = 0;
658+
659+
double get next {
660+
if (_idx >= _vals.length) {
661+
_idx = 0;
662+
}
663+
return _vals[_idx++];
664+
}
665+
}
666+
667+
class _PathDasher {
668+
_PathDasher(List<double> intervals)
669+
: assert(!intervals.every((double interval) => interval == 0)),
670+
_intervals = _CircularIntervalList(intervals);
671+
672+
final _CircularIntervalList _intervals;
673+
674+
late double length;
675+
Point currentPoint = Point.zero;
676+
Point currentSubpathPoint = Point.zero;
677+
late bool draw;
678+
679+
final List<PathCommand> _dashedCommands = <PathCommand>[];
680+
681+
void _dashLineTo(Point target) {
682+
double distance = Point.distance(currentPoint, target);
683+
684+
if (distance <= 0 || length <= 0) {
685+
return;
686+
}
687+
688+
while (distance >= length) {
689+
final double t = length / distance;
690+
currentPoint = Point.lerp(currentPoint, target, t);
691+
length = _intervals.next;
692+
693+
if (draw) {
694+
_dashedCommands.add(LineToCommand(currentPoint.x, currentPoint.y));
695+
} else {
696+
_dashedCommands.add(MoveToCommand(currentPoint.x, currentPoint.y));
697+
}
698+
699+
distance = Point.distance(currentPoint, target);
700+
draw = !draw;
701+
}
702+
if (distance > 0) {
703+
length -= distance;
704+
if (draw) {
705+
_dashedCommands.add(LineToCommand(target.x, target.y));
706+
}
707+
}
708+
currentPoint = target;
709+
}
710+
711+
void _dashCubicTo(CubicToCommand cubic) {
712+
double distance = cubic.computeLength(currentPoint);
713+
while (distance >= length) {
714+
final double t = length / distance;
715+
final List<Point> dividedPoints = CubicToCommand.subdivide(
716+
currentPoint,
717+
cubic.controlPoint1,
718+
cubic.controlPoint2,
719+
cubic.endPoint,
720+
t,
721+
);
722+
currentPoint = dividedPoints[3];
723+
if (draw) {
724+
_dashedCommands.add(CubicToCommand._fromIterablePoints(
725+
dividedPoints.skip(1).take(3),
726+
));
727+
} else {
728+
_dashedCommands.add(MoveToCommand(
729+
currentPoint.x,
730+
currentPoint.y,
731+
));
732+
}
733+
cubic = CubicToCommand._fromIterablePoints(
734+
dividedPoints.skip(4).take(3),
735+
);
736+
length = _intervals.next;
737+
distance = cubic.computeLength(currentPoint);
738+
draw = !draw;
739+
}
740+
length -= distance;
741+
currentPoint = cubic.endPoint;
742+
if (draw) {
743+
_dashedCommands.add(cubic);
744+
}
745+
}
746+
747+
Path dash(Path path) {
748+
length = _intervals.next;
749+
draw = true;
750+
for (final PathCommand command in path._commands) {
751+
switch (command.type) {
752+
case PathCommandType.move:
753+
final MoveToCommand move = command as MoveToCommand;
754+
currentPoint = Point(move.x, move.y);
755+
currentSubpathPoint = currentPoint;
756+
_dashedCommands.add(command);
757+
break;
758+
case PathCommandType.line:
759+
final LineToCommand line = command as LineToCommand;
760+
_dashLineTo(Point(line.x, line.y));
761+
break;
762+
case PathCommandType.cubic:
763+
_dashCubicTo(command as CubicToCommand);
764+
break;
765+
case PathCommandType.close:
766+
_dashLineTo(currentSubpathPoint);
767+
currentPoint = currentSubpathPoint;
768+
break;
769+
}
770+
}
771+
return Path(commands: _dashedCommands, fillType: path.fillType);
772+
}
773+
}

0 commit comments

Comments
 (0)