1. 程式人生 > >可視化n次貝塞爾曲線及過程動畫演示--大寶劍

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

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次貝塞爾曲線及過程動畫演示--大寶劍