JavaScript实现随机曲线平滑切换:基于贝塞尔插值算法

作者:代码拾荒

时间:2026-10-31

浏览:212

本文详解如何利用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曲线平滑切换的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章