参考文档
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-curve#curvesinterpolatingspring10
可视化工具网站
https://easingwizard.com/
https://www.desmos.com/calculator/k01p40v0ct?lang=zh-CN
基本介绍
import { curves } from '@kit.ArkUI'
curves.interpolatingSpring(10, 1, 228, 30) // 创建一个时长由弹簧参数决定的弹簧插值曲线
/**
代码核心功能:
该代码片段用于创建一个弹簧插值曲线,曲线的时长由弹簧参数决定。这在动画或界面过渡效果中非常有用,可以根据物理模拟的弹簧运动来平滑过渡。
代码逻辑走读:
1. 导入模块:代码首先从`@kit.ArkUI`模块中导入`curves`对象,这个对象包含了各种用于动画和过渡效果的函数和方法。
2. 创建弹簧插值曲线:调用`curves.interpolatingSpring`方法,传入四个参数:`10`(初始位置)、`1`(初始速度)、`228`(弹簧常数)和`30`(摩擦常数)。这个方法根据这些参数创建一个弹簧插值曲线,曲线的时长和形状由这些参数定义。
3. 曲线应用:生成的曲线可以用于界面元素的动画效果,使其在移动或变化时遵循弹簧运动的物理规律,从而实现平滑、自然的过渡。
本次解答由人工智能生成,仅供参考
*/
- damping(阻尼):控制弹簧震荡的衰减速度。
阻尼值越小,弹簧震荡次数越多,衰减越慢(如软弹簧,弹性强);
阻尼值越大,震荡越快停止,甚至可能无明显回弹(如硬弹簧,接近刚性)。 - stiffness(刚度 / 劲度系数):控制弹簧的 “硬度”。
刚度越大,弹簧越 “硬”,运动速度快、回弹幅度小(如金属弹簧);
刚度越小,弹簧越 “软”,运动更平缓、回弹幅度大(如橡胶弹簧)。 - mass(质量):模拟被弹簧拉动的物体质量(部分实现中默认固定值)。
质量越大,动画启动和停止的惯性越强,运动更迟缓;质量越小,响应越灵敏。 - initialVelocity(初始速度):动画开始时的初始运动速度,影响初始震荡的幅度(如快速滑动后的惯性回弹)。
from(起始值) 与 to(目标值):定义动画的起始状态和最终稳定的目标状态(如位置、大小、透明度等属性值)。
代码
弹簧曲线动画
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from scipy.interpolate import make_interp_spline
import matplotlib.widgets as widgets
# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "Heiti TC", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False # 正确显示负号
class ParametricSpringAnimation:
def __init__(self):
# 物理参数(默认值)
self.initial_velocity = -2.0 # 初始速度(负值表示向左运动)
self.mass = 1.0 # 质量
self.stiffness = 5.0 # 刚度(劲度系数)
self.damping = 0.5 # 阻尼系数
# 弹簧基本参数
self.start_point = np.array([2, 5]) # 弹簧固定端
self.equilibrium_pos = 8 # 平衡位置X坐标
self.spring_coils = 12 # 弹簧圈数
self.coil_height = 0.6 # 线圈高度
# 状态变量
self.current_pos = self.equilibrium_pos # 当前位置
self.current_vel = self.initial_velocity # 当前速度
self.time = 0.0 # 时间
# 动画参数
self.total_frames = 300 # 总帧数
self.fps = 60 # 帧率
self.dt = 1.0 / self.fps # 时间步长
# 创建图形和轴
self.fig = plt.figure(figsize=(12, 8))
self.ax = self.fig.add_axes([0.1, 0.3, 0.8, 0.6]) # 主绘图区
self.fig.suptitle('参数可控的插值弹簧曲线动画', fontsize=16)
# 设置主坐标轴范围
self.ax.set_xlim(0, 12)
self.ax.set_ylim(2, 12)
self.ax.set_xlabel('X轴')
self.ax.set_ylabel('Y轴')
self.ax.grid(True, alpha=0.3)
self.ax.set_aspect('equal', adjustable='box')
# 初始化绘图元素
self.spring_line, = self.ax.plot([], [], 'b-', linewidth=3) # 弹簧曲线
self.fixed_point, = self.ax.plot([], [], 'ro', markersize=10) # 固定端点
self.mass_point, = self.ax.plot([], [], 'go', markersize=15) # 重物
self.trace_line, = self.ax.plot([], [], 'r-', linewidth=1, alpha=0.3) # 轨迹线
# 轨迹记录
self.trace_points = []
# 信息文本
self.info_text = self.ax.text(0.02, 0.98, '', transform=self.ax.transAxes,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
# 添加参数控制面板
self.add_parameter_controls()
def add_parameter_controls(self):
"""添加参数控制滑块"""
# 初始速度滑块
ax_vel = self.fig.add_axes([0.2, 0.2, 0.65, 0.03])
self.vel_slider = widgets.Slider(
ax=ax_vel,
label='初始速度',
valmin=-5.0,
valmax=5.0,
valinit=self.initial_velocity,
valstep=0.1
)
# 质量滑块
ax_mass = self.fig.add_axes([0.2, 0.15, 0.65, 0.03])
self.mass_slider = widgets.Slider(
ax=ax_mass,
label='质量',
valmin=0.1,
valmax=5.0,
valinit=self.mass,
valstep=0.1
)
# 刚度滑块
ax_stiff = self.fig.add_axes([0.2, 0.1, 0.65, 0.03])
self.stiff_slider = widgets.Slider(
ax=ax_stiff,
label='刚度',
valmin=1.0,
valmax=20.0,
valinit=self.stiffness,
valstep=0.5
)
# 阻尼滑块
ax_damp = self.fig.add_axes([0.2, 0.05, 0.65, 0.03])
self.damp_slider = widgets.Slider(
ax=ax_damp,
label='阻尼',
valmin=0.1,
valmax=2.0,
valinit=self.damping,
valstep=0.1
)
# 重置按钮
ax_reset = self.fig.add_axes([0.85, 0.05, 0.1, 0.04])
self.reset_btn = widgets.Button(ax_reset, '重置')
# 绑定事件处理函数
self.vel_slider.on_changed(self.update_parameters)
self.mass_slider.on_changed(self.update_parameters)
self.stiff_slider.on_changed(self.update_parameters)
self.damp_slider.on_changed(self.update_parameters)
self.reset_btn.on_clicked(self.reset_animation)
def init_animation(self):
"""初始化动画元素"""
self.spring_line.set_data([], [])
self.fixed_point.set_data([], [])
self.mass_point.set_data([], [])
self.trace_line.set_data([], [])
self.info_text.set_text('')
return self.spring_line, self.fixed_point, self.mass_point, self.trace_line, self.info_text
def update_parameters(self, val):
"""更新物理参数"""
self.initial_velocity = self.vel_slider.val
self.mass = self.mass_slider.val
self.stiffness = self.stiff_slider.val
self.damping = self.damp_slider.val
self.reset_animation(None) # 参数改变后重置动画
def reset_animation(self, event):
"""重置动画状态"""
self.current_pos = self.equilibrium_pos
self.current_vel = self.initial_velocity
self.time = 0.0
self.trace_points = []
def generate_spring_points(self, end_x):
"""生成弹簧上的点并进行插值平滑"""
end_point = np.array([end_x, self.start_point[1]])
# 计算弹簧总长度和方向
length = np.linalg.norm(end_point - self.start_point)
# 生成弹簧的控制点
t = np.linspace(0, 1, self.spring_coils * 2 + 1)
x = self.start_point[0] + t * (end_point[0] - self.start_point[0])
# 生成弹簧的波动形状
y = self.start_point[1] + np.sin(t * self.spring_coils * 2 * np.pi) * self.coil_height
# 使用三次样条插值使曲线更平滑
spl = make_interp_spline(t, np.column_stack((x, y)), k=3)
t_smooth = np.linspace(0, 1, 200) # 更密集的点
smooth_points = spl(t_smooth)
return smooth_points
def calculate_physics(self):
"""根据物理规律计算下一帧状态"""
# 胡克定律:F = -k(x - x0) - c*v
displacement = self.current_pos - self.equilibrium_pos
force = -self.stiffness * displacement - self.damping * self.current_vel
# 牛顿第二定律:a = F/m
acceleration = force / self.mass
# 更新速度和位置
self.current_vel += acceleration * self.dt
self.current_pos += self.current_vel * self.dt
# 限制位置范围,防止弹簧过度拉伸
if self.current_pos < self.start_point[0] + 1.0:
self.current_pos = self.start_point[0] + 1.0
self.current_vel = 0.0
if self.current_pos > 11.0:
self.current_pos = 11.0
self.current_vel = 0.0
self.time += self.dt
def update_animation(self, frame):
"""更新动画帧"""
# 计算物理状态
self.calculate_physics()
# 生成弹簧曲线点
spring_points = self.generate_spring_points(self.current_pos)
# 更新弹簧曲线
self.spring_line.set_data(spring_points[:, 0], spring_points[:, 1])
# 更新固定端点
self.fixed_point.set_data(self.start_point[0], self.start_point[1])
# 更新重物位置
self.mass_point.set_data(self.current_pos, self.start_point[1])
# 更新轨迹
self.trace_points.append([self.time, self.current_pos - self.equilibrium_pos])
if len(self.trace_points) > 1000: # 限制轨迹点数量
self.trace_points.pop(0)
# 绘制轨迹
if len(self.trace_points) > 1:
trace_array = np.array(self.trace_points)
self.trace_line.set_data(trace_array[:, 0], trace_array[:, 1] + self.equilibrium_pos)
# 更新信息文本
displacement = self.current_pos - self.equilibrium_pos
self.info_text.set_text(
f'时间: {self.time:.1f}s\n'
f'位移: {displacement:.2f}\n'
f'速度: {self.current_vel:.2f}'
)
return self.spring_line, self.fixed_point, self.mass_point, self.trace_line, self.info_text
def create_animation(self, save_path=None):
"""创建并显示动画"""
anim = FuncAnimation(
self.fig,
self.update_animation,
frames=self.total_frames,
init_func=self.init_animation,
interval=1000/self.fps, # 每帧间隔毫秒
blit=True,
repeat=True # 动画循环播放
)
# 修复matplotlib版本兼容性问题
anim._resize_id = None
# 如果提供了保存路径,则保存动画
if save_path:
try:
anim.save(save_path, writer='ffmpeg', fps=self.fps)
print(f"动画已保存至: {save_path}")
except Exception as e:
print(f"保存动画失败: {e}")
print("请确保已安装ffmpeg")
plt.show()
if __name__ == "__main__":
# 创建并显示弹簧动画
spring_anim = ParametricSpringAnimation()
# 如需保存动画,取消下面一行的注释并指定路径
spring_anim.create_animation('parametric_spring_animation.mp4')
# 显示动画
spring_anim.create_animation()
贝塞尔曲线动画
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import matplotlib.animation as animation
# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False # 正确显示负号
class BezierAnimation:
def __init__(self):
# 初始化控制点 - 可以修改这些点来获得不同的曲线
self.controls = np.array([
[0, 0], # 起点
[2, 0], # 控制点1
[2, 5], # 控制点2
[5, 5] # 终点
])
self.num_points = 100 # 曲线上的点数量
self.t = np.linspace(0, 1, self.num_points) # 参数t从0到1
# 创建图形和轴
self.fig, self.ax = plt.subplots(figsize=(8, 6))
self.fig.suptitle('贝塞尔曲线动画演示', fontsize=15)
# 设置坐标轴范围
self.ax.set_xlim(-1, 6)
self.ax.set_ylim(-1, 5)
self.ax.set_xlabel('X轴')
self.ax.set_ylabel('Y轴')
self.ax.grid(True)
# 初始化绘图元素
self.control_line, = self.ax.plot([], [], 'r--', alpha=0.6) # 控制点连接线
self.control_points, = self.ax.plot([], [], 'ro', markersize=8) # 控制点
self.bezier_curve, = self.ax.plot([], [], 'b-', linewidth=2) # 贝塞尔曲线
self.animated_point, = self.ax.plot([], [], 'go', markersize=10) # 曲线上的动画点
# 添加控制点标签
self.control_labels = [self.ax.text(0, 0, '', fontsize=12) for _ in range(len(self.controls))]
# 动画帧数量
self.animation_frames = 100
def bezier_curve_calc(self, t):
"""计算贝塞尔曲线上的点"""
n = len(self.controls) - 1 # 曲线阶数 = 控制点数量 - 1
result = np.zeros(2)
for i in range(n + 1):
# 计算二项式系数
binom = np.math.comb(n, i)
# 计算贝塞尔基函数
basis = binom * (t ** i) * ((1 - t) ** (n - i))
# 累加计算曲线上的点
result += basis * self.controls[i]
return result
def init_animation(self):
"""初始化动画"""
self.control_line.set_data([], [])
self.control_points.set_data([], [])
self.bezier_curve.set_data([], [])
self.animated_point.set_data([], [])
for label in self.control_labels:
label.set_text('')
return (self.control_line, self.control_points, self.bezier_curve,
self.animated_point, *self.control_labels)
def update_animation(self, frame):
"""更新动画帧"""
# 计算当前帧对应的t值
current_t = frame / self.animation_frames
# 更新控制点显示
self.control_points.set_data(self.controls[:, 0], self.controls[:, 1])
self.control_line.set_data(self.controls[:, 0], self.controls[:, 1])
# 更新控制点标签
for i, label in enumerate(self.control_labels):
label.set_position((self.controls[i, 0] + 0.1, self.controls[i, 1] + 0.1))
label.set_text(f'P{i}')
# 计算当前t值范围内的贝塞尔曲线
curve_points = np.array([self.bezier_curve_calc(t) for t in self.t if t <= current_t])
if len(curve_points) > 0:
self.bezier_curve.set_data(curve_points[:, 0], curve_points[:, 1])
# 更新动画点(当前t对应的点)
current_point = self.bezier_curve_calc(current_t)
self.animated_point.set_data(current_point[0], current_point[1])
return (self.control_line, self.control_points, self.bezier_curve,
self.animated_point, *self.control_labels)
def create_animation(self, save_path=None):
"""创建并显示动画"""
anim = FuncAnimation(
self.fig,
self.update_animation,
frames=self.animation_frames + 1,
init_func=self.init_animation,
interval=50, # 每帧间隔毫秒
blit=True
)
# 如果提供了保存路径,则保存动画
if save_path:
# 需要安装ffmpeg才能保存为mp4
anim.save(save_path, writer='ffmpeg', fps=20)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
# 创建并显示贝塞尔曲线动画
bezier_anim = BezierAnimation()
# 如需保存动画,取消下面一行的注释并指定路径
# 保存并显示动画
bezier_anim.create_animation('bezier_animation.mp4')
# 显示动画
bezier_anim.create_animation()
弹簧动画模拟器
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>弹簧曲线模拟器</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
accent: '#8B5CF6',
dark: '#1E293B',
light: '#F8FAFC'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.card-shadow {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
}
.slider-thumb {
@apply appearance-none w-5 h-5 rounded-full bg-primary cursor-pointer;
}
.fixed-dimension {
width: 400px;
height: 200px;
flex-shrink: 0;
overflow: hidden;
}
}
input[type="range"]::-webkit-slider-thumb {
@apply slider-thumb;
}
input[type="range"]::-moz-range-thumb {
@apply slider-thumb;
}
</style>
</head>
<body class="bg-gray-50 font-sans text-dark">
<div class="container mx-auto px-4 py-8 max-w-5xl">
<header class="text-center mb-10">
<h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-dark mb-2">弹簧曲线模拟器</h1>
<p class="text-gray-600 max-w-2xl mx-auto">调整参数以模拟不同弹簧特性,实时查看位移-时间曲线</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- 控制面板 -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl-6 card-shadowshadow">
<h2 class="text-xl font-semibold mb-6 flex items-center">
<i class="fa fa-sliders text-primary mr-2"></i>参数控制
</h2>
<div class="space-y-6">
<!-- 初始速度 -->
<div>
<div class="flex justify-between mb-1">
<label for="velocity" class="text-sm font-medium text-gray-700">初始速度</label>
<span id="velocity-value" class="text-sm font-medium text-primary">-10</span>
</div>
<input
type="range"
id="velocity"
min="-50"
max="50"
value="-10"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>-50</span>
<span>0</span>
<span>50</span>
</div>
</div>
<!-- 质量 -->
<div>
<div class="flex justify-between mb-1">
<label for="mass" class="text-sm font-medium text-gray-700">质量</label>
<span id="mass-value" class="text-sm font-medium text-primary">1.0</span>
</div>
<input
type="range"
id="mass"
min="0.1"
max="5"
step="0.1"
value="1.0"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>0.1</span>
<span>2.5</span>
<span>5.0</span>
</div>
</div>
<!-- 刚度 -->
<div>
<div class="flex justify-between mb-1">
<label for="stiffness" class="text-sm font-medium text-gray-700">刚度</label>
<span id="stiffness-value" class="text-sm font-medium text-primary">100</span>
</div>
<input
type="range"
id="stiffness"
min="10"
max="500"
value="100"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>10</span>
<span>250</span>
<span>500</span>
</div>
</div>
<!-- 阻尼 -->
<div>
<div class="flex justify-between mb-1">
<label for="damping" class="text-sm font-medium text-gray-700">阻尼</label>
<span id="damping-value" class="text-sm font-medium text-primary">10</span>
</div>
<input
type="range"
id="damping"
min="0"
max="50"
value="10"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>0</span>
<span>25</span>
<span>50</span>
</div>
</div>
<!-- 初始位置 -->
<div>
<div class="flex justify-between mb-1">
<label for="position" class="text-sm font-medium text-gray-700">初始位置</label>
<span id="position-value" class="text-sm font-medium text-primary">100</span>
</div>
<input
type="range"
id="position"
min="-200"
max="200"
value="100"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>-200</span>
<span>0</span>
<span>200</span>
</div>
</div>
<!-- 动画速度 -->
<div>
<div class="flex justify-between mb-1">
<label for="speed" class="text-sm font-medium text-gray-700">动画速度</label>
<span id="speed-value" class="text-sm font-medium text-primary">1.0x</span>
</div>
<input
type="range"
id="speed"
min="0.1"
max="3"
step="0.1"
value="1.0"
class="w-full h-2 bg-gray-200 rounded-lg"
>
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>慢速</span>
<span>正常</span>
<span>快速</span>
</div>
</div>
<div class="pt-4">
<button id="simulate-btn" class="w-full bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-lg transition-allduration-300 flex items-center justify-center">
<i class="fa fa-play mr-2"></i> 开始模拟
</button>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 mt-6 card-shadow">
<h2 class="text-xl font-semibold mb-4 flex items-center">
<i class="fa fa-info-circle text-accent mr-2"></i>参数说明
</h2>
<ul class="text-sm text-gray-600 space-y-2">
<li class="flex items-start">
<i class="fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>初始速度</strong>:物体开始运动的速度,正值向右,负值向左</span>
</li>
<li class="flex items-start">
<i class="fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>质量</strong>:物体的质量,越大惯性越大</span>
</li>
<li class="flex items-start">
<i class="fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>刚度</strong>:弹簧的硬度,越大弹簧越硬</span>
</li>
<li class="flex items-start">
<i class="fa fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>阻尼</strong>:阻力大小,越大震荡衰减越快</span>
</li>
<li class="flex items-start">
<i class="fa fa fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>初始位置</strong>:物体的起始位置,偏离平衡位置的距离</span>
</li>
<li class="flex items-start">
<i class="fa fa-arrow-right text-primary mt-1 mr-2"></i>
<span><strong>动画速度</strong>:控制动画播放速度,1.0x为正常速度</span>
</li>
</ul>
</div>
</div>
<!-- 可视化区域 -->
<div class="lg:col-span-2">
<div class="bg-white rounded-xl p-6 card-shadow">
<!-- 弹簧动画动画演示(固定大小) -->
<div class="mb-6">
<h2 class="text-lg font-semibold mb-2 flex items-center">
<i class="fa fa-film text-secondary mr-2"></i>弹簧动画
</h2>
<div class="fixed-dimension border borderborderborderborderborder-gray-200 rounded-lg bg-gray-50 relative">
<div id="spring-container" class="absolute inset-0 flex items-center px-4">
<!-- 墙面 -->
<div class="w-3 h-12 bg-gray-400 rounded-sm"></div>
<!-- 弹簧 -->
<div id="spring" class="flex-1 h-3 flex justifyjustify-between items-centeritems-center mx-1">
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
<div class="h-full w-1 bg-primary"></div>
</div>
<!-- 物体 -->
<div id="mass-object" class="w-12 h-12 bg-accent rounded-full-full-fullflexitemsitemsitemsitems-centertext-white font-bold text-sm">
m
</div>
</div>
</div>
</div>
<!-- 曲线图 -->
<div>
<h2 class="text-lg font-semibold mb-2 flex items-center">
<i class="fa fa-line-chart text-primary mr-2"></i>位移-时间曲线
</h2>
<div class="fixed-dimension border border-gray-200 rounded-lg">
<canvas id="spring-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<footer class="mt-12 text-center text-gray-500 text-sm">
<p>弹簧曲线曲线模拟器基于胡克定律模拟:F = -kx - cv</p>
</footer>
</div>
<script>
// 获取DOM元素
const velocitySlider = document.getElementById('velocity');
const velocityValue = document.getElementById('velocity-value');
const massSlider = document.getElementById('mass');
const massValue = document.getElementById('mass-value');
const stiffnessSlider = document.getElementById('stiffness');
const stiffnessValue = document.getElementById('stiffness-value');
const dampingSlider = document.getElementById('damping');
const dampingValue = document.getElementById('damping-value');
const positionSlider = document.getElementById('position');
const positionValue = document.getElementById('position-value');
const speedSlider = document.getElementById('speed');
const speedValue = document.getElementById('speed-value');
const simulateBtn = document.getElementById('simulate-btn');
const massObject = document.getElementById('mass-object');
const springContainer = document.getElementById('spring-container');
// 更新显示的参数值
velocitySlider.addEventListener('input', () => {
velocityValue.textContent = velocitySlider.value;
});
massSlider.addEventListener('input', () => {
massValue.textContent = parseFloat(massSlider.value).toFixed(1);
});
stiffnessSlider.addEventListener('input', () => {
stiffnessValue.textContent = stiffnessSlider.value;
});
dampingSlider.addEventListener('input', () => {
dampingValue.textContent = dampingSlider.value;
});
positionSlider.addEventListener('input', () => {
positionValue.textContent = positionSlider.value;
});
speedSlider.addEventListener('input', () => {
speedValue.textContent = parseFloat(speedSlider.value).toFixed(1) + 'x';
// 如果模拟正在运行,实时时更新速度
if (simulation && simulation.isRunning) {
simulation.speedFactor = parseFloat(speedSlider.value);
}
});
// 初始化图表
const ctx = document.getElementById('spring-chart').getContext('2d');
let springChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '位移',
data: [],
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.1,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
title: {
display: true,
text: '时间 (ms)',
font: {
size: 10
}
},
ticks: {
font: {
size: 8
}
}
},
y: {
title: {
display: true,
text: '位移',
font: {
size: 10
}
},
min: -250,
max: 250,
ticks: {
font: {
size: 8
}
}
}
},
animation: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: {
labels: {
font: {
size: 10
}
}
}
}
}
});
// 弹簧模拟类
class SpringSimulation {
constructor(params) {
// 物理参数
this.stiffness = params.stiffness; // 刚度
this.damping = params.damping; // 阻尼
this.mass = params.mass; // 质量
this.initialPosition = params.position; // 初始位置
this.initialVelocity = params.velocity; // 初始速度
this.speedFactor = params.speed || 1.0; // 动画速度因子
// 状态变量
this.position = params.position; // 当前位置
this.velocity = params.velocity; // 当前速度
this.time = 0; // 时间
this.history = []; // 历史数据
this.isRunning = false; // 模拟是否运行
this.animationFrameId = null; // 动画帧ID
this.lastTime = 0; // 上一帧时间
// 固定容器宽度(400px减去内边距和元素宽度)
this.containerWidth = 400 - 30 - 48; // 固定计算,不受窗口影响
this.centerX = this.containerWidth / 2; // 平衡位置
}
// 更新模拟状态
update(currentTime) {
if (!this.lastTime) this.lastTime = currentTime;
// 应用速度因子调整时间增量
const deltaTime = ((currentTime - this.lastTime) / 1000) * this.speedFactor;
this.lastTime = currentTime;
// 计算加速度: F = -kx - cv, a = F/m
const acceleration = (-this.stiffness * this.position - this.damping * this.velocity) / this.mass;
// 更新速度和位置
this.velocity += acceleration * deltaTime;
this.position += this.velocity * deltaTime;
// 记录时间和位置
this.time += (currentTime - (this.lastTime - (currentTime - this.lastTime))) / 1000 * 1000;
this.history.push({
time: this.time,
position: this.position
});
// 更新物体位置
const objectX = this.centerX + this.position;
massObject.style.transform = `translateX(${objectX}px)`;
// 检查是否应该停止模拟
if (Math.abs(this.velocity) < 0.1 && Math.abs(this.position) < 0.5) {
this.stop();
return false;
}
return true;
}
// 开始模拟
start() {
this.isRunning = true;
this.lastTime = 0;
this.history = [];
this.time = 0;
// 重置图表
springChart.data.labels = [];
springChart.data.datasets[0].data = [];
springChart.update();
// 初始位置
const initialX = this.centerX + this.initialPosition;
massObject.style.transform = `translateX(${initialX}px)`;
// 动画循环
const animate = (timestamp) => {
if (!this.isRunning) return;
const shouldContinue = this.update(timestamp);
// 更新图表
if (this.history.length % 2 === 0) {
springChart.data.labels.push(Math.round(this.time));
springChart.data.datasets[0].data.push(this.position);
// 限制图表数据点数量
if (springChart.data.labels.length > 100) {
springChart.data.labels.shift();
springChart.data.datasets[0].data.shift();
}
springChart.update();
}
if (shouldContinue) {
this.animationFrameId = requestAnimationFrame(animate);
}
};
this.animationFrameId = requestAnimationFrame(animate);
}
// 停止模拟
stop() {
this.isRunning = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
}
// 模拟控制
let simulation = null;
simulateBtn.addEventListener('click', () => {
// 如果已有模拟在运行,先停止
if (simulation && simulation.isRunning) {
simulation.stop();
}
// 获取参数
const params = {
velocity: parseFloat(velocitySlider.value),
mass: parseFloat(massSlider.value),
stiffness: parseFloat(stiffnessSlider.value),
damping: parseFloat(dampingSlider.value),
position: parseFloat(positionSlider.value),
speed: parseFloat(speedSlider.value)
};
// 创建并启动新模拟
simulation = new SpringSimulation(params);
simulation.start();
// 更新按钮文本
simulateBtn.innerHTML = '<i class="fa fa-refresh mr-2"></i> 重新模拟';
});
// 初始位置设置
window.addEventListener('load', () => {
const containerWidth = 400 - 30 - 48; // 固定值
const centerX = containerWidth / 2;
const initialX = centerX + parseFloat(positionSlider.value);
massObject.style.transform = `translateX(${initialX}px)`;
});
// 移除窗口大小变化的影响
window.removeEventListener('resize', () => {});
</script>
</body>
</html>
弹簧曲线curves.interpolatingSpring(10, 1, 228, 30)
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False # 正确显示负号
def calculate_spring_curve(mass, stiffness, damping, initial_velocity, duration=1.0, fps=100):
"""计算弹簧曲线的数值点"""
dt = 1.0 / fps
total_frames = int(duration * fps)
time_points = []
position_points = []
velocity_points = []
position = 0.0 # 初始位置在平衡位置
velocity = initial_velocity
for _ in range(total_frames):
time = len(time_points) * dt
# 计算力和加速度 (F = -kx - cv, a = F/m)
force = -stiffness * position - damping * velocity
acceleration = force / mass
# 更新速度和位置
velocity += acceleration * dt
position += velocity * dt
time_points.append(time)
position_points.append(position)
velocity_points.append(velocity)
return np.array(time_points), np.array(position_points), np.array(velocity_points)
# 计算ArkUI interpolatingSpring(10, 1, 228, 30)的曲线数据
mass = 10.0
stiffness = 1.0
damping = 228.0
initial_velocity = 16.0
time, position, velocity = calculate_spring_curve(
mass, stiffness, damping, initial_velocity, duration=1.0
)
# 创建图形
fig, ax = plt.subplots(figsize=(10, 6))
fig.suptitle('curves.interpolatingSpring(10, 1, 228, 30) 数值曲线', fontsize=16)
# 绘制位移曲线
ax.plot(time, position, 'b-', linewidth=2, label='位移')
ax.set_xlabel('时间 (秒)')
ax.set_ylabel('位移')
ax.grid(True, alpha=0.3)
ax.set_xlim(0, max(time))
ax.set_ylim(min(position)*1.1, max(position)*1.1)
# 标记关键 points
peak1_idx = np.argmax(position)
peak1_time = time[peak1_idx]
peak1_pos = position[peak1_idx]
trough1_idx = np.argmin(position)
trough1_time = time[trough1_idx]
trough1_pos = position[trough1_idx]
# 添加关键点标注
ax.plot(peak1_time, peak1_pos, 'ro', markersize=8)
ax.annotate(f'峰值: ({peak1_time:.2f}s, {peak1_pos:.2f})',
xy=(peak1_time, peak1_pos),
xytext=(peak1_time+0.05, peak1_pos+0.2),
arrowprops=dict(arrowstyle='->', color='red'))
ax.plot(trough1_time, trough1_pos, 'go', markersize=8)
ax.annotate(f'谷值: ({trough1_time:.2f}s, {trough1_pos:.2f})',
xy=(trough1_time, trough1_pos),
xytext=(trough1_time+0.05, trough1_pos-0.3),
arrowprops=dict(arrowstyle='->', color='green'))
# 添加参数说明
param_text = (f'参数: mass={mass}, stiffness={stiffness}\n'
f'damping={damping}, initialVelocity={initial_velocity}')
plt.figtext(0.15, 0.01, param_text, fontsize=10,
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
# 添加图例
ax.legend()
plt.tight_layout(rect=[0, 0.05, 1, 0.95])
plt.show()