分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>5连杆平面缩放机构 · 最终理想解原理动画</title>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&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:#050a12;--bg2:#0b1220;--fg:#d8e4f0;--muted:#4e6078;
  --accent:#00e5ff;--accent-g:rgba(0,229,255,.25);
  --force:#ff6b35;--force-g:rgba(255,107,53,.25);
  --ideal:#00ff88;--ideal-g:rgba(0,255,136,.22);
  --card:#0d1829;--border:#162440;
  --link-active:#00e5ff;--link-passive:#0097a7;
  --link-couple:#ffc107;--joint:#b0bec5;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'Noto Sans SC',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;overflow-x:hidden}
.page-title{width:100%;padding:18px 24px 10px;text-align:center;background:linear-gradient(180deg,rgba(0,229,255,.06) 0%,transparent 100%)}
.page-title h1{font-family:'Rajdhani',sans-serif;font-weight:700;font-size:clamp(1.3rem,3vw,1.8rem);letter-spacing:.06em;color:var(--accent);text-shadow:0 0 20px var(--accent-g)}
.page-title p{font-size:.82rem;color:var(--muted);margin-top:2px;letter-spacing:.04em}
.canvas-wrap{width:100%;max-width:960px;flex:1;display:flex;justify-content:center;align-items:center;padding:6px 12px}
#mechanism-svg{width:100%;height:auto;max-height:72vh;border-radius:10px;background:var(--bg2);border:1px solid var(--border);box-shadow:0 0 40px rgba(0,229,255,.06)}
.ctrl-panel{width:100%;max-width:960px;padding:10px 16px 18px;display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;justify-content:center;background:var(--card);border-top:1px solid var(--border);border-radius:0 0 12px 12px}
.ctrl-group{display:flex;align-items:center;gap:6px}
.ctrl-group label{font-size:.75rem;color:var(--muted);white-space:nowrap}
.ctrl-group input[type=range]{-webkit-appearance:none;width:100px;height:4px;border-radius:2px;background:var(--border);outline:none}
.ctrl-group 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 6px var(--accent-g)}
.btn{display:inline-flex;align-items:center;gap:5px;padding:5px 14px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--fg);font-size:.78rem;cursor:pointer;transition:all .2s;font-family:inherit}
.btn:hover{border-color:var(--accent);color:var(--accent);box-shadow:0 0 8px var(--accent-g)}
.btn.active{border-color:var(--ideal);color:var(--ideal);box-shadow:0 0 8px var(--ideal-g)}
.btn i{font-size:.72rem}
.phase-badge{display:inline-block;padding:3px 10px;border-radius:4px;font-size:.72rem;font-weight:600;letter-spacing:.04em;transition:all .3s}
.phase-badge.support{background:rgba(0,255,136,.12);color:var(--ideal);border:1px solid rgba(0,255,136,.3)}
.phase-badge.swing{background:rgba(0,229,255,.12);color:var(--accent);border:1px solid rgba(0,229,255,.3)}
.info-bar{width:100%;max-width:960px;display:flex;flex-wrap:wrap;gap:8px 20px;padding:8px 16px 14px;justify-content:center;font-size:.72rem;color:var(--muted)}
.info-bar span{display:flex;align-items:center;gap:4px}
.info-bar .val{color:var(--fg);font-family:'Rajdhani',sans-serif;font-weight:600}
@media(max-width:640px){.ctrl-group input[type=range]{width:70px}.ctrl-panel{gap:6px 10px}}
</style>
</head>
<body>

<div class="page-title">
  <h1>5-BAR PANTOGRAPH LEG &mdash; IFR PRINCIPLE</h1>
  <p>最终理想解:结构即控制 &mdash; 无需复杂主动控制,机构自身顺应地形并实现柔性落地与刚性发力</p>
</div>

