Solved

Add arrowheads to drawn lines HTML 5 canvas

Posted on 2012-03-17
8
1,516 Views
Last Modified: 2012-03-30
I am trying to add arrowheads to the ends of lines drawn by the user on HTML 5 Canvas. Of course, the big problem is figuring out which way the line is pointing.   I have tried creating a stack keeping track of the points along the line then popping 5-20 points off the stack to get the direction of the end of the line. This method has two problems.  If the stack is two short, no arrow is drawn (I might be able to handle this by first checking the size of the stack and adjust the number of pops).  The bigger problem is if the user moves the mouse a little as he releases the button, the arrow may point the wrong way (see the image.)  You can see this solution in action at www.barnwellmd.com/PainDiagram/PainDiagram.html (click on referred pain to see the lines with the arrowheads). Below is the code using the stack (just the pertinent code).

 Any suggestions? TIA
Arrows
    // This is called when you start holding down the mouse button.
    // This starts the pencil drawing.
    this.mousedown = function (ev) {
	    stackx = new Array();
	    stacky = new Array();
	    context.lineWidth = 10;
	    context.beginPath();
            tool.started = true;
    };

    // This function is called every time you move the mouse. 
    this.mousemove = function (ev) {
      if (tool.started) {
	        stackx.push(ev._x);
		stacky.push(ev._y);
		context.lineTo(ev._x, ev._y);
	        context.stroke();
      }

    // This is called when you release the mouse button.
    this.mouseup = function (ev) {
		if (tool.started) 
		{
			for (var i = 0; i < 20; i++)
			{
				stackx.pop();
				stacky.pop();
			}
			var angle = Math.atan2(ev._y-stacky.pop(),ev._x-stackx.pop());
		  	context.beginPath();
			context.moveTo(ev._x, ev._y);
			context.lineTo(ev._x-20*Math.cos(angle-Math.PI/6),ev._y-20*Math.sin(angle-Math.PI/6));
		  	context.lineTo(ev._x-20*Math.cos(angle+Math.PI/6),ev._y-20*Math.sin(angle+Math.PI/6)); 
			context.closePath();
			context . fillStyle  = color_value;
			context.fill();
			}
			tool.started = false;
			tool.mousemove(ev);
		}

Open in new window

0
Comment
Question by:thenelson
  • 4
  • 4
8 Comments
 
LVL 38

Expert Comment

by:Tom Beck
ID: 37737411
I think you are giving the end user too much freedom to draw a line with an arrow at the end. If Referred Pain is pain that "starts in one area which causes pain elsewhere", then a simple straight line with an arrow at the end would be the best graphic illustration of that. Why let them draw a curved line at all? This would simplify the task for you. Just record the mousedown coordinates and on mouseup, draw a line between the two points and add the arrow head.
0
 
LVL 39

Author Comment

by:thenelson
ID: 37740360
Actually, a curved line is needed.A classic referred pain you may have heard of is the pain running down the left arm from a heart attack. In this case the line would be drawn from the center of the chest over and down the left arm.  Another you may have heard about is sciatica.which starts at the lower spine and runs down the back of the leg. We have added this type of pain to determine the new patient's depth of understanding of their problem and also for the doctor to record her findings.  Your response got me thinking, however.. Suppose I code the script to create connected line segments of, say, 20 pixels long.  In other words, as the user moves the mouse, the code creates a line segment every 20 pixels. I'm thinking about how to accomplish that.
0
 
LVL 38

Expert Comment

by:Tom Beck
ID: 37740572
That would be easy to do.
this.mousemove = function (ev) {
      if (tool.started) {
	        stackx.push(ev._x);
			stacky.push(ev._y);
			if(stackx.length % 20 == 0){  //draw a line every time the array length is divisible by 20
			    context.lineTo(ev._x, ev._y);
	                    context.stroke();
			}
      }
    };

Open in new window

0
 
LVL 38

Assisted Solution

by:Tom Beck
Tom Beck earned 500 total points
ID: 37740732
Complete solution:
var lastXseg, lastYseg, prevXseg, prevYseg;
    this.mousemove = function (ev) {
      if (tool.started) {
	        stackx.push(ev._x);
			stacky.push(ev._y);
			if (color_value == "#FF7F50")	 //referred pain
			{
			    if(stackx.length % 20 == 0){
			        if(stackx.length == 20){
			            prevXseg = stackx[0];
			            prevYseg = stacky[0];
			        }else{
			            prevXseg = lastXseg;
			            prevYseg = lastYseg;
			        }
			        lastXseg = ev._x;
			        lastYseg = ev._y;				
			        context.lineTo(ev._x, ev._y);
	                context.stroke();
			    }
			}else{
			    context.lineTo(ev._x, ev._y);
	            context.stroke();
			}
      }
    };

Open in new window


I am saving the beginning and end points of each 20 pixel long line segment and using them later in the mouseup event to get the segment's angle so I can apply the arrow head in the correct orientation. Because the segments are straight for every 20 pixels and any ending short segments are not recorded, the arrows are consistently pointing in the proper direction to the line.

B.T.W, I am using the canvasutilities.js file from this site to draw the arrow heads. Your method is clever, but because I flunked trigonometry, I don't understand it : )
http://www.dbp-consulting.com/tutorials/canvas/CanvasArrow.html
Referred Pain Arrows
0
Free Trending Threat Insights Every Day

Enhance your security with threat intelligence from the web. Get trending threat insights on hackers, exploits, and suspicious IP addresses delivered to your inbox with our free Cyber Daily.

 
LVL 39

Author Comment

by:thenelson
ID: 37742945
With your code, I am not getting the line drawn on the screen starting where I am starting the line with my mouse (where i click the mouse button).  I am using a very slow computer (mine got a bad virus) so I think what is happening is the first pixels are not getting captured. Perhaps reducing the modulo form 20 to 5 might help.  If not, I think we need to draw the line without the modulo operation and keep track of the 20 pixel long line segments for the arrow direction. I got several meetings today (and fixing my main computer) so I won't get to it until tonight. I guess it is a good thing I am using this slow computer.  

The arrowheads draw by the other routine are very nice.  I think I'll switch to them.
0
 
LVL 38

Expert Comment

by:Tom Beck
ID: 37743095
My script will not draw the first line segment until you have traveled at least 20 pixels. Maybe you could allow the first segment to draw immediately until it gets to 20 and then switch to drawing in 20 pixel segments. Reducing the modulo below the size of the arrow head (I'm using a 15 pixel arrow head) will put you right back to the original problem; i.e. if the last 5 pixel segment is skewed off the general direction of the drawn line, the arrow head will orient to that skewed segment instead of the general direction.
0
 
LVL 39

Accepted Solution

by:
thenelson earned 0 total points
ID: 37762872
I optimized the code you suggested to create line segments of a specific length. It now runs fine on a Pentium 500 mhz computer with 500 mb of ram. I also changed the shape of the arrows and now the arrow draws itself with the base of the arrow attached to the end of the line instead of the point of the arrow. With all that, I find using a line segment length of 7 works right.

Thanks again for your help. I have one more question I would appreciate your help with: http:Q_27647700.html.
arrows
this.mousedown = function (ev) {
   segCount = 0;
   if (color_value == "#FF7F50")	 //referred pain
   {
      context.lineWidth = 2;
      reptSeg = 7;
   }
   else
   {
      context.lineWidth = 10;
      reptSeg = 1;
   }
}:

 this.mousemove = function (ev)
{
   if (tool.started)
  {
      if(!segCount)
      {
         prevX = lastX;
         prevY = lastY;
         lastX = ev._x;
         lastY = ev._y;
         segCount = reptSeg;
         context.lineTo(ev._x, ev._y);
         context.stroke();
      }
      segCount--;
   }
};

this.mouseup = function (ev) {
   if (tool.started) 
   {
      if (color_value == "#FF7F50")
      {
         var angle = Math.atan2(lastY-prevY,lastX-prevX);
         context.beginPath();
         context.moveTo(lastX+15*Math.cos(angle),lastY+15*Math.sin(angle));
         context.lineTo(lastX-7*Math.cos(angle-Math.PI/4),lastY-7*Math.sin(angle-Math.PI/4));
         context.lineTo(lastX-7*Math.cos(angle+Math.PI/4),lastY-7*Math.sin(angle+Math.PI/4)); 
         context.closePath();
         context . fillStyle  = color_value;
         context.fill();
      }
...

Open in new window

0
 
LVL 39

Author Closing Comment

by:thenelson
ID: 37786119
tommyBoy's suggestion pointed me to a very workable solution.
0

Featured Post

What Security Threats Are You Missing?

Enhance your security with threat intelligence from the web. Get trending threat insights on hackers, exploits, and suspicious IP addresses delivered to your inbox with our free Cyber Daily.

Join & Write a Comment

Suggested Solutions

Keep your audience engaged and get the most out of your next presentation with these quick Prezi tips.
Boost your ability to deliver ambitious and competitive web apps by choosing the right JavaScript framework to best suit your project’s needs.
In this Micro Tutorial viewers will learn how to remove an unwanted object using Photoshop’s feature known as content-aware fill.
Illustrator's Shape Builder tool will let you combine shapes visually and interactively. This video shows the Mac version, but the tool works the same way in Windows. To follow along with this video, you can draw your own shapes or download the file…

758 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

22 Experts available now in Live!

Get 1:1 Help Now