独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多段铰接柔性底盘 · 阶梯自适应攀爬</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
background:#0c1117;
display:flex;flex-direction:column;
align-items:center;justify-content:center;
font-family:-apple-system,"Segoe UI",system-ui,sans-serif;
color:#90a4ae;padding:8px;
}
.hd{text-align:center;margin-bottom:6px}
.hd h2{font-size:18px;color:#4fc3f7;font-weight:700;letter-spacing:.4px}
.hd p{font-size:11px;color:#546e7a;margin-top:2px}
.stg{
width:96vw;max-width:1160px;
border:1px solid #1a2a38;border-radius:8px;
background:#101820;overflow:hidden;
}
svg{display:block;width:100%;height:auto}
.bar{margin-top:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center}
.bt{
padding:3px 13px;border:1px solid #4fc3f7;background:none;
color:#4fc3f7;border-radius:3px;cursor:pointer;font-size:12px;
}
.bt:hover{background:#4fc3f718}
.lb{font-size:11px}
.vl{color:#4fc3f7;font-weight:600}
input[type=range]{width:88px;accent-color:#4fc3f7}
</style>
</head>
<body>
<div class="hd">
<h2>多段铰接式柔性底盘 — 阶梯自适应攀爬原理</h2>
<p>万向节串联独立舱段 · 被动折叠贴合台阶 · 履带持续卷动抓地 · 自重恢复平直</p>
</div>
<div class="stg">
<svg id="scene" viewBox="0 0 1100 560" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow"><feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="glowSm"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<linearGradient id="gB" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#546e7a"/><stop offset="100%" stop-color="#37474f"/></linearGradient>
<linearGradient id="gS" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1a2530"/><stop offset="100%" stop-color="#121a20"/></linearGradient>
<pattern id="gd" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M40 0L0 0 0 40" fill="none" stroke="#4fc3f7" stroke-width=".3"/>
</pattern>
<marker id="arrM" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#ff7043"/>
</marker>
<marker id="arrC" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#4fc3f7"/>
</marker>
</defs>
<!-- Grid -->
<rect width="1100" height="560" fill="url(#gd)" opacity=".04"/>
<!-- Terrain -->
<g id="terrain">
<path d="M0,470 L370,470 L370,560 L0,560Z" fill="#131b22" stroke="#1e3040" stroke-width=".5"/>
<path d="M370,470 L370,410 L520,410 L520,560 L370,560Z" fill="url(#gS)" stroke="#1e3040" stroke-width=".5"/>
<path d="M520,410 L520,350 L670,350 L670,560 L520,560Z" fill="url(#gS)" stroke="#1e3040" stroke-width=".5"/>
<path d="M670,350 L1100,350 L1100,560 L670,560Z" fill="#131b22" stroke="#1e3040" stroke-width=".5"/>
<!-- Step edge highlights -->
<line x1="370" y1="470" x2="370" y2="410" stroke="#4fc3f7" stroke-width="2.5" opacity=".25"/>
<line x1="370" y1="410" x2="520" y2="410" stroke="#4fc3f7" stroke-width="2.5" opacity=".35"/>
<line x1="520" y1="410" x2="520" y2="350" stroke="#4fc3f7" stroke-width="2.5" opacity=".25"/>
<line x1="520" y1="350" x2="670" y2="350" stroke="#4fc3f7" stroke-width="2.5" opacity=".35"/>
<!-- Dimension annotations -->
<g opacity=".3" fill="#4fc3f7" font-size="9">
<text x="345" y="445" text-anchor="end">←60→</text>
<line x1="350" y1="470" x2="350" y2="410" stroke="#4fc3f7" stroke-width=".6" stroke-dasharray="3 2"/>
<text x="445" y="403" text-anchor="middle">←150→</text>
</g>
<g fill="#4fc3f7" font-size="10" opacity=".22">
<text x="185" y="490" text-anchor="middle">平地行驶</text>
<text x="445" y="448" text-anchor="middle">台阶 1</text>
<text x="595" y="388" text-anchor="middle">台阶 2</text>
<text x="880" y="370" text-anchor="middle">顶部平台</text>
</g>
</g>
<!-- Force arrows (appear during contact) -->
<g id="forceArrows" opacity="0">
<line id="fArr1" x1="370" y1="460" x2="370" y2="432" stroke="#ff7043" stroke-width="2.5" marker-end="url(#arrM)"/>
<text x="355" y="448" text-anchor="end" fill="#ff7043" font-size="10" font-weight="600">反力</text>
<line id="fArr2" x1="520" y1="400" x2="520" y2="372" stroke="#ff7043" stroke-width="2.5" marker-end="url(#arrM)"/>
<text x="505" y="388" text-anchor="end" fill="#ff7043" font-size="10" font-weight="600">反力</text>
</g>
<!-- Trajectory hint -->
<path id="trajHint" d="M370,470 Q370,440 440,410 Q510,380 520,350" fill="none" stroke="#4fc3f7" stroke-width="1.2" stroke-dasharray="6 4" opacity="0" filter="url(#glowSm)"/>
<!-- Chassis root -->
<g id="chassisRoot"></g>
<!-- Callout banners -->
<g id="anns">
<g id="ann-fold" opacity="0">
<rect x="310" y="60" width="260" height="34" rx="6" fill="#0a1520" stroke="#ff9800" stroke-width="1.2" opacity=".9"/>
<text x="440" y="82" text-anchor="middle" fill="#ff9800" font-size="13" font-weight="700">铰接关节被动折叠 ≤ 45°</text>
<line x1="440" y1="94" x2="440" y2="140" stroke="#ff9800" stroke-width="1" stroke-dasharray="4 3" opacity=".5"/>
</g>
<g id="ann-track" opacity="0">
<rect x="40" y="510" width="230" height="34" rx="6" fill="#0a1520" stroke="#66bb6a" stroke-width="1.2" opacity=".9"/>
<text x="155" y="532" text-anchor="middle" fill="#66bb6a" font-size="13" font-weight="700">履带持续卷动 → 抓地力</text>
</g>
<g id="ann-recover" opacity="0">
<rect x="730" y="280" width="230" height="34" rx="6" fill="#0a1520" stroke="#4fc3f7" stroke-width="1.2" opacity=".9"/>
<text x="845" y="302" text-anchor="middle" fill="#4fc3f7" font-size="13" font-weight="700">自重恢复平直形态 ✓</text>
</g>
</g>
<!-- Phase indicator -->
<g id="phaseLabel" opacity=".5">
<rect x="16" y="16" width="120" height="24" rx="4" fill="#0a1520" stroke="#263238" stroke-width="1"/>
<text id="phaseText" x="76" y="33" text-anchor="middle" fill="#78909c" font-size="11" font-weight="600">平地行驶</text>
</g>
</svg>
</div>
<div class="bar">
<button class="bt" id="bReplay">⏮ 重播</button>
<button class="bt" id="bPause">⏸ 暂停</button>
<span class="lb">速度 <span class="vl" id="spdV">1.0x</span></span>
<input type="range" id="spdR" min="0.3" max="2.5" step="0.1" value="1">
<span class="lb">弯折角 <span class="vl" id="angV">38°</span></span>
<input type="range" id="angR" min="20" max="45" step="1" value="38">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
(function(){
const NS="http://www.w3.org/2000/svg";
const root=document.getElementById("chassisRoot");
/* ---- Config ---- */
const SW=72,BH=18,WR=10,N=5;
const GY=470,S1Y=410,S2Y=350;
let SA=-38; // step angle (will be controlled by slider)
const segRots=[],joints=[],trackPaths=[],wheelSpokes=[];
/* ---- Build segments ---- */
function buildSeg(par,idx){
// Rotation group
const rg=document.createElementNS(NS,"g");
rg.id=`sr${idx}`;
par.appendChild(rg);
segRots.push(rg);
// Track outline (dashed for tread motion)
const trk=document.createElementNS(NS,"rect");
trk.setAttribute("x","2");trk.setAttribute("y",`${-WR-2}`);
trk.setAttribute("width",`${SW-4}`);trk.setAttribute("height",`${WR*2+4}`);
trk.setAttribute("rx",`${WR}`);
trk.setAttribute("fill","#141c22");trk.setAttribute("stroke","#37474f");
trk.setAttribute("stroke-width","1.5");
trk.setAttribute("stroke-dasharray","5 3.5");
trk.classList.add("tk");
rg.appendChild(trk);
trackPaths.push(trk);
// Tread marks (bottom of track)
for(let t=0;t<8;t++){
const tx=6+t*((SW-12)/8);
const ln=document.createElementNS(NS,"line");
ln.setAttribute("x1",tx);ln.setAttribute("y1",`${WR-2}`);
ln.setAttribute("x2",tx);ln.setAttribute("y2",`${WR+2}`);
ln.setAttribute("stroke","#455a64");ln.setAttribute("stroke-width","1.8");
ln.setAttribute("opacity",".5");ln.setAttribute("stroke-linecap","round");
rg.appendChild(ln);
}
// Flexible coupling membrane at rear
if(idx>0){
const cp=document.createElementNS(NS,"rect");
cp.setAttribute("x","-7");cp.setAttribute("y",`${-WR}`);
cp.setAttribute("width","14");cp.setAttribute("height",`${WR*2}`);
cp.setAttribute("rx","3");cp.setAttribute("fill","#1a252e");
cp.setAttribute("stroke","#263238");cp.setAttribute("stroke-width",".8");
rg.appendChild(cp);
}
// Body
const bd=document.createElementNS(NS,"rect");
bd.setAttribute("x","4");bd.setAttribute("y",`${-WR-BH-2}`);
bd.setAttribute("width",`${SW-8}`);bd.setAttribute("height",BH);
bd.setAttribute("rx","3");bd.setAttribute("fill","url(#gB)");
bd.setAttribute("stroke","#607d8b");bd.setAttribute("stroke-width","1");
rg.appendChild(bd);
// Segment label
const lb=document.createElementNS(NS,"text");
lb.setAttribute("x",`${SW/2}`);lb.setAttribute("y",`${-WR-BH/2+1}`);
lb.setAttribute("text-anchor","middle");lb.setAttribute("fill","#b0bec5");
lb.setAttribute("font-size","9");lb.setAttribute("font-weight","700");
lb.textContent=idx===0?"尾":idx===N-1?"首":`S${idx+1}`;
rg.appendChild(lb);
// Motor indicator (small rect on body)
const mt=document.createElementNS(NS,"rect");
mt.setAttribute("x",`${SW/2-6}`);mt.setAttribute("y",`${-WR-BH+2}`);
mt.setAttribute("width","12");mt.setAttribute("height","5");
mt.setAttribute("rx","1");mt.setAttribute("fill","#26a69a");
mt.setAttribute("opacity",".6");
rg.appendChild(mt);
// Wheels with spokes
[WR+2,SW-WR-2].forEach(cx=>{
const wh=document.createElementNS(NS,"circle");
wh.setAttribute("cx",cx);wh.setAttribute("cy","0");
wh.setAttribute("r",WR);wh.setAttribute("fill","#0d1218");
wh.setAttribute("stroke","#455a64");wh.setAttribute("stroke-width","1.5");
rg.appendChild(wh);
// Spoke (for rotation viz)
const sp=document.createElementNS(NS,"line");
sp.setAttribute("x1",`${cx-5}`);sp.setAttribute("y1","0");
sp.setAttribute("x2",`${cx+5}`);sp.setAttribute("y2","0");
sp.setAttribute("stroke","#546e7a");sp.setAttribute("stroke-width","1.2");
sp.classList.add("spoke");
rg.appendChild(sp);
wheelSpokes.push(sp);
// Hub
const hb=document.createElementNS(NS,"circle");
hb.setAttribute("cx",cx);hb.setAttribute("cy","0");
hb.setAttribute("r","2");hb.setAttribute("fill","#607d8b");
rg.appendChild(hb);
});
// Joint indicator
if(idx>0){
// Outer glow ring
const jg=document.createElementNS(NS,"circle");
jg.setAttribute("cx","0");jg.setAttribute("cy","0");
jg.setAttribute("r","8");jg.setAttribute("fill","none");
jg.setAttribute("stroke","#ff9800");jg.setAttribute("stroke-width","1");
jg.setAttribute("opacity","0");
jg.classList.add("jg");jg.id=`jg${idx}`;
rg.appendChild(jg);
// Inner dot
const jt=document.createElementNS(NS,"circle");
jt.setAttribute("cx","0");jt.setAttribute("cy","0");
jt.setAttribute("r","4.5");jt.setAttribute("fill","#ff9800");
jt.setAttribute("opacity",".45");
jt.id=`jt${idx}`;
rg.appendChild(jt);
joints.push(jt);
}
// Front arrow
if(idx===N-1){
const ar=document.createElementNS(NS,"polygon");
ar.setAttribute("points",`${SW},0 ${SW-8},-5 ${SW-8},5`);
ar.setAttribute("fill","#4fc3f7");ar.setAttribute("opacity",".55");
rg.appendChild(ar);
}
// Nest next segment
if(idx<N-1){
const nx=document.createElementNS(NS,"g");
nx.setAttribute("transform",`translate(${SW},0)`);
rg.appendChild(nx);
buildSeg(nx,idx+1);
}
}
buildSeg(root,0);
/* ---- Keyframes ---- */
function makeKF(){
const a=SA;
return [
{t:0, rx:-280,ry:GY, abs:[0,0,0,0,0]},
{t:2.8, rx:10, ry:GY, abs:[0,0,0,0,0]}, // approach
{t:3.9, rx:45, ry:GY, abs:[0,0,0,0,a]}, // seg4 rising
{t:5.0, rx:80, ry:GY, abs:[0,0,0,a,0]}, // seg3 rising
{t:6.1, rx:115, ry:GY, abs:[0,0,a,0,0]}, // seg2 rising
{t:7.2, rx:150, ry:GY, abs:[0,a,0,0,0]}, // seg1 rising
{t:8.3, rx:185, ry:GY-15, abs:[a,0,0,0,0]}, // seg0 rising
{t:9.4, rx:220, ry:S1Y, abs:[0,0,0,0,0]}, // all on step1
// Step 2
{t:10.5,rx:255, ry:S1Y, abs:[0,0,0,0,a]},
{t:11.6,rx:290, ry:S1Y, abs:[0,0,0,a,0]},
{t:12.7,rx:325, ry:S1Y, abs:[0,0,a,0,0]},
{t:13.8,rx:360, ry:S1Y, abs:[0,a,0,0,0]},
{t:14.9,rx:395, ry:S1Y-15, abs:[a,0,0,0,0]},
{t:16.0,rx:430, ry:S2Y, abs:[0,0,0,0,0]}, // all on top
// Exit
{t:18.5,rx:800, ry:S2Y, abs:[0,0,0,0,0]},
];
}
function absToRel(abs){
const r=[abs[0]];
for(let i=1;i<abs.length;i++) r.push(abs[i]-abs[i-1]);
return r;
}
/* ---- Tread dash animation ---- */
let treadOffset=0;
function animateTreads(){
treadOffset-=0.8;
trackPaths.forEach(tp=>{
tp.setAttribute("stroke-dashoffset",treadOffset);
});
wheelSpokes.forEach(sp=>{
const cur=parseFloat(sp.getAttribute("transform")?.replace(/[^0-9.-]/g,"")||"0");
sp.setAttribute("transform",`rotate(${cur-3},${sp.getAttribute("x1").replace(/-.*/,"")||sp.x1?.baseVal?.value||0},0)`);
});
requestAnimationFrame(animateTreads);
}
// Simpler spoke rotation via GSAP
gsap.to(".spoke",{rotation:-360,duration:1,ease:"none",repeat:-1,svgOrigin:"center center"});
// Tread stroke-dashoffset animation
gsap.to(trackPaths,{strokeDashoffset:-20,duration:0.6,ease:"none",repeat:-1});
/* ---- Build timeline ---- */
let tl;
function buildTimeline(){
if(tl) tl.kill();
tl=gsap.timeline({repeat:-1,repeatDelay:1.2,defaults:{ease:"power2.inOut"}});
const KF=makeKF();
// Convert abs to rel
KF.forEach(k=>k.rel=absToRel(k.abs));
// Set initial
gsap.set(root,{x:KF[0].rx,y:KF[0].ry});
segRots.forEach((sr,i)=>gsap.set(sr,{rotation:KF[0].rel[i],svgOrigin:"0 0"}));
// Root position keyframes
for(let k=1;k<KF.length;k++){
const p=KF[k-1],c=KF[k],dur=c.t-p.t;
const ez=p.t<2.5?"none":"power2.inOut";
tl.to(root,{x:c.rx,y:c.ry,duration:dur,ease:ez},p.t);
}
// Segment rotation keyframes
for(let i=0;i<N;i++){
for(let k=1;k<KF.length;k++){
const p=KF[k-1],c=KF[k],dur=c.t-p.t;
tl.to(segRots[i],{rotation:c.rel[i],duration:dur},p.t);
}
}
// Joint highlighting
const jh=[
{j:4,s:2.8,e:5.0},{j:3,s:3.9,e:6.1},{j:2,s:5.0,e:7.2},{j:1,s:6.1,e:8.3},
{j:4,s:10.5,e:11.6},{j:3,s:11.6,e:12.7},{j:2,s:12.7,e:13.8},{j:1,s:13.8,e:14.9},
];
jh.forEach(({j,s,e})=>{
const jt=document.getElementById(`jt${j}`);
const jg=document.getElementById(`jg${j}`);
if(jt){
tl.to(jt,{attr:{r:6,fill:"#ffeb3b"},opacity:1,duration:.25},s)
.to(jt,{attr:{r:4.5,fill:"#ff9800"},opacity:.45,duration:.25},e-.25);
}
if(jg){
tl.to(jg,{opacity:.6,duration:.25},s)
.to(jg,{opacity:0,duration:.25},e-.25);
}
});
// Force arrows
tl.to("#forceArrows",{opacity:1,duration:.4},2.8)
.to("#forceArrows",{opacity:0,duration:.4},9.0)
.to("#forceArrows",{opacity:1,duration:.4},10.5)
.to("#forceArrows",{opacity:0,duration:.4},16.0);
// Trajectory hint
tl.to("#trajHint",{opacity:.35,duration:.5},3.0)
.to("#trajHint",{opacity:0,duration:.5},9.0);
// Annotations
tl.to("#ann-fold",{opacity:1,duration:.4},3.2)
.to("#ann-fold",{opacity:0,duration:.4},8.5)
.to("#ann-fold",{opacity:1,duration:.4},10.8)
.to("#ann-fold",{opacity:0,duration:.4},15.2);
tl.to("#ann-track",{opacity:1,duration:.4},3.0)
.to("#ann-track",{opacity:0,duration:.4},7.5);
tl.to("#ann-recover",{opacity:1,duration:.4},16.0)
.to("#ann-recover",{opacity:0,duration:.4},18.0);
// Phase label
const phases=[
{t:0,txt:"平地行驶"},{t:2.8,txt:"触碰台阶"},{t:3.9,txt:"被动折叠攀爬"},
{t:9.4,txt:"台阶1完成"},{t:10.5,txt:"继续攀爬台阶2"},
{t:16.0,txt:"自重恢复平直"},{t:17.0,txt:"顶部行驶"},
];
phases.forEach(({t,txt},i)=>{
const dur=i<phases.length-1?phases[i+1].t-t:2;
tl.to("#phaseText",{textContent:txt,duration:.01},t);
});
}
buildTimeline();
/* ---- Controls ---- */
document.getElementById("bReplay").addEventListener("click",()=>tl.restart());
let paused=false;
document.getElementById("bPause").addEventListener("click",()=>{
if(paused){tl.play();paused=false;document.getElementById("bPause").textContent="⏸ 暂停";}
else{tl.pause();paused=true;document.getElementById("bPause").textContent="▶ 继续";}
});
const spdR=document.getElementById("spdR"),spdV=document.getElementById("spdV");
spdR.addEventListener("input",()=>{
const v=parseFloat(spdR.value);
tl.timeScale(v);spdV.textContent=v.toFixed(1)+"x";
});
const angR=document.getElementById("angR"),angV=document.getElementById("angV");
angR.addEventListener("input",()=>{
const v=parseInt(angR.value);
SA=-v;angV.textContent=v+"°";
buildTimeline();
tl.seek(0);
});
/* ---- Keyboard shortcuts ---- */
document.addEventListener("keydown",e=>{
if(e.key===" "){e.preventDefault();document.getElementById("bPause").click();}
if(e.key==="r")document.getElementById("bReplay").click();
});
})();
</script>
</body>
</html>
实现说明:
核心机构建模:5 个独立舱段通过嵌套 SVG
<g>元素实现万向节串联,每个舱段包含车体、双驱动轮、履带轮廓(虚线描边模拟滚动纹理)、防滑筋纹路、电机指示块及柔性连接膜片。攀爬时序编排:采用绝对角度→相对角度转换策略——先定义每个关键帧下各舱段的绝对倾斜角,再递推算出嵌套结构所需的相对旋转值(
rel[i] = abs[i] - abs[i-1]),完美解决嵌套变换的级联补偿问题。整条时间轴统一编排在一个gsap.timeline()中,包含平地行驶→触碰台阶→被动折叠→履带卷动→自重恢复的完整过程。视觉引导:
- 铰接关节在弯折时从橙色突变为黄色并显示外圈辉光
- 台阶立面处出现反力箭头(橙红色)
- 攀爬轨迹虚线提示路径
- 三个浮动标注(折叠/抓地/恢复)在对应阶段淡入
交互控制:
- 弯折角滑块(20°–45°):实时改变舱段最大折叠角度,重建时间轴
- 速度滑块:
timeScale控制播放速率 - 重播/暂停按钮及键盘快捷键(空格暂停、R 重播)
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
