分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>周期性差速制动 — IFR 原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{--bg:#060a12;--panel:#0b1220;--border:#14253a;--fg:#c8d6e5;--muted:#4a6580;--accent:#00e5ff;--brake:#ff7a1a;--free:#00e676;--warn:#ff2d55;--card:#0f1a2a;--glow-accent:0 0 18px rgba(0,229,255,.45);--glow-brake:0 0 22px rgba(255,122,26,.55);--glow-free:0 0 14px rgba(0,230,118,.4)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
header{width:100%;padding:28px 32px 10px;text-align:center;background:linear-gradient(180deg,rgba(0,229,255,.04) 0%,transparent 100%)}
header h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:clamp(22px,3.2vw,38px);letter-spacing:.06em;color:var(--accent);text-shadow:var(--glow-accent)}
header p{font-size:13px;color:var(--muted);margin-top:6px;letter-spacing:.04em}
.panels{display:flex;gap:14px;padding:14px 18px;width:100%;max-width:1540px;flex-wrap:wrap;justify-content:center}
.panel{flex:1 1 700px;max-width:760px;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;position:relative}
.panel-label{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:13px;color:var(--accent);padding:10px 16px 0;letter-spacing:.08em;text-transform:uppercase;display:flex;align-items:center;gap:8px}
.panel-label i{font-size:11px;opacity:.7}
.panel svg{display:block;width:100%;height:auto}
.controls{display:flex;gap:18px;padding:10px 24px 6px;flex-wrap:wrap;justify-content:center;align-items:center;max-width:1540px;width:100%}
.ctrl-group{display:flex;align-items:center;gap:8px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 16px}
.ctrl-group label{font-size:12px;color:var(--muted);white-space:nowrap;font-family:'Chakra Petch',sans-serif;letter-spacing:.04em}
.ctrl-group input[type=range]{-webkit-appearance:none;appearance:none;width:120px;height:4px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);box-shadow:var(--glow-accent);cursor:pointer}
.ctrl-group .val{font-size:13px;color:var(--accent);min-width:42px;text-align:right;font-weight:500}
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 18px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--fg);font-family:'Chakra Petch',sans-serif;font-size:13px;cursor:pointer;letter-spacing:.04em;transition:all .2s}
.btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:var(--glow-accent)}
.btn.active{background:rgba(0,229,255,.1);border-color:var(--accent);color:var(--accent)}
.btn i{font-size:12px}
.info{display:flex;gap:14px;padding:8px 24px 24px;flex-wrap:wrap;justify-content:center;max-width:1540px;width:100%}
.info-card{flex:1 1 280px;max-width:500px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px 20px}
.info-card h3{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:14px;margin-bottom:8px;display:flex;align-items:center;gap:8px}
.info-card h3.ifr{color:var(--accent)}
.info-card h3.risk{color:var(--warn)}
.info-card p,.info-card li{font-size:12px;color:var(--muted);line-height:1.7}
.info-card ul{padding-left:16px}
.info-card li::marker{color:var(--border)}
.tag{display:inline-block;font-size:10px;padding:2px 7px;border-radius:4px;font-weight:500;letter-spacing:.04em;vertical-align:middle}
.tag-accent{background:rgba(0,229,255,.12);color:var(--accent);border:1px solid rgba(0,229,255,.25)}
.tag-brake{background:rgba(255,122,26,.12);color:var(--brake);border:1px solid rgba(255,122,26,.25)}
.tag-free{background:rgba(0,230,118,.1);color:var(--free);border:1px solid rgba(0,230,118,.2)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<header>
  <h1>周期性差速制动 — 最终理想解</h1>
  <p>取消前轮转向 &rarr; 固定前轴 + 后轮独立差速制动 &rarr; 全势能推进、自动S型轨迹</p>
</header>

<div class="panels">
  <!-- 俯视图 -->
  <div class="panel">
    <div class="panel-label"><i class="fas fa-route"></i> 俯视轨迹 — S型自动绕行</div>
    <svg id="topSvg" viewBox="0 0 900 480" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
  <!-- 机构图 -->
  <div class="panel">
    <div class="panel-label"><i class="fas fa-cogs"></i> 差速制动机构 — 端面凸轮驱动</div>
    <svg id="mechSvg" viewBox="0 0 900 480" xmlns="http://www.w3.org/2000/svg"></svg>
  </div>
</div>

<div class="controls">
  <button class="btn active" id="btnPlay"><i class="fas fa-pause"></i> 暂停</button>
  <button class="btn" id="btnReset"><i class="fas fa-redo"></i> 重置</button>
  <div class="ctrl-group">
    <label>制动力矩</label>
    <input type="range" id="sliderBrake" min="5" max="80" value="35">
    <span class="val" id="valBrake">0.35</span>
  </div>
  <div class="ctrl-group">
    <label>凸轮转速</label>
    <input type="range" id="sliderCam" min="8" max="60" value="28">
    <span class="val" id="valCam">0.028</span>
  </div>
</div>

<div class="info">
  <div class="info-card">
    <h3 class="ifr"><i class="fas fa-bolt"></i> IFR 最终理想解</h3>
    <ul>
      <li>前轮固定死轴 &mdash; <span class="tag tag-free">零侧滑摩擦</span> 全部势能用于前进</li>
      <li>端面凸轮复用主轴旋转 &mdash; <span class="tag tag-accent">零附加能耗</span> 转向自行发生</li>
      <li>差速制动替代偏转转向 &mdash; <span class="tag tag-brake">低耗能转向</span> 成熟差速原理</li>
    </ul>
  </div>
  <div class="info-card">
    <h3 class="risk"><i class="fas fa-exclamation-triangle"></i> 失效边界</h3>
    <ul>
      <li>地面附着系数须恒定,打滑即失效</li>
      <li>制动力标定极难:过制动浪费动能,不足则撞柱</li>
      <li>凸轮推程 <span class="tag tag-accent">3mm</span> / 制动力矩 <span class="tag tag-brake">0.01Nm</span> 须精确匹配</li>
    </ul>
  </div>
</div>

<script>
/* ========== 工具函数 ========== */
const NS = 'http://www.w3.org/2000/svg';
function el(tag, attrs, parent) {
  const e = document.createElementNS(NS, tag);
  for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
  if (parent) parent.appendChild(e);
  return e;
}
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

/* ========== 配置 ========== */
const C = {
  wheelSep: 54,       // 后轮距(px)
  baseSpeed: 1.6,     // 基础前进速度
  carLen: 64, carW: 28,
  trailMax: 3000,
  poleSpacing: 140,
  poleRowL: 130, poleRowR: 350,
};

/* ========== 状态 ========== */
const S = {
  camAngle: 0,
  heading: 0,
  carX: 80, carY: 240,
  leftBrake: 0, rightBrake: 0,
  leftSpd: 1, rightSpd: 1,
  trail: [],
  running: true,
  brakeForce: 0.35,
  camSpeed: 0.028,
  time: 0,
  leftWheelAngle: 0,
  rightWheelAngle: 0,
};

/* ========== 俯视图 SVG 构建 ========== */
const topSvg = document.getElementById('topSvg');

// defs
const topDefs = el('defs', {}, topSvg);
// 发光滤镜
const fGlow = el('filter', {id:'tGlow',x:'-50%',y:'-50%',width:'200%',height:'200%'}, topDefs);
el('feGaussianBlur', {in:'SourceGraphic',stdDeviation:'4',result:'b'}, fGlow);
const fMerge = el('feMerge', {}, fGlow);
el('feMergeNode', {in:'b'}, fMerge);
el('feMergeNode', {in:'SourceGraphic'}, fMerge);

const fGlowS = el('filter', {id:'tGlowS',x:'-50%',y:'-50%',width:'200%',height:'200%'}, topDefs);
el('feGaussianBlur', {in:'SourceGraphic',stdDeviation:'2',result:'b'}, fGlowS);
const fMergeS = el('feMerge', {}, fGlowS);
el('feMergeNode', {in:'b'}, fMergeS);
el('feMergeNode', {in:'SourceGraphic'}, fMergeS);

// 网格背景
const gridG = el('g', {opacity:'0.08'}, topSvg);
for (let x = 0; x <= 900; x += 40) el('line', {x1:x,y1:0,x2:x,y2:480,stroke:'#00e5ff','stroke-width':'0.5'}, gridG);
for (let y = 0; y <= 480; y += 40) el('line', {x1:0,y1:y,x2:900,y2:y,stroke:'#00e5ff','stroke-width':'0.5'}, gridG);

// 障碍柱 (两排)
const polesG = el('g', {}, topSvg);
function drawPoles() {
  while (polesG.firstChild) polesG.removeChild(polesG.firstChild);
  for (let x = 120; x < 900; x += C.poleSpacing) {
    el('circle', {cx:x,cy:C.poleRowL,r:8,fill:'none',stroke:'#1e3a50','stroke-width':'1.5','stroke-dasharray':'3,3'}, polesG);
    el('circle', {cx:x,cy:C.poleRowR,r:8,fill:'none',stroke:'#1e3a50','stroke-width':'1.5','stroke-dasharray':'3,3'}, polesG);
    el('circle', {cx:x,cy:C.poleRowL,r:2.5,fill:'#1e3a50'}, polesG);
    el('circle', {cx:x,cy:C.poleRowR,r:2.5,fill:'#1e3a50'}, polesG);
  }
}
drawPoles();

// 中心线
el('line', {x1:0,y1:240,x2:900,y2:240,stroke:'#14253a','stroke-width':'1','stroke-dasharray':'8,6'}, topSvg);

// 轨迹路径
const trailPath = el('path', {fill:'none',stroke:'url(#trailGrad)','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#tGlowS)'}, topSvg);
const tGrad = el('linearGradient', {id:'trailGrad',x1:'0%',y1:'0%',x2:'100%',y2:'0%'}, topDefs);
el('stop', {offset:'0%','stop-color':'rgba(0,229,255,0)'}, tGrad);
el('stop', {offset:'30%','stop-color':'rgba(0,229,255,0.5)'}, tGrad);
el('stop', {offset:'100%','stop-color':'#00e5ff'}, tGrad);

// 车辆组
const carG = el('g', {}, topSvg);

// 车身
el('rect', {x:-C.carLen/2,y:-C.carW/2,width:C.carLen,height:C.carW,rx:6,ry:6,fill:'#1a2d42',stroke:'#2a4a68','stroke-width':'1.2'}, carG);

// 前轮(固定死轴) — 用方形表示不可转向
const fwG = el('g', {}, carG);
el('rect', {x:22,y:-5,width:10,height:10,rx:1.5,fill:'#3a5a78',stroke:'#5a8ab0','stroke-width':'0.8'}, fwG);
// 死轴标记 — 竖线
el('line', {x1:27,y1:-7,x2:27,y2:7,stroke:'#00e676','stroke-width':'1.5'}, fwG);
// "固定" 标注
const fwLabel = el('text', {x:27,y:-12,fill:'#00e676','font-size':'8','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'}, fwG);
fwLabel.textContent = 'FIXED';

// 左后轮
const lwG = el('g', {}, carG);
el('circle', {cx:-20,cy:-C.wheelSep/2,r:9,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'}, lwG);
const lwInd = el('line', {x1:-20,y1:-C.wheelSep/2,x2:-20,y1:-C.wheelSep/2-9,stroke:'var(--free)','stroke-width':'1.5','stroke-linecap':'round'}, lwG);

// 右后轮
const rwG = el('g', {}, carG);
el('circle', {cx:-20,cy:C.wheelSep/2,r:9,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'}, rwG);
const rwInd = el('line', {x1:-20,y1:C.wheelSep/2,x2:-20,y2:C.wheelSep/2+9,stroke:'var(--free)','stroke-width':'1.5','stroke-linecap':'round'}, rwG);

// 方向箭头
el('polygon', {points:'34,0 26,-5 26,5',fill:'#00e5ff',opacity:'0.7'}, carG);

// 制动状态指示灯
const leftBrakeLight = el('circle', {cx:-20,cy:-C.wheelSep/2,r:12,fill:'none',stroke:'#ff7a1a','stroke-width':'2',opacity:'0'}, carG);
const rightBrakeLight = el('circle', {cx:-20,cy:C.wheelSep/2,r:12,fill:'none',stroke:'#ff7a1a','stroke-width':'2',opacity:'0'}, carG);

// 实时数据标注
const dataG = el('g', {}, topSvg);
const txtLeftSpd = el('text', {x:20,y:440,fill:'var(--muted)','font-size':'11','font-family':'IBM Plex Mono,monospace'}, dataG);
const txtRightSpd = el('text', {x:20,y:458,fill:'var(--muted)','font-size':'11','font-family':'IBM Plex Mono,monospace'}, dataG);
const txtTurn = el('text', {x:200,y:440,fill:'var(--accent)','font-size':'11','font-family':'IBM Plex Mono,monospace'}, dataG);
const txtEnergy = el('text', {x:200,y:458,fill:'#00e676','font-size':'11','font-family':'IBM Plex Mono,monospace'}, dataG);
txtEnergy.textContent = '侧滑损耗: 0% (IFR)';

/* ========== 机构图 SVG 构建 ========== */
const mechSvg = document.getElementById('mechSvg');
const mDefs = el('defs', {}, mechSvg);
// 制动发光滤镜
const bGlow = el('filter', {id:'bGlow',x:'-80%',y:'-80%',width:'260%',height:'260%'}, mDefs);
el('feGaussianBlur', {in:'SourceGraphic',stdDeviation:'6',result:'b'}, bGlow);
const bMerge = el('feMerge', {}, bGlow);
el('feMergeNode', {in:'b'}, bMerge);
el('feMergeNode', {in:'SourceGraphic'}, bMerge);

// 网格
const mGridG = el('g', {opacity:'0.05'}, mechSvg);
for (let x = 0; x <= 900; x += 40) el('line', {x1:x,y1:0,x2:x,y2:480,stroke:'#00e5ff','stroke-width':'0.5'}, mGridG);
for (let y = 0; y <= 480; y += 40) el('line', {x1:0,y1:y,x2:900,y2:y,stroke:'#00e5ff','stroke-width':'0.5'}, mGridG);

// ---------- 端面凸轮 (顶部) ----------
const camCX = 450, camCY = 115;
const camG = el('g', {}, mechSvg);

// 凸轮外圈
el('circle', {cx:camCX,cy:camCY,r:72,fill:'#0a1520',stroke:'#1a3a55','stroke-width':'1.5'}, camG);

// 左侧区(制动左轮)
const camLeftArc = el('path', {fill:'rgba(255,122,26,0.08)',stroke:'none'}, camG);
// 右侧区(制动右轮)
const camRightArc = el('path', {fill:'rgba(0,229,255,0.08)',stroke:'none'}, camG);

// 凸轮轮廓 (椭圆偏心)
const camProfile = el('ellipse', {cx:camCX,cy:camCY,rx:62,ry:58,fill:'none',stroke:'#2a5a7a','stroke-width':'1','stroke-dasharray':'4,3'}, camG);

// 主轴
el('circle', {cx:camCX,cy:camCY,r:8,fill:'#1a2d42',stroke:'#00e5ff','stroke-width':'1.5'}, camG);

// 旋转指针
const camPointer = el('line', {x1:camCX,y1:camCY,x2:camCX+55,y2:camCY,stroke:'#00e5ff','stroke-width':'2','stroke-linecap':'round'}, camG);
const camPointerDot = el('circle', {cx:camCX+55,cy:camCY,r:4,fill:'#00e5ff'}, camG);

// 拨叉A (左)
const forkA = el('g', {}, camG);
el('rect', {x:camCX-85,y:camCY-6,width:18,height:12,rx:3,fill:'#2a1a0a',stroke:'#ff7a1a','stroke-width':'1.2'}, forkA);
const forkALine = el('line', {x1:camCX-67,y1:camCY,x2:camCX-72,y2:camCY,stroke:'#ff7a1a','stroke-width':'2'}, forkA);

// 拨叉B (右)
const forkB = el('g', {}, camG);
el('rect', {x:camCX+67,y:camCY-6,width:18,height:12,rx:3,fill:'#0a1a2a',stroke:'#00e5ff','stroke-width':'1.2'}, forkB);
const forkBLine = el('line', {x1:camCX+67,y1:camCY,x2:camCX+72,y2:camCY,stroke:'#00e5ff','stroke-width':'2'}, forkB);

// 凸轮标注
const camLabel = el('text', {x:camCX,y:camCY-84,fill:'var(--accent)','font-size':'11','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600','letter-spacing':'0.08em'}, camG);
camLabel.textContent = '端面凸轮';

// "复用主轴旋转" 标注
const reuseLabel = el('text', {x:camCX,y:camCY+100,fill:'#00e676','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','opacity':'0.8'}, camG);
reuseLabel.textContent = '⟳ 复用主轴旋转 — 零附加能耗';

// ---------- 连接线 (拨叉 → 制动鼓) ----------
const connAL = el('line', {x1:camCX-85,y1:camCY,x2:220,y2:310,stroke:'#ff7a1a','stroke-width':'1','stroke-dasharray':'5,4','opacity':'0.4'}, mechSvg);
const connBL = el('line', {x1:camCX+85,y1:camCY,x2:680,y2:310,stroke:'#00e5ff','stroke-width':'1','stroke-dasharray':'5,4','opacity':'0.4'}, mechSvg);

// ---------- 后轴示意图 (底部) ----------
const axleY = 340;

// 主轴
el('line', {x1:120,y1:axleY,x2:780,y2:axleY,stroke:'#1e3a55','stroke-width':'3','stroke-linecap':'round'}, mechSvg);
el('text', {x:450,y:axleY+52,fill:'var(--muted)','font-size':'9','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'}, mechSvg).textContent = '后轴 (主绕线轮驱动)';

// 左制动鼓
const lDrumG = el('g', {}, mechSvg);
el('circle', {cx:220,cy:axleY,r:38,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'}, lDrumG);
const lBrakeBand = el('path', {fill:'none',stroke:'#3a5a70','stroke-width':'4','stroke-linecap':'round'}, lDrumG);
const lBrakeBandGlow = el('path', {fill:'none',stroke:'#ff7a1a','stroke-width':'5','stroke-linecap':'round',opacity:'0',filter:'url(#bGlow)'}, lDrumG);
el('text', {x:220,y:axleY+2,fill:'var(--muted)','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'}, lDrumG).textContent = '制动鼓';

// 左轮
const lWheelG = el('g', {}, mechSvg);
el('circle', {cx:220,cy:axleY,r:52,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'}, lWheelG);
const lWheelSpokes = el('g', {}, lWheelG);
for (let i = 0; i < 6; i++) {
  const a = i * Math.PI / 3;
  el('line', {x1:220+18*Math.cos(a),y1:axleY+18*Math.sin(a),x2:220+48*Math.cos(a),y2:axleY+48*Math.sin(a),stroke:'#2a4a68','stroke-width':'0.8'}, lWheelSpokes);
}
el('text', {x:220,y:axleY-60,fill:'var(--muted)','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif',opacity:'0.6'}, lWheelG).textContent = '左轮';

// 左轮速度条
const lSpeedBarBg = el('rect', {x:160,y:axleY+62,width:120,height:8,rx:4,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'0.8'}, mechSvg);
const lSpeedBar = el('rect', {x:160,y:axleY+62,width:120,height:8,rx:4,fill:'#00e676',opacity:'0.8'}, mechSvg);
const lSpeedVal = el('text', {x:220,y:axleY+84,fill:'var(--fg)','font-size':'11','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'}, mechSvg);

// 右制动鼓
const rDrumG = el('g', {}, mechSvg);
el('circle', {cx:680,cy:axleY,r:38,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'}, rDrumG);
const rBrakeBand = el('path', {fill:'none',stroke:'#3a5a70','stroke-width':'4','stroke-linecap':'round'}, rDrumG);
const rBrakeBandGlow = el('path', {fill:'none',stroke:'#ff7a1a','stroke-width':'5','stroke-linecap':'round',opacity:'0',filter:'url(#bGlow)'}, rDrumG);
el('text', {x:680,y:axleY+2,fill:'var(--muted)','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'}, rDrumG).textContent = '制动鼓';

// 右轮
const rWheelG = el('g', {}, mechSvg);
el('circle', {cx:680,cy:axleY,r:52,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'}, rWheelG);
const rWheelSpokes = el('g', {}, rWheelG);
for (let i = 0; i < 6; i++) {
  const a = i * Math.PI / 3;
  el('line', {x1:680+18*Math.cos(a),y1:axleY+18*Math.sin(a),x2:680+48*Math.cos(a),y2:axleY+48*Math.sin(a),stroke:'#2a4a68','stroke-width':'0.8'}, rWheelSpokes);
}
el('text', {x:680,y:axleY-60,fill:'var(--muted)','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif',opacity:'0.6'}, rWheelG).textContent = '右轮';

// 右轮速度条
const rSpeedBarBg = el('rect', {x:620,y:axleY+62,width:120,height:8,rx:4,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'0.8'}, mechSvg);
const rSpeedBar = el('rect', {x:620,y:axleY+62,width:120,height:8,rx:4,fill:'#00e676',opacity:'0.8'}, mechSvg);
const rSpeedVal = el('text', {x:680,y:axleY+84,fill:'var(--fg)','font-size':'11','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'}, mechSvg);

// 制动状态文字
const brakeStateText = el('text', {x:450,y:axleY+84,fill:'var(--fg)','font-size':'12','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'}, mechSvg);

// 动作阶段标签
const phaseText = el('text', {x:450,y:25,fill:'var(--accent)','font-size':'13','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600','letter-spacing':'0.06em'}, mechSvg);

/* ========== 制动鼓弧线路径生成 ========== */
function brakeBandPath(cx, cy, r, tightness) {
  // tightness: 0 = 松弛, 1 = 紧贴
  const span = 200 + tightness * 60; // 弧度范围(度)
  const offsetR = r + (1 - tightness) * 8; // 松弛时半径大
  const startAngle = -span / 2;
  const endAngle = span / 2;
  const s = startAngle * Math.PI / 180;
  const e = endAngle * Math.PI / 180;
  const sx = cx + offsetR * Math.cos(s);
  const sy = cy + offsetR * Math.sin(s);
  const ex = cx + offsetR * Math.cos(e);
  const ey = cy + offsetR * Math.sin(e);
  const largeArc = span > 180 ? 1 : 0;
  return `M${sx},${sy} A${offsetR},${offsetR} 0 ${largeArc} 1 ${ex},${ey}`;
}

/* ========== 凸轮扇区路径 ========== */
function sectorPath(cx, cy, r, startDeg, endDeg) {
  const s = startDeg * Math.PI / 180;
  const e = endDeg * Math.PI / 180;
  const sx = cx + r * Math.cos(s);
  const sy = cy + r * Math.sin(s);
  const ex = cx + r * Math.cos(e);
  const ey = cy + r * Math.sin(e);
  const large = (endDeg - startDeg) > 180 ? 1 : 0;
  return `M${cx},${cy} L${sx},${sy} A${r},${r} 0 ${large} 1 ${ex},${ey} Z`;
}

/* ========== 动画主循环 ========== */
let lastTime = 0;

function resetState() {
  S.camAngle = 0;
  S.heading = 0;
  S.carX = 80;
  S.carY = 240;
  S.leftBrake = 0;
  S.rightBrake = 0;
  S.leftSpd = 1;
  S.rightSpd = 1;
  S.trail = [];
  S.time = 0;
  S.leftWheelAngle = 0;
  S.rightWheelAngle = 0;
  trailPath.setAttribute('d', '');
}

function update(dt) {
  if (!S.running) return;
  const steps = Math.max(1, Math.round(dt / 16));
  for (let s = 0; s < steps; s++) {
    S.camAngle += S.camSpeed;
    S.time += 1;

    // 计算制动力 (正弦交替)
    const cn = S.camAngle;
    S.leftBrake = Math.max(0, Math.sin(cn)) * S.brakeForce;
    S.rightBrake = Math.max(0, -Math.sin(cn)) * S.brakeForce;

    // 轮速
    S.leftSpd = 1 - S.leftBrake;
    S.rightSpd = 1 - S.rightBrake;

    // 运动学
    const vL = S.leftSpd * C.baseSpeed;
    const vR = S.rightSpd * C.baseSpeed;
    const v = (vL + vR) / 2;
    const omega = (vR - vL) / C.wheelSep;

    S.heading += omega;
    S.carX += v * Math.cos(S.heading);
    S.carY += v * Math.sin(S.heading);

    // 轮旋转角
    S.leftWheelAngle += vL * 0.15;
    S.rightWheelAngle += vR * 0.15;

    // 轨迹
    S.trail.push({ x: S.carX, y: S.carY });
    if (S.trail.length > C.trailMax) S.trail.shift();

    // 越界重置
    if (S.carX > 920 || S.carX < -20 || S.carY > 500 || S.carY < -20) {
      S.carX = 80;
      S.carY = 240;
      S.heading = 0;
      S.trail = [];
    }
  }
}

function render() {
  // ---- 俯视图 ----
  // 轨迹
  if (S.trail.length > 2) {
    const recent = S.trail.slice(-600);
    let d = `M${recent[0].x.toFixed(1)},${recent[0].y.toFixed(1)}`;
    for (let i = 1; i < recent.length; i += 2) {
      d += ` L${recent[i].x.toFixed(1)},${recent[i].y.toFixed(1)}`;
    }
    trailPath.setAttribute('d', d);
  }

  // 车辆位置
  const deg = S.heading * 180 / Math.PI;
  carG.setAttribute('transform', `translate(${S.carX.toFixed(1)},${S.carY.toFixed(1)}) rotate(${deg.toFixed(1)})`);

  // 后轮转速指示
  const la = S.leftWheelAngle;
  const ra = S.rightWheelAngle;
  lwInd.setAttribute('x2', (-20 + 9 * Math.cos(la - Math.PI/2)).toFixed(1));
  lwInd.setAttribute('y2', (-C.wheelSep/2 + 9 * Math.sin(la - Math.PI/2)).toFixed(1));
  rwInd.setAttribute('x2', (-20 + 9 * Math.cos(ra - Math.PI/2)).toFixed(1));
  rwInd.setAttribute('y2', (C.wheelSep/2 + 9 * Math.sin(ra - Math.PI/2)).toFixed(1));

  // 后轮颜色
  lwInd.setAttribute('stroke', S.leftBrake > 0.05 ? '#ff7a1a' : '#00e676');
  rwInd.setAttribute('stroke', S.rightBrake > 0.05 ? '#ff7a1a' : '#00e676');

  // 制动光环
  leftBrakeLight.setAttribute('opacity', clamp(S.leftBrake * 2, 0, 0.9).toFixed(2));
  rightBrakeLight.setAttribute('opacity', clamp(S.rightBrake * 2, 0, 0.9).toFixed(2));

  // 数据标注
  const turnDir = S.leftBrake > S.rightBrake ? '← 左转' : S.rightBrake > S.leftBrake ? '→ 右转' : '— 直行';
  txtLeftSpd.textContent = `左轮速: ${S.leftSpd.toFixed(2)}`;
  txtRightSpd.textContent = `右轮速: ${S.rightSpd.toFixed(2)}`;
  txtTurn.textContent = `转向: ${turnDir}`;

  // ---- 机构图 ----
  // 凸轮指针旋转
  const cDeg = S.camAngle * 180 / Math.PI;
  camPointer.setAttribute('transform', `rotate(${cDeg.toFixed(1)},${camCX},${camCY})`);
  camPointerDot.setAttribute('transform', `rotate(${cDeg.toFixed(1)},${camCX},${camCY})`);

  // 凸轮扇区 (跟随旋转)
  const rotOffset = cDeg;
  camLeftArc.setAttribute('d', sectorPath(camCX, camCY, 70, rotOffset - 90, rotOffset + 90));
  camRightArc.setAttribute('d', sectorPath(camCX, camCY, 70, rotOffset + 90, rotOffset + 270));
  const lActive = S.leftBrake > 0.05;
  const rActive = S.rightBrake > 0.05;
  camLeftArc.setAttribute('fill', lActive ? 'rgba(255,122,26,0.18)' : 'rgba(255,122,26,0.03)');
  camRightArc.setAttribute('fill', rActive ? 'rgba(0,229,255,0.18)' : 'rgba(0,229,255,0.03)');

  // 拨叉位移
  const forkDispA = S.leftBrake * 8;
  const forkDispB = S.rightBrake * 8;
  forkA.setAttribute('transform', `translate(${(-forkDispA).toFixed(1)},0)`);
  forkB.setAttribute('transform', `translate(${forkDispB.toFixed(1)},0)`);
  forkALine.setAttribute('x1', (camCX - 67 - forkDispA).toFixed(1));
  forkALine.setAttribute('x2', (camCX - 72 - forkDispA).toFixed(1));
  forkBLine.setAttribute('x1', (camCX + 67 + forkDispB).toFixed(1));
  forkBLine.setAttribute('x2', (camCX + 72 + forkDispB).toFixed(1));

  // 拨叉颜色
  forkA.setAttribute('opacity', lActive ? '1' : '0.4');
  forkB.setAttribute('opacity', rActive ? '1' : '0.4');

  // 连接线
  connAL.setAttribute('x1', (camCX - 85 - forkDispA).toFixed(1));
  connBL.setAttribute('x1', (camCX + 85 + forkDispB).toFixed(1));
  connAL.setAttribute('opacity', lActive ? '0.7' : '0.2');
  connBL.setAttribute('opacity', rActive ? '0.7' : '0.2');
  connAL.setAttribute('stroke', lActive ? '#ff7a1a' : '#3a5a70');
  connBL.setAttribute('stroke', rActive ? '#ff7a1a' : '#3a5a70');

  // 制动鼓
  const lTight = clamp(S.leftBrake / Math.max(0.01, S.brakeForce), 0, 1);
  const rTight = clamp(S.rightBrake / Math.max(0.01, S.brakeForce), 0, 1);
  lBrakeBand.setAttribute('d', brakeBandPath(220, axleY, 34, lTight));
  rBrakeBand.setAttribute('d', brakeBandPath(680, axleY, 34, rTight));
  lBrakeBandGlow.setAttribute('d', brakeBandPath(220, axleY, 34, lTight));
  rBrakeBandGlow.setAttribute('d', brakeBandPath(680, axleY, 34, rTight));
  lBrakeBandGlow.setAttribute('opacity', (lTight * 0.8).toFixed(2));
  rBrakeBandGlow.setAttribute('opacity', (rTight * 0.8).toFixed(2));
  lBrakeBand.setAttribute('stroke', lActive ? '#ff7a1a' : '#3a5a70');
  rBrakeBand.setAttribute('stroke', rActive ? '#ff7a1a' : '#3a5a70');

  // 轮辐旋转
  lWheelSpokes.setAttribute('transform', `rotate(${(S.leftWheelAngle * 180 / Math.PI).toFixed(1)},220,${axleY})`);
  rWheelSpokes.setAttribute('transform', `rotate(${(S.rightWheelAngle * 180 / Math.PI).toFixed(1)},680,${axleY})`);

  // 速度条
  const barW = 120;
  lSpeedBar.setAttribute('width', (S.leftSpd * barW).toFixed(1));
  rSpeedBar.setAttribute('width', (S.rightSpd * barW).toFixed(1));
  lSpeedBar.setAttribute('fill', lActive ? '#ff7a1a' : '#00e676');
  rSpeedBar.setAttribute('fill', rActive ? '#ff7a1a' : '#00e676');
  lSpeedVal.textContent = `v = ${S.leftSpd.toFixed(2)}`;
  rSpeedVal.textContent = `v = ${S.rightSpd.toFixed(2)}`;

  // 制动状态
  if (lActive && !rActive) {
    brakeStateText.textContent = '◀ 左轮制动 → 右转';
    brakeStateText.setAttribute('fill', '#ff7a1a');
  } else if (rActive && !lActive) {
    brakeStateText.textContent = '右轮制动 ◀ → 左转';
    brakeStateText.setAttribute('fill', '#ff7a1a');
  } else {
    brakeStateText.textContent = '— 过渡 —';
    brakeStateText.setAttribute('fill', '#4a6580');
  }

  // 阶段标签
  const phase = lActive ? '凸轮推程 → 拨叉A拉紧左制动带' : rActive ? '凸轮推程 → 拨叉B拉紧右制动带' : '凸轮过渡区 — 制动切换';
  phaseText.textContent = phase;
}

function loop(ts) {
  const dt = lastTime ? Math.min(ts - lastTime, 50) : 16;
  lastTime = ts;
  update(dt);
  render();
  requestAnimationFrame(loop);
}

/* ========== 控件绑定 ========== */
const btnPlay = document.getElementById('btnPlay');
const btnReset = document.getElementById('btnReset');
const sliderBrake = document.getElementById('sliderBrake');
const sliderCam = document.getElementById('sliderCam');
const valBrake = document.getElementById('valBrake');
const valCam = document.getElementById('valCam');

btnPlay.addEventListener('click', () => {
  S.running = !S.running;
  btnPlay.innerHTML = S.running ? '<i class="fas fa-pause"></i> 暂停' : '<i class="fas fa-play"></i> 播放';
  btnPlay.classList.toggle('active', S.running);
});

btnReset.addEventListener('click', () => {
  resetState();
});

sliderBrake.addEventListener('input', () => {
  S.brakeForce = parseInt(sliderBrake.value) / 100;
  valBrake.textContent = S.brakeForce.toFixed(2);
});

sliderCam.addEventListener('input', () => {
  S.camSpeed = parseInt(sliderCam.value) / 1000;
  valCam.textContent = S.camSpeed.toFixed(3);
});

/* ========== 启动 ========== */
resetState();
requestAnimationFrame(loop);
</script>
</body>
</html>

这个实现包含以下核心设计与技术要点:

视觉设计

  • 采用深色技术蓝图风格(#060a12 底色),搭配青色(#00e5ff)结构线与橙色(#ff7a1a)制动高亮,绿色(#00e676)表示自由滚动状态
  • 字体选用 Chakra Petch(展示/标签)+ IBM Plex Mono(数据/等宽),营造工程仪器感
  • SVG 滤镜实现制动发光效果,轨迹带渐变与辉光

IFR 理想解表达

  • 前轮标注「FIXED」并加绿色死轴线,强调零侧滑摩擦
  • 凸轮区域标注"复用主轴旋转 — 零附加能耗",突出资源自利用
  • 底部信息卡明确列出三项 IFR 特征与失效边界

动画原理

  • 左侧俯视图:基于差速运动学模型(v = (vL+vR)/2, ω = (vR-vL)/W)实时解算车辆位姿,自动生成 S 型轨迹
  • 右侧机构图:端面凸轮旋转→拨叉位移→制动带收紧/松弛的完整因果链,轮速条与颜色实时联动
  • 两图通过共享 camAngle 状态严格同步

交互控制

  • 「制动力矩」滑块:调节制动强度,直观观察转弯半径变化——过小则撞柱,过大则蛇行
  • 「凸轮转速」滑块:调节 S 曲线频率
  • 播放/暂停与重置按钮
端面凸轮旋转→拨叉位移没体现出来,也没看到零部件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>周期性差速制动 — IFR 原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{--bg:#060a12;--panel:#0b1220;--border:#14253a;--fg:#c8d6e5;--muted:#4a6580;--accent:#00e5ff;--brake:#ff7a1a;--free:#00e676;--warn:#ff2d55;--card:#0f1a2a;--glow-a:0 0 18px rgba(0,229,255,.45);--glow-b:0 0 22px rgba(255,122,26,.55)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
header{width:100%;padding:22px 32px 8px;text-align:center;background:linear-gradient(180deg,rgba(0,229,255,.04) 0%,transparent 100%)}
header h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:clamp(20px,3vw,34px);letter-spacing:.06em;color:var(--accent);text-shadow:var(--glow-a)}
header p{font-size:12px;color:var(--muted);margin-top:4px;letter-spacing:.04em}
.panels{display:flex;gap:12px;padding:10px 14px;width:100%;max-width:1560px;flex-wrap:wrap;justify-content:center}
.panel{flex:1 1 720px;max-width:780px;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.panel-label{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:12px;color:var(--accent);padding:8px 14px 0;letter-spacing:.08em;text-transform:uppercase;display:flex;align-items:center;gap:7px}
.panel-label i{font-size:10px;opacity:.7}
.panel svg{display:block;width:100%;height:auto}
.controls{display:flex;gap:14px;padding:8px 20px 4px;flex-wrap:wrap;justify-content:center;align-items:center;max-width:1560px;width:100%}
.ctrl-group{display:flex;align-items:center;gap:7px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:7px 14px}
.ctrl-group label{font-size:11px;color:var(--muted);white-space:nowrap;font-family:'Chakra Petch',sans-serif;letter-spacing:.03em}
.ctrl-group input[type=range]{-webkit-appearance:none;appearance:none;width:110px;height:4px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:var(--accent);box-shadow:var(--glow-a);cursor:pointer}
.ctrl-group .val{font-size:12px;color:var(--accent);min-width:40px;text-align:right;font-weight:500}
.btn{display:inline-flex;align-items:center;gap:5px;padding:7px 16px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--fg);font-family:'Chakra Petch',sans-serif;font-size:12px;cursor:pointer;letter-spacing:.03em;transition:all .2s}
.btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:var(--glow-a)}
.btn.active{background:rgba(0,229,255,.08);border-color:var(--accent);color:var(--accent)}
.btn i{font-size:11px}
.info{display:flex;gap:12px;padding:6px 20px 20px;flex-wrap:wrap;justify-content:center;max-width:1560px;width:100%}
.info-card{flex:1 1 280px;max-width:520px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 18px}
.info-card h3{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:13px;margin-bottom:6px;display:flex;align-items:center;gap:7px}
.info-card h3.ifr{color:var(--accent)}
.info-card h3.risk{color:var(--warn)}
.info-card p,.info-card li{font-size:11px;color:var(--muted);line-height:1.7}
.info-card ul{padding-left:14px}
.tag{display:inline-block;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:500;letter-spacing:.03em;vertical-align:middle}
.tag-a{background:rgba(0,229,255,.1);color:var(--accent);border:1px solid rgba(0,229,255,.2)}
.tag-b{background:rgba(255,122,26,.1);color:var(--brake);border:1px solid rgba(255,122,26,.2)}
.tag-f{background:rgba(0,230,118,.08);color:var(--free);border:1px solid rgba(0,230,118,.18)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
<body>
<header>
  <h1>周期性差速制动 — 最终理想解</h1>
  <p>固定前轴 + 端面凸轮驱动双拨叉交替制动 → 全势能推进、自动S型绕行</p>
</header>
<div class="panels">
  <div class="panel"><div class="panel-label"><i class="fas fa-route"></i> 俯视轨迹</div><svg id="topSvg" viewBox="0 0 900 420"></svg></div>
  <div class="panel"><div class="panel-label"><i class="fas fa-cogs"></i> 差速制动机构</div><svg id="mechSvg" viewBox="0 0 960 580"></svg></div>
</div>
<div class="controls">
  <button class="btn active" id="btnPlay"><i class="fas fa-pause"></i> 暂停</button>
  <button class="btn" id="btnReset"><i class="fas fa-redo"></i> 重置</button>
  <div class="ctrl-group"><label>制动力矩</label><input type="range" id="sBrake" min="5" max="80" value="35"><span class="val" id="vBrake">0.35</span></div>
  <div class="ctrl-group"><label>凸轮转速</label><input type="range" id="sCam" min="8" max="60" value="28"><span class="val" id="vCam">0.028</span></div>
</div>
<div class="info">
  <div class="info-card">
    <h3 class="ifr"><i class="fas fa-bolt"></i> IFR 最终理想解</h3>
    <ul>
      <li>前轮固定死轴 — <span class="tag tag-f">零侧滑</span> 全势能推进</li>
      <li>端面凸轮复用主轴旋转 — <span class="tag tag-a">零附加能耗</span> 转向自行发生</li>
      <li>差速制动替代偏转 — <span class="tag tag-b">低耗能</span> 成熟履带差速原理</li>
    </ul>
  </div>
  <div class="info-card">
    <h3 class="risk"><i class="fas fa-exclamation-triangle"></i> 失效边界</h3>
    <ul>
      <li>地面附着系数须恒定,打滑即失效</li>
      <li>制动力标定极难:过制动浪费动能,不足则撞柱</li>
      <li>推程 <span class="tag tag-a">3mm</span> / 力矩 <span class="tag tag-b">0.01Nm</span> 须精确匹配</li>
    </ul>
  </div>
</div>
<script>
const NS='http://www.w3.org/2000/svg';
function el(t,a,p){const e=document.createElementNS(NS,t);for(const[k,v]of Object.entries(a||{}))e.setAttribute(k,v);if(p)p.appendChild(e);return e}
function lerp(a,b,t){return a+(b-a)*t}
function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v))}

/* ===== 全局状态 ===== */
const S={
  camAngle:0,heading:0,carX:80,carY:210,
  leftBrake:0,rightBrake:0,leftSpd:1,rightSpd:1,
  trail:[],running:true,brakeForce:.35,camSpeed:.028,time:0,
  lwAngle:0,rwAngle:0
};
const CFG={wheelSep:54,baseSpeed:1.6,carLen:62,carW:26,trailMax:3500,poleSpacing:140};

/* ===== 俯视图构建 ===== */
const topSvg=document.getElementById('topSvg');
const tDefs=el('defs',{},topSvg);
// 轨迹渐变
const tGrad=el('linearGradient',{id:'tGrad',x1:'0%',y1:'0%',x2:'100%',y2:'0%'},tDefs);
el('stop',{offset:'0%','stop-color':'rgba(0,229,255,0)'},tGrad);
el('stop',{offset:'25%','stop-color':'rgba(0,229,255,0.35)'},tGrad);
el('stop',{offset:'100%','stop-color':'#00e5ff'},tGrad);
// 发光
const fG=el('filter',{id:'fG',x:'-50%',y:'-50%',width:'200%',height:'200%'},tDefs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'3',result:'b'},fG);
const fM=el('feMerge',{},fG);el('feMergeNode',{in:'b'},fM);el('feMergeNode',{in:'SourceGraphic'},fM);

// 网格
const tGrid=el('g',{opacity:'.06'},topSvg);
for(let x=0;x<=900;x+=40)el('line',{x1:x,y1:0,x2:x,y2:420,stroke:'#00e5ff','stroke-width':'.5'},tGrid);
for(let y=0;y<=420;y+=40)el('line',{x1:0,y1:y,x2:900,y2:y,stroke:'#00e5ff','stroke-width':'.5'},tGrid);

// 中心线
el('line',{x1:0,y1:210,x2:900,y2:210,stroke:'#14253a','stroke-width':'1','stroke-dasharray':'8,6'},topSvg);

// 障碍柱
const polesG=el('g',{},topSvg);
for(let x=100;x<900;x+=CFG.poleSpacing){
  for(const cy of[110,310]){
    el('circle',{cx:x,cy:cy,r:7,fill:'none',stroke:'#1e3a50','stroke-width':'1.2','stroke-dasharray':'3,3'},polesG);
    el('circle',{cx:x,cy:cy,r:2,fill:'#1e3a50'},polesG);
  }
}

// 轨迹
const trailPath=el('path',{fill:'none',stroke:'url(#tGrad)','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#fG)'},topSvg);

// 车辆
const carG=el('g',{},topSvg);
el('rect',{x:-CFG.carLen/2,y:-CFG.carW/2,width:CFG.carLen,height:CFG.carW,rx:5,ry:5,fill:'#1a2d42',stroke:'#2a4a68','stroke-width':'1.2'},carG);
// 前轮(固定)
const fwG=el('g',{},carG);
el('rect',{x:22,y:-5,width:9,height:10,rx:1.5,fill:'#3a5a78',stroke:'#5a8ab0','stroke-width':'.8'},fwG);
el('line',{x1:26.5,y1:-7,x2:26.5,y2:7,stroke:'#00e676','stroke-width':'1.5'},fwG);
const fwTxt=el('text',{x:26.5,y:-11,fill:'#00e676','font-size':'7','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'},fwG);fwTxt.textContent='FIXED';
// 左后轮
const lwG=el('g',{},carG);
el('circle',{cx:-20,cy:-CFG.wheelSep/2,r:8,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'},lwG);
const lwInd=el('line',{x1:-20,y1:-CFG.wheelSep/2,x2:-20,y1:-CFG.wheelSep/2-8,stroke:'#00e676','stroke-width':'1.5','stroke-linecap':'round'},lwG);
// 右后轮
const rwG=el('g',{},carG);
el('circle',{cx:-20,cy:CFG.wheelSep/2,r:8,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'},rwG);
const rwInd=el('line',{x1:-20,y1:CFG.wheelSep/2,x2:-20,y2:CFG.wheelSep/2+8,stroke:'#00e676','stroke-width':'1.5','stroke-linecap':'round'},rwG);
// 方向箭头
el('polygon',{points:'33,0 26,-4 26,4',fill:'#00e5ff',opacity:'.65'},carG);
// 制动光环
const lbLight=el('circle',{cx:-20,cy:-CFG.wheelSep/2,r:11,fill:'none',stroke:'#ff7a1a','stroke-width':'1.8',opacity:'0'},carG);
const rbLight=el('circle',{cx:-20,cy:CFG.wheelSep/2,r:11,fill:'none',stroke:'#ff7a1a','stroke-width':'1.8',opacity:'0'},carG);
// 数据
const dG=el('g',{},topSvg);
const tLS=el('text',{x:16,y:395,fill:'var(--muted)','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const tRS=el('text',{x:16,y:410,fill:'var(--muted)','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const tTN=el('text',{x:180,y:395,fill:'var(--accent)','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const tEN=el('text',{x:180,y:410,fill:'#00e676','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);tEN.textContent='侧滑损耗: 0% (IFR)';

/* ===== 机构图构建 ===== */
const mSvg=document.getElementById('mechSvg');
const mDefs=el('defs',{},mSvg);
// 制动发光
const bfG=el('filter',{id:'bfG',x:'-80%',y:'-80%',width:'260%',height:'260%'},mDefs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'5',result:'b'},bfG);
const bfM=el('feMerge',{},bfG);el('feMergeNode',{in:'b'},bfM);el('feMergeNode',{in:'SourceGraphic'},bfM);
// 拨叉发光
const fkG=el('filter',{id:'fkG',x:'-60%',y:'-60%',width:'220%',height:'220%'},mDefs);
el('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'4',result:'b'},fkG);
const fkM=el('feMerge',{},fkG);el('feMergeNode',{in:'b'},fkM);el('feMergeNode',{in:'SourceGraphic'},fkM);

// 网格
const mGrid=el('g',{opacity:'.04'},mSvg);
for(let x=0;x<=960;x+=40)el('line',{x1:x,y1:0,x2:x,y2:580,stroke:'#00e5ff','stroke-width':'.5'},mGrid);
for(let y=0;y<=580;y+=40)el('line',{x1:0,y1:y,x2:960,y2:y,stroke:'#00e5ff','stroke-width':'.5'},mGrid);

// === 凸轮参数 ===
const CAM={cx:480,cy:145,baseR:58,lift:22};

// 凸轮外圈参考圆
el('circle',{cx:CAM.cx,cy:CAM.cy,r:CAM.baseR+CAM.lift+8,fill:'none',stroke:'#0d1f30','stroke-width':'1','stroke-dasharray':'4,4'},mSvg);
el('circle',{cx:CAM.cx,cy:CAM.cy,r:CAM.baseR-4,fill:'#070d16',stroke:'#0d1f30','stroke-width':'1'},mSvg);

// 凸轮轮廓path(动态更新)
const camPath=el('path',{fill:'#0e1a28',stroke:'#2a5a7a','stroke-width':'1.5'},mSvg);
// 凸轮凸起高亮
const camHighlight=el('path',{fill:'rgba(0,229,255,0.06)',stroke:'none'},mSvg);
// 凸轮中心轴
el('circle',{cx:CAM.cx,cy:CAM.cy,r:7,fill:'#1a2d42',stroke:'#00e5ff','stroke-width':'1.5'},mSvg);
// 凸轮旋转指针
const camPtr=el('line',{x1:CAM.cx,y1:CAM.cy,x2:CAM.cx+40,y2:CAM.cy,stroke:'#00e5ff','stroke-width':'2','stroke-linecap':'round'},mSvg);
const camDot=el('circle',{cx:CAM.cx+40,cy:CAM.cy,r:3.5,fill:'#00e5ff'},mSvg);

// 旋转方向弧形箭头
const rotArrow=el('path',{fill:'none',stroke:'#00e5ff','stroke-width':'1.2',opacity:'.5'},mSvg);

// === 拨叉A(上方,控制左制动)===
const forkAG=el('g',{},mSvg);
// 拨叉杆
const forkARod=el('rect',{x:CAM.cx-4,width:8,rx:3,fill:'#1e3a55',stroke:'#3a6a8a','stroke-width':'1'},forkAG);
// 滚子
const forkARoller=el('circle',{r:7,fill:'#2a4a68',stroke:'#5a9aba','stroke-width':'1.2'},forkAG);
// 滚子销
const forkAPin=el('circle',{r:2.5,fill:'#5a9aba'},forkAG);
// 拨叉臂(L形)
const forkAArm=el('path',{fill:'none',stroke:'#3a6a8a','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round'},forkAG);
// 拉线连接点
const forkAWire=el('circle',{r:4,fill:'#ff7a1a',stroke:'#ff9944','stroke-width':'1',opacity:'0'},forkAG);
// 标注
const forkALabel=el('text',{fill:'var(--brake)','font-size':'10','text-anchor':'end','font-family':'Chakra Petch,sans-serif','font-weight':'600'},forkAG);
forkALabel.textContent='拨叉A → 左制动';

// === 拨叉B(下方,控制右制动)===
const forkBG=el('g',{},mSvg);
const forkBRod=el('rect',{x:CAM.cx-4,width:8,rx:3,fill:'#1e3a55',stroke:'#3a6a8a','stroke-width':'1'},forkBG);
const forkBRoller=el('circle',{r:7,fill:'#2a4a68',stroke:'#5a9aba','stroke-width':'1.2'},forkBG);
const forkBPin=el('circle',{r:2.5,fill:'#5a9aba'},forkBG);
const forkBArm=el('path',{fill:'none',stroke:'#3a6a8a','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round'},forkBG);
const forkBWire=el('circle',{r:4,fill:'#ff7a1a',stroke:'#ff9944','stroke-width':'1',opacity:'0'},forkBG);
const forkBLabel=el('text',{fill:'var(--brake)','font-size':'10','text-anchor':'end','font-family':'Chakra Petch,sans-serif','font-weight':'600'},forkBG);
forkBLabel.textContent='拨叉B → 右制动';

// === 拉线 ===
const wireA=el('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'1.2','stroke-dasharray':'6,4',opacity:'.3'},mSvg);
const wireB=el('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'1.2','stroke-dasharray':'6,4',opacity:'.3'},mSvg);

// === 后桥区域 ===
const AXLE_Y=435;
// 主轴
el('line',{x1:100,y1:AXLE_Y,x2:860,y2:AXLE_Y,stroke:'#1e3a55','stroke-width':'3','stroke-linecap':'round'},mSvg);
// 绕线轮
const spoolG=el('g',{},mSvg);
el('rect',{x:CAM.cx-22,y:AXLE_Y-16,width:44,height:32,rx:6,fill:'#0e1825',stroke:'#2a5a7a','stroke-width':'1.2'},spoolG);
el('line',{x1:CAM.cx-14,y1:AXLE_Y-10,x2:CAM.cx-14,y2:AXLE_Y+10,stroke:'#2a5a7a','stroke-width':'.8'},spoolG);
el('line',{x1:CAM.cx+14,y1:AXLE_Y-10,x2:CAM.cx+14,y2:AXLE_Y+10,stroke:'#2a5a7a','stroke-width':'.8'},spoolG);
// 绕线
const spoolWire=el('path',{fill:'none',stroke:'#4a7a9a','stroke-width':'1.5'},spoolG);
const spoolLabel=el('text',{x:CAM.cx,y:AXLE_Y+28,fill:'var(--muted)','font-size':'9','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},spoolG);
spoolLabel.textContent='主绕线轮';

// === 左制动鼓 + 左轮 ===
const L_CX=200;
// 左制动鼓
el('circle',{cx:L_CX,cy:AXLE_Y,r:34,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'},mSvg);
const lBrakeLabel=el('text',{x:L_CX,y:AXLE_Y+3,fill:'#2a4a68','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mSvg);
lBrakeLabel.textContent='制动鼓';
// 左制动带(动态)
const lBrakeBand=el('path',{fill:'none',stroke:'#3a5a70','stroke-width':'5','stroke-linecap':'round'},mSvg);
const lBrakeGlow=el('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'6','stroke-linecap':'round',opacity:'0',filter:'url(#bfG)'},mSvg);
// 单向轴承标记
const lOWB=el('g',{},mSvg);
el('circle',{cx:L_CX,cy:AXLE_Y-42,r:6,fill:'#0e1825',stroke:'#00e676','stroke-width':'1'},lOWB);
el('path',{d:`M${L_CX-3},${AXLE_Y-44} L${L_CX+3},${AXLE_Y-44} L${L_CX+1},${AXLE_Y-46}`,fill:'none',stroke:'#00e676','stroke-width':'1'},lOWB);
const lOWBLabel=el('text',{x:L_CX,y:AXLE_Y-52,fill:'#00e676','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.7'},mSvg);
lOWBLabel.textContent='单向轴承';
// 左轮
const lWheelG=el('g',{},mSvg);
el('circle',{cx:L_CX,cy:AXLE_Y,r:50,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'},lWheelG);
const lSpokes=el('g',{},lWheelG);
for(let i=0;i<8;i++){const a=i*Math.PI/4;el('line',{x1:L_CX+16*Math.cos(a),y1:AXLE_Y+16*Math.sin(a),x2:L_CX+46*Math.cos(a),y2:AXLE_Y+46*Math.sin(a),stroke:'#2a4a68','stroke-width':'.7'},lSpokes);}
el('circle',{cx:L_CX,cy:AXLE_Y,r:14,fill:'#0e1825',stroke:'#2a4a68','stroke-width':'1'},lWheelG);
const lWheelLabel=el('text',{x:L_CX,y:AXLE_Y-58,fill:'var(--muted)','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},mSvg);
lWheelLabel.textContent='左轮';
// 左速度条
el('rect',{x:L_CX-55,y:AXLE_Y+58,width:110,height:7,rx:3.5,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'.6'},mSvg);
const lBar=el('rect',{x:L_CX-55,y:AXLE_Y+58,width:110,height:7,rx:3.5,fill:'#00e676',opacity:'.8'},mSvg);
const lSpdTxt=el('text',{x:L_CX,y:AXLE_Y+78,fill:'var(--fg)','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'},mSvg);

// === 右制动鼓 + 右轮 ===
const R_CX=760;
el('circle',{cx:R_CX,cy:AXLE_Y,r:34,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'},mSvg);
const rBrakeLabel=el('text',{x:R_CX,y:AXLE_Y+3,fill:'#2a4a68','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mSvg);
rBrakeLabel.textContent='制动鼓';
const rBrakeBand=el('path',{fill:'none',stroke:'#3a5a70','stroke-width':'5','stroke-linecap':'round'},mSvg);
const rBrakeGlow=el('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'6','stroke-linecap':'round',opacity:'0',filter:'url(#bfG)'},mSvg);
// 单向轴承
const rOWB=el('g',{},mSvg);
el('circle',{cx:R_CX,cy:AXLE_Y-42,r:6,fill:'#0e1825',stroke:'#00e676','stroke-width':'1'},rOWB);
el('path',{d:`M${R_CX-3},${AXLE_Y-44} L${R_CX+3},${AXLE_Y-44} L${R_CX+1},${AXLE_Y-46}`,fill:'none',stroke:'#00e676','stroke-width':'1'},rOWB);
const rOWBLabel=el('text',{x:R_CX,y:AXLE_Y-52,fill:'#00e676','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.7'},mSvg);
rOWBLabel.textContent='单向轴承';
// 右轮
const rWheelG=el('g',{},mSvg);
el('circle',{cx:R_CX,cy:AXLE_Y,r:50,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'},rWheelG);
const rSpokes=el('g',{},rWheelG);
for(let i=0;i<8;i++){const a=i*Math.PI/4;el('line',{x1:R_CX+16*Math.cos(a),y1:AXLE_Y+16*Math.sin(a),x2:R_CX+46*Math.cos(a),y2:AXLE_Y+46*Math.sin(a),stroke:'#2a4a68','stroke-width':'.7'},rSpokes);}
el('circle',{cx:R_CX,cy:AXLE_Y,r:14,fill:'#0e1825',stroke:'#2a4a68','stroke-width':'1'},rWheelG);
const rWheelLabel=el('text',{x:R_CX,y:AXLE_Y-58,fill:'var(--muted)','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},mSvg);
rWheelLabel.textContent='右轮';
el('rect',{x:R_CX-55,y:AXLE_Y+58,width:110,height:7,rx:3.5,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'.6'},mSvg);
const rBar=el('rect',{x:R_CX-55,y:AXLE_Y+58,width:110,height:7,rx:3.5,fill:'#00e676',opacity:'.8'},mSvg);
const rSpdTxt=el('text',{x:R_CX,y:AXLE_Y+78,fill:'var(--fg)','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'},mSvg);

// 主轴标注
const axleLabel=el('text',{x:CAM.cx,y:AXLE_Y-24,fill:'var(--muted)','font-size':'9','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mSvg);
axleLabel.textContent='主轴';

// 制动状态
const brakeTxt=el('text',{x:480,y:AXLE_Y+78,fill:'var(--fg)','font-size':'11','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'},mSvg);

// 阶段标签
const phaseTxt=el('text',{x:480,y:20,fill:'var(--accent)','font-size':'12','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600','letter-spacing':'.05em'},mSvg);

// 零能耗标注
const ifrTxt=el('text',{x:480,y:285,fill:'#00e676','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.7'},mSvg);
ifrTxt.textContent='⟳ 凸轮复用主轴旋转 — 零附加能耗 (IFR)';

/* ===== 凸轮轮廓生成 ===== */
function genCamPath(cx,cy,baseR,lift,rot){
  const pts=[];const n=180;
  for(let i=0;i<n;i++){
    const a=(i/n)*2*Math.PI;
    // 凸起在π方向(左侧),使用cos函数
    const rise=Math.max(0,-Math.cos(a));
    const r=baseR+rise*lift;
    const ra=a+rot;
    pts.push({x:cx+r*Math.cos(ra),y:cy+r*Math.sin(ra)});
  }
  let d=`M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`;
  for(let i=1;i<pts.length;i++)d+=` L${pts[i].x.toFixed(1)},${pts[i].y.toFixed(1)}`;
  return d+'Z';
}

// 凸轮凸起区域高亮路径
function genCamHighlight(cx,cy,baseR,lift,rot){
  const pts=[];const n=180;
  for(let i=0;i<n;i++){
    const a=(i/n)*2*Math.PI;
    const rise=Math.max(0,-Math.cos(a));
    if(rise<0.01)continue;
    const r=baseR+rise*lift;
    const ra=a+rot;
    pts.push({x:cx+r*Math.cos(ra),y:cy+r*Math.sin(ra)});
  }
  if(pts.length<3)return'';
  let d=`M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`;
  for(let i=1;i<pts.length;i++)d+=` L${pts[i].x.toFixed(1)},${pts[i].y.toFixed(1)}`;
  return d+'Z';
}

/* ===== 制动带弧线路径 ===== */
function brakeBandPath(cx,cy,r,tightness){
  const span=180+tightness*80;
  const offR=r+(1-tightness)*10;
  const s=(-span/2)*Math.PI/180;
  const e=(span/2)*Math.PI/180;
  const sx=cx+offR*Math.cos(s),sy=cy+offR*Math.sin(s);
  const ex=cx+offR*Math.cos(e),ey=cy+offR*Math.sin(e);
  const lg=span>180?1:0;
  return`M${sx.toFixed(1)},${sy.toFixed(1)} A${offR},${offR} 0 ${lg} 1 ${ex.toFixed(1)},${ey.toFixed(1)}`;
}

/* ===== 动画核心 ===== */
let lastT=0;

function resetState(){
  S.camAngle=0;S.heading=0;S.carX=80;S.carY=210;
  S.leftBrake=0;S.rightBrake=0;S.leftSpd=1;S.rightSpd=1;
  S.trail=[];S.time=0;S.lwAngle=0;S.rwAngle=0;
  trailPath.setAttribute('d','');
}

function update(dt){
  if(!S.running)return;
  const steps=Math.max(1,Math.round(dt/16));
  for(let s=0;s<steps;s++){
    S.camAngle+=S.camSpeed;
    S.time++;
    // 制动力
    S.leftBrake=Math.max(0,Math.sin(S.camAngle))*S.brakeForce;
    S.rightBrake=Math.max(0,-Math.sin(S.camAngle))*S.brakeForce;
    S.leftSpd=1-S.leftBrake;
    S.rightSpd=1-S.rightBrake;
    // 运动学
    const vL=S.leftSpd*CFG.baseSpeed;
    const vR=S.rightSpd*CFG.baseSpeed;
    const v=(vL+vR)/2;
    const omega=(vR-vL)/CFG.wheelSep;
    S.heading+=omega;
    S.carX+=v*Math.cos(S.heading);
    S.carY+=v*Math.sin(S.heading);
    S.lwAngle+=vL*.15;
    S.rwAngle+=vR*.15;
    S.trail.push({x:S.carX,y:S.carY});
    if(S.trail.length>CFG.trailMax)S.trail.shift();
    if(S.carX>920||S.carX<-20||S.carY>440||S.carY<-20){
      S.carX=80;S.carY=210;S.heading=0;S.trail=[];
    }
  }
}

function render(){
  const phi=S.camAngle;
  // 制动激活标志
  const lAct=S.leftBrake>0.03;
  const rAct=S.rightBrake>0.03;
  // 拨叉位移(与制动力同步)
  const dispA=Math.max(0,Math.sin(phi))*CAM.lift;
  const dispB=Math.max(0,-Math.sin(phi))*CAM.lift;

  // ===== 俯视图 =====
  if(S.trail.length>2){
    const rc=S.trail.slice(-700);
    let d=`M${rc[0].x.toFixed(1)},${rc[0].y.toFixed(1)}`;
    for(let i=1;i<rc.length;i+=2)d+=` L${rc[i].x.toFixed(1)},${rc[i].y.toFixed(1)}`;
    trailPath.setAttribute('d',d);
  }
  const deg=S.heading*180/Math.PI;
  carG.setAttribute('transform',`translate(${S.carX.toFixed(1)},${S.carY.toFixed(1)}) rotate(${deg.toFixed(1)})`);
  // 后轮指示
  const la=S.lwAngle,ra=S.rwAngle;
  lwInd.setAttribute('x2',(-20+8*Math.cos(la-Math.PI/2)).toFixed(1));
  lwInd.setAttribute('y2',(-CFG.wheelSep/2+8*Math.sin(la-Math.PI/2)).toFixed(1));
  rwInd.setAttribute('x2',(-20+8*Math.cos(ra-Math.PI/2)).toFixed(1));
  rwInd.setAttribute('y2',(CFG.wheelSep/2+8*Math.sin(ra-Math.PI/2)).toFixed(1));
  lwInd.setAttribute('stroke',lAct?'#ff7a1a':'#00e676');
  rwInd.setAttribute('stroke',rAct?'#ff7a1a':'#00e676');
  lbLight.setAttribute('opacity',clamp(S.leftBrake*2.5,0,.9).toFixed(2));
  rbLight.setAttribute('opacity',clamp(S.rightBrake*2.5,0,.9).toFixed(2));
  const turnDir=lAct?'← 左转':rAct?'→ 右转':'— 直行';
  tLS.textContent=`左轮速: ${S.leftSpd.toFixed(2)}`;
  tRS.textContent=`右轮速: ${S.rightSpd.toFixed(2)}`;
  tTN.textContent=`转向: ${turnDir}`;

  // ===== 机构图 =====
  // 凸轮轮廓
  camPath.setAttribute('d',genCamPath(CAM.cx,CAM.cy,CAM.baseR,CAM.lift,phi));
  camHighlight.setAttribute('d',genCamHighlight(CAM.cx,CAM.cy,CAM.baseR,CAM.lift,phi));
  // 凸轮指针
  const ptrAngle=phi;
  const ptrX=CAM.cx+40*Math.cos(ptrAngle);
  const ptrY=CAM.cy+40*Math.sin(ptrAngle);
  camPtr.setAttribute('x2',ptrX.toFixed(1));
  camPtr.setAttribute('y2',ptrY.toFixed(1));
  camDot.setAttribute('cx',ptrX.toFixed(1));
  camDot.setAttribute('cy',ptrY.toFixed(1));

  // 旋转方向弧线
  const arcR=CAM.baseR+CAM.lift+16;
  const arcStart=-Math.PI/4;
  const arcEnd=Math.PI/3;
  const as_x=CAM.cx+arcR*Math.cos(arcStart),as_y=CAM.cy+arcR*Math.sin(arcStart);
  const ae_x=CAM.cx+arcR*Math.cos(arcEnd),ae_y=CAM.cy+arcR*Math.sin(arcEnd);
  rotArrow.setAttribute('d',`M${as_x.toFixed(1)},${as_y.toFixed(1)} A${arcR},${arcR} 0 0 1 ${ae_x.toFixed(1)},${ae_y.toFixed(1)}`);
  // 箭头头部
  const arrAe=arcEnd;
  const arrDir=arrAe+Math.PI/2+0.3;
  const arrLen=8;

  // 拨叉A(上方,-y方向)
  const forkAY=CAM.cy-CAM.baseR-dispA;
  const rodALen=35;
  forkARod.setAttribute('y',forkAY-rodALen);
  forkARod.setAttribute('height',rodALen+5);
  forkARoller.setAttribute('cx',CAM.cx);
  forkARoller.setAttribute('cy',forkAY);
  forkAPin.setAttribute('cx',CAM.cx);
  forkAPin.setAttribute('cy',forkAY);
  // 拨叉A臂(L形连接到拉线点)
  const fAWireX=CAM.cx-50;
  const fAWireY=forkAY-rodALen+5;
  forkAArm.setAttribute('d',`M${CAM.cx},${forkAY-rodALen+5} L${CAM.cx},${forkAY-rodALen-5} L${fAWireX},${forkAY-rodALen-5} L${fAWireX},${forkAY-rodALen+10}`);
  forkAWire.setAttribute('cx',fAWireX);
  forkAWire.setAttribute('cy',forkAY-rodALen+10);
  forkAWire.setAttribute('opacity',lAct?'1':'0.3');
  forkALabel.setAttribute('x',CAM.cx-60);
  forkALabel.setAttribute('y',forkAY-rodALen-10);
  // 拨叉A发光
  forkARoller.setAttribute('stroke',lAct?'#ff7a1a':'#5a9aba');
  forkARod.setAttribute('stroke',lAct?'#ff7a1a':'#3a6a8a');
  forkARod.setAttribute('fill',lAct?'#2a1a0a':'#1e3a55');
  forkARoller.setAttribute('fill',lAct?'#2a1a0a':'#2a4a68');
  forkAG.setAttribute('filter',lAct?'url(#fkG)':'none');

  // 拨叉B(下方,+y方向)
  const forkBY=CAM.cy+CAM.baseR+dispB;
  forkBRod.setAttribute('y',forkBY-5);
  forkBRod.setAttribute('height',rodALen+5);
  forkBRoller.setAttribute('cx',CAM.cx);
  forkBRoller.setAttribute('cy',forkBY);
  forkBPin.setAttribute('cx',CAM.cx);
  forkBPin.setAttribute('cy',forkBY);
  const fBWireX=CAM.cx+50;
  const fBWireY=forkBY+rodALen-5;
  forkBArm.setAttribute('d',`M${CAM.cx},${forkBY+rodALen-5} L${CAM.cx},${forkBY+rodALen+5} L${fBWireX},${forkBY+rodALen+5} L${fBWireX},${forkBY+rodALen-10}`);
  forkBWire.setAttribute('cx',fBWireX);
  forkBWire.setAttribute('cy',forkBY+rodALen-10);
  forkBWire.setAttribute('opacity',rAct?'1':'0.3');
  forkBLabel.setAttribute('x',CAM.cx+60);
  forkBLabel.setAttribute('y',forkBY+rodALen+18);
  forkBRoller.setAttribute('stroke',rAct?'#ff7a1a':'#5a9aba');
  forkBRod.setAttribute('stroke',rAct?'#ff7a1a':'#3a6a8a');
  forkBRod.setAttribute('fill',rAct?'#2a1a0a':'#1e3a55');
  forkBRoller.setAttribute('fill',rAct?'#2a1a0a':'#2a4a68');
  forkBG.setAttribute('filter',rAct?'url(#fkG)':'none');

  // 拉线
  const wireAEndX=L_CX;
  const wireAEndY=AXLE_Y-36;
  wireA.setAttribute('d',`M${fAWireX},${forkAY-rodALen+10} Q${fAWireX-40},${(forkAY-rodALen+10+wireAEndY)/2} ${wireAEndX},${wireAEndY}`);
  wireA.setAttribute('opacity',lAct?'0.8':'0.15');
  wireA.setAttribute('stroke',lAct?'#ff7a1a':'#3a5a70');
  const wireBEndX=R_CX;
  const wireBEndY=AXLE_Y-36;
  wireB.setAttribute('d',`M${fBWireX},${forkBY+rodALen-10} Q${fBWireX+40},${(forkBY+rodALen-10+wireBEndY)/2} ${wireBEndX},${wireBEndY}`);
  wireB.setAttribute('opacity',rAct?'0.8':'0.15');
  wireB.setAttribute('stroke',rAct?'#ff7a1a':'#3a5a70');

  // 绕线轮
  const spoolAngle=phi*2;
  const sw_r=12;
  let sw_d='';
  for(let i=0;i<4;i++){
    const a=spoolAngle+i*Math.PI/2;
    const x1=CAM.cx+8*Math.cos(a),y1=AXLE_Y+8*Math.sin(a);
    const x2=CAM.cx+sw_r*Math.cos(a),y2=AXLE_Y+sw_r*Math.sin(a);
    sw_d+=`M${x1.toFixed(1)},${y1.toFixed(1)} L${x2.toFixed(1)},${y2.toFixed(1)} `;
  }
  spoolWire.setAttribute('d',sw_d);

  // 制动带
  const lTight=clamp(S.leftBrake/Math.max(0.01,S.brakeForce),0,1);
  const rTight=clamp(S.rightBrake/Math.max(0.01,S.brakeForce),0,1);
  lBrakeBand.setAttribute('d',brakeBandPath(L_CX,AXLE_Y,30,lTight));
  rBrakeBand.setAttribute('d',brakeBandPath(R_CX,AXLE_Y,30,rTight));
  lBrakeGlow.setAttribute('d',brakeBandPath(L_CX,AXLE_Y,30,lTight));
  rBrakeGlow.setAttribute('d',brakeBandPath(R_CX,AXLE_Y,30,rTight));
  lBrakeGlow.setAttribute('opacity',(lTight*.85).toFixed(2));
  rBrakeGlow.setAttribute('opacity',(rTight*.85).toFixed(2));
  lBrakeBand.setAttribute('stroke',lAct?'#ff7a1a':'#3a5a70');
  rBrakeBand.setAttribute('stroke',rAct?'#ff7a1a':'#3a5a70');

  // 车轮旋转
  lSpokes.setAttribute('transform',`rotate(${(S.lwAngle*180/Math.PI).toFixed(1)},${L_CX},${AXLE_Y})`);
  rSpokes.setAttribute('transform',`rotate(${(S.rwAngle*180/Math.PI).toFixed(1)},${R_CX},${AXLE_Y})`);

  // 速度条
  const barW=110;
  lBar.setAttribute('width',(S.leftSpd*barW).toFixed(1));
  rBar.setAttribute('width',(S.rightSpd*barW).toFixed(1));
  lBar.setAttribute('fill',lAct?'#ff7a1a':'#00e676');
  rBar.setAttribute('fill',rAct?'#ff7a1a':'#00e676');
  lSpdTxt.textContent=`v = ${S.leftSpd.toFixed(2)}`;
  rSpdTxt.textContent=`v = ${S.rightSpd.toFixed(2)}`;

  // 制动状态
  if(lAct&&!rAct){brakeTxt.textContent='◀ 左轮制动 → 右轮全速 → 小车左转';brakeTxt.setAttribute('fill','#ff7a1a');}
  else if(rAct&&!lAct){brakeTxt.textContent='右轮制动 ◀ → 左轮全速 → 小车右转';brakeTxt.setAttribute('fill','#ff7a1a');}
  else{brakeTxt.textContent='— 过渡区 —';brakeTxt.setAttribute('fill','#4a6580';}

  // 阶段标签
  if(lAct&&!rAct)phaseTxt.textContent='凸轮推程 → 拨叉A外移 → 左制动带收紧';
  else if(rAct&&!lAct)phaseTxt.textContent='凸轮推程 → 拨叉B外移 → 右制动带收紧';
  else phaseTxt.textContent='凸轮过渡区 — 制动切换中';
}

function loop(ts){
  const dt=lastT?Math.min(ts-lastT,50):16;
  lastT=ts;
  update(dt);
  render();
  requestAnimationFrame(loop);
}

/* ===== 控件 ===== */
document.getElementById('btnPlay').addEventListener('click',function(){
  S.running=!S.running;
  this.innerHTML=S.running?'<i class="fas fa-pause"></i> 暂停':'<i class="fas fa-play"></i> 播放';
  this.classList.toggle('active',S.running);
});
document.getElementById('btnReset').addEventListener('click',resetState);
document.getElementById('sBrake').addEventListener('input',function(){
  S.brakeForce=parseInt(this.value)/100;
  document.getElementById('vBrake').textContent=S.brakeForce.toFixed(2);
});
document.getElementById('sCam').addEventListener('input',function(){
  S.camSpeed=parseInt(this.value)/1000;
  document.getElementById('vCam').textContent=S.camSpeed.toFixed(3);
});

resetState();
requestAnimationFrame(loop);
</script>
</body>
</html>

等一下,我发现代码中有一个语法错误:brakeTxt.setAttribute('fill','#4a6580';} 应该是 brakeTxt.setAttribute('fill','#4a6580');}。让我修正这个并重新输出完整代码。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>周期性差速制动 — IFR 原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400;600;700&family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{--bg:#060a12;--panel:#0b1220;--border:#14253a;--fg:#c8d6e5;--muted:#4a6580;--accent:#00e5ff;--brake:#ff7a1a;--free:#00e676;--warn:#ff2d55;--card:#0f1a2a}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
header{width:100%;padding:22px 32px 6px;text-align:center;background:linear-gradient(180deg,rgba(0,229,255,.04) 0%,transparent 100%)}
header h1{font-family:'Chakra Petch',sans-serif;font-weight:700;font-size:clamp(18px,2.8vw,32px);letter-spacing:.06em;color:var(--accent);text-shadow:0 0 18px rgba(0,229,255,.4)}
header p{font-size:11px;color:var(--muted);margin-top:3px;letter-spacing:.03em}
.panels{display:flex;gap:10px;padding:8px 12px;width:100%;max-width:1560px;flex-wrap:wrap;justify-content:center}
.panel{flex:1 1 720px;max-width:780px;background:var(--panel);border:1px solid var(--border);border-radius:10px;overflow:hidden}
.plbl{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:11px;color:var(--accent);padding:7px 12px 0;letter-spacing:.07em;text-transform:uppercase;display:flex;align-items:center;gap:6px}
.plbl i{font-size:9px;opacity:.7}
.panel svg{display:block;width:100%;height:auto}
.ctrls{display:flex;gap:12px;padding:7px 18px 3px;flex-wrap:wrap;justify-content:center;align-items:center;max-width:1560px;width:100%}
.cg{display:flex;align-items:center;gap:6px;background:var(--card);border:1px solid var(--border);border-radius:7px;padding:6px 12px}
.cg label{font-size:10px;color:var(--muted);white-space:nowrap;font-family:'Chakra Petch',sans-serif;letter-spacing:.03em}
.cg input[type=range]{-webkit-appearance:none;appearance:none;width:100px;height:3px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
.cg input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px rgba(0,229,255,.5);cursor:pointer}
.cg .vl{font-size:11px;color:var(--accent);min-width:38px;text-align:right;font-weight:500}
.btn{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border:1px solid var(--border);border-radius:7px;background:var(--card);color:var(--fg);font-family:'Chakra Petch',sans-serif;font-size:11px;cursor:pointer;letter-spacing:.03em;transition:all .2s}
.btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 0 12px rgba(0,229,255,.3)}
.btn.on{background:rgba(0,229,255,.08);border-color:var(--accent);color:var(--accent)}
.btn i{font-size:10px}
.info{display:flex;gap:10px;padding:5px 18px 16px;flex-wrap:wrap;justify-content:center;max-width:1560px;width:100%}
.ic{flex:1 1 260px;max-width:500px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px}
.ic h3{font-family:'Chakra Petch',sans-serif;font-weight:600;font-size:12px;margin-bottom:5px;display:flex;align-items:center;gap:6px}
.ic h3.ifr{color:var(--accent)}.ic h3.rsk{color:var(--warn)}
.ic li{font-size:10px;color:var(--muted);line-height:1.7}
.ic ul{padding-left:13px}
.tg{display:inline-block;font-size:8px;padding:1px 5px;border-radius:3px;font-weight:500;vertical-align:middle}
.tg-a{background:rgba(0,229,255,.1);color:var(--accent);border:1px solid rgba(0,229,255,.2)}
.tg-b{background:rgba(255,122,26,.1);color:var(--brake);border:1px solid rgba(255,122,26,.2)}
.tg-f{background:rgba(0,230,118,.08);color:var(--free);border:1px solid rgba(0,230,118,.18)}
</style>
</head>
<body>
<header>
  <h1>周期性差速制动 — 最终理想解</h1>
  <p>固定前轴 + 端面凸轮驱动双拨叉交替制动 → 全势能推进、自动S型绕行</p>
</header>
<div class="panels">
  <div class="panel"><div class="plbl"><i class="fas fa-route"></i> 俯视轨迹 — S型自动绕行</div><svg id="tS" viewBox="0 0 900 420"></svg></div>
  <div class="panel"><div class="plbl"><i class="fas fa-cogs"></i> 差速制动机构 — 端面凸轮驱动</div><svg id="mS" viewBox="0 0 960 580"></svg></div>
</div>
<div class="ctrls">
  <button class="btn on" id="bP"><i class="fas fa-pause"></i> 暂停</button>
  <button class="btn" id="bR"><i class="fas fa-redo"></i> 重置</button>
  <div class="cg"><label>制动力矩</label><input type="range" id="sB" min="5" max="80" value="35"><span class="vl" id="vB">0.35</span></div>
  <div class="cg"><label>凸轮转速</label><input type="range" id="sC" min="8" max="60" value="28"><span class="vl" id="vC">0.028</span></div>
</div>
<div class="info">
  <div class="ic"><h3 class="ifr"><i class="fas fa-bolt"></i> IFR 最终理想解</h3><ul>
    <li>前轮固定死轴 — <span class="tg tg-f">零侧滑</span> 全势能推进</li>
    <li>端面凸轮复用主轴旋转 — <span class="tg tg-a">零附加能耗</span> 转向自行发生</li>
    <li>差速制动替代偏转 — <span class="tg tg-b">低耗能</span> 履带差速原理</li>
  </ul></div>
  <div class="ic"><h3 class="rsk"><i class="fas fa-exclamation-triangle"></i> 失效边界</h3><ul>
    <li>地面附着系数须恒定,打滑即失效</li>
    <li>制动力标定极难:过制动浪费动能,不足则撞柱</li>
    <li>推程 <span class="tg tg-a">3mm</span> / 力矩 <span class="tg tg-b">0.01Nm</span> 须精确匹配</li>
  </ul></div>
</div>

<script>
const NS='http://www.w3.org/2000/svg';
function $(t,a,p){const e=document.createElementNS(NS,t);for(const[k,v]of Object.entries(a||{}))e.setAttribute(k,v);p&&p.appendChild(e);return e}
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}

/* ===== 状态 ===== */
const S={ca:0,hd:0,cx:80,cy:210,lb:0,rb:0,ls:1,rs:1,tr:[],on:true,bf:.35,cs:.028,t:0,la:0,ra:0};
const W=54,BS=1.6,CL=62,CW=26,TM=3500,PS=140;

/* ===== 俯视图 ===== */
const tS=document.getElementById('tS');
const tD=$('defs',{},tS);
const tGr=$('linearGradient',{id:'tGr',x1:'0%',y1:'0%',x2:'100%',y2:'0%'},tD);
$('stop',{offset:'0%','stop-color':'rgba(0,229,255,0)'},tGr);
$('stop',{offset:'25%','stop-color':'rgba(0,229,255,0.35)'},tGr);
$('stop',{offset:'100%','stop-color':'#00e5ff'},tGr);
const fG=$('filter',{id:'fG',x:'-50%',y:'-50%',width:'200%',height:'200%'},tD);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'3',result:'b'},fG);
const fM=$('feMerge',{},fG);$('feMergeNode',{in:'b'},fM);$('feMergeNode',{in:'SourceGraphic'},fM);

// 网格+中心线
const tGrid=$('g',{opacity:'.06'},tS);
for(let x=0;x<=900;x+=40)$('line',{x1:x,y1:0,x2:x,y2:420,stroke:'#00e5ff','stroke-width':'.5'},tGrid);
for(let y=0;y<=420;y+=40)$('line',{x1:0,y1:y,x2:900,y2:y,stroke:'#00e5ff','stroke-width':'.5'},tGrid);
$('line',{x1:0,y1:210,x2:900,y2:210,stroke:'#14253a','stroke-width':'1','stroke-dasharray':'8,6'},tS);

// 障碍柱
for(let x=100;x<900;x+=PS){for(const cy of[110,310]){
  $('circle',{cx:x,cy:cy,r:7,fill:'none',stroke:'#1e3a50','stroke-width':'1.2','stroke-dasharray':'3,3'},tS);
  $('circle',{cx:x,cy:cy,r:2,fill:'#1e3a50'},tS);
}}

const tPath=$('path',{fill:'none',stroke:'url(#tGr)','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round',filter:'url(#fG)'},tS);

// 车辆
const cG=$('g',{},tS);
$('rect',{x:-CL/2,y:-CW/2,width:CL,height:CW,rx:5,fill:'#1a2d42',stroke:'#2a4a68','stroke-width':'1.2'},cG);
// 前轮固定
const fw=$('g',{},cG);
$('rect',{x:22,y:-5,width:9,height:10,rx:1.5,fill:'#3a5a78',stroke:'#5a8ab0','stroke-width':'.8'},fw);
$('line',{x1:26.5,y1:-7,x2:26.5,y2:7,stroke:'#00e676','stroke-width':'1.5'},fw);
const fwt=$('text',{x:26.5,y:-11,fill:'#00e676','font-size':'7','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'},fw);fwt.textContent='FIXED';
// 左后轮
$('circle',{cx:-20,cy:-W/2,r:8,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'},cG);
const lI=$('line',{x1:-20,y1:-W/2,x2:-20,y1:-W/2-8,stroke:'#00e676','stroke-width':'1.5','stroke-linecap':'round'},cG);
const lBL=$('circle',{cx:-20,cy:-W/2,r:11,fill:'none',stroke:'#ff7a1a','stroke-width':'1.8',opacity:'0'},cG);
// 右后轮
$('circle',{cx:-20,cy:W/2,r:8,fill:'#1a2535',stroke:'#4a7a9a','stroke-width':'1'},cG);
const rI=$('line',{x1:-20,y1:W/2,x2:-20,y2:W/2+8,stroke:'#00e676','stroke-width':'1.5','stroke-linecap':'round'},cG);
const rBL=$('circle',{cx:-20,cy:W/2,r:11,fill:'none',stroke:'#ff7a1a','stroke-width':'1.8',opacity:'0'},cG);
$('polygon',{points:'33,0 26,-4 26,4',fill:'#00e5ff',opacity:'.6'},cG);

// 数据
const dG=$('g',{},tS);
const t1=$('text',{x:14,y:392,fill:'#4a6580','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const t2=$('text',{x:14,y:407,fill:'#4a6580','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const t3=$('text',{x:170,y:392,fill:'#00e5ff','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);
const t4=$('text',{x:170,y:407,fill:'#00e676','font-size':'10','font-family':'IBM Plex Mono,monospace'},dG);t4.textContent='侧滑损耗: 0% (IFR)';

/* ===== 机构图 ===== */
const mS=document.getElementById('mS');
const mD=$('defs',{},mS);
const bF=$('filter',{id:'bF',x:'-80%',y:'-80%',width:'260%',height:'260%'},mD);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'5',result:'b'},bF);
const bM=$('feMerge',{},bF);$('feMergeNode',{in:'b'},bM);$('feMergeNode',{in:'SourceGraphic'},bM);
const fF=$('filter',{id:'fF',x:'-60%',y:'-60%',width:'220%',height:'220%'},mD);
$('feGaussianBlur',{in:'SourceGraphic',stdDeviation:'3',result:'b'},fF);
const fMm=$('feMerge',{},fF);$('feMergeNode',{in:'b'},fMm);$('feMergeNode',{in:'SourceGraphic'},fMm);

// 网格
const mGr=$('g',{opacity:'.04'},mS);
for(let x=0;x<=960;x+=40)$('line',{x1:x,y1:0,x2:x,y2:580,stroke:'#00e5ff','stroke-width':'.5'},mGr);
for(let y=0;y<=580;y+=40)$('line',{x1:0,y1:y,x2:960,y2:y,stroke:'#00e5ff','stroke-width':'.5'},mGr);

/* --- 凸轮区域 --- */
const CCX=480,CCY=148,CB=58,CL_=22;
// 参考圆
$('circle',{cx:CCX,cy:CCY,r:CB+CL_+10,fill:'none',stroke:'#0d1f30','stroke-width':'1','stroke-dasharray':'4,4'},mS);
$('circle',{cx:CCX,cy:CCY,r:CB-5,fill:'#070d16',stroke:'#0d1f30','stroke-width':'1'},mS);
// 凸轮轮廓
const cPath=$('path',{fill:'#0e1a28',stroke:'#2a5a7a','stroke-width':'1.5'},mS);
const cHi=$('path',{fill:'rgba(0,229,255,0.06)',stroke:'none'},mS);
// 中心轴
$('circle',{cx:CCX,cy:CCY,r:7,fill:'#1a2d42',stroke:'#00e5ff','stroke-width':'1.5'},mS);
// 旋转指针
const cPtr=$('line',{x1:CCX,y1:CCY,x2:CCX+42,y2:CCY,stroke:'#00e5ff','stroke-width':'2','stroke-linecap':'round'},mS);
const cDot=$('circle',{cx:CCX+42,cy:CCY,r:3.5,fill:'#00e5ff'},mS);
// 旋转方向弧
const rArc=$('path',{fill:'none',stroke:'#00e5ff','stroke-width':'1.2',opacity:'.5'},mS);
// 凸轮标注
$('text',{x:CCX,y:CCY-CB-CL_-22,fill:'var(--accent)','font-size':'11','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600','letter-spacing':'.06em'},mS).textContent='端面凸轮';
// 推程标注(动态)
const liftLabel=$('text',{x:CCX+CB+CL_+16,y:CCY,fill:'var(--accent)','font-size':'9','font-family':'IBM Plex Mono,monospace',opacity:'0'},mS);

/* --- 拨叉A(上方) --- */
const fAG=$('g',{},mS);
const fAR=$('rect',{x:CCX-4,width:8,rx:3,fill:'#1e3a55',stroke:'#3a6a8a','stroke-width':'1'},fAG);
const fARl=$('circle',{r:7,fill:'#2a4a68',stroke:'#5a9aba','stroke-width':'1.2'},fAG);
const fAPn=$('circle',{r:2.5,fill:'#5a9aba'},fAG);
const fAAm=$('path',{fill:'none',stroke:'#3a6a8a','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round'},fAG);
const fAWr=$('circle',{r:4,fill:'#ff7a1a',stroke:'#ff9944','stroke-width':'1',opacity:'.3'},fAG);
const fALb=$('text',{fill:'#ff7a1a','font-size':'10','font-family':'Chakra Petch,sans-serif','font-weight':'600'},fAG);fALb.textContent='拨叉A';
const fALb2=$('text',{fill:'#ff7a1a','font-size':'8','font-family':'IBM Plex Mono,monospace',opacity:'.7'},fAG);fALb2.textContent='→ 左制动带';
// 回位弹簧符号
const fASpr=$('path',{fill:'none',stroke:'#5a9aba','stroke-width':'1.2'},fAG);

/* --- 拨叉B(下方) --- */
const fBG=$('g',{},mS);
const fBR=$('rect',{x:CCX-4,width:8,rx:3,fill:'#1e3a55',stroke:'#3a6a8a','stroke-width':'1'},fBG);
const fBRl=$('circle',{r:7,fill:'#2a4a68',stroke:'#5a9aba','stroke-width':'1.2'},fBG);
const fBPn=$('circle',{r:2.5,fill:'#5a9aba'},fBG);
const fBAm=$('path',{fill:'none',stroke:'#3a6a8a','stroke-width':'2.5','stroke-linecap':'round','stroke-linejoin':'round'},fBG);
const fBWr=$('circle',{r:4,fill:'#ff7a1a',stroke:'#ff9944','stroke-width':'1',opacity:'.3'},fBG);
const fBLb=$('text',{fill:'#ff7a1a','font-size':'10','font-family':'Chakra Petch,sans-serif','font-weight':'600'},fBG);fBLb.textContent='拨叉B';
const fBLb2=$('text',{fill:'#ff7a1a','font-size':'8','font-family':'IBM Plex Mono,monospace',opacity:'.7'},fBG);fBLb2.textContent='→ 右制动带';
const fBSpr=$('path',{fill:'none',stroke:'#5a9aba','stroke-width':'1.2'},fBG);

/* --- 拉线 --- */
const wA=$('path',{fill:'none',stroke:'#3a5a70','stroke-width':'1.2','stroke-dasharray':'6,4',opacity:'.2'},mS);
const wB=$('path',{fill:'none',stroke:'#3a5a70','stroke-width':'1.2','stroke-dasharray':'6,4',opacity:'.2'},mS);

/* --- 后桥 --- */
const AY=440;
// 主轴
$('line',{x1:90,y1:AY,x2:870,y2:AY,stroke:'#1e3a55','stroke-width':'3','stroke-linecap':'round'},mS);
$('text',{x:CCX,y:AY-22,fill:'#4a6580','font-size':'9','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mS).textContent='主轴';

// 绕线轮
const spG=$('g',{},mS);
$('rect',{x:CCX-24,y:AY-18,width:48,height:36,rx:7,fill:'#0e1825',stroke:'#2a5a7a','stroke-width':'1.2'},spG);
$('line',{x1:CCX-15,y1:AY-12,x2:CCX-15,y2:AY+12,stroke:'#2a5a7a','stroke-width':'.8'},spG);
$('line',{x1:CCX,y1:AY-12,x2:CCX,y2:AY+12,stroke:'#2a5a7a','stroke-width':'.8'},spG);
$('line',{x1:CCX+15,y1:AY-12,x2:CCX+15,y2:AY+12,stroke:'#2a5a7a','stroke-width':'.8'},spG);
const spW=$('path',{fill:'none',stroke:'#4a7a9a','stroke-width':'1.5'},spG);
$('text',{x:CCX,y:AY+30,fill:'#4a6580','font-size':'9','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},spG).textContent='主绕线轮';
// 拉线释放箭头
$('text',{x:CCX,y:AY+42,fill:'#00e676','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.6'},spG).textContent='↓ 拉力释放驱动旋转';

// --- 左侧 ---
const LX=200;
$('circle',{cx:LX,cy:AY,r:34,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'},mS);
$('text',{x:LX,y:AY+3,fill:'#2a4a68','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mS).textContent='制动鼓';
const lBB=$('path',{fill:'none',stroke:'#3a5a70','stroke-width':'5','stroke-linecap':'round'},mS);
const lBG=$('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'6','stroke-linecap':'round',opacity:'0',filter:'url(#bF)'},mS);
// 单向轴承
$('circle',{cx:LX,cy:AY-44,r:7,fill:'#0e1825',stroke:'#00e676','stroke-width':'1'},mS);
$('path',{d:`M${LX-4},${AY-46} L${LX+4},${AY-46} L${LX+2},${AY-48}`,fill:'none',stroke:'#00e676','stroke-width':'1'},mS);
$('text',{x:LX,y:AY-56,fill:'#00e676','font-size':'7','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.7'},mS).textContent='单向轴承';
// 左轮
$('circle',{cx:LX,cy:AY,r:52,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'},mS);
const lSp=$('g',{},mS);
for(let i=0;i<8;i++){const a=i*Math.PI/4;$('line',{x1:LX+16*Math.cos(a),y1:AY+16*Math.sin(a),x2:LX+48*Math.cos(a),y2:AY+48*Math.sin(a),stroke:'#2a4a68','stroke-width':'.7'},lSp);}
$('circle',{cx:LX,cy:AY,r:14,fill:'#0e1825',stroke:'#2a4a68','stroke-width':'1'},mS);
$('text',{x:LX,y:AY-60,fill:'#4a6580','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},mS).textContent='左轮';
// 速度条
$('rect',{x:LX-55,y:AY+60,width:110,height:7,rx:3.5,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'.6'},mS);
const lBr=$('rect',{x:LX-55,y:AY+60,width:110,height:7,rx:3.5,fill:'#00e676',opacity:'.8'},mS);
const lST=$('text',{x:LX,y:AY+80,fill:'#c8d6e5','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'},mS);

// --- 右侧 ---
const RX=760;
$('circle',{cx:RX,cy:AY,r:34,fill:'#0a0f18',stroke:'#1e3a55','stroke-width':'1.5'},mS);
$('text',{x:RX,y:AY+3,fill:'#2a4a68','font-size':'8','text-anchor':'middle','font-family':'IBM Plex Mono,monospace'},mS).textContent='制动鼓';
const rBB=$('path',{fill:'none',stroke:'#3a5a70','stroke-width':'5','stroke-linecap':'round'},mS);
const rBG=$('path',{fill:'none',stroke:'#ff7a1a','stroke-width':'6','stroke-linecap':'round',opacity:'0',filter:'url(#bF)'},mS);
$('circle',{cx:RX,cy:AY-44,r:7,fill:'#0e1825',stroke:'#00e676','stroke-width':'1'},mS);
$('path',{d:`M${RX-4},${AY-46} L${RX+4},${AY-46} L${RX+2},${AY-48}`,fill:'none',stroke:'#00e676','stroke-width':'1'},mS);
$('text',{x:RX,y:AY-56,fill:'#00e676','font-size':'7','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.7'},mS).textContent='单向轴承';
$('circle',{cx:RX,cy:AY,r:52,fill:'none',stroke:'#2a4a68','stroke-width':'1','stroke-dasharray':'3,3'},mS);
const rSp=$('g',{},mS);
for(let i=0;i<8;i++){const a=i*Math.PI/4;$('line',{x1:RX+16*Math.cos(a),y1:AY+16*Math.sin(a),x2:RX+48*Math.cos(a),y2:AY+48*Math.sin(a),stroke:'#2a4a68','stroke-width':'.7'},rSp);}
$('circle',{cx:RX,cy:AY,r:14,fill:'#0e1825',stroke:'#2a4a68','stroke-width':'1'},mS);
$('text',{x:RX,y:AY-60,fill:'#4a6580','font-size':'10','text-anchor':'middle','font-family':'Chakra Petch,sans-serif'},mS).textContent='右轮';
$('rect',{x:RX-55,y:AY+60,width:110,height:7,rx:3.5,fill:'#0a1520',stroke:'#1a2a3a','stroke-width':'.6'},mS);
const rBr=$('rect',{x:RX-55,y:AY+60,width:110,height:7,rx:3.5,fill:'#00e676',opacity:'.8'},mS);
const rST=$('text',{x:RX,y:AY+80,fill:'#c8d6e5','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace','font-weight':'500'},mS);

// 状态文字
const bTx=$('text',{x:CCX,y:AY+80,fill:'#c8d6e5','font-size':'11','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600'},mS);
const pTx=$('text',{x:CCX,y:18,fill:'#00e5ff','font-size':'12','text-anchor':'middle','font-family':'Chakra Petch,sans-serif','font-weight':'600','letter-spacing':'.05em'},mS);
// IFR标注
$('text',{x:CCX,y:295,fill:'#00e676','font-size':'10','text-anchor':'middle','font-family':'IBM Plex Mono,monospace',opacity:'.6'},mS).textContent='⟳ 凸轮复用主轴旋转 — 零附加能耗 (IFR)';

/* ===== 凸轮轮廓 ===== */
function camPath(cx,cy,b,l,rot){
  const p=[];const n=180;
  for(let i=0;i<n;i++){
    const a=(i/n)*2*Math.PI;
    const rise=Math.max(0,-Math.cos(a));
    const r=b+rise*l;
    const ra=a+rot;
    p.push({x:cx+r*Math.cos(ra),y:cy+r*Math.sin(ra)});
  }
  let d=`M${p[0].x.toFixed(1)},${p[0].y.toFixed(1)}`;
  for(let i=1;i<p.length;i++)d+=` L${p[i].x.toFixed(1)},${p[i].y.toFixed(1)}`;
  return d+'Z';
}
function camHi(cx,cy,b,l,rot){
  const p=[];
  for(let i=0;i<180;i++){
    const a=(i/180)*2*Math.PI;
    const rise=Math.max(0,-Math.cos(a));
    if(rise<.01)continue;
    const r=b+rise*l;const ra=a+rot;
    p.push({x:cx+r*Math.cos(ra),y:cy+r*Math.sin(ra)});
  }
  if(p.length<3)return'';
  let d=`M${p[0].x.toFixed(1)},${p[0].y.toFixed(1)}`;
  for(let i=1;i<p.length;i++)d+=` L${p[i].x.toFixed(1)},${p[i].y.toFixed(1)}`;
  return d+'Z';
}
/* 制动带弧线 */
function bbPath(cx,cy,r,t){
  const span=180+t*80;const offR=r+(1-t)*10;
  const s=(-span/2)*Math.PI/180,e=(span/2)*Math.PI/180;
  const sx=cx+offR*Math.cos(s),sy=cy+offR*Math.sin(s);
  const ex=cx+offR*Math.cos(e),ey=cy+offR*Math.sin(e);
  return`M${sx.toFixed(1)},${sy.toFixed(1)} A${offR},${offR} 0 ${span>180?1:0} 1 ${ex.toFixed(1)},${ey.toFixed(1)}`;
}
/* 弹簧路径 */
function sprPath(x1,y1,x2,y2,coils){
  const dx=x2-x1,dy=y2-y1,len=Math.sqrt(dx*dx+dy*dy);
  if(len<1)return`M${x1},${y1} L${x2},${y2}`;
  const ux=dx/len,uy=dy/len,nx=-uy,ny=ux;
  const amp=4;const segs=coils*2;
  let d=`M${x1},${y1} L${(x1+ux*6).toFixed(1)},${(y1+uy*6).toFixed(1)}`;
  for(let i=0;i<segs;i++){
    const t=(i+1)/(segs+1);
    const px=x1+dx*t+nx*amp*(i%2===0?1:-1);
    const py=y1+dy*t+ny*amp*(i%2===0?1:-1);
    d+=` L${px.toFixed(1)},${py.toFixed(1)}`;
  }
  d+=` L${(x2-ux*6).toFixed(1)},${(y2-uy*6).toFixed(1)} L${x2},${y2}`;
  return d;
}

/* ===== 动画 ===== */
let lastT=0;
function reset(){S.ca=0;S.hd=0;S.cx=80;S.cy=210;S.lb=0;S.rb=0;S.ls=1;S.rs=1;S.tr=[];S.t=0;S.la=0;S.ra=0;tPath.setAttribute('d','');}

function update(dt){
  if(!S.on)return;
  const steps=Math.max(1,Math.round(dt/16));
  for(let s=0;s<steps;s++){
    S.ca+=S.cs;S.t++;
    S.lb=Math.max(0,Math.sin(S.ca))*S.bf;
    S.rb=Math.max(0,-Math.sin(S.ca))*S.bf;
    S.ls=1-S.lb;S.rs=1-S.rb;
    const vL=S.ls*BS,vR=S.rs*BS,v=(vL+vR)/2,om=(vR-vL)/W;
    S.hd+=om;S.cx+=v*Math.cos(S.hd);S.cy+=v*Math.sin(S.hd);
    S.la+=vL*.15;S.ra+=vR*.15;
    S.tr.push({x:S.cx,y:S.cy});
    if(S.tr.length>TM)S.tr.shift();
    if(S.cx>920||S.cx<-20||S.cy>440||S.cy<-20){S.cx=80;S.cy=210;S.hd=0;S.tr=[];}
  }
}

function render(){
  const phi=S.ca;
  const lA=S.lb>.03,rA=S.rb>.03;
  const dA=Math.max(0,Math.sin(phi))*CL_;
  const dB=Math.max(0,-Math.sin(phi))*CL_;

  // --- 俯视图 ---
  if(S.tr.length>2){
    const rc=S.tr.slice(-700);
    let d=`M${rc[0].x.toFixed(1)},${rc[0].y.toFixed(1)}`;
    for(let i=1;i<rc.length;i+=2)d+=` L${rc[i].x.toFixed(1)},${rc[i].y.toFixed(1)}`;
    tPath.setAttribute('d',d);
  }
  cG.setAttribute('transform',`translate(${S.cx.toFixed(1)},${S.cy.toFixed(1)}) rotate(${(S.hd*180/Math.PI).toFixed(1)})`);
  const la=S.la,ra=S.ra;
  lI.setAttribute('x2',(-20+8*Math.cos(la-Math.PI/2)).toFixed(1));
  lI.setAttribute('y2',(-W/2+8*Math.sin(la-Math.PI/2)).toFixed(1));
  rI.setAttribute('x2',(-20+8*Math.cos(ra-Math.PI/2)).toFixed(1));
  rI.setAttribute('y2',(W/2+8*Math.sin(ra-Math.PI/2)).toFixed(1));
  lI.setAttribute('stroke',lA?'#ff7a1a':'#00e676');
  rI.setAttribute('stroke',rA?'#ff7a1a':'#00e676');
  lBL.setAttribute('opacity',clamp(S.lb*2.5,0,.9).toFixed(2));
  rBL.setAttribute('opacity',clamp(S.rb*2.5,0,.9).toFixed(2));
  t1.textContent=`左轮速: ${S.ls.toFixed(2)}`;
  t2.textContent=`右轮速: ${S.rs.toFixed(2)}`;
  t3.textContent=`转向: ${lA?'← 左转':rA?'→ 右转':'— 直行'}`;

  // --- 机构图 ---
  // 凸轮
  cPath.setAttribute('d',camPath(CCX,CCY,CB,CL_,phi));
  cHi.setAttribute('d',camHi(CCX,CCY,CB,CL_,phi));
  const px=CCX+42*Math.cos(phi),py=CCY+42*Math.sin(phi);
  cPtr.setAttribute('x2',px.toFixed(1));cPtr.setAttribute('y2',py.toFixed(1));
  cDot.setAttribute('cx',px.toFixed(1));cDot.setAttribute('cy',py.toFixed(1));
  // 旋转方向弧
  const aR=CB+CL_+18;
  const as=Math.PI*0.7,ae=Math.PI*1.3;
  rArc.setAttribute('d',`M${(CCX+aR*Math.cos(as)).toFixed(1)},${(CCY+aR*Math.sin(as)).toFixed(1)} A${aR},${aR} 0 0 1 ${(CCX+aR*Math.cos(ae)).toFixed(1)},${(CCY+aR*Math.sin(ae)).toFixed(1)}`);
  // 推程标注
  const liftActive=dA>1||dB>1;
  liftLabel.setAttribute('opacity',liftActive?'.8':'0');
  liftLabel.textContent=`推程 ${Math.max(dA,dB).toFixed(1)}px`;

  // 拨叉A(上方)
  const fAY=CCY-CB-dA;
  const rodL=38;
  fAR.setAttribute('y',fAY-rodL);fAR.setAttribute('height',rodL+6);
  fARl.setAttribute('cx',CCX);fARl.setAttribute('cy',fAY);
  fAPn.setAttribute('cx',CCX);fAPn.setAttribute('cy',fAY);
  // L形臂
  const armAX=CCX-55,armAY=fAY-rodL+6;
  fAAm.setAttribute('d',`M${CCX},${fAY-rodL+6} L${CCX},${fAY-rodL-8} L${armAX},${fAY-rodL-8} L${armAX},${armAY+12}`);
  fAWr.setAttribute('cx',armAX);fAWr.setAttribute('cy',armAY+12);
  fAWr.setAttribute('opacity',lA?'1':'.3');
  fALb.setAttribute('x',armAX-5);fALb.setAttribute('y',fAY-rodL-12);
  fALb2.setAttribute('x',armAX-5);fALb2.setAttribute('y',fAY-rodL-1);
  // 弹簧(回位)
  fASpr.setAttribute('d',sprPath(CCX+8,fAY-10,CCX+8,fAY-rodL+2,4));
  // 拨叉A颜色
  fARl.setAttribute('stroke',lA?'#ff7a1a':'#5a9aba');
  fARl.setAttribute('fill',lA?'#2a1a0a':'#2a4a68');
  fAR.setAttribute('stroke',lA?'#ff7a1a':'#3a6a8a');
  fAR.setAttribute('fill',lA?'#2a1a0a':'#1e3a55');
  fAAm.setAttribute('stroke',lA?'#ff7a1a':'#3a6a8a');
  fAG.setAttribute('filter',lA?'url(#fF)':'none');

  // 拨叉B(下方)
  const fBY=CCY+CB+dB;
  fBR.setAttribute('y',fBY-6);fBR.setAttribute('height',rodL+6);
  fBRl.setAttribute('cx',CCX);fBRl.setAttribute('cy',fBY);
  fBPn.setAttribute('cx',CCX);fBPn.setAttribute('cy',fBY);
  const armBX=CCX+55,armBY=fBY+rodL-6;
  fBAm.setAttribute('d',`M${CCX},${fBY+rodL-6} L${CCX},${fBY+rodL+8} L${armBX},${fBY+rodL+8} L${armBX},${armBY-12}`);
  fBWr.setAttribute('cx',armBX);fBWr.setAttribute('cy',armBY-12);
  fBWr.setAttribute('opacity',rA?'1':'.3');
  fBLb.setAttribute('x',armBX+5);fBLb.setAttribute('y',fBY+rodL+20);
  fBLb2.setAttribute('x',armBX+5);fBLb2.setAttribute('y',fBY+rodL+31);
  fBSpr.setAttribute('d',sprPath(CCX+8,fBY+10,CCX+8,fBY+rodL-2,4));
  fBRl.setAttribute('stroke',rA?'#ff7a1a':'#5a9aba');
  fBRl.setAttribute('fill',rA?'#2a1a0a':'#2a4a68');
  fBR.setAttribute('stroke',rA?'#ff7a1a':'#3a6a8a');
  fBR.setAttribute('fill',rA?'#2a1a0a':'#1e3a55');
  fBAm.setAttribute('stroke',rA?'#ff7a1a':'#3a6a8a');
  fBG.setAttribute('filter',rA?'url(#fF)':'none');

  // 拉线
  const wAEx=LX,wAEy=AY-36;
  wA.setAttribute('d',`M${armAX},${armAY+12} C${armAX-50},${(armAY+12+wAEy)/2} ${wAEx+40},${wAEy-30} ${wAEx},${wAEy}`);
  wA.setAttribute('opacity',lA?'.7':'.12');
  wA.setAttribute('stroke',lA?'#ff7a1a':'#3a5a70');
  const wBEx=RX,wBEy=AY-36;
  wB.setAttribute('d',`M${armBX},${armBY-12} C${armBX+50},${(armBY-12+wBEy)/2} ${wBEx-40},${wBEy-30} ${wBEx},${wBEy}`);
  wB.setAttribute('opacity',rA?'.7':'.12');
  wB.setAttribute('stroke',rA?'#ff7a1a':'#3a5a70');

  // 绕线轮旋转
  const sa=phi*2;
  let sd='';
  for(let i=0;i<4;i++){const a=sa+i*Math.PI/2;
    sd+=`M${(CCX+8*Math.cos(a)).toFixed(1)},${(AY+8*Math.sin(a)).toFixed(1)} L${(CCX+14*Math.cos(a)).toFixed(1)},${(AY+14*Math.sin(a)).toFixed(1)} `}
  spW.setAttribute('d',sd);

  // 制动带
  const lT=clamp(S.lb/Math.max(.01,S.bf),0,1);
  const rT=clamp(S.rb/Math.max(.01,S.bf),0,1);
  lBB.setAttribute('d',bbPath(LX,AY,30,lT));
  rBB.setAttribute('d',bbPath(RX,AY,30,rT));
  lBG.setAttribute('d',bbPath(LX,AY,30,lT));
  rBG.setAttribute('d',bbPath(RX,AY,30,rT));
  lBG.setAttribute('opacity',(lT*.85).toFixed(2));
  rBG.setAttribute('opacity',(rT*.85).toFixed(2));
  lBB.setAttribute('stroke',lA?'#ff7a1a':'#3a5a70');
  rBB.setAttribute('stroke',rA?'#ff7a1a':'#3a5a70');

  // 车轮旋转
  lSp.setAttribute('transform',`rotate(${(S.la*180/Math.PI).toFixed(1)},${LX},${AY})`);
  rSp.setAttribute('transform',`rotate(${(S.ra*180/Math.PI).toFixed(1)},${RX},${AY})`);

  // 速度条
  const bW=110;
  lBr.setAttribute('width',(S.ls*bW).toFixed(1));
  rBr.setAttribute('width',(S.rs*bW).toFixed(1));
  lBr.setAttribute('fill',lA?'#ff7a1a':'#00e676');
  rBr.setAttribute('fill',rA?'#ff7a1a':'#00e676');
  lST.textContent=`v = ${S.ls.toFixed(2)}`;
  rST.textContent=`v = ${S.rs.toFixed(2)}`;

  // 制动状态
  if(lA&&!rA){bTx.textContent='◀ 左轮制动 → 右轮全速 → 小车左转';bTx.setAttribute('fill','#ff7a1a');}
  else if(rA&&!lA){bTx.textContent='右轮制动 ◀ → 左轮全速 → 小车右转';bTx.setAttribute('fill','#ff7a1a');}
  else{bTx.textContent='— 过渡区 —';bTx.setAttribute('fill','#4a6580');}

  // 阶段
  if(lA&&!rA)pTx.textContent='凸轮推程 → 拨叉A外移 → 左制动带收紧 → 左轮降速';
  else if(rA&&!lA)pTx.textContent='凸轮推程 → 拨叉B外移 → 右制动带收紧 → 右轮降速';
  else pTx.textContent='凸轮过渡区 — 制动切换中';
}

function loop(ts){
  const dt=lastT?Math.min(ts-lastT,50):16;lastT=ts;
  update(dt);render();requestAnimationFrame(loop);
}

/* 控件 */
document.getElementById('bP').addEventListener('click',function(){
  S.on=!S.on;
  this.innerHTML=S.on?'<i class="fas fa-pause"></i> 暂停':'<i class="fas fa-play"></i> 播放';
  this.classList.toggle('on',S.on);
});
document.getElementById('bR').addEventListener('click',reset);
document.getElementById('sB').addEventListener('input',function(){
  S.bf=parseInt(this.value)/100;document.getElementById('vB').textContent=S.bf.toFixed(2);
});
document.getElementById('sC').addEventListener('input',function(){
  S.cs=parseInt(this.value)/1000;document.getElementById('vC').textContent=S.cs.toFixed(3);
});

reset();requestAnimationFrame(loop);
</script>
</body>
</html>

这次的核心改进:

机构图大幅增强 — 清晰展示凸轮→拨叉→制动带完整传动链

  1. 端面凸轮:用参数化路径画出带单段凸起的不规则盘形轮廓,凸起区域用半透明青色高亮,随主轴实时旋转,旋转指针和弧形方向箭头标示转向
  2. 双拨叉:每个拨叉由滚子(圆形)+ 杆(矩形)+ L形臂 + 回位弹簧组成。滚子紧贴凸轮外缘,当凸轮凸起转到拨叉位置时,滚子被推向外侧,整根杆沿径向位移,L形臂末端拉线点随之移动。被推动时拨叉整体变为橙色并加发光滤镜
  3. 拉线:从拨叉L形臂末端用贝塞尔曲线连到对应制动鼓的制动带端点,制动时变亮变橙
  4. 制动鼓+制动带:制动带以弧线绕在鼓上,收紧时弧度增大、颜色变橙、加辉光滤镜
  5. 单向轴承:在制动鼓与车轮之间标注了带方向箭头的单向轴承符号
  6. 主绕线轮:在主轴上画出带辐条旋转的绕线轮,标注拉力释放驱动
  7. 所有零部件均有文字标注(端面凸轮、拨叉A/B、制动鼓、单向轴承、左/右轮、主轴、主绕线轮、推程数值等)

顶部阶段标签实时描述当前动作时序:凸轮推程 → 拨叉A外移 → 左制动带收紧 → 左轮降速

积分规则:第一轮对话扣减6分,后续每轮扣4分