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 control points for curvature
  if (isStartVisible || isEndVisible) {
    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);

    var midPoint = (startCartesian3D + endCartesian3D) / 2;
    midPoint.normalize();

    final angle = calculateCentralAngle(connection.start, connection.end);
    midPoint.scale(((radius + (angle) * 10 * pi) * connection.curveScale));

    final midPoint2D = Offset(center.dx + midPoint.y, center.dy - midPoint.z);

    path.quadraticBezierTo(
        midPoint2D.dx, midPoint2D.dy, endCartesian2D.dx, endCartesian2D.dy);

    PathMetric pathMetric = path.computeMetrics().first;

    double start = 0;
    double end = pathMetric.length * animationValue;

    if (!isStartVisible) {
      Offset? intersection =
          findCurveSphereIntersection(path, center, radius, 1, true);
      if (intersection != null) {
        // canvas.drawCircle(intersection, 5, paint);
        final perc = 1 - getDrawPercentage(path, intersection) / 100;
        // print(perc);
        start = pathMetric.length - (perc * pathMetric.length);
      } else {
        return null;
      }
    } else if (!isEndVisible) {
      Offset? intersection =
          findCurveSphereIntersection(path, center, radius, 1, false);
      if (intersection != null) {
        final perc =
            1 - getDrawPercentage(path, intersection, first: false) / 100;
        end = (pathMetric.length - (perc * pathMetric.length)) * animationValue;
      } else {
        return null;
      }
    }

    Path extractPath = pathMetric.extractPath(start, end);
    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 null;
}