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?
This question is related to
javascript
canvas
html5-canvas
bezier
spline
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>_x000D_
<html>_x000D_
<head>_x000D_
<meta charset=utf-8 />_x000D_
<title>Demo smooth connection</title>_x000D_
</head>_x000D_
<body>_x000D_
<div id="display">_x000D_
Click to build a smooth path. _x000D_
(See Rob Spencer's <a href="http://scaledinnovation.com/analytics/splines/aboutSplines.html">article</a>)_x000D_
<br><label><input type="checkbox" id="showPoints" checked> Show points</label>_x000D_
<br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label>_x000D_
<br>_x000D_
<label>_x000D_
<input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span>_x000D_
</label>_x000D_
<div id="mouse"></div>_x000D_
</div>_x000D_
<canvas id="canvas"></canvas>_x000D_
<style>_x000D_
html { position: relative; height: 100%; width: 100%; }_x000D_
body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; } _x000D_
canvas { outline: 1px solid red; }_x000D_
#display { position: fixed; margin: 8px; background: white; z-index: 1; }_x000D_
</style>_x000D_
<script>_x000D_
function update() {_x000D_
$("tensionvalue").innerHTML="("+$("tension").value+")";_x000D_
drawSplines();_x000D_
}_x000D_
$("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update;_x000D_
_x000D_
// utility function_x000D_
function $(id){ return document.getElementById(id); }_x000D_
var canvas=$("canvas"), ctx=canvas.getContext("2d");_x000D_
_x000D_
function setCanvasSize() {_x000D_
canvas.width = parseInt(window.getComputedStyle(document.body).width);_x000D_
canvas.height = parseInt(window.getComputedStyle(document.body).height);_x000D_
}_x000D_
window.onload = window.onresize = setCanvasSize();_x000D_
_x000D_
function mousePositionOnCanvas(e) {_x000D_
var el=e.target, c=el;_x000D_
var scaleX = c.width/c.offsetWidth || 1;_x000D_
var scaleY = c.height/c.offsetHeight || 1;_x000D_
_x000D_
if (!isNaN(e.offsetX)) _x000D_
return { x:e.offsetX*scaleX, y:e.offsetY*scaleY };_x000D_
_x000D_
var x=e.pageX, y=e.pageY;_x000D_
do {_x000D_
x -= el.offsetLeft;_x000D_
y -= el.offsetTop;_x000D_
el = el.offsetParent;_x000D_
} while (el);_x000D_
return { x: x*scaleX, y: y*scaleY };_x000D_
}_x000D_
_x000D_
canvas.onclick = function(e){_x000D_
var p = mousePositionOnCanvas(e);_x000D_
addSplinePoint(p.x, p.y);_x000D_
};_x000D_
_x000D_
function drawPoint(x,y,color){_x000D_
ctx.save();_x000D_
ctx.fillStyle=color;_x000D_
ctx.beginPath();_x000D_
ctx.arc(x,y,3,0,2*Math.PI);_x000D_
ctx.fill()_x000D_
ctx.restore();_x000D_
}_x000D_
canvas.onmousemove = function(e) {_x000D_
var p = mousePositionOnCanvas(e);_x000D_
$("mouse").innerHTML = p.x+","+p.y;_x000D_
};_x000D_
_x000D_
var pts=[]; // a list of x and ys_x000D_
_x000D_
// given an array of x,y's, return distance between any two,_x000D_
// note that i and j are indexes to the points, not directly into the array._x000D_
function dista(arr, i, j) {_x000D_
return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2));_x000D_
}_x000D_
_x000D_
// return vector from i to j where i and j are indexes pointing into an array of points._x000D_
function va(arr, i, j){_x000D_
return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]]_x000D_
}_x000D_
_x000D_
function ctlpts(x1,y1,x2,y2,x3,y3) {_x000D_
var t = $("tension").value;_x000D_
var v = va(arguments, 0, 2);_x000D_
var d01 = dista(arguments, 0, 1);_x000D_
var d12 = dista(arguments, 1, 2);_x000D_
var d012 = d01 + d12;_x000D_
return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012,_x000D_
x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ];_x000D_
}_x000D_
_x000D_
function addSplinePoint(x, y){_x000D_
pts.push(x); pts.push(y);_x000D_
drawSplines();_x000D_
}_x000D_
function drawSplines() {_x000D_
clear();_x000D_
cps = []; // There will be two control points for each "middle" point, 1 ... len-2e_x000D_
for (var i = 0; i < pts.length - 2; i += 1) {_x000D_
cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1], _x000D_
pts[2*i+2], pts[2*i+3], _x000D_
pts[2*i+4], pts[2*i+5]));_x000D_
}_x000D_
if ($("showControlLines").checked) drawControlPoints(cps);_x000D_
if ($("showPoints").checked) drawPoints(pts);_x000D_
_x000D_
drawCurvedPath(cps, pts);_x000D_
_x000D_
}_x000D_
function drawControlPoints(cps) {_x000D_
for (var i = 0; i < cps.length; i += 4) {_x000D_
showPt(cps[i], cps[i+1], "pink");_x000D_
showPt(cps[i+2], cps[i+3], "pink");_x000D_
drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink");_x000D_
} _x000D_
}_x000D_
_x000D_
function drawPoints(pts) {_x000D_
for (var i = 0; i < pts.length; i += 2) {_x000D_
showPt(pts[i], pts[i+1], "black");_x000D_
} _x000D_
}_x000D_
_x000D_
function drawCurvedPath(cps, pts){_x000D_
var len = pts.length / 2; // number of points_x000D_
if (len < 2) return;_x000D_
if (len == 2) {_x000D_
ctx.beginPath();_x000D_
ctx.moveTo(pts[0], pts[1]);_x000D_
ctx.lineTo(pts[2], pts[3]);_x000D_
ctx.stroke();_x000D_
}_x000D_
else {_x000D_
ctx.beginPath();_x000D_
ctx.moveTo(pts[0], pts[1]);_x000D_
// from point 0 to point 1 is a quadratic_x000D_
ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]);_x000D_
// for all middle points, connect with bezier_x000D_
for (var i = 2; i < len-1; i += 1) {_x000D_
// console.log("to", pts[2*i], pts[2*i+1]);_x000D_
ctx.bezierCurveTo(_x000D_
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],_x000D_
cps[(2*(i-1))*2], cps[(2*(i-1))*2+1],_x000D_
pts[i*2], pts[i*2+1]);_x000D_
}_x000D_
ctx.quadraticCurveTo(_x000D_
cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1],_x000D_
pts[i*2], pts[i*2+1]);_x000D_
ctx.stroke();_x000D_
}_x000D_
}_x000D_
function clear() {_x000D_
ctx.save();_x000D_
// use alpha to fade out_x000D_
ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen_x000D_
ctx.fillRect(0,0,canvas.width,canvas.height);_x000D_
ctx.restore();_x000D_
}_x000D_
_x000D_
function showPt(x,y,fillStyle) {_x000D_
ctx.save();_x000D_
ctx.beginPath();_x000D_
if (fillStyle) {_x000D_
ctx.fillStyle = fillStyle;_x000D_
}_x000D_
ctx.arc(x, y, 5, 0, 2*Math.PI);_x000D_
ctx.fill();_x000D_
ctx.restore();_x000D_
}_x000D_
_x000D_
function drawLine(x1, y1, x2, y2, strokeStyle){_x000D_
ctx.beginPath();_x000D_
ctx.moveTo(x1, y1);_x000D_
ctx.lineTo(x2, y2);_x000D_
if (strokeStyle) {_x000D_
ctx.save();_x000D_
ctx.strokeStyle = strokeStyle;_x000D_
ctx.stroke();_x000D_
ctx.restore();_x000D_
}_x000D_
else {_x000D_
ctx.save();_x000D_
ctx.strokeStyle = "pink";_x000D_
ctx.stroke();_x000D_
ctx.restore();_x000D_
}_x000D_
}_x000D_
_x000D_
</script>_x000D_
_x000D_
_x000D_
</body>_x000D_
</html>
_x000D_
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);
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();
}
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);
}
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>_x000D_
<html>_x000D_
<body>_x000D_
<canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas>_x000D_
<script>_x000D_
var cv = document.getElementById("myCanvas");_x000D_
var ctx = cv.getContext("2d");_x000D_
_x000D_
function gradient(a, b) {_x000D_
return (b.y-a.y)/(b.x-a.x);_x000D_
}_x000D_
_x000D_
function bzCurve(points, f, t) {_x000D_
//f = 0, will be straight line_x000D_
//t suppose to be 1, but changing the value can control the smoothness too_x000D_
if (typeof(f) == 'undefined') f = 0.3;_x000D_
if (typeof(t) == 'undefined') t = 0.6;_x000D_
_x000D_
ctx.beginPath();_x000D_
ctx.moveTo(points[0].x, points[0].y);_x000D_
_x000D_
var m = 0;_x000D_
var dx1 = 0;_x000D_
var dy1 = 0;_x000D_
_x000D_
var preP = points[0];_x000D_
for (var i = 1; i < points.length; i++) {_x000D_
var curP = points[i];_x000D_
nexP = points[i + 1];_x000D_
if (nexP) {_x000D_
m = gradient(preP, nexP);_x000D_
dx2 = (nexP.x - curP.x) * -f;_x000D_
dy2 = dx2 * m * t;_x000D_
} else {_x000D_
dx2 = 0;_x000D_
dy2 = 0;_x000D_
}_x000D_
ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y);_x000D_
dx1 = dx2;_x000D_
dy1 = dy2;_x000D_
preP = curP;_x000D_
}_x000D_
ctx.stroke();_x000D_
}_x000D_
_x000D_
// Generate random data_x000D_
var lines = [];_x000D_
var X = 10;_x000D_
var t = 40; //to control width of X_x000D_
for (var i = 0; i < 100; i++ ) {_x000D_
Y = Math.floor((Math.random() * 300) + 50);_x000D_
p = { x: X, y: Y };_x000D_
lines.push(p);_x000D_
X = X + t;_x000D_
}_x000D_
_x000D_
//draw straight line_x000D_
ctx.beginPath();_x000D_
ctx.setLineDash([5]);_x000D_
ctx.lineWidth = 1;_x000D_
bzCurve(lines, 0, 1);_x000D_
_x000D_
//draw smooth line_x000D_
ctx.setLineDash([0]);_x000D_
ctx.lineWidth = 2;_x000D_
ctx.strokeStyle = "blue";_x000D_
bzCurve(lines, 0.3, 1);_x000D_
</script>_x000D_
</body>_x000D_
</html>
_x000D_
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.
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/
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];
}
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");_x000D_
_x000D_
_x000D_
function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) {_x000D_
_x000D_
ctx.beginPath();_x000D_
_x000D_
drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments));_x000D_
_x000D_
if (showPoints) {_x000D_
ctx.beginPath();_x000D_
for(var i=0;i<ptsa.length-1;i+=2) _x000D_
ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4);_x000D_
}_x000D_
_x000D_
ctx.stroke();_x000D_
}_x000D_
_x000D_
_x000D_
var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points_x000D_
var tension = 1;_x000D_
_x000D_
drawCurve(ctx, myPoints); //default tension=0.5_x000D_
drawCurve(ctx, myPoints, tension);_x000D_
_x000D_
_x000D_
function getCurvePoints(pts, tension, isClosed, numOfSegments) {_x000D_
_x000D_
// use input value if provided, or use a default value _x000D_
tension = (typeof tension != 'undefined') ? tension : 0.5;_x000D_
isClosed = isClosed ? isClosed : false;_x000D_
numOfSegments = numOfSegments ? numOfSegments : 16;_x000D_
_x000D_
var _pts = [], res = [], // clone array_x000D_
x, y, // our x,y coords_x000D_
t1x, t2x, t1y, t2y, // tension vectors_x000D_
c1, c2, c3, c4, // cardinal points_x000D_
st, t, i; // steps based on num. of segments_x000D_
_x000D_
// clone array so we don't change the original_x000D_
//_x000D_
_pts = pts.slice(0);_x000D_
_x000D_
// The algorithm require a previous and next point to the actual point array._x000D_
// Check if we will draw closed or open curve._x000D_
// If closed, copy end points to beginning and first points to end_x000D_
// If open, duplicate first points to befinning, end points to end_x000D_
if (isClosed) {_x000D_
_pts.unshift(pts[pts.length - 1]);_x000D_
_pts.unshift(pts[pts.length - 2]);_x000D_
_pts.unshift(pts[pts.length - 1]);_x000D_
_pts.unshift(pts[pts.length - 2]);_x000D_
_pts.push(pts[0]);_x000D_
_pts.push(pts[1]);_x000D_
}_x000D_
else {_x000D_
_pts.unshift(pts[1]); //copy 1. point and insert at beginning_x000D_
_pts.unshift(pts[0]);_x000D_
_pts.push(pts[pts.length - 2]); //copy last point and append_x000D_
_pts.push(pts[pts.length - 1]);_x000D_
}_x000D_
_x000D_
// ok, lets start.._x000D_
_x000D_
// 1. loop goes through point array_x000D_
// 2. loop goes through each segment between the 2 pts + 1e point before and after_x000D_
for (i=2; i < (_pts.length - 4); i+=2) {_x000D_
for (t=0; t <= numOfSegments; t++) {_x000D_
_x000D_
// calc tension vectors_x000D_
t1x = (_pts[i+2] - _pts[i-2]) * tension;_x000D_
t2x = (_pts[i+4] - _pts[i]) * tension;_x000D_
_x000D_
t1y = (_pts[i+3] - _pts[i-1]) * tension;_x000D_
t2y = (_pts[i+5] - _pts[i+1]) * tension;_x000D_
_x000D_
// calc step_x000D_
st = t / numOfSegments;_x000D_
_x000D_
// calc cardinals_x000D_
c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1; _x000D_
c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); _x000D_
c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; _x000D_
c4 = Math.pow(st, 3) - Math.pow(st, 2);_x000D_
_x000D_
// calc x and y cords with common control vectors_x000D_
x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x;_x000D_
y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y;_x000D_
_x000D_
//store points in array_x000D_
res.push(x);_x000D_
res.push(y);_x000D_
_x000D_
}_x000D_
}_x000D_
_x000D_
return res;_x000D_
}_x000D_
_x000D_
function drawLines(ctx, pts) {_x000D_
ctx.moveTo(pts[0], pts[1]);_x000D_
for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]);_x000D_
}
_x000D_
canvas { border: 1px solid red; }
_x000D_
<canvas id="c"><canvas>
_x000D_
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.
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);
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);
}
Source: Stackoverflow.com