https://en.wikipedia.org/wiki/Bézier_curve
贝塞尔曲线公式推导原理
https://www.cnblogs.com/equal/p/6414263.html
贝塞尔曲线原理(简单阐述)
https://www.cnblogs.com/hnfxs/p/3148483.html
n 阶贝塞尔曲线计算公式实现
https://blog.youkuaiyun.com/aimeimeits/article/details/72809382
[转]匀速贝塞尔曲线运动的实现
http://as3.iteye.com/blog/865587
贝塞尔曲线运动n阶追踪方程的数学原理及其匀速化方法和应用
https://blog.youkuaiyun.com/iSunwish/article/details/78935257
https://www.zhihu.com/question/27715729 如何得到贝塞尔曲线的曲线长度和 t 的近似关系?
https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
Approximation of a cubic bezier curve by circular arcs and vice versa
https://www.researchgate.net/publication/265893293_Approximation_of_a_cubic_bezier_curve_by_circular_arcs_and_vice_versa
Approximation of a planar cubic Bezier spiral by circular arcs
https://www.sciencedirect.com/science/article/pii/S0377042796000568
<html>
<head>
<meta charset="UTF-8">
<title>二阶贝塞尔曲线</title>
<style type="text/css">
.box {
background-color: black;
}
</style>
</head>
<body>
<canvas id="canvas" class="box">
canvas not supported, please use html5 browser.
</canvas>
</body>
<script>
window.onload = function() {
var canvas = document.querySelector(".box");
canvas.width = 800;
canvas.height = 600;
var origin = {x: 400, y: 550};
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.strokeStyle = "green";
var p0 = {x: 140, y: 370};
var p1 = {x: 340, y: 120};
var p2 = {x: 690, y: 520};
var p3 = {x: 230, y: 440};
var t = 0, step = 0.005;
//二阶贝塞尔曲线
// function enterFrame() {
// t += step;
// var p = {};
// p.x = (1-t)**2 * p0.x + 2 * (1-t) * t * p1.x + t**2 * p2.x;
// p.y = (1-t)**2 * p0.y + 2 * (1-t) * t * p1.y + t**2 * p2.y;
// ctx.lineTo(p.x, p.y);
// ctx.stroke();
// if (t >= 1) {
// return;
// }
// requestAnimationFrame(enterFrame);
// }
// enterFrame();
var radius = 7;
ctx.fillStyle = "#EE9611"; //#2BD56F
ctx.arc(p0.x, p0.y, radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(p1.x, p1.y, radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(p2.x, p2.y, radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(p3.x, p3.y, radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = "#8855AA";
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "green";
ctx.moveTo(p0.x, p0.y);
//三阶贝塞尔曲线
function enterFrame() {
t += step;
var p = {};
p.x = (1-t)**3 * p0.x + 3 * (1-t)**2 * t * p1.x + 3*(1-t)*t**2 * p2.x + t**3 * p3.x;
p.y = (1-t)**3 * p0.y + 3 * (1-t)**2 * t * p1.y + 3*(1-t)*t**2 * p2.y + t**3 * p3.y;
ctx.lineTo(p.x, p.y);
ctx.stroke();
if (t >= 1) {
return;
}
requestAnimationFrame(enterFrame);
}
enterFrame();
}
function getX(origin, x) {
return origin.x + x;
}
function getY(origin, y) {
return origin.y - y;
}
</script>
</html>
<html>
<head>
<meta charset="UTF-8">
<title>贝塞尔曲线</title>
<style type="text/css">
.box {
/*float: left;*/
background-color: black;
}
.ctrlBox {
float: left;
/*border: 1px solid #A0F;*/
width: 400;
/*height: 700;*/
}
</style>
</head>
<body>
<canvas id="canvas" class="box">
canvas not supported, please use html5 browser.
</canvas>
<div class="ctrlBox">
<p>p0:<span id="p0"></span><p>
<p>p1:<span id="p1"></span><p>
<p>p2:<span id="p2"></span><p>
<p>p3:<span id="p3"></span><p>
<p><span><input type="checkbox" name="cb" onclick="hideCbClick(event)" id="hideCb" />隐藏控制点</span></p>
</div>
</body>
<script>
window.onload = function() {
var canvas = document.querySelector(".box");
canvas.width = 800;
canvas.height = 600;
var ctx = canvas.getContext('2d');
var t = 0, step = 0.01;
//二阶贝塞尔曲线
// function enterFrame() {
// t += step;
// var p = {};
// p.x = (1-t)**2 * p0.x + 2 * (1-t) * t * p1.x + t**2 * p2.x;
// p.y = (1-t)**2 * p0.y + 2 * (1-t) * t * p1.y + t**2 * p2.y;
// ctx.lineTo(p.x, p.y);
// ctx.stroke();
// if (t >= 1) {
// return;
// }
// requestAnimationFrame(enterFrame);
// }
// enterFrame();
var p0 = DraggableCircle({x: 140, y: 370, dom: document.querySelector("#p0")});
var p1 = DraggableCircle({x: 340, y: 120, dom: document.querySelector("#p1")});
var p2 = DraggableCircle({x: 690, y: 520, dom: document.querySelector("#p2")});
var p3 = DraggableCircle({x: 230, y: 440, dom: document.querySelector("#p3")});
var draggables = [p0, p1, p2, p3];
var selectedP;
canvas.onmousedown = function(e) {
var evt = e || event;//获取事件对象
var x = evt.offsetX;
var y = evt.offsetY;
// console.log("clicked: " + x + ", " + y);
for (var i = 0; i < draggables.length; i++) {
var p = draggables[i];
if (p.hitTest(x, y)) {
selectedP = p;
p.selected = true;
break;
}
}
};
canvas.onmousemove = function(e) {
if (selectedP) {
var evt = e || event;//获取事件对象
var x = evt.offsetX;
var y = evt.offsetY;
selectedP.move(x, y);
}
};
canvas.onmouseup = function(e) {
if (selectedP) {
selectedP.selected = false;
showCoordinates();
selectedP = null;
}
};
function showCoordinates() {
draggables.forEach(p => p.dom.innerText = "x: " + p.x + ", y: " + p.y);
}
showCoordinates();
var hideCb = document.querySelector("#hideCb");
function enterFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!hideCb.checked) {
draggables.forEach(p => p.draw(ctx));
for (var i = 0; i < draggables.length; i++) {
var p = draggables[i];
if (i === 0) {
ctx.beginPath();
ctx.strokeStyle = "#8855AA";
ctx.moveTo(p.x, p.y);
} else {
ctx.lineTo(p.x, p.y);
}
}
ctx.stroke();
}
var stepLen = 0.01;
var tt = 0;
ctx.beginPath();
ctx.strokeStyle = "green";
ctx.moveTo(draggables[0].x, draggables[0].y);
while (tt <= 1) {
var p = {};
p.x = (1-tt)**3 * p0.x + 3 * (1-tt)**2 * tt * p1.x + 3*(1-tt)*tt**2 * p2.x + tt**3 * p3.x;
p.y = (1-tt)**3 * p0.y + 3 * (1-tt)**2 * tt * p1.y + 3*(1-tt)*tt**2 * p2.y + tt**3 * p3.y;
ctx.lineTo(p.x, p.y);
tt += stepLen;
}
ctx.stroke();
requestAnimationFrame(enterFrame);
}
enterFrame();
}
var DraggableCircle = function(point) {
var that = {
dom: point.dom,
x: point.x,
y: point.y,
radius: 7,
selected: false,
hitTest: function(x2, y2) {
var distance = (that.x-x2)**2 + (that.y-y2)**2;
return distance <= that.radius**2;
},
move: function(x3, y3) {
that.x = x3;
that.y = y3;
},
draw: function(ctx) {
ctx.beginPath();
ctx.fillStyle = "#EE9611";
if (that.selected) {
ctx.fillStyle = "#2BD56F";
}
ctx.arc(that.x, that.y, that.radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
}
};
return that;
};
function hideCbClick(event) {
console.log("hide checkbox checked: " + event.target.checked);
}
</script>
</html>
用微积分对二阶贝塞尔曲线,进行定距等分
相应的 javascript 代码如下:
<html>
<head>
<meta charset="UTF-8">
<title>贝塞尔曲线</title>
<style type="text/css">
.box {
/*float: left;*/
background-color: black;
}
.ctrlBox {
float: left;
/*border: 1px solid #A0F;*/
width: 400;
/*height: 700;*/
}
</style>
</head>
<body>
<canvas id="canvas" class="box">
canvas not supported, please use html5 browser.
</canvas>
<div class="ctrlBox">
<p>p0 = {<span id="p0"></span>}<p>
<p>p1 = {<span id="p1"></span>}<p>
<p>p2 = {<span id="p2"></span>}<p>
<p>p3 = {<span id="p3"></span>}<p>
<p><span><input type="checkbox" name="cb" onclick="hideCbClick(event)" id="hideCb" />隐藏控制点</span></p>
</div>
</body>
<script>
var moreAccurate = true; //false表示不考虑二阶导数的保号性,即忽略掉拐点
window.onload = function() {
var canvas = document.querySelector(".box");
canvas.width = 800;
canvas.height = 600;
var ctx = canvas.getContext('2d');
var t = 0, step = 0.01;
var p0 = DraggableCircle({x: 140, y: 370, dom: document.querySelector("#p0")});
var p1 = DraggableCircle({x: 340, y: 120, dom: document.querySelector("#p1")});
var p2 = DraggableCircle({x: 690, y: 520, dom: document.querySelector("#p2")});
var p3 = DraggableCircle({x: 230, y: 440, dom: document.querySelector("#p3")});
var draggables = [p0, p1, p2];
var selectedP, curveObj;
var dividePoints = [];
var dividePoints2 = [];
var dividePoints3 = [];
function divideCurve(curveObj, divideObj) {
var subLen = divideObj.len;
var total = curveObj.len;
if (divideObj.partNum) {
subLen = total / divideObj.partNum;
}
var curLen = subLen;
var arr = [];
while (curLen < total) {
arr.push(divideCurveBy(curveObj, curLen));
curLen += subLen;
}
return arr;
}
//从起始点算起的长度 subLen
function divideCurveBy(curveObj, subLen) {
var f_x0, x0, f_x_1; //f_x_1 表示 f(x)的一阶导数
var considerSpinodal = moreAccurate && curveObj.part1 > 0;
var subLen2 = subLen;
if (curveObj.a > 0 ) {
x0 = curveObj.b;
// f_x0 = subLen;
f_x0 = curveObj.len - subLen;
considerSpinodal = considerSpinodal && curveObj.part1 > subLen;
if (considerSpinodal) {
subLen2 -= curveObj.len - subLen - curveObj.part2;//curveObj.part1 - subLen;
f_x0 = subLen2;
x0 = curveObj.t0_2;
}
} else {
x0 = curveObj.a;
f_x0 = -subLen;
considerSpinodal = considerSpinodal && curveObj.part1 < subLen;
if (considerSpinodal) {
subLen2 = subLen - curveObj.part1;
f_x0 = -subLen2;
x0 = curveObj.t0_2;
}
}
f_x_1 = Math.sqrt(x0 * x0 + curveObj.C) * 2/curveObj.A;
var accuracy = 0.0001;
var x = -1;
while (true) {
x = x0 - f_x0 / f_x_1;
// console.log("divideCurveBy, x=" + x);
if (Math.abs(x - x0) <= accuracy) {
break;
}
x0 = x;
if (curveObj.a > 0 ) {
// f_x0 = curveObj.foo(x) - curveObj.yb + subLen2;
if (considerSpinodal) {
f_x0 = curveObj.foo(x) - curveObj.y0 + subLen2;
} else {
f_x0 = curveObj.foo(x) - curveObj.yb + curveObj.len - subLen;
}
} else {
if (considerSpinodal) {
f_x0 = curveObj.foo(x) - curveObj.y0 - subLen2;
} else {
f_x0 = curveObj.foo(x) - curveObj.ya - subLen;
}
}
f_x_1 = Math.sqrt(x * x + curveObj.C) * 2/curveObj.A;
}
var t = (x - curveObj.B/(2*curveObj.A)) / curveObj.A;
// console.log("divideCurveBy, t=" + t);
var p = {};
p.x = (1-t)**2 * p0.x + 2 * (1-t) * t * p1.x + t**2 * p2.x;
p.y = (1-t)**2 * p0.y + 2 * (1-t) * t * p1.y + t**2 * p2.y;
return p;
}
function getCurveLen() {
var a_x = p0.x - 2*p1.x + p2.x;
var b_x = p1.x - p0.x;
var a_y = p0.y - 2*p1.y + p2.y;
var b_y = p1.y - p0.y;
var A = Math.sqrt(a_x**2 + a_y**2);
var B = 2*(a_x*b_x + a_y*b_y);
var C = b_x**2 + b_y**2 - B*B/(4*A*A);
var xb = (2*A*A + B)/(2*A), xa = B/(2*A),len=0;
var T0 = -B/(2*A*A); //原函数的二阶导数为零,原函数在x = T0处的点,是拐点
var part1 = -1, yb, ya; //二阶导数小于零的长度
if (C >= 0) {
yb = curve2_1(C, xb) / A;
ya = curve2_1(C, xa) / A;
len = yb - ya;
if (isBetween(xa, xb, T0)) {
part1 = curve2_1(C, T0) / A - ya;
}
} else {
yb = curve2_2(C, xb) / A;
ya = curve2_2(C, xa) / A;
len = yb - ya;
if (isBetween(xa, xb, T0)) {
part1 = curve2_2(C, T0) / A - ya;
}
}
console.log("len = " + len + ", xb="+xb+", xa="+xa+ ", yb="+yb+", ya="+ya+", C="+C+", B="+B+", A="+A+", T0="+T0+", part1="+part1);
//T == 0
var obj = {len:len, t0_2:T0, part1: part1, a: xa, b: xb, ya:ya, yb:yb, C:C, A:A, B:B,
foo:function(x) {
if (obj.C >= 0) {
return curve2_1(obj.C, x) / obj.A;
} else {
return curve2_2(obj.C, x) / obj.A;
}
}};
obj.y0 = obj.foo(T0);
obj.part2 = obj.len - obj.part1;
return obj;
}
function isBetween(min, max, val) {
return min < val && val < max;
}
//以直带曲,第二种朴素的,适用范围更广,但效率较低的定距等分方法
function divideCurve2(subLen) {
var step = 0.001, t = 0;
var p = {};
var sum = 0, dd = 0;
var start = {x: p0.x, y: p0.y};
var divides = [];
while (t <= 1) {
p.x = (1-t)**2 * p0.x + 2 * (1-t) * t * p1.x + t**2 * p2.x;
p.y = (1-t)**2 * p0.y + 2 * (1-t) * t * p1.y + t**2 * p2.y;
var d = Math.sqrt((p.x - start.x)**2 + (p.y - start.y)**2);
sum += d;
dd += d;
if (dd >= subLen) {
dd = 0;
divides.push({x: p.x, y: p.y});
}
t += step;
start.x = p.x;
start.y = p.y;
}
console.log("divideCurveBy2, total len: " + sum);
return divides;
}
curveObj = getCurveLen();
dividePoints = divideCurve(curveObj, {partNum: 10});
dividePoints2 = divideCurve2(curveObj.len/10);
// dividePoints = [divideCurveBy(curveObj, curveObj.len*0.3)];
function curve2_1(C, x) {
return x * Math.sqrt(x*x + C) + C * Math.log(x + Math.sqrt(x*x + C));
}
function curve2_2(C, x) {
var c = -C;
return x * Math.sqrt(x*x - c) - c * Math.log(Math.abs(x + Math.sqrt(x*x - c) ) );
}
canvas.onmousedown = function(e) {
var evt = e || event;//获取事件对象
var x = evt.offsetX;
var y = evt.offsetY;
// console.log("clicked: " + x + ", " + y);
for (var i = 0; i < draggables.length; i++) {
var p = draggables[i];
if (p.hitTest(x, y)) {
selectedP = p;
p.selected = true;
break;
}
}
};
canvas.onmousemove = function(e) {
if (selectedP) {
var evt = e || event;//获取事件对象
var x = evt.offsetX;
var y = evt.offsetY;
selectedP.move(x, y);
}
};
canvas.onmouseup = function(e) {
if (selectedP) {
selectedP.selected = false;
showCoordinates();
curveObj = getCurveLen();
dividePoints = divideCurve(curveObj, {
partNum: 10
// len: 30
});
dividePoints2 = divideCurve2(curveObj.len/10);
selectedP = null;
}
};
function showCoordinates() {
draggables.forEach(p => p.dom.innerText = "x: " + p.x + ", y: " + p.y);
}
showCoordinates();
var hideCb = document.querySelector("#hideCb");
function enterFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!hideCb.checked) {
draggables.forEach(p => p.draw(ctx));
for (var i = 0; i < draggables.length; i++) {
var p = draggables[i];
if (i === 0) {
ctx.beginPath();
ctx.strokeStyle = "#8855AA";
ctx.moveTo(p.x, p.y);
} else {
ctx.lineTo(p.x, p.y);
}
}
ctx.stroke();
}
var stepLen = 0.01;
var tt = 0;
ctx.beginPath();
ctx.strokeStyle = "green";
ctx.moveTo(draggables[0].x, draggables[0].y);
while (tt <= 1) {
var p = {};
// p.x = (1-tt)**3 * p0.x + 3 * (1-tt)**2 * tt * p1.x + 3*(1-tt)*tt**2 * p2.x + tt**3 * p3.x;
// p.y = (1-tt)**3 * p0.y + 3 * (1-tt)**2 * tt * p1.y + 3*(1-tt)*tt**2 * p2.y + tt**3 * p3.y;
p.x = (1-tt)**2 * p0.x + 2 * (1-tt) * tt * p1.x + tt**2 * p2.x;
p.y = (1-tt)**2 * p0.y + 2 * (1-tt) * tt * p1.y + tt**2 * p2.y;
ctx.lineTo(p.x, p.y);
tt += stepLen;
}
ctx.stroke();
var pp = {};
tt = curveObj.t0_2;
if ( tt >= 0 && tt <= 1) {
pp.x = (1-tt)**2 * p0.x + 2 * (1-tt) * tt * p1.x + tt**2 * p2.x;
pp.y = (1-tt)**2 * p0.y + 2 * (1-tt) * tt * p1.y + tt**2 * p2.y;
ctx.beginPath();
ctx.fillStyle = "#4DB39E";
ctx.arc(pp.x, pp.y, 4, Math.PI*2, false);
ctx.fill();
ctx.closePath();
}
dividePoints.forEach(dp => {
ctx.beginPath();
ctx.fillStyle = "#EE11C2";
ctx.arc(dp.x, dp.y, 4, Math.PI*2, false);
ctx.fill();
ctx.closePath();
});
dividePoints2.forEach(dp => {
ctx.beginPath();
ctx.fillStyle = "#694ED3";
ctx.arc(dp.x, dp.y, 4, Math.PI*2, false);
ctx.fill();
ctx.closePath();
});
requestAnimationFrame(enterFrame);
}
enterFrame();
}
var DraggableCircle = function(point) {
var that = {
dom: point.dom,
x: point.x,
y: point.y,
radius: 7,
selected: false,
hitTest: function(x2, y2) {
var distance = (that.x-x2)**2 + (that.y-y2)**2;
return distance <= that.radius**2;
},
move: function(x3, y3) {
that.x = x3;
that.y = y3;
},
draw: function(ctx) {
ctx.beginPath();
ctx.fillStyle = "#EE9611";
if (that.selected) {
ctx.fillStyle = "#2BD56F";
}
ctx.arc(that.x, that.y, that.radius, Math.PI*2, false);
ctx.fill();
ctx.closePath();
}
};
return that;
};
function hideCbClick(event) {
console.log("hide checkbox checked: " + event.target.checked);
}
</script>
</html>
moreAccurate = false;
divideCurveBy, x=10.07993902100614
curve.html:94 divideCurveBy, x=48.34371660200954
curve.html:94 divideCurveBy, x=48.084959281910635
curve.html:94 divideCurveBy, x=48.08493456734062
moreAccurate = true
divideCurveBy, x=48.37720923128524
curve.html:95 divideCurveBy, x=48.08496610650087
curve.html:95 divideCurveBy, x=48.084934567340724