So I've wrote a microbe animation.
It's all cool, but I think that it would be even better, if the microbe would be able to eat diatoms, and to destroy bubbles.
The issue is that the microbe is made of bezier curves.
I have no idea how to check collision between object made of bezier curves, and a circle in a reasonable way.
The only thing that comes to my mind, is to paint the microbe shape and bubbles a hidden canvas, and then check if they paint to the same pixels. But that would cause big performance issues IMHO.
Code: https://codepen.io/michaelKurowski/pen/opWeKY
class Cell is the cell, while class CellWallNode is a node of bezier curve, in case if somebody needs to look up the implementation.
The bubbles and diatoms can be easily simplified to circles.
Solution to bounds testing object defined by beziers
Below is an example solution to finding if a circle is inside an object defined by a center point and a set of beziers defining the perimeter.
The solution has only been tested for non intersecting cubic beziers. Also will not work if there are more than two intercepts between the object being tested and the center of the cell. However all you need to solve for the more complex bounds is there in the code.
The method
Define a center point to test from as a 2D point
Define the test point as a 2D point
Define a line from the center to the test point
For each bezier
Translate bezier so first point is at start of line
Rotate the bezier such that the line is aligned to the x axis
Solve the bezier polynomials to find the roots (location of x axis intercepts)
Use the roots to find position on bezier curve of line intercept.
Use the closest intercept to the point to find distance from center to perimeter.
If perimeter distance is greater than test point distance plus radius then inside.
Notes
The test is to a point along a line to the center not to a circle which would be a area defined by a triangle. As long as the circle radius is small compared to the size of the beziers the approximation works well.
Not sure if you are using cubic or quadratic beziers so the solution covers both cubic and quadratic beziers.
Example
The snippet creates a set of beziers (cubic) around a center point. the object theBlob holds the animated beziers. The function testBlob tests the mouse position and returns true if inside theBlob. The object bezHelper contains all the functionality needed to solve the problem.
The cubic root solver was derived from github intersections cube root solver.
const bezHelper = (()=>{
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// To prevent heap thrashing close over some pre defined 2D points
const v1 = P2();
const v2 = P2();
const v3 = P2();
const v4 = P2();
var u,u1,u2;
// solves quadratic for bezier 2 returns first root
function solveBezier2(A, B, C){
// solve the 2nd order bezier equation.
// There can be 2 roots, u,u1 hold the results;
// 2nd order function a+2(-a+b)x+(a-2b+c)x^2
a = (A - 2 * B + C);
b = 2 * ( - A + B);
c = A;
a1 = 2 * a;
c = b * b - 4 * a * c;
if(c < 0){
u = Infinity;
u1 = Infinity;
return u;
}else{
b1 = Math.sqrt(c);
}
u = (-b + b1) / a1;
u1 = (-b - b1) / a1;
return u;
}
// solves cubic for bezier 3 returns first root
function solveBezier3(A, B, C, D){
// There can be 3 roots, u,u1,u2 hold the results;
// Solves 3rd order a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3 Cardano method for finding roots
// this function was derived from http://pomax.github.io/bezierinfo/#intersections cube root solver
// Also see https://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
function sqrt(v) {
if(v<0) return -Math.sqrt(-v);
return Math.sqrt(v);
}
var a, b, c, d, p, p3, q, q2, discriminant, U, v1, r, t, mp3, cosphi,phi, t1, sd;
u2 = u1 = u = -Infinity;
d = (-A + 3 * B - 3 * C + D);
a = (3 * A - 6 * B + 3 * C) / d;
b = (-3 * A + 3 * B) / d;
c = A / d;
p = (3 * b - a * a) / 3;
p3 = p / 3;
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27;
q2 = q / 2;
a /= 3;
discriminant = q2 * q2 + p3 * p3 * p3;
if (discriminant < 0) {
mp3 = -p / 3;
r = sqrt(mp3 * mp3 * mp3);
t = -q / (2 * r);
cosphi = t < -1 ? -1 : t > 1 ? 1 : t;
phi = Math.acos(cosphi);
t1 = 2 * crt(r);
u = t1 * Math.cos(phi / 3) - a;
u1 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a;
u2 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a;
return u;
}
if(discriminant === 0) {
U = q2 < 0 ? crt(-q2) : -crt(q2);
u = 2 * U - a;
u1 = -U - a;
return u;
}
sd = sqrt(discriminant);
u = crt(sd - q2) - crt(sd + q2) - a;
return u;
}
// get a point on the bezier at pos ( from 0 to 1 values outside this range will be outside the bezier)
// p1, p2 are end points and cp1, cp2 are control points.
// ret is the resulting point. If given it is set to the result, if not given a new point is created
function getPositionOnBez(pos,p1,p2,cp1,cp2,ret = P2()){
if(pos === 0){
ret.x = p1.x;
ret.y = p1.y;
return ret;
}else
if(pos === 1){
ret.x = p2.x;
ret.y = p2.y;
return ret;
}
v1.x = p1.x;
v1.y = p1.y;
var c = pos;
if(cp2 === undefined){
v2.x = cp1.x;
v2.y = cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (p2.x - v2.x) * c;
v2.y += (p2.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
v2.x = cp1.x;
v2.y = cp1.y;
v3.x = cp2.x;
v3.y = cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (p2.x - v3.x) * c;
v3.y += (p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
const cubicBez = 0;
const quadraticBez = 1;
const none = 2;
var type = none;
// working bezier
const p1 = P2();
const p2 = P2();
const cp1 = P2();
const cp2 = P2();
// rotated bezier
const rp1 = P2();
const rp2 = P2();
const rcp1 = P2();
const rcp2 = P2();
// translate and rotate bezier
function transformBez(pos,rot){
const ax = Math.cos(rot);
const ay = Math.sin(rot);
var x = p1.x - pos.x;
var y = p1.y - pos.y;
rp1.x = x * ax - y * ay;
rp1.y = x * ay + y * ax;
x = p2.x - pos.x;
y = p2.y - pos.y;
rp2.x = x * ax - y * ay;
rp2.y = x * ay + y * ax;
x = cp1.x - pos.x;
y = cp1.y - pos.y;
rcp1.x = x * ax - y * ay;
rcp1.y = x * ay + y * ax;
if(type === cubicBez){
x = cp2.x - pos.x;
y = cp2.y - pos.y;
rcp2.x = x * ax - y * ay;
rcp2.y = x * ay + y * ax;
}
}
function getPosition2(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
}
function getPosition3(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
}
const API = {
getPosOnQBez(pos,p1,cp1,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
},
getPosOnCBez(pos,p1,cp1,cp2,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
},
set bezQ(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(p2, points[2]);
type = quadraticBez;
},
set bezC(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(cp2, points[2]);
setP2As(p2, points[3]);
type = cubicBez;
},
isInside(center, testPoint, pointRadius){
drawLine(testPoint , center);
v1.x = (testPoint.x - center.x);
v1.y = (testPoint.y - center.y);
const pointDist = Math.sqrt(v1.x * v1.x + v1.y * v1.y)
const dir = -Math.atan2(v1.y,v1.x);
transformBez(center,dir);
if(type === cubicBez){
solveBezier3(rp1.y, rcp1.y, rcp2.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { u = u2 }
if (u < 0 || u > 1) { return }
getPosition3(u, v4);
}else{
solveBezier2(rp1.y, rcp1.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { return }
getPosition2(u, v4);
}
drawCircle(v4);
const dist = Math.sqrt((v4.x - center.x) ** 2 + (v4.y - center.y) ** 2);
const dist1 = Math.sqrt((v4.x - testPoint.x) ** 2 + (v4.y - testPoint.y) ** 2);
return dist1 < dist && dist > pointDist - pointRadius;
}
}
return API;
})();
const ctx = canvas.getContext("2d");
const m = {x : 0, y : 0};
document.addEventListener("mousemove",e=>{
var b = canvas.getBoundingClientRect();
m.x = e.pageX - b.left - scrollX - 2;
m.y = e.pageY - b.top - scrollY - 2;
});
function drawCircle(p,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.arc(p.x,p.y,r,0,Math.PI*2)
ctx.stroke();
}
function drawLine(p1,p2,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.lineTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
}
const w = 400;
const h = 400;
const diag = Math.sqrt(w * w + h * h);
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// random int and double
const randI = (min, max = min + (min = 0)) => (Math.random()*(max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const theBlobSet = [];
const theBlob = [];
function createCubicBlob(segs){
const step = Math.PI / segs;
for(var i = 0; i < Math.PI * 2; i += step){
const dist = rand(diag * (1/6), diag * (1/5));
const ang = i + rand(-step * 0.2,step * 0.2);
const p = P2(
w / 2 + Math.cos(ang) * dist,
h / 2 + Math.sin(ang) * dist
);
theBlobSet.push(p);
theBlob.push(P2(p));
}
theBlobSet[theBlobSet.length -1] = theBlobSet[0];
theBlob[theBlobSet.length -1] = theBlob[0];
}
createCubicBlob(8);
function animateTheBlob(time){
for(var i = 0; i < theBlobSet.length-1; i++){
const ang = Math.sin(time + i) * 6;
theBlob[i].x = theBlobSet[i].x + Math.cos(ang) * diag * 0.04;
theBlob[i].y = theBlobSet[i].y + Math.sin(ang) * diag * 0.04;
}
}
function drawTheBlob(){
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
var i = 0;
ctx.moveTo(theBlob[i].x,theBlob[i++].y);
while(i < theBlob.length){
ctx.bezierCurveTo(
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y
);
}
ctx.stroke();
}
var center = P2(w/2,h/2);
function testBlob(){
var i = 0;
while(i < theBlob.length-3){
bezHelper.bezC = [theBlob[i++], theBlob[i++], theBlob[i++], theBlob[i]];
if(bezHelper.isInside(center,m,6)){
return true;
}
}
return false;
}
// main update function
function update(timer){
ctx.clearRect(0,0,w,h);
animateTheBlob(timer/1000)
drawTheBlob();
if(testBlob()){
ctx.strokeStyle = "red";
}else{
ctx.strokeStyle = "black";
}
ctx.beginPath();
ctx.arc(m.x,m.y,5,0,Math.PI*2)
ctx.stroke();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas" width = "400" height = "400"></canvas>
I had created an animation of bubbles in which al the circle will expand which are 50px neer to the mouse.
so here is the trick. you can just simply change mouseX,mouseY with your microbe's X and Y coordinates and 50 to the radius of your microbe.
And when my bubbles get bigger, so there you can destroy you air bubbles.
here is the link to my Animation.
https://ankittorenzo.github.io/canvasAnimations/Elements/Bubbles/
here is the link to my GitHub Code.
https://github.com/AnkitTorenzo/canvasAnimations/blob/master/Elements/Bubbles/js/main.js
Let Me Know if you have any problem.
I'm trying to write rotated text (on various angles) on canvas but wish not to overlap the texts. Therefore after rotating the canvas and before filling the text I tried to test the text background using measureText().width and getImageData() to see that there is no text already there to get messed with new. I fail to find the text (coloured pixels) while the canvas is rotated. Here is a simplified version (using rectangle) to my problem. I wonder why no coloured pixels are found?
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="300" height="150" style="border:1px solid black;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var cWidth=300, cHeight= 150;
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// Rotate context around centre point
ctx.translate( cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
// Draw 100x50px rectangle at centre of rotated ctx
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Is my rotated rectangle really there?
// i.e. any coloured pixels at rotated cxt centre
imgData = ctx.getImageData(-50, -25, 100, 50);
// All rectangle pixels should be coloured
for (var j=0; j<imgData.data.length; j++){
if (imgData.data[j] > 0){
alert ("Coloured");
break;
};
};
// Why none is found?
</script>
</body>
</html>
The yellow rectangle should be at the same spot and angle as the tested image data area is. What went wrong? How I can test the colour of a rotated area? Being novice to Javascript I try to avoid libraries at this stage.
pekka
The problem is the translate function. You need to account for the displacement.
Try this:
imgData = ctx.getImageData(-50+cWidth/2,-25+cHeight/2,100,50);
The issue with your code is that getImageData() was targeting the wrong portion of the canvas. The x and y coordinates should have the same value of the translate() function. This is how your code should look like:
// Translate the rectangle, rotate it and fill it
ctx.translate(cWidth/2, cHeight/2);
ctx.rotate(20 * Math.PI / 180);
ctx.fillStyle = "yellow";
ctx.fillRect(-50, -25, 100, 50);
// Get the rectangle rotation
var imgData = ctx.getImageData(cWidth/2, cHeight/2, 100, 50);
And this is the JSfiddle with the complete code. I hope that my answer helps you!
General purpose answer.
This answer will work for any type of transformation and relies on the fact that the 2D context transform is mirrored in javascript. It gets the pixels one by one. Another way is to get the pixels as a block by transforming the corners of the rendered box and finding the bounding box that holds the transformed box. Use that box to get the image data. You will still have to transform each pixel address you want to check as the image data will also include extra pixels outside the rendered box. This may be quicker than the solution presented below if the searched for pixels are far and few between, The solution below is better if the odds of finding the pixel you want are high and you can break out of the iterations early.
You will need to use the transform API at the bottom of this answer
var mMatrix = new Transform(); // creates a default matrix
// define the bounds
const box = {x : -50, y : -25, w : 100, h : 50}; // our box
ctx.translate( cWidth/2, cHeight/2); // set the context transform
ctx.rotate(20 * Math.PI / 180);
// draw the stuff
ctx.fillStyle = "yellow";
ctx.fillRect(box.x, box.y, box.w, box.h);
// mirror the ctx transformations
mMatrix.translate(cWidth/2, cHeight/2).rotate(20 * Math.PI / 180);
var ix,iy,x,y;
var v = new Transform.Vec(); // create a working vec to save memory usage and anyoning GC hits
for(iy = 0; iy < box.h; iy ++){ // for each vertical pixel
for(ix = 0; iy < box.w; ix ++){ // for each horizontal
v.x = ix + 0.5 + box.x; // use pixel center
v.y = iy + 0.5 + box.y;
mMatrix.applyToVec(v); // transform into screen coords.
v.x = Math.floor(v.x); // get rid of grogens
v.y = Math.floor(v.y);
// now we have the pixel address corresponding to the box coordinate ix,iy
// get one pixel first check if it is on the canvas
if(v.x >= 0 && v.x < ctx.canvas.width && v.y >= 0 && v.y < ctx.canvas.height){
var pD = ctx.getImageData(v.x,v.y,1,1).data;
var red = pD[0];
var green = pD[1];
var blue = pD[2];
var alpha = pD[3];
// now you have the RGB values
... do what ever you want with that info
}
}
}
Transform API
This is the transform API required by the answer. It is a cut down of a API written by me and you can do with what you want (apart from evil things). See answer for usage. It is just the very basics, you can find more comprehensive Transform API on the net (but I don't think you will find one much faster)
See comment at the bottom for method details. Most functions are chainable.
Code snippet runs nothing.
var Transform = (function () {
var tx, ty, v1, v2, v3, mat;
// work vecs and transform provide pre assigned working memory
v1 = new Vec();
v2 = new Vec();
v3 = new Vec();
mat = new Transform();
ty = tx = 0;
function Transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
if (xAxisX === undefined) { // create identity matrix
this.xAxis = new Vec(); // Default vec is 1,0
this.yAxis = new Vec(0, 1);
this.origin = new Vec(0, 0);
} else if (yAxisY === undefined) { // if only 3 arguments assume that the 3 arguments are vecs
this.xAxis = new Vec(xAxisX.x, xAxisX.y); //
this.yAxis = new Vec(xAxisY.x, xAxisY.y);
this.origin = new Vec(yAxisY.x, xAxisY.y);
} else {
this.xAxis = new Vec(xAxisX, xAxisY); // Default vec is 1,0
this.yAxis = new Vec(yAxisX, yAxisY);
this.origin = new Vec(originX, originY);
}
};
function Vec(x, y) {
if (x === undefined || x === null) {
this.x = 1;
this.y = 0;
} else {
this.x = x;
this.y = y;
}
};
Vec.prototype = {
copy : function () {
return new Vec(this.x, this.y);
},
setAs : function (vec, y) { // set this to the value of vec, or if two arguments vec is x and y is y
if (y !== undefined) {
this.x = vec;
this.y = y;
return this;
}
this.x = vec.x;
this.y = vec.y;
return this;
}
}
Transform.prototype = {
xAxis : undefined,
yAxis : undefined,
origin : undefined,
Vec : Vec, // expose the Vec interface
copy : function () {
return new Transform(this.xAxis, this.yAxis, this.origin);
},
setAs : function (transform) {
this.xAxis.x = transform.xAxis.x;
this.xAxis.y = transform.xAxis.y;
this.yAxis.x = transform.yAxis.x;
this.yAxis.y = transform.yAxis.y;
this.origin.x = transform.origin.x;
this.origin.y = transform.origin.y;
return;
},
reset : function () { // resets this to the identity transform
this.xAxis.x = 1;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1;
this.origin.x = 0;
this.origin.y = 0;
return this;
},
apply : function (x, y) { // returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
return {
x : x * this.xAxis.x + y * this.yAxis.x + this.origin.x,
y : x * this.xAxis.y + y * this.yAxis.y + this.origin.y
};
},
applyToVec : function (vec) { // WARNING returns this not the vec.
tx = vec.x * this.xAxis.x + vec.y * this.yAxis.x + this.origin.x;
vec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + this.origin.y;
vec.x = tx;
return this;
},
invert : function () { // inverts the transform
// first check if just a scale translated identity matrix and invert that as it is quicker
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
this.xAxis.x = 1 / this.xAxis.x;
this.xAxis.y = 0;
this.yAxis.x = 0;
this.yAxis.y = 1 / this.yAxis.y;
this.origin.x = -this.xAxis.x * this.origin.x;
this.origin.y = -this.yAxis.y * this.origin.y;
return this;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
v1.x = this.yAxis.y / cross;
v1.y = -this.xAxis.y / cross;
v2.x = -this.yAxis.x / cross;
v2.y = this.xAxis.x / cross;
v3.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
v3.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
asInverse : function (transform) { // creates a new or uses supplied transform to return the inverse of this matrix
if (transform === undefined) {
transform = new Transform();
}
if (this.xAxis.y === 0 && this.yAxis.x === 0 && this.xAxis.x !== 0 && this.yAxis.y !== 0) {
transform.xAxis.x = 1 / this.xAxis.x;
transform.xAxis.y = 0;
transform.yAxis.x = 0;
transform.yAxis.y = 1 / this.yAxis.y;
transform.origin.x = -transform.xAxis.x * this.origin.x;
transform.origin.y = -transform.yAxis.y * this.origin.y;
return transform;
}
var cross = this.xAxis.x * this.yAxis.y - this.xAxis.y * this.yAxis.x;
transform.xAxis.x = this.yAxis.y / cross;
transform.xAxis.y = -this.xAxis.y / cross;
transform.yAxis.x = -this.yAxis.x / cross;
transform.yAxis.y = this.xAxis.x / cross;
transform.origin.x = (this.yAxis.x * this.origin.y - this.yAxis.y * this.origin.x) / cross;
transform.origin.y = - (this.xAxis.x * this.origin.y - this.xAxis.y * this.origin.x) / cross;
return transform;
},
multiply : function (transform) { // multiplies this with transform
var tt = transform;
var t = this;
v1.x = tt.xAxis.x * t.xAxis.x + tt.yAxis.x * t.xAxis.y;
v1.y = tt.xAxis.y * t.xAxis.x + tt.yAxis.y * t.xAxis.y;
v2.x = tt.xAxis.x * t.yAxis.x + tt.yAxis.x * t.yAxis.y;
v2.y = tt.xAxis.y * t.yAxis.x + tt.yAxis.y * t.yAxis.y;
v3.x = tt.xAxis.x * t.origin.x + tt.yAxis.x * t.origin.y + tt.origin.x;
v3.y = tt.xAxis.y * t.origin.x + tt.yAxis.y * t.origin.y + tt.origin.y;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
rotate : function (angle) { // Multiply matrix by rotation matrix at angle
var xdx = Math.cos(angle);
var xdy = Math.sin(angle);
v1.x = xdx * this.xAxis.x + (-xdy) * this.xAxis.y;
v1.y = xdy * this.xAxis.x + xdx * this.xAxis.y;
v2.x = xdx * this.yAxis.x + (-xdy) * this.yAxis.y;
v2.y = xdy * this.yAxis.x + xdx * this.yAxis.y;
v3.x = xdx * this.origin.x + (-xdy) * this.origin.y;
v3.y = xdy * this.origin.x + xdx * this.origin.y;
this.xAxis.x = v1.x;
this.xAxis.y = v1.y;
this.yAxis.x = v2.x;
this.yAxis.y = v2.y;
this.origin.x = v3.x;
this.origin.y = v3.y;
return this;
},
scale : function (scaleX, scaleY) { // Multiply the matrix by scaleX and scaleY
this.xAxis.x *= scaleX;
this.xAxis.y *= scaleY;
this.yAxis.x *= scaleX;
this.yAxis.y *= scaleY;
this.origin.x *= scaleX;
this.origin.y *= scaleY;
return this;
},
translate : function (x, y) { // Multiply the matrix by translate Matrix
this.origin.x += x;
this.origin.y += y;
return this;
},
setTransform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
this.xAxis.x = xAxisX;
this.xAxis.y = xAxisY;
this.yAxis.x = yAxisX;
this.yAxis.y = yAxisY;
this.origin.x = originX;
this.origin.y = originY;
return this;
},
transform : function (xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) {
var t = this;
v1.x = xAxisX * t.xAxis.x + yAxisX * t.xAxis.x;
v1.y = xAxisY * t.xAxis.x + yAxisY * t.xAxis.y;
v2.x = xAxisX * t.yAxis.x + yAxisX * t.yAxis.x;
v2.y = xAxisY * t.yAxis.x + yAxisY * t.yAxis.y;
v3.x = xAxisX * t.origin.x + yAxisX * t.origin.y + originX;
v3.y = xAxisY * t.origin.x + yAxisY * t.origin.y + originY;
t.xAxis.x = v1.x;
t.xAxis.y = v1.y;
t.yAxis.x = v2.x;
t.yAxis.y = v2.y;
t.origin.x = v3.x;
t.origin.y = v3.y;
return this;
},
contextTransform : function (ctx) {
ctx.transform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
contextSetTransform : function (ctx) {
ctx.Settransform(this.xAxis.x, this.xAxis.y, this.yAxis.x, this.yAxis.y, this.origin.x, this.origin.y);
return this;
},
setFromCurrentContext : function(ctx){
if(ctx && typeof ctx.currentTransform === "object"){
var mat = ctx.currentTransform;
this.xAxis.x = mat.a;
this.xAxis.y = mat.b;
this.yAxis.x = mat.c;
this.yAxis.y = mat.d;
this.origin.x = mat.e;
this.origin.y = mat.f;
}
return this;
}
}
if(typeof document.createElement("canvas").getContext("2d").currentTransform !== "object"){
Transform.prototype.setFromCurrentContext = undefined;
}
return Transform;
})();
/*
rotate(angle) // Multiply matrix by rotation matrix at angle. Same as ctx.rotate
scale(scaleX, scaleY) // Multiply the matrix by scaleX and scaleY. Same as ctx.scale
translate(x, y) // Multiply the matrix by translate Matrix. Same as ctx.translate
setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) //Replaces the current reansform with the new values. Same as ctx.setTransform
transform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) // multiplies this transform with the supplied transform. Same as ctx.transform
Transform.xAxis // Vec object defining the direction and scale of the x Axis. Values are in canvas pixel coordinates
Transform.yAxis // Vec object defining the direction and scale of the y Axis. Values are in canvas pixel coordinates
Transform.origin // Vec object defining canvas pixel coordinates of the origin
Transform.Vec // interface to a vec object with basic interface needed to support Transform
Transform.reset() // resets the transform to the identity matrix (the default matrix used by 2D context)
Transform.copy() // creates a new copy of this object
Transform.setAs(transform) // sets the content of this to the values of the argument transform
Transform.apply(x, y) { // Transforms the coords x,y by multiplying them with this. Returns an object {x : trabsformedX, y : trabsformedY} the returned object does not have the Vec prototype
Transform.applyToVec(vec) // transforms the point vec. WARNING returns this not the vec.
Transform.invert() // inverts the transform
Transform.asInverse(transform) // creates a new or uses supplied transform to return the inverse of this matrix
Transform.multiply(transform) // multiplies this with transform
Transform.contextTransform(ctx) // multiplies the supplied context (ctx) transform by this.
Transform.contextSetTransform(ctx) // set the supplied context (ctx) transform to this
Transform.setFromCurrentContext(ctx) // Only for supported browser. Sets this to the supplied context current transformation. May not be available if there is no browser support
There is also access to the very simple Vec object. To create a vec `new Transform.Vec(x,y)`
*/
I need to detect collision circle with any line. I have array with verticles of polygon (x, y) and draw this polygon in loop. For detection I use algorithm, which calculate triangle height. Then I check if this height < 0, then circle collided with line.
The picture, that describe this method:
But I have unexpected result. My circle collide with transparent line (what?). I can't explain how it happens.
Demo at jsfiddle: https://jsfiddle.net/f458rdz6/1/
Function, which check the collisions and response it:
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var polygonPoints = [
{
x: 240,
y: 130
},
{
x: 140,
y: 100
},
{
x: 180,
y: 250
},
{
x: 320,
y: 280
},
{
x: 400,
y: 50
}
];
var game = {
ball: new Ball()
};
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 100;
this.oldX = this.x - 1;
this.oldY = this.y + 1;
this.velocityX = 0;
this.velocityY = 0;
this.radius = 8;
};
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
};
Ball.prototype.update = function() {
var x = this.x;
var y = this.y;
this.velocityX = this.x - this.oldX;
this.velocityY = this.y - this.oldY;
this.x += this.velocityX;
this.y += this.velocityY;
this.oldX = x;
this.oldY = y;
};
Ball.prototype.collision = function() {
var p = polygonPoints;
for (var i = 0, n = p.length; i < n; i++) {
var start = i;
var end = (i + 1) % n;
var x0 = p[start].x;
var y0 = p[start].y;
var x1 = p[end].x;
var y1 = p[end].y;
// detection collision
var dx = x1 - x0;
var dy = y1 - y0;
var len = Math.sqrt(dx * dx + dy * dy);
var dist = (dx * (this.y - y0) - dy * (this.x - x0)) / len;
if (dist < this.radius) {
continue;
}
// calculate reflection, because collided
var wallAngle = Math.atan2(dy, dx);
var wallNormalX = Math.sin(wallAngle);
var wallNormalY = -Math.cos(wallAngle);
var d = 2 * (this.velocityX * wallNormalX + this.velocityY * wallNormalY);
this.x -= d * wallNormalX;
this.y -= d * wallNormalY;
}
};
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPolygon() {
ctx.beginPath();
ctx.strokeStyle = '#333';
ctx.moveTo(polygonPoints[0].x, polygonPoints[0].y);
for (var i = 1, n = polygonPoints.length; i < n; i++) {
ctx.lineTo(polygonPoints[i].x, polygonPoints[i].y);
}
ctx.lineTo(polygonPoints[0].x, polygonPoints[0].y);
ctx.stroke();
ctx.closePath();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawPolygon();
game.ball.draw();
game.ball.update();
game.ball.collision();
window.requestAnimationFrame(render);
}
render();
canvas {
border: 1px solid #333;
}
<canvas id="myCanvas" width="480" height="320"></canvas>
What the problem? Maybe I need use other method for detect collision? I tried to use this one, but if my circle has high speed this method not working.
Thank you.
Circle line segment intercept
UPDATE
This answer includes line line intercept, moving a line along its normal, distance point (circle) to line, and circle line intercept.
The circle is
var circle = {
radius : 500,
center : point(1000,1000),
}
The line segment is
var line = {
p1 : point(500,500),
p2 : point(2000,1000),
}
A point is
var point = {
x : 100,
y : 100,
}
Thus the function to find the intercept of a line segment width a circle
The function returns an array of up to two point on the line segment. If no points found returns an empty array.
function inteceptCircleLineSeg(circle, line){
var a, b, c, d, u1, u2, ret, retP1, retP2, v1, v2;
v1 = {};
v2 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = line.p1.x - circle.center.x;
v2.y = line.p1.y - circle.center.y;
b = (v1.x * v2.x + v1.y * v2.y);
c = 2 * (v1.x * v1.x + v1.y * v1.y);
b *= -2;
d = Math.sqrt(b * b - 2 * c * (v2.x * v2.x + v2.y * v2.y - circle.radius * circle.radius));
if(isNaN(d)){ // no intercept
return [];
}
u1 = (b - d) / c; // these represent the unit distance of point one and two on the line
u2 = (b + d) / c;
retP1 = {}; // return points
retP2 = {}
ret = []; // return array
if(u1 <= 1 && u1 >= 0){ // add point if on the line segment
retP1.x = line.p1.x + v1.x * u1;
retP1.y = line.p1.y + v1.y * u1;
ret[0] = retP1;
}
if(u2 <= 1 && u2 >= 0){ // second add point if on the line segment
retP2.x = line.p1.x + v1.x * u2;
retP2.y = line.p1.y + v1.y * u2;
ret[ret.length] = retP2;
}
return ret;
}
UPDATE
Line line intercept.
Returns a point if found else returns undefined.
function interceptLines(line,line1){
var v1, v2, c, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x; // vector of line
v1.y = line.p2.y - line.p1.y;
v2.x = line1.p2.x - line1.p1.x; //vector of line2
v2.y = line1.p2.y - line1.p1.y;
var c = v1.x * v2.y - v1.y * v2.x; // cross of the two vectors
if(c !== 0){
v3.x = line.p1.x - line1.p1.x;
v3.y = line.p1.y - line1.p1.y;
u = (v2.x * v3.y - v2.y * v3.x) / c; // unit distance of intercept point on this line
return {x : line.p1.x + v1.x * u, y : line.p1.y + v1.y * u};
}
return undefined;
}
Lift Line
Move line along its normal
function liftLine(line,dist){
var v1,l
v1 = {};
v1.x = line.p2.x - line.p1.x; // convert line to vector
v1.y = line.p2.y - line.p1.y;
l = Math.sqrt(v1.x * v1.x + v1.y * v1.y); // get length;
v1.x /= l; // Assuming you never pass zero length lines
v1.y /= l;
v1.x *= dist; // set the length
v1.y *= dist;
// move the line along its normal the required distance
line.p1.x -= v1.y;
line.p1.y += v1.x;
line.p2.x -= v1.y;
line.p2.y += v1.x;
return line; // if needed
}
Distance circle (or point) to a line segment
Returns the closest distance to the line segment. It is just the circle center that I am using. So you can replace circle with a point
function circleDistFromLineSeg(circle,line){
var v1, v2, v3, u;
v1 = {};
v2 = {};
v3 = {};
v1.x = line.p2.x - line.p1.x;
v1.y = line.p2.y - line.p1.y;
v2.x = circle.center.x - line.p1.x;
v2.y = circle.center.y - line.p1.y;
u = (v2.x * v1.x + v2.y * v1.y) / (v1.y * v1.y + v1.x * v1.x); // unit dist of point on line
if(u >= 0 && u <= 1){
v3.x = (v1.x * u + line.p1.x) - circle.center.x;
v3.y = (v1.y * u + line.p1.y) - circle.center.y;
v3.x *= v3.x;
v3.y *= v3.y;
return Math.sqrt(v3.y + v3.x); // return distance from line
}
// get distance from end points
v3.x = circle.center.x - line.p2.x;
v3.y = circle.center.y - line.p2.y;
v3.x *= v3.x; // square vectors
v3.y *= v3.y;
v2.x *= v2.x;
v2.y *= v2.y;
return Math.min(Math.sqrt(v2.y + v2.x), Math.sqrt(v3.y + v3.x)); // return smaller of two distances as the result
}
I like to get some points from bezier curve.I found
Find all the points of a cubic bezier curve in javascript
Position is easy. First, compute the blending functions. These control the "effect" of your control points on the curve.
B0_t = (1-t)^3
B1_t = 3 * t * (1-t)^2
B2_t = 3 * t^2 * (1-t)
B3_t = t^3
Notice how B0_t is1 when t is 0 (and everything else is zero). Also, B3_t is 1 when t is 1 (and everything else is zero). So the curve starts at (ax, ay), and ends at (dx, dy).
Any intermediate point (px_t, py_t) will be given by the following (vary t from 0 to 1, in small increments inside a loop):
px_t = (B0_t * ax) + (B1_t * bx) + (B2_t * cx) + (B3_t * dx)
py_t = (B0_t * ay) + (B1_t * by) + (B2_t * cy) + (B3_t * dy)
My code
var ax = 100, ay = 250;
var bx = 150, by = 100;
var cx = 350, cy = 100;
var dx = 400, dy = 250;
ctx.lineWidth = 1;
ctx.strokeStyle = "#333";
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.bezierCurveTo(bx, by, cx, cy, dx, dy);
ctx.stroke();
var t = 0
var B0_t = (1 - t) ^ 3
var B1_t = 3 * t * (1 - t) ^ 2
var B2_t = 3 * t ^ 2 * (1 - t)
var B3_t = t ^ 3
// override manually *Notice* above
//This is work first and laste point in curve
// B0_t = 1; B1_t = 0; B2_t = 0; B3_t = 0; t = 0;
// B0_t = 0; B1_t = 0; B2_t = 0; B3_t = 1; t = 1;
var px_t = (B0_t * ax) + (B1_t * bx) + (B2_t * cx) + (B3_t * dx)
var py_t = (B0_t * ay) + (B1_t * by) + (B2_t * cy) + (B3_t * dy)
// doesnt work
var t = 0
var B0_t = (1 - t) ^ 3 //*Notice* above should be 1
//Debug (1 - t) ^ 3 = 2 ??
var B1_t = 3 * t * (1 - t) ^ 2 //*Notice* above should be 0
//Debug 3 * t * (1 - t) ^ 2 = 2 ??
var B2_t = 3 * t ^ 2 * (1 - t)//*Notice* above should be 0
//Debug 3 * t ^ 2 * (1 - t) =2 ??
var B3_t = t ^ 3//*Notice* above should be 0 but its 2
//Debug t ^ 3 = 3 ??
var px_t = (B0_t * ax) + (B1_t * bx) + (B2_t * cx) + (B3_t * dx)
var py_t = (B0_t * ay) + (B1_t * by) + (B2_t * cy) + (B3_t * dy)
Appreciate any help thanks
How to find the pixels along a Bezier Curve
This set of functions will find an [x,y] point at interval T along cubic Bezier curve where 0<=T<=1.
In simple terms: It plots points along a cubic Bezier curve from start to end.
// Given the 4 control points on a Bezier curve
// get x,y at interval T along the curve (0<=T<=1)
// The curve starts when T==0 and ends when T==1
function getCubicBezierXYatPercent(startPt, controlPt1, controlPt2, endPt, percent) {
var x = CubicN(percent, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(percent, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({
x: x,
y: y
});
}
// cubic helper formula
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;
}
You can fetch the points along the curve by sending the plotting function a large number of T values between 0.00 & 1.00.
Example code and a demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var cBez1=[{x:250,y: 120},{x:290,y:-40},{x:300,y:200},{x:400,y:150}]
drawBez(cBez1);
var cPoints=findCBezPoints(cBez1);
drawPlots(cPoints);
function findCBezPoints(b){
var startPt=b[0];
var controlPt1=b[1];
var controlPt2=b[2];
var endPt=b[3];
var pts=[b[0]];
var lastPt=b[0];
var tests=5000;
for(var t=0;t<=tests;t++){
// calc another point along the curve
var pt=getCubicBezierXYatT(b[0],b[1],b[2],b[3], t/tests);
// add the pt if it's not already in the pts[] array
var dx=pt.x-lastPt.x;
var dy=pt.y-lastPt.y;
var d=Math.sqrt(dx*dx+dy*dy);
var dInt=parseInt(d);
if(dInt>0 || t==tests){
lastPt=pt;
pts.push(pt);
}
}
return(pts);
}
// Given the 4 control points on a Bezier curve
// Get x,y at interval T along the curve (0<=T<=1)
// The curve starts when T==0 and ends when T==1
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
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;
}
function drawPlots(pts){
ctx.fillStyle='red';
// don't draw the last dot b/ its radius will display past the curve
for(var i=0;i<pts.length-1;i++){
ctx.beginPath();
ctx.arc(pts[i].x,pts[i].y,1,0,Math.PI*2);
ctx.fill();
}
}
function drawBez(b){
ctx.lineWidth=7;
ctx.beginPath();
ctx.moveTo(b[0].x,b[0].y);
ctx.bezierCurveTo(b[1].x,b[1].y, b[2].x,b[2].y, b[3].x,b[3].y);
ctx.stroke();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<h4>Black line is context.bezierCurveTo<br>Red "line" is really dot-points plotted along the curve</h4>
<canvas id="canvas" width=500 height=300></canvas>
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();
}