flutterdartflutter-custompainterflutter-path

flutter custom paint road path


i have this code in flutter , iam using a custom paint to draw a "road like" and the path is done but when i draw circles inside it this happens :

enter image description here

and here is what i want :

enter image description here

and here is my code :

import 'dart:math' as math;

import 'package:flutter/material.dart';

//Add this CustomPaint widget to the Widget Tree

//Copy this CustomPainter code to the Bottom of the File

extension DrawOnPathExtension on Canvas {
  void drawOnPath(
    Path path, {
    required double spacing,
    required int steps,
    required Color defaultColor,
    required Color stepCompletedColor,
    double offsetFromPath = 10.0, // Adjust for desired offset from the path
  }) {
    final paint = Paint()..style = PaintingStyle.fill;
    final pathMetrics = path.computeMetrics();
    double distance = 0.0; // Start at the beginning of the path
    int shapeCounter = 0;

    for (final metric in pathMetrics) {
      while (distance < metric.length) {
        final tangent = metric.getTangentForOffset(distance);
        if (tangent == null) continue;

        // Calculate the normal (perpendicular) vector, consistently on one side
        final normal =
            Offset(-tangent.vector.dy, tangent.vector.dx).normalize();
        // Offset the position by the normal vector and specified offset
        final offsetPosition = tangent.position + normal * offsetFromPath;

        // Determine color based on steps completed
        paint.color =
            shapeCounter < steps * 5 ? defaultColor : stepCompletedColor;

        // Draw circle at the offset position
        drawCircle(this, offsetPosition, paint,
            radius: 5.0); // Adjust radius as needed

        distance += spacing; // Move to the next position along the path
        shapeCounter++;
      }
      distance = 0.0; // Reset distance for next path metric
    }
  }

  void drawCircle(Canvas canvas, Offset position, Paint paint,
      {double? radius}) {
    canvas.drawCircle(position, radius!, paint);
  }
}

extension on Offset {
  // Normalize the Offset to get a unit vector
  Offset normalize() {
    final length = math.sqrt(dx * dx + dy * dy);
    return Offset(dx / length, dy / length);
  }
}

class RPSCustomPainter1 extends CustomPainter {
  final int stepNumber;

  RPSCustomPainter1({required this.stepNumber});

