可視化n次貝塞爾曲線及過程動畫演示--大寶劍
阿新 • • 發佈:2018-06-04
ike all AS 2個 pat title pre while todo
先拋一個動畫模擬的一個例子,吊一吊Xing趣(4次)
不夠強?再來一個
這樣子,滿足你。demo說明
git倉庫地址示例
- 我眼睛花,沒看懂,能暫停不了?
- 可以控制動畫暫停與繼續。(供大家清楚地時刻看到每一幀)
- 我研究,先不追求性能,能控制播放時間不了?
- 可以是setInterval代替requestAnimationFrame控制每一幀的時間(已經註釋,大家可以註釋開控制時間)
起因
研究css中提供了2次、3次bezier,但是沒有對n次bezier實現。對n次的實現有很大興趣,所以就用js的canvas搞一下,順便把過程動畫模擬了一下。
投入真實生產之中,偏少。
好像很吊的樣子,怎麽實現的?------------------------------------------------------------------------1:只畫一個bezier曲線,看我BB
看我是這樣理解bezeir公式
最主要理解bezier曲線的公式,看我抄百度的貝塞爾公式圖,看抄
- 線的個數 輔助線的個數
- n個節點(n>2),
- 總線數:(n-1)+(n-2)+...+1,公差為1等差數列求和,S=(1+n-1)(n-1)/2=n(n-1)/2
- 中間輔助線(包含最後一條):n*(n-1)/2-(n-1)
- 假如:2個節點,總1條 0輔助
- 假如:3個節點,總3條 1輔助
- 假如:4個節點,總6條 3輔助
- 假如:5個節點,總10條 6輔助
- 我是這樣子理解 t的(自變量t的範圍)
- 不論幾次貝塞爾,t從0->1[0,1],這個過程:
- 假如:描了100個點,就是把範圍1分成100份 ,每份0.01
- 假如:描了1000個點,就是把範圍1分成100份 ,每份0.001
組合:數學偏low的人是組合哪個符號,表示不明白,舉爪。
- 兩個圓括號(n i)是什麽?是組合嗎,組合不C n i嗎。我也是數學偏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坐標
//曲線上的一個點,分別求出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
}
點都確定了,開始canvas
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
}
demo
<!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如此簡單,是否特別想怎麽用動畫模仿出來這個貝塞爾的過程?-------------2:動畫模擬bezier曲線過程,繼續看我BB
模擬動畫的思路,那讓我們繼續想,怎麽畫這個動畫呢?
....想來想去------>每一幀,把t的所有連線都畫好。下一幀把上一幀的連線抹除後,再畫t=t+0.01(這裏分了100份,每份0.01)的的所有連線。
所有線,每一幀到底有多少線需要畫?見下圖。
針對每一幀:根據t
假使畫5次貝賽爾曲線,先畫4個線,(得到4個點,先畫3個線),(得到3個點,再畫2條)。
假使畫4次貝賽爾曲線,先畫3個線,(得到3個點,再畫2條)。
假使畫3次貝賽爾曲線,(畫2條)。
畫折線
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)
}
動畫每一幀中的2個技術點
t固定下,怎麽得到上個折線中對應下次點坐標折線集合?看圖說話。順便看下代碼
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曲線上的值了
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完?完了..,不行,不要砍我...我只是想上個示例demo.....運行大寶劍
<!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,一起大寶劍
可視化n次貝塞爾曲線及過程動畫演示--大寶劍