4

I'm trying to define racecourse with a dynamic line that user can draw on a canvas element. So when line has been drawn, program should add sidelines for it as shown in the picture below:

idea of the result

I have managed to mimic the idea already by using line normals but can't get it done correctly. At the moment I put point in the midway of the lines in the direction of the line normals and draw outlines using those points. While generated line is relatively smooth in cases on large turns, tight turns tend to produce loops.

As seen in image below:

current state of result

Here is current code that generates points for side lines above (I'm using p5.js JavaScript library):

var sketch = function (p) {
  with(p) {

    let handpoints;
    let walkhandpoints;
    let collect;
    let parsepath;
    let shapse;
    let step;
    let tmp; 
    let dorender;
    let lineoffset;

    p.setup = function() {
      createCanvas(600, 600);
      handpoints = [];
      walkhandpoints = 10;
      collect = true;
      parsepath = false;
      shapes = [];
      step = 2;
      tmp = [];
      dorender = true;
      lineoffset = 15;
    };

    p.draw = function() {
      if(dorender) {
        background(220);
        update();
        for (let shape of shapes) {
          shape.show();
        }
      }
    };

    function update() {
      if (mouseIsPressed) {
        if (collect) {
          let mouse = createVector(mouseX, mouseY);
          handpoints.push(mouse);
          Shape.drawPath(handpoints);
          parsepath = true;
        }
      } else if (parsepath) {
        let tmp1 = Shape.cleanPath(handpoints, step);
        let s1 = new Shape(tmp1, 1, 'line', color(175));
        shapes.push(s1);
        let tmp2 = Line.sidePoints(tmp1, lineoffset);
        let s2 = new Shape(tmp2.sideA, 1, 'line', color(175,120,0));
        let s3 = new Shape(tmp2.sideB, 1, 'line', color(175,0, 120));
        shapes.push(s2);
        shapes.push(s3);
        handpoints = [];
        parsepath = false;
        //dorender = false;
      }
    }

    class Shape {
      constructor(points, mag, type = 'line', shader = color(200, 0, 100)) {
        this.points = points.slice().map(item => item.copy());
        this.type = type;
        this.mag = mag;
        this.shader = shader;
      }

      static cleanPath(points, step) {
        let tmp = [];
        let output = [];
        for (let i = 1; i < points.length; i++) {
          let prev = points[i - 1];
          let curr = points[i];
          if (!prev.equals(curr)) {
            tmp.push(prev.copy())
            if (i === points.length - 1) {
              tmp.push(curr.copy())
            }
          }
        }
        for (let i = 0; i < tmp.length; i++) {
          if(i % step === 0) {
            output.push(tmp[i]);
          }
        }
        output.push(output[0]);
        return output;
      }
  
      static drawPath(points, mag = 1, type = 'line', shader = color(175)) {
        let s = new Shape(points, mag, type, shader);
        s.show();
      }
  
      show() {

        for (let i = 0; i < this.points.length; i++) {
          if (this.type === 'line' && i > 0) {
            let prev = this.points[i - 1];
            let curr = this.points[i];
            strokeWeight(this.mag);
            stroke(this.shader);
            line(prev.x, prev.y, curr.x, curr.y);
          } else if (this.type === 'point') {
            noStroke();
            fill(this.shader);
            ellipse(this.points[i].x, this.points[i].y, this.mag * 2, this.mag * 2);
          }
        }
      }
    }

    class Line {
      static sidePoints(points, lineoffset) {
        let sideA = [];
        let sideB = [];
        for(let i = 1; i < points.length; i++) {

          // take consecutive points
          let prev = points[i-1];
          let curr = points[i];

          // calculate normals
          let dx = curr.x-prev.x;
          let dy = curr.y-prev.y;
          let a = createVector(-dy, dx).normalize();
          let b = createVector(dy, -dx).normalize();

          // calculate midway of the two points
          let px = (prev.x+curr.x)/2;
          let py = (prev.y+curr.y)/2;
          let p = createVector(px,py);
      
          // put created points back along drawed line
          a.mult(lineoffset).add(p);
          b.mult(lineoffset).add(p);
          sideA.push(a);
          sideB.push(b);
        }

        // close paths
        if(!sideA[0].equals(sideA[sideA.length-1])) {
          sideA.push(sideA[0]);
        }
        if(!sideB[0].equals(sideB[sideB.length-1])) {
          sideB.push(sideB[0]);
        }
        return {sideA, sideB};
      }
    }


  }
};

let node = document.createElement('div');
window.document.getElementById('p5-container').appendChild(node);
new p5(sketch, node);
body {
  background-color:#ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script>
    <div id="p5-container"></div>
  • Firstly I'd like to find a way to draw those points in the corresponding corner points of the drawn line so when drawn line has only few points the outlines would retain the drawn shape.
  • Secondly is there some good way to reduce points on the areas where there are several of them to reduce those loops in small corners and other type errors in generated lines?
  • Idea is to get points for lines, so it would be easy to detect with line intersection if race car velocity vector crosses it.
  • Unfortunately I'm not very familiar with math notations so please try to use easy to understand version of them if there is some fancy math that would do the job.
Donald Duck
  • 8,409
  • 22
  • 75
  • 99

1 Answers1

1

In the future, please try to post a minimal example. I was not able to run your code as you posted it.

That being said, one option you could consider is using the strokeWeight() function to draw the path at different widths. Here's an example:

const path = [];

function setup() {
  createCanvas(400, 400);
  
  // Add some default points to the path.
  path.push(createVector(0, 0));
  path.push(createVector(width/4, height/4));
}

function draw() {
  background(220);
  
  // Draw the path with a thick gray line.
  strokeWeight(50);
  stroke(200);
  for(let i = 1; i < path.length; i++){
    const prevPoint = path[i-1];
    const nextPoint = path[i];
    line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
  }
 
  // Draw the path with a thin black line.
  strokeWeight(1);
  stroke(0);
  for(let i = 1; i < path.length; i++){
    const prevPoint = path[i-1];
    const nextPoint = path[i];    
     line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
  }
}

// Add a point to the path when the user clicks.
function mousePressed(){
 path.push(createVector(mouseX, mouseY));
}

path

The trick here is to draw the path in two passes. First you draw the path using a thick line, and then you draw the path again, this time using a thin line.

Kevin Workman
  • 41,537
  • 9
  • 68
  • 107
  • Thank you @KevinWorkman . I'll try to do that minimal example in the future. But while I need at least 2 lines for each side of the "road", strokeWeight() just changes pixel representation of a single line on the center of the "road". – lazydistribution Sep 07 '20 at 10:05
  • @lazydistribution Why do you need two lines? – Kevin Workman Sep 07 '20 at 15:35
  • My idea was to detect with line intersection if the velocity vector of the race car crosses the road bench to know that the game is over. – lazydistribution Sep 08 '20 at 07:57
  • @lazydistribution You could do collision detection with the thick line that was drawn as a result of my suggestion. – Kevin Workman Sep 09 '20 at 03:11
  • how would I do that? – lazydistribution Sep 09 '20 at 09:00
  • @lazydistribution You might start by reading up on collision detection. One approach that comes to mind is using the thick line as a mask and then checking the pixels of that mask, but there are probably smarter ways to do it. – Kevin Workman Sep 10 '20 at 03:49
  • Thanks. I'm aware that collision detection by pixels can be made, but I thought it's slower than make intersection test with lines. At least as I know using pixels you need to test every pixel under velocity vector to test if the point is of the road while line intersection point with sideline and velocity vector can be calculated relatively fast. Or is there some algorithm that could generate sidelines using that pixel detection? Or Is this pixel detection as fast as line intersection test? – lazydistribution Sep 10 '20 at 07:55
  • Of course there may be way to draw thick line with lineWeight(), take a snapshot from the canvas, and using marching squares to generate the outer lines of the thick line of the snapshot, but that is lot of more work what I wish I don't have to do. But of course I will if needed. – lazydistribution Sep 10 '20 at 08:01