  @override
  void paint(Canvas canvas, Size size) {
    Path path_0 = Path();
    path_0.moveTo(size.width * 0.7180468, size.height * 0.02709575);
    path_0.lineTo(size.width * 0.7585899, size.height * 0.04322462);
    path_0.lineTo(size.width * 0.5463654, size.height * 0.1534690);
    path_0.lineTo(size.width * 0.5066828, size.height * 0.1674390);
    path_0.lineTo(size.width * 0.4452413, size.height * 0.1719870);
    path_0.lineTo(size.width * 0.3967298, size.height * 0.1617205);
    path_0.lineTo(size.width * 0.2607467, size.height * 0.09137139);
    path_0.lineTo(size.width * 0.1801492, size.height * 0.04966274);
    path_0.lineTo(size.width * 0.1535241, size.height * 0.03829289);
    path_0.lineTo(size.width * 0.1110580, size.height * 0.03719908);
    path_0.lineTo(size.width * 0.07781390, size.height * 0.04915422);
    path_0.lineTo(size.width * 0.06558509, size.height * 0.06854533);
    path_0.lineTo(size.width * 0.08100125, size.height * 0.09115071);
    path_0.lineTo(size.width * 0.3775313, size.height * 0.2422786);
    path_0.lineTo(size.width * 0.5260088, size.height * 0.3199390);
    path_0.lineTo(size.width * 0.5832749, size.height * 0.3077632);
    path_0.lineTo(size.width * 0.6205457, size.height * 0.2884680);
    path_0.lineTo(size.width * 0.7098659, size.height * 0.2422786);
    path_0.lineTo(size.width * 0.7729755, size.height * 0.2284525);
    path_0.lineTo(size.width * 0.8304328, size.height * 0.2393905);
    path_0.lineTo(size.width * 0.8658337, size.height * 0.2554810);
    path_0.lineTo(size.width * 0.9350524, size.height * 0.2889861);
    path_0.lineTo(size.width * 0.9755636, size.height * 0.3107759);
    path_0.lineTo(size.width * 0.9982788, size.height * 0.3450582);
    path_0.lineTo(size.width * 0.9873037, size.height * 0.3732573);
    path_0.lineTo(size.width * 0.9598287, size.height * 0.3941836);
    path_0.lineTo(size.width * 0.9035826, size.height * 0.4244648);
    path_0.lineTo(size.width * 0.7605979, size.height * 0.4929238);
    path_0.lineTo(size.width * 0.6050551, size.height * 0.5751610);
    path_0.lineTo(size.width * 0.5392469, size.height * 0.6110743);
    path_0.lineTo(size.width * 0.4607637, size.height * 0.6302256);
    path_0.lineTo(size.width * 0.3966235, size.height * 0.6215039);
    path_0.lineTo(size.width * 0.3524150, size.height * 0.6004145);
    path_0.lineTo(size.width * 0.2721787, size.height * 0.5561536);
    path_0.lineTo(size.width * 0.2467648, size.height * 0.5403798);
    path_0.lineTo(size.width * 0.2365122, size.height * 0.5156923);
    path_0.lineTo(size.width * 0.2399545, size.height * 0.4979611);
    path_0.lineTo(size.width * 0.2555832, size.height * 0.4740029);
    path_0.lineTo(size.width * 0.2765772, size.height * 0.4647535);
    path_0.lineTo(size.width * 0.2883704, size.height * 0.4458325);
    path_0.lineTo(size.width * 0.2851406, size.height * 0.4353166);
    path_0.lineTo(size.width * 0.2691613, size.height * 0.4266237);
    path_0.lineTo(size.width * 0.1527698, size.height * 0.3681721);
    path_0.lineTo(size.width * 0.1179639, size.height * 0.3632883);
    path_0.lineTo(size.width * 0.09614118, size.height * 0.3655431);
    path_0.lineTo(size.width * 0.07278851, size.height * 0.3788031);
    path_0.lineTo(size.width * 0.06388517, size.height * 0.3943659);
    path_0.lineTo(size.width * 0.07820701, size.height * 0.4161270);
    path_0.lineTo(size.width * 0.1041414, size.height * 0.4326012);
    path_0.lineTo(size.width * 0.1138416, size.height * 0.4567322);
    path_0.lineTo(size.width * 0.1047683, size.height * 0.4849218);
    path_0.lineTo(size.width * 0.08567604, size.height * 0.4976637);
    path_0.lineTo(size.width * 0.06644568, size.height * 0.5117584);
    path_0.lineTo(size.width * 0.06388517, size.height * 0.5238287);
    path_0.lineTo(size.width * 0.07097172, size.height * 0.5405237);
    path_0.lineTo(size.width * 0.3899620, size.height * 0.7089222);
    path_0.lineTo(size.width * 0.5029536, size.height * 0.7667981);
    path_0.lineTo(size.width * 0.5419349, size.height * 0.7749921);
    path_0.lineTo(size.width * 0.5645120, size.height * 0.7719985);
    path_0.lineTo(size.width * 0.6117592, size.height * 0.7522140);
    path_0.lineTo(size.width * 0.6923780, size.height * 0.7080011);
    path_0.lineTo(size.width * 0.7431206, size.height * 0.6833137);
    path_0.lineTo(size.width * 0.8080151, size.height * 0.6485037);
    path_0.lineTo(size.width * 0.8541361, size.height * 0.6376711);
    path_0.lineTo(size.width * 0.8930962, size.height * 0.6366445);
    path_0.lineTo(size.width * 0.9610293, size.height * 0.6549898);
    path_0.lineTo(size.width * 0.9933278, size.height * 0.6862209);
    path_0.lineTo(size.width * 0.9846051, size.height * 0.7166077);
    path_0.lineTo(size.width * 0.9506279, size.height * 0.7450563);
    path_0.lineTo(size.width * 0.8039672, size.height * 0.8161442);
    path_0.lineTo(size.width * 0.7521408, size.height * 0.8452837);
    path_0.lineTo(size.width * 0.7301375, size.height * 0.8622089);
    path_0.lineTo(size.width * 0.7290750, size.height * 0.8811587);
    path_0.lineTo(size.width * 0.7547226, size.height * 0.8975274);
    path_0.lineTo(size.width * 0.7695332, size.height * 0.9127160);
    path_0.lineTo(size.width * 0.7793927, size.height * 0.9243449);
    path_0.lineTo(size.width * 0.7789465, size.height * 0.9451848);
    path_0.lineTo(size.width * 0.7551476, size.height * 0.9706687);
    path_0.lineTo(size.width * 0.7082404, size.height * 0.9895033);
    path_0.lineTo(size.width * 0.6569877, size.height * 0.9948572);
    path_0.lineTo(size.width * 0.6282378, size.height * 0.9920267);
    path_0.lineTo(size.width * 0.5725016, size.height * 0.9744298);
    path_0.lineTo(size.width * 0.5310873, size.height * 0.9512775);
    path_0.lineTo(size.width * 0.4816515, size.height * 0.9247095);
    path_0.lineTo(size.width * 0.4392491, size.height * 0.9106531);
    path_0.lineTo(size.width * 0.3966235, size.height * 0.9176190);
    path_0.lineTo(size.width * 0.3565054, size.height * 0.9344099);
    path_0.lineTo(size.width * 0.3246850, size.height * 0.9391881);
    path_0.lineTo(size.width * 0.2660696, size.height * 0.9344099);
    path_0.lineTo(size.width * 0.2456280, size.height * 0.9260816);
    path_0.lineTo(size.width * 0.1933873, size.height * 0.9024783);
    path_0.lineTo(size.width * 0.1445146, size.height * 0.8758240);
    path_0.lineTo(size.width * 0.1218737, size.height * 0.8622089);
    path_0.lineTo(size.width * 0.1673254, size.height * 0.8428274);
    path_0.lineTo(size.width * 0.2722318, size.height * 0.9009815);
    path_0.lineTo(size.width * 0.3126793, size.height * 0.9096649);
    path_0.lineTo(size.width * 0.3505557, size.height * 0.9011063);
    path_0.lineTo(size.width * 0.3743227, size.height * 0.8880286);
    path_0.lineTo(size.width * 0.4257134, size.height * 0.8826651);
    path_0.lineTo(size.width * 0.4815771, size.height * 0.8886042);
    path_0.lineTo(size.width * 0.5185823, size.height * 0.9014133);
    path_0.lineTo(size.width * 0.5595397, size.height * 0.9229153);
    path_0.lineTo(size.width * 0.6098468, size.height * 0.9499439);
    path_0.lineTo(size.width * 0.6647543, size.height * 0.9672241);
    path_0.lineTo(size.width * 0.7005801, size.height * 0.9561229);
    path_0.lineTo(size.width * 0.7179512, size.height * 0.9415292);
    path_0.lineTo(size.width * 0.7179512, size.height * 0.9280293);
    path_0.lineTo(size.width * 0.7068911, size.height * 0.9127160);
    path_0.lineTo(size.width * 0.6867045, size.height * 0.8975274);
    path_0.lineTo(size.width * 0.6740613, size.height * 0.8811587);
    path_0.lineTo(size.width * 0.6740613, size.height * 0.8651929);
    path_0.lineTo(size.width * 0.6790973, size.height * 0.8498892);
    path_0.lineTo(size.width * 0.6906037, size.height * 0.8335492);
    path_0.lineTo(size.width * 0.8937549, size.height * 0.7272099);
    path_0.lineTo(size.width * 0.9333312, size.height * 0.7059382);
    path_0.lineTo(size.width * 0.9358705, size.height * 0.6904426);
    path_0.lineTo(size.width * 0.9233973, size.height * 0.6779502);
    path_0.lineTo(size.width * 0.9100635, size.height * 0.6667722);
    path_0.lineTo(size.width * 0.8955292, size.height * 0.6680003);
    path_0.lineTo(size.width * 0.8606489, size.height * 0.6648341);
    path_0.lineTo(size.width * 0.8164722, size.height * 0.6822486);
    path_0.lineTo(size.width * 0.7452668, size.height * 0.7216737);
    path_0.lineTo(size.width * 0.6578271, size.height * 0.7673066);
    path_0.lineTo(size.width * 0.5956312, size.height * 0.7950644);
    path_0.lineTo(size.width * 0.5511995, size.height * 0.8046592);
    path_0.lineTo(size.width * 0.4933703, size.height * 0.7980100);
    path_0.lineTo(size.width * 0.4545909, size.height * 0.7847692);
    path_0.lineTo(size.width * 0.03892820, size.height * 0.5665064);
    path_0.lineTo(size.width * 0.008340239, size.height * 0.5365131);
    path_0.lineTo(size.width * 0.003527337, size.height * 0.5134759);
    path_0.lineTo(size.width * 0.01826353, size.height * 0.4914078);
    path_0.lineTo(size.width * 0.04395359, size.height * 0.4764591);
    path_0.lineTo(size.width * 0.05453560, size.height * 0.4598985);
    path_0.lineTo(size.width * 0.05588492, size.height * 0.4498047);
    path_0.lineTo(size.width * 0.03163979, size.height * 0.4302409);
    path_0.lineTo(size.width * 0.01340813, size.height * 0.4089884);
    path_0.lineTo(size.width * 0.008340239, size.height * 0.3902881);
    path_0.lineTo(size.width * 0.02125964, size.height * 0.3624440);
    path_0.lineTo(size.width * 0.04919147, size.height * 0.3438013);
    path_0.lineTo(size.width * 0.08431610, size.height * 0.3354250);
    path_0.lineTo(size.width * 0.1433352, size.height * 0.3320572);
    path_0.lineTo(size.width * 0.1968190, size.height * 0.3464974);
    path_0.lineTo(size.width * 0.2771828, size.height * 0.3903361);
    path_0.lineTo(size.width * 0.3188096, size.height * 0.4118381);
    path_0.lineTo(size.width * 0.3344808, size.height * 0.4280629);
    path_0.lineTo(size.width * 0.3424385, size.height * 0.4426662);
    path_0.lineTo(size.width * 0.3355751, size.height * 0.4748568);
    path_0.lineTo(size.width * 0.3045834, size.height * 0.4914078);
    path_0.lineTo(size.width * 0.2930027, size.height * 0.5031039);
    path_0.lineTo(size.width * 0.2930027, size.height * 0.5199236);
    path_0.lineTo(size.width * 0.3055609, size.height * 0.5349395);
    path_0.lineTo(size.width * 0.3848516, size.height * 0.5768400);
    path_0.lineTo(size.width * 0.4280402, size.height * 0.5960393);
    path_0.lineTo(size.width * 0.4721532, size.height * 0.6004145);
    path_0.lineTo(size.width * 0.4980876, size.height * 0.5913186);
    path_0.lineTo(size.width * 0.9008627, size.height * 0.3799353);
    path_0.lineTo(size.width * 0.9333100, size.height * 0.3623480);
    path_0.lineTo(size.width * 0.9405559, size.height * 0.3441083);
    path_0.lineTo(size.width * 0.9300695, size.height * 0.3320572);
    path_0.lineTo(size.width * 0.8011411, size.height * 0.2642315);
    path_0.lineTo(size.width * 0.7637322, size.height * 0.2617081);
    path_0.lineTo(size.width * 0.7242409, size.height * 0.2749969);
    path_0.lineTo(size.width * 0.6447908, size.height * 0.3163985);
    path_0.lineTo(size.width * 0.6063088, size.height * 0.3347917);
    path_0.lineTo(size.width * 0.5646183, size.height * 0.3454132);
    path_0.lineTo(size.width * 0.5051741, size.height * 0.3411435);
    path_0.lineTo(size.width * 0.4420539, size.height * 0.3163985);
    path_0.lineTo(size.width * 0.04462294, size.height * 0.1069150);
    path_0.lineTo(size.width * 0.02193961, size.height * 0.08686183);
    path_0.lineTo(size.width * 0.01177196, size.height * 0.06529269);
    path_0.lineTo(size.width * 0.02783621, size.height * 0.03075137);
    path_0.lineTo(size.width * 0.06909118, size.height * 0.01136985);
    path_0.lineTo(size.width * 0.1378318, size.height * 0.003444537);
    path_0.lineTo(size.width * 0.1783961, size.height * 0.01136985);
    path_0.lineTo(size.width * 0.3515544, size.height * 0.09681164);
    path_0.lineTo(size.width * 0.4009477, size.height * 0.1242528);
    path_0.lineTo(size.width * 0.4404709, size.height * 0.1401994);
    path_0.lineTo(size.width * 0.4821083, size.height * 0.1407367);
    path_0.lineTo(size.width * 0.5315229, size.height * 0.1189277);
    path_0.lineTo(size.width * 0.6837296, size.height * 0.03577905);
    path_0.lineTo(size.width * 0.7055524, size.height * 0.02407338);
    path_0.lineTo(size.width * 0.7180468, size.height * 0.02709575);
    path_0.close();

    Paint paint_0_fill = Paint()..style = PaintingStyle.fill;
    paint_0_fill.color = Color(0xffffc629).withOpacity(1.0);
    canvas.drawPath(path_0, paint_0_fill);
    // Set your colors and spacing here
    final Color defaultColor = Colors.blue;
    final Color stepCompletedColor = Colors.blue.shade100;
    final double spacing = 20.0; // Adjust spacing based on your requirement

    // Use the drawOnPath method
    canvas.drawOnPath(
      path_0,
      spacing: spacing,
      steps: stepNumber,
      defaultColor: defaultColor,
      stepCompletedColor: stepCompletedColor,
    );
  }

