可視化n次貝塞爾曲線及過程動畫演示--大寶劍

原由

研究css中提供了2次、3次bezier,可是沒有對n次bezier實現。對n次的實現有很大興趣,因此就用js的canvas搞一下,順便把過程動畫模擬了一下。
投入真實生產之中,偏少。
n次bezier曲線,作前端實際生產中,並無很大對幫助。僅僅學習研究之。
1,因爲css樣式中僅提供了2次/3次bezier曲線的造成,對n次bezier曲線的實現有很強的好奇心。
2,愛好數學之美和js動畫,想實現bezier曲線的描繪過程,實現其過程演示動畫。
故作此文。javascript

先拋的兩個例子,吊一吊Xing趣

demo提供的api概述

git倉庫地址示例css

  • 我眼睛花,沒看懂,能暫停不了?
    • 能夠控制動畫暫停與繼續。(供你們清楚地時刻看到每一幀)
  • 我研究,先不追求性能,能控制播放時間不了?
    • 能夠是setInterval代替requestAnimationFrame控制每一幀的時間(已經註釋,你們能夠註釋開控制時間)

1:只畫一個bezier曲線,理解bezeir公式

好像很吊的樣子,怎麼實現的?我是這樣最主要理解bezier曲線的公式,看我抄百度的貝塞爾公式圖,看抄
html

  • 線的個數 輔助線的個數
    • 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座標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
        }

一個貝塞爾曲線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>

2:動畫模擬bezier曲線過程

如今你明白了畫一個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)
        }

動畫每一幀中的2個技術點

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)
        }

給折線上點顏色

給中間折線上上隨機色啊,增長丟丟美感。
爲顯目,第一輪折線爲白色,最後貝塞爾線肯定爲紅色

一個貝塞爾曲線動畫demo

最後的最後有完沒完?還沒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,一塊兒大寶劍

相關文章
相關標籤/搜索