弹簧振子互动课件
这个程序要实现的是沪科版选择性必修一第二章 第二节 简谐运动的回复力和能量中的图 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,它的回答说还要区分是瞬间变化还是缓慢变化,比方说可以在小球运动的某个瞬间黏上一块物体,也可以让小球里的沙子慢慢漏出,很多物理量都会发生变化。
最终让我放弃数值积分方法的原因是:通常振幅会发生变化,在模拟过程中小球会撞墙甚至跑出屏幕之外,看起来不太美观。正因为如此,在小球运动的过程中,改变劲度系数和质量的滑动条会处于灰色状态不可拖动,否则按照这个程序的算法,你虽然能看到运动周期发生了明显变化,但振幅却保持不变,会出现科学性错误。
物理量 x、F、v、a 随时间变化的图像
编写这个程序最花时间的不是物理,而是如何绘制四个图像。在电磁波的传播互动课件一文中也提到过绘制行波的物理思路,但 x–t 图像思路有点不同。虽然两者都是正(余)弦曲线,但波的视觉感观是在平移,振动图像的视觉观感是在延伸。因此编程基本思路虽然相同——都是在一个数组中存储足够多的采样点,然后将这些点连接起来形成光滑曲线,但是波的情况下采样点的数量保持最大且不变,只随时间改变这些点的值,而振动图像采样点的数量随时间慢慢增加,为了防止点无限增加,可以在曲线超出图框范围后,在添加一个新点的同时删除数组中的第一个值,点的数量就保持不变了,而且这样做在视觉上也会有向右延伸的效果。
在程序中,在动画的每一帧都调用 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
