Make A Canvas Line With A Point At The End
Solution 1:
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:
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 endvar 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 <= 1functionorder3(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
}
}
<canvaswidth=220height=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 objectsvar 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 anglevar diffX = pt1.x - pt2.x; // see update comment abovevar diffY = pt1.y - pt2.y;
var angle = Math.atan2(diffY, diffX);
var tangent = Math.atan2(diffX, -diffY);
(shown slightly offset on purpose)
Step 3 - draw cap
Now we have enough information to draw a cap on the line:
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 endvar 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 <= 1functionorder3(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
}
}
<canvaswidth=240height=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.
Solution 2:
@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 pointsvar 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 & cp3var 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:
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;}
<canvasid="canvas"width=300height=300></canvas>
Post a Comment for "Make A Canvas Line With A Point At The End"