drawAnimatedLine function
Draws an animated line on the canvas connecting two points on a sphere.
The line is drawn using the provided canvas object and is animated based on the animationValue.
The radius parameter specifies the radius of the sphere.
The rotationY and rotationZ parameters control the rotation of the sphere.
The size parameter represents the size of the canvas.
The hoverPoint parameter is an optional offset representing the position of a hover point.
Returns a Map with a path and a Offset representing the drawn line, or null if the line is not visible.
Implementation
Map? drawAnimatedLine(
Canvas canvas,
AnimatedPointConnection connection,
double radius,
double rotationY,
double rotationZ,
double animationValue,
Size size,
Offset? hoverPoint) {
// Calculate 3D positions for the start and end points
Vector3 startCartesian3D =
getSpherePosition3D(connection.start, radius, rotationY, rotationZ);
Vector3 endCartesian3D =
getSpherePosition3D(connection.end, radius, rotationY, rotationZ);
// Project 3D positions to 2D canvas
final center = Offset(size.width / 2, size.height / 2);
Offset startCartesian2D =
Offset(center.dx + startCartesian3D.y, center.dy - startCartesian3D.z);
Offset endCartesian2D =
Offset(center.dx + endCartesian3D.y, center.dy - endCartesian3D.z);
// Check if points are on the visible side of the sphere
bool isStartVisible = startCartesian3D.x > 0;
bool isEndVisible = endCartesian3D.x > 0;
// Calculate midpoint visibility for cases where both endpoints are hidden
// but the arc passes over the visible side
var midPoint3D = (startCartesian3D + endCartesian3D) / 2;
midPoint3D.normalize();
final angle = calculateCentralAngle(connection.start, connection.end);
midPoint3D.scale(((radius + (angle) * 10 * pi) * connection.curveScale));
bool isMidpointVisible = midPoint3D.x > 0;
// Only draw if at least one endpoint or the midpoint is visible
if (isStartVisible || isEndVisible || isMidpointVisible) {
Paint paint = Paint()
..color = connection.style.color
..strokeWidth = connection.style.lineWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
Path path = Path();
path.moveTo(startCartesian2D.dx, startCartesian2D.dy);
final midPoint2D =
Offset(center.dx + midPoint3D.y, center.dy - midPoint3D.z);
path.quadraticBezierTo(
midPoint2D.dx, midPoint2D.dy, endCartesian2D.dx, endCartesian2D.dy);
PathMetric pathMetric = path.computeMetrics().first;
double totalLength = pathMetric.length;
double drawStart = 0;
double drawEnd = totalLength * animationValue;
// Handle visibility clipping
if (!isStartVisible && !isEndVisible) {
// Both ends hidden - find both intersection points
Offset? startIntersection =
findCurveSphereIntersection(path, center, radius, 1, true);
Offset? endIntersection =
findCurveSphereIntersection(path, center, radius, 1, false);
if (startIntersection != null && endIntersection != null) {
final startPerc = getDrawPercentage(path, startIntersection) / 100;
final endPerc =
getDrawPercentage(path, endIntersection, first: false) / 100;
drawStart = totalLength * startPerc;
drawEnd = min(totalLength * endPerc, totalLength * animationValue);
} else {
return {'path': null, 'midPoint': midPoint2D};
}
} else if (!isStartVisible) {
// Start is hidden - clip from intersection
Offset? intersection =
findCurveSphereIntersection(path, center, radius, 1, true);
if (intersection != null) {
final perc = getDrawPercentage(path, intersection) / 100;
drawStart = totalLength * perc;
// Don't extend drawEnd beyond animation progress
drawEnd = totalLength * animationValue;
} else {
return {'path': null, 'midPoint': midPoint2D};
}
} else if (!isEndVisible) {
// End is hidden - clip at intersection
Offset? intersection =
findCurveSphereIntersection(path, center, radius, 1, false);
if (intersection != null) {
final perc = getDrawPercentage(path, intersection, first: false) / 100;
double visibleEnd = totalLength * perc;
// Cap the draw end at the visible portion, scaled by animation progress
drawEnd = min(visibleEnd, totalLength * animationValue);
} else {
return {'path': null, 'midPoint': midPoint2D};
}
}
// Ensure drawStart doesn't exceed drawEnd
if (drawStart >= drawEnd) {
return {'path': null, 'midPoint': midPoint2D};
}
Path extractPath = pathMetric.extractPath(drawStart, drawEnd);
final pathMetrics = extractPath.computeMetrics();
if (pathMetrics.isNotEmpty) {
switch (connection.style.type) {
case PointConnectionType.solid:
canvas.drawPath(extractPath, paint);
break;
case PointConnectionType.dashed:
PathMetric extractPathMetric = extractPath.computeMetrics().first;
double dashLength = connection.style.dashSize;
double gapLength = connection.style.spacing;
double animationOffset = connection.animationOffset;
double distance = animationOffset;
while (distance < extractPathMetric.length) {
final double startDash = distance;
final double endDash = startDash + dashLength;
if (endDash < extractPathMetric.length) {
final Tangent? startTangent =
extractPathMetric.getTangentForOffset(startDash);
final Tangent? endTangent =
extractPathMetric.getTangentForOffset(endDash);
if (startTangent != null && endTangent != null) {
final Offset startPoint = startTangent.position;
final Offset endPoint = endTangent.position;
canvas.drawLine(startPoint, endPoint, paint);
}
}
distance += dashLength + gapLength;
}
break;
case PointConnectionType.dotted:
PathMetric extractPathMetric = extractPath.computeMetrics().first;
double distance = connection.animationOffset;
while (distance < extractPathMetric.length) {
Tangent? tangent = extractPathMetric.getTangentForOffset(distance);
if (tangent != null) {
final Offset point = tangent.position;
canvas.drawCircle(point, connection.style.dotSize, paint);
}
distance += connection.style.spacing;
}
break;
}
}
double t = 0.5;
Offset realMidPoint = Offset(
pow(1 - t, 2) * startCartesian2D.dx +
2 * (1 - t) * t * midPoint2D.dx +
pow(t, 2) * endCartesian2D.dx,
pow(1 - t, 2) * startCartesian2D.dy +
2 * (1 - t) * t * midPoint2D.dy +
pow(t, 2) * endCartesian2D.dy,
);
// paint text on the midpoint
if ((connection.isLabelVisible &&
(connection.label?.isNotEmpty ?? false)) &&
connection.labelBuilder == null) {
paintText(connection.label ?? '', connection.labelTextStyle, realMidPoint,
size, canvas);
}
return {'path': extractPath, 'midPoint': realMidPoint};
}
// Return midpoint even when completely hidden for potential future use
final hiddenMidPoint3D = (startCartesian3D + endCartesian3D) / 2;
hiddenMidPoint3D.normalize();
final hiddenAngle = calculateCentralAngle(connection.start, connection.end);
hiddenMidPoint3D
.scale(((radius + (hiddenAngle) * 10 * pi) * connection.curveScale));
final hiddenMidPoint2D =
Offset(center.dx + hiddenMidPoint3D.y, center.dy - hiddenMidPoint3D.z);
return {'path': null, 'midPoint': hiddenMidPoint2D};
}