使用JavaScript實(shí)現(xiàn)隨機(jī)曲線之間進(jìn)行平滑切換
介紹
今天,我運(yùn)用拉格朗日插值法繪制了一條曲線。然而,我并未止步于靜態(tài)展示,而是引入了一個(gè)定時(shí)器,每隔一段時(shí)間便對(duì)曲線上的點(diǎn)進(jìn)行動(dòng)態(tài)更新,從而賦予曲線生命般的動(dòng)態(tài)變化。
然而,在刷新過(guò)程中,我敏銳地察覺(jué)到曲線之間的切換顯得過(guò)于突兀,缺乏流暢感(請(qǐng)見(jiàn)下圖)。于是,一個(gè)大膽的想法在我腦海中閃現(xiàn):何不嘗試構(gòu)造一個(gè)曲線過(guò)渡算法,以實(shí)現(xiàn)曲線切換時(shí)的平滑過(guò)渡?這不僅將提升視覺(jué)效果,更將為動(dòng)態(tài)曲線的展示增添一抹細(xì)膩與和諧。
在具體實(shí)現(xiàn)之前,我們先了解下拉格朗日插值法。
拉格朗日插值法
拉格朗日插值法是一種用于在給定數(shù)據(jù)點(diǎn)之間進(jìn)行多項(xiàng)式插值的方法。
該方法可以找到一個(gè)多項(xiàng)式,該多項(xiàng)式恰好穿過(guò)二維平面上若干個(gè)給定數(shù)據(jù)點(diǎn)。
拉格朗日插值多項(xiàng)式
拉格朗日插值多項(xiàng)式的代碼實(shí)現(xiàn)
function lagrange(x, points) { const n = points.length; const result = []; for (let i = 0; i < n; i++) { let tmp = points[i].y; for (let j = 0; j < n; j++) { if (j !== i) { tmp *= (x - points[j].x) / (points[i].x - points[j].x); } } result.push(tmp); } return result.reduce((sum, cur) => sum + cur, 0); }
實(shí)現(xiàn)曲線突兀切換
我們首先完整實(shí)現(xiàn)一下開(kāi)頭介紹部分圖片所展示的效果代碼:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; } canvas { border-radius: 15px; background-color: #ffffff; } </style> </head> <body> <canvas id="demo-canvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('demo-canvas'); const ctx = canvas.getContext('2d'); let points = []; function drawLine(x1, y1, x2, y2, color) { ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = color; ctx.stroke(); } function lagrange(x, points) { const n = points.length; const result = []; for (let i = 0; i < n; i++) { let tmp = points[i].y; for (let j = 0; j < n; j++) { if (j !== i) { tmp *= (x - points[j].x) / (points[i].x - points[j].x); } } result.push(tmp); } return result.reduce((sum, cur) => sum + cur, 0); } function fillPoints() { const randomNumber = (min, max) => { const randomBuffer = new Uint32Array(1); window.crypto.getRandomValues(randomBuffer); const number = randomBuffer[0] / (0xffffffff + 1); return number * (max - min + 1) + min; } points = []; const count = 7; for (let i = 0; i < count; i++) { points.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); fillPoints(); const step = 1; for (let x = points[0].x; x < points[points.length - 1].x; x += step) { drawLine(x, lagrange(x, points), x + step, lagrange(x + step, points), 'red'); } setTimeout(draw, 1000); } draw(); </script> </body> </html>
實(shí)現(xiàn)曲線平滑切換
簡(jiǎn)單構(gòu)思一下,解決方案其實(shí)非常簡(jiǎn)單:只需保存當(dāng)前曲線與下一條曲線,然后在每個(gè)橫坐標(biāo) x
值上,兩條曲線分別具有兩個(gè)縱坐標(biāo) y
值,通過(guò)利用這兩個(gè) y
值,我們可以構(gòu)建一條 111 階貝塞爾曲線進(jìn)行插值,其他位置上的點(diǎn)重復(fù)同樣的步驟,在相同的時(shí)間內(nèi)完成插值即可實(shí)現(xiàn)曲線的平滑切換。
原理圖如下:
開(kāi)始行動(dòng),我們首先構(gòu)造 111 階貝塞爾曲線:
B(t)=(1−t)P 0 ? +tP 1 ? 0≤t≤1
其中 P0為當(dāng)前曲線的縱坐標(biāo),P1為下一條曲線的縱坐標(biāo),ttt 為插值系數(shù)。
function bezier(t, y0, y1) { return (1 - t) * y0 + t * y1; }
然后,我們構(gòu)造用于保存下一條曲線控制點(diǎn)的數(shù)組 nextPoints
:
let nextPoints = [];
對(duì)應(yīng)的填充曲線控制點(diǎn)的函數(shù) fillPoints
也需要做相應(yīng)調(diào)整:
function fillPoints() { const randomNumber = (min, max) => { const randomBuffer = new Uint32Array(1); window.crypto.getRandomValues(randomBuffer); const number = randomBuffer[0] / (0xffffffff + 1); return number * (max - min + 1) + min; } const count = 7; if (points.length === 0 && nextPoints.length === 0) { for (let i = 0; i < count; i++) { points.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); nextPoints.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); } } else { points = []; points = nextPoints; nextPoints = []; for (let i = 0; i < count; i++) { nextPoints.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); } } }
fillPoints
函數(shù)在第一次運(yùn)行時(shí)填充兩條曲線控制點(diǎn),之后每次運(yùn)行時(shí),先將 nextPoints
中的數(shù)據(jù)復(fù)制到 points
中,最后填充下一條曲線控制點(diǎn)到 nextPoints
中。
然后,我們構(gòu)造用于平滑切換的動(dòng)畫函數(shù) animate
:
let t = 0; function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); const step = 1; for (let x = points[0].x; x < points[points.length - 1].x; x += step) { const y = bezier(t, lagrange(x, points), lagrange(x, nextPoints)); const y_step = bezier(t, lagrange(x + step, points), lagrange(x + step, nextPoints)); drawLine(x, y, x + step, y_step, 'red'); } t += 0.05; if (t < 1) { requestAnimationFrame(animate); } }
animate
函數(shù)在每次調(diào)用中的第一次運(yùn)行時(shí)需要保證 t
值為 0
,然后通過(guò)調(diào)用 requestAnimationFrame(animate)
函數(shù)反復(fù)執(zhí)行 animate
函數(shù)完成動(dòng)畫繪制,直到 t
值達(dá)到 1
時(shí),動(dòng)畫結(jié)束。
最后,我們對(duì)繪制函數(shù) draw
做相應(yīng)調(diào)整:
function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); fillPoints(); const step = 1; t = 0; for (let x = points[0].x; x < points[points.length - 1].x; x += step) { drawLine(x, lagrange(x, points), x + step, lagrange(x + step, points), 'red'); } animate(); setTimeout(draw, 1000); }
保證繪制完當(dāng)前的曲線后,立即調(diào)用 animate
函數(shù)完成平滑切換,最后通過(guò) setTimeout
函數(shù)定時(shí)反復(fù)調(diào)用 draw
函數(shù)完成動(dòng)畫循環(huán)。
完整代碼
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Canvas</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; } canvas { border-radius: 15px; background-color: #ffffff; } </style> </head> <body> <canvas id="demo-canvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('demo-canvas'); const ctx = canvas.getContext('2d'); let points = [], nextPoints = []; function drawLine(x1, y1, x2, y2, color) { ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = color; ctx.stroke(); } function lagrange(x, points) { const n = points.length; const result = []; for (let i = 0; i < n; i++) { let tmp = points[i].y; for (let j = 0; j < n; j++) { if (j !== i) { tmp *= (x - points[j].x) / (points[i].x - points[j].x); } } result.push(tmp); } return result.reduce((sum, cur) => sum + cur, 0); } function bezier(t, y0, y1) { return (1 - t) * y0 + t * y1; } function fillPoints() { const randomNumber = (min, max) => { const randomBuffer = new Uint32Array(1); window.crypto.getRandomValues(randomBuffer); const number = randomBuffer[0] / (0xffffffff + 1); return number * (max - min + 1) + min; } const count = 7; if (points.length === 0 && nextPoints.length === 0) { for (let i = 0; i < count; i++) { points.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); nextPoints.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); } } else { points = []; points = nextPoints; nextPoints = []; for (let i = 0; i < count; i++) { nextPoints.push({ x: (i + 1) * 100, y: randomNumber(200, 400) }); } } } let t = 0; function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); const step = 1; for (let x = points[0].x; x < points[points.length - 1].x; x += step) { const y = bezier(t, lagrange(x, points), lagrange(x, nextPoints)); const y_step = bezier(t, lagrange(x + step, points), lagrange(x + step, nextPoints)); drawLine(x, y, x + step, y_step, 'red'); } t += 0.05; if (t < 1) { requestAnimationFrame(animate); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); fillPoints(); const step = 1; t = 0; for (let x = points[0].x; x < points[points.length - 1].x; x += step) { drawLine(x, lagrange(x, points), x + step, lagrange(x + step, points), 'red'); } animate(); setTimeout(draw, 1000); } draw(); </script> </body> </html>
展示
以上就是使用JavaScript實(shí)現(xiàn)隨機(jī)曲線之間進(jìn)行平滑切換的詳細(xì)內(nèi)容,更多關(guān)于JavaScript曲線平滑切換的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript裝飾器之項(xiàng)目數(shù)據(jù)轉(zhuǎn)換詳解
這篇文章主要為大家詳細(xì)介紹了TypeScript項(xiàng)目中是如何進(jìn)行數(shù)據(jù)轉(zhuǎn)換的,文中的示例代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10uniapp上傳圖片和上傳視頻的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于uniapp上傳圖片和上傳視頻的實(shí)現(xiàn)方法,文中還介紹了上傳文件的方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01layui實(shí)現(xiàn)根據(jù)table數(shù)據(jù)判斷按鈕顯示情況的方法
今天小編就為大家分享一篇layui實(shí)現(xiàn)根據(jù)table數(shù)據(jù)判斷按鈕顯示情況的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09JXTree對(duì)象,讀取外部xml文件數(shù)據(jù),生成樹的函數(shù)
JXTree對(duì)象,讀取外部xml文件數(shù)據(jù),生成樹的函數(shù)...2007-04-04Electron autoUpdater實(shí)現(xiàn)Windows安裝包自動(dòng)更新的方法
這篇文章主要介紹了Electron autoUpdater實(shí)現(xiàn)Windows安裝包自動(dòng)更新的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12詳解JS中定時(shí)器setInterval和setTImeout的this指向問(wèn)題
在js中setTimeout和setInterval都是用來(lái)定時(shí)的一個(gè)功能,下面這篇文章主要給介紹了JS中setInterval和setTImeout的this指向問(wèn)題,文中通過(guò)示例介紹的很詳細(xì),有需要的朋友可以參考借鑒,一起來(lái)看看吧。2017-01-01