in lib/web_ui/lib/src/engine/html/path/path.dart [436:642]
void arcTo(
ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) {
assert(rectIsValid(rect));
// If width or height is 0, we still stroke a line, only abort if both
// are empty.
if (rect.width == 0 && rect.height == 0) {
return;
}
if (pathRef.countPoints() == 0) {
forceMoveTo = true;
}
final ui.Offset? lonePoint = _arcIsLonePoint(rect, startAngle, sweepAngle);
if (lonePoint != null) {
if (forceMoveTo) {
moveTo(lonePoint.dx, lonePoint.dy);
} else {
lineTo(lonePoint.dx, lonePoint.dy);
}
}
// Convert angles to unit vectors.
double stopAngle = startAngle + sweepAngle;
final double cosStart = math.cos(startAngle);
final double sinStart = math.sin(startAngle);
double cosStop = math.cos(stopAngle);
double sinStop = math.sin(stopAngle);
// If the sweep angle is nearly (but less than) 360, then due to precision
// loss in radians-conversion and/or sin/cos, we may end up with coincident
// vectors, which will fool quad arc build into doing nothing (bad) instead
// of drawing a nearly complete circle (good).
// e.g. canvas.drawArc(0, 359.99, ...)
// -vs- canvas.drawArc(0, 359.9, ...)
// Detect this edge case, and tweak the stop vector.
if (SPath.nearlyEqual(cosStart, cosStop) && SPath.nearlyEqual(sinStart, sinStop)) {
final double sweep = sweepAngle.abs() * 180.0 / math.pi;
if (sweep <= 360 && sweep > 359) {
// Use tiny angle (in radians) to tweak.
final double deltaRad = sweepAngle < 0 ? -1.0 / 512.0 : 1.0 / 512.0;
do {
stopAngle -= deltaRad;
cosStop = math.cos(stopAngle);
sinStop = math.sin(stopAngle);
} while (cosStart == cosStop && sinStart == sinStop);
}
}
final int dir = sweepAngle > 0 ? SPathDirection.kCW : SPathDirection.kCCW;
final double endAngle = startAngle + sweepAngle;
final double radiusX = rect.width / 2.0;
final double radiusY = rect.height / 2.0;
final double px = rect.center.dx + (radiusX * math.cos(endAngle));
final double py = rect.center.dy + (radiusY * math.sin(endAngle));
// At this point, we know that the arc is not a lone point, but
// startV == stopV indicates that the sweepAngle is too small such that
// angles_to_unit_vectors cannot handle it
if (cosStart == cosStop && sinStart == sinStop) {
// Add moveTo to start point if forceMoveTo is true. Otherwise a lineTo
// unless we're sufficiently close to start point currently. This prevents
// spurious lineTos when adding a series of contiguous arcs from the same
// oval.
if (forceMoveTo) {
moveTo(px, py);
} else {
_lineToIfNotTooCloseToLastPoint(px, py);
}
// We are done with tiny sweep approximated by line.
return;
}
// Convert arc defined by start/end unit vectors to conics (max 5).
// Dot product
final double x = (cosStart * cosStop) + (sinStart * sinStop);
// Cross product
double y = (cosStart * sinStop) - (sinStart * cosStop);
final double absY = y.abs();
// Check for coincident vectors (angle is nearly 0 or 180).
// The cross product for angles 0 and 180 will be zero, we use the
// dot product sign to distinguish between the two.
if (absY <= SPath.scalarNearlyZero &&
x > 0 &&
((y >= 0 && dir == SPathDirection.kCW) ||
(y <= 0 && dir == SPathDirection.kCCW))) {
// No conics, just use single line to connect point.
if (forceMoveTo) {
moveTo(px, py);
} else {
_lineToIfNotTooCloseToLastPoint(px, py);
}
return;
}
// Normalize to clockwise
if (dir == SPathDirection.kCCW) {
y = -y;
}
// Use 1 conic per quadrant of a circle.
// 0..90 -> quadrant 0
// 90..180 -> quadrant 1
// 180..270 -> quadrant 2
// 270..360 -> quadrant 3
const List<ui.Offset> quadPoints = <ui.Offset>[
ui.Offset(1, 0),
ui.Offset(1, 1),
ui.Offset(0, 1),
ui.Offset(-1, 1),
ui.Offset(-1, 0),
ui.Offset(-1, -1),
ui.Offset(0, -1),
ui.Offset(1, -1),
];
int quadrant = 0;
if (0 == y) {
// 180 degrees between vectors.
quadrant = 2;
assert((x + 1).abs() <= SPath.scalarNearlyZero);
} else if (0 == x) {
// Dot product 0 means 90 degrees between vectors.
assert((absY - 1) <= SPath.scalarNearlyZero);
quadrant = y > 0 ? 1 : 3; // 90 or 270
} else {
if (y < 0) {
quadrant += 2;
}
if ((x < 0) != (y < 0)) {
quadrant += 1;
}
}
final List<Conic> conics = <Conic>[];
const double quadrantWeight = SPath.scalarRoot2Over2;
int conicCount = quadrant;
for (int i = 0; i < conicCount; i++) {
final int quadPointIndex = i * 2;
final ui.Offset p0 = quadPoints[quadPointIndex];
final ui.Offset p1 = quadPoints[quadPointIndex + 1];
final ui.Offset p2 = quadPoints[quadPointIndex + 2];
conics
.add(Conic(p0.dx, p0.dy, p1.dx, p1.dy, p2.dx, p2.dy, quadrantWeight));
}
// Now compute any remaining ( < 90degree ) arc for last conic.
final double finalPx = x;
final double finalPy = y;
final ui.Offset lastQuadrantPoint = quadPoints[quadrant * 2];
// Dot product between last quadrant vector and last point on arc.
final double dot = (x * lastQuadrantPoint.dx) + (y * lastQuadrantPoint.dy);
if (dot < 1) {
// Compute the bisector vector and then rescale to be the off curve point.
// Length is cos(theta/2) using half angle identity we get
// length = sqrt(2 / (1 + cos(theta)). We already have cos from computing
// dot. Computed weight is cos(theta/2).
double offCurveX = lastQuadrantPoint.dx + x;
double offCurveY = lastQuadrantPoint.dy + y;
final double cosThetaOver2 = math.sqrt((1.0 + dot) / 2.0);
final double unscaledLength =
math.sqrt((offCurveX * offCurveX) + (offCurveY * offCurveY));
assert(unscaledLength > SPath.scalarNearlyZero);
offCurveX /= cosThetaOver2 * unscaledLength;
offCurveY /= cosThetaOver2 * unscaledLength;
if (!SPath.nearlyEqual(offCurveX, lastQuadrantPoint.dx) ||
!SPath.nearlyEqual(offCurveY, lastQuadrantPoint.dy)) {
conics.add(Conic(lastQuadrantPoint.dx, lastQuadrantPoint.dy, offCurveX,
offCurveY, finalPx, finalPy, cosThetaOver2));
++conicCount;
}
}
// Any points we generate based on unit vectors cos/sinStart , cos/sinStop
// we rotate to start vector, scale by rect.width/2 rect.height/2 and
// then translate to center point.
final double scaleX = rect.width / 2;
final bool ccw = dir == SPathDirection.kCCW;
final double scaleY = rect.height / 2;
final double centerX = rect.center.dx;
final double centerY = rect.center.dy;
for (final Conic conic in conics) {
double x = conic.p0x;
double y = ccw ? -conic.p0y : conic.p0y;
conic.p0x = (cosStart * x - sinStart * y) * scaleX + centerX;
conic.p0y = (cosStart * y + sinStart * x) * scaleY + centerY;
x = conic.p1x;
y = ccw ? -conic.p1y : conic.p1y;
conic.p1x = (cosStart * x - sinStart * y) * scaleX + centerX;
conic.p1y = (cosStart * y + sinStart * x) * scaleY + centerY;
x = conic.p2x;
y = ccw ? -conic.p2y : conic.p2y;
conic.p2x = (cosStart * x - sinStart * y) * scaleX + centerX;
conic.p2y = (cosStart * y + sinStart * x) * scaleY + centerY;
}
// Now output points.
final double firstConicPx = conics[0].p0x;
final double firstConicPy = conics[0].p0y;
if (forceMoveTo) {
moveTo(firstConicPx, firstConicPy);
} else {
_lineToIfNotTooCloseToLastPoint(firstConicPx, firstConicPy);
}
for (int i = 0; i < conicCount; i++) {
final Conic conic = conics[i];
conicTo(conic.p1x, conic.p1y, conic.p2x, conic.p2y, conic.fW);
}
_resetAfterEdit();
}