Draw arrow on canvas tag
Asked Answered
C

15

75

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

Cyathus answered 30/4, 2009 at 20:18 Comment(0)
P
127

As simple as I can get it. You'll have to prepend context.beginPath() and append context.stroke() yourself:

ctx = document.getElementById("c").getContext("2d");
ctx.beginPath();
canvas_arrow(ctx, 10, 30, 200, 150);
canvas_arrow(ctx, 100, 200, 400, 50);
canvas_arrow(ctx, 200, 30, 10, 150);
canvas_arrow(ctx, 400, 200, 100, 50);
ctx.stroke();


function canvas_arrow(context, fromx, fromy, tox, toy) {
  var headlen = 10; // length of head in pixels
  var dx = tox - fromx;
  var dy = toy - fromy;
  var angle = Math.atan2(dy, dx);
  context.moveTo(fromx, fromy);
  context.lineTo(tox, toy);
  context.lineTo(tox - headlen * Math.cos(angle - Math.PI / 6), toy - headlen * Math.sin(angle - Math.PI / 6));
  context.moveTo(tox, toy);
  context.lineTo(tox - headlen * Math.cos(angle + Math.PI / 6), toy - headlen * Math.sin(angle + Math.PI / 6));
}
<html>

<body>
  <canvas id="c" width="500" height="500"></canvas>


</body>
Pup answered 13/6, 2011 at 17:13 Comment(6)
That produces an odd shape, you want to get rid of that last move and add at the end lineTo(tox,toy)Vorticella
function doesnt work well when lineWidth is not == 1Sideway
For larger line widths, just add context.moveTo(tox, toy); after context.lineTo(tox, toy); (see: i.imgur.com/jMOsLM9.png)Chipman
@Chipman is the image a canvas arrow? looks rounded, if so how do you do that?Monoceros
@Monoceros context.lineCap = 'round' should do it.Chipman
@ProstoTrader I've posted a new answer that considers the line width and adjusts the position accordingly.Brister
M
37

Ok, so the first answer on this page helped me greatly when I was trying to figure this problem out myself, although as someone else already stated, if you have a line width greater than 1px you get funny shapes. The fix that someone else suggested almost worked, but I still had some issues when trying to go for a thicker width arrow. After several hours of playing around with it I was able to combine the above solution with some of my own tinkering to come up with the following code that will draw an arrow at whatever thickness you desire without distorting the arrow shape.

function drawArrow(fromx, fromy, tox, toy){
                //variables to be used when creating the arrow
                var c = document.getElementById("myCanvas");
                var ctx = c.getContext("2d");
                const width = 22;
                var headlen = 10;
                // This makes it so the end of the arrow head is located at tox, toy, don't ask where 1.15 comes from
                tox -= Math.cos(angle) * ((width*1.15));
                toy -= Math.sin(angle) * ((width*1.15));

                var angle = Math.atan2(toy-fromy,tox-fromx);
                
                //starting path of the arrow from the start square to the end square and drawing the stroke
                ctx.beginPath();
                ctx.moveTo(fromx, fromy);
                ctx.lineTo(tox, toy);
                ctx.strokeStyle = "#cc0000";
                ctx.lineWidth = width;
                ctx.stroke();
                
                //starting a new path from the head of the arrow to one of the sides of the point
                ctx.beginPath();
                ctx.moveTo(tox, toy);
                ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
                
                //path from the side point of the arrow, to the other side point
                ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
                
                //path from the side point back to the tip of the arrow, and then again to the opposite side point
                ctx.lineTo(tox, toy);
                ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));

                //draws the paths created above
                ctx.strokeStyle = "#cc0000";
                ctx.lineWidth = width;
                ctx.stroke();
                ctx.fillStyle = "#cc0000";
                ctx.fill();
            }

This is now the code that I am using in my program. What I found to be the key with eliminating the distortion issue was continuing the stroke from the tip of the arrow to one side point, to the other side point, back to the tip, and back over to the first side point, then doing a fill. This corrected the shape of the arrow.

Hope this helps!

Marjana answered 28/9, 2014 at 0:27 Comment(3)
I am not able to locate the exact point of arrow head in the solution. Do you have any idea of this?Stevenage
I never got that into the weeds with the code. I basically just played with dimensions until it looked correct after I drew the arrow. Sorry I don't have a better answer for you. :\Marjana
Modify the function by adding these two lines after var angle = Math.atan2(toy-fromy,tox-fromx);, tox -= Math.cos(angle) * ((width*1.15)); toy -= Math.sin(angle) * ((width*1.15)); This will make it so the arrow head ends at tox and toy. @CodingbyRajTraction
A
17

Here is another method to draw arrows. It uses the triangle method from here: https://mcmap.net/q/270377/-create-equilateral-triangle-in-the-middle-of-canvas

A little helper function.

function canvas_arrow(context, fromx, fromy, tox, toy, r){
    var x_center = tox;
    var y_center = toy;

    var angle;
    var x;
    var y;

    context.beginPath();

    angle = Math.atan2(toy-fromy,tox-fromx)
    x = r*Math.cos(angle) + x_center;
    y = r*Math.sin(angle) + y_center;

    context.moveTo(x, y);

    angle += (1/3)*(2*Math.PI)
    x = r*Math.cos(angle) + x_center;
    y = r*Math.sin(angle) + y_center;

    context.lineTo(x, y);

    angle += (1/3)*(2*Math.PI)
    x = r*Math.cos(angle) + x_center;
    y = r*Math.sin(angle) + y_center;

    context.lineTo(x, y);

    context.closePath();

    context.fill();
}

And here is a demonstration of it to draw arrows at the start and at the end of a line.

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