<div class="canvas-wrap">
<svg id="mechanism-svg" viewBox="0 0 700 520" xmlns="http://www.w3.org/2000/svg">
<defs>
  <!-- 辉光滤镜 -->
  <filter id="glowCyan" x="-40%" y="-40%" width="180%" height="180%">
    <feGaussianBlur stdDeviation="4" result="b"/><feFlood flood-color="#00e5ff" flood-opacity=".5"/>
    <feComposite in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="glowGreen" x="-40%" y="-40%" width="180%" height="180%">
    <feGaussianBlur stdDeviation="5" result="b"/><feFlood flood-color="#00ff88" flood-opacity=".45"/>
    <feComposite in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="glowOrange" x="-40%" y="-40%" width="180%" height="180%">
    <feGaussianBlur stdDeviation="3.5" result="b"/><feFlood flood-color="#ff6b35" flood-opacity=".55"/>
    <feComposite in2="b" operator="in" result="g"/><feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
  <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
    <feGaussianBlur stdDeviation="8"/>
  </filter>
  <!-- 箭头标记 -->
  <marker id="arrowForce" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
    <path d="M0,0 L8,3 L0,6 Z" fill="#ff6b35"/>
  </marker>
  <marker id="arrowIdeal" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
    <path d="M0,0 L8,3 L0,6 Z" fill="#00ff88"/>
  </marker>
  <!-- 网格图案 -->
  <pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
    <path d="M 30 0 L 0 0 0 30" fill="none" stroke="#0e1a30" stroke-width=".5"/>
  </pattern>
  <!-- 碳纤维纹理 -->
  <pattern id="carbon" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
    <rect width="3" height="6" fill="rgba(0,229,255,.06)"/>
  </pattern>
</defs>

<!-- 背景网格 -->
<rect width="700" height="520" fill="url(#grid)"/>

<!-- 地面 -->
<line id="ground-line" x1="80" y1="410" x2="620" y2="410" stroke="#1a3050" stroke-width="2"/>
<g id="ground-hatch"></g>

<!-- 轨迹层 -->
<polyline id="trajectory" fill="none" stroke="#00ff88" stroke-width="1.5" stroke-dasharray="4,3" opacity=".6"/>

<!-- 桁架高亮层 -->
<g id="truss-highlight" opacity="0"></g>

<!-- 力矢量层 -->
<g id="force-vectors" opacity="0"></g>

<!-- 连杆层 -->
<g id="links-group"></g>

<!-- 关节层 -->
<g id="joints-group"></g>

<!-- 足底 -->
<g id="foot-group"></g>

<!-- 机体 -->
<g id="body-group"></g>

<!-- 电机指示 -->
<g id="motor-group"></g>

<!-- 标注层 -->
<g id="labels-group"></g>

<!-- 相位文字 -->
<g id="phase-text-group"></g>

</svg>
</div>

<div class="ctrl-panel">
  <button class="btn" id="btn-play"><i class="fa-solid fa-pause"></i><span>暂停</span></button>
  <div class="ctrl-group"><label>速度</label><input type="range" id="speed-slider" min="0.2" max="3" step="0.1" value="1"></div>
  <div class="ctrl-group"><label>相位</label><input type="range" id="phase-slider" min="0" max="1" step="0.002" value="0"></div>
  <button class="btn active" id="btn-force"><i class="fa-solid fa-arrows-up-down"></i>力矢量</button>
  <button class="btn active" id="btn-traj"><i class="fa-solid fa-route"></i>轨迹</button>
  <button class="btn active" id="btn-truss"><i class="fa-solid fa-shapes"></i>桁架</button>
  <span class="phase-badge support" id="phase-badge">支撑相</span>
</div>

<div class="info-bar">
  <span>θ₁ = <b class="val" id="info-theta1">0°</b></span>
  <span>θ₂ = <b class="val" id="info-theta2">0°</b></span>
  <span>足端 = <b class="val" id="info-foot">(0, 0)</b></span>
  <span>连杆受力 = <b class="val" id="info-force-mode">--</b></span>
</div>

<script>
// ========== 状态 ==========
const S = {
  phase: 0, playing: true, speed: 1, manual: false,
  showForce: true, showTraj: true, showTruss: true,
  trajPts: []
};

// ========== 机构参数 ==========
const M = {
  O1: {x:300, y:120}, O2: {x:400, y:120},
  hip: {x:350, y:120},
  L1: 125, L2: 155, L3: 125, L4: 155,
  groundY: 405, strideHalf: 52, liftH: 72
};