  void drawCircle(Canvas canvas, Offset position, Paint paint) {
    canvas.drawCircle(position, 10.0, paint); // Circle radius of 10
  }

  // Simplified heart drawing function, you'll want to replace this with your actual heart drawing logic
  void drawHeart(Canvas canvas, Offset position, Paint paint) {
    Path heartPath = Path();
    // This is a placeholder, implement your heart drawing logic here
    heartPath.addOval(Rect.fromCircle(center: position, radius: 10));
    canvas.drawPath(heartPath, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

my code depends on a steps that is coming from the database as this form :

 "steps": [
                {
                    "id": 1,
                    "scholarship_type_id": 1,
                    "sort": 1,
                    "name": "Start",
                    "name_ar": "البداية",
                    "amount": 0,
                    "created_at": "2023-12-18T15:35:29.000000Z",
                    "updated_at": "2023-12-18T15:45:56.000000Z",
                    "deleted_at": null
                },
                {
                    "id": 2,
                    "scholarship_type_id": 1,
                    "sort": 2,
                    "name": "Seat",
                    "name_ar": "مقعد الدراسة",
                    "amount": 100,
                    "created_at": "2023-12-18T15:35:56.000000Z",
                    "updated_at": "2023-12-18T15:45:56.000000Z",
                    "deleted_at": null
                }
            ]

what iam looking to is that : every step has its own heart shape and i want this heart to be in another color if he didnot reach the step yet , how should i do this ?

and also a simple question if i have too many paths how to handle it , and can i use a normal svg and place my widgets dynamically on it ?


Solution

  • Let's call the circles as the steps and the heart-shaped positions as the milestones.

    To find out what the problem is, and if you are doing some parts on purpose or not, let’s walk through the code.

    The first problem is the loop. To view what is the problem, we can remove the position normalizing and use tangent.position instead. The result is:

    Without normalizing the position

    I think the solution is obvious, change the while loop:

    double maxDistance = metric.length / 2;
    while (distance < maxDistance){
      // Your codes here
    }
    

    And the result is:

    One way Path

    Now we have fixed the distance, lets put back the normalizing and see how it works:

    enter image description here

    The next, in the while loop, you are normalizing the position, I think there are some minor problem in here: The road width, and overlapping steps (caused by positions and vectors).

    I mentioned that because I guess that causes a problem:

    But how to fix it? Can I cheat?

    Instead of creating the full path, can I create one side of the road? Or is the list should be on this exact positions?

    Let us cheat a little bit. Remove the closing part of the path, so in your road path list, comment out the lines where creating the right edge:

    /*
    path_0.lineTo(size.width * 0.1673254, size.height * 0.8428274);
    path_0.lineTo(size.width * 0.2722318, size.height * 0.9009815);
    .
    .
    .
    path_0.lineTo(size.width * 0.7180468, size.height * 0.02709575);
    path_0.close();
    */
    

    Now, instead of using this path as the edge of the road, lets use it as the middle of the road. This part of the code should look like this:

    // Paint paint0Fill = Paint()..style = PaintingStyle.fill; //Removed this line
    Paint paint0Fill = Paint(); // added this line
    paint0Fill.strokeWidth = 20; // and added this line
    paint0Fill.color = const Color(0xffffc629).withOpacity(1.0);
    canvas.drawPath(path_0, paint0Fill);
    

    Since you removed the other side of the road in path, now you can use the full distance:

    double maxDistance = metric.length;
    while (distance < maxDistance){
      // Your codes here
    }
    

    Using strokeWidth

    It's better, I think. But I don't like the first step. I prefer to remove it (but it is your choice).

    Removed the first step

    I removed the first moveTo and started the moveTo from the second element.

    // path_0.moveTo(size.width * 0.7180468, size.height * 0.02709575);
    // path_0.lineTo(size.width * 0.7585899, size.height * 0.04322462);
    path_0.moveTo(size.width * 0.7585899, size.height * 0.04322462);
    

    Let's go back to the while loop. Your code uses:

    paint.color = shapeCounter < steps * 5 ? defaultColor : stepCompletedColor;
    

    But why are you multiplying the steps? I think it is more calculatable if you don't do that and use it like:

    paint.color = shapeCounter < steps ? defaultColor : stepCompletedColor;
    

    enter image description here

    You have to take more steps and of course it is your choice.

    So far, the drawOnPath looks like this:

    void drawOnPath(
      Path path, {
      required double spacing,
      required int steps,
      required Color defaultColor,
      required Color stepCompletedColor,
      double offsetFromPath =
          10.0,
    }) {
      final paint = Paint()..style = PaintingStyle.fill;
      final pathMetrics = path.computeMetrics();
      double distance = 0.0;
      int shapeCounter = 0; 
      for (final metric in pathMetrics) {
        double maxDistance = metric.length;
        while (distance < maxDistance) {
          final tangent = metric.getTangentForOffset(distance);
          if (tangent == null) continue;
          paint.color = shapeCounter < steps ? defaultColor : stepCompletedColor;
          drawCircle(this, tangent.position, paint, radius: 5);
          distance += spacing;
          shapeCounter++;
        }
        distance = 0.0;
      }
    }
    

    And the paint is:

      @override
      void paint(Canvas canvas, Size size) {
        Path path_0 = Path();
        path_0.moveTo(size.width * 0.7585899, size.height * 0.04322462);
        path_0.lineTo(size.width * 0.5463654, size.height * 0.1534690);
        path_0.lineTo(size.width * 0.5066828, size.height * 0.1674390);
        path_0.lineTo(size.width * 0.4452413, size.height * 0.1719870);
        path_0.lineTo(size.width * 0.3967298, size.height * 0.1617205);
        path_0.lineTo(size.width * 0.2607467, size.height * 0.09137139);
        path_0.lineTo(size.width * 0.1801492, size.height * 0.04966274);
        path_0.lineTo(size.width * 0.1535241, size.height * 0.03829289);
        path_0.lineTo(size.width * 0.1110580, size.height * 0.03719908);
        path_0.lineTo(size.width * 0.07781390, size.height * 0.04915422);
        path_0.lineTo(size.width * 0.06558509, size.height * 0.06854533);
        path_0.lineTo(size.width * 0.08100125, size.height * 0.09115071);
        path_0.lineTo(size.width * 0.3775313, size.height * 0.2422786);
        path_0.lineTo(size.width * 0.5260088, size.height * 0.3199390);
        path_0.lineTo(size.width * 0.5832749, size.height * 0.3077632);
        path_0.lineTo(size.width * 0.6205457, size.height * 0.2884680);
        path_0.lineTo(size.width * 0.7098659, size.height * 0.2422786);
        path_0.lineTo(size.width * 0.7729755, size.height * 0.2284525);
        path_0.lineTo(size.width * 0.8304328, size.height * 0.2393905);
        path_0.lineTo(size.width * 0.8658337, size.height * 0.2554810);
        path_0.lineTo(size.width * 0.9350524, size.height * 0.2889861);
        path_0.lineTo(size.width * 0.9755636, size.height * 0.3107759);
        path_0.lineTo(size.width * 0.9982788, size.height * 0.3450582);
        path_0.lineTo(size.width * 0.9873037, size.height * 0.3732573);
        path_0.lineTo(size.width * 0.9598287, size.height * 0.3941836);
        path_0.lineTo(size.width * 0.9035826, size.height * 0.4244648);
        path_0.lineTo(size.width * 0.7605979, size.height * 0.4929238);
        path_0.lineTo(size.width * 0.6050551, size.height * 0.5751610);
        path_0.lineTo(size.width * 0.5392469, size.height * 0.6110743);
        path_0.lineTo(size.width * 0.4607637, size.height * 0.6302256);
        path_0.lineTo(size.width * 0.3966235, size.height * 0.6215039);
        path_0.lineTo(size.width * 0.3524150, size.height * 0.6004145);
        path_0.lineTo(size.width * 0.2721787, size.height * 0.5561536);
        path_0.lineTo(size.width * 0.2467648, size.height * 0.5403798);
        path_0.lineTo(size.width * 0.2365122, size.height * 0.5156923);
        path_0.lineTo(size.width * 0.2399545, size.height * 0.4979611);
        path_0.lineTo(size.width * 0.2555832, size.height * 0.4740029);
        path_0.lineTo(size.width * 0.2765772, size.height * 0.4647535);
        path_0.lineTo(size.width * 0.2883704, size.height * 0.4458325);
        path_0.lineTo(size.width * 0.2851406, size.height * 0.4353166);
        path_0.lineTo(size.width * 0.2691613, size.height * 0.4266237);
        path_0.lineTo(size.width * 0.1527698, size.height * 0.3681721);
        path_0.lineTo(size.width * 0.1179639, size.height * 0.3632883);
        path_0.lineTo(size.width * 0.09614118, size.height * 0.3655431);
        path_0.lineTo(size.width * 0.07278851, size.height * 0.3788031);
        path_0.lineTo(size.width * 0.06388517, size.height * 0.3943659);
        path_0.lineTo(size.width * 0.07820701, size.height * 0.4161270);
        path_0.lineTo(size.width * 0.1041414, size.height * 0.4326012);
        path_0.lineTo(size.width * 0.1138416, size.height * 0.4567322);
        path_0.lineTo(size.width * 0.1047683, size.height * 0.4849218);
        path_0.lineTo(size.width * 0.08567604, size.height * 0.4976637);
        path_0.lineTo(size.width * 0.06644568, size.height * 0.5117584);
        path_0.lineTo(size.width * 0.06388517, size.height * 0.5238287);
        path_0.lineTo(size.width * 0.07097172, size.height * 0.5405237);
        path_0.lineTo(size.width * 0.3899620, size.height * 0.7089222);
        path_0.lineTo(size.width * 0.5029536, size.height * 0.7667981);
        path_0.lineTo(size.width * 0.5419349, size.height * 0.7749921);
        path_0.lineTo(size.width * 0.5645120, size.height * 0.7719985);
        path_0.lineTo(size.width * 0.6117592, size.height * 0.7522140);
        path_0.lineTo(size.width * 0.6923780, size.height * 0.7080011);
        path_0.lineTo(size.width * 0.7431206, size.height * 0.6833137);
        path_0.lineTo(size.width * 0.8080151, size.height * 0.6485037);
        path_0.lineTo(size.width * 0.8541361, size.height * 0.6376711);
        path_0.lineTo(size.width * 0.8930962, size.height * 0.6366445);
        path_0.lineTo(size.width * 0.9610293, size.height * 0.6549898);
        path_0.lineTo(size.width * 0.9933278, size.height * 0.6862209);
        path_0.lineTo(size.width * 0.9846051, size.height * 0.7166077);
        path_0.lineTo(size.width * 0.9506279, size.height * 0.7450563);
        path_0.lineTo(size.width * 0.8039672, size.height * 0.8161442);
        path_0.lineTo(size.width * 0.7521408, size.height * 0.8452837);
        path_0.lineTo(size.width * 0.7301375, size.height * 0.8622089);
        path_0.lineTo(size.width * 0.7290750, size.height * 0.8811587);
        path_0.lineTo(size.width * 0.7547226, size.height * 0.8975274);
        path_0.lineTo(size.width * 0.7695332, size.height * 0.9127160);
        path_0.lineTo(size.width * 0.7793927, size.height * 0.9243449);
        path_0.lineTo(size.width * 0.7789465, size.height * 0.9451848);
        path_0.lineTo(size.width * 0.7551476, size.height * 0.9706687);
        path_0.lineTo(size.width * 0.7082404, size.height * 0.9895033);
        path_0.lineTo(size.width * 0.6569877, size.height * 0.9948572);
        path_0.lineTo(size.width * 0.6282378, size.height * 0.9920267);
        path_0.lineTo(size.width * 0.5725016, size.height * 0.9744298);
        path_0.lineTo(size.width * 0.5310873, size.height * 0.9512775);
        path_0.lineTo(size.width * 0.4816515, size.height * 0.9247095);
        path_0.lineTo(size.width * 0.4392491, size.height * 0.9106531);
        path_0.lineTo(size.width * 0.3966235, size.height * 0.9176190);
        path_0.lineTo(size.width * 0.3565054, size.height * 0.9344099);
        path_0.lineTo(size.width * 0.3246850, size.height * 0.9391881);
        path_0.lineTo(size.width * 0.2660696, size.height * 0.9344099);
        path_0.lineTo(size.width * 0.2456280, size.height * 0.9260816);
        path_0.lineTo(size.width * 0.1933873, size.height * 0.9024783);
        path_0.lineTo(size.width * 0.1445146, size.height * 0.8758240);
        path_0.lineTo(size.width * 0.1218737, size.height * 0.8622089);
    
        Paint paint0Fill = Paint();
        paint0Fill.strokeWidth = 20;
        paint0Fill.color = const Color(0xffffc629).withOpacity(1.0);
        canvas.drawPath(path_0, paint0Fill);
    
        const Color defaultColor = Colors.blue;
        final Color stepCompletedColor = Colors.blue.shade100;
        const double spacing = 20.0;
    
        canvas.drawOnPath(
          path_0,
          spacing: spacing,
          steps: stepNumber,
          defaultColor: defaultColor,
          stepCompletedColor: stepCompletedColor,
        );
      }
    

    Last part is drawing milestones. You have too many options, but the problem is you didn't mentioned where you want to put those!

    Suppose you want to add milestones every 10 steps. Now, you can add a condition to check if it is the place you want to put the heart shape or not.

    Define a list List milestones = []; before your for loop and then, in the while loop add:

    if (shapeCounter % 10 == 0) {
      milestones.add({
        'position': tangent.position,
        'color': paint.color,
        'paint': paint
      });
    }
    

    After the while loop finished:

    for (var milestone in milestones) {
      drawHeart(this, milestone['position'], milestone['color'], milestone['paint']);
    }
    

    Because when you draw next element, it shows on top previous elements, and it leads to having parts of the circles on top of the hearts. We don't want that, right? So, a quick solution is drawing all the circles and after that, drawing all the hearts.

    Drawing the heart-shape is like:

    void drawHeart(Canvas canvas, Offset position, Color color, Paint paint) {
      Paint paint = Paint();
      paint.color = color;
      double width = 30;
      double height = 30;
      position -= Offset(0.7 * width, 0.7 * height);
    
      Path path = Path();
      path.moveTo(position.dx + 0.5 * width, position.dy + height * 0.35);
      path.cubicTo(
          position.dx + 0.2 * width,
          position.dy + height * 0.1,
          position.dx + -0.25 * width,
          position.dy + height * 0.6,
          position.dx + 0.5 * width,
          position.dy + height);
      path.moveTo(position.dx + 0.5 * width, position.dy + height * 0.35);
      path.cubicTo(
          position.dx + 0.8 * width,
          position.dy + height * 0.1,
          position.dx + 1.25 * width,
          position.dy + height * 0.6,
          position.dx + 0.5 * width,
          position.dy + height);
    
      canvas.drawPath(path, paint);
    }
    

    The final code looks like this:

    void drawOnPath(
      Path path, {
      required double spacing,
      required int steps,
      required Color defaultColor,
      required Color stepCompletedColor,
      double offsetFromPath = 10.0,
    }) {
      final paint = Paint()..style = PaintingStyle.fill;
      final pathMetrics = path.computeMetrics();
      double distance = 0.0;
      int shapeCounter = 0;
    
      List milestones = [];
      for (final metric in pathMetrics) {
        double maxDistance = metric.length;
        while (distance < maxDistance) {
          final tangent = metric.getTangentForOffset(distance);
          if (tangent == null) continue;
          paint.color = shapeCounter < steps ? defaultColor : stepCompletedColor;
          drawCircle(this, tangent.position, paint, radius: 5);
          distance += spacing;
          shapeCounter++;
          if (shapeCounter % 10 == 0) {
            milestones.add({
              'position': tangent.position,
              'color': paint.color,
              'paint': paint
            });
          }
        }
        distance = 0.0;
        for (var milestone in milestones) {
          drawHeart(
              this, milestone['position'], milestone['color'], milestone['paint']);
        }
      }
    }
    

    And the result is:

    The final result.

    The last point doesn't match the (shapeCounter % 10 == 0) condition but if you want, you can add it by adding the last position to the list. :)

    The final part, if you want to have different steps number in each milestone, you must change the condition where you put the heart-shapes.

    Suppose you have a list like List<int> stepsInMilestones = [5, 12, 4, 14, 9, 4, 5, 12, 7, 14, 9, 4]; which defines the counts of steps in milestones.

    First change the class:

    class RPSCustomPainter1 extends CustomPainter {
      final int stepNumber;
      final List<int> stepsInMilestones; // Added this and update the class
    
      RPSCustomPainter1(
          {required this.stepNumber, required this.stepsInMilestones});
    
      // Your old codes here
    
        canvas.drawOnPath(path_0,
            spacing: spacing,
            steps: stepNumber,
            defaultColor: defaultColor,
            stepCompletedColor: stepCompletedColor,
            stepsInMilestones: stepsInMilestones, // Added this line
        );
    }
    

    Pass this to RPSCustomPainter1 like:

    RPSCustomPainter1(stepNumber: 30, stepsInMilestones: stepsInMilestones)
    

    And update the drawOnPath:

     void drawOnPath(Path path,
          {required double spacing,
          required int steps,
          required Color defaultColor,
          required Color stepCompletedColor,
          double offsetFromPath = 10.0,
          required List<int> stepsInMilestones, // Added this line
    }) {
     // Your code here
    }
    

    Now, You must change the condition, and check if it reached your desired step for that particular milestone instead of checking the 10th steps for every milestones.

    First, add these two variables before the for loop:

    int milestoneCounter = 0;
    int totalSteps = stepsInMilestones[milestoneCounter];
    

    Remove the previous condition from the loop:

    // if (shapeCounter % 10 == 0) {
    //   milestones.add({
    //     'position': tangent.position,
    //     'color': paint.color,
    //     'paint': paint
    //   });
    // }
    

    And add the new one:

    if (shapeCounter == totalSteps) {
      // Add this milestone position
      milestones.add({
        'position': tangent.position,
        'color': paint.color,
        'paint': paint
      });
      milestoneCounter += 1; // Move to the next milestone
      if (milestoneCounter >= stepsInMilestones.length - 1) {
        // Added this condition to prevent index out of range error.
        // I added steps randomly and there is a chance that
        // I needed to put more values in stepsInMilestones.
        milestoneCounter = stepsInMilestones.length - 1;
      }
      totalSteps += stepsInMilestones[
          milestoneCounter]; // This is the next milestone step
    }
    

    The final drawOnPath is:

    void drawOnPath(Path path,
        {required double spacing,
        required int steps,
        required Color defaultColor,
        required Color stepCompletedColor,
        double offsetFromPath = 10.0,
        required List<int> stepsInMilestones}) {
      final paint = Paint()..style = PaintingStyle.fill;
      final pathMetrics = path.computeMetrics();
      double distance = 0.0;
      int shapeCounter = 0;
    
      List milestones = [];
      int milestoneCounter = 0;
      int totalSteps = stepsInMilestones[milestoneCounter];
      for (final metric in pathMetrics) {
        double maxDistance = metric.length;
        while (distance < maxDistance) {
          final tangent = metric.getTangentForOffset(distance);
          if (tangent == null) continue;
          paint.color = shapeCounter < steps ? defaultColor : stepCompletedColor;
          drawCircle(this, tangent.position, paint, radius: 5);
          distance += spacing;
          shapeCounter++;
          if (shapeCounter == totalSteps) {
            milestones.add({
              'position': tangent.position,
              'color': paint.color,
              'paint': paint
            });
            milestoneCounter += 1; // Move to the next milestone
            if (milestoneCounter >= stepsInMilestones.length - 1) {
              // Added this condition to prevent index out of range error.
              // I added steps randomly and there is a chance that
              // I needed to put more values in stepsInMilestones.
              milestoneCounter = stepsInMilestones.length - 1;
            }
            totalSteps += stepsInMilestones[
                milestoneCounter]; // This is the next milestone step
          }
        }
        distance = 0.0;
        for (var milestone in milestones) {
          drawHeart(
              this, milestone['position'], milestone['color'], milestone['paint']);
        }
      }
    }
    

    And the result is:

    The road with different steps in milestones