drawAnimatedLine function

Map? drawAnimatedLine(
  1. Canvas canvas,
  2. AnimatedPointConnection connection,
  3. double radius,
  4. double rotationY,
  5. double rotationZ,
  6. double animationValue,
  7. Size size,
  8. Offset? hoverPoint,
)

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};
}