// ========== DOM引用 ==========
const svg = document.getElementById('mechanism-svg');
const linksG = document.getElementById('links-group');
const jointsG = document.getElementById('joints-group');
const footG = document.getElementById('foot-group');
const bodyG = document.getElementById('body-group');
const motorG = document.getElementById('motor-group');
const labelsG = document.getElementById('labels-group');
const phaseTG = document.getElementById('phase-text-group');
const trajEl = document.getElementById('trajectory');
const forceG = document.getElementById('force-vectors');
const trussG = document.getElementById('truss-highlight');
const groundHatch = document.getElementById('ground-hatch');

// 地面阴影线
for(let i=80;i<=620;i+=12){
  const l=document.createElementNS('http://www.w3.org/2000/svg','line');
  l.setAttribute('x1',i);l.setAttribute('y1',410);l.setAttribute('x2',i-8);l.setAttribute('y2',418);
  l.setAttribute('stroke','#1a3050');l.setAttribute('stroke-width','1');
  groundHatch.appendChild(l);
}

// ========== 工具函数 ==========
function lerp(a,b,t){return a+(b-a)*t}
function dist(a,b){return Math.hypot(b.x-a.x,b.y-a.y)}
function deg(rad){return(rad*180/Math.PI).toFixed(1)}

function circleIsect(c1,r1,c2,r2){
  const dx=c2.x-c1.x, dy=c2.y-c1.y, d=Math.hypot(dx,dy);
  if(d>r1+r2+.1||d<Math.abs(r1-r2)-.1||d<.001)return null;
  const a=(r1*r1-r2*r2+d*d)/(2*d);
  const h=Math.sqrt(Math.max(0,r1*r1-a*a));
  const mx=c1.x+a*dx/d, my=c1.y+a*dy/d;
  return[
    {x:mx+h*dy/d, y:my-h*dx/d},
    {x:mx-h*dy/d, y:my+h*dx/d}
  ];
}

// ========== 足端轨迹 ==========
function footPos(t){
  const sw=0.4, cx=M.hip.x, gy=M.groundY, sh=M.strideHalf, lh=M.liftH;
  let x,y;
  if(t<=1-sw){
    const s=t/(1-sw);
    x=cx+sh*(1-2*s); y=gy;
  } else {
    const s=(t-1+sw)/sw;
    x=cx+sh*(2*s-1); y=gy-lh*Math.sin(Math.PI*s);
  }
  return {x,y};
}

// ========== 逆运动学 ==========
function solveIK(P){
  const iA=circleIsect(M.O1,M.L1,P,M.L2);
  const iB=circleIsect(M.O2,M.L3,P,M.L4);
  if(!iA||!iB)return null;

  // 选择内侧点(X形交叉构型)
  const d1={x:P.x-M.O1.x,y:P.y-M.O1.y};
  const cr1=d1.x*(iA[0].y-M.O1.y)-d1.y*(iA[0].x-M.O1.x);
  const A=cr1<0?iA[0]:iA[1]; // 右侧(内侧)

  const d2={x:P.x-M.O2.x,y:P.y-M.O2.y};
  const cr2=d2.x*(iB[0].y-M.O2.y)-d2.y*(iB[0].x-M.O2.x);
  const B=cr2>0?iB[0]:iB[1]; // 左侧(内侧)

  const th1=Math.atan2(A.y-M.O1.y,A.x-M.O1.x);
  const th2=Math.atan2(B.y-M.O2.y,B.x-M.O2.x);
  return {A,B,P,th1,th2};
}

// ========== SVG创建辅助 ==========
const NS='http://www.w3.org/2000/svg';
function svgEl(tag,attrs,parent){
  const e=document.createElementNS(NS,tag);
  for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);
  (parent||svg).appendChild(e);
  return e;
}

// ========== 初始化静态元素 ==========
// 机体
(function initBody(){
  // 机体主体
  svgEl('rect',{x:270,y:85,width:160,height:50,rx:10,fill:'#111d33',stroke:'#1e3456','stroke-width':1.5},bodyG);
  svgEl('rect',{x:274,y:89,width:152,height:42,rx:8,fill:'none',stroke:'#1a2e4a','stroke-width':.5,'stroke-dasharray':'3,3'},bodyG);
  // 电机安装座
  [M.O1,M.O2].forEach((o,i)=>{
    svgEl('rect',{x:o.x-14,y:o.y-10,width:28,height:22,rx:4,fill:'#0d1a2e',stroke:'var(--accent)','stroke-width':1.2},bodyG);
    svgEl('text',{x:o.x,y:o.y+4,'text-anchor':'middle','font-size':'7',fill:'var(--accent)','font-family':'Rajdhani'},bodyG).textContent='M'+(i+1);
  });
})();

