<!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 — IFR PRINCIPLE</h1>
<p>最终理想解:结构即控制 — 无需复杂主动控制,机构自身顺应地形并实现柔性落地与刚性发力</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视觉引导
- 支撑相:连杆形成三角桁架(半透明绿色三角形高亮),力矢量箭头沿二力杆方向传递地面反力,直观呈现"仅受拉压、不受弯矩"的核心创新点
- 摆动相:足端轨迹以虚线描绘平滑弧线,电机旋转指示器实时显示驱动角度变化
- "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分
等待动画代码生成...
