Search code examples
javascriptmathcanvasedgesarrows

How to draw parallel edges (arrows) between vertices with canvas?


I'm working on a flow-network visualization with Javascript. Vertices are represented as circles and edges are represented as arrows.

Here is my Edge class:

function Edge(u, v) {
  this.u = u; // start vertex
  this.v = v; // end vertex

  this.draw = function() {
    var x1 = u.x;
    var y1 = u.y;
    var x2 = v.x;
    var y2 = v.y;

    context.beginPath();
    context.moveTo(x1, y1);
    context.lineTo(x2, y2);
    context.stroke();

    var dx = x1 - x2;
    var dy = y1 - y2;
    var length = Math.sqrt(dx * dx + dy * dy);

    x1 = x1 - Math.round(dx / ((length / (radius))));
    y1 = y1 - Math.round(dy / ((length / (radius))));
    x2 = x2 + Math.round(dx / ((length / (radius))));
    y2 = y2 + Math.round(dy / ((length / (radius))));

    // calculate the angle of the edge
    var deg = (Math.atan(dy / dx)) * 180.0 / Math.PI;
    if (dx < 0) {
      deg += 180.0;
    }
    if (deg < 0) {
      deg += 360.0;
    }
    // calculate the angle for the two triangle points
    var deg1 = ((deg + 25 + 90) % 360) * Math.PI * 2 / 360.0;
    var deg2 = ((deg + 335 + 90) % 360) * Math.PI * 2 / 360.0;
    // calculate the triangle points
    var arrowx = [];
    var arrowy = [];
    arrowx[0] = x2;
    arrowy[0] = y2;
    arrowx[1] = Math.round(x2 + 12 * Math.sin(deg1));
    arrowy[1] = Math.round(y2 - 12 * Math.cos(deg1));
    arrowx[2] = Math.round(x2 + 12 * Math.sin(deg2));
    arrowy[2] = Math.round(y2 - 12 * Math.cos(deg2));

    context.beginPath();
    context.moveTo(arrowx[0], arrowy[0]);
    context.lineTo(arrowx[1], arrowy[1]);
    context.lineTo(arrowx[2], arrowy[2]);
    context.closePath();
    context.stroke();
    context.fillStyle = "black";
    context.fill();
  };
}

Given the code

var canvas = document.getElementById('canvas'); // canvas element
var context = canvas.getContext("2d");
context.lineWidth = 1;
context.strokeStyle = "black";

var radius = 20; // vertex radius

var u = {
  x: 50,
  y: 80
};

var v = {
  x: 150,
  y: 200
};

var e = new Edge(u, v);

e.draw();

The draw() function will draw an edge between two vertices like this:

edge

If we add the code

var k = new Edge(v, u);
k.draw();

We will get:

not good

but I want to draw edges both directions as following: (sorry for my bad paint skills)

both edges

Of course the vertices and the edge directions are not fixed. A working example (with drawing vertex fucntion) on JSFiddle: https://jsfiddle.net/Romansko/0fu01oec/18/


Solution

  • Aligning axis to a line.

    It can make everything a little easier if you rotate the rendering to align with the line. Once you do that it is then easy to draw above or below the line as that is just in the y direction and along the line is the x direction.

    Thus if you have a line

    const line = { 
       p1 : { x : ? , y : ? },
       p2 : { x : ? , y : ? },
    };
    

    Convert it to a vector and normalise that vector

    // as vector from p1 to p2
    var nx = line.p2.x - line.p1.x;    
    var ny = line.p2.y - line.p1.y;
    
    // then get length
    const len = Math.sqrt(nx * nx + ny * ny);
    
    // use the length to normalise the vector
    nx /= len;
    ny /= len;
    

    The normalised vector represents the new x axis we want to render along, and the y axis is at 90 deg to that. We can use setTransform to set both axis and the origin (0,0) point at the start of the line.

    ctx.setTransform(
        nx, ny,   // the x axis
        -ny, nx,  // the y axis at 90 deg to the x axis
        line.p1.x, line.p1.y  // the origin (0,0)
    )
    

    Now rendering the line and arrow heads is easy as they are axis aligned

    ctx.beginPath();
    ctx.lineTo(0,0); // start of line
    ctx.lineTo(len,0); // end of line
    ctx.stroke();
    
    // add the arrow head
    ctx.beginPath();
    ctx.lineTo(len,0); // tip of arrow
    ctx.lineTo(len - 10, 10);
    ctx.lineTo(len - 10, -10);
    ctx.fill();
    

    To render two lines offset from the center

    var offset = 10;
    ctx.beginPath();
    ctx.lineTo(0,offset); // start of line
    ctx.lineTo(len,offset); // end of line
    ctx.moveTo(0,-offset); // start of second line
    ctx.lineTo(len,-offset); // end of second line
    ctx.stroke();
    
    // add the arrow head
    ctx.beginPath();
    ctx.lineTo(len,offset); // tip of arrow
    ctx.lineTo(len - 10, offset+10);
    ctx.lineTo(len - 10, offset-10);
    ctx.fill();
    
    offset = -10;
    
    // add second  arrow head
    ctx.beginPath();
    ctx.lineTo(0,offset); // tip of arrow
    ctx.lineTo(10, offset+10);
    ctx.lineTo(10, offset-10);
    ctx.fill();
    

    And you can reset the transform with

    ctx.setTransform(1,0,0,1,0,0);  // restore default transform