// 电机旋转指示器
const motorIndicators=[];
[M.O1,M.O2].forEach(o=>{
  const g=svgEl('g',{},motorG);
  svgEl('circle',{cx:o.x,cy:o.y,r:8,fill:'none',stroke:'var(--accent)','stroke-width':.8,opacity:.5},g);
  const needle=svgEl('line',{x1:o.x,y1:o.y,x2:o.x+7,y2:o.y,stroke:'var(--accent)','stroke-width':1.5,'stroke-linecap':'round'},g);
  motorIndicators.push(needle);
});

// 连杆(5根)
const linkEls=[];
const linkData=[
  {id:'L1',cls:'active',color:'var(--link-active)',w:5},
  {id:'L2',cls:'passive',color:'var(--link-passive)',w:4},
  {id:'L3',cls:'active',color:'var(--link-active)',w:5},
  {id:'L4',cls:'passive',color:'var(--link-passive)',w:4},
  {id:'L5',cls:'couple',color:'var(--link-couple)',w:2.5}
];
linkData.forEach(d=>{
  const g=svgEl('g',{},linksG);
  // 辉光底层
  const glow=svgEl('line',{'stroke-width':d.w+6,stroke:d.color==='var(--link-couple)'?'rgba(255,193,7,.15)':'rgba(0,229,255,.12)',
    'stroke-linecap':'round',opacity:.6},g);
  // 主线
  const main=svgEl('line',{'stroke-width':d.w,stroke:d.color,'stroke-linecap':'round'},g);
  // 碳纤维纹理
  const tex=svgEl('line',{'stroke-width':d.w-1.5,stroke:'rgba(255,255,255,.04)','stroke-linecap':'round',
    'stroke-dasharray':d.w>3?'2,4':'1,3'},g);
  linkEls.push({glow,main,tex,data:d});
});

// 关节
const jointEls=[];
[{id:'O1',r:7},{id:'A',r:5.5},{id:'O2',r:7},{id:'B',r:5.5},{id:'P',r:6}].forEach(j=>{
  const g=svgEl('g',{},jointsG);
  svgEl('circle',{r:j.r+3,fill:'rgba(0,229,255,.08)'},g);
  svgEl('circle',{r:j.r,fill:'#0d1829',stroke:'#b0bec5','stroke-width':1.5},g);
  svgEl('circle',{r:j.r-2.5,fill:'#1a2e4a'},g);
  svgEl('circle',{r:1.5,fill:'#b0bec5',opacity:.7},g);
  jointEls.push(g);
});

// 足底橡胶垫
const footPad=svgEl('rect',{x:0,y:0,width:28,height:8,rx:3,fill:'#2e7d32',stroke:'#4caf50','stroke-width':1},footG);
const footPadGlow=svgEl('rect',{x:0,y:0,width:28,height:8,rx:3,fill:'none',stroke:'rgba(0,255,136,.3)','stroke-width':3,opacity:0},footG);

// 接触效果
const contactRing=svgEl('circle',{r:0,fill:'none',stroke:'var(--ideal)','stroke-width':1.5,opacity:0},footG);

// 标注
const labelTexts=[
  {text:'大腿电机M1',anchor:'end',ox:-18,oy:-6},
  {text:'连杆①(主动杆)',anchor:'end',ox:-10,oy:-8},
  {text:'髋关节O₂',anchor:'start',ox:18,oy:-6},
  {text:'连杆③(主动杆)',anchor:'start',ox:10,oy:-8},
  {text:'从动杆②',anchor:'end',ox:-10,oy:-4},
  {text:'从动杆④',anchor:'start',ox:10,oy:-4},
  {text:'耦合杆⑤',anchor:'middle',ox:0,oy:-10},
  {text:'足踝P',anchor:'start',ox:14,oy:4},
];
const labelEls=labelTexts.map(l=>{
  const t=svgEl('text',{'font-size':'9',fill:'#607d8b','font-family':'Noto Sans SC','text-anchor':l.anchor,'font-weight':400},labelsG);
  t.textContent=l.text;
  return t;
});

