<!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>取消前轮转向 → 固定前轴 + 后轮独立差速制动 → 全势能推进、自动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>前轮固定死轴 — <span class="tag tag-free">零侧滑摩擦</span> 全部势能用于前进</li>
<li>端面凸轮复用主轴旋转 — <span class="tag tag-accent">零附加能耗</span> 转向自行发生</li>
<li>差速制动替代偏转转向 — <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>
这次的核心改进:
机构图大幅增强 — 清晰展示凸轮→拨叉→制动带完整传动链
- 端面凸轮:用参数化路径画出带单段凸起的不规则盘形轮廓,凸起区域用半透明青色高亮,随主轴实时旋转,旋转指针和弧形方向箭头标示转向
- 双拨叉:每个拨叉由滚子(圆形)+ 杆(矩形)+ L形臂 + 回位弹簧组成。滚子紧贴凸轮外缘,当凸轮凸起转到拨叉位置时,滚子被推向外侧,整根杆沿径向位移,L形臂末端拉线点随之移动。被推动时拨叉整体变为橙色并加发光滤镜
- 拉线:从拨叉L形臂末端用贝塞尔曲线连到对应制动鼓的制动带端点,制动时变亮变橙
- 制动鼓+制动带:制动带以弧线绕在鼓上,收紧时弧度增大、颜色变橙、加辉光滤镜
- 单向轴承:在制动鼓与车轮之间标注了带方向箭头的单向轴承符号
- 主绕线轮:在主轴上画出带辐条旋转的绕线轮,标注拉力释放驱动
- 所有零部件均有文字标注(端面凸轮、拨叉A/B、制动鼓、单向轴承、左/右轮、主轴、主绕线轮、推程数值等)
顶部阶段标签实时描述当前动作时序:凸轮推程 → 拨叉A外移 → 左制动带收紧 → 左轮降速
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
