maudulus
maudulus

Reputation: 11025

Make a canvas line with a point at the end

I am trying to make the end of a canvas into a point (sort of like an arrow, except that the sides should not come out beyond the width of the line...see the following picture for an example of how the line end should look).

See the following picture for an example of how the line end should look.

I was experimenting with line caps, but the only available caps are 'round' or 'square' (http://www.w3schools.com/tags/canvas_linecap.asp).

The following fiddle is the line that I am trying to give points to the end of.

http://jsfiddle.net/699ktkv8/

The code is below as well:

<body>

<canvas id="myCanvas" width="300" height="150" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>

<script>

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 200, 100, 200, 20);
ctx.lineWidth=10
ctx.stroke();

</script> 

</body>

Upvotes: 1

Views: 716

Answers (2)

user1693593
user1693593

Reputation:

There is no such feature in canvas unfortunately. You will have to manually calculate the outcome angle of the line (which means you need to implement the Bezier math).

Then use the width of the line to draw the cap yourselves based on that angle.

Step 1 - find direction

Lets make it a little more challenging by using one end that isn't 90° up or to the side:

Step 1

var ctx = document.querySelector("canvas").getContext("2d");

// draw as normal
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 170, 70, 200, 20);
ctx.lineWidth=10
ctx.stroke();

// get two points from the end
var pt1 = order3(20, 20, 20, 100, 170, 70, 200, 20, 0.98);
var pt2 = order3(20, 20, 20, 100, 170, 70, 200, 20, 1);

// show direction
ctx.lineWidth = 2;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.moveTo(pt1.x, pt1.y);
ctx.lineTo(pt1.x + (pt2.x - pt1.x) * 10, pt1.y + (pt2.y - pt1.y) * 10);
ctx.stroke();

//B(t) = (1-t)^3 * z0 + 3t (1-t)^2 * c0 + 3 t^2 (1-t) * c1 + t^3 * z1 for 0 <=t <= 1
function order3(z0x, z0y, c0x, c0y, c1x, c1y, z1x, z1y, t) {

  var tm1 = 1 - t,        // (1 - t)
    tm12 = tm1 * tm1,     // (1 - t) ^ 2
    tm13 = tm12 * tm1,    // (1 - t) ^ 3
    t2 = t * t,           // t ^ 2
    t3 = t2 * t,          // t ^ 3
    tmm3 = t * 3 * tm12,  // 3 x t * (1 - t) ^ 2
    tmm23 = t2 * 3 * tm1, // t ^ 2 * 3 * (1 - t)
    x, y;

  x = (tm13 * z0x + tmm3 * c0x + tmm23 * c1x + t3 * z1x + 0.5) | 0;
  y = (tm13 * z0y + tmm3 * c0y + tmm23 * c1y + t3 * z1y + 0.5) | 0;

  return {
    x: x,
    y: y
  }
}
<canvas width=220 height=100 />

Update Or as markE points, you could calculate it from the control points (my bad, I forgot about this completely - thanks markE) - This is probably the better approach for most cases compared to using the "t" approach.

I'll include it here for the sake of completeness:

// calculate the ending angle from the two last nodes (cp2 and end point)
var dx = pt2.x - cp2.x;   // assumes points and control points as objects
var dy = pt2.y - cp2.y;
var angle = Math.atan2(dy, dx);

Update end

Step 2 - find angle and distance

We need to calculate the actual angle so that we can use that for the base of the arrow:

// get angle
var diffX = pt1.x - pt2.x;   // see update comment above
var diffY = pt1.y - pt2.y;
var angle = Math.atan2(diffY, diffX);
var tangent = Math.atan2(diffX, -diffY);

step 2

(shown slightly offset on purpose)

Step 3 - draw cap

Now we have enough information to draw a cap on the line:

step 3

var ctx = document.querySelector("canvas").getContext("2d");

// draw as normal
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 170, 70, 200, 20);
ctx.lineWidth=40
ctx.stroke();

