研究css中提供了2次、3次bezier,可是沒有對n次bezier實現。對n次的實現有很大興趣,因此就用js的canvas搞一下,順便把過程動畫模擬了一下。
投入真實生產之中,偏少。
n次bezier曲線,作前端實際生產中,並無很大對幫助。僅僅學習研究之。
1,因爲css樣式中僅提供了2次/3次bezier曲線的造成,對n次bezier曲線的實現有很強的好奇心。
2,愛好數學之美和js動畫,想實現bezier曲線的描繪過程,實現其過程演示動畫。
故作此文。javascript
git倉庫地址示例css
好像很吊的樣子,怎麼實現的?我是這樣最主要理解bezier曲線的公式,看我抄百度的貝塞爾公式圖,看抄
html
數學偏low的人是組合哪一個符號,表示不明白,舉爪。前端
//組合 function C(n, i) { return f(n) / f(i) / f(n - i) } //階乘公式 n! //階乘 factorial function f(n) { if (n < 0) { return -1 } else if (n === 0 || n === 1) { return 1 } else { return (n * f(n - 1)) } }
控制點固定,t爲【0,1】的一個值的時候,獲取bezier曲線的一個點的x y座標java
//曲線上的一個點,分別求出x,和y //points肯定係數 //t是自變量,這裏獲取一個點的時候,須要t固定,畫線的時候再賦值[0,1],分100份的話,每次t差距0.01,循環t //公式中須要組合 function getOnePointXY(points, t) { return { x: Sigmar('x', points, t), y: Sigmar('y', points, t) } } //x或者y方向上的座標,bezier曲線求和 function sigmar(direction, points, t) { var result = 0 //n+1個節點,是n次bezier曲線 let n = points.length - 1 for (let [i, { x, y }] of points.entries()) { var A = C(n, i) var P = direction === 'x' ? x : direction === 'y' ? y : x//不傳'x' 'y'默認x方向 var t1 = Math.pow(1 - t, n - i) var t2 = Math.pow(t, i) result += A * P * t1 * t2 } return result }
點都肯定了,開始畫canvasnode
var controlPoints = [{ x: 100, y: 500 }, { x: 150, y: 400 }, { x: 600, y: 300 }, { x: 400, y: 150 }] //一條bezier曲線上有多少個點, //分100份的話,每次t差距0.01,循環。 //todo,用戶配置--點--暫停--嵌入動畫裏面 var pointCount = 1000 var allBezeirPoints = nbezeirCurve(controlPoints, pointCount) const pen = canvas.getContext('2d') pen.moveTo(allBezeirPoints[0].x, allBezeirPoints[0].y) //pen.moveTo(0, allBezeirPoints[0].y) for (let { x, y } of allBezeirPoints) { pen.lineTo(x, y) } pen.stroke() console.log(nbezeirCurve(controlPoints, pointCount)) //獲得n次bezier曲線的pointCount個數個點數組 function nbezeirCurve(controlPoints, pointCount, t = 0) { var step = 1 / pointCount//t->step++[0,1] var pointArr = [] while (t < 1) { pointArr.push(getOnePointXY(controlPoints, t)) t += step } return pointArr }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>bezeir by 李可</title> </head> <body> <canvas id="canvas" width="800" height="600"></canvas> <script> var controlPoints = [{ x: 100, y: 500 }, { x: 150, y: 400 }, { x: 600, y: 300 }, { x: 400, y: 150 }] //一條bezier曲線上有多少個點, //分100份的話,每次t差距0.01,循環。 //todo,用戶配置--點--暫停--嵌入動畫裏面 var pointCount = 1000 var allBezeirPoints = nbezeirCurve(controlPoints, pointCount) const pen = canvas.getContext('2d') pen.moveTo(allBezeirPoints[0].x, allBezeirPoints[0].y) //pen.moveTo(0, allBezeirPoints[0].y) for (let { x, y } of allBezeirPoints) { pen.lineTo(x, y) } pen.stroke() console.log(nbezeirCurve(controlPoints, pointCount)) //獲得n次bezier曲線的pointCount個數個點數組 function nbezeirCurve(controlPoints, pointCount, t = 0) { var step = 1 / pointCount//t->step++[0,1] var pointArr = [] while (t < 1) { pointArr.push(getOnePointXY(controlPoints, t)) t += step } return pointArr } //曲線上的一個點,分別求出x,和y //points肯定係數 //t是自變量,這裏獲取一個點的時候,須要t固定,畫線的時候再賦值[0,1],分100份的話,每次t差距0.01,循環t //公式中須要組合 function getOnePointXY(points, t) { return { x: Sigmar('x', points, t), y: Sigmar('y', points, t) } } //x或者y方向上的座標,bezier曲線求和 function Sigmar(direction, points, t) { var result = 0 //n+1個節點,是n次bezier曲線 let n = points.length - 1 for (let [i, { x, y }] of points.entries()) { var A = C(n, i) var P = direction === 'x' ? x : direction === 'y' ? y : x//不傳'x' 'y'默認x方向 var t1 = Math.pow(1 - t, n - i) var t2 = Math.pow(t, i) result += A * P * t1 * t2 } return result } //組合 function C(n, i) { return f(n) / f(i) / f(n - i) } //階乘 factorial function f(n) { if (n < 0) { return -1 } else if (n === 0 || n === 1) { return 1 } else { return (n * f(n - 1)) } } </script> </body> </html>
如今你明白了畫一個bezier如此簡單,是否特別想怎麼用動畫模仿出來這個貝塞爾的過程?繼續看我BB 模擬動畫的思路,那讓咱們繼續想,怎麼畫這個動畫呢?
....想來想去------>每一幀,把t的全部連線都畫好。下一幀把上一幀的連線抹除後,再畫t=t+0.01(這裏分了100份,每份0.01)的的全部連線。
全部線,每一幀到底有多少線須要畫?見下圖。
針對每一幀:根據t
假使畫5次貝賽爾曲線,先畫4個線,(獲得4個點,先畫3個線),(獲得3個點,再畫2條)。
假使畫4次貝賽爾曲線,先畫3個線,(獲得3個點,再畫2條)。
假使畫3次貝賽爾曲線,(畫2條)。github
function drawBrokenLine(points, t = 1, lineColor = 'white', hasNode = true, nodeColor = 'white') { if (points.length >= 2) { for (var i = 0; i < points.length - 1; i++) { var current = points[i] var next = points[i + 1] drawLine(current, next, lineColor) hasNode && drawNode(current, nodeColor) } hasNode && drawNode(points[points.length - 1], nodeColor) } return getPercentPoints(points, t) }
t固定下,怎麼獲得上個折線中對應下次點座標折線集合?看圖說話。順便看下代碼
canvas
function getPercentPoints(points, t) { if (points.length <= 1) { return points } const perPoints = [] var inx = 0 while (inx < points.length - 1) { const current = points[inx] const next = points[inx + 1] var perPoint = { x: current.x + (next.x - current.x) * t, y: current.y + (next.y - current.y) * t } perPoints.push(perPoint) inx++ } return perPoints }
直到剩下 1個點時候,就是besier曲線上的值了api
function drawframe(points, t) { var lineColors = getColors(points) canvas.width = canvas.width init(pen) //畫第一折線 var percentPoints = drawBrokenLine(points, t, 'white', true, 'yellow') var i = 0 //循環畫中間折線 while (percentPoints.length > 1) { const currentColor = lineColors[++i] percentPoints = drawBrokenLine(percentPoints, t, currentColor, true, currentColor) } //循環畫貝塞爾折(曲)線 const bezeirPoints = getBezierPoints(controlPoints, step, t) drawBrokenLine(bezeirPoints, t, 'red', false) }
給中間折線上上隨機色啊,增長丟丟美感。
爲顯目,第一輪折線爲白色,最後貝塞爾線肯定爲紅色
最後的最後有完沒完?還沒BB完?完了..,不行,不要砍我........運行大寶劍
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>bezier by 李可</title> </head> <body> <canvas id="canvas" width="1000" height="600"></canvas> <br> <input type="button" id="btn1" value="繪製"> <input type="button" id="btn2" value="清空"> <input type="button" id="btn3" value="暫停"> <script> function getPercentPoints(points, t) { if (points.length <= 1) { return points } const perPoints = [] var inx = 0 while (inx < points.length - 1) { const current = points[inx] const next = points[inx + 1] var perPoint = { x: current.x + (next.x - current.x) * t, y: current.y + (next.y - current.y) * t } perPoints.push(perPoint) inx++ } return perPoints } function getBezierPoints(points, t, end = 1, start = 0) { var pointArr = [] while (start <= end) { var node = getOneBezierPoint(points, start) pointArr.push(node) start += t } return pointArr } //曲線上的一個點,分別求出x,和y //points肯定係數 //t是自變量,這裏獲取一個點的時候,須要t固定,畫線的時候再賦值[0,1],分100份的話,每次t差距0.01,循環t //公式中須要組合 function getOneBezierPoint(points, t) { return { x: sigmar('x', points, t), y: sigmar('y', points, t) } } //x或者y方向上的座標,bezier曲線求和 function sigmar(direction, points, t) { var result = 0 //n+1個節點,是n次bezier曲線 let n = points.length - 1 for (let [i, { x, y }] of points.entries()) { var A = C(n, i) var P = direction === 'x' ? x : direction === 'y' ? y : x//不傳'x' 'y'默認x方向 var t1 = Math.pow(1 - t, n - i) var t2 = Math.pow(t, i) result += A * P * t1 * t2 } return result } //組合 function C(n, i) { return f(n) / f(i) / f(n - i) } //階乘 factorial function f(n) { if (n < 0) { return -1 } else if (n === 0 || n === 1) { return 1 } else { return (n * f(n - 1)) } } </script> <script> const controlPoints = []//{ x: 100, y: 500 }, { x: 150, y: 400 }, { x: 600, y: 300 }, { x: 400, y: 150 } const pen = canvas.getContext('2d') function init(pen) { pen.fillStyle = "#444" pen.fillRect(0, 0, canvas.width, canvas.height) } init(pen) canvas.onmousedown = function (e) { const point = { x: e.offsetX, y: e.offsetY } controlPoints.push(point) drawText(point, controlPoints.length) drawNode(point) drawLastLine(controlPoints) } //顯示點擊位置 function drawText(point, inx, y = 10, font = 16) { pen.fillStyle = "#fff" pen.textAlign = 'end' pen.textBaseline = 'hanging' pen.font = `${font}px`//times pen.fillText(`${point.x}x${point.y}:${inx}`, 1000 - 20, inx === 1 ? y : (inx - 1) * font + y) } function drawLastLine(points) { //畫最後兩點連線 -折線 var count = points.length var current = points[count - 2] var next = points[count - 1] if (count >= 2) { drawLine(current, next) } } function drawNode(point, nodeColor = 'white') { //畫節點 pen.beginPath() pen.strokeStyle = nodeColor pen.lineWidth = 2 pen.arc(point.x, point.y, 8, 0, 2 * Math.PI) pen.stroke() } function drawLine(current, next, color = "white") { //畫最後兩點連線 -折線 pen.beginPath() pen.strokeStyle = color pen.lineWidth = 2 pen.moveTo(current.x, current.y) pen.lineTo(next.x, next.y) pen.stroke() } const pointCount = 100 const step = 1 / pointCount//t->step++[0,1] //繪bezier曲線 function drawBrokenLine(points, t = 1, lineColor = 'white', hasNode = true, nodeColor = 'white') { if (points.length >= 2) { for (var i = 0; i < points.length - 1; i++) { var current = points[i] var next = points[i + 1] drawLine(current, next, lineColor) hasNode && drawNode(current, nodeColor) } hasNode && drawNode(points[points.length - 1], nodeColor) } return getPercentPoints(points, t) } function getRandomColor() { var color = "#" for (let i = 0; i < 6; i++) { color += Array.from('0123456789abcdef')[Math.floor(16 * Math.random())] } return color } //n次,畫n-1條折線 var lineColors = [] function getColors(points) { const len = points.length for (let i = 0; i < len - 1; i++) { lineColors.push(getRandomColor()) } return lineColors } function drawframe(points, t) { var lineColors = getColors(points) canvas.width = canvas.width init(pen) var percentPoints = drawBrokenLine(points, t, 'white', true, 'yellow') var i = 0 while (percentPoints.length > 1) { const currentColor = lineColors[++i] percentPoints = drawBrokenLine(percentPoints, t, currentColor, true, currentColor) } const bezeirPoints = getBezierPoints(controlPoints, step, t) drawBrokenLine(bezeirPoints, t, 'red', false) } var timer var state var runFlag = true function startBezier(t, recursive = false) {//iteration // timer = setInterval(() => { // if (t <= 1) { // drawframe(controlPoints, t) // t += step // state = t // } else { // clearInterval(timer) // drawframe(controlPoints, 1) // recursive && startBezier(0) // } // }, 200) timer = requestAnimationFrame(function frame() { if (runFlag) { if (t <= 1) { drawframe(controlPoints, t) t += step state = t requestAnimationFrame(frame) } else { cancelAnimationFrame(timer) drawframe(controlPoints, 1) recursive && startBezier(0) } } else { cancelAnimationFrame(timer) } }) // const bezeirPoints = getBezierPoints(controlPoints, step, 0.5) // drawBrokenLine(bezeirPoints, 1, 'red') } btn1.onclick = function () { startBezier(0) } btn2.onclick = function () { controlPoints.splice(0, controlPoints.length) canvas.width = canvas.width // clearInterval(timer) runFlag = true init(pen) } var count = 0 btn3.onclick = function () { if (++count % 2 === 1) { btn3.value = '繼續' if (timer) { //clearInterval(timer) runFlag = false } } else { btn3.value = '暫停' console.log(state) runFlag = true startBezier(state) } } </script> </body> </html>
歡迎你們加入QQ羣471838073,一塊兒大寶劍