使用JavaScript實(shí)現(xiàn)隨機(jī)曲線之間進(jìn)行平滑切換
介紹
今天,我運(yùn)用拉格朗日插值法繪制了一條曲線。然而,我并未止步于靜態(tài)展示,而是引入了一個定時器,每隔一段時間便對曲線上的點(diǎn)進(jìn)行動態(tài)更新,從而賦予曲線生命般的動態(tài)變化。
然而,在刷新過程中,我敏銳地察覺到曲線之間的切換顯得過于突兀,缺乏流暢感(請見下圖)。于是,一個大膽的想法在我腦海中閃現(xiàn):何不嘗試構(gòu)造一個曲線過渡算法,以實(shí)現(xiàn)曲線切換時的平滑過渡?這不僅將提升視覺效果,更將為動態(tài)曲線的展示增添一抹細(xì)膩與和諧。
在具體實(shí)現(xiàn)之前,我們先了解下拉格朗日插值法。
拉格朗日插值法
拉格朗日插值法是一種用于在給定數(shù)據(jù)點(diǎn)之間進(jìn)行多項(xiàng)式插值的方法。
該方法可以找到一個多項(xiàng)式,該多項(xiàng)式恰好穿過二維平面上若干個給定數(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)一下開頭介紹部分圖片所展示的效果代碼:
<!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)曲線平滑切換
簡單構(gòu)思一下,解決方案其實(shí)非常簡單:只需保存當(dāng)前曲線與下一條曲線,然后在每個橫坐標(biāo) x
值上,兩條曲線分別具有兩個縱坐標(biāo) y
值,通過利用這兩個 y
值,我們可以構(gòu)建一條 111 階貝塞爾曲線進(jìn)行插值,其他位置上的點(diǎn)重復(fù)同樣的步驟,在相同的時間內(nèi)完成插值即可實(shí)現(xiàn)曲線的平滑切換。
原理圖如下:
開始行動,我們首先構(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 = [];
對應(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)行時填充兩條曲線控制點(diǎn),之后每次運(yùn)行時,先將 nextPoints
中的數(shù)據(jù)復(fù)制到 points
中,最后填充下一條曲線控制點(diǎn)到 nextPoints
中。
然后,我們構(gòu)造用于平滑切換的動畫函數(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)行時需要保證 t
值為 0
,然后通過調(diào)用 requestAnimationFrame(animate)
函數(shù)反復(fù)執(zhí)行 animate
函數(shù)完成動畫繪制,直到 t
值達(dá)到 1
時,動畫結(jié)束。
最后,我們對繪制函數(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ù)完成平滑切換,最后通過 setTimeout
函數(shù)定時反復(fù)調(diào)用 draw
函數(shù)完成動畫循環(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曲線平滑切換的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript裝飾器之項(xiàng)目數(shù)據(jù)轉(zhuǎn)換詳解
這篇文章主要為大家詳細(xì)介紹了TypeScript項(xiàng)目中是如何進(jìn)行數(shù)據(jù)轉(zhuǎn)換的,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-10-10uniapp上傳圖片和上傳視頻的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于uniapp上傳圖片和上傳視頻的實(shí)現(xiàn)方法,文中還介紹了上傳文件的方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01layui實(shí)現(xiàn)根據(jù)table數(shù)據(jù)判斷按鈕顯示情況的方法
今天小編就為大家分享一篇layui實(shí)現(xiàn)根據(jù)table數(shù)據(jù)判斷按鈕顯示情況的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09JXTree對象,讀取外部xml文件數(shù)據(jù),生成樹的函數(shù)
JXTree對象,讀取外部xml文件數(shù)據(jù),生成樹的函數(shù)...2007-04-04Electron autoUpdater實(shí)現(xiàn)Windows安裝包自動更新的方法
這篇文章主要介紹了Electron autoUpdater實(shí)現(xiàn)Windows安裝包自動更新的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12詳解JS中定時器setInterval和setTImeout的this指向問題
在js中setTimeout和setInterval都是用來定時的一個功能,下面這篇文章主要給介紹了JS中setInterval和setTImeout的this指向問題,文中通過示例介紹的很詳細(xì),有需要的朋友可以參考借鑒,一起來看看吧。2017-01-01