4

I'm using the wPaint plugin and I am attempting to add a few more features. What I need is a drawn line to end with an "arrowhead". I have tried just about everything I could think of, but I can only get half of the arrow ( imagine <-----, but the head only extends to the bottom or the top, but never both directions.)

Here is the function for drawing the line (with the half arrowhead):

  drawArrowMove: function(e, _self)
  {
        var xo = _self.canvasTempLeftOriginal;
        var yo = _self.canvasTempTopOriginal;

        if(e.pageX < xo) { e.x = e.x + e.w; e.w = e.w * -1}
        if(e.pageY < yo) { e.y = e.y + e.h; e.h = e.h * -1}

        _self.ctxTemp.lineJoin = "round";
        _self.ctxTemp.beginPath();
        _self.ctxTemp.moveTo(e.x, e.y);
        _self.ctxTemp.lineTo(e.x + e.w, e.y + e.h);

        _self.ctxTemp.closePath();
        _self.ctxTemp.moveTo(e.x, e.y);

        _self.ctxTemp.lineTo(15,10);                   
        _self.ctxTemp.stroke();
  }

Any help/ideas/tips would be helpful.

Thanks.

JP4
  • 1,008
  • 2
  • 15
  • 25
Kisaragi
  • 2,198
  • 3
  • 16
  • 28
  • can you find the solution.. iam also using wapint and trying to make line into arrow.. tried your code it make half arrow head and also on the left side – Umer Fayyaz Apr 20 '22 at 09:40

5 Answers5

14

This is how to create a line object that draws arrowheads on both ends

The interesting part is calculating the angle of the arrowheads like this:

var startRadians=Math.atan((this.y2-this.y1)/(this.x2-this.x1));
startRadians+=((this.x2>=this.x1)?-90:90)*Math.PI/180;

var endRadians=Math.atan((this.y2-this.y1)/(this.x2-this.x1));
endRadians+=((this.x2>=this.x1)?90:-90)*Math.PI/180;

The rest is just drawing the line and 2 triangles for arrowheads the calculated rotations

Line.prototype.drawArrowhead=function(ctx,x,y,radians){
    ctx.save();
    ctx.beginPath();
    ctx.translate(x,y);
    ctx.rotate(radians);
    ctx.moveTo(0,0);
    ctx.lineTo(5,20);
    ctx.lineTo(-5,20);
    ctx.closePath();
    ctx.restore();
    ctx.fill();
}

Here is code and a Fiddle: http://jsfiddle.net/m1erickson/Sg7EZ/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var context=canvas.getContext("2d");

    function Line(x1,y1,x2,y2){
        this.x1=x1;
        this.y1=y1;
        this.x2=x2;
        this.y2=y2;
    }
    Line.prototype.drawWithArrowheads=function(ctx){

        // arbitrary styling
        ctx.strokeStyle="blue";
        ctx.fillStyle="blue";
        ctx.lineWidth=1;

        // draw the line
        ctx.beginPath();
        ctx.moveTo(this.x1,this.y1);
        ctx.lineTo(this.x2,this.y2);
        ctx.stroke();

        // draw the starting arrowhead
        var startRadians=Math.atan((this.y2-this.y1)/(this.x2-this.x1));
        startRadians+=((this.x2>this.x1)?-90:90)*Math.PI/180;
        this.drawArrowhead(ctx,this.x1,this.y1,startRadians);
        // draw the ending arrowhead
        var endRadians=Math.atan((this.y2-this.y1)/(this.x2-this.x1));
        endRadians+=((this.x2>this.x1)?90:-90)*Math.PI/180;
        this.drawArrowhead(ctx,this.x2,this.y2,endRadians);


    }
    Line.prototype.drawArrowhead=function(ctx,x,y,radians){
        ctx.save();
        ctx.beginPath();
        ctx.translate(x,y);
        ctx.rotate(radians);
        ctx.moveTo(0,0);
        ctx.lineTo(5,20);
        ctx.lineTo(-5,20);
        ctx.closePath();
        ctx.restore();
        ctx.fill();
    }

    // create a new line object
    var line=new Line(50,50,150,150);
    // draw the line
    line.drawWithArrowheads(context);

}); // end $(function(){});
</script>

</head>

<body>
    <canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
markE
  • 102,905
  • 11
  • 164
  • 176
  • 1
    Make sure you check out @PierreDuc's anwer and replace the `((this.x2 > this.x1)?-90:90)*Math.PI/180;` with `((this.x2 >= this.x1)?-90:90)*Math.PI/180;` - i.e., use greater than or equals – ElDog Apr 12 '15 at 16:42
  • 2
    Did not work for me ONLY when lines are angled. It works fine if lines are 0 or 90°. I took 1 hour and finally found that https://stackoverflow.com/questions/808826/draw-arrow-on-canvas-tag/26080467#26080467 – philippe Apr 28 '20 at 19:43
  • Only works for some range of angles, breaks for others. Removing the update radians part `endRadians+=((this.x2>this.x1)?90:-90)*Math.PI/180;` and editing to ctx.lineTo(-5,20); ctx.lineTo(-5,-20); made it work for me – Manik Aug 06 '23 at 15:20
