研究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,一块儿大宝剑