通过很少的控制点,去生成复杂的平滑曲线,也就是贝塞尔曲线。
学习CSS3中的 transition 及 animation 时遇到了
*-timing-function
属性,一直不甚了解,今天专门看了下,大概了解了其原理,只是归纳后的数学公式还不能理解(遗忘高中及大学里的相关知识)。
1 资源放送
2 历史-关键词
爸爸:法国工程师皮埃尔·贝济埃(Pierre Bézier)
正式生日:1962年
应用:图形设计、路径规划
原理:等比,递归
关联:伯恩斯坦多项式、德卡斯特里奥(de Casteljau)算法
3 推导
3.1 二阶贝塞尔
在平面内任选 3 个不共线的点,依次用线段连接。在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。
比值t,(0 <= t <= 1)
t = AD/AB
t = BE/BC
DE上存在一点F,使得
DF/DE = t
将F点的轨迹描绘下来,即为下图红线
公式推导
D = t*A + (1-t)*B
E = t*B + (1-t)*C
F = t*D + (1-t)*E
= t*(t*A + (1-t)*B) + (1-t)*(t*B + (1-t)*C)
= t^2*A + 2*t*(1-t)*B + (1-t)^2*C
3.2 三阶贝塞尔
二阶的贝塞尔通过在控制点之间再采点的方式实现降阶, 每一次选点都是一次的降阶。
四个点对应是三次的贝塞尔曲线. 分别在 AB BC CD 之间采EFG点, EFG三个点对应着二阶贝塞尔, 在EF FG之间采集HI点来降阶为一阶贝塞尔曲线。
高阶的贝塞尔可以通过不停的递归直到一阶。
找到所有的J点,依次将J点连接起来
公式推导
E = t*A + (1-t)*B
F = t*B + (1-t)*C
G = t*C + (1-t)*D
H = t*E + (1-t)*F
I = t*F + (1-t)*G
J = t*H + (1-t)*I
= t*(t*E + (1-t)*F) + (1-t)*(t*F + (1-t)*G)
= t^2*(t*A + (1-t)*B) + t*(1-t)*(t*B + (1-t)*C) + t*(1-t)*(t*B + (1-t)*C)
+ (1-t)^2*(t*C + (1-t)*D)
= t^3*A + 3*t^2*(1-t)*B + 3*t*(1-t)^2*C + (1-t)^3*D
3.3 三阶、四阶gif
4 贝塞尔曲线的性质
-
各项系数之和为1
系数是二项式的展开(t+(1-t))^n = (1)^n非负性
-
对称性
第i项系数和倒数第i项系数相同
-
递归性
-
凸包性质
贝塞尔曲线始终会在包含了所有控制点的最小凸多边形中, 不是按照控制点的顺序围成的最小多边形
-
端点性质
第一个控制点和最后一个控制点,恰好是曲线的起始点和终点
-
一阶导数性质
这一点的性质可以用在贝塞尔曲线的拼接,只要保证三点一线中的中间点是两段贝塞尔曲线的连接点,就可以保证两端贝塞尔曲线的导数连续连续。
Bezier Curves Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BezierCurves</title>
<script src="https://www.w3cplus.com/sites/default/files/blogs/2017/1703/wind-all-0.7.3.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
}
body {
display: flex;
justify-content: center;
}
.wrapper {
text-align: center;
}
small {
font-size: 16px;
margin-left: 20px;
}
p * {
vertical-align: middle;
margin-left: 5px;
}
.wrapper div {
position: relative;
box-shadow: 0 0 0 1px hsl(0, 0%, 80%);
background: hsl(0, 0%, 95%);
width: 800px;
height: 600px;
}
canvas {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
</head>
<body>
<div class="wrapper">
<h1>
Bézier Curve<small>(click the canvas to set the control points)</small>
</h1>
<p>
Number of control points:
<input type="range" min="2" max="30" value="3" disabled />
<span></span>
</p>
<div id="canvasBox">
<canvas
id="canvasOne"
width="800"
height="600"
style="z-index: 1"
></canvas>
<canvas
id="canvasTwo"
width="800"
height="600"
style="z-index: 2"
></canvas>
<canvas
id="canvasThree"
width="800"
height="600"
style="z-index: 3"
></canvas>
</div>
</div>
<script>
window.addEventListener("load", eventWindowLoaded, false);
var Debugger = function () {};
Debugger.log = function (message) {
try {
console.log(message);
} catch (exception) {
return;
}
};
function eventWindowLoaded() {
canvasApp();
}
function canvasApp() {
// var theCanvas = document.getElementById('canvasOne');
// var context = theCanvas.getContext('2d');
Debugger.log("Drawing Canvas");
var input = document.getElementsByTagName("input")[0],
span = document.getElementsByTagName("span")[0],
div = document.getElementById("canvasBox"),
ctx1 = document.getElementById("canvasOne").getContext("2d"),
ctx2 = document.getElementById("canvasTwo").getContext("2d"),
ctx3 = document.getElementById("canvasThree").getContext("2d"),
points = [],
colors = [],
running = true,
steps = 200,
interval = 16,
num;
ctx1.font = "16px consolas";
ctx1.fillStyle = ctx1.strokeStyle = "hsl(0, 0%, 50%)";
ctx1.lineWidth = ctx2.lineWidth = 2;
ctx3.strokeStyle = "hsl(0, 90%, 70%)";
function count() {
num = parseInt(input.value);
span.innerHTML = num;
}
function toggle() {
input.disabled = running = !running;
}
function draw(per, arr, color) {
var ary = [],
node;
ctx2.strokeStyle = ctx2.fillStyle = colors[color];
node = arr.reduce(function (previous, current, index) {
var p = {
x: arr[index - 1].x + (arr[index].x - arr[index - 1].x) * per,
y: arr[index - 1].y + (arr[index].y - arr[index - 1].y) * per,
};
if (index > 1) {
ctx2.beginPath();
ctx2.moveTo(previous.x, previous.y);
ctx2.lineTo(p.x, p.y);
ctx2.stroke();
ctx2.closePath();
}
ctx2.beginPath();
ctx2.arc(p.x, p.y, 3, 0, Math.PI * 2, true);
ctx2.fill();
ctx2.closePath();
ary.push(p);
return p;
});
if (ary.length > 1) {
draw(per, ary, color + 1);
} else {
ctx3.lineTo(node.x, node.y);
ctx3.stroke();
}
}
var drawAsync = eval(
Wind.compile("async", function () {
toggle();
ctx3.beginPath();
ctx3.moveTo(points[0].x, points[0].y);
for (var i = 0; i <= steps; i++) {
draw(i / steps, points, 0);
$await(Wind.Async.sleep(interval));
ctx2.clearRect(0, 0, 800, 600);
}
ctx3.closePath();
points = [];
toggle();
})
);
div.addEventListener(
"click",
function (e) {
if (running) {
return;
}
var point = {
x: e.pageX - div.offsetLeft,
y: e.pageY - div.offsetTop,
};
if (points.length == 0) {
ctx1.clearRect(0, 0, 800, 600);
ctx2.clearRect(0, 0, 800, 600);
ctx3.clearRect(0, 0, 800, 600);
} else {
ctx1.beginPath();
ctx1.moveTo(point.x, point.y);
ctx1.lineTo(
points[points.length - 1].x,
points[points.length - 1].y
);
ctx1.stroke();
ctx1.closePath();
}
ctx1.beginPath();
ctx1.fillText(
"[" + point.x + "," + point.y + "]",
15,
25 * (points.length + 1)
);
ctx1.arc(point.x, point.y, 4, 0, Math.PI * 2, true);
ctx1.fill();
ctx1.closePath();
points.push(point);
if (points.length == num) {
drawAsync().start();
}
},
false
);
input.addEventListener("change", count, false);
for (var i = 0; i < parseInt(input.max); i++) {
colors[i] = "hsl(" + 60 * (i + 1) + ", 60%, 60%)";
}
count();
toggle();
}
</script>
</body>
</html>