5

This goes wrong for vertical lines. Try

var line=new Line(50,50,50,275)

demongolem
  • 9,474
  • 36
  • 90
  • 105
user1707810
  • 123
  • 4
  • 10
4

As addition to markE's answer combined with user1707810 comment:

Both blocks of (start/end radians):

 - ((this.x2 > this.x1)?-90:90)*Math.PI/180;

should be changed to :

 - ((this.x2 >= this.x1)?-90:90)*Math.PI/180;
Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
4

Simpler version

key difference. Using Math.atan2 removes the need for if

This one also puts the arrowheads at the ends of the line rather than past the end of the line

In other words

this

start         end
    |<------->|

vs this

   <|---------|>

function arrow(ctx, x1, y1, x2, y2, start, end) {
  var rot = -Math.atan2(x1 - x2, y1 - y2);
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
  if (start) {
    arrowHead(x1, y1, rot);
  }
  if (end) {
    arrowHead(x2, y2, rot + Math.PI);
  }
}

function arrowHead(x, y, rot) {
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(rot);
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(-5, -12);
  ctx.lineTo(5, -12);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

// test it -------

var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);


// draw some arrows
ctx.save();
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);

for (var ii = 0; ii <= 12; ++ii) {
  var u = ii / 12;
  var color = hsl(u * 360, 1, 0.5);
  ctx.fillStyle = color;
  ctx.strokeStyle = color;
  var a = u * Math.PI;
  var x = Math.cos(a) * 120;
  var y = Math.sin(a) * 70;
  arrow(ctx, -x, -y, x, y, true, true);
  
  // draw the end points to see the arrowheads match
  ctx.fillStyle = "#000";
  ctx.fillRect(-x - 1, -y - 1, 3, 3);
  ctx.fillRect( x - 1,  y - 1, 3, 3);
}

ctx.restore();

function hsl(h, s, l) {
  return `hsl(${h},${s * 100}%,${l * 100}%)`;
}
canvas { border: 1px solid black; }

One problem with the above solution is if you thicken the line stroke it would poke through the arrowhead. It's not hard to fix but you'd have to compute the length of the line in pixels, then subtract the size of the arrowhead from both sides.

something like this

function arrow(ctx, x1, y1, x2, y2, start, end) {
  var dx = x2 - x1;
  var dy = y2 - y1;
  var rot = -Math.atan2(dx, dy);
  var len = Math.sqrt(dx * dx + dy * dy);
  var arrowHeadLen = 10;
  ctx.save();
  ctx.translate(x1, y1);
  ctx.rotate(rot);
  ctx.beginPath();    
  ctx.moveTo(0, start ? arrowHeadLen : 0);
  ctx.lineTo(0, len - (end ? arrowHeadLen : 0));
  ctx.stroke();
  if (end) {
    ctx.save();
    ctx.translate(0, len);
    arrowHead(ctx);
    ctx.restore();
  }
  if (start) {
    ctx.rotate(Math.PI);
    arrowHead(ctx);
  }
  ctx.restore();
}

function arrowHead(ctx) {
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(-5, -12);
  ctx.lineTo(5, -12);
  ctx.closePath();
  ctx.fill();
}

// test it -------

var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);


// draw some arrows
ctx.save();
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);

for (var ii = 0; ii < 12; ++ii) {
  var u = ii / 12;
  var color = hsl(u * 360, 1, 0.5);
  ctx.fillStyle = color;
  ctx.strokeStyle = color;
  var a = u * Math.PI;
  var x = Math.cos(a) * 120;
  var y = Math.sin(a) * 70;
  arrow(ctx, -x, -y, x, y, true, true);
  ctx.fillStyle = "#000";  // mark the ends so we can see where they are
  ctx.fillRect(-x - 1, -y - 1, 3, 3);
  ctx.fillRect( x - 1,  y - 1, 3, 3);
}

ctx.restore();

function hsl(h, s, l) {
  return `hsl(${h},${s * 100}%,${l * 100}%)`;
}
canvas { border: 1px solid black; }

In other words the first solution draws arrows like this

top arrow

Where as the second solution draws arrows like this

bottom arrow

gman
  • 100,619
  • 31
  • 269
  • 393
1

my simple solution

ctx.beginPath(); 
ctx.moveTo(ax,ay);
ctx.lineTo(bx,by);
ctx.stroke();
ctx.closePath();
angle=Math.PI+Math.atan2(by-ay,bx-ax);
angle1=angle+Math.PI/6;
angle2=angle-Math.PI/6;
ctx.beginPath(); 
ctx.moveTo(bx,by);
ctx.arc(bx,by,5,angle1,angle2,true);
ctx.lineTo(bx,by);
ctx.fill();
ctx.closePath();
mgomec
  • 11
  • 1
  • I'm having trouble making the arrow head larger without making it rounded at the bottom. Do you have any advice? – skybondsor Oct 04 '20 at 02:32