Draw arrow on canvas tag

Viewed 90279

I want to draw an arrow using the canvas tag, javascript. I've made it using the quadratic function, but I'm having problems to calculate the angle of rotation of the arrow...

Anyone have a clue on this?

Thank you

15 Answers

You can do:

ctx.save();
ctx.translate(xOrigin, yOrigin);
ctx.rotate(angle);
 // draw your arrow, with its origin at [0, 0]
ctx.restore();

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

ctx.clearRect(0, 0, canvas.width, canvas.height); 
arrow({x: 10, y: 10}, {x: 100, y: 170}, 10);
arrow({x: 40, y: 250}, {x: 10, y: 70}, 5);


function arrow (p1, p2, size) {
  var angle = Math.atan2((p2.y - p1.y) , (p2.x - p1.x));
  var hyp = Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y));

  ctx.save();
  ctx.translate(p1.x, p1.y);
  ctx.rotate(angle);

  // line
  ctx.beginPath(); 
  ctx.moveTo(0, 0);
  ctx.lineTo(hyp - size, 0);
  ctx.stroke();

  // triangle
  ctx.fillStyle = 'blue';
  ctx.beginPath();
  ctx.lineTo(hyp - size, size);
  ctx.lineTo(hyp, 0);
  ctx.lineTo(hyp - size, -size);
  ctx.fill();

  ctx.restore();
}
<canvas id = "canvas" width = "300" height = "400"></canvas>

Typescript version, with the fixed arrow tip when line width >> 1

function canvas_arrow( context, fromx, fromy, tox, toy ) {
    const dx = tox - fromx;
    const dy = toy - fromy;
    const headlen = Math.sqrt( dx * dx + dy * dy ) * 0.3; // length of head in pixels
    const angle = Math.atan2( dy, dx );
    context.beginPath();
    context.moveTo( fromx, fromy );
    context.lineTo( tox, toy );
    context.stroke();
    context.beginPath();
    context.moveTo( tox - headlen * Math.cos( angle - Math.PI / 6 ), toy - headlen * Math.sin( angle - Math.PI / 6 ) );
    context.lineTo( tox, toy );
    context.lineTo( tox - headlen * Math.cos( angle + Math.PI / 6 ), toy - headlen * Math.sin( angle + Math.PI / 6 ) );
    context.stroke();
}

enter image description here

function RTEShape()
{   
    this.x = 50;
  this.y = 50;
  this.w = 100; // default width and height?
  this.h = 100;
  this.fill = '#444444';
  this.text = "Test String";
  this.type;
  this.color;
  this.size = 6;    

    // The selection color and width. Right now we have a red selection with a small width
    this.mySelColor = '#CC0000';
    this.mySelWidth = 2;
    this.mySelBoxColor = 'darkred';// New for selection boxes
    this.mySelBoxSize = 6;
}

RTEShape.prototype.buildArrow = function(canvas)
{
    this.type = "arrow";

  // Make sure we don't execute when canvas isn't supported
  if (canvas.getContext){

    // use getContext to use the canvas for drawing
    var ctx = canvas.getContext('2d');           

    var oneThirdX = this.x + (this.w/3);             
    var twoThirdX = this.x + ((this.w*2)/3);

    var oneFifthY = this.y - (this.y/5);    
    var twoFifthY = this.y - ((this.y*3)/5);

    /**/
    //ctx.beginPath();
    ctx.moveTo(oneThirdX,this.y); // 125,125
    ctx.lineTo(oneThirdX,oneFifthY); // 125,105

    ctx.lineTo(this.x*2,oneFifthY); // 225,105      
    ctx.lineTo(this.x*2,twoFifthY); // 225,65

    ctx.lineTo(oneThirdX,twoFifthY); // 125,65      
    ctx.lineTo(oneThirdX,(this.y/5)); // 125,45

    ctx.lineTo(this.x,(this.y+(this.y/5))/2); // 45,85

        ctx.fillStyle = "green";
    ctx.fill();

    ctx.fillStyle = "yellow";
    ctx.fillRect(this.x,this.y,this.w,this.h);

  } else {
    alert('Error on buildArrow!\n'+err.description);
  }
}

Hello and thank you very much for your suggestions.

May I suggest you drop the cumbersome atan ? You may as well use linear algebra to add or subtract angles:

var cospix=0.866025404; //cosinus of pi/6

function canvas_arrow(context, fromx, fromy, tox, toy) {
ctx.strokeStyle = '#AA0000';
var headlen = 10; // length of head in pixels
var dx = tox - fromx;
var dy = toy - fromy;
var length = Math.sqrt(dy*dy + dx*dx); //length of arrow
var sina = dy/length, cosa = dx/length; //computing sin and cos of arrow angle
var cosp=cosa*cospix-0.5*sina, cosm=cosa*cospix+0.5*sina,
sinp=cosa*0.5+cospix*sina, sinm=cospix*sina-cosa*0.5;
//computing cos and sin of arrow angle plus pi/6, respectively minus pi/6
//(p for plus, m for minus at the end of variable's names)
context.moveTo(fromx, fromy);
context.lineTo(tox, toy);
context.lineTo(tox - headlen * cosm, toy - headlen * sinm); //computing coordinates using the cos and sin computed above
context.moveTo(tox, toy);
context.lineTo(tox - headlen * cosp, toy - headlen * sinp); //computing coordinates using the cos and sin computed above
}

I also stumbled across this problem and gotta say that none of these solutions works nicely if you want to fill your arrow and make it transparent.

I wrote some code to achieve this. (I usually code in C++ so dont judge my code style please) :)