// 相位文字
const phaseMainText=svgEl('text',{x:350,y:480,'text-anchor':'middle','font-size':'15','font-weight':700,
  'font-family':'Rajdhani, Noto Sans SC',fill:'var(--ideal)',opacity:0},phaseTG);
const phaseSubText=svgEl('text',{x:350,y:498,'text-anchor':'middle','font-size':'10',
  'font-family':'Noto Sans SC',fill:'var(--muted)',opacity:0},phaseTG);

// 力矢量箭头(预创建4个)
const forceArrows=[];
for(let i=0;i<5;i++){
  const a=svgEl('line',{'stroke-width':2.5,stroke:'var(--force)','stroke-linecap':'round',
    'marker-end':'url(#arrowForce)',opacity:0},forceG);
  forceArrows.push(a);
}
// 地面反力
const grfArrow=svgEl('line',{'stroke-width':3,stroke:'var(--ideal)','stroke-linecap':'round',
  'marker-end':'url(#arrowIdeal)',opacity:0},forceG);

// 桁架三角形
const trussTris=[];
for(let i=0;i<3;i++){
  const p=svgEl('polygon',{fill:'rgba(0,255,136,.06)',stroke:'rgba(0,255,136,.2)','stroke-width':1,'stroke-dasharray':'4,3',opacity:0},trussG);
  trussTris.push(p);
}

// ========== 更新函数 ==========
let prevPhase=-1;