// get two points from the end
var pt1 = order3(20, 20, 20, 100, 170, 70, 200, 20, 0.98);
var pt2 = order3(20, 20, 20, 100, 170, 70, 200, 20, 1);
var diffX = pt1.x - pt2.x;
var diffY = pt1.y - pt2.y;
var angle = Math.atan2(diffY, diffX);
var tangent = Math.atan2(diffX, -diffY);
var lw = ctx.lineWidth * 0.5 - 0.5;

// draw cap
ctx.beginPath();
ctx.moveTo(pt2.x + lw * Math.cos(tangent), pt2.y + lw * Math.sin(tangent));
ctx.lineTo(pt2.x - lw * Math.cos(tangent), pt2.y - lw * Math.sin(tangent));
ctx.lineTo(pt1.x - lw * Math.cos(angle), pt1.y - lw * Math.sin(angle));

ctx.fill();

// due to inaccuracies, you may have to mask tiny gaps 
ctx.lineWidth = 1;
ctx.stroke();

//B(t) = (1-t)^3 * z0 + 3t (1-t)^2 * c0 + 3 t^2 (1-t) * c1 + t^3 * z1 for 0 <=t <= 1
function order3(z0x, z0y, c0x, c0y, c1x, c1y, z1x, z1y, t) {

  var tm1 = 1 - t,        // (1 - t)
    tm12 = tm1 * tm1,     // (1 - t) ^ 2
    tm13 = tm12 * tm1,    // (1 - t) ^ 3
    t2 = t * t,           // t ^ 2
    t3 = t2 * t,          // t ^ 3
    tmm3 = t * 3 * tm12,  // 3 x t * (1 - t) ^ 2
    tmm23 = t2 * 3 * tm1, // t ^ 2 * 3 * (1 - t)
    x, y;

  x = (tm13 * z0x + tmm3 * c0x + tmm23 * c1x + t3 * z1x + 0.5) | 0;
  y = (tm13 * z0y + tmm3 * c0y + tmm23 * c1y + t3 * z1y + 0.5) | 0;

  return {
    x: x,
    y: y
  }
}
<canvas width=240 height=100 />

The actual angle at the end depends on what point near the end you choose (not the actual end point, ie. t=1). You may have to calculate the total line length and use that as basis of how much t shall be.

You may also run into situations where the angle is not entire correct and small gaps appear.

You can either mask these gaps by applying a stroke, or offset the cap a little based on the previous calculated angle/direction (use linear interpolation as in step 1, just with negative t), or the only other way to get it accurate is to manually calculate the walls of the line etc., e.g. treat it as a polygon and fill it as a single object.

Upvotes: 4

markE
markE

Reputation: 105015

@KenFrystenberg has answered your question well.

Here's an interesting math note that simplifies the calculations.

The ending angle (and starting angle) of a cubic Bezier curve can be calculated directly from the control points:

// define 4 cubic Bezier control points
var cp0={x:20,y:20};
var cp1={x:20,y:100};
var cp2={x:170,y:70};
var cp3={x:200,y:20};

// calculate the ending angle from cp2 & cp3
var dx=cp3.x-cp2.x;
var dy=cp3.y-cp2.y;
var angle=Math.atan2(dy,dx);

Here's example code and a demo about this math note:

enter image description here

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;

var cp0={x:20,y:20};
var cp1={x:20,y:100};
var cp2={x:170,y:70};
var cp3={x:200,y:20};

ctx.lineWidth=10;

ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 170, 70, 200, 20);
ctx.lineWidth=20
ctx.stroke();

ctx.beginPath();
ctx.arc(200,20,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle='red';
ctx.fill();

var dx=cp3.x-cp2.x;
var dy=cp3.y-cp2.y;
var angle=Math.atan2(dy,dx);

var x=cp3.x+15*Math.cos(angle);
var y=cp3.y+15*Math.sin(angle);


ctx.beginPath();
ctx.moveTo(cp3.x,cp3.y);
ctx.lineTo(x,y);
ctx.lineWidth=1;
ctx.strokeStyle='red';
ctx.stroke();
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>

Upvotes: 1

Related Questions