<!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=Syne:wght@400;600;700;800&family=DM+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#070b12;--fg:#dfe6f0;--muted:#4a5670;--accent:#00ffa3;--accent2:#ff6b35;--hinge:#ff2d78;--card:#0c1018;--border:#162030;--force:#4db8ff}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'DM Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
.wrap{width:100%;max-width:1440px;padding:16px 20px}
.hdr{text-align:center;margin-bottom:14px}
.hdr h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(1.3rem,3vw,2.1rem);letter-spacing:-.02em;line-height:1.2}
.hdr h1 em{font-style:normal;color:var(--accent)}
.hdr p{color:var(--muted);font-size:.78rem;margin-top:6px;max-width:740px;margin-inline:auto;line-height:1.65}
.svg-box{width:100%;background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden}
.svg-box svg{width:100%;height:auto;display:block}
.ctrls{display:flex;gap:16px;margin-top:14px;flex-wrap:wrap;justify-content:center}
.cg{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;min-width:200px;flex:1;max-width:280px}
.cg label{display:block;font-size:.68rem;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.12em}
.cg .val{font-family:'Syne',sans-serif;font-weight:700;font-size:1.25rem;color:var(--accent);margin-bottom:6px}
input[type=range]{-webkit-appearance:none;width:100%;height:3px;background:var(--border);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 8px var(--accent)}
.btn-row{display:flex;gap:8px;align-items:flex-end;padding-bottom:4px}
.btn{font-family:'DM Mono',monospace;font-size:.75rem;padding:7px 14px;border:1px solid var(--border);background:var(--card);color:var(--fg);border-radius:5px;cursor:pointer;transition:.2s}
.btn:hover{border-color:var(--accent);color:var(--accent)}
.btn.on{background:var(--accent);color:var(--bg);border-color:var(--accent)}
.leg{display:flex;gap:16px;margin-top:12px;justify-content:center;flex-wrap:wrap}
.leg span{display:flex;align-items:center;gap:6px;font-size:.7rem;color:var(--muted)}
.leg i{width:9px;height:9px;border-radius:50%;display:inline-block}
@media(max-width:700px){.ctrls{flex-direction:column;align-items:center}.cg{max-width:100%}}
</style>
</head>
<body>
<div class="wrap">
<div class="hdr">
<h1>弹性蛇行机构 · <em>最终理想解</em>原理动画</h1>
<p>偏心曲柄驱动弹性底盘铰链,以整车柔性扭动替代刚性转向——振幅与周期均可由螺钉微调,实现可调S型蛇行轨迹</p>
</div>
<div class="svg-box">
<svg id="svg" viewBox="0 0 1200 720" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div class="ctrls">
<div class="cg"><label>偏心距 Eccentric Offset</label><div class="val" id="vOff">4.0 mm</div><input type="range" id="sOff" min="0" max="8" step="0.5" value="4"></div>
<div class="cg"><label>铰链刚度 Hinge Stiffness</label><div class="val" id="vStf">0.50 N·m/rad</div><input type="range" id="sStf" min="0.1" max="2.0" step="0.05" value="0.5"></div>
<div class="cg"><label>动画速度</label><div class="val" id="vSpd">1.0x</div><input type="range" id="sSpd" min="0.2" max="3" step="0.1" value="1"></div>
<div class="cg btn-row"><button class="btn on" id="bPlay">播放</button><button class="btn" id="bRst">重置</button></div>
</div>
<div class="leg">
<span><i style="background:#00ffa3"></i>曲柄-连杆驱动</span>
<span><i style="background:#ff2d78"></i>弹性铰链(核心创新)</span>
<span><i style="background:#ff6b35"></i>S型蛇行轨迹</span>
<span><i style="background:#4db8ff"></i>侧向推力矢量</span>
</div>
</div>
<script>
const NS='http://www.w3.org/2000/svg';
const svg=document.getElementById('svg');
/* ─── 状态 ─── */
let crankAngle=0, playing=true, spd=1;
let eccOff=4, stiff=0.5;
let lastTs=0;
/* ─── 工具 ─── */
function el(tag,attr,parent){
const e=document.createElementNS(NS,tag);
for(const k in attr) e.setAttribute(k,attr[k]);
if(parent) parent.appendChild(e);
return e;
}
function clr(c,a){return c.replace(')',`,${a})`).replace('rgb','rgba')}
/* ─── 计算参数 ─── */
function oscAmp(){return(eccOff/8)*(0.5/Math.max(stiff,0.1))}
function maxRearAngle(){return oscAmp()*0.42} // 弧度,最大约24°
function trailAmpPx(){return oscAmp()*90} // 俯视轨迹振幅像素
function crankVisR(){return 12+eccOff*2.5} // 曲柄视觉半径
/* ─── 构建 SVG ─── */
function build(){
svg.innerHTML='';
/* defs */
const defs=el('defs',{},svg);
// 辉光滤镜
const f1=el('filter',{id:'glow',x:'-80%',y:'-80%',width:'260%',height:'260%'},defs);
el('feGaussianBlur',{stdDeviation:'5',result:'b'},f1);
const fm1=el('feMerge',{},f1);el('feMergeNode',{in:'b'},fm1);el('feMergeNode',{in:'SourceGraphic'},fm1);
const f2=el('filter',{id:'glowBig',x:'-120%',y:'-120%',width:'340%',height:'340%'},defs);
el('feGaussianBlur',{stdDeviation:'10',result:'b'},f2);
const fm2=el('feMerge',{},f2);el('feMergeNode',{in:'b'},fm2);el('feMergeNode',{in:'SourceGraphic'},fm2);
const f3=el('filter',{id:'glowSm',x:'-60%',y:'-60%',width:'220%',height:'220%'},defs);
el('feGaussianBlur',{stdDeviation:'3',result:'b'},f3);
const fm3=el('feMerge',{},f3);el('feMergeNode',{in:'b'},fm3);el('feMergeNode',{in:'SourceGraphic'},fm3);
// 渐变
const g1=el('linearGradient',{id:'gTrail',x1:'0%',y1:'0%',x2:'100%',y2:'0%'},defs);
el('stop',{offset:'0%','stop-color':'#ff6b35','stop-opacity':'0'},g1);
el('stop',{offset:'70%','stop-color':'#ff6b35','stop-opacity':'0.5'},g1);
el('stop',{offset:'100%','stop-color':'#ff6b35','stop-opacity':'0.85'},g1);
const g2=el('linearGradient',{id:'gTraj',x1:'0%',y1:'0%',x2:'100%',y2:'0%'},defs);
el('stop',{offset:'0%','stop-color':'#ff6b35','stop-opacity':'0.15'},g2);
el('stop',{offset:'50%','stop-color':'#ff6b35','stop-opacity':'0.7'},g2);
el('stop',{offset:'100%','stop-color':'#ff6b35','stop-opacity':'0.15'},g2);
const g3=el('radialGradient',{id:'gHinge',cx:'50%',cy:'50%',r:'50%'},defs);
el('stop',{offset:'0%','stop-color':'#ff2d78','stop-opacity':'0.6'},g3);
el('stop',{offset:'100%','stop-color':'#ff2d78','stop-opacity':'0'},g3);
// 箭头标记
const m1=el('marker',{id:'arrForce',markerWidth:'8',markerHeight:'6',refX:'8',refY:'3',orient:'auto'},defs);
el('path',{d:'M0,0 L8,3 L0,6 Z',fill:'#4db8ff'},m1);
const m2=el('marker',{id:'arrAccent',markerWidth:'7',markerHeight:'5',refX:'7',refY:'2.5',orient:'auto'},defs);
el('path',{d:'M0,0 L7,2.5 L0,5 Z',fill:'#00ffa3'},m2);
/* ─── 背景网格 ─── */
const bgG=el('g',{id:'bgGrid'},svg);
for(let x=0;x<=1200;x+=40) el('line',{x1:x,y1:0,x2:x,y2:720,stroke:'#0e1520','stroke-width':'0.5'},bgG);
for(let y=0;y<=720;y+=40) el('line',{x1:0,y1:y,x2:1200,y2:y,stroke:'#0e1520','stroke-width':'0.5'},bgG);
/* ─── 面板分隔线 ─── */
el('line',{x1:600,y1:20,x2:600,y2:440,stroke:'#1a2838','stroke-width':'1','stroke-dasharray':'4,4'},svg);
el('line',{x1:20,y1:455,x2:1180,y2:455,stroke:'#1a2838','stroke-width':'1','stroke-dasharray':'4,4'},svg);
/* ─── 面板标签 ─── */
const lb1=el('text',{x:300,y:28,fill:'#5a6a82','font-family':"'Syne',sans-serif",'font-size':'13','font-weight':'600','text-anchor':'middle','letter-spacing':'0.08em'},svg);
lb1.textContent='俯视蛇行动作';
const lb2=el('text',{x:900,y:28,fill:'#5a6a82','font-family':"'Syne',sans-serif",'font-size':'13','font-weight':'600','text-anchor':'middle','letter-spacing':'0.08em'},svg);
lb2.textContent='曲柄-连杆-铰链 机构原理';
const lb3=el('text',{x:600,y:472,fill:'#5a6a82','font-family':"'Syne',sans-serif",'font-size':'13','font-weight':'600','text-anchor':'middle','letter-spacing':'0.08em'},svg);
lb3.textContent='S型轨迹预览 · 参数敏感性';
/* ─── 左面板:俯视蛇行 ─── */
const topG=el('g',{id:'topView'},svg);
// 跑道线
el('line',{x1:40,y1:140,x2:570,y2:140,stroke:'#162030','stroke-width':'1','stroke-dasharray':'6,6'},topG);
el('line',{x1:40,y1:340,x2:570,y2:340,stroke:'#162030','stroke-width':'1','stroke-dasharray':'6,6'},topG);
// 锥桶(静态,会随时间滚动)
const coneG=el('g',{id:'cones'},topG);
// 轨迹路径
const trailP=el('polyline',{id:'trailPath',fill:'none',stroke:'url(#gTrail)','stroke-width':'3','stroke-linecap':'round','stroke-linejoin':'round'},topG);
// 力矢量组
const forceG=el('g',{id:'forceArrows'},topG);
// 车辆组
const vehG=el('g',{id:'vehicle'},topG);
// 前段底盘
const frontCh=el('rect',{x:-70,y:-12,width:70,height:24,rx:6,fill:'#182838',stroke:'#2a4060','stroke-width':'1.5'},vehG);
// 前轮(万向轮)
el('circle',{cx:40,cy:0,r:5,fill:'#3a5068',stroke:'#5a7090','stroke-width':'1'},vehG);
// 弹性铰链辉光
el('circle',{cx:0,cy:0,r:18,fill:'url(#gHinge)'},vehG);
// 后段底盘组(可旋转)
const rearG=el('g',{id:'rearChassis'},vehG);
const rearCh=el('rect',{x:0,y:-12,width:75,height:24,rx:5,fill:'#182838',stroke:'#2a4060','stroke-width':'1.5'},rearG);
// 后轮
el('rect',{x:55,y:-18,width:14,height:6,rx:2,fill:'#3a5068',stroke:'#5a7090','stroke-width':'1'},rearG);
el('rect',{x:55,y:12,width:14,height:6,rx:2,fill:'#3a5068',stroke:'#5a7090','stroke-width':'1'},rearG);
// 铰链圆点
const hingeC=el('circle',{cx:0,cy:0,r:5,fill:'#ff2d78',filter:'url(#glow)'},vehG);
// 弹簧标记(在铰链处)
const springP=el('path',{id:'spring',fill:'none',stroke:'#ff2d78','stroke-width':'1.5','stroke-linecap':'round'},vehG);
// 重物与绳
const weightG=el('g',{id:'weightGroup'},vehG);
el('rect',{x:-20,y:-35,width:16,height:14,rx:2,fill:'#2a3a52',stroke:'#4a6a8a','stroke-width':'1'},weightG);
el('line',{x1:-12,y1:-21,x2:-12,y2:-12,stroke:'#4a6a8a','stroke-width':'1'},weightG);
const wArrow=el('path',{d:'M-12,-40 L-12,-48 M-15,-44 L-12,-48 L-9,-44',fill:'none',stroke:'#00ffa3','stroke-width':'1.2'},weightG);
/* ─── 右面板:机构原理 ─── */
const mechG=el('g',{id:'mechView'},svg);
// 主轴中心位置
const SX=870,SY=160;
// 底盘基准线
el('line',{x1:720,y1:310,x2:1100,y2:310,stroke:'#0e1825','stroke-width':'1','stroke-dasharray':'3,5'},mechG);
// 前段底盘(固定)
const mFront=el('rect',{x:890,y:294,width:150,height:32,rx:6,fill:'#182838',stroke:'#2a4060','stroke-width':'1.5'},mechG);
el('text',{x:965,y:314,fill:'#5a7a9a','font-size':'10','text-anchor':'middle','font-family':"'DM Mono',monospace"},mechG).textContent='前段底盘';
// 铰链辉光
el('circle',{cx:890,cy:310,r:22,fill:'url(#gHinge)'},mechG);
// 后段底盘组(可旋转)
const mRearG=el('g',{id:'mRearChassis'},mechG);
const mRear=el('rect',{x:810,y:294,width:80,height:32,rx:5,fill:'#182838',stroke:'#2a4060','stroke-width':'1.5'},mRearG);
el('text',{x:850,y:314,fill:'#5a7a9a','font-size':'10','text-anchor':'middle','font-family':"'DM Mono',monospace"},mRearG).textContent='后段';
// 后轮(机构视图)
el('rect',{x:812,y:286,width:12,height:6,rx:1.5,fill:'#3a5068',stroke:'#5a7090','stroke-width':'0.8'},mRearG);
el('rect',{x:812,y:328,width:12,height:6,rx:1.5,fill:'#3a5068',stroke:'#5a7090','stroke-width':'0.8'},mRearG);
// 铰链圆点
const mHinge=el('circle',{cx:890,cy:310,r:6,fill:'#ff2d78',filter:'url(#glow)'},mechG);
// 弹簧符号
const mSpring=el('path',{id:'mSpringPath',fill:'none',stroke:'#ff2d78','stroke-width':'1.8','stroke-linecap':'round'},mechG);
// 主轴
const shaftC=el('circle',{cx:SX,cy:SY,r:14,fill:'none',stroke:'#2a4060','stroke-width':'1.5'},mechG);
const shaftDot=el('circle',{cx:SX,cy:SY,r:3,fill:'#00ffa3'},mechG);
el('text',{x:SX,y:SY-22,fill:'#5a7a9a','font-size':'9','text-anchor':'middle','font-family':"'DM Mono',monospace"},mechG).textContent='主轴';
// 曲柄臂
const crankArm=el('line',{id:'crankArm',x1:SX,y1:SY,x2:SX,y2:SY,stroke:'#00ffa3','stroke-width':'2.5','stroke-linecap':'round'},mechG);
// 曲柄销
const crankPin=el('circle',{id:'crankPin',cx:SX,cy:SY,r:4.5,fill:'#00ffa3',filter:'url(#glowSm)'},mechG);
// 曲柄轨迹圆
const crankOrbit=el('circle',{id:'crankOrbit',cx:SX,cy:SY,r:crankVisR(),fill:'none',stroke:'#00ffa3','stroke-width':'0.6','stroke-dasharray':'3,3',opacity:'0.4'},mechG);
// 连杆
const connRod=el('line',{id:'connRod',x1:SX,y1:SY,x2:SX,y2:SY,stroke:'#00ffa3','stroke-width':'2','stroke-linecap':'round',opacity:'0.85'},mechG);
// 连杆连接点(后段尾部)
const rodJoint=el('circle',{id:'rodJoint',cx:820,cy:310,r:3.5,fill:'#00ffa3',filter:'url(#glowSm)'},mechG);
// 重物
const mWeight=el('g',{id:'mWeight'},mechG);
el('rect',{x:SX-10,y:SY-60,width:20,height:20,rx:3,fill:'#2a3a52',stroke:'#4a6a8a','stroke-width':'1'},mWeight);
el('line',{x1:SX,y1:SY-40,x2:SX,y2:SY-14,stroke:'#4a6a8a','stroke-width':'1.2'},mWeight);
el('text',{x:SX,y:SY-66,fill:'#6a8aaa','font-size':'9','text-anchor':'middle','font-family':"'DM Mono',monospace"},mWeight).textContent='重物 ↓';
// 偏心距标注
const eccLabel=el('g',{id:'eccLabel'},mechG);
const eccLine1=el('line',{id:'eccLine1',x1:SX,y1:SY,x2:SX,y2:SY,stroke:'#ffcc00','stroke-width':'0.8','stroke-dasharray':'2,2'},eccLabel);
const eccLine2=el('line',{id:'eccLine2',x1:SX,y1:SY,x2:SX,y2:SY,stroke:'#ffcc00','stroke-width':'0.8','stroke-dasharray':'2,2'},eccLabel);
const eccText=el('text',{id:'eccText',x:SX,y:SY,fill:'#ffcc00','font-size':'10','font-family':"'DM Mono',monospace"},eccLabel);
// 角度弧线
const angleArc=el('path',{id:'angleArc',fill:'none',stroke:'#ff2d78','stroke-width':'1.5','stroke-dasharray':'3,2',opacity:'0.7'},mechG);
const angleText=el('text',{id:'angleText',x:890,y:370,fill:'#ff2d78','font-size':'11','text-anchor':'middle','font-family':"'DM Mono',monospace"},mechG);
// 侧向力箭头(机构视图)
const mForceArr=el('line',{id:'mForceArr',x1:820,y1:310,x2:820,y2:310,stroke:'#4db8ff','stroke-width':'2','marker-end':'url(#arrForce)'},mechG);
const mForceText=el('text',{id:'mForceText',x:820,y:310,fill:'#4db8ff','font-size':'9','font-family':"'DM Mono',monospace"},mechG);
// 旋转方向箭头
const rotArc=el('path',{id:'rotArc',fill:'none',stroke:'#00ffa3','stroke-width':'1',opacity:'0.5'},mechG);
/* ─── 底部:S轨迹预览 ─── */
const trajG=el('g',{id:'trajView'},svg);
// 中心线
el('line',{x1:60,y1:600,x2:1140,y2:600,stroke:'#162030','stroke-width':'1','stroke-dasharray':'4,6'},trajG);
// 锥桶位置
const trajCones=el('g',{},trajG);
// S轨迹路径
const trajPath=el('path',{id:'trajPath',fill:'none',stroke:'url(#gTraj)','stroke-width':'3','stroke-linecap':'round'},trajG);
// 振幅标注
const ampLine1=el('line',{id:'ampLine1',x1:0,y1:0,x2:0,y2:0,stroke:'#ff6b35','stroke-width':'0.8','stroke-dasharray':'3,3',opacity:'0.6'},trajG);
const ampLine2=el('line',{id:'ampLine2',x1:0,y1:0,x2:0,y2:0,stroke:'#ff6b35','stroke-width':'0.8','stroke-dasharray':'3,3',opacity:'0.6'},trajG);
const ampText=el('text',{id:'ampText',x:0,y:0,fill:'#ff6b35','font-size':'11','text-anchor':'middle','font-family':"'DM Mono',monospace"},trajG);
// 参数信息文本
const infoText=el('text',{id:'infoText',x:1130,y:695,fill:'#3a5068','font-size':'10','text-anchor':'end','font-family':"'DM Mono',monospace"},trajG);
// 动态车辆指示点
const trajDot=el('circle',{id:'trajDot',cx:60,cy:600,r:4,fill:'#ff6b35',filter:'url(#glowSm)'},trajG);
}
/* ─── 弹簧路径生成 ─── */
function springPath(x1,y1,x2,y2,coils,stiffness){
const dx=x2-x1,dy=y2-y1;
const len=Math.sqrt(dx*dx+dy*dy);
if(len<1)return`M${x1},${y1}`;
const nx=-dy/len,ny=dx/len;
const amp=Math.max(3, 12/Math.max(stiffness,0.2));
let d=`M${x1},${y1}`;
const steps=coils*2;
for(let i=1;i<=steps;i++){
const t=i/(steps+1);
const px=x1+dx*t, py=y1+dy*t;
const side=(i%2===0?1:-1)*amp;
d+=` L${px+nx*side},${py+ny*side}`;
}
d+=` L${x2},${y2}`;
return d;
}
/* ─── 阶跃锥桶 ─── */
let conePositions=[];
function initCones(){
conePositions=[];
for(let i=0;i<8;i++){
conePositions.push({worldX:i*160+80, side:(i%2===0)?-1:1});
}
}
initCones();
/* ─── 动画帧 ─── */
function animate(ts){
if(!lastTs) lastTs=ts;
const dt=Math.min((ts-lastTs)/1000, 0.05);
lastTs=ts;
if(playing){
crankAngle+=dt*spd*2.5;
}
const rearA=maxRearAngle()*Math.sin(crankAngle);
const tAmp=trailAmpPx();
/* ── 俯视车辆 ── */
const vx=300, vy=240+tAmp*Math.sin(crankAngle);
const heading=Math.atan2(tAmp*Math.cos(crankAngle)*2.5*spd, 80)*0.3;
const veh=document.getElementById('vehicle');
veh.setAttribute('transform',`translate(${vx},${vy}) rotate(${heading*180/Math.PI})`);
const rear=document.getElementById('rearChassis');
rear.setAttribute('transform',`rotate(${rearA*180/Math.PI})`);
// 弹簧
const sp=document.getElementById('spring');
const sAmp=Math.max(2,8/Math.max(stiff,0.2));
sp.setAttribute('d',springPath(0,-10,0,10,3,stiff));
sp.setAttribute('transform',`scale(${1+Math.abs(rearA)*2},1)`);
// 轨迹
let pts='';
const tLen=280;
for(let i=0;i<=tLen;i++){
const t=crankAngle-(i*0.025);
const px=vx-i*1.6;
const py=240+tAmp*Math.sin(t);
pts+=`${px},${py} `;
}
document.getElementById('trailPath').setAttribute('points',pts.trim());
// 锥桶
const coneG=document.getElementById('cones');
coneG.innerHTML='';
const scrollX=(crankAngle*40*spd)%160;
for(let i=0;i<8;i++){
const cx=580-((i*160+scrollX)%1280);
if(cx<30||cx>580) continue;
const cy=conePositions[i].side>0?350:130;
const tri=`M${cx},${cy} L${cx-6},${cy+12} L${cx+6},${cy+12} Z`;
el('path',{d:tri,fill:'#ff6b35',opacity:'0.7'},coneG);
el('circle',{cx,cy:y=cy+14,r:2,fill:'#ff6b35',opacity:'0.3'},coneG);
}
// 侧向力箭头
const fG=document.getElementById('forceArrows');
fG.innerHTML='';
const fDir=rearA>0?1:-1;
const fMag=Math.abs(rearA)*120;
if(fMag>5){
const rwx=vx+55*Math.cos(heading+rearA);
const rwy=vy+55*Math.sin(heading+rearA);
el('line',{x1:rwx,y1:rwy,x2:rwx+fDir*fMag*0.5,y2:rwy,'marker-end':'url(#arrForce)',stroke:'#4db8ff','stroke-width':'2',opacity:'0.7'},fG);
}
/* ── 机构视图 ── */
const SX=870,SY=160;
const cR=crankVisR();
const pinX=SX+cR*Math.cos(crankAngle);
const pinY=SY+cR*Math.sin(crankAngle);
document.getElementById('crankArm').setAttribute('x2',pinX);
document.getElementById('crankArm').setAttribute('y2',pinY);
document.getElementById('crankPin').setAttribute('cx',pinX);
document.getElementById('crankPin').setAttribute('cy',pinY);
document.getElementById('crankOrbit').setAttribute('r',cR);
// 后段底盘旋转
const mRearG=document.getElementById('mRearChassis');
mRearG.setAttribute('transform',`rotate(${rearA*180/Math.PI},890,310)`);
// 连杆:从曲柄销到后段尾部
const tailBaseX=820, tailBaseY=310;
const cosR=Math.cos(rearA), sinR=Math.sin(rearA);
const tailX=890+(tailBaseX-890)*cosR-(tailBaseY-310)*sinR;
const tailY=310+(tailBaseX-890)*sinR+(tailBaseY-310)*cosR;
document.getElementById('connRod').setAttribute('x1',pinX);
document.getElementById('connRod').setAttribute('y1',pinY);
document.getElementById('connRod').setAttribute('x2',tailX);
document.getElementById('connRod').setAttribute('y2',tailY);
document.getElementById('rodJoint').setAttribute('cx',tailX);
document.getElementById('rodJoint').setAttribute('cy',tailY);
// 机构弹簧
const mSp=document.getElementById('mSpringPath');
mSp.setAttribute('d',springPath(890,294,890,280,3,stiff));
// 偏心距标注
if(eccOff>0){
document.getElementById('eccLine1').setAttribute('x1',SX);
document.getElementById('eccLine1').setAttribute('y1',SY);
document.getElementById('eccLine1').setAttribute('x2',pinX);
document.getElementById('eccLine1').setAttribute('y2',pinY);
const mx=(SX+pinX)/2, my=(SY+pinY)/2-12;
document.getElementById('eccText').setAttribute('x',mx);
document.getElementById('eccText').setAttribute('y',my);
document.getElementById('eccText').textContent=`e=${eccOff.toFixed(1)}`;
}
// 角度弧线
if(Math.abs(rearA)>0.01){
const arcR=40;
const startA=Math.PI;
const endA=Math.PI+rearA;
const x1=890+arcR*Math.cos(startA), y1=310+arcR*Math.sin(startA);
const x2=890+arcR*Math.cos(endA), y2=310+arcR*Math.sin(endA);
const largeArc=Math.abs(rearA)>Math.PI?1:0;
const sweep=rearA>0?1:0;
document.getElementById('angleArc').setAttribute('d',
`M${x1},${y1} A${arcR},${arcR} 0 ${largeArc},${sweep} ${x2},${y2}`);
document.getElementById('angleText').textContent=`θ=${(rearA*180/Math.PI).toFixed(1)}°`;
} else {
document.getElementById('angleArc').setAttribute('d','');
document.getElementById('angleText').textContent='';
}
// 侧向力箭头(机构视图)
const flX=tailX, flY=tailY;
const fDirM=rearA>0?1:-1;
const fMagM=Math.abs(rearA)*80;
if(fMagM>3){
document.getElementById('mForceArr').setAttribute('x1',flX);
document.getElementById('mForceArr').setAttribute('y1',flY);
document.getElementById('mForceArr').setAttribute('x2',flX);
document.getElementById('mForceArr').setAttribute('y2',flY+fDirM*fMagM);
document.getElementById('mForceText').setAttribute('x',flX+14);
document.getElementById('mForceText').setAttribute('y',flY+fDirM*fMagM*0.5);
document.getElementById('mForceText').textContent='F侧';
} else {
document.getElementById('mForceArr').setAttribute('x1',0);
document.getElementById('mForceArr').setAttribute('x2',0);
document.getElementById('mForceText').textContent='';
}
// 旋转方向弧线
const rotR=24;
const rotStart=crankAngle-0.3;
const rotEnd=crankAngle+0.8;
const rx1=SX+rotR*Math.cos(rotStart), ry1=SY+rotR*Math.sin(rotStart);
const rx2=SX+rotR*Math.cos(rotEnd), ry2=SY+rotR*Math.sin(rotEnd);
document.getElementById('rotArc').setAttribute('d',
`M${rx1},${ry1} A${rotR},${rotR} 0 0,1 ${rx2},${ry2}`);
/* ── S轨迹预览 ── */
const tCenterY=600;
const tStartX=80, tEndX=1120;
let d=`M${tStartX},${tCenterY}`;
for(let x=tStartX;x<=tEndX;x+=2){
const phase=(x-tStartX)/(tEndX-tStartX)*5*Math.PI;
const y=tCenterY+tAmp*Math.sin(phase);
d+=` L${x},${y}`;
}
document.getElementById('trajPath').setAttribute('d',d);
// 轨迹上的移动点
const dotPhase=((crankAngle*0.8)%(5*Math.PI));
const dotX=tStartX+dotPhase/(5*Math.PI)*(tEndX-tStartX);
const dotY=tCenterY+tAmp*Math.sin(dotPhase);
document.getElementById('trajDot').setAttribute('cx',dotX);
document.getElementById('trajDot').setAttribute('cy',dotY);
// 振幅标注线
const ampX=tStartX+(tEndX-tStartX)*0.12;
document.getElementById('ampLine1').setAttribute('x1',ampX);
document.getElementById('ampLine1').setAttribute('y1',tCenterY);
document.getElementById('ampLine1').setAttribute('x2',ampX);
document.getElementById('ampLine1').setAttribute('y2',tCenterY-tAmp);
document.getElementById('ampLine2').setAttribute('x1',ampX-8);
document.getElementById('ampLine2').setAttribute('y1',tCenterY);
document.getElementById('ampLine2').setAttribute('x2',ampX+8);
document.getElementById('ampLine2').setAttribute('y2',tCenterY);
document.getElementById('ampText').setAttribute('x',ampX+18);
document.getElementById('ampText').setAttribute('y',tCenterY-tAmp/2+4);
document.getElementById('ampText').textContent=`A=${tAmp.toFixed(0)}px`;
// 信息文本
document.getElementById('infoText').textContent=
`偏心距 ${eccOff.toFixed(1)}mm | 刚度 ${stiff.toFixed(2)} N·m/rad | 振幅系数 ${oscAmp().toFixed(2)}`;
// 锥桶(轨迹视图)
const tcG=svg.querySelector('#trajView > g');
if(tcG) tcG.innerHTML='';
for(let i=0;i<5;i++){
const cx=tStartX+(tEndX-tStartX)*(i+0.5)/5;
const side=i%2===0?-1:1;
const cy=tCenterY+side*(tAmp+35);
if(tcG){
el('path',{d:`M${cx},${cy-8} L${cx-5},${cy+4} L${cx+5},${cy+4} Z`,fill:'#ff6b35',opacity:'0.4'},tcG);
}
}
requestAnimationFrame(animate);
}
/* ─── 控件绑定 ─── */
document.getElementById('sOff').addEventListener('input',function(){
eccOff=parseFloat(this.value);
document.getElementById('vOff').textContent=eccOff.toFixed(1)+' mm';
});
document.getElementById('sStf').addEventListener('input',function(){
stiff=parseFloat(this.value);
document.getElementById('vStf').textContent=stiff.toFixed(2)+' N·m/rad';
});
document.getElementById('sSpd').addEventListener('input',function(){
spd=parseFloat(this.value);
document.getElementById('vSpd').textContent=spd.toFixed(1)+'x';
});
document.getElementById('bPlay').addEventListener('click',function(){
playing=!playing;
this.textContent=playing?'播放':'暂停';
this.classList.toggle('on',playing);
});
document.getElementById('bRst').addEventListener('click',function(){
crankAngle=0;
lastTs=0;
});
/* ─── 启动 ─── */
build();
requestAnimationFrame(animate);
</script>
</body>
</html>
这个动画实现了以下核心要素:
IFR 理想解聚焦展示
- 直接展示弹性蛇行机构在理想工作状态下的运作,无前后对比。整车柔性扭动替代刚性转向的核心矛盾消解过程一目了然。
三个联动视图
- 俯视蛇行动作(左面板):车辆在跑道上蛇行绕锥,底盘铰链处可见柔性扭动,橙色渐变轨迹清晰呈现S型路径,蓝色力矢量箭头指示后轮侧向推力方向。
- 曲柄-连杆-铰链机构原理(右面板):主轴旋转 → 偏心曲柄推动连杆 → 后段底盘绕铰链摆动,完整传动链实时联动。铰链以品红色辉光+弹簧符号双重标示为"核心创新点",偏心距用黄色标注随曲柄销动态移动。
- S型轨迹预览(底部):参数变化即时反映到轨迹振幅上,振幅标注线直观显示当前幅值。
交互控制
- 偏心距滑块(0~8mm):增大偏心距 → 曲柄圆变大 → 摆角增加 → S轨迹振幅增大
- 铰链刚度滑块(0.10~2.00 N·m/rad):增大刚度 → 弹簧变硬 → 摆角减小 → S轨迹振幅减小
- 速度与播放控制:可暂停观察机构细节
视觉引导设计
- 弹性铰链始终以品红色辉光标记(
#ff2d78+ glow滤镜),引导视线聚焦破除矛盾的关键 - 弹簧振幅随刚度参数实时变化(刚度大→弹簧扁,刚度小→弹簧振幅大)
- 角度弧线与度数标注让铰链摆角量化可读
- 侧向力箭头仅在有实质摆角时出现,强化"甩尾产生侧推力"的因果链
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
