🎮ArcadeLab

线速度公式推导动画

by EpicCoder88
284 lines8.3 KB
▶ Play
<!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.