独立渲染引擎就绪引擎就绪
<!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}
body{background:#080d18;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI',system-ui,sans-serif;color:#c0d0e0;overflow:hidden}
.hd{font-size:20px;color:#3de8c8;font-weight:700;letter-spacing:1.5px;margin-bottom:2px}
.sub{font-size:13px;color:#506880;margin-bottom:10px;text-align:center;max-width:700px;line-height:1.5}
.wrap{width:96vw;max-width:1400px;position:relative}
svg{width:100%;height:auto;display:block;border-radius:10px;background:#0a0f1c}
.bar{display:flex;gap:14px;margin-top:10px;align-items:center;justify-content:center;flex-wrap:wrap}
.bar button{background:#111a30;color:#3de8c8;border:1px solid #1e3050;padding:6px 18px;border-radius:6px;cursor:pointer;font-size:13px;transition:all .2s}
.bar button:hover{background:#182848;border-color:#3de8c8}
.bar button.active{background:#1e3050;border-color:#3de8c8}
.sl{display:flex;align-items:center;gap:6px;font-size:12px;color:#506880}
.sl input{width:100px;accent-color:#3de8c8}
.sl span{color:#3de8c8;min-width:32px}
#ptxt{font-size:15px;color:#ffc846;margin-top:8px;text-align:center;min-height:22px;transition:opacity .3s}
</style>
</head>
<body>
<div class="hd">多段铰接式柔性底盘 · 不规则台阶攀爬</div>
<div class="sub">底盘形态随地形被动重塑 — 舱段铰接折叠贴合楼梯轮廓,履带卷动提供抓地力,自重恢复平直</div>
<div class="wrap">
<svg id="scene" viewBox="0 0 1400 640" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gS" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#182840"/><stop offset="100%" stop-color="#0e1828"/></linearGradient>
<linearGradient id="gC" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3a6898"/><stop offset="100%" stop-color="#1c3858"/></linearGradient>
<linearGradient id="gF" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#e08838"/><stop offset="100%" stop-color="#a05820"/></linearGradient>
<linearGradient id="gT" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#404040"/><stop offset="100%" stop-color="#2a2a2a"/></linearGradient>
<filter id="gl"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="gl2"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="shd"><feGaussianBlur stdDeviation="8"/></filter>
<clipPath id="sceneClip"><rect x="0" y="0" width="1400" height="640"/></clipPath>
</defs>
<g clip-path="url(#sceneClip)">
<!-- Background grid -->
<g opacity="0.04">
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="1" fill="#4a7a9a"/>
</pattern>
<rect width="1400" height="640" fill="url(#grid)"/>
</g>
<!-- Stairs -->
<g id="stairs">
<rect x="0" y="520" width="560" height="130" fill="url(#gS)"/>
<rect x="560" y="435" width="130" height="215" fill="url(#gS)"/>
<rect x="690" y="330" width="140" height="320" fill="url(#gS)"/>
<rect x="830" y="330" width="570" height="320" fill="url(#gS)"/>
<!-- Step edges -->
<line x1="560" y1="435" x2="690" y2="435" stroke="#2a506e" stroke-width="1.5"/>
<line x1="560" y1="435" x2="560" y2="520" stroke="#2a506e" stroke-width="1.5"/>
<line x1="690" y1="330" x2="830" y2="330" stroke="#2a506e" stroke-width="1.5"/>
<line x1="690" y1="330" x2="690" y2="435" stroke="#2a506e" stroke-width="1.5"/>
<!-- Edge highlights -->
<line x1="560" y1="435" x2="690" y2="435" stroke="#4a88b0" stroke-width="0.5" opacity="0.5"/>
<line x1="690" y1="330" x2="830" y2="330" stroke="#4a88b0" stroke-width="0.5" opacity="0.5"/>
<!-- Dimension labels -->
<g fill="#38607a" font-size="10" text-anchor="middle">
<text x="625" y="490">h85</text>
<text x="760" y="395">h105</text>
</g>
<!-- Ground line -->
<line x1="0" y1="520" x2="560" y2="520" stroke="#2a506e" stroke-width="1" opacity="0.4"/>
<line x1="830" y1="330" x2="1400" y2="330" stroke="#2a506e" stroke-width="1" opacity="0.4"/>
</g>
<!-- Vehicle shadow -->
<ellipse id="vshadow" cx="200" cy="526" rx="120" ry="6" fill="#000" opacity="0.25" filter="url(#shd)"/>
<!-- Vehicle -->
<g id="vehicle">
<g id="vm">
<!-- === TRACK (dynamic path, drawn first) === -->
<path id="trackFill" d="" fill="#2a2a2a" stroke="none"/>
<path id="trackStroke" d="" fill="none" stroke="#444" stroke-width="1"/>
<path id="treadLine" d="" fill="none" stroke="#505050" stroke-width="1.5" stroke-dasharray="6 5" stroke-linecap="round"/>
<!-- === REAR CABIN === -->
<g id="cabinR">
<rect x="3" y="-18" width="72" height="16" rx="3" fill="url(#gC)" stroke="#4a88b8" stroke-width="1"/>
<circle cx="17" cy="0" r="5.5" fill="#2a4a6a" stroke="#4a88b8" stroke-width="0.8"/>
<circle cx="63" cy="0" r="5.5" fill="#2a4a6a" stroke="#4a88b8" stroke-width="0.8"/>
<circle cx="17" cy="0" r="2" fill="#1a2a44"/><circle cx="63" cy="0" r="2" fill="#1a2a44"/>
<!-- Wheel spokes -->
<line x1="17" y1="-4" x2="17" y2="4" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="13" y1="0" x2="21" y2="0" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="63" y1="-4" x2="63" y2="4" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="59" y1="0" x2="67" y2="0" stroke="#1a2a44" stroke-width="1.2"/>
<text x="39" y="-6" fill="#6aaace" font-size="7" text-anchor="middle" font-weight="600">后舱</text>
</g>
<!-- === HINGE 1 === -->
<g transform="translate(78,-10)">
<g id="h1r">
<circle cx="0" cy="0" r="5.5" fill="#ffc846" opacity="0.9" filter="url(#gl)"/>
<circle cx="0" cy="0" r="2.5" fill="#aa8800"/>
<!-- === MID CABIN === -->
<g id="cabinM">
<rect x="3" y="-8" width="72" height="16" rx="3" fill="url(#gC)" stroke="#4a88b8" stroke-width="1"/>
<circle cx="17" cy="10" r="5.5" fill="#2a4a6a" stroke="#4a88b8" stroke-width="0.8"/>
<circle cx="63" cy="10" r="5.5" fill="#2a4a6a" stroke="#4a88b8" stroke-width="0.8"/>
<circle cx="17" cy="10" r="2" fill="#1a2a44"/><circle cx="63" cy="10" r="2" fill="#1a2a44"/>
<line x1="17" y1="6" x2="17" y2="14" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="13" y1="10" x2="21" y2="10" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="63" y1="6" x2="63" y2="14" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="59" y1="10" x2="67" y2="10" stroke="#1a2a44" stroke-width="1.2"/>
<text x="39" y="4" fill="#6aaace" font-size="7" text-anchor="middle" font-weight="600">中舱</text>
</g>
<!-- === HINGE 2 === -->
<g transform="translate(78,0)">
<g id="h2r">
<circle cx="0" cy="0" r="5.5" fill="#ffc846" opacity="0.9" filter="url(#gl)"/>
<circle cx="0" cy="0" r="2.5" fill="#aa8800"/>
<!-- === FRONT CABIN === -->
<g id="cabinF">
<rect x="3" y="-8" width="72" height="16" rx="3" fill="url(#gF)" stroke="#e09040" stroke-width="1"/>
<circle cx="17" cy="10" r="5.5" fill="#a05020" stroke="#e09040" stroke-width="0.8"/>
<circle cx="63" cy="10" r="5.5" fill="#a05020" stroke="#e09040" stroke-width="0.8"/>
<circle cx="17" cy="10" r="2" fill="#1a2a44"/><circle cx="63" cy="10" r="2" fill="#1a2a44"/>
<line x1="17" y1="6" x2="17" y2="14" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="13" y1="10" x2="21" y2="10" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="63" y1="6" x2="63" y2="14" stroke="#1a2a44" stroke-width="1.2"/>
<line x1="59" y1="10" x2="67" y2="10" stroke="#1a2a44" stroke-width="1.2"/>
<text x="39" y="4" fill="#ffe0a0" font-size="7" text-anchor="middle" font-weight="600">前舱</text>
<!-- Front indicator arrow -->
<polygon points="78,2 84,0 78,-2" fill="#e09040" opacity="0.7"/>
</g>
</g>
</g>
</g>
</g>
<!-- Angle indicators (dynamic) -->
<g id="angIndicators">
<text id="ang1" x="0" y="0" fill="#ffc846" font-size="10" font-weight="600" opacity="0"></text>
<text id="ang2" x="0" y="0" fill="#ffc846" font-size="10" font-weight="600" opacity="0"></text>
</g>
</g>
</g>
<!-- Phase HUD -->
<rect x="16" y="16" width="300" height="38" rx="8" fill="#080d18" fill-opacity="0.88" stroke="#1e3050" stroke-width="1"/>
<circle id="phaseDot" cx="34" cy="35" r="5" fill="#3de8c8" filter="url(#gl2)"/>
<text id="phaseLbl" x="48" y="40" fill="#3de8c8" font-size="14" font-weight="600">准备就绪</text>
<!-- Legend -->
<g transform="translate(16,590)">
<rect x="0" y="0" width="440" height="36" rx="6" fill="#080d18" fill-opacity="0.8" stroke="#1e3050" stroke-width="0.5"/>
<circle cx="18" cy="18" r="5" fill="#ffc846" opacity="0.8"/><text x="30" y="22" fill="#8098b0" font-size="10">铰接点 (≤45°)</text>
<rect x="120" y="13" width="20" height="10" rx="2" fill="#3a6898"/><text x="146" y="22" fill="#8098b0" font-size="10">舱段</text>
<rect x="195" y="13" width="20" height="10" rx="2" fill="#e08838"/><text x="221" y="22" fill="#8098b0" font-size="10">前舱(高亮)</text>
<rect x="300" y="13" width="20" height="10" rx="2" fill="#2a2a2a" stroke="#444" stroke-width="0.5"/><text x="326" y="22" fill="#8098b0" font-size="10">柔性履带</text>
</g>
<!-- Principle callout -->
<g id="callout" opacity="0">
<rect x="900" y="440" width="380" height="90" rx="8" fill="#0c1524" fill-opacity="0.92" stroke="#3de8c8" stroke-width="0.8"/>
<text x="920" y="465" fill="#3de8c8" font-size="12" font-weight="600">核心原理:形态被动重塑</text>
<text x="920" y="485" fill="#8098b0" font-size="11">底盘形态随地形被动改变,而非刚性对抗。</text>
<text x="920" y="503" fill="#8098b0" font-size="11">铰接结构使舱段贴合不规则轮廓,</text>
<text x="920" y="521" fill="#ffc846" font-size="11">履带持续卷动提供抓地力 → 如毛毛虫蠕动攀爬</text>
</g>
</g>
</svg>
</div>
<div class="bar">
<button id="btnReplay">🔄 重播</button>
<button id="btnPause">⏸ 暂停</button>
<div class="sl">
<span>速度</span>
<input type="range" id="spdSlider" min="0.3" max="2.5" step="0.1" value="1">
<span id="spdLbl">1.0x</span>
</div>
</div>
<div id="ptxt"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
(function(){
/* ── State ── */
const S = { vx:60, vy:513, a1:0, a2:0, tread:0 };
const GROUND_Y = 520, STEP1_TOP = 435, STEP2_TOP = 330;
const TRACK_BOT = 7; // track bottom in VM local y
/* ── DOM ── */
const vm = document.getElementById('vm');
const h1r = document.getElementById('h1r');
const h2r = document.getElementById('h2r');
const trackFill = document.getElementById('trackFill');
const trackStroke = document.getElementById('trackStroke');
const treadLine = document.getElementById('treadLine');
const vshadow = document.getElementById('vshadow');
const ang1 = document.getElementById('ang1');
const ang2 = document.getElementById('ang2');
const phaseLbl = document.getElementById('phaseLbl');
const phaseDot = document.getElementById('phaseDot');
const callout = document.getElementById('callout');
const ptxt = document.getElementById('ptxt');
/* ── Transform helpers ── */
function h1ToVM(lx,ly){
const c=Math.cos(S.a1*Math.PI/180), s=Math.sin(S.a1*Math.PI/180);
return {x:78+lx*c-ly*s, y:-10+lx*s+ly*c};
}
function h2ToVM(lx,ly){
const c1=Math.cos(S.a1*Math.PI/180), s1=Math.sin(S.a1*Math.PI/180);
const c2=Math.cos(S.a2*Math.PI/180), s2=Math.sin(S.a2*Math.PI/180);
const rx=lx*c2-ly*s2, ry=lx*s2+ly*c2;
const tx=rx+78, ty=ry;
return {x:78+tx*c1-ty*s1, y:-10+tx*s1+ty*c1};
}
/* ── Track path ── */
function buildTrack(){
const a1r=S.a1*Math.PI/180, a2r=S.a2*Math.PI/180;
// Bottom contour key points (VM coords)
const bp=[
{x:-2,y:TRACK_BOT},{x:82,y:TRACK_BOT},
h1ToVM(-2,17), h1ToVM(82,17),
h2ToVM(-2,17), h2ToVM(82,17)
];
// Top contour key points
const tp=[
{x:-2,y:-20},{x:82,y:-20},
h1ToVM(-2,-10), h1ToVM(82,-10),
h2ToVM(-2,-10), h2ToVM(82,-10)
];
// Front cap control point
const fmx=(bp[5].x+tp[5].x)/2, fmy=(bp[5].y+tp[5].y)/2;
const totalA=(S.a1+S.a2)*Math.PI/180;
const fcx=fmx+14*Math.cos(totalA), fcy=fmy+14*Math.sin(totalA);
// Rear cap control point
const rmx=(bp[0].x+tp[0].x)/2, rmy=(bp[0].y+tp[0].y)/2;
const rcx=rmx-14, rcy=rmy;
let d=`M${bp[0].x},${bp[0].y}`;
for(let i=1;i<bp.length;i++) d+=` L${bp[i].x},${bp[i].y}`;
d+=` Q${fcx},${fcy} ${tp[5].x},${tp[5].y}`;
for(let i=tp.length-2;i>=0;i--) d+=` L${tp[i].x},${tp[i].y}`;
d+=` Q${rcx},${rcy} ${bp[0].x},${bp[0].y} Z`;
trackFill.setAttribute('d',d);
trackStroke.setAttribute('d',d);
// Tread line (bottom contour only, offset inward slightly)
let td=`M${bp[0].x},${bp[0].y-3}`;
for(let i=1;i<bp.length;i++) td+=` L${bp[i].x},${bp[i].y-3}`;
treadLine.setAttribute('d',td);
}
/* ── Scene update ── */
function update(){
vm.setAttribute('transform',`translate(${S.vx},${S.vy})`);
h1r.setAttribute('transform',`rotate(${S.a1})`);
h2r.setAttribute('transform',`rotate(${S.a2})`);
buildTrack();
// Tread dashoffset animation
const tl2 = treadLine.getTotalLength();
treadLine.style.strokeDashoffset = S.tread;
// Shadow
vshadow.setAttribute('cx', S.vx+120);
const surfaceY = S.vy + TRACK_BOT;
vshadow.setAttribute('cy', surfaceY + 8);
const shadowScale = Math.max(0.3, 1 - (520 - surfaceY) / 400);
vshadow.setAttribute('rx', 110 * shadowScale);
vshadow.setAttribute('opacity', 0.2 * shadowScale);
// Angle indicators
const h1pos = {x: S.vx+78, y: S.vy-10};
const h2pos = h2ScreenPos();
if(Math.abs(S.a1)>2){
ang1.setAttribute('x', h1pos.x - 30);
ang1.setAttribute('y', h1pos.y - 14);
ang1.textContent = Math.round(Math.abs(S.a1))+'°';
ang1.setAttribute('opacity','0.9');
} else { ang1.setAttribute('opacity','0'); }
if(Math.abs(S.a2)>2){
ang2.setAttribute('x', h2pos.x - 30);
ang2.setAttribute('y', h2pos.y - 14);
ang2.textContent = Math.round(Math.abs(S.a2))+'°';
ang2.setAttribute('opacity','0.9');
} else { ang2.setAttribute('opacity','0'); }
}
function h2ScreenPos(){
const c1=Math.cos(S.a1*Math.PI/180), s1=Math.sin(S.a1*Math.PI/180);
return {x: S.vx+78+78*c1, y: S.vy-10+78*s1};
}
/* ── Phase label ── */
function setPhase(txt,color){
phaseLbl.textContent = txt;
phaseDot.setAttribute('fill', color||'#3de8c8');
ptxt.textContent = txt;
}
/* ── Timeline ── */
let tl = null;
function buildTimeline(){
if(tl) tl.kill();
// Reset state
gsap.set(S,{vx:60,vy:513,a1:0,a2:0,tread:0});
vm.setAttribute('transform','translate(60,513)');
h1r.setAttribute('transform','rotate(0)');
h2r.setAttribute('transform','rotate(0)');
setPhase('准备就绪','#3de8c8');
callout.setAttribute('opacity','0');
tl = gsap.timeline({
onUpdate: update,
defaults: {ease:'power2.inOut'}
});
// ── Phase 1: 平地接近 ──
tl.to(S,{vx:310,duration:2.2,ease:'power1.inOut'})
.add(()=>setPhase('① 平地接近','#3de8c8'))
// ── Phase 2: 前舱接触台阶并折叠 ──
.to(S,{vx:360,a2:-42,duration:2,ease:'power2.inOut'})
.add(()=>setPhase('② 前舱接触台阶,被动折叠','#ffc846'))
// ── Phase 3: 中舱折叠,整车抬升上第一级 ──
.to(S,{vx:445,vy:428,a1:-38,a2:-6,duration:2.8,ease:'power2.inOut'})
.add(()=>setPhase('③ 中舱被动折叠贴合台阶','#ffc846'))
.to(callout,{attr:{opacity:0.95},duration:0.8},'<0.5')
// ── Phase 4: 前舱到达第二级台阶面,折叠 ──
.to(S,{vx:530,vy:428,a1:0,a2:-42,duration:2.2,ease:'power2.inOut'})
.add(()=>setPhase('④ 前舱折叠贴合第二级台阶','#e08040'))
// ── Phase 5: 中舱折叠,整车攀爬第二级 ──
.to(S,{vx:620,vy:323,a1:-38,a2:-6,duration:2.8,ease:'power2.inOut'})
.add(()=>setPhase('⑤ 履带卷动攀爬,如毛毛虫蠕动','#3de8c8'))
// ── Phase 6: 跨越台阶,自重恢复平直 ──
.to(S,{vx:760,vy:323,a1:0,a2:0,duration:2.5,ease:'power2.inOut'})
.add(()=>setPhase('⑥ 跨越台阶,自重恢复平直','#3de8c8'))
// Hold
.to({},{duration:1.5})
.add(()=>setPhase('攀爬完成 — 柔性底盘自适应不规则台阶','#3de8c8'))
// Tread animation throughout
tl.to(S,{tread:-600,duration:16,ease:'none'},0);
return tl;
}
/* ── Controls ── */
const btnReplay = document.getElementById('btnReplay');
const btnPause = document.getElementById('btnPause');
const spdSlider = document.getElementById('spdSlider');
const spdLbl = document.getElementById('spdLbl');
btnReplay.addEventListener('click',()=>{
buildTimeline();
});
btnPause.addEventListener('click',()=>{
if(!tl) return;
if(tl.isActive()){
tl.pause();
btnPause.textContent = '▶ 继续';
} else {
tl.resume();
btnPause.textContent = '⏸ 暂停';
}
});
spdSlider.addEventListener('input',()=>{
const v = parseFloat(spdSlider.value);
spdLbl.textContent = v.toFixed(1)+'x';
if(tl) tl.timeScale(v);
});
/* ── Auto-play on load ── */
buildTimeline();
})();
</script>
</body>
</html>
实现说明:
层级结构:三轮舱段通过嵌套
<g>+transform="translate"实现铰接层级——后舱固定在vehicle_mover坐标系内,中舱嵌套在hinge1_rotator内绕铰接点旋转,前舱嵌套在hinge2_rotator内,旋转自动级联。柔性履带:通过
h1ToVM/h2ToVM函数将各舱段的轨道轮廓关键点换算到统一坐标系,动态拼合底边+顶边+前后弧帽的闭合路径。每帧onUpdate重算d属性,保证履带始终包裹所有舱段并在弯折处平滑过渡。动作时序:GSAP 主时间轴按 6 个阶段编排——平地接近 → 前舱折叠 → 中舱折叠 → 前舱第二级折叠 → 中舱第二级折叠+攀爬 → 自重恢复平直。铰接角度通过
rotate()SVG 属性直接设置,绕各铰链(0,0)旋转,不存在 transform-origin 歧义。视觉引导:前舱用橙色高亮标记创新焦点;铰接点用黄色光晕强调;弯折时动态显示角度数值;阶段标签实时更新;原理说明浮窗在关键阶段淡入。
交互:重播按钮重建时间轴从头播放;暂停/继续按钮控制播放状态;速度滑块通过
timeScale()实时调速。
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