function update(){
  const t=S.phase;
  const P=footPos(t);
  const ik=solveIK(P);
  if(!ik)return;

  const {A,B,th1,th2}=ik;
  const isSupport=t<0.6;
  const swingT=isSupport?0:(t-0.6)/0.4;

  // 耦合杆中点
  const M1={x:(A.x+P.x)/2,y:(A.y+P.y)/2};
  const M2={x:(B.x+P.x)/2,y:(B.y+P.y)/2};

  // 更新连杆
  const pts=[M.O1,A,M.O2,B,P];
  const linkPairs=[[M.O1,A],[A,P],[M.O2,B],[B,P],[M1,M2]];
  linkPairs.forEach((pair,i)=>{
    const el=linkEls[i];
    [el.glow,el.main,el.tex].forEach(line=>{
      line.setAttribute('x1',pair[0].x);line.setAttribute('y1',pair[0].y);
      line.setAttribute('x2',pair[1].x);line.setAttribute('y2',pair[1].y);
    });
    // 支撑相时主动杆辉光增强
    if(isSupport&&(i===0||i===2)){
      el.glow.setAttribute('opacity','1');
      el.glow.setAttribute('stroke-width',el.data.w+10);
    } else {
      el.glow.setAttribute('opacity','.6');
      el.glow.setAttribute('stroke-width',el.data.w+6);
    }
  });

  // 更新关节
  const jPts=[M.O1,A,M.O2,B,P];
  jointEls.forEach((g,i)=>{
    g.setAttribute('transform',`translate(${jPts[i].x},${jPts[i].y})`);
  });

  // 足底
  const footY=P.y;
  const padCompression=isSupport?2:0;
  footPad.setAttribute('x',P.x-14);footPad.setAttribute('y',footY-2+padCompression);
  footPad.setAttribute('height',8-padCompression);
  footPad.setAttribute('width',28+(isSupport?2:0));
  footPadGlow.setAttribute('x',P.x-14);footPadGlow.setAttribute('y',footY-2+padCompression);
  footPadGlow.setAttribute('opacity',isSupport?'1':'0');

  // 接触效果
  if(isSupport){
    contactRing.setAttribute('cx',P.x);contactRing.setAttribute('cy',footY+4);
    const pulse=4+3*Math.sin(Date.now()*0.005);
    contactRing.setAttribute('r',pulse);
    contactRing.setAttribute('opacity','0.4');
  } else {
    contactRing.setAttribute('opacity','0');
  }

  // 电机旋转指示
  motorIndicators[0].setAttribute('x2',M.O1.x+7*Math.cos(th1));
  motorIndicators[0].setAttribute('y2',M.O1.y+7*Math.sin(th1));
  motorIndicators[1].setAttribute('x2',M.O2.x+7*Math.cos(th2));
  motorIndicators[1].setAttribute('y2',M.O2.y+7*Math.sin(th2));

  // 标注位置
  const labelPositions=[
    {x:M.O1.x,y:M.O1.y},   // M1
    {x:(M.O1.x+A.x)/2,y:(M.O1.y+A.y)/2}, // L1
    {x:M.O2.x,y:M.O2.y},   // O2
    {x:(M.O2.x+B.x)/2,y:(M.O2.y+B.y)/2}, // L3
    {x:(A.x+P.x)/2,y:(A.y+P.y)/2}, // L2
    {x:(B.x+P.x)/2,y:(B.y+P.y)/2}, // L4
    {x:(M1.x+M2.x)/2,y:(M1.y+M2.y)/2}, // L5
    {x:P.x,y:P.y},          // P
  ];
  labelEls.forEach((el,i)=>{
    const lp=labelPositions[i];
    el.setAttribute('x',lp.x+labelTexts[i].ox);
    el.setAttribute('y',lp.y+labelTexts[i].oy);
  });

  // 相位文字
  if(isSupport){
    phaseMainText.textContent='STRUCTURE IS CONTROL';
    phaseSubText.textContent='支撑相 · 电机锁死 · 连杆形成稳定桁架 · 二力杆传力';
    phaseMainText.setAttribute('fill','var(--ideal)');
    const pulse=0.7+0.3*Math.sin(Date.now()*0.003);
    phaseMainText.setAttribute('opacity',pulse);
    phaseSubText.setAttribute('opacity','0.8');
  } else {
    phaseMainText.textContent='SWING PHASE';
    phaseSubText.textContent='摆动相 · 电机协同旋转 · 足端划出平滑弧线';
    phaseMainText.setAttribute('fill','var(--accent)');
    phaseMainText.setAttribute('opacity','0.8');
    phaseSubText.setAttribute('opacity','0.7');
  }

  // 轨迹
  S.trajPts.push({x:P.x,y:P.y});
  if(S.trajPts.length>300)S.trajPts.shift();
  if(S.showTraj){
    trajEl.setAttribute('points',S.trajPts.map(p=>p.x+','+p.y).join(' '));
    trajEl.setAttribute('opacity','0.6');
  } else {
    trajEl.setAttribute('opacity','0');
  }

  // 力矢量
  if(S.showForce&&isSupport){
    // 二力杆方向:从P到A/B(压力)→从A/B到O1/O2
    const forceScale=0.55;
    // 连杆1力:A→O1方向
    const f1dx=(M.O1.x-A.x)*forceScale,f1dy=(M.O1.y-A.y)*forceScale;
    forceArrows[0].setAttribute('x1',A.x);forceArrows[0].setAttribute('y1',A.y);
    forceArrows[0].setAttribute('x2',A.x+f1dx);forceArrows[0].setAttribute('y2',A.y+f1dy);
    forceArrows[0].setAttribute('opacity','0.85');

    // 连杆2力:P→A方向
    const f2dx=(A.x-P.x)*forceScale,f2dy=(A.y-P.y)*forceScale;
    forceArrows[1].setAttribute('x1',P.x);forceArrows[1].setAttribute('y1',P.y);
    forceArrows[1].setAttribute('x2',P.x+f2dx);forceArrows[1].setAttribute('y2',P.y+f2dy);
    forceArrows[1].setAttribute('opacity','0.85');

    // 连杆3力:B→O2方向
    const f3dx=(M.O2.x-B.x)*forceScale,f3dy=(M.O2.y-B.y)*forceScale;
    forceArrows[2].setAttribute('x1',B.x);forceArrows[2].setAttribute('y1',B.y);
    forceArrows[2].setAttribute('x2',B.x+f3dx);forceArrows[2].setAttribute('y2',B.y+f3dy);
    forceArrows[2].setAttribute('opacity','0.85');

    // 连杆4力:P→B方向
    const f4dx=(B.x-P.x)*forceScale,f4dy=(B.y-P.y)*forceScale;
    forceArrows[3].setAttribute('x1',P.x);forceArrows[3].setAttribute('y1',P.y);
    forceArrows[3].setAttribute('x2',P.x+f4dx);forceArrows[3].setAttribute('y2',P.y+f4dy);
    forceArrows[3].setAttribute('opacity','0.85');

    // 耦合杆力(较小)
    const f5dx=(M2.x-M1.x)*forceScale*0.4,f5dy=(M2.y-M1.y)*forceScale*0.4;
    forceArrows[4].setAttribute('x1',M1.x);forceArrows[4].setAttribute('y1',M1.y);
    forceArrows[4].setAttribute('x2',M1.x+f5dx);forceArrows[4].setAttribute('y2',M1.y+f5dy);
    forceArrows[4].setAttribute('opacity','0.5');

    // 地面反力
    grfArrow.setAttribute('x1',P.x);grfArrow.setAttribute('y1',P.y+6);
    grfArrow.setAttribute('x2',P.x);grfArrow.setAttribute('y2',P.y-55);
    grfArrow.setAttribute('opacity','0.9');

    forceG.setAttribute('opacity','1');
  } else {
    forceG.setAttribute('opacity','0');
  }

  // 桁架高亮
  if(S.showTruss&&isSupport){
    // 三角形1: O1-A-P
    trussTris[0].setAttribute('points',`${M.O1.x},${M.O1.y} ${A.x},${A.y} ${P.x},${P.y}`);
    // 三角形2: O2-B-P
    trussTris[1].setAttribute('points',`${M.O2.x},${M.O2.y} ${B.x},${B.y} ${P.x},${P.y}`);
    // 三角形3: A-B-P
    trussTris[2].setAttribute('points',`${A.x},${A.y} ${B.x},${B.y} ${P.x},${P.y}`);
    const pulse=0.6+0.4*Math.sin(Date.now()*0.004);
    trussTris.forEach(t=>t.setAttribute('opacity',pulse));
    trussG.setAttribute('opacity','1');
  } else {
    trussG.setAttribute('opacity','0');
  }

  // 信息栏
  document.getElementById('info-theta1').textContent=deg(th1)+'°';
  document.getElementById('info-theta2').textContent=deg(th2)+'°';
  document.getElementById('info-foot').textContent=`(${P.x.toFixed(0)}, ${P.y.toFixed(0)})`;
  document.getElementById('info-force-mode').textContent=isSupport?'纯拉压(二力杆)':'--';

  // 相位徽章
  const badge=document.getElementById('phase-badge');
  if(isSupport){
    badge.className='phase-badge support';badge.textContent='支撑相';
  } else {
    badge.className='phase-badge swing';badge.textContent='摆动相';
  }
}

