首页 > 编程语言 >使用JavaScript实现:随机曲线之间进行平滑切换

使用JavaScript实现:随机曲线之间进行平滑切换

时间:2024-10-31 14:51:46浏览次数:5  
标签:canvas const 平滑 JavaScript step points let 随机 nextPoints

介绍

今天,我运用拉格朗日插值法绘制了一条曲线。然而,我并未止步于静态展示,而是引入了一个定时器,每隔一段时间便对曲线上的点进行动态更新,从而赋予曲线生命般的动态变化。

然而,在刷新过程中,我敏锐地察觉到曲线之间的切换显得过于突兀,缺乏流畅感(请见下图)。于是,一个大胆的想法在我脑海中闪现:何不尝试构造一个曲线过渡算法,以实现曲线切换时的平滑过渡?这不仅将提升视觉效果,更将为动态曲线的展示增添一抹细腻与和谐。

在这里插入图片描述

在具体实现之前,我们先了解下拉格朗日插值法。

拉格朗日插值法

拉格朗日插值法是一种用于在给定数据点之间进行多项式插值的方法。

该方法可以找到一个多项式,该多项式恰好穿过二维平面上若干个给定数据点。

拉格朗日插值多项式

给定 n + 1 n+1 n+1 个点 ( x 0 , y 0 ) , ( x 1 , y 1 ) , ⋯   , ( x n , y n ) (x_0,y_0), (x_1,y_1),\cdots,(x_n,y_n) (x0​,y0​),(x1​,y1​),⋯,(xn​,yn​),则存在一个 n n n 次多项式 P ( x ) P(x) P(x) 使得 P ( x i ) = y i i = 0 , 1 , ⋯   , n P(x_i)=y_i \quad i=0,1,\cdots,n P(xi​)=yi​i=0,1,⋯,n,即:

P ( x ) = ∑ i = 0 n y i l i ( x ) P(x)=\sum_{i=0}^n y_i l_i(x) P(x)=i=0∑n​yi​li​(x)

其中 l i ( x ) l_i(x) li​(x) 是拉格朗日基函数,定义为:

l i ( x ) = ∏ j = 0 , j ≠ i n x − x j x i − x j l_i(x)=\prod_{j=0,j\neq i}^n \frac{x-x_j}{x_i-x_j} li​(x)=j=0,j=i∏n​xi​−xj​x−xj​​

拉格朗日插值多项式的代码实现

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 值,我们可以构建一条 1 1 1 阶贝塞尔曲线进行插值,其他位置上的点重复同样的步骤,在相同的时间内完成插值即可实现曲线的平滑切换。

原理图如下:

在这里插入图片描述

开始行动,我们首先构造 1 1 1 阶贝塞尔曲线:

B ( t ) = ( 1 − t ) P 0 + t P 1 0 ≤ t ≤ 1 B(t) = (1 - t)P_0 + tP_1 \quad 0 \leq t \leq 1 B(t)=(1−t)P0​+tP1​0≤t≤1

其中 P 0 P_0 P0​ 为当前曲线的纵坐标, P 1 P_1 P1​ 为下一条曲线的纵坐标, t t 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>

展示

在这里插入图片描述

标签:canvas,const,平滑,JavaScript,step,points,let,随机,nextPoints
From: https://blog.csdn.net/qq_19661207/article/details/143400620

相关文章

  • JavaScript
    JavaScriptJavaScript因为互联网而生,紧随着浏览器的出现而问世1、JavaScript的历史https://javascript.ruanyifeng.com/introduction/history.htmljs参数描述throw声明的作用是创建exception(异常或错误)this的四种用法1.在一般函数方法中使用this指代......
  • JavaScript 网页设计案例操作指南
    随着互联网的发展和现代网页设计的需求,JavaScript已成为构建动态网页和增强用户体验的重要工具。无论是简单的网页交互,还是复杂的单页应用,JavaScript的使用场景都越来越广泛。在本指南中,我们将通过构建一个简单的动态网页应用示例,深入探讨JavaScript在网页设计中的实际应用......
  • 关于Web前端使用JavaScript常见的数据类型处理小技巧
    1.1获取字符串长度  如果想获取这个字符串的长度,也就是它里面有多少个字符,可以使用 length 属性:consts="HelloWorld";console.log(s.length)1.2 转换大小写toUpperCase() 方法可以将字母全部大写consts="HelloWorld";console.log(s.toLowerCase());......
  • 【Web前端】JavaScript 对象原型与继承机制
    JavaScript是一种动态类型的编程语言,其核心特性之一就是对象和原型链。理解原型及其工作机制对于掌握JavaScript的继承和对象关系非常重要。什么是原型每个对象都有一个内部属性 ​​[[Prototype]]​​​,这个属性指向创建该对象的构造函数的原型对象。这个内部属性通......
  • Javascript实现的网页版绘图板
    项目简介这是一个基于HTML5Canvas和jQuery实现的简单网页版绘图编辑器。提供了基础的图片编辑功能,包括画笔工具、橡皮擦、亮度/对比度调节等功能。可以用于简单的图片编辑和绘图需求。主要功能1.基础绘图工具画笔工具:支持自定义颜色和大小橡皮擦工具:支持自定义大小撤......
  • Java如何获取随机数
    引言1. java.util.Random 类1.1基本用法1.2优点1.3缺点2. Math.random() 方法2.1基本用法2.2优点2.3缺点3. ThreadLocalRandom 类3.1基本用法3.2优点3.3缺点4.应用场景5.总结引言在Java编程中,生成随机数是一个常见的需求,无论是用于模拟、测......
  • 【JavaScript】之浏览器对象模型(BOM)详解
    浏览器对象模型(BOM:BrowserObjectModel)是JavaScript的一部分,它允许你与浏览器窗口进行交互。不同于DOM(文档对象模型)主要处理网页内容,BOM关注的是浏览器窗口本身及其各种特性,例如导航、窗口大小、浏览器历史记录等等。就是JavaScript将浏览器的各个组成部分封装成了对......
  • JavaScript 实现对 JSON 对象数组数据进行分页处理
    JavaScript实现对JSON对象数组数据进行分页处理在前端JavaScript中对JSON对象数组进行分页,可以通过以下方式实现:分页函数示例代码假设有一组JSON对象数据,比如一组用户信息:constdata=[{id:1,name:"Alice"},{id:2,name:"Bob"},{id:3,name:"......
  • 时间序列分析:一种二次指数平滑法构建的纺织生产布料年产量线性预测模型 | 基于SQL语言
    目录0问题描述1 符号规定与基本假设 2模型的分析与建立 3模型的求解【基于SQL语言实现】3.1数据准备3.2问题分析步骤1:计算初始值。步骤2:计算一次平滑值。步骤3:计算二次平滑值 步骤4:计算直线趋势模型的系数 及步骤5:构建线性预测模型进行结果预测3.3结......
  • 顺序存取与随机存取
    一、顺序存取方法1、定义:顺序存取把文件内容存储在连续的物理块或逻辑块中,方便按照文件中记录的排列顺序依次进行存取。读取文件时,从文件的起始位置开始,逐个记录(或字节)地读取,直到所需的位置。后一次存取总是在前一次存取的基础上进行,因此不必给出具体的存储位置。就像我们阅......