线速度公式推导动画
by EpicCoder88284 lines8.3 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>线速度公式推导动画</title>
<style>
body { margin:0; background:#0a0e14; overflow:hidden; display:flex; flex-direction:column; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; color:#fff; }
canvas { background:#111; border-radius:12px; box-shadow: 0 0 40px rgba(88,166,255,0.15); }
.controls { position:absolute; bottom:30px; display:flex; gap:10px; }
button { padding:8px 16px; background:#1c2128; border:1px solid #30363d; color:#fff; border-radius:6px; cursor:pointer; }
button:hover { background:#22272e; }
</style>
</head>
<body>
<canvas id="canvas" width="900" height="550"></canvas>
<div class="controls">
<button onclick="restartAnimation()">重新播放</button>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height / 2;
let startTime = null;
const duration = 9000; // 总时长 9 秒
// 颜色配置
const colors = {
white: '#e6edf3',
blue: '#58a6ff',
green: '#3fb950',
red: '#f85149',
orange: '#f0883e',
dim: '#8b949e'
};
// 定义动画步骤的时间节点(毫秒)
const timeline = {
formula1: 0, // 0s: 显示万有引力
formula2: 2000, // 2s: 显示向心力
equate: 3000, // 3s: 联立等式
cancelM: 4500, // 4.5s: 消去 m
cancelR: 5500, // 5.5s: 消去 r
solveV: 7000, // 7s: 得到 v^2 = GM/r
final: 8000 // 8s: 开根号,得到最终公式
};
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function drawText(text, x, y, color, size = 36, align = "center") {
ctx.fillStyle = color;
ctx.font = `bold ${size}px 'Times New Roman', serif`;
ctx.textAlign = align;
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function drawFraction(numerator, denominator, x, y, color, lineColor = colors.white) {
ctx.fillStyle = color;
ctx.font = "bold 36px 'Times New Roman', serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// 分子
ctx.fillText(numerator, x, y - 25);
// 分数线
ctx.beginPath();
ctx.moveTo(x - 50, y);
ctx.lineTo(x + 50, y);
ctx.strokeStyle = lineColor;
ctx.lineWidth = 2;
ctx.stroke();
// 分母
ctx.fillText(denominator, x, y + 25);
}
// 绘制行星星轨(动态背景)
function drawOrbitBackground(progress) {
ctx.beginPath();
ctx.arc(cx, cy, 200, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(88, 166, 255, 0.1)";
ctx.lineWidth = 1;
ctx.stroke();
// 中心天体
ctx.beginPath();
ctx.arc(cx, cy, 15, 0, Math.PI * 2);
ctx.fillStyle = "rgba(240, 136, 62, 0.8)";
ctx.shadowColor = colors.orange;
ctx.shadowBlur = 20;
ctx.fill();
ctx.shadowBlur = 0;
// 运动的小行星(线速度的具象化)
let angle = progress * Math.PI * 4; // 转两圈
let px = cx + 200 * Math.cos(angle);
let py = cy + 200 * Math.sin(angle);
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fillStyle = colors.blue;
ctx.shadowColor = colors.blue;
ctx.shadowBlur = 10;
ctx.fill();
ctx.shadowBlur = 0;
// 绘制速度方向向量(切向箭头)
let vx = -Math.sin(angle);
let vy = Math.cos(angle);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + vx * 40, py + vy * 40);
ctx.strokeStyle = colors.green;
ctx.lineWidth = 2;
ctx.stroke();
// 箭头
let arrowAngle = Math.atan2(vy, vx);
ctx.beginPath();
ctx.moveTo(px + vx * 40, py + vy * 40);
ctx.lineTo(px + vx * 40 - 10 * Math.cos(arrowAngle - 0.5), py + vy * 40 - 10 * Math.sin(arrowAngle - 0.5));
ctx.lineTo(px + vx * 40 - 10 * Math.cos(arrowAngle + 0.5), py + vy * 40 - 10 * Math.sin(arrowAngle + 0.5));
ctx.closePath();
ctx.fillStyle = colors.green;
ctx.fill();
}
function animate(time) {
if (!startTime) startTime = time;
let elapsed = time - startTime;
let progress = Math.min(elapsed / duration, 1);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景轨道
drawOrbitBackground(progress);
// 第一步:显示万有引力(左侧)
if (elapsed >= timeline.formula1) {
let p = Math.min((elapsed - timeline.formula1) / 500, 1);
ctx.globalAlpha = easeOutCubic(p);
drawText("万有引力:", cx - 280, cy - 80, colors.dim, 24, "left");
drawText("F =", cx - 180, cy, colors.white);
drawFraction("G · M · m", "r²", cx - 80, cy, colors.blue);
ctx.globalAlpha = 1;
}
// 第二步:显示圆周运动向心力(右侧)
if (elapsed >= timeline.formula2) {
let p = Math.min((elapsed - timeline.formula2) / 500, 1);
ctx.globalAlpha = easeOutCubic(p);
drawText("向心力:", cx + 60, cy - 80, colors.dim, 24, "left");
drawText("F =", cx + 120, cy, colors.white);
drawFraction("m · v²", "r", cx + 220, cy, colors.green);
ctx.globalAlpha = 1;
}
// 第三步:联立等式,开始消去
if (elapsed >= timeline.equate) {
let p = Math.min((elapsed - timeline.equate) / 500, 1);
ctx.globalAlpha = easeOutCubic(p);
// 居中放置等号
drawText("=", cx, cy, colors.white, 48);
// 把左右两侧公式向中间靠拢(通过平移画布实现)
ctx.save();
ctx.translate(-40 * easeOutCubic(p), 0);
drawFraction("G · M · m", "r²", cx - 120, cy, colors.blue);
ctx.restore();
ctx.save();
ctx.translate(40 * easeOutCubic(p), 0);
drawFraction("m · v²", "r", cx + 120, cy, colors.green);
ctx.restore();
// 标注提示
drawText("(两边质量 m 可以约掉)", cx, cy + 100, colors.dim, 18);
ctx.globalAlpha = 1;
}
// 第四步:消去 m 和 r
if (elapsed >= timeline.cancelM) {
// 画红色的叉,划掉 m
let p = Math.min((elapsed - timeline.cancelM) / 500, 1);
ctx.beginPath();
ctx.moveTo(cx - 150, cy - 45);
ctx.lineTo(cx - 130, cy - 25);
ctx.moveTo(cx - 130, cy - 45);
ctx.lineTo(cx - 150, cy - 25);
ctx.strokeStyle = colors.red;
ctx.lineWidth = 3;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + 130, cy - 45);
ctx.lineTo(cx + 150, cy - 25);
ctx.moveTo(cx + 150, cy - 45);
ctx.lineTo(cx + 130, cy - 25);
ctx.stroke();
}
if (elapsed >= timeline.cancelR) {
// 划掉分母的 r
ctx.beginPath();
ctx.moveTo(cx - 100, cy + 15);
ctx.lineTo(cx - 60, cy + 35);
ctx.moveTo(cx - 60, cy + 15);
ctx.lineTo(cx - 100, cy + 35);
ctx.strokeStyle = colors.red;
ctx.lineWidth = 3;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + 180, cy + 15);
ctx.lineTo(cx + 220, cy + 35);
ctx.moveTo(cx + 220, cy + 15);
ctx.lineTo(cx + 180, cy + 35);
ctx.stroke();
}
// 第五步:化简后的公式
if (elapsed >= timeline.solveV) {
let p = Math.min((elapsed - timeline.solveV) / 500, 1);
ctx.globalAlpha = easeOutCubic(p);
// 清空前一步的痕迹,强行重绘新公式
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawOrbitBackground(progress);
drawText("化简后:", cx - 250, cy - 80, colors.dim, 24, "left");
drawText("v² =", cx - 180, cy, colors.white);
drawFraction("G · M", "r", cx - 80, cy, colors.blue);
drawText("线速度的平方等于引力常数乘以中心天体质量除以轨道半径", cx, cy + 100, colors.dim, 18);
ctx.globalAlpha = 1;
}
// 第六步:最终结果
if (elapsed >= timeline.final) {
let p = Math.min((elapsed - timeline.final) / 500, 1);
ctx.globalAlpha = easeOutCubic(p);
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawOrbitBackground(progress);
drawText("线速度公式:", cx - 280, cy - 80, colors.green, 28, "left");
drawText("v =", cx - 200, cy, colors.white, 48);
// 绘制根号
ctx.beginPath();
ctx.moveTo(cx - 120, cy + 5);
ctx.lineTo(cx - 100, cy - 35);
ctx.lineTo(cx - 60, cy + 20);
ctx.lineTo(cx - 20, cy + 20);
ctx.strokeStyle = colors.white;
ctx.lineWidth = 3;
ctx.stroke();
drawFraction("G · M", "r", cx + 50, cy, colors.blue, colors.white);
drawText("这个公式告诉我们:轨道半径 r 越大,线速度 v 越小。", cx, cy + 100, colors.orange, 20);
ctx.globalAlpha = 1;
}
if (progress < 1) {
requestAnimationFrame(animate);
}
}
function restartAnimation() {
startTime = null;
ctx.clearRect(0, 0, canvas.width, canvas.height);
requestAnimationFrame(animate);
}
// 启动
requestAnimationFrame(animate);
</script>
</body>
</html>Game Source: 线速度公式推导动画
Creator: EpicCoder88
Libraries: none
Complexity: complex (284 lines, 8.3 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-epiccoder88-muuv2uo0" to link back to the original. Then publish at arcadelab.ai/publish.