前端 BezierCurves 相關(guān)知識(shí)
什么是貝塞爾曲線
只需要很少的控制點(diǎn)就能夠生成復(fù)雜平滑曲線(更加具體的解釋可以搜索一下)
Photoshop的鋼筆工具
掛鉤的前端技術(shù)
canvas
canvas繪制二次貝塞爾曲線 -- quadraticCurveTo(demo2_1)
var myCanvas = document.getElementById('myCanvas');
var myCtx = myCanvas.getContext('2d');
myCtx.beginPath();
myCtx.moveTo(P0.x, P0.y);
//利用quadraticCurveTo 繪制canvas 二次貝塞爾曲線
// 填入控制點(diǎn)(P1) 和 終點(diǎn)(p2)
myCtx.quadraticCurveTo(P1.x, P1.y, P2.x, P2.y);
myCtx.stroke();
myCtx.closePath();
topic_c5_5.png
canvas繪制三次貝塞爾曲線 -- bezierCurveTo(demo2_2)
var myCanvas = document.getElementById('myCanvas');
var myCtx = myCanvas.getContext('2d');
myCtx.beginPath();
myCtx.moveTo(P0.x, P0.y);
//利用bezierCurveTo 繪制canvas 三次貝塞爾曲線
// 填入控制點(diǎn)(P1,P2) 和 終點(diǎn)(p3)
myCtx.bezierCurveTo(P1.x, P1.y, P2.x, P2.y, P3.x, P3.y);
myCtx.stroke();
myCtx.closePath();
topic_c5_6.png
svg(demo2_3)
svg -- path 可以繪制貝塞爾曲線
C 后面參數(shù)為 控制點(diǎn)P1,控制點(diǎn)P2,以及終點(diǎn)P3
S 后面參數(shù)為 控制點(diǎn)P1 和終點(diǎn)P3 (省略了P2)
Q 后面為控制點(diǎn)P1 和終點(diǎn)P2
T 后面為終點(diǎn)P2(省略P1)
L 直線
<svg width="190px" height="860px">
<!--M moveTo C 三次貝塞爾曲線 S 簡(jiǎn)化的三次貝塞爾曲線 Q 二次貝塞爾曲線 T 簡(jiǎn)化的二次貝塞爾曲線 L 直線-->
<path d="M10 10 C 20 20, 40 20, 50 10" style="fill:none;stroke:red;"/>
<path d="M10 50 S 150 150, 200 50" style="fill:none;stroke:red;"/>
<path d="M10 150 Q 150 250, 200 150" style="fill:none;stroke:red;"/>
<path d="M10 250 T 50 350 T 200 200" style="fill:none;stroke:red;"/>
<path d="M10 350 L 50 450 L 200 300" style="fill:none;stroke:red;"/>
</svg>
topic_c5_8.png
動(dòng)畫 緩動(dòng)效果(demo2_4)
- css ease 目前chrome瀏覽器 有內(nèi)置的工具
特殊的緩懂可以實(shí)現(xiàn)一些特殊的效果,例如(Out · Back)
![Uploading topic_c5_1_669418.png . . .]
- canvas 動(dòng)畫也需要 ease效果,各個(gè)canvas 動(dòng)畫庫(kù)都有支持,也涉及一系列動(dòng)畫的算法,不在此展開。
貝塞爾曲線繪制原理(demo1_1)
- 初始化二次貝塞爾曲線的起始點(diǎn),控制點(diǎn)和終點(diǎn)
var myCanvas = document.getElementById('myCanvas');
var myCtx = myCanvas.getContext('2d');
//初始化3個(gè)點(diǎn)
var P0 = {
x: 30,
y: 30,
name: 'P0'
};
var P1 = {
x: 300,
y: 300,
name: 'P1'
};
var P2 = {
x: 500,
y: 30,
name: 'P2'
};
var PointList = [P0, P1, P2];
//繪制三個(gè)點(diǎn)
for (var i = 0; i < PointList.length; i++) {
var point = PointList[i];
fillPoint(myCtx, point);
}
//連線P0,P1,P2
linePoint(myCtx, PointList);
function fillPoint(ctx, point) {
if (!ctx || !point) {
return false;
}
ctx.beginPath();
ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI, true);
ctx.fill();
ctx.font = "36px";
ctx.fillText(point.name + '(' + point.x + ',' + point.y + ')', point.x + 10, point.y);
ctx.closePath();
}
function linePoint(ctx, PointList) {
if (!ctx || !PointList.length) {
return false;
}
ctx.beginPath();
for (var i = 0; i < PointList.length; i++) {
var point = PointList[i];
if (i == 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
}
ctx.stroke();
ctx.closePath();
}
topic_c5_1.png
-
確定貝塞爾曲線上的某一點(diǎn)
// 隨意設(shè)置一個(gè)比例 var t = 1 / 5; // 在線段P0P1 上找到t比例的點(diǎn)P3, 即 P0P3:P0P1=t; var P3 = { name: 'P3', x: P0.x + (P1.x - P0.x) * t, y: P0.y + (P1.y - P0.y) * t, } fillPoint(myCtx, P3); //在線段P1P2 上找到t距離的點(diǎn) P4, 即P1P4:P1P2=t; var P4 = { name: 'P4', x: P1.x + (P2.x - P1.x) * t, y: P1.y + (P2.y - P1.y) * t, } fillPoint(myCtx, P4); //連線P3P4 linePoint(myCtx, [P3, P4]); //在線段P3P4 上找到t距離的點(diǎn)P5 即P3P5:P3P4=t; var P5 = { name: 'P5', x: P3.x + (P4.x - P3.x) * t, y: P3.y + (P4.y - P3.y) * t, } fillPoint(myCtx, P5); console.log('P5 在P0P2 為起點(diǎn)終點(diǎn),P1為控制點(diǎn)的 二次貝塞爾曲線上');
topic_c5_2.png
-
將一個(gè)點(diǎn) 循環(huán)成曲線
var precision = 500; for (var i = 0; i < precision; i++) { var t = i / precision; getBezierPoint(t); } // 根據(jù)t獲得貝塞爾曲線上面的點(diǎn) function getBezierPoint(t) { // 在線段P0P1 上找到t比例的點(diǎn)P3, 即 P0P3:P0P1=t; var P3 = getTPoint(myCtx, P0, P1, t, {needfill: false}) //在線段P1P2 上找到t距離的點(diǎn) P4, 即P1P4:P1P2=t; var P4 = getTPoint(myCtx, P1, P2, t, {needfill: false}); //在線段P3P4 上找到t距離的點(diǎn)P5 即P3P5:P3P4=t; var P5 = getTPoint(myCtx, P3, P4, t); return P5; } /* * 在線段P0P1 上找到點(diǎn)TP, 使P0TP:P0P1=t, 并繪制出來(lái)然后返回點(diǎn) * */ function getTPoint(myCtx, P0, P1, t, option) { var needfill = true, name = ''; if (option) { needfill = typeof option.needfill == 'boolean' ? option.needfill : true; name = option.name || ''; } var TP = { name: name, x: P0.x + (P1.x - P0.x) * t, y: P0.y + (P1.y - P0.y) * t, } if (needfill) { fillPoint(myCtx, TP); } return TP; }
topic_c5_3.png
- 依次類推到三次貝塞爾曲線
由二次貝塞爾曲線變成多次貝塞爾曲線,原理是一樣的,一層套一層,一個(gè)迭代的關(guān)系
將 getBezierPoint 重寫,plist將大于等于3 (demo1_4)
// 根據(jù)t獲得貝塞爾曲線上面的點(diǎn)
function getBezierPoint(plist, t) {
var newlist = [];
for (var i = 0; i < plist.length - 1; i++) {
var p = getTPoint(myCtx, plist[i], plist[i + 1], t, {needFill: false});
newlist.push(p);
}
if (newlist.length > 1) {
return getBezierPoint(newlist, t);
} else {
return newlist[0];
}
}
topic_c5_4.png
代碼倉(cāng)庫(kù)(https://github.com/Lotuslwb/BezierCurves)
使用公式繪制貝塞爾曲線
topic_c5_9.png
topic_c5_10.png
topic_c5_11.png
(n k) 是什么呢
topic_c5_12.png
- n=5:第一個(gè)系數(shù)(5 0)=5!/(0!5?。?1。第二個(gè)系數(shù)(5 1)=5!/(1!4!)=5。第三個(gè)系數(shù)(5 2)=5!/(2!3!)=10。接下來(lái)是對(duì)稱的
function binomial(n, k) {
if ((typeof n !== 'number') || (typeof k !== 'number'))
return false;
var coeff = 1;
for (var x = n-k+1; x <= n; x++) coeff *= x;
for (x = 1; x <= k; x++) coeff /= x;
return coeff;
}
多次貝塞爾曲線的公式j(luò)s版本
function BezierFunction(plist, t) {
if (t > 1 || t < 0) {
return false;
}
// B(t)=P0*(1-t)^5 + 5*P1*t*(1-t)^4+10*P2*t^2(1-t)^3+10*P3*t^3(1-t)^2+ 5*P4*t^4*(1-t)^1+P5*t^5
// 從 0 開始 到 P(n-1) ~~ 3個(gè)點(diǎn) 為 2次貝塞爾曲線
var n = plist.length - 1;
var bt = 0;
for (var i = 0; i <= n; i++) {
bt += getBinomial(n, i) * plist[i] * Math.pow((1 - t), (n - i)) * Math.pow(t, i);
return bt;
}
}
繪制貝塞爾曲線動(dòng)畫(demo4_1)
只需要將曲線的實(shí)現(xiàn) 簡(jiǎn)化為點(diǎn)的運(yùn)動(dòng)就可以了
drawKeyframe()
function drawKeyframe() {
currentPrecision++;
if (currentPrecision <= maxPrecision) {
myCtx.clearRect(0, 0, myCanvas.width, myCanvas.height);
//繪制三個(gè)點(diǎn)
for (var i = 0; i < PointList.length; i++) {
var point = PointList[i];
fillPoint(myCtx, point);
}
//連線P0,P1,P2
linePoint(myCtx, PointList);
var p = getBezierPoint(currentPrecision/maxPrecision);
drawBall(myCtx, p, 10);
window.requestAnimationFrame(drawKeyframe);
}
}
function drawBall(ctx, point, r) {
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, 2 * Math.PI, true);
ctx.fill();
ctx.closePath();
}
// 根據(jù)t獲得貝塞爾曲線上面的點(diǎn)
function getBezierPoint(t) {
var p = {
x: quadraticBezierFunction(P0.x, P1.x, P2.x, t),
y: quadraticBezierFunction(P0.y, P1.y, P2.y, t)
};
return p;
}
如何繪制平滑的貝塞爾曲線(獲取合理的控制點(diǎn))
百度地圖 計(jì)算獲取控制點(diǎn)(demo4_2)
function getControlPoint() {
var p0 = PointList[0]; //起始點(diǎn)
var p2 = PointList[1]; //終止點(diǎn)
var curveness = 0.3; //邊的曲度
var inv = 1;
var p1 = {
'name': 'P1',
'x': (p0.x + p2.x) / 2 - inv * (p0.y - p2.y) * curveness,
'y': (p0.y + p2.y) / 2 - inv * (p0.x - p2.x) * curveness
};
p2.name = 'P2';
PointList.splice(1, 0, p1);
for (var i = 0; i < PointList.length; i++) {
var p = PointList[i];
PointListX.push(p.x);
PointListY.push(p.y);
}
}
貝塞爾曲線的一些應(yīng)用
canvas 大波浪動(dòng)效
用大波浪做loading, qq的例子 如下, 文章鏈接
topic_c5_4.gif
- 先用canvas + quadraticCurveTo 畫出波浪曲線(demo5_1)
var myCanvas = document.getElementById('myCanvas');
var myCtx = myCanvas.getContext('2d');
var canvasHeight = myCanvas.height, canvasWidth = myCanvas.width;
var animationFrame;
//半波長(zhǎng)
var waveLen = 100, waveHeight = 30;
//水位線初始點(diǎn)
var p0 = {
x: 0,
y: canvasHeight / 2
};
drawKeyframe();
function drawKeyframe() {
//記錄當(dāng)前位置
var currentX = p0.x, currentY = p0.y;
myCtx.clearRect(0, 0, canvasWidth, canvasHeight);
myCtx.beginPath();
myCtx.moveTo(p0.x, p0.y);
for (var i = 0; currentX <= canvasWidth + waveLen; i++) {
if (i % 2 == 0) {
//上半部波
myCtx.quadraticCurveTo(currentX + waveLen, currentY - waveHeight, currentX + waveLen * 2, currentY);
} else {
//下半部波
myCtx.quadraticCurveTo(currentX + waveLen, currentY + waveHeight, currentX + waveLen * 2, currentY);
}
currentX += waveLen * 2;
myCtx.moveTo(currentX, currentY)
}
myCtx.lineWidth = 5;
myCtx.fillStyle = "red";
myCtx.lineTo(canvasWidth, canvasHeight);
myCtx.lineTo(0, canvasHeight);
myCtx.lineTo(0, canvasHeight / 2);
myCtx.fill();
myCtx.closePath();
p0.x -= 5;
animationFrame = window.requestAnimationFrame(drawKeyframe);
}
- 增加動(dòng)畫 效果 (demo5_2)
topic_c5_13.gif
貝塞爾曲線擬合計(jì)算
貝塞爾曲線有一個(gè)非常常用的動(dòng)畫效果——MetaBall算法。
相信很多開發(fā)者都見過(guò)類似的動(dòng)畫,例如QQ的小紅點(diǎn)消除,下拉刷新loading等等。
要做好這個(gè)動(dòng)畫,實(shí)際上最重要的就是通過(guò)貝塞爾曲線來(lái)擬合兩個(gè)圖形。
topic_c5_14.png
- 矩形擬合 (demo5_3)
控制點(diǎn)為兩圓圓心連線的中點(diǎn),
連接線為圖中的這樣一個(gè)矩形,
當(dāng)圓比較小時(shí),這種通過(guò)矩形來(lái)擬合的方式幾乎是沒有問(wèn)題的。
我們把圓放大,就會(huì)不擬合。
// 圓R0 圓點(diǎn)
var p0 = {
x: 120,
y: 120,
r: 20
};
// 圓R1 圓點(diǎn)
var p1 = {
x: 400,
y: 400,
r: 20
};
//獲得 R0 和 R1 的中點(diǎn)
var p2 = {
x: (p0.x + p1.x) / 2,
y: (p0.y + p1.y) / 2,
name: 'p2'
};
myCtx.beginPath();
//畫出2個(gè)圓
myCtx.arc(p0.x, p0.y, p0.r, 0, 2 * Math.PI, true);
myCtx.stroke();
myCtx.closePath();
myCtx.beginPath();
myCtx.arc(p1.x, p1.y, p1.r, 0, 2 * Math.PI, true);
myCtx.stroke();
myCtx.closePath();
//畫出中點(diǎn)和圓心連線
linePoint(myCtx, [p0, p1]);
fillPoint(myCtx, p2);
// p0 的2個(gè)端點(diǎn)
var p0_1 = {
x: p0.x - p0.r / Math.sqrt(2),
y: p0.y + p0.r / Math.sqrt(2),
name: 'p0_1'
}, p0_2 = {
x: p0.x + p0.r / Math.sqrt(2),
y: p0.y - p0.r / Math.sqrt(2),
name: 'p0_2'
};
linePoint(myCtx, [p0_1, p0_2]);
fillPoint(myCtx, p0_1);
fillPoint(myCtx, p0_2);
//p1 的2個(gè)端點(diǎn)
var p1_1 = {
x: p1.x - p1.r / Math.sqrt(2),
y: p1.y + p1.r / Math.sqrt(2),
name: 'p1_1'
}, p1_2 = {
x: p1.x + p1.r / Math.sqrt(2),
y: p1.y - p1.r / Math.sqrt(2),
name: 'p1_2'
};
linePoint(myCtx, [p1_1, p1_2]);
fillPoint(myCtx, p1_1);
fillPoint(myCtx, p1_2);
// 連接2個(gè)圓的端點(diǎn)
linePoint(myCtx, [p0_1, p1_1]);
linePoint(myCtx, [p0_2, p1_2]);
//繪制曲線
myCtx.beginPath();
myCtx.strokeStyle = 'red';
myCtx.moveTo(p0_1.x, p0_1.y);
myCtx.quadraticCurveTo(p2.x, p2.y, p1_1.x, p1_1.y);
myCtx.stroke();
myCtx.closePath();
myCtx.beginPath();
myCtx.strokeStyle = 'red';
myCtx.moveTo(p0_2.x, p0_2.y);
myCtx.quadraticCurveTo(p2.x, p2.y, p1_2.x, p1_2.y);
myCtx.stroke();
myCtx.closePath();
- 切線擬合(demo5_5)
如前面所說(shuō),矩形擬合在半徑較小的情況下,是可以實(shí)現(xiàn)完美擬合的,而當(dāng)半徑變大后,就會(huì)出現(xiàn)貝塞爾曲線與圓相交的情況,導(dǎo)致擬合失敗。
那么如何來(lái)實(shí)現(xiàn)完美的擬合呢?實(shí)際上,也就是說(shuō)貝塞爾曲線與圓的連接點(diǎn)到貝塞爾曲線的控制點(diǎn)的連線,一定是圓的切線,這樣的話,無(wú)論圓的半徑如何變化,貝塞爾曲線一定是與圓擬合的,具體效果如圖所示:
topic_c5_15.jpeg
// 獲取切點(diǎn)坐標(biāo) 數(shù)組,經(jīng)過(guò)圓外一點(diǎn)有2個(gè)切點(diǎn)
//p0 為圓心1, p1圓心1, p2為2圓中點(diǎn),r為圓的半徑
function getTangencyPoint(p0, p1, p2, r, sencond) {
//獲取小角 角度
var x = Math.abs(p0.x - p1.x);
var y = Math.abs(p0.y - p1.y);
var angles1 = Math.atan(y / x);
if (sencond) {
angles1 = Math.PI - angles1;
}
//獲取大角 角度
//中點(diǎn)到圓點(diǎn)的距離
var len = Math.sqrt((p0.x - p2.x) * (p0.x - p2.x) + (p0.y - p2.y) * (p0.y - p2.y));
var angles2 = Math.acos(r / len);
//獲得需要的角度
var angles3 = Math.abs(angles2 - angles1);
//獲取距離圓心的距離
var diffx = Math.cos(angles3) * r;
var diffy = Math.sin(angles3) * r;
return [{
x: p0.x - diffy,
y: p0.y + diffx
}, {
x: p0.x + diffx,
y: p0.y - diffy
}]
}
- 最終效果 (demo5_7)
topic_c5_15.gif