Good afternoon all,
I come to you with yet another request for a lesson/example/answers. While messing around with a canvas in HTML5, I have learned how to manipulate Depth(Z-Buffer) and several other neat things. However, now I am trying to find a way to perform Pathfinding with the canvas. Most of the examples on the Internet are a little difficult to comprehend for me due to the fact that they are handling pathfinding far differently than I am trying to achieve(Which is that they use tile based pathfinding). Most other examples seem to deal in boxes or rectangles as well.
This is my code that I used as an example to draw a Polygon:
var canvas = document.getElementById('CanvasPath');
var context = canvas.getContext('2d');
// begin custom shape
context.beginPath();
context.moveTo(170, 80);
context.bezierCurveTo(1, 200, 125, 230, 230, 150);
context.bezierCurveTo(250, 180, 320, 200, 340, 200);
context.bezierCurveTo(420, 150, 420, 120, 390, 100);
context.bezierCurveTo(430, 40, 370, 30, 340, 50);
context.bezierCurveTo(320, 5, 250, 20, 250, 50);
context.bezierCurveTo(200, 5, 150, 20, 170, 80);
context.closePath();
context.lineWidth = 2;
context.strokeStyle = 'gray';
context.stroke();
Lets say I have a small box that I want to move around in that Polygon(I really will be creating the polygon with line points rather than Bezier curvers. I just wantd to show an example here) when I click at the goal position I want it to be at... How can I create a pathfinding algorithm that will find its way around and yet not have the bottom points of the box touch outside the polygon? I am assuming I would need to get all the pixels that are in that polygon to create a path from? I am thinking that the Bezier Curves and points may need to be created and pushed from an Array instead and then find the path???
Any suggestions on approach and can you provide an example for how to go about this? Please be gentle... While I have been an experienced scripter and programmer, I have not been one to mess to much with games, or graphics and I am still learning to manipulate the canvas in HTML5. Thanks for your help in advance.
The key part of what you are asking is how to shrink a polygon so that your box can travel along the shrunken polygon without extending outside the original polygon.
The illustration below shows an original black polygon, a shrunken red polygon and a blue traveling box.
Precisely shrinking a polygon
The code to precisely shrink a polygon is quite complex. It involves repositioning the original polygon vertices based on whether each particular vertex forms a convex or concave angle.
Here's example code I derived from Hans Muller's Blog on Webkits Shape-Padding. This example requires that the polygon vertices defined in a clockwise order.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var shapePadding = 10;
var dragVertexIndex = null;
var hoverLocation = null;
var polygonVertexRadius = 9;
var polygon, marginPolygon, paddingPolygon;
var polygonVertices = [{x: 143, y: 327}, {x: 80, y: 236}, {x: 151, y: 148}, {x: 454, y: 69}, {x: 560, y: 320}];
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon);
drawAll();
/////////////////////////////////
// Polygon creation and drawing
function drawAll(){
draw(polygon,'black');
draw(paddingPolygon,'red');
}
function draw(p,stroke){
var v=p.vertices;
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
//////////////////////
// Support functions
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<h4>Original black polygon and shrunken red polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
Calculating the waypoints along the polygon for your traveling box
To have your box travel along the inside of the polygon, you need an array containing points along each line of the polygon. You can calculate these points using linear interpolation.
Given an object defining a line like this:
var line={
x0:50,
y0:50,
x1:200,
y1:50};
You can use linear interpolation to calculate points along that line every 1px like this:
var allPoints = allLinePoints(line);
function allLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
Get the point on the polygon closest to the mouse click
You can use the distance formula (derived from Pythagorean theorem) to calculate which of the calculated waypoints is closest to the mouse click position:
// this will be the index in of the waypoint closest to the mouse
var indexOfClosestPoint;
// iterate all waypoints and find the closest to the mouse
var minLengthSquared=1000000*1000000;
for(var i=0;i<allPoints.length;i++){
var p=allPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
Animate the box along each waypoint up to the calculated ending waypoint
The only thing left to do is set up an animation loop that redraws the traveling box at each waypoint along the polygon until it reaches the ending waypoint:
// start animating at the first waypoint
var animationIndex=0;
// use requestAnimationFrame to redraw the traveling box
// along each waypoint
function animate(time){
// redraw the line and the box at its current (percent) position
var pt=allPoints[animationIndex];
// redraw the polygon and the traveling box
ctx.strokeStyle='black';
ctx.lineWidth=1;
ctx.clearRect(0,0,cw,ch);
ctx.strokeStyle='black';
drawLines(linePoints);
ctx.strokeStyle='red';
drawLines(insideLinePoints);
ctx.fillStyle='skyblue';
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// Are we done?
if(animationIndex<=indexOfClosestPoint){
// request another frame loop
requestAnimationFrame(animate);
}else{
// set the flag indicating the animation is complete
isAnimating=false;
}
}
Here's what it looks like when you put it all together
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
// polygon vertices
var polygonVertices=[
{x:143,y:327},
{x:80,y:236},
{x:151,y:148},
{x:454,y:69},
{x:560,y:320},
];
var shrunkenVertices=getShrunkenVertices(polygonVertices,10);
var polyPoints=getPolygonPoints(shrunkenVertices)
//log(shrunkenVertices);
// animation variables
var isAnimating=false;
var animationIndex=0;
//
var indexOfClosestPoint=-99;
// define the movable box
var boxWidth=12;
var boxHeight=10;
var boxRadius=Math.sqrt(boxWidth*boxWidth+boxHeight*boxHeight);
var boxFill='skyblue';
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
drawPolys(polygonVertices,shrunkenVertices);
////////////////////////////
// animate box to endpoint
////////////////////////////
function animate(time){
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// redraw the line and the box at its current (percent) position
var pt=polyPoints[animationIndex];
ctx.fillStyle=boxFill;
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// request another frame loop
if(animationIndex<=indexOfClosestPoint){
requestAnimationFrame(animate);
}else{
isAnimating=false;
}
}
////////////////////////////////////
// select box endpoint with click
////////////////////////////////////
function handleMouseDown(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// start animating
animationIndex=0;
isAnimating=true;
requestAnimationFrame(animate);
}
function handleMouseMove(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// find the nearest waypoint
indexOfClosestPoint=findNearestPointToMouse(mouseX,mouseY);
// redraw
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// draw a red dot at the nearest waypoint
drawDot(polyPoints[indexOfClosestPoint],'red');
}
function findNearestPointToMouse(mx,my){
// find the nearest waypoint
var minLengthSquared=1000000*1000000;
for(var i=0;i<polyPoints.length;i++){
var p=polyPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
return(indexOfClosestPoint);
}
////////////////////////////////
// Drawing functions
////////////////////////////////
function drawPolys(polygon,shrunken){
drawPoly(polygon,'black');
drawPoly(shrunken,'blue');
}
function drawPoly(v,stroke){
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function drawDot(pt,color){
ctx.beginPath();
ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle=color;
ctx.fill();
}
////////////////////////////////
// Get points along a polygon
////////////////////////////////
function getPolygonPoints(vertices){
// For this purpose, be sure to close the polygon
var v=vertices.slice(0);
var v0=v[0];
var vx=v[v.length-1];
if(v0.x!==vx.x || v0.y!==vx.y){v.push(v[0]);}
//
var points=[];
for(var i=1;i<v.length;i++){
var p0=v[i-1];
var p1=v[i];
var line={x0:p0.x,y0:p0.y,x1:p1.x,y1:p1.y};
points=points.concat(getLinePoints(line));
}
return(points);
}
function getLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
/////////////////////////
// "shrink" a polygon
/////////////////////////
function getShrunkenVertices(vertices,shapePadding){
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon,shapePadding);
return(paddingPolygon.vertices);
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon,shapePadding){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
/////////////////////////
// End "shrink polygon"
/////////////////////////
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Move mouse to where you want the box to end up<br>Then click to start the box animating from start to end.<br>Note: The starting point is on the bottom left of the polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
Using curved paths
If you want to make your closed path with Bezier curves, you will have to calculate waypoints along the curves using De Casteljau's algorithm. You will want to over-sample the number of points--maybe 500 values of T between 0.00 and 1.00. Here is a javascript version of the algorithm that calculates x,y points at interval T along a Cubic Bezier Curve:
// De Casteljau's algorithm which calculates points along a cubic Bezier curve
// plot a point at interval T along a bezier curve
// T==0.00 at beginning of curve. T==1.00 at ending of curve
// Calculating 300 T's between 0-1 will usually define the curve sufficiently
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
Related
I have a Polyline on the HiDPICanvas (html5 canvas). When I move mouse left and right I track its coordinates and on corresponding point with same X coordinate on the polyline I draw a Circle. You can try it now to see the result.
// Create a canvas
var HiDPICanvas = function(container_id, color, w, h) {
/*
objects are objects on the canvas, first elements of dictionary are background elements, last are on the foreground
canvas will be placed in the container
canvas will have width w and height h
*/
var objects = {
box : [],
borders : [],
circles : [],
polyline: []
}
var doNotMove = ['borders']
// is mouse down & its coords
var mouseDown = false
lastX = window.innerWidth/2
lastY = window.innerHeight/2
// return pixel ratio
var getRatio = function() {
var ctx = document.createElement("canvas").getContext("2d");
var dpr = window.devicePixelRatio || 1;
var bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
// return high dots per inch canvas
var createHiDPICanvas = function() {
var ratio = getRatio();
var chart_container = document.getElementById(container_id);
var can = document.createElement("canvas");
can.style.backgroundColor = color
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
chart_container.appendChild(can);
return can;
}
// add object to the canvas
var add = function(object, category) {
objects[category].push(object)
}
// clear canvas
var clearCanvas = function(x0, y0, x1, y1) {
ctx.clearRect(x0, y0, x1, y1);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
}
// check function do I can move this group of objects
var canMove = function(groupname) {
for (var i = 0; i < doNotMove.length; i++) {
var restricted = doNotMove[i]
if (restricted == groupname) {
return false
}
}
return true
}
// refresh all objects on the canvas
var refresh = function() {
clearCanvas(0, 0, w, h)
var object
for (var key in objects) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.refresh()
}
}
}
// shift all objects on the canvas except left and down borders and its content
var shiftObjects = function(event) {
event.preventDefault()
// if mouse clicked now -> we can move canvas view left\right
if (mouseDown) {
var object
for (var key in objects) {
if (canMove(key)) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.move(event.movementX, event.movementY)
}
}
}
cci.refresh()
}
}
// transfer x to canvas drawing zone x coord (for not drawing on borders of the canvas)
var transferX = function(x) {
return objects.borders[0].width + x
}
var transferCoords = function(x, y) {
// no need to transfer y because borders are only at the left
return {
x : transferX(x),
y : y
}
}
// change mouse state on the opposite
var toggleMouseState = function() {
mouseDown = !mouseDown
}
// make mouseDown = false, (bug removal function when mouse down & leaving the canvas)
var refreshMouseState = function() {
mouseDown = false
}
// print information about all objects on the canvas
var print = function() {
var groupLogged = true
console.log("Objects on the canvas:")
for (var key in objects) {
groupLogged = !groupLogged
if (!groupLogged) {console.log(key, ":"); groupLogged = !groupLogged}
for (var i = 0 ; i < objects[key].length; i++) {
console.log(objects[key][i])
}
}
}
var restrictEdges = function() {
console.log("offsetLeft", objects['borders'][0])
}
var getMouseCoords = function() {
return {
x : lastX,
y : lastY
}
}
var addCircleTracker = function() {
canvas.addEventListener("mousemove", (e) => {
var polyline = objects.polyline[0]
var mouseCoords = getMouseCoords()
var adjNodes = polyline.get2NeighbourNodes(mouseCoords.x)
if (adjNodes != -1) {
var prevNode = adjNodes.prev
var currNode = adjNodes.curr
var cursorNode = polyline.linearInterpolation(prevNode, currNode, mouseCoords.x)
// cursorNode.cursorX, cursorNode.cursorY are coords
// for circle that should be drawn on the polyline
// between the closest neighbour nodes
var circle = objects.circles[0]
circle.changePos(cursorNode.x, cursorNode.y)
refresh()
}
})
}
// create canvas
var canvas = createHiDPICanvas()
addCircleTracker()
// we created canvas so we can track mouse coords
var trackMouse = function(event) {
lastX = event.offsetX || (event.pageX - canvas.offsetLeft)
lastY = event.offsetY || (event.pageY - canvas.offsetTop)
}
// 2d context
var ctx = canvas.getContext("2d")
// add event listeners to the canvas
canvas.addEventListener("mousemove" , shiftObjects )
canvas.addEventListener("mousemove", (e) =>{ trackMouse(e) })
canvas.addEventListener("mousedown" , () => { toggleMouseState () })
canvas.addEventListener("mouseup" , () => { toggleMouseState () })
canvas.addEventListener("mouseleave", () => { refreshMouseState() })
canvas.addEventListener("mouseenter", () => { refreshMouseState() })
return {
// base objects
canvas : canvas,
ctx : ctx,
// sizes of the canvas
width : w,
height : h,
color : color,
// add object on the canvas for redrawing
add : add,
print : print,
// refresh canvas
refresh: refresh,
// objects on the canvas
objects: objects,
// get mouse coords
getMouseCoords : getMouseCoords
}
}
// cci -> canvas ctx info (dict)
var cci = HiDPICanvas("lifespanChart", "bisque", 780, 640)
var ctx = cci.ctx
var canvas = cci.canvas
var Polyline = function(path, color) {
var create = function() {
if (this.path === undefined) {
this.path = path
this.color = color
}
ctx.save()
ctx.beginPath()
p = this.path
ctx.fillStyle = color
ctx.moveTo(p[0].x, p[0].y)
for (var i = 0; i < p.length - 1; i++) {
var currentNode = p[i]
var nextNode = p[i+1]
// draw smooth polyline
// var xc = (currentNode.x + nextNode.x) / 2;
// var yc = (currentNode.y + nextNode.y) / 2;
// taken from https://stackoverflow.com/a/7058606/13727076
// ctx.quadraticCurveTo(currentNode.x, currentNode.y, xc, yc);
// draw rough polyline
ctx.lineTo(currentNode.x, currentNode.y)
}
ctx.stroke()
ctx.restore()
ctx.closePath()
}
// circle that will track mouse coords and be
// on the corresponding X coord on the path
// following mouse left\right movements
var circle = new Circle(50, 50, 5, "purple")
cci.add(circle, "circles")
create()
var get2NeighbourNodes = function(x) {
// x, y are cursor coords on the canvas
//
// Get 2 (left and right) neighbour nodes to current cursor x,y
// N are path nodes, * is Node we search coords for
//
// N-----------*----------N
//
for (var i = 1; i < this.path.length; i++) {
var prevNode = this.path[i-1]
var currNode = this.path[i]
if ( prevNode.x <= x && currNode.x >= x ) {
return {
prev : prevNode,
curr : currNode
}
}
}
return -1
}
var linearInterpolation = function(prevNode, currNode, cursorX) {
// calculate x, y for the node between 2 nodes
// on the path using linearInterpolation
// https://en.wikipedia.org/wiki/Linear_interpolation
var cursorY = prevNode.y + (cursorX - prevNode.x) * ((currNode.y - prevNode.y)/(currNode.x - prevNode.x))
return {
x : cursorX,
y : cursorY
}
}
var move = function(diff_x, diff_y) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].x += diff_x
this.path[i].y += diff_y
}
}
return {
create : create,
refresh: create,
move : move,
get2NeighbourNodes : get2NeighbourNodes,
linearInterpolation : linearInterpolation,
path : path,
color : color
}
}
var Circle = function(x, y, radius, fillStyle) {
var create = function() {
if (this.x === undefined) {
this.x = x
this.y = y
this.radius = radius
this.fillStyle = fillStyle
}
ctx.save()
ctx.beginPath()
ctx.arc(this.x, this.y, radius, 0, 2*Math.PI)
ctx.fillStyle = fillStyle
ctx.strokeStyle = fillStyle
ctx.fill()
ctx.stroke()
ctx.closePath()
ctx.restore()
}
create()
var changePos = function(new_x, new_y) {
this.x = new_x
this.y = new_y
}
var move = function(diff_x, diff_y) {
this.x += diff_x
this.y += diff_y
}
return {
refresh : create,
create : create,
changePos: changePos,
move : move,
radius : radius,
x : this.x,
y : this.y
}
}
var Node = function(x, y) {
this.x = x
this.y = y
return {
x : this.x,
y : this.y
}
}
var poly = new Polyline([
Node(30,30), Node(150,150),
Node(290, 150), Node(320,200),
Node(350,350), Node(390, 250),
Node(450, 140)
], "green")
cci.add(poly, "polyline")
<div>
<div id="lifespanChart"></div>
</div>
But if you go to the comment draw smooth polyline and uncomment code below (and comment line that draws rough polyline) - it will draw smooth polyline now (quadratic Bézier curve). But when you try to move mouse left and right - Circle sometimes goes out of polyline bounds.
before quadratic curve:
after quadratic curve:
Here is a question : I calculated x, y coordinates for the Circle on the rough polyline using linear interpolation, but how could I calculate x, y coordinates for the Circle on the smooth quadratic curve?
ADD 1 : QuadraticCurve using Beizer curve as a base in calculations when smoothing polyline
ADD 2 For anyone who a little stucked with the implementation I found & saved easier solution from here, example:
var canvas = document.getElementById("canv")
var canvasRect = canvas.getBoundingClientRect()
var ctx = canvas.getContext('2d')
var p0 = {x : 30, y : 30}
var p1 = {x : 20, y :100}
var p2 = {x : 200, y :100}
var p3 = {x : 200, y :20}
// Points are objects with x and y properties
// p0: start point
// p1: handle of start point
// p2: handle of end point
// p3: end point
// t: progression along curve 0..1
// returns an object containing x and y values for the given t
// link https://stackoverflow.com/questions/14174252/how-to-find-out-y-coordinate-of-specific-point-in-bezier-curve-in-canvas
var BezierCubicXY = function(p0, p1, p2, p3, t) {
var ret = {};
var coords = ['x', 'y'];
var i, k;
for (i in coords) {
k = coords[i];
ret[k] = Math.pow(1 - t, 3) * p0[k] + 3 * Math.pow(1 - t, 2) * t * p1[k] + 3 * (1 - t) * Math.pow(t, 2) * p2[k] + Math.pow(t, 3) * p3[k];
}
return ret;
}
var draw_poly = function () {
ctx.beginPath()
ctx.lineWidth=2
ctx.strokeStyle="white"
ctx.moveTo(p0.x, p0.y)// start point
// cont cont end
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
ctx.stroke()
ctx.closePath()
}
var clear_canvas = function () {
ctx.clearRect(0,0,300,300);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
};
var draw_circle = function(x, y) {
ctx.save();
// semi-transparent arua around the circle
ctx.globalCompositeOperation = "source-over";
ctx.beginPath()
ctx.fillStyle = "white"
ctx.strokeStyle = "white"
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
var refresh = function(circle_x, circle_y) {
clear_canvas()
draw_circle(circle_x, circle_y)
draw_poly()
}
var dist = function(mouse, point) {
return Math.abs(mouse.x - point.x)
// return ((mouse.x - point.x)**2 + (mouse.y - point.y)**2)**0.5
}
var returnClosest = function(curr, prev) {
if (curr < prev) {
return curr
}
return prev
}
refresh(30,30)
canvas.addEventListener("mousemove", (e) => {
var mouse = {
x : e.clientX - canvasRect.left,
y : e.clientY - canvasRect.top
}
var Point = BezierCubicXY(p0, p1, p2, p3, 0)
for (var t = 0; t < 1; t += 0.01) {
var nextPoint = BezierCubicXY(p0, p1, p2, p3, t)
if (dist(mouse, Point) > dist(mouse, nextPoint)) {
Point = nextPoint
}
// console.log(Point)
}
refresh(Point.x, Point.y)
})
canvas {
background: grey;
}
<canvas id="canv" width = 300 height = 300></canvas>
Just iterate through all the lines of the curve & find closest position using this pattern
This can be done using an iterative search, as you have done with the lines.
BTW there is a much better way to find the closest point on a line that has a complexity of O(1) rather than O(n) where n is length of line segment.
Search for closest point
The following function can be used for both quadratic and cubic beziers and returns the unit position of closest point on bezier to a given coordinate.
The function also has a property foundPoint that has the position of the point found
The function uses the object Point that defines a 2D coordinate.
Signatures
The function has two signatures, one for quadratic beziers and the other for cubic.
closestPointOnBezier(point, resolution, p1, p2, cp1)
closestPointOnBezier(point, resolution, p1, p2, cp1, cp2)
Where
point as Point is the position to check
resolution as Number The approx resolution to search the bezier. If 0 then this is fixed to DEFAULT_SCAN_RESOLUTION else it is the distance between start and end points times resolution IE if resolution = 1 then approx scan is 1px, if resolution = 2 then approx scan is 1/2px
p1, p2 as Point's are the start and end points of the bezier
cp1, cp2 as Point's are the first and/or second control points of the bezier
Results
They both return Number that is the unit pos on the bezier of closest point. The value will be 0 <= result <= 1 Where 0 is at start of bezier and 1 is end
The function property closestPointOnBezier.foundPoint as Point has the coordinate of the closest point on the bezier and can be used to calculate the distance to the point on the bezier.
The function
const Point = (x = 0, y = 0) => ({x, y});
const MAX_RESOLUTION = 2048;
const DEFAULT_SCAN_RESOLUTION = 256;
closestPointOnBezier.foundPoint = Point();
function closestPointOnBezier(point, resolution, p1, p2, cp1, cp2) {
var unitPos, a, b, b1, c, i, vx, vy, closest = Infinity;
const v1 = Point(p1.x - point.x, p1.y - point.y);
const v2 = Point(p2.x - point.x, p2.y - point.y);
const v3 = Point(cp1.x - point.x, cp1.y - point.y);
resolution = resolution > 0 && reolution < MAX_RESOLUTION ? (Math.hypot(p1.x - p2.x, p1.y - p2.y) + 1) * resolution : 100;
const fp = closestPointOnBezier.foundPoint;
const step = 1 / resolution;
const end = 1 + step / 2;
const checkClosest = (e = (vx * vx + vy * vy) ** 0.5) => {
if (e < closest ){
unitPos = i;
closest = e;
fp.x = vx;
fp.y = vy;
}
}
if (cp2 === undefined) { // find quadratic
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = a*2*i;
a *= a;
vx = v1.x * a + v3.x * b + v2.x * c;
vy = v1.y * a + v3.y * b + v2.y * c;
checkClosest();
}
} else { // find cubic
const v4 = Point(cp2.x - point.x, cp2.y - point.y);
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = 3 * a * a * i;
b1 = 3 * c * a;
a = a * a * a;
c *= i;
vx = v1.x * a + v3.x * b + v4.x * b1 + v2.x * c;
vy = v1.y * a + v3.y * b + v4.y * b1 + v2.y * c;
checkClosest();
}
}
return unitPos < 1 ? unitPos : 1; // unit pos on bezier. clamped
}
Usage
Example usage to find closest point on two beziers
The defined geometry
const bzA = {
p1: Point(10, 100), // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const bzB = {
p1: bzA.p3, // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const mouse = Point(?,?);
Finding closest
// Find first point
closestPointOnBezier(mouse, 2, bzA.p1, bzA.p3, bzA.p2);
// copy point
var found = Point(closestPointOnBezier.foundPoint.x, closestPointOnBezier.foundPoint.y);
// get distance to mouse
var dist = Math.hypot(found.x - mouse.x, found.y - mouse.y);
// find point on second bezier
closestPointOnBezier(mouse, 2, bzB.p1, bzB.p3, bzB.p2);
// get distance of second found point
const distB = Math.hypot(closestPointOnBezier.foundPoint.x - mouse.x, closestPointOnBezier.foundPoint.y - mouse.y);
// is closer
if (distB < dist) {
found.x = closestPointOnBezier.foundPoint.x;
found.y = closestPointOnBezier.foundPoint.y;
dist = distB;
}
The closet point is found as Point
There is something I need to build, but my math ability is not up to par. What I am looking to build is something like this demo, but I need it to be a hybrid of a circle and polygon instead of a line, so to speak. The black line should be dynamic and randomly generated that basically acts as a border on the page.
Currently, I am dissecting this answer with the aim of hopefully being able to transpose it into this, but I am having massive doubts that I will be able to figure this out.
Any idea how to do this or can anybody explain the mathematics?
Below are my notes about the code from the answer I linked above.
var
cw = cvs.width = window.innerWidth,
ch = cvs.height = window.innerHeight,
cx = cw / 2,
cy = ch / 2,
xs = Array(),
ys = Array(),
npts = 20,
amplitude = 87, // can be val from 1 to 100
frequency = -2, // can be val from -10 to 1 in steps of 0.1
ctx.lineWidth = 4
// creates array of coordinates that
// divides page into regular portions
// creates array of weights
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i
ys[i] = 2.0*(Math.random()-0.5)*amplitude
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (let x = 0; x < cw; x++) {
y = 0.0
wsum = 0.0
for (let i = -5; i <= 5; i++) {
xx = x; // 0 / 1 / 2 / to value of screen width
// creates sequential sets from [-5 to 5] to [15 to 25]
ii = Math.round(x/xs[1]) + i
// `xx` is a sliding range with the total value equal to client width
// keeps `ii` within range of 0 to 20
if (ii < 0) {
xx += cw
ii += npts
}
if (ii >= npts){
xx -= cw
ii -= npts
}
// selects eleven sequential array items
// which are portions of the screen width and height
// to create staggered inclines in increments of those portions
w = Math.abs(xs[ii] - xx)
// creates irregular arcs
// based on the inclining values
w = Math.pow(w, frequency)
// also creates irregular arcs therefrom
y += w*ys[ii];
// creates sets of inclining values
wsum += w;
}
// provides a relative position or weight
// for each y-coordinate in the total path
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
This is my answer. Please read the comments in the code. I hope this is what you need.
// initiate the canvas
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 600),
cx = cw / 2;
let ch = (canvas.height = 400),
cy = ch / 2;
ctx.fillStyle = "white"
// define the corners of an rectangle
let corners = [[100, 100], [500, 100], [500, 300], [100, 300]];
let amplitud = 20;// oscilation amplitude
let speed = 0.01;// the speed of the oscilation
let points = []; // an array of points to draw the curve
class Point {
constructor(x, y, hv) {
// the point is oscilating around this point (cx,cy)
this.cx = x;
this.cy = y;
// the current angle of oscilation
this.a = Math.random() * 2 * Math.PI;
this.hv = hv;// a variable to know if the oscilation is horizontal or vertical
this.update();
}
// a function to update the value of the angle
update() {
this.a += speed;
if (this.hv == 0) {
this.x = this.cx;
this.y = this.cy + amplitud * Math.cos(this.a);
} else {
this.x = this.cx + amplitud * Math.cos(this.a);
this.y = this.cy;
}
}
}
// a function to divide a line that goes from a to b in n segments
// I'm using the resulting points to create a new point object and push this new point into the points array
function divide(n, a, b) {
for (var i = 0; i <= n; i++) {
let p = {
x: (b[0] - a[0]) * i / n + a[0],
y: (b[1] - a[1]) * i / n + a[1],
hv: b[1] - a[1]
};
points.push(new Point(p.x, p.y, p.hv));
}
}
divide(10, corners[0], corners[1]);points.pop();
divide(5, corners[1], corners[2]);points.pop();
divide(10, corners[2], corners[3]);points.pop();
divide(5, corners[3], corners[0]);points.pop();
// this is a function that takes an array of points and draw a curved line through those points
function drawCurves() {
//find the first midpoint and move to it
let p = {};
p.x = (points[points.length - 1].x + points[0].x) / 2;
p.y = (points[points.length - 1].y + points[0].y) / 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
//curve through the rest, stopping at each midpoint
for (var i = 0; i < points.length - 1; i++) {
let mp = {};
mp.x = (points[i].x + points[i + 1].x) / 2;
mp.y = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, mp.x, mp.y);
}
//curve through the last point, back to the first midpoint
ctx.quadraticCurveTo(
points[points.length - 1].x,
points[points.length - 1].y,
p.x,
p.y
);
ctx.stroke();
ctx.fill();
}
function Draw() {
window.requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
points.map(p => {
p.update();
});
drawCurves();
}
Draw();
canvas{border:1px solid; background:#6ab150}
<canvas></canvas>
I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
I have two questions, the first being how do I access the indexes within my array separately, because my console.log of [n][0] results in two values - x and y. Secondly, for the butterfly curve, https://en.wikipedia.org/wiki/Butterfly_curve_%28transcendental%29, how would I determine the values of t? and reiterate through a certain minimum and maximum. In need of logic support.
Here's my progress so far.
/*function drawButterFly(n){
c.beginPath();
console.log(n[2])
for (var i = 0; i < n.length; i++){
if (i === 0) {
c.moveTo();
} else {
c.lineTo();
}
c.stroke();
}
}*/
function butterFly() {
var r = 5;
var N = 3;
var value = [];
for (var a = 0.2; a < 2*Math.PI; a = a + 0.1){
value.push(a);
}
var t = value[Math.floor(Math.random()*value.length)];
var cos = r*Math.cos(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var sin = r*Math.sin(t)*( (Math.exp(Math.cos(t))) - (2*Math.cos(4*t)) - (Math.sin(t/12)^5) );
var n = [];
for (var u = 0; u < N; u++){
var x = sin * -u;
var y = cos * -u;
n.push([x,y]);
}
drawButterFly(n);
}
Since you're pushing an array here: n.push([x,y]) you can access the x component of the first element with n[0][0] and the y component of the same element with n[0][1]
Example:
var n = [];
n.push( ["x", "y"] );
console.log( n[0][0] );
console.log( n[0][1] );
As for the useful values of t - in the image you've shown, you'll notice that the same butterfly is drawn several times at different sizes. To draw a complete butterfly, you need to use the range for t of [0..2pi]. If you want to draw two butterflies, you need to use the range [0..4pi]. That is it's cyclic over the same period that a circle is. Unlike a circle however, each cycle doesn't draw over the previous one.
Here's a quick and nasty example:
function byId(id) {
return document.getElementById(id);
}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt) {
butterFly();
}
function butterFly() {
var pointArray = [];
var stepSize = 0.05; // ~125 steps for every 360°
var upperLimit = 4 * Math.PI;
var scale = 20;
for (var t = 0.0; t < upperLimit; t += stepSize) {
var xVal = Math.sin(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
var yVal = Math.cos(t) * ((Math.exp(Math.cos(t))) - (2 * Math.cos(4 * t)) - (Math.pow(Math.sin(t / 12), 5)));
pointArray.push([scale * xVal, -scale * yVal]); // -1 value since screen-y direction is opposite direction to cartesian coords y
}
drawButterFly(pointArray);
}
function drawButterFly(pointArray) {
var can = byId('myCan');
var ctx = can.getContext('2d');
var originX, originY;
originX = can.width / 2;
originY = can.height / 2;
ctx.beginPath();
for (var i = 0; i < pointArray.length; i++) {
if (i === 0) {
ctx.moveTo(originX + pointArray[i][0], originX + pointArray[i][1]);
} else {
ctx.lineTo(originX + pointArray[i][0], originY + pointArray[i][1]);
}
}
ctx.closePath();
ctx.stroke();
}
canvas {
border: solid 1px red;
}
<canvas id='myCan' width='256' height='256' />
If I'm not mistaken, the Butterfly curve is given as a pair of parametric equations, meaning you increment t to get the next (x, y) points on your curve. In other words, your t is what you should be using in place of u in your code, and the range of values for t should be 0 .. 24*pi as that's the range in which sin(t / 12) has its unique values).
Here's a version that demonstrates the drawing of the curve to a canvas:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
// First path
ctx.beginPath();
ctx.strokeStyle = 'blue';
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var p = getPoint(0, scale, offset);
ctx.moveTo(p.x, canvas.height - p.y);
for (var t = 0.01; t <= maxT; t += 0.01) {
p = getPoint(t, scale, offset);
ctx.lineTo(p.x, canvas.height - p.y);
}
ctx.stroke();
#c {
border: solid 1px black;
}
<canvas id="c"></canvas>
One thing to note: canvases have y = 0 start at the top, so you need to inverse your y (i.e. canvas.height - y) to have your curve orient correctly.
UPDATE: Added animated version
As requested by royhowie, here's an animated version, using requestAnimationFrame:
function getPoint(t, S, O) {
var cos_t = Math.cos(t);
var factor = Math.exp(cos_t) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5);
return {
x: S * Math.sin(t) * factor + O.x,
y: S * cos_t * factor + O.y
};
}
var canvas = document.getElementById("c");
canvas.width = 300;
canvas.height = 300;
var ctx = canvas.getContext("2d");
var offset = {x:150, y:120};
var scale = 40;
var maxT = 24 * Math.PI;
var animationID;
var started = false;
var t = 0;
document.getElementById('start').addEventListener('click', function(e) {
e.preventDefault();
if (!started) {
animationID = requestAnimationFrame(animate);
started = true;
}
});
document.getElementById('pause').addEventListener('click', function(e) {
e.preventDefault();
if (started) {
cancelAnimationFrame(animationID);
started = false;
}
});
function animate() {
animationID = requestAnimationFrame(animate);
var p = getPoint(t, scale, offset);
if (t === 0) {
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.moveTo(p.x, canvas.height - p.y);
t += 0.01;
} else if (t < maxT) {
ctx.lineTo(p.x, canvas.height - p.y);
ctx.stroke();
t += 0.01;
} else {
cancelAnimationFrame(animationID);
}
}
#c {
border: solid 1px black;
}
<div>
<button id="start">Start</button>
<button id="pause">Pause</button>
</div>
<canvas id="c"></canvas>
Question 1
For an arbitrary integer i, let n[i] = [xi, yi]. xi can then be accessed via n[i][0] and yi via n[i][1]
Question 2
For values of t, I am sure you're gonna want to use sub-integer values, so I recommend using a constant increment value representing the "resolution" of your graph.
Let's call it dt. Also, I'd advise changing your variable names from single letters to something more descriptive, like min_t and max_t, and instead of n I'm going to call your array points.
function drawButterFly(points){
for (var i = 0, n = points.count; i < n; ++i) {
var x = points[i][0];
var y = points[i][1];
...
}
}
function butterFly(min_t, max_t, dt, r) {
var points = [];
for (var t = min_t; t < max_t; t+=dt){
var x = r*Math.sin(t)*...
var y = r*Math.cos(t)*...
points.push([x,y]);
}
drawButterFly(points, dt);
}
I'm not sure what the other loop inside of that function was for, but if you need it, you can adapt from the pattern above.
Usage example: butterFly(0, 10, 0.01, 3) -> t goes from 0 to 10 with an increment of 0.01, and r=3
Regarding your first question it's a better option to replace the multidimensional array containing the x and y coordinates with an object. Then when iterating over the array you can check for the object values.
So instead of:
n.push([x,y]);
you should do:
m.push({
'xPos' : x,
'yPos' : y
})
Later you can access this by m.xPos or m.yPos
Then you can access the x and y values by object literal names.
Regarding the second question: for a good pseudo code implementation of butterfly curves you might check Paul Burke site: http://paulbourke.net/geometry/butterfly/. So t in your case is:
t = i * 24.0 * PI / N;
As you see t is a parametric value which got incremented on each step when iterating over the array.
For a drawing application, I'm saving the mouse movement coordinates to an array then drawing them with lineTo. The resulting line is not smooth. How can I produce a single curve between all the gathered points?
I've googled but I have only found 3 functions for drawing lines: For 2 sample points, simply use lineTo. For 3 sample points quadraticCurveTo, for 4 sample points, bezierCurveTo.
(I tried drawing a bezierCurveTo for every 4 points in the array, but this leads to kinks every 4 sample points, instead of a continuous smooth curve.)
How do I write a function to draw a smooth curve with 5 sample points and beyond?
The problem with joining subsequent sample points together with disjoint "curveTo" type functions, is that where the curves meet is not smooth. This is because the two curves share an end point but are influenced by completely disjoint control points. One solution is to "curve to" the midpoints between the next 2 subsequent sample points. Joining the curves using these new interpolated points gives a smooth transition at the end points (what is an end point for one iteration becomes a control point for the next iteration.) In other words the two disjointed curves have much more in common now.
This solution was extracted out of the book "Foundation ActionScript 3.0 Animation: Making things move". p.95 - rendering techniques: creating multiple curves.
Note: this solution does not actually draw through each of the points, which was the title of my question (rather it approximates the curve through the sample points but never goes through the sample points), but for my purposes (a drawing application), it's good enough for me and visually you can't tell the difference. There is a solution to go through all the sample points, but it is much more complicated (see http://www.cartogrammar.com/blog/actionscript-curves-update/)
Here is the the drawing code for the approximation method:
// move to the first point
ctx.moveTo(points[0].x, points[0].y);
for (i = 1; i < points.length - 2; i ++)
{
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
// curve through the last two points
ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);
A bit late, but for the record.
You can achieve smooth lines by using cardinal splines (aka canonical spline) to draw smooth curves that goes through the points.
I made this function for canvas - it's split into three function to increase versatility. The main wrapper function looks like this:
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {
showPoints = showPoints ? showPoints : false;
ctx.beginPath();
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
if (showPoints) {
ctx.stroke();
ctx.beginPath();
for(var i=0;i<ptsa.length-1;i+=2)
ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
}
}
To draw a curve have an array with x, y points in the order: x1,y1, x2,y2, ...xn,yn.
Use it like this:
var myPoints = [10,10, 40,30, 100,10]; //minimum two points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
The function above calls two sub-functions, one to calculate the smoothed points. This returns an array with new points - this is the core function which calculates the smoothed points:
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = (typeof tension != 'undefined') ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
var _pts = [], res = [], // clone array
x, y, // our x,y coords
t1x, t2x, t1y, t2y, // tension vectors
c1, c2, c3, c4, // cardinal points
st, t, i; // steps based on num. of segments
// clone array so we don't change the original
//
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
}
else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i=2; i < (_pts.length - 4); i+=2) {
for (t=0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i+2] - _pts[i-2]) * tension;
t2x = (_pts[i+4] - _pts[i]) * tension;
t1y = (_pts[i+3] - _pts[i-1]) * tension;
t2y = (_pts[i+5] - _pts[i+1]) * tension;
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
And to actually draw the points as a smoothed curve (or any other segmented lines as long as you have an x,y array):
function drawLines(ctx, pts) {
ctx.moveTo(pts[0], pts[1]);
for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
var ctx = document.getElementById("c").getContext("2d");
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {
ctx.beginPath();
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));
if (showPoints) {
ctx.beginPath();
for(var i=0;i<ptsa.length-1;i+=2)
ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);
}
ctx.stroke();
}
var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points
var tension = 1;
drawCurve(ctx, myPoints); //default tension=0.5
drawCurve(ctx, myPoints, tension);
function getCurvePoints(pts, tension, isClosed, numOfSegments) {
// use input value if provided, or use a default value
tension = (typeof tension != 'undefined') ? tension : 0.5;
isClosed = isClosed ? isClosed : false;
numOfSegments = numOfSegments ? numOfSegments : 16;
var _pts = [], res = [], // clone array
x, y, // our x,y coords
t1x, t2x, t1y, t2y, // tension vectors
c1, c2, c3, c4, // cardinal points
st, t, i; // steps based on num. of segments
// clone array so we don't change the original
//
_pts = pts.slice(0);
// The algorithm require a previous and next point to the actual point array.
// Check if we will draw closed or open curve.
// If closed, copy end points to beginning and first points to end
// If open, duplicate first points to befinning, end points to end
if (isClosed) {
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.unshift(pts[pts.length - 1]);
_pts.unshift(pts[pts.length - 2]);
_pts.push(pts[0]);
_pts.push(pts[1]);
}
else {
_pts.unshift(pts[1]); //copy 1. point and insert at beginning
_pts.unshift(pts[0]);
_pts.push(pts[pts.length - 2]); //copy last point and append
_pts.push(pts[pts.length - 1]);
}
// ok, lets start..
// 1. loop goes through point array
// 2. loop goes through each segment between the 2 pts + 1e point before and after
for (i=2; i < (_pts.length - 4); i+=2) {
for (t=0; t <= numOfSegments; t++) {
// calc tension vectors
t1x = (_pts[i+2] - _pts[i-2]) * tension;
t2x = (_pts[i+4] - _pts[i]) * tension;
t1y = (_pts[i+3] - _pts[i-1]) * tension;
t2y = (_pts[i+5] - _pts[i+1]) * tension;
// calc step
st = t / numOfSegments;
// calc cardinals
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1;
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2);
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st;
c4 = Math.pow(st, 3) - Math.pow(st, 2);
// calc x and y cords with common control vectors
x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;
y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;
//store points in array
res.push(x);
res.push(y);
}
}
return res;
}
function drawLines(ctx, pts) {
ctx.moveTo(pts[0], pts[1]);
for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);
}
canvas { border: 1px solid red; }
<canvas id="c"><canvas>
This results in this:
You can easily extend the canvas so you can call it like this instead:
ctx.drawCurve(myPoints);
Add the following to the javascript:
if (CanvasRenderingContext2D != 'undefined') {
CanvasRenderingContext2D.prototype.drawCurve =
function(pts, tension, isClosed, numOfSegments, showPoints) {
drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}
}
You can find a more optimized version of this on NPM (npm i cardinal-spline-js) or on GitLab.
The first answer will not pass through all the points. This graph will exactly pass through all the points and will be a perfect curve with the points as [{x:,y:}] n such points.
var points = [{x:1,y:1},{x:2,y:3},{x:3,y:4},{x:4,y:2},{x:5,y:6}] //took 5 example points
ctx.moveTo((points[0].x), points[0].y);
for(var i = 0; i < points.length-1; i ++)
{
var x_mid = (points[i].x + points[i+1].x) / 2;
var y_mid = (points[i].y + points[i+1].y) / 2;
var cp_x1 = (x_mid + points[i].x) / 2;
var cp_x2 = (x_mid + points[i+1].x) / 2;
ctx.quadraticCurveTo(cp_x1,points[i].y ,x_mid, y_mid);
ctx.quadraticCurveTo(cp_x2,points[i+1].y ,points[i+1].x,points[i+1].y);
}
I decide to add on, rather than posting my solution to another post.
Below are the solution that I build, may not be perfect, but so far the output are good.
Important: it will pass through all the points!
If you have any idea, to make it better, please share to me. Thanks.
Here are the comparison of before after:
Save this code to HTML to test it out.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var cv = document.getElementById("myCanvas");
var ctx = cv.getContext("2d");
function gradient(a, b) {
return (b.y-a.y)/(b.x-a.x);
}
function bzCurve(points, f, t) {
//f = 0, will be straight line
//t suppose to be 1, but changing the value can control the smoothness too
if (typeof(f) == 'undefined') f = 0.3;
if (typeof(t) == 'undefined') t = 0.6;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
var m = 0;
var dx1 = 0;
var dy1 = 0;
var preP = points[0];
for (var i = 1; i < points.length; i++) {
var curP = points[i];
nexP = points[i + 1];
if (nexP) {
m = gradient(preP, nexP);
dx2 = (nexP.x - curP.x) * -f;
dy2 = dx2 * m * t;
} else {
dx2 = 0;
dy2 = 0;
}
ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y);
dx1 = dx2;
dy1 = dy2;
preP = curP;
}
ctx.stroke();
}
// Generate random data
var lines = [];
var X = 10;
var t = 40; //to control width of X
for (var i = 0; i < 100; i++ ) {
Y = Math.floor((Math.random() * 300) + 50);
p = { x: X, y: Y };
lines.push(p);
X = X + t;
}
//draw straight line
ctx.beginPath();
ctx.setLineDash([5]);
ctx.lineWidth = 1;
bzCurve(lines, 0, 1);
//draw smooth line
ctx.setLineDash([0]);
ctx.lineWidth = 2;
ctx.strokeStyle = "blue";
bzCurve(lines, 0.3, 1);
</script>
</body>
</html>
As Daniel Howard points out, Rob Spencer describes what you want at http://scaledinnovation.com/analytics/splines/aboutSplines.html.
Here's an interactive demo: http://jsbin.com/ApitIxo/2/
Here it is as a snippet in case jsbin is down.
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Demo smooth connection</title>
</head>
<body>
<div id="display">
Click to build a smooth path.
(See Rob Spencer's article)
<br><label><input type="checkbox" id="showPoints" checked> Show points</label>
<br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label>
<br>
<label>
<input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span>
</label>
<div id="mouse"></div>
</div>
<canvas id="canvas"></canvas>
<style>
html { position: relative; height: 100%; width: 100%; }
body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; }
canvas { outline: 1px solid red; }
#display { position: fixed; margin: 8px; background: white; z-index: 1; }
</style>
<script>
function update() {
$("tensionvalue").innerHTML="("+$("tension").value+")";
drawSplines();
}
$("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update;
// utility function
function $(id){ return document.getElementById(id); }
var canvas=$("canvas"), ctx=canvas.getContext("2d");
function setCanvasSize() {
canvas.width = parseInt(window.getComputedStyle(document.body).width);
canvas.height = parseInt(window.getComputedStyle(document.body).height);
}
window.onload = window.onresize = setCanvasSize();
function mousePositionOnCanvas(e) {
var el=e.target, c=el;
var scaleX = c.width/c.offsetWidth || 1;
var scaleY = c.height/c.offsetHeight || 1;
if (!isNaN(e.offsetX))
return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };
var x=e.pageX, y=e.pageY;
do {
x -= el.offsetLeft;
y -= el.offsetTop;
el = el.offsetParent;
} while (el);
return { x: x*scaleX, y: y*scaleY };
}
canvas.onclick = function(e){
var p = mousePositionOnCanvas(e);
addSplinePoint(p.x, p.y);
};
function drawPoint(x,y,color){
ctx.save();
ctx.fillStyle=color;
ctx.beginPath();
ctx.arc(x,y,3,0,2*Math.PI);
ctx.fill()
ctx.restore();
}
canvas.onmousemove = function(e) {
var p = mousePositionOnCanvas(e);
$("mouse").innerHTML = p.x+","+p.y;
};
var pts=[]; // a list of x and ys
// given an array of x,y's, return distance between any two,
// note that i and j are indexes to the points, not directly into the array.
function dista(arr, i, j) {
return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2));
}
// return vector from i to j where i and j are indexes pointing into an array of points.
function va(arr, i, j){
return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]]
}
function ctlpts(x1,y1,x2,y2,x3,y3) {
var t = $("tension").value;
var v = va(arguments, 0, 2);
var d01 = dista(arguments, 0, 1);
var d12 = dista(arguments, 1, 2);
var d012 = d01 + d12;
return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012,
x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ];
}
function addSplinePoint(x, y){
pts.push(x); pts.push(y);
drawSplines();
}
function drawSplines() {
clear();
cps = []; // There will be two control points for each "middle" point, 1 ... len-2e
for (var i = 0; i < pts.length - 2; i += 1) {
cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1],
pts[2*i+2], pts[2*i+3],
pts[2*i+4], pts[2*i+5]));
}
if ($("showControlLines").checked) drawControlPoints(cps);
if ($("showPoints").checked) drawPoints(pts);
drawCurvedPath(cps, pts);
}
function drawControlPoints(cps) {
for (var i = 0; i < cps.length; i += 4) {
showPt(cps[i], cps[i+1], "pink");
showPt(cps[i+2], cps[i+3], "pink");
drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink");
}
}
function drawPoints(pts) {
for (var i = 0; i < pts.length; i += 2) {
showPt(pts[i], pts[i+1], "black");
}
}
function drawCurvedPath(cps, pts){
var len = pts.length / 2; // number of points
if (len < 2) return;
if (len == 2) {
ctx.beginPath();
ctx.moveTo(pts[0], pts[1]);
ctx.lineTo(pts[2], pts[3]);
ctx.stroke();
}
else {
ctx.beginPath();
ctx.moveTo(pts[0], pts[1]);
// from point 0 to point 1 is a quadratic
ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);
// for all middle points, connect with bezier
for (var i = 2; i < len-1; i += 1) {
// console.log("to", pts[2*i], pts[2*i+1]);
ctx.bezierCurveTo(
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
cps[(2*(i-1))*2], cps[(2*(i-1))*2+1],
pts[i*2], pts[i*2+1]);
}
ctx.quadraticCurveTo(
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],
pts[i*2], pts[i*2+1]);
ctx.stroke();
}
}
function clear() {
ctx.save();
// use alpha to fade out
ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.restore();
}
function showPt(x,y,fillStyle) {
ctx.save();
ctx.beginPath();
if (fillStyle) {
ctx.fillStyle = fillStyle;
}
ctx.arc(x, y, 5, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
}
function drawLine(x1, y1, x2, y2, strokeStyle){
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
if (strokeStyle) {
ctx.save();
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.restore();
}
else {
ctx.save();
ctx.strokeStyle = "pink";
ctx.stroke();
ctx.restore();
}
}
</script>
</body>
</html>
I found this to work nicely
function drawCurve(points, tension) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
var t = (tension != null) ? tension : 1;
for (var i = 0; i < points.length - 1; i++) {
var p0 = (i > 0) ? points[i - 1] : points[0];
var p1 = points[i];
var p2 = points[i + 1];
var p3 = (i != points.length - 2) ? points[i + 2] : p2;
var cp1x = p1.x + (p2.x - p0.x) / 6 * t;
var cp1y = p1.y + (p2.y - p0.y) / 6 * t;
var cp2x = p2.x - (p3.x - p1.x) / 6 * t;
var cp2y = p2.y - (p3.y - p1.y) / 6 * t;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
ctx.stroke();
}
Give KineticJS a try - you can define a Spline with an array of points. Here's an example:
Old url: http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/
See archive url: https://web.archive.org/web/20141204030628/http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/
Bonjour
I appreciate the solution of user1693593 : Hermite polynomials seems the best way to control what will be drawn, and the most satisfying from a mathematical point of view.
The subject seems to be closed for a long time but may be some latecomers like me are still interested in it.
I've looked for a free interactive plot builder which could allow me to store the curve and reuse it anywhere else, but didn't find this kind of thing on the web : so I made it on my own way, from the wikipedia source mentionned by user1693593.
It's difficult to explain how it works here, and the best way to know if it is worth while is to look at https://sites.google.com/view/divertissements/accueil/splines.
Incredibly late but inspired by Homan's brilliantly simple answer, allow me to post a more general solution (general in the sense that Homan's solution crashes on arrays of points with less than 3 vertices):
function smooth(ctx, points)
{
if(points == undefined || points.length == 0)
{
return true;
}
if(points.length == 1)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[0].x, points[0].y);
return true;
}
if(points.length == 2)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
return true;
}
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length - 2; i ++)
{
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}
This code is perfect for me:
this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
this.context.bezierCurveTo(
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i - 1].y,
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i].y,
data[i].x,
data[i].y);
}
you have correct smooth line and correct endPoints
NOTICE! (y = "canvas height" - y);
A slightly different answer to the original question;
If anyone is desiring to draw a shape:
that is described by a series of points
where the line has a small curve at the points
the line doesn't necessarily have to pass through the points (i.e. passes slightly "inside", of them)
Then hopefully the below function of mine could help
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="1200" height="700" style="border: 1px solid #d3d3d3">Your browser does not support the
HTML5 canvas tag.</canvas>
<script>
var cv = document.getElementById("myCanvas");
var ctx = cv.getContext("2d");
const drawPointsWithCurvedCorners = (points, ctx) => {
for (let n = 0; n <= points.length - 1; n++) {
let pointA = points[n];
let pointB = points[(n + 1) % points.length];
let pointC = points[(n + 2) % points.length];
const midPointAB = {
x: pointA.x + (pointB.x - pointA.x) / 2,
y: pointA.y + (pointB.y - pointA.y) / 2,
};
const midPointBC = {
x: pointB.x + (pointC.x - pointB.x) / 2,
y: pointB.y + (pointC.y - pointB.y) / 2,
};
ctx.moveTo(midPointAB.x, midPointAB.y);
ctx.arcTo(
pointB.x,
pointB.y,
midPointBC.x,
midPointBC.y,
radii[pointB.r]
);
ctx.lineTo(midPointBC.x, midPointBC.y);
}
};
const shapeWidth = 200;
const shapeHeight = 150;
const topInsetDepth = 35;
const topInsetSideWidth = 20;
const topInsetHorizOffset = shapeWidth * 0.25;
const radii = {
small: 15,
large: 30,
};
const points = [
{
// TOP-LEFT
x: 0,
y: 0,
r: "large",
},
{
x: topInsetHorizOffset,
y: 0,
r: "small",
},
{
x: topInsetHorizOffset + topInsetSideWidth,
y: topInsetDepth,
r: "small",
},
{
x: shapeWidth - (topInsetHorizOffset + topInsetSideWidth),
y: topInsetDepth,
r: "small",
},
{
x: shapeWidth - topInsetHorizOffset,
y: 0,
r: "small",
},
{
// TOP-RIGHT
x: shapeWidth,
y: 0,
r: "large",
},
{
// BOTTOM-RIGHT
x: shapeWidth,
y: shapeHeight,
r: "large",
},
{
// BOTTOM-LEFT
x: 0,
y: shapeHeight,
r: "large",
},
];
// ACTUAL DRAWING OF POINTS
ctx.beginPath();
drawPointsWithCurvedCorners(points, ctx);
ctx.stroke();
</script>
</body>
</html>
To add to K3N's cardinal splines method and perhaps address T. J. Crowder's concerns about curves 'dipping' in misleading places, I inserted the following code in the getCurvePoints() function, just before res.push(x);
if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) {
y = (_pts[i+1] + _pts[i+3]) / 2;
}
if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) {
x = (_pts[i] + _pts[i+2]) / 2;
}
This effectively creates a (invisible) bounding box between each pair of successive points and ensures the curve stays within this bounding box - ie. if a point on the curve is above/below/left/right of both points, it alters its position to be within the box. Here the midpoint is used, but this could be improved upon, perhaps using linear interpolation.
If you want to determine the equation of the curve through n points then the following code will give you the coefficients of the polynomial of degree n-1 and save these coefficients to the coefficients[] array (starting from the constant term). The x coordinates do not have to be in order. This is an example of a Lagrange polynomial.
var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
for (var m=0; m<xPoints.length; m++) {
var newCoefficients=[];
for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
if (m>0) {
newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
} else {
newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
}
var startIndex=1;
if (m==0) startIndex=2;
for (var n=startIndex; n<xPoints.length; n++) {
if (m==n) continue;
for (var nc=xPoints.length-1; nc>=1; nc--) {
newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
}
newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
}
for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}
I somehow need a way that uses only quadratic bezier. This is my method and can be extended to 3d:
The formula for the quad bezier curve is
b(t) = (1-t)^2A + 2(1-t)tB + t^2*C
When t = 0 or 1, the curve can pass through point A or C but is not guaranteed to pass through B.
Its first-order derivative is
b'(t) = 2(t-1)A + 2(1-2t)B + 2tC
To construct a curve passing through points P0,P1,P2 with two quad bezier curves, the slopes of the two bezier curves at p1 should be equal
b'α(t) = 2(t-1)P0 + 2(1-2t)M1 + 2tP1
b'β(t) = 2(t-1)P1 + 2(1-2t)M2 + 2tP2
b'α(1) = b'β(0)
This gives
(M1 + M2) / 2 = P1
So a curve through 3 points can be drawn like this
bezier(p0, m1, p1);
bezier(p1, m2, p2);
Where m1p1 = p1m2. The direction of m1m2 is not matter, can be found by p2 - p1.
For curves passing through 4 or more points
bezier(p0, m1, p1);
bezier(p1, m2, (m2 + m3) / 2);
bezier((m2 + m3) / 2, m3, p2);
bezier(p2, m4, p3);
Where m1p1 = p1m2 and m3p2 = p2m4.
function drawCurve(ctx: CanvasRenderingContext2D, points: { x: number, y: number }[], tension = 2) {
if (points.length < 2) {
return;
}
ctx.beginPath();
if (points.length === 2) {
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.stroke();
return;
}
let prevM2x = 0;
let prevM2y = 0;
for (let i = 1, len = points.length; i < len - 1; ++i) {
const p0 = points[i - 1];
const p1 = points[i];
const p2 = points[i + 1];
let tx = p2.x - (i === 1 ? p0.x : prevM2x);
let ty = p2.y - (i === 1 ? p0.y : prevM2y);
const tLen = Math.sqrt(tx ** 2 + ty ** 2);
if (tLen > 1e-8) {
const inv = 1 / tLen;
tx *= inv;
ty *= inv;
} else {
tx = 0;
ty = 0;
}
const det = Math.sqrt(Math.min(
(p0.x - p1.x) ** 2 + (p0.y - p1.y) ** 2,
(p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
)) / (2 * tension);
const m1x = p1.x - tx * det;
const m1y = p1.y - ty * det;
const m2x = p1.x + tx * det;
const m2y = p1.y + ty * det;
if (i === 1) {
ctx.moveTo(p0.x, p0.y);
ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
} else {
const mx = (prevM2x + m1x) / 2;
const my = (prevM2y + m1y) / 2;
ctx.quadraticCurveTo(prevM2x, prevM2y, mx, my);
ctx.quadraticCurveTo(m1x, m1y, p1.x, p1.y);
}
if (i === len - 2) {
ctx.quadraticCurveTo(m2x, m2y, p2.x, p2.y);
}
prevM2x = m2x;
prevM2y = m2y;
}
ctx.stroke();
}