弹簧振子互动课件

这个程序要实现的是沪科版选择性必修一第二章 第二节 简谐运动的回复力和能量中的图 2–10。如下图所示。

图 2–10  弹簧振子受力分析

做出的成品如下所示:

核心代码解读

物理规律

直接使用了解析解,更新物理量的 updatePhysics 方法代码如下:

// 更新物理量
function updatePhysics(dt) {
    if (!state.isOscillating) return;

    // 使用简谐运动的振动方程
    // x = A * cos(ωt + φ)
    // v = -A * ω * sin(ωt + φ)
    // a = -A * ω² * cos(ωt + φ) = -ω² * x

    // 计算角频率 ω = √(k/m)
    const omega = Math.sqrt(state.k / state.m);

    // 更新时间
    state.t += dt;

    // 计算相位
    const phase = omega * state.t + state.phi;

    // 计算位移
    state.x = state.A * Math.cos(phase);

    // 计算速度
    state.v = -state.A * omega * Math.sin(phase);

    // 计算加速度
    state.a = -omega * omega * state.x;

    // 计算力 F = -kx
    const actualX = state.x * SCALE_FACTOR_M_PER_PX;
    state.F = -state.k * actualX;

    // 计算动能和势能
    const vInM = state.v * SCALE_FACTOR_M_PER_PX;  // 转换为米/秒
    state.Ek = 0.5 * state.m * vInM * vInM;  // 动能
    state.Ep = 0.5 * state.k * actualX * actualX;  // 势能
}

一开始用的是动力学方法进行数值积分,这样不仅可以模拟阻尼运动的情况,还能模拟在振动过程中改变劲度系数或小球质量的情况。后一种情况理论分析很难,问了一下 AI,它的回答说还要区分是瞬间变化还是缓慢变化,比方说可以在小球运动的某个瞬间黏上一块物体,也可以让小球里的沙子慢慢漏出,很多物理量都会发生变化。

最终让我放弃数值积分方法的原因是:通常振幅会发生变化,在模拟过程中小球会撞墙甚至跑出屏幕之外,看起来不太美观。正因为如此,在小球运动的过程中,改变劲度系数和质量的滑动条会处于灰色状态不可拖动,否则按照这个程序的算法,你虽然能看到运动周期发生了明显变化,但振幅却保持不变,会出现科学性错误。

物理量 xFva 随时间变化的图像

编写这个程序最花时间的不是物理,而是如何绘制四个图像。在电磁波的传播互动课件一文中也提到过绘制行波的物理思路,但 xt 图像思路有点不同。虽然两者都是正(余)弦曲线,但波的视觉感观是在平移,振动图像的视觉观感是在延伸。因此编程基本思路虽然相同——都是在一个数组中存储足够多的采样点,然后将这些点连接起来形成光滑曲线,但是波的情况下采样点的数量保持最大且不变,只随时间改变这些点的值,而振动图像采样点的数量随时间慢慢增加,为了防止点无限增加,可以在曲线超出图框范围后,在添加一个新点的同时删除数组中的第一个值,点的数量就保持不变了,而且这样做在视觉上也会有向右延伸的效果。

在程序中,在动画的每一帧都调用 addDataPoint 方法将当前时间 t、位移 x、力 F、加速度 a、速度v 的数据添加到对应数组中。设定最多记录 timeWindow = 4π 秒的数据,因此最多记录 754 个数据(4π 秒*60 帧/秒),之后就不再增加,而是添加 1 个,删除最前面的一个。具体代码如下:

// 添加数据点(包含滚动逻辑)
function addDataPoint(t, x, F, a, v) {
    // 添加新数据点
    dataPoints.t.push(t);
    dataPoints.x.push(x);
    dataPoints.F.push(F);
    dataPoints.a.push(a);
    dataPoints.v.push(v);

    // 当时间超过时间窗口时,删除最早的数据点,直到最前面的数据点的时间大于等于当前时间减去时间窗口
    while (dataPoints.t.length > 0 && t - dataPoints.t[0] > graphConfig.timeWindow) {
        dataPoints.t.shift();
        dataPoints.x.shift();
        dataPoints.F.shift();
        dataPoints.a.shift();
        dataPoints.v.shift();
    }
}

有了这些数据点,就继续调用 drawGraph 方法将这些点的坐标映射到屏幕的像素坐标,然后连接成一条光滑的曲线,代码如下:

// 绘制通用图像(基于时间数组t和物理量数据)
function drawGraph(graphElement, timeData, valueData, color, maxValue) {
    if (timeData.length < 1 || valueData.length < 1) return;

    const width = graphConfig.width;
    const height = graphConfig.height;
    const centerY = height / 2;
    const timeWindow = graphConfig.timeWindow;

    // 计算缩放因子:最大值对应到图像顶部(留出一点边距)
    const yScale = centerY / maxValue;

    // 找到当前显示的时间范围
    const currentTime = timeData[timeData.length - 1];
    let startTime = 0;

    // 如果当前时间超过时间窗口,只显示最近的一个时间窗口
    if (currentTime > timeWindow) {
        startTime = currentTime - timeWindow;
    }

    // 绘制数组中的所有数据点
    let path = '';
    for (let i = 0; i < timeData.length; i++) {
    // x坐标:时间在时间窗口内的相对位置
    const relativeTime = timeData[i] - startTime;
    const x = (relativeTime / timeWindow) * width;
    // y坐标:物理量值根据maxValue缩放,中心线为0
    const y = centerY - valueData[i] * yScale;

    if (i == 0) {
        path = `M ${x} ${y}`;
        } else {
            path += ` L ${x} ${y}`;
        }
    }

    graphElement.setAttribute('d', path);
    graphElement.setAttribute('stroke', color);
}

完整代码

 

发布时间:2026/3/5 下午7:37:15  阅读次数:24

2006 - 2026,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号