Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw an arrow on HTML5 Canvas between two objects

I'm working on concept maps application, which has a set of nodes and links. I have connected the links to nodes using the center of the node as reference. Since I have nodes with different size and shapes, it is not advisable to draw arrow-head for the link by specifying height or width of the shape. My approach is to draw a link, starting from one node, pixel by pixel till the next node is reached(here the nodes are of different color from that of the background), then by accessing the pixel value, I want to be able to decide the point of intersection of link and the node, which is actually the co-ordinate for drawing the arrow-head.

It would be great, if I could get some help with this.

Sample Code: http://jsfiddle.net/9tUQP/4/

Here the green squares are nodes and the line starting from left square and entering into the right square is the link. I want the arrow-head to be drawn at the point of intersection of link and the right square.

like image 207
allwyn.menezes Avatar asked Nov 21 '11 12:11

allwyn.menezes


People also ask

How do I draw a rectangle in HTML canvas?

The rect() method creates a rectangle. Tip: Use the stroke() or the fill() method to actually draw the rectangle on the canvas.


1 Answers

I've created an example that does this. I use Bresenham's Line Algorithm to walk the line of whole canvas pixels and check the alpha at each point; whenever it crosses a 'threshold' point I record that as a candidate. I then use the first and last such points to draw an arrow (with properly-rotated arrowhead).

Here's the example: http://phrogz.net/tmp/canvas_shape_edge_arrows.html

Refresh the example to see a new random test case. It 'fails' if you have another 'shape' already overlapping one of the end points. One way to solve this would be to draw your shapes first to a blank canvas and then copy the result (drawImage) to the final canvas.

For Stack Overflow posterity (in case my site is down) here's the relevant code:

<!DOCTYPE html>
<html><head>
  <meta charset="utf-8">
  <title>HTML5 Canvas Shape Edge Detection (for Arrow)</title>
  <style type="text/css">
    body { background:#eee; margin:2em 4em; text-align:center; }
    canvas { background:#fff; border:1px solid #666 }
  </style>
</head><body>
  <canvas width="800" height="600"></canvas>
  <script type="text/javascript">
    var ctx = document.querySelector('canvas').getContext('2d');

    for (var i=0;i<20;++i) randomCircle(ctx,'#999');

    var start = randomDiamond(ctx,'#060');
    var end   = randomDiamond(ctx,'#600');
    ctx.lineWidth = 2;
    ctx.fillStyle = ctx.strokeStyle = '#099';
    arrow(ctx,start,end,10);

    function arrow(ctx,p1,p2,size){
      ctx.save();

      var points = edges(ctx,p1,p2);
      if (points.length < 2) return 
      p1 = points[0], p2=points[points.length-1];

      // Rotate the context to point along the path
      var dx = p2.x-p1.x, dy=p2.y-p1.y, len=Math.sqrt(dx*dx+dy*dy);
      ctx.translate(p2.x,p2.y);
      ctx.rotate(Math.atan2(dy,dx));

      // line
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(0,0);
      ctx.lineTo(-len,0);
      ctx.closePath();
      ctx.stroke();

      // arrowhead
      ctx.beginPath();
      ctx.moveTo(0,0);
      ctx.lineTo(-size,-size);
      ctx.lineTo(-size, size);
      ctx.closePath();
      ctx.fill();

      ctx.restore();
    }

    // Find all transparent/opaque transitions between two points
    // Uses http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
    function edges(ctx,p1,p2,cutoff){
      if (!cutoff) cutoff = 220; // alpha threshold
      var dx = Math.abs(p2.x - p1.x), dy = Math.abs(p2.y - p1.y),
          sx = p2.x > p1.x ? 1 : -1,  sy = p2.y > p1.y ? 1 : -1;
      var x0 = Math.min(p1.x,p2.x), y0=Math.min(p1.y,p2.y);
      var pixels = ctx.getImageData(x0,y0,dx+1,dy+1).data;
      var hits=[], over=null;
      for (x=p1.x,y=p1.y,e=dx-dy; x!=p2.x||y!=p2.y;){
        var alpha = pixels[((y-y0)*(dx+1)+x-x0)*4 + 3];
        if (over!=null && (over ? alpha<cutoff : alpha>=cutoff)){
          hits.push({x:x,y:y});
        }
        var e2 = 2*e;
        if (e2 > -dy){ e-=dy; x+=sx }
        if (e2 <  dx){ e+=dx; y+=sy  }
        over = alpha>=cutoff;
      }
      return hits;
    }

    function randomDiamond(ctx,color){
      var x = Math.round(Math.random()*(ctx.canvas.width  - 100) + 50),
          y = Math.round(Math.random()*(ctx.canvas.height - 100) + 50);
      ctx.save();
      ctx.fillStyle = color;
      ctx.translate(x,y);
      ctx.rotate(Math.random() * Math.PI);
      var scale = Math.random()*0.8 + 0.4;
      ctx.scale(scale,scale);
      ctx.lineWidth = 5/scale;
      ctx.fillRect(-50,-50,100,100);
      ctx.strokeRect(-50,-50,100,100);
      ctx.restore();
      return {x:x,y:y};
    }

    function randomCircle(ctx,color){
      ctx.save();
      ctx.beginPath();
      ctx.arc(
        Math.round(Math.random()*(ctx.canvas.width  - 100) + 50),
        Math.round(Math.random()*(ctx.canvas.height - 100) + 50),
        Math.random()*20 + 10,
        0, Math.PI * 2, false
      );
      ctx.fillStyle = color;
      ctx.fill();
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.restore();
    }

  </script>
</body></html>
like image 178
Phrogz Avatar answered Sep 24 '22 17:09

Phrogz