JavaScript实现随机曲线平滑切换:基于贝塞尔插值算法
本文详解如何利用JavaScript在Canvas中实现随机曲线的平滑切换。通过结合拉格朗日插值法生成基础曲线,并引入一阶贝塞尔曲线算法进行逐点插值,解决了传统刷新导致的视觉突兀问题。文章提供了完整的代码实现与动画逻辑解析,适用于需要动态数据可视化平滑过渡的场景。
介绍
本文利用拉格朗日插值法绘制曲线,并通过定时器定期更新曲线上的点数据,使静态曲线呈现出动态变化的效果。
在初步实现中,发现曲线刷新时的切换过程过于生硬,缺乏流畅感(见下图)。为此,本文提出一种曲线过渡算法,旨在实现曲线切换时的平滑过渡,从而提升视觉体验的细腻度与和谐感。

在深入具体实现之前,有必要先回顾拉格朗日插值法的基本原理。
拉格朗日插值法
拉格朗日插值法是一种多项式插值技术,用于在已知数据点之间构建函数。
该方法的核心目标是寻找一个多项式,使其精确穿过二维平面上的若干个指定数据点。
拉格朗日插值多项式

拉格朗日插值多项式的代码实现
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);
}
实现曲线突兀切换
首先,我们实现前文介绍中图片所展示的基础效果代码:
<!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>
实现曲线平滑切换
解决方案的核心思路如下:保存当前曲线与下一条曲线的状态。对于每个横坐标 x 值,两条曲线分别对应两个纵坐标 y 值。利用这两个 y 值,构建一条 111 阶贝塞尔曲线进行插值。对其他位置重复此步骤,并在相同时间内完成所有点的插值计算,即可实现曲线的平滑切换。
原理示意图如下:

接下来开始具体编码,首先构造 111 阶贝塞尔曲线:
B(t)=(1−t)P 0 +tP 1 0≤t≤1
其中 P0 表示当前曲线的纵坐标值,P1 表示下一条曲线的纵坐标值,t 为插值系数。
function bezier(t, y0, y1) {
return (1 - t) * y0 + t * y1;
}
随后,构造用于存储下一条曲线控制点的数组 nextPoints:
let nextPoints = [];
同时,填充曲线控制点的函数 fillPoints 也需进行相应调整:
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 函数在首次运行时负责填充两条曲线的控制点;在后续每次运行时,先将 nextPoints 中的数据复制到 points 中,最后将下一条曲线的控制点填充至 nextPoints。
接着,构造用于执行平滑切换的动画函数 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 函数在每次调用的首次执行时,需确保 t 的值为 0。随后通过调用 requestAnimationFrame(animate) 函数反复执行 animate 函数以完成动画绘制,直到 t 的值达到 1 时,动画结束。
最后,对绘制函数 draw 进行相应调整:
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);
}
确保在绘制完当前曲线后,立即调用 animate 函数以完成平滑切换,并通过 setTimeout 函数定时反复调用 draw 函数,从而完成动画循环。
完整代码
<!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实现随机曲线之间进行平滑切换的详细内容,更多关于JavaScript曲线平滑切换的资料请关注本站其它相关文章!
5款主流JS在线运行工具对比:CodePen、JSFiddle与StackBlitz怎么选
2026-11-05 17:04:04
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
JavaScript函数定义的三种方式:声明、表达式与箭头函数
2026-11-05 13:50:55