// ========== 动画循环 ==========
let lastT=0;
const cycleMs=4200;

function loop(now){
  if(!lastT)lastT=now;
  const dt=now-lastT;
  lastT=now;

  if(S.playing&&!S.manual){
    S.phase=(S.phase+dt*S.speed/cycleMs)%1;
    document.getElementById('phase-slider').value=S.phase;
  }

  update();
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

// ========== 控件绑定 ==========
const btnPlay=document.getElementById('btn-play');
btnPlay.addEventListener('click',()=>{
  S.playing=!S.playing;
  btnPlay.querySelector('i').className=S.playing?'fa-solid fa-pause':'fa-solid fa-play';
  btnPlay.querySelector('span').textContent=S.playing?'暂停':'播放';
});

document.getElementById('speed-slider').addEventListener('input',e=>{
  S.speed=parseFloat(e.target.value);
});

const phaseSlider=document.getElementById('phase-slider');
let phaseSliding=false;
phaseSlider.addEventListener('mousedown',()=>{phaseSliding=true;S.manual=true});
phaseSlider.addEventListener('touchstart',()=>{phaseSliding=true;S.manual=true},{passive:true});
phaseSlider.addEventListener('input',e=>{
  S.phase=parseFloat(e.target.value);
  S.manual=true;
});
phaseSlider.addEventListener('mouseup',()=>{phaseSliding=false;S.manual=false});
phaseSlider.addEventListener('touchend',()=>{phaseSliding=false;S.manual=false});

function toggleBtn(id,key){
  const btn=document.getElementById(id);
  btn.addEventListener('click',()=>{
    S[key]=!S[key];
    btn.classList.toggle('active',S[key]);
  });
}
toggleBtn('btn-force','showForce');
toggleBtn('btn-traj','showTraj');
toggleBtn('btn-truss','showTruss');

// 键盘快捷键
document.addEventListener('keydown',e=>{
  if(e.code==='Space'){e.preventDefault();btnPlay.click()}
});

// 初始轨迹预计算(填充轨迹曲线)
(function precompute(){
  for(let i=0;i<200;i++){
    S.trajPts.push(footPos(i/200));
  }
})();
</script>
</body>
</html>

实现说明

这个动画完整呈现了5连杆平面缩放机构的**最终理想解(IFR)**工作原理,核心围绕"结构即控制"理念展开:

机构运动学

  • 采用逆运动学方法:先定义足端在步态周期中的平滑轨迹(支撑相平移+摆动相正弦抬脚),再通过双圆交点求解两个"膝关节"A、B的位置
  • 选择内侧交叉构型(X形),这是5连杆缩放机构的标志性形态,两条链的主动杆在空间交叉,清晰展示非传统构型
  • 耦合杆⑤连接两根从动杆中点,呈现完整的5杆结构

IFR视觉引导

  1. 支撑相:连杆形成三角桁架(半透明绿色三角形高亮),力矢量箭头沿二力杆方向传递地面反力,直观呈现"仅受拉压、不受弯矩"的核心创新点
  2. 摆动相:足端轨迹以虚线描绘平滑弧线,电机旋转指示器实时显示驱动角度变化
  3. "STRUCTURE IS CONTROL" 文字在支撑相脉冲闪烁,强化理想解的核心概念

交互控制

  • 播放/暂停(空格键快捷键)、速度滑块相位滑块(可手动拖拽至任意步态时刻观察)
  • 力矢量/轨迹/桁架三个独立开关,可按需组合查看
  • 信息栏实时显示θ₁、θ₂角度、足端坐标和受力模式
你是怎么实现的

您的问题可以从两个层面理解,我分别回应:


关于动画的技术实现原理(工程内容,属于我的服务领域)

这个动画的核心实现包含以下几个关键技术环节:

1. 逆运动学求解

整个动画的驱动逻辑是"先定足端,再反推关节":

  • 首先定义足端 P 在一个步态周期中的参数化轨迹——支撑相(0~60%)沿地面水平移动,摆动相(60~100%)用正弦曲线抬起跨越障碍
  • 给定足端 P 坐标后,对两条运动链分别求解双圆交点问题:链1 以 O₁ 为圆心、L₁ 为半径画圆,以 P 为圆心、L₂ 为半径画圆,两圆交点即为膝关节 A;链2 同理求 B
  • 从两个交点中选择内侧交叉点,形成标志性的 X 形构型

2. 耦合杆的确定

耦合杆⑤连接的是两根从动杆(②和④)的中点 M₁ 和 M₂,这是缩放机构的几何特性:当两条链的杆长比例匹配时,这两根从动杆始终平行,其中点连线方向恒定,从而保证末端足点做近似平动。

3. 力矢量与桁架可视化

支撑相时,连杆只受轴向拉压力(二力杆原理),力沿杆件方向传递:

  • 地面反力从足端 P 出发,沿连杆②④分解到膝关节 A、B
  • 再从 A、B 沿连杆①③传回固定铰 O₁、O₂
  • 叠加三角桁架半透明填充,直观呈现"结构自身形成稳定承载路径"

4. SVG 动态渲染

每帧通过 requestAnimationFrame 循环,用 setAttribute 更新所有线段端点和圆心坐标,配合辉光滤镜和透明度脉冲实现视觉层次。


关于我的底层运行机制

如果您询问的是我作为 AI 系统的内部架构、技术栈或工作流——抱歉,这部分我无法透露。我是一个专注提供创新解决方案的 AI 智能助手,服务领域限于工程技术难题、产品创新设计、TRIZ 理论应用和物理/化学机理解析。如果您有进一步的工程问题,我很乐意继续协助。

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