function transform(xy,angle,xy0){
    // put x and y relative to x0 and y0 so we can rotate around that
    const rel_x = xy[0] - xy0[0];
    const rel_y = xy[1] - xy0[1];

    // compute rotated relative points
    const new_rel_x = Math.cos(angle) * rel_x - Math.sin(angle) * rel_y;
    const new_rel_y = Math.sin(angle) * rel_x + Math.cos(angle) * rel_y;

    return [xy0[0] + new_rel_x, xy0[1] + new_rel_y];
}

function draw_arrow(context, x0, y0, x1, y1, width, head_width, head_length){

    // compute length first
    const length = Math.sqrt((x1-x0)*(x1-x0)+(y1-y0)*(y1-y0))
    let angle  = Math.atan2(y1-y0, x1-x0);
    // adjust the angle by 90 degrees since the arrow we rotate is rotated by 90 degrees
    angle -= Math.PI / 2;

    let p0 = [x0,y0];

    // order will be: p1 -> p3 -> p5 -> p7 -> p6 -> p4 -> p2
    // formulate the two base points
    let p1 = [x0 + width / 2, y0];
    let p2 = [x0 - width / 2, y0];

    // formulate the upper base points which connect the pointy end with the lengthy thing
    let p3 = [x0 + width / 2, y0 + length - head_length];
    let p4 = [x0 - width / 2, y0 + length - head_length];

    // formulate the outter points of the triangle
    let p5 = [x0 + head_width / 2, y0 + length - head_length];
    let p6 = [x0 - head_width / 2, y0 + length - head_length];

    // end point of the arrow
    let p7 = [x0, y0 + length];

    p1 = transform(p1,angle,p0);
    p2 = transform(p2,angle,p0);
    p3 = transform(p3,angle,p0);
    p4 = transform(p4,angle,p0);
    p5 = transform(p5,angle,p0);
    p6 = transform(p6,angle,p0)
    p7 = transform(p7,angle,p0);

    // move to start first
    context.moveTo(p1[0], p1[1]);
    context.beginPath();
    // start drawing the lines
    context.lineTo(p3[0], p3[1]);
    context.lineTo(p5[0], p5[1]);
    context.lineTo(p7[0], p7[1]);
    context.lineTo(p6[0], p6[1]);
    context.lineTo(p4[0], p4[1]);
    context.lineTo(p2[0], p2[1]);
    context.lineTo(p1[0], p1[1]);
    context.closePath();
    context.arc(x0,y0,width/2,angle-Math.PI,angle)
    context.fill();
}

This results in a nicely looking arrow which I used for a chess website:

enter image description here

You can push your matrix, rotate it, draw your arrow and then pop the matrix.

I've been struggeling with this for quite some time now. I needed to to this in both javascript and c#. For javascript i found a nice library jCanvas.

My main problem was drawing nicely looking arrow heads, which jCanvas does perfectly. For my c# project i reverse engineered the jCanvas code.

Hopefully this helps somebody

Here is the working solution

function draw_arrow(ctx,fx,fy,tx,ty){ //ctx is the context
    var angle=Math.atan2(ty-fy,tx-fx);
    ctx.moveTo(fx,fy); ctx.lineTo(tx,ty);
    var w=3.5; //width of arrow to one side. 7 pixels wide arrow is pretty
    ctx.strokeStyle="#4d4d4d"; ctx.fillStyle="#4d4d4d";
    angle=angle+Math.PI/2; tx=tx+w*Math.cos(angle); ty=ty+w*Math.sin(angle);
    ctx.lineTo(tx,ty);
  //Drawing an isosceles triangle of sides proportional to 2:7:2
    angle=angle-1.849096; tx=tx+w*3.5*Math.cos(angle); ty=ty+w*3.5*Math.sin(angle);
    ctx.lineTo(tx,ty);
    angle=angle-2.584993; tx=tx+w*3.5*Math.cos(angle); ty=ty+w*3.5*Math.sin(angle);
    ctx.lineTo(tx,ty);
    angle=angle-1.849096; tx=tx+w*Math.cos(angle); ty=ty+w*Math.sin(angle);
    ctx.lineTo(tx,ty);
    ctx.stroke(); ctx.fill();
}

While this question is mostly answered, I find the answers lacking. The top answer produces ugly arrows, many go beyond the point when using a width other than 1, and others have unnecessary steps.

This is the simplest answer that draws a pretty arrow head (proper triangle filled with color), and retracts the point of the arrow to consider the width of lines.

ctx = document.getElementById('canvas').getContext('2d');

/* Draw barrier */
ctx.beginPath();
ctx.moveTo(50, 30);
ctx.lineTo(450, 30);
ctx.stroke();

draw_arrow(50, 180, 150, 30);
draw_arrow(250, 180, 250, 30);
draw_arrow(450, 180, 350, 30);

function draw_arrow(x0, y0, x1, y1) {
  const width = 8;
  const head_len = 16;
  const head_angle = Math.PI / 6;
  const angle = Math.atan2(y1 - y0, x1 - x0);

  ctx.lineWidth = width;

  /* Adjust the point */
  x1 -= width * Math.cos(angle);
  y1 -= width * Math.sin(angle);

  ctx.beginPath();
  ctx.moveTo(x0, y0);
  ctx.lineTo(x1, y1);
  ctx.stroke();

  ctx.beginPath();
  ctx.lineTo(x1, y1);
  ctx.lineTo(x1 - head_len * Math.cos(angle - head_angle), y1 - head_len * Math.sin(angle - head_angle));
  ctx.lineTo(x1 - head_len * Math.cos(angle + head_angle), y1 - head_len * Math.sin(angle + head_angle));
  ctx.closePath();
  ctx.stroke();
  ctx.fill();
}
<canvas id="canvas" width="500" height="180"></canvas>

Related