ctx.lineWidth = 10;
ctx.strokeStyle = 'steelblue';
ctx.fillStyle = 'steelbllue'; // for the triangle fill
ctx.lineJoin = 'butt';

ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 150);
ctx.stroke();

canvas_arrow(ctx, 50, 50, 150, 150, 10);
canvas_arrow(ctx, 150, 150, 50, 50, 10);

function canvas_arrow(context, fromx, fromy, tox, toy, r){
	var x_center = tox;
	var y_center = toy;
	
	var angle;
	var x;
	var y;
	
	context.beginPath();
	
	angle = Math.atan2(toy-fromy,tox-fromx)
	x = r*Math.cos(angle) + x_center;
	y = r*Math.sin(angle) + y_center;

	context.moveTo(x, y);
	
	angle += (1/3)*(2*Math.PI)
	x = r*Math.cos(angle) + x_center;
	y = r*Math.sin(angle) + y_center;
	
	context.lineTo(x, y);
	
	angle += (1/3)*(2*Math.PI)
	x = r*Math.cos(angle) + x_center;
	y = r*Math.sin(angle) + y_center;
	
	context.lineTo(x, y);
	
	context.closePath();
	
	context.fill();
}
<canvas id="c" width=300 height=300></canvas>
Agitator answered 23/4, 2016 at 1:22 Comment(2)
Why would you use (1/3)*(2*Math.PI) and not just Math.PI / 1.5? It gives the exact same result but in less operations.Chili
The tip is offset (it centers around the line end instead of exactly pointing onto it), but adding x_center -= r * Math.cos(angle); y_center -= r * Math.sin(angle); after the first angle calculation fixes that.Esparza
C
9

You can do:

ctx.save();
ctx.translate(xOrigin, yOrigin);
ctx.rotate(angle);
 // draw your arrow, with its origin at [0, 0]
ctx.restore();
Cilicia answered 30/4, 2009 at 23:35 Comment(0)
S
7

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>
Strobotron answered 3/10, 2017 at 1:35 Comment(1)
Thank you sir you helped me out greatly worked great with what I neededFewness
F
6

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

Futures answered 9/11, 2020 at 17:21 Comment(2)
Best answer, I think. You say TypeScript, but I don't see any type declarations? This worked without modifications in plain JS.Courbet
@RogerDahl I meant that it compiles with tsc right away so you can use it immediately in a .ts file.Futures
W
5

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

Whitley answered 16/3, 2022 at 16:9 Comment(1)
Perfect solution! Thank you. Just one suggestion - where is the fillStyle() to make it look grey like that?Cully
G
4

Given a size and the starting position, following code will draw the arrow for you.

function draw_arrow(context, startX, startY, size) {
  var arrowX = startX + 0.75 * size;
  var arrowTopY = startY - 0.707 * (0.25 * size);
  var arrowBottomY = startY + 0.707 * (0.25 * size);
  context.moveTo(startX, startY);
  context.lineTo(startX + size, startX);
  context.lineTo(arrowX, arrowTopY);
  context.moveTo(startX + size, startX);
  context.lineTo(arrowX, arrowBottomY);
  context.stroke();
}
window.onload = function() {
  var canvas = document.getElementById("myCanvas");
  var context = canvas.getContext("2d");
  var startX = 50;
  var startY = 50;
  var size = 100;
  context.lineWidth = 2;
  draw_arrow(context, startX, startY, size);
};
body {
  margin: 0px;
  padding: 0px;
}

#myCanvas {
  border: 1px solid #9C9898;
}
<!DOCTYPE HTML>
<html>

<body onmousedown="return false;">
  <canvas id="myCanvas" width="578" height="200"></canvas>
</body>

</html>
Grapheme answered 26/12, 2011 at 9:44 Comment(0)
B
4

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>
Brister answered 27/1, 2022 at 5:16 Comment(2)
x1 -= width * Math.cos(angle); on this point get the error. X1 is undefinedDrusilla
x1 is a function argument.Brister
A
3

This code is similar to Titus Cieslewski's solution, maybe the arrow is a bit nicer:

function canvasDrawArrow(context, fromx, fromy, tox, toy) {
    var headlen = 10.0;
    var back = 4.0;
    var angle1 = Math.PI / 13.0;
    var angle2 = Math.atan2(toy - fromy, tox - fromx);
    var diff1 = angle2 - angle1;
    var diff2 = angle2 + angle1;
    var xx = getBack(back, fromx, fromy, tox, toy);
    var yy = getBack(back, fromy, fromx, toy, tox);

    context.moveTo(fromx, fromy);
    context.lineTo(tox, toy);

    context.moveTo(xx, yy);
    context.lineTo(xx - headlen * Math.cos(diff1), yy - headlen * Math.sin(diff1));

    context.moveTo(xx, yy);
    context.lineTo(xx - headlen * Math.cos(diff2), yy - headlen * Math.sin(diff2));
}

function getBack(len, x1, y1, x2, y2) {
    return x2 - (len * (x2 - x1) / (Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2))));
}

this works well with lineWidth > 1. It can come in handy when drawing x and y axis

Alboran answered 23/4, 2016 at 14:20 Comment(0)
N
1
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);
  }
}
Newport answered 23/12, 2010 at 8:59 Comment(0)
P
1

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
}
Performing answered 31/1, 2021 at 7:54 Comment(0)
F
0

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

Fryd answered 30/4, 2009 at 21:29 Comment(0)
D
0

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

Dilks answered 26/10, 2018 at 11:19 Comment(0)
B
0

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();
}
Balinese answered 15/1, 2019 at 10:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.