分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柔性分段底盘 · 被动贴合越障</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0f1c;color:#a0b0c8;font-family:'JetBrains Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;position:relative}
body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 25% 15%,rgba(61,107,156,.15),transparent 55%),radial-gradient(ellipse at 75% 85%,rgba(255,149,0,.05),transparent 55%);pointer-events:none;z-index:0}
body::after{content:'';position:fixed;inset:0;background-image:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(255,255,255,.006) 2px,rgba(255,255,255,.006) 4px);pointer-events:none;z-index:0}
.container{width:100%;max-width:1340px;padding:14px 24px;position:relative;z-index:1;display:flex;flex-direction:column;align-items:center}
.header{width:100%;display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:6px;animation:fadeInDown .6s ease-out}
.header-left h1{font-family:'Big Shoulders Display',sans-serif;font-weight:900;font-size:28px;letter-spacing:2px;color:#e8eef5;text-transform:uppercase;line-height:1}
.header-left .subtitle{font-size:10px;color:#ff9500;letter-spacing:2px;margin-top:4px;text-transform:uppercase}
.header-right{text-align:right;font-size:9px;color:#5a6a80;line-height:1.6}
.header-right .ifr-tag{display:inline-block;padding:2px 8px;background:rgba(255,149,0,.1);border:1px solid rgba(255,149,0,.3);border-radius:3px;color:#ff9500;font-weight:700;letter-spacing:1px;margin-bottom:3px}
.svg-wrapper{width:100%;display:flex;justify-content:center;position:relative;animation:fadeIn .8s ease-out .15s both}
#main-svg{width:100%;max-width:1340px;height:auto;max-height:74vh}
.params{position:absolute;bottom:6px;right:10px;font-size:8px;color:#506070;line-height:1.6;text-align:right;font-family:'JetBrains Mono',monospace;pointer-events:none}
.params strong{color:#ff9500;font-weight:500}
.controls{display:flex;gap:18px;align-items:center;margin-top:8px;padding:5px 14px;background:rgba(15,22,38,.7);border:1px solid rgba(70,100,150,.15);border-radius:6px;animation:fadeInUp .6s ease-out .3s both;backdrop-filter:blur(8px)}
.control-group{display:flex;align-items:center;gap:8px;font-size:10px;color:#708090;font-family:'JetBrains Mono',monospace}
.control-group label{white-space:nowrap;letter-spacing:.5px}
input[type=range]{-webkit-appearance:none;width:90px;height:2px;background:rgba(70,100,150,.3);border-radius:1px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:10px;height:10px;background:#ff9500;border-radius:50%;cursor:pointer;box-shadow:0 0 6px rgba(255,149,0,.5)}
input[type=range]::-moz-range-thumb{width:10px;height:10px;background:#ff9500;border-radius:50%;cursor:pointer;border:none;box-shadow:0 0 6px rgba(255,149,0,.5)}
.toggle-btn{background:rgba(255,149,0,.08);border:1px solid rgba(255,149,0,.2);color:#ff9500;padding:3px 10px;font-size:10px;font-family:'JetBrains Mono',monospace;border-radius:3px;cursor:pointer;transition:all .2s;letter-spacing:.5px}
.toggle-btn:hover{background:rgba(255,149,0,.15)}
.toggle-btn.active{background:rgba(255,149,0,.2);box-shadow:0 0 8px rgba(255,149,0,.2)}
.toggle-btn.green{background:rgba(0,230,118,.08);border-color:rgba(0,230,118,.2);color:#00e676}
.toggle-btn.green:hover{background:rgba(0,230,118,.15)}
.toggle-btn.green.active{background:rgba(0,230,118,.18);box-shadow:0 0 8px rgba(0,230,118,.2)}
.scanline{position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,rgba(90,143,199,.25),transparent);animation:scan 9s linear infinite;pointer-events:none;z-index:2}
@keyframes scan{0%{top:0;opacity:0}10%{opacity:1}90%{opacity:1}100%{top:100%;opacity:0}}
@keyframes fadeInDown{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeInUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
</style>
</head>
<body>
<div class="container">
  <div class="header">
    <div class="header-left">
      <h1>柔性分段底盘 · 被动贴合越障</h1>
      <p class="subtitle">Flexible Segmented Chassis · Passive Conforming</p>
    </div>
    <div class="header-right">
      <div class="ifr-tag">TRIZ · IFR</div>
      <div>底盘随形变形 / 货物稳如磐石</div>
      <div>被动贴合 · 零功耗稳货</div>
    </div>
  </div>
  <div class="svg-wrapper">
    <div class="scanline"></div>
    <svg id="main-svg" viewBox="0 0 1200 700" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="3" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <filter id="glowS" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="6" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
        <linearGradient id="trackGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#5a8fc7" stop-opacity=".85"/>
          <stop offset="100%" stop-color="#2a4a6b" stop-opacity=".5"/>
        </linearGradient>
        <linearGradient id="cargoGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#ff7043"/>
          <stop offset="100%" stop-color="#d84315"/>
        </linearGradient>
        <linearGradient id="terrainGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#1a2332"/>
          <stop offset="100%" stop-color="#0a0f1c"/>
        </linearGradient>
        <linearGradient id="platformGrad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#d0d5dd"/>
          <stop offset="100%" stop-color="#9aa4b8"/>
        </linearGradient>
      </defs>
      <g id="grid" opacity=".12"></g>
      <g id="terrain-group"></g>
      <g id="trail-group"></g>
      <g id="track-group"></g>
      <g id="segments-group"></g>
      <g id="hinges-group"></g>
      <g id="beam-group"></g>
      <g id="cargo-group"></g>
      <g id="annotations-group"></g>
      <g id="phase-group"></g>
    </svg>
    <div class="params">
      单段模块 <strong>180mm</strong> · 铰链折角 ≤<strong>28°</strong> · 履带宽度 <strong>160mm</strong><br>
      4段铰接 · 限位扭簧+阻尼 · 宽体履带
    </div>
  </div>
  <div class="controls">
    <div class="control-group">
      <label>速度</label>
      <input type="range" id="speedSlider" min="0.3" max="3" step="0.1" value="1.2">
      <span id="speedValue" style="color:#ff9500;min-width:28px">1.2x</span>
    </div>
    <div class="control-group">
      <button class="toggle-btn active" id="highlightBtn">高亮创新点</button>
    </div>
    <div class="control-group">
      <button class="toggle-btn green active" id="trailBtn">稳定轨迹</button>
    </div>
  </div>
</div>
<script>
(function(){
'use strict';
const NS='http://www.w3.org/2000/svg';
function el(t,a){const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);return e}
function txt(p,t,a,s){const e=el(t,a);e.textContent=s;p.appendChild(e);return e}

// 地形定义:不规则台阶 + 边缘缺口
const terrainPoints=[
  {x:-200,y:520},
  {x:300,y:520},
  {x:305,y:480},   // 台阶1 (40mm 微斜)
  {x:375,y:480},
  {x:378,y:490},   // 缺口 (10mm深)
  {x:398,y:490},
  {x:401,y:480},
  {x:500,y:480},
  {x:508,y:418},   // 台阶2 (62mm 不规则偏大)
  {x:720,y:418},
  {x:724,y:390},   // 台阶3 (28mm 不规则偏小)
  {x:1400,y:390}
];

// 弧长参数化
function buildPath(pts){
  const segs=[];let total=0;
  for(let i=0;i<pts.length-1;i++){
    const dx=pts[i+1].x-pts[i].x;
    const dy=pts[i+1].y-pts[i].y;
    const len=Math.sqrt(dx*dx+dy*dy);
    segs.push({x1:pts[i].x,y1:pts[i].y,x2:pts[i+1].x,y2:pts[i+1].y,dx,dy,len,cum:total,angle:Math.atan2(dy,dx)});
    total+=len;
  }
  return{segs,total};
}
function pointAt(path,s){
  if(s<=0){const sg=path.segs[0];const t=s/sg.len;return{x:sg.x1+sg.dx*t,y:sg.y1+sg.dy*t,angle:sg.angle}}
  if(s>=path.total){const l=path.segs[path.segs.length-1];const ex=s-path.total;return{x:l.x2+ex*Math.cos(l.angle),y:l.y2+ex*Math.sin(l.angle),angle:l.angle}}
  for(const sg of path.segs){if(s<=sg.cum+sg.len){const t=(s-sg.cum)/sg.len;return{x:sg.x1+sg.dx*t,y:sg.y1+sg.dy*t,angle:sg.angle}}}
  const l=path.segs[path.segs.length-1];return{x:l.x2,y:l.y2,angle:l.angle};
}
const terrainPath=buildPath(terrainPoints);

// 常量
const SEG_LEN=130,NUM_SEG=4,TRACK_T=24,CARGO_W=200,CARGO_H=50,PLAT_H=8,PLAT_GAP=32;

// 状态
let progress=SEG_LEN*NUM_SEG+80;
let speed=1.2,highlightMode=true,showTrail=true;
let smoothedAngle=0,cargoTrail=[],lastTime=0;

// 控件
const speedSlider=document.getElementById('speedSlider');
const speedValue=document.getElementById('speedValue');
const highlightBtn=document.getElementById('highlightBtn');
const trailBtn=document.getElementById('trailBtn');
speedSlider.addEventListener('input',e=>{speed=parseFloat(e.target.value);speedValue.textContent=speed.toFixed(1)+'x'});
highlightBtn.addEventListener('click',()=>{highlightMode=!highlightMode;highlightBtn.classList.toggle('active',highlightMode)});
trailBtn.addEventListener('click',()=>{showTrail=!showTrail;trailBtn.classList.toggle('active',showTrail)});

// 网格
function drawGrid(){
  const g=document.getElementById('grid');g.innerHTML='';
  for(let x=0;x<=1200;x+=40)g.appendChild(el('line',{x1:x,y1:0,x2:x,y2:700,stroke:x%200===0?'#4a6080':'#2a3a52','stroke-width':x%200===0?.6:.3}));
  for(let y=0;y<=700;y+=40)g.appendChild(el('line',{x1:0,y1:y,x2:1200,y2:y,stroke:y%200===0?'#4a6080':'#2a3a52','stroke-width':y%200===0?.6:.3}));
}

// 地形
function drawTerrain(){
  const g=document.getElementById('terrain-group');g.innerHTML='';
  let fp=terrainPoints.map(p=>`${p.x},${p.y}`).join(' ');
  fp+=' 1400,700 -200,700';
  g.appendChild(el('polygon',{points:fp,fill:'url(#terrainGrad)',stroke:'none'}));
  let ol='';
  for(let i=0;i<terrainPoints.length;i++)ol+=(i===0?'M':'L')+`${terrainPoints[i].x},${terrainPoints[i].y} `;
  g.appendChild(el('path',{d:ol,fill:'none',stroke:'#3a5070','stroke-width':1.5,'stroke-linejoin':'round'}));
  // 台阶高度标注
  const steps=[
    {x:302,y1:520,y2:480,label:'42mm'},
    {x:504,y1:480,y2:418,label:'62mm'},
    {x:722,y1:418,y2:390,label:'28mm'}
  ];
  for(const s of steps){
    g.appendChild(el('line',{x1:s.x-12,y1:s.y1,x2:s.x-12,y2:s.y2,stroke:'#ff9500','stroke-width':.8,opacity:.3,'stroke-dasharray':'2,2'}));
    g.appendChild(el('line',{x1:s.x-15,y1:s.y1,x2:s.x-9,y2:s.y1,stroke:'#ff9500','stroke-width':.8,opacity:.3}));
    g.appendChild(el('line',{x1:s.x-15,y1:s.y2,x2:s.x-9,y2:s.y2,stroke:'#ff9500','stroke-width':.8,opacity:.3}));
    txt(g,'text',{x:s.x-18,y:(s.y1+s.y2)/2+3,fill:'#ff9500','font-size':7,'font-family':'JetBrains Mono','text-anchor':'end',opacity:.5},s.label);
  }
  // 缺口标注
  txt(g,'text',{x:388,y:502,fill:'#607080','font-size':7,'font-family':'JetBrains Mono','text-anchor':'middle',opacity:.5},'缺口');
  // 地面纹理
  for(let x=-200;x<1400;x+=18){
    let ty=520;
    for(let i=0;i<terrainPoints.length-1;i++){
      if(x>=terrainPoints[i].x&&x<=terrainPoints[i+1].x){
        const t=(x-terrainPoints[i].x)/Math.max(.01,terrainPoints[i+1].x-terrainPoints[i].x);
        ty=terrainPoints[i].y+(terrainPoints[i+1].y-terrainPoints[i].y)*t;break;
      }
    }
    g.appendChild(el('line',{x1:x,y1:ty+1,x2:x+7,y2:ty+5,stroke:'#2a3a52','stroke-width':.5,opacity:.4}));
  }
  txt(g,'text',{x:510,y:548,fill:'#506070','font-size':9,'font-family':'JetBrains Mono','text-anchor':'middle',opacity:.45,'letter-spacing':'1px'},'不规则台阶');
}

// 底盘计算
function computeChassis(prog){
  const contacts=[];
  for(let i=0;i<=NUM_SEG;i++)contacts.push(pointAt(terrainPath,prog-i*SEG_LEN));
  const segments=[];
  for(let i=0;i<NUM_SEG;i++){
    const p1=contacts[i],p2=contacts[i+1];
    const mx=(p1.x+p2.x)/2,my=(p1.y+p2.y)/2-TRACK_T/2;
    const angle=Math.atan2(p2.y-p1.y,p2.x-p1.x);
    segments.push({x:mx,y:my,angle,p1,p2});
  }
  const hinges=[];
  for(let i=1;i<NUM_SEG;i++){
    const p=contacts[i];
    let bend=segments[i-1].angle-segments[i].angle;
    while(bend>Math.PI)bend-=2*Math.PI;
    while(bend<-Math.PI)bend+=2*Math.PI;
    const bd=Math.abs(bend*180/Math.PI);
    hinges.push({x:p.x,y:p.y-TRACK_T/2,bendDeg:bd,active:bd>2});
  }
  const platformX=(segments[1].x+segments[2].x)/2;
  const platformY=(segments[1].y+segments[2].y)/2-PLAT_GAP;
  let avgAngle=0;
  for(const s of segments)avgAngle+=s.angle;
  avgAngle/=segments.length;
  const targetAngle=avgAngle*.12;
  smoothedAngle+=(targetAngle-smoothedAngle)*.08;
  return{contacts,segments,hinges,platformX,platformY,platformAngle:smoothedAngle};
}

// 履带
function drawTrack(contacts){
  const g=document.getElementById('track-group');g.innerHTML='';
  const bot=contacts.map(c=>`${c.x},${c.y}`);
  const top=contacts.slice().reverse().map(c=>`${c.x},${c.y-TRACK_T}`);
  g.appendChild(el('polygon',{points:[...bot,...top].join(' '),fill:'url(#trackGrad)',stroke:'#4a7aad','stroke-width':1,'stroke-linejoin':'round',opacity:.75}));
  // 履带齿
  for(let i=0;i<contacts.length-1;i++){
    const c1=contacts[i],c2=contacts[i+1];
    const len=Math.hypot(c2.x-c1.x,c2.y-c1.y);
    const n=Math.max(2,Math.floor(len/16));
    for(let j=0;j<n;j++){
      const t=(j+.5)/n;
      const x=c1.x+(c2.x-c1.x)*t,y=c1.y+(c2.y-c1.y)*t;
      const a=Math.atan2(c2.y-c1.y,c2.x-c1.x);
      const px=Math.sin(a)*4,py=-Math.cos(a)*4;
      g.appendChild(el('line',{x1:x-px,y1:y-py,x2:x+px,y2:y+py,stroke:'rgba(120,170,220,.4)','stroke-width':1}));
    }
  }
}

// 分段模块
function drawSegments(segments){
  const g=document.getElementById('segments-group');g.innerHTML='';
  const colors=[
    {fill:'rgba(50,85,120,.4)',stroke:'#3a6a9a'},
    {fill:'rgba(40,75,110,.4)',stroke:'#2a5a8a'},
    {fill:'rgba(50,85,120,.4)',stroke:'#3a6a9a'},
    {fill:'rgba(40,75,110,.4)',stroke:'#2a5a8a'}
  ];
  for(let i=0;i<segments.length;i++){
    const s=segments[i];
    const c1=s.p1,c2=s.p2;
    const a=Math.atan2(c2.y-c1.y,c2.x-c1.x);
    const cosA=Math.cos(a),sinA=Math.sin(a);
    const insS=i>0?3:0,insE=i<NUM_SEG-1?3:0;
    const x1=c1.x+cosA*insS,y1=c1.y+sinA*insS;
    const x2=c2.x-cosA*insE,y2=c2.y-sinA*insE;
    const pts=`${x1},${y1} ${x2},${y2} ${x2},${y2-TRACK_T} ${x1},${y1-TRACK_T}`;
    g.appendChild(el('polygon',{points:pts,fill:colors[i].fill,stroke:colors[i].stroke,'stroke-width':.8,'stroke-linejoin':'round'}));
    const cx=(x1+x2)/2,cy=(y1+y2)/2-TRACK_T/2;
    txt(g,'text',{x:cx,y:cy+2,'text-anchor':'middle',fill:'#6a8ab0','font-size':7,'font-family':'JetBrains Mono','font-weight':500,opacity:.55},`M${i+1}`);
  }
}

// 铰链
function drawHinges(hinges){
  const g=document.getElementById('hinges-group');g.innerHTML='';
  for(const h of hinges){
    const intensity=Math.min(h.bendDeg/28,1);
    const active=h.active&&highlightMode;
    if(active){
      g.appendChild(el('circle',{cx:h.x,cy:h.y,r:8+intensity*8,fill:`rgba(255,149,0,${.06+intensity*.1})`}));
      g.appendChild(el('circle',{cx:h.x,cy:h.y,r:6+intensity*4,fill:`rgba(255,149,0,${.1+intensity*.1})`}));
      // 扭簧锯齿
      if(intensity>.1){
        const r=6,n=5;
        let d=`M ${h.x-r} ${h.y}`;
        for(let z=0;z<n;z++){
          const zx=h.x-r+(r*2*(z+.5)/n);
          const zy=h.y+(z%2===0?-2:2);
          d+=` L ${zx} ${zy}`;
        }
        d+=` L ${h.x+r} ${h.y}`;
        g.appendChild(el('path',{d,fill:'none',stroke:`rgba(255,180,50,${.3+intensity*.4})`,'stroke-width':1,'stroke-linecap':'round'}));
      }
    }
    g.appendChild(el('circle',{cx:h.x,cy:h.y,r:4,fill:active?'#ff9500':'#4a6a8a',stroke:active?'#ffcc66':'#6a8aaa','stroke-width':1.2}));
    g.appendChild(el('circle',{cx:h.x,cy:h.y,r:1.5,fill:active?'#fff5e0':'#8090a0'}));
    if(highlightMode&&h.bendDeg>1.5){
      txt(g,'text',{x:h.x,y:h.y-14,'text-anchor':'middle',fill:'#ff9500','font-size':8,'font-family':'JetBrains Mono','font-weight':700,opacity:.8+intensity*.2},`${h.bendDeg.toFixed(0)}°`);
    }
  }
}

// 主梁
function drawBeam(segments){
  const g=document.getElementById('beam-group');g.innerHTML='';
  const s1=segments[1],s2=segments[2];
  const by1=s1.y-TRACK_T*.2,by2=s2.y-TRACK_T*.2;
  g.appendChild(el('line',{x1:s1.x-SEG_LEN*.3,y1:by1,x2:s2.x+SEG_LEN*.3,y2:by2,stroke:'#7a8fa8','stroke-width':4,'stroke-linecap':'round',opacity:.7}));
  g.appendChild(el('line',{x1:s1.x-SEG_LEN*.3,y1:by1-1,x2:s2.x+SEG_LEN*.3,y2:by2-1,stroke:'#a0b5ce','stroke-width':1,'stroke-linecap':'round',opacity:.5}));
}

// 货物
function drawCargo(px,py,angle){
  const g=document.getElementById('cargo-group');g.innerHTML='';
  const rg=el('g',{transform:`translate(${px},${py}) rotate(${angle*180/Math.PI})`});
  const strutW=CARGO_W*.32;
  const sTop=-PLAT_H/2,sBot=PLAT_GAP-TRACK_T*.2;
  for(const sx of[-strutW,strutW]){
    rg.appendChild(el('line',{x1:sx,y1:sTop,x2:sx,y2:sBot,stroke:'#5a7a9a','stroke-width':2.5,'stroke-linecap':'round'}));
    rg.appendChild(el('circle',{cx:sx,cy:sBot,r:2,fill:'#6a8aaa'}));
  }
  rg.appendChild(el('rect',{x:-CARGO_W/2,y:-PLAT_H/2,width:CARGO_W,height:PLAT_H,rx:2,fill:'url(#platformGrad)',stroke:'#8a94a8','stroke-width':1}));
  const cy=-PLAT_H/2-CARGO_H;
  rg.appendChild(el('rect',{x:-CARGO_W*.35,y:cy,width:CARGO_W*.7,height:CARGO_H,rx:2,fill:'url(#cargoGrad)',stroke:'#bf3a0f','stroke-width':1}));
  rg.appendChild(el('rect',{x:-CARGO_W*.35,y:cy+CARGO_H*.35,width:CARGO_W*.7,height:1.5,fill:'rgba(0,0,0,.25)'}));
  txt(rg,'text',{x:0,y:cy+CARGO_H*.55+3,'text-anchor':'middle',fill:'#fff','font-size':10,'font-family':'JetBrains Mono','font-weight':700,'letter-spacing':'1px'},'货物');
  // 稳定指示徽章
  const bx=CARGO_W*.35+4,by=cy+4;
  rg.appendChild(el('rect',{x:bx,y:by,width:10,height:10,rx:5,fill:'rgba(0,230,118,.15)',stroke:'#00e676','stroke-width':.8}));
  rg.appendChild(el('circle',{cx:bx+5,cy:by+5,r:2,fill:'#00e676',filter:'url(#glow)'}));
  g.appendChild(rg);
}

// 货物轨迹
function drawTrail(){
  const g=document.getElementById('trail-group');g.innerHTML='';
  if(!showTrail||cargoTrail.length<2)return;
  let d=`M${cargoTrail[0].x},${cargoTrail[0].y}`;
  for(let i=1;i<cargoTrail.length;i++)d+=` L${cargoTrail[i].x},${cargoTrail[i].y}`;
  g.appendChild(el('path',{d,fill:'none',stroke:'#00e676','stroke-width':1.5,opacity:.22,'stroke-linecap':'round','stroke-dasharray':'4,3'}));
  const last=cargoTrail[cargoTrail.length-1];
  g.appendChild(el('circle',{cx:last.x,cy:last.y,r:4,fill:'rgba(0,230,118,.15)'}));
  g.appendChild(el('circle',{cx:last.x,cy:last.y,r:2,fill:'#00e676',filter:'url(#glow)'}));
  if(last.x<1150){
    txt(g,'text',{x:last.x+8,y:last.y+3,fill:'#00e676','font-size':8,'font-family':'JetBrains Mono',opacity:.5},'货物轨迹');
  }
}

// 标注
function drawAnnotations(chassis){
  const g=document.getElementById('annotations-group');g.innerHTML='';
  const activeHinges=chassis.hinges.filter(h=>h.bendDeg>5);
  if(activeHinges.length>0){
    const h=activeHinges.sort((a,b)=>b.x-a.x)[0];
    let label='';
    if(h.bendDeg>18)label='铰链折转 · 履带牵引上翻';
    else if(h.bendDeg>8)label='多段连续变形';
    else label='前段触阶';
    txt(g,'text',{x:h.x,y:h.y-28,'text-anchor':'middle',fill:'#ff9500','font-size':9,'font-family':'JetBrains Mono','font-weight':700,opacity:.85},label);
    g.appendChild(el('line',{x1:h.x,y1:h.y-22,x2:h.x,y2:h.y-10,stroke:'#ff9500','stroke-width':.8,opacity:.4,'stroke-dasharray':'2,2'}));
  }
  const allRelaxed=chassis.hinges.every(h=>h.bendDeg<3);
  if(allRelaxed){
    const head=chassis.contacts[0],tail=chassis.contacts[NUM_SEG];
    const onFlat=(tail.y>510&&head.y>510)||(tail.y<395&&head.y<395);
    if(onFlat){
      txt(g,'text',{x:chassis.platformX,y:chassis.platformY-62,'text-anchor':'middle',fill:'#00e676','font-size':9,'font-family':'JetBrains Mono','font-weight':700,opacity:.55},'扭簧回正 · 货物平稳');
    }
  }
  const fc=chassis.contacts[0];
  if(fc.x>20&&fc.x<1180){
    txt(g,'text',{x:fc.x-55,y:fc.y+18,fill:'#4a6a8a','font-size':7,'font-family':'JetBrains Mono',opacity:.45},'宽体履带');
  }
  txt(g,'text',{x:chassis.platformX+CARGO_W*.35+18,y:chassis.platformY-3,fill:'#607080','font-size':7,'font-family':'JetBrains Mono',opacity:.45},'载货平台');
}

// 阶段指示
function drawPhase(chassis){
  const g=document.getElementById('phase-group');g.innerHTML='';
  const head=chassis.contacts[0],tail=chassis.contacts[NUM_SEG];
  const anyActive=chassis.hinges.some(h=>h.bendDeg>5);
  const allTop=chassis.contacts.every(c=>c.y<395);
  const allGround=chassis.contacts.every(c=>c.y>510);
  let phase='',color='#5a7a9a';
  if(allGround){phase='地面行驶';color='#5a8fc7'}
  else if(allTop){phase='平稳行驶 · 扭簧回正';color='#00e676'}
  else if(anyActive){
    const maxBend=Math.max(...chassis.hinges.map(h=>h.bendDeg));
    if(maxBend>18){phase='铰链折转 · 履带牵引';color='#ff9500'}
    else{phase='多段连续变形';color='#ffcc66'}
  }else{phase='过渡调整';color='#5a8fc7'}
  const px=1055,py=35,w=150;
  g.appendChild(el('rect',{x:px-w/2,y:py-12,width:w,height:22,rx:11,fill:'rgba(15,22,38,.85)',stroke:color,'stroke-width':1,opacity:.9}));
  g.appendChild(el('circle',{cx:px-w/2+14,cy:py,r:3,fill:color,filter:'url(#glow)'}));
  txt(g,'text',{x:px-w/2+24,y:py+3,fill:color,'font-size':9,'font-family':'JetBrains Mono','font-weight':500},phase);
}

// 动画循环
function animate(ts){
  if(!lastTime)lastTime=ts;
  const dt=Math.min((ts-lastTime)/16.67,3);
  lastTime=ts;
  progress+=speed*dt*1.5;
  const startP=SEG_LEN*NUM_SEG+80;
  const endP=terrainPath.total+80;
  if(progress>endP){
    progress=startP;
    cargoTrail=[];smoothedAngle=0;
  }
  const chassis=computeChassis(progress);
  drawTrack(chassis.contacts);
  drawSegments(chassis.segments);
  drawHinges(chassis.hinges);
  drawBeam(chassis.segments);
  drawCargo(chassis.platformX,chassis.platformY,chassis.platformAngle);
  drawAnnotations(chassis);
  drawPhase(chassis);
  if(showTrail){
    cargoTrail.push({x:chassis.platformX,y:chassis.platformY-PLAT_H/2-CARGO_H/2});
    if(cargoTrail.length>300)cargoTrail.shift();
  }
  drawTrail();
  requestAnimationFrame(animate);
}

// 初始化
drawGrid();
drawTerrain();
requestAnimationFrame(animate);
})();
</script>
</body>
</html>

这个动画展示了柔性分段底盘被动贴合越障的最终理想解(IFR)工作原理:

核心视觉设计

  • 深色工程蓝图风格背景,配合 Big Shoulders Display 工业字体与 JetBrains Mono 技术字体
  • 颜色语义化:蓝色=结构(履带/模块),琥珀色=创新点(铰链/扭簧),绿色=理想结果(货物稳定轨迹),橙红色=被保护对象(货物)

IFR 体现

  1. 理想状态直展示:画面始终展示底盘已变形贴合台阶、货物平稳的最终理想状态,不做前后对比
  2. 资源利用:4段铰接模块(已有结构)+ 限位扭簧阻尼(极小增量)即消除颠簸矛盾,铰链处以扭簧锯齿纹可视化
  3. 视觉引导:铰链弯曲时琥珀色辉光脉冲 + 实时折角角度标注;绿色虚线轨迹追踪货物中心路径——台阶是锐折的,货物轨迹却近乎平滑上升,矛盾消除一目了然
  4. 交互控制:速度滑块(0.3x–3x)、高亮创新点开关、稳定轨迹开关
  5. 不规则场景:三级台阶高度分别为 42mm/62mm/28mm(尺寸波动大),台阶表面还有一处 10mm 缺口,展示"边缘有缺口"场景下的被动贴合能力
  6. 自动播放:页面加载即自动启动循环动画,铰链回正后回到起点重新开始
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柔性分段底盘 · 被动贴合爬阶原理</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600;700&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden}
body{background:#05080d;font-family:'IBM Plex Mono',monospace;display:flex;align-items:center;justify-content:center}
.stage{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}
.stage svg{width:100%;height:100%;display:block}
.stage::after{content:'';position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 3px,rgba(120,160,200,.012) 3px,rgba(120,160,200,.012) 4px);pointer-events:none;z-index:5}
.ttl{position:absolute;top:18px;left:26px;z-index:10;pointer-events:none}
.ttl h1{font-family:'Chakra Petch',sans-serif;font-size:15px;font-weight:700;letter-spacing:.1em;color:#bcc8d8;text-transform:uppercase;line-height:1.2}
.ttl .sub{font-size:9px;color:#3e4e62;margin-top:4px;letter-spacing:.08em}
.ttl .acc{color:#f59e0b}
.lg{position:absolute;top:18px;right:26px;z-index:10;display:flex;flex-direction:column;gap:5px;pointer-events:none}
.lg-i{display:flex;align-items:center;gap:7px;font-size:9px;color:#4e5e72;letter-spacing:.05em}
.lg-d{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.ph{position:absolute;bottom:64px;left:50%;transform:translateX(-50%);z-index:10;font-size:9px;color:#22d3ee;letter-spacing:.18em;background:rgba(6,10,16,.75);padding:4px 14px;border-radius:3px;border:1px solid rgba(34,211,238,.18);pointer-events:none;white-space:nowrap}
.ctl{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);z-index:10;display:flex;align-items:center;gap:14px;background:rgba(8,12,20,.88);border:1px solid rgba(40,56,80,.35);border-radius:6px;padding:7px 16px;backdrop-filter:blur(6px)}
.ctl label{font-size:8px;color:#3e4e62;letter-spacing:.1em;text-transform:uppercase}
.ctl input[type=range]{width:90px;height:3px;-webkit-appearance:none;appearance:none;background:#15202e;border-radius:2px;outline:none}
.ctl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;border-radius:50%;background:#f59e0b;cursor:pointer;border:2px solid #080c14}
.ctl input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:#f59e0b;cursor:pointer;border:2px solid #080c14}
.ctl .v{font-size:10px;color:#bcc8d8;min-width:30px;font-weight:500}
.ctl .dv{width:1px;height:14px;background:rgba(40,56,80,.4)}
</style>
</head>
<body>
<div class="stage">
  <div class="ttl">
    <h1>柔性分段底盘 · 被动贴合爬阶</h1>
    <div class="sub">IFR 最终理想解 <span class="acc">—</span> 底盘随形 · 货物稳平</div>
  </div>
  <div class="lg">
    <div class="lg-i"><span class="lg-d" style="background:#f59e0b"></span>铰链折转</div>
    <div class="lg-i"><span class="lg-d" style="background:#22d3ee"></span>平台轨迹</div>
    <div class="lg-i"><span class="lg-d" style="background:#3a4a5e"></span>履带模块</div>
  </div>
  <div class="ph" id="phaseTag">平地行驶</div>
  <svg id="scene" viewBox="0 0 1500 600" preserveAspectRatio="xMidYMid meet">
    <defs>
      <radialGradient id="bgG" cx="50%" cy="55%" r="75%"><stop offset="0%" stop-color="#0b1119"/><stop offset="100%" stop-color="#04070c"/></radialGradient>
      <pattern id="grd" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(44,64,92,.08)" stroke-width=".5"/></pattern>
      <pattern id="htc" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#141e2c" stroke-width="1"/></pattern>
      <filter id="glw" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
      <filter id="sglw" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
      <linearGradient id="cgG" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#fbbf24"/><stop offset="100%" stop-color="#b45309"/></linearGradient>
      <linearGradient id="btG" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#222e3e"/><stop offset="50%" stop-color="#1a2230"/><stop offset="100%" stop-color="#222e3e"/></linearGradient>
    </defs>
    <rect width="1500" height="600" fill="url(#bgG)"/>
    <rect width="1500" height="600" fill="url(#grd)"/>
    <path id="trnF" fill="#0c1320"/>
    <path id="trnH" fill="url(#htc)" opacity=".45"/>
    <path id="trnE" fill="none" stroke="#2a3e54" stroke-width="1.5"/>
    <g id="stepAnno"></g>
    <path id="trail" fill="none" stroke="#22d3ee" stroke-width="1.5" opacity=".3" stroke-dasharray="3 3" stroke-linecap="round"/>
    <g id="vehicle" opacity="1">
      <path id="belt" fill="url(#btG)" stroke="#3a4a5e" stroke-width="1" stroke-linejoin="round"/>
      <g id="segG"></g>
      <g id="whlG"></g>
      <g id="hngG"></g>
      <g id="arcG"></g>
      <g id="pltG"></g>
      <g id="cgoG"></g>
      <g id="annoG"></g>
    </g>
  </svg>
  <div class="ctl">
    <label>速度</label>
    <input type="range" id="spd" min=".25" max="2" step=".25" value="1"/>
    <span class="v" id="spdV">1.0×</span>
    <div class="dv"></div>
    <label>铰链限位</label>
    <input type="range" id="ang" min="15" max="35" step="1" value="28"/>
    <span class="v" id="angV">28°</span>
  </div>
</div>
<script>
(function(){
'use strict';
const NS='http://www.w3.org/2000/svg';
const VB_W=1500,VB_H=600;
const GY=460,R=18,L=110,NW=5;
const STEPS=[{x:420,h:42},{x:560,h:35},{x:700,h:48}];
const SM=7,SX=40,EX=1000;
const PW=140,PH=32,PO=42,CW=72,CH=42;
const CYC=12000;
let spd=1,mAD=28,t0=null,trail=[],MT=180;
const el={};

function ss(x){x=Math.max(0,Math.min(1,x));return x*x*(3-2*x)}
function tY(x){let y=GY;for(const s of STEPS){if(x>s.x+SM)y-=s.h;else if(x>s.x-SM){const t=ss((x-(s.x-SM))/(2*SM));y-=s.h*t}}return y}

function computeWheels(rx,maxAR){
  const w=new Array(NW),sa=new Array(NW-1);
  w[NW-1]={x:rx,y:tY(rx)-R};
  for(let i=NW-2;i>=0;i--){
    const p=w[i+1];
    let x=p.x+L;
    for(let it=0;it<15;it++){
      const y=tY(x)-R,dx=x-p.x,dy=y-p.y,d=Math.hypot(dx,dy);
      if(Math.abs(d-L)<.05)break;
      const dy2=dy*dy;
      if(dy2>=L*L*.9999){x=p.x+.5;break}
      x=p.x+Math.sqrt(L*L-dy2);
    }
    let y=tY(x)-R;
    let segA=Math.atan2(p.y-y,x-p.x);
    if(i<NW-2){
      const psa=sa[i+1],ha=segA-psa;
      if(Math.abs(ha)>maxAR){segA=psa+Math.sign(ha)*maxAR;x=p.x+L*Math.cos(segA);y=p.y-L*Math.sin(segA)}
    }
    sa[i]=segA;
    w[i]={x,y};
  }
  return w;
}

function computeAngles(w){
  const a=[];
  for(let i=0;i<NW-1;i++)a.push(Math.atan2(w[i+1].y-w[i].y,w[i].x-w[i+1].x));
  return a;
}
function computeHinges(a){const h=[];for(let i=0;i<a.length-1;i++)h.push(a[i]-a[i+1]);return h}

function computeBelt(w){
  const o=[w[4],w[3],w[2],w[1],w[0]],n=o.length,up=[],lo=[];
  for(let i=0;i<n-1;i++){
    const dx=o[i+1].x-o[i].x,dy=o[i+1].y-o[i].y,ln=Math.hypot(dx,dy);
    if(ln<.01){up.push({x:0,y:-R});lo.push({x:0,y:R});continue}
    const nx=dy/ln,ny=-dx/ln;
    up.push({x:R*nx,y:R*ny});lo.push({x:-R*nx,y:-R*ny});
  }
  let p='';const s={x:o[0].x+up[0].x,y:o[0].y+up[0].y};
  p=`M ${s.x.toFixed(1)} ${s.y.toFixed(1)}`;
  for(let i=1;i<n;i++){
    const ar={x:o[i].x+up[i-1].x,y:o[i].y+up[i-1].y};
    p+=` L ${ar.x.toFixed(1)} ${ar.y.toFixed(1)}`;
    if(i<n-1){const dp={x:o[i].x+up[i].x,y:o[i].y+up[i].y};p+=` A ${R} ${R} 0 0 1 ${dp.x.toFixed(1)} ${dp.y.toFixed(1)}`}
  }
  const fU={x:o[n-1].x+up[n-2].x,y:o[n-1].y+up[n-2].y};
  const fL={x:o[n-1].x+lo[n-2].x,y:o[n-1].y+lo[n-2].y};
  p+=` A ${R} ${R} 0 0 1 ${fL.x.toFixed(1)} ${fL.y.toFixed(1)}`;
  for(let i=n-2;i>=0;i--){
    const ar={x:o[i].x+lo[i].x,y:o[i].y+lo[i].y};
    p+=` L ${ar.x.toFixed(1)} ${ar.y.toFixed(1)}`;
    if(i>0){const dp={x:o[i].x+lo[i-1].x,y:o[i].y+lo[i-1].y};p+=` A ${R} ${R} 0 0 1 ${dp.x.toFixed(1)} ${dp.y.toFixed(1)}`}
  }
  p+=` A ${R} ${R} 0 0 1 ${s.x.toFixed(1)} ${s.y.toFixed(1)} Z`;
  return p;
}

function computePlatform(w,a){
  const mx=(w[1].x+w[2].x)/2,my=(w[1].y+w[2].y)/2,ang=a[1];
  return{x:mx+PO*(-Math.sin(ang)),y:my+PO*(-Math.cos(ang)),angle:ang};
}

function ce(t,a){const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);return e}

function initElems(){
  const sg=document.getElementById('segG');
  el.segs=[];
  for(let i=0;i<NW-1;i++){
    const r=ce('rect',{rx:3,ry:3,fill:'#1e2838',stroke:'#36465c','stroke-width':1});
    sg.appendChild(r);el.segs.push(r);
    // Rivets
    const rv1=ce('circle',{r:2,fill:'#4a5a6e'});
    const rv2=ce('circle',{r:2,fill:'#4a5a6e'});
    sg.appendChild(rv1);sg.appendChild(rv2);
    el.segs.push({rv1,rv2});
  }
  const wg=document.getElementById('whlG');
  el.whls=[];el.hubs=[];
  for(let i=0;i<NW;i++){
    const c=ce('circle',{r:R-2,fill:'#0e1420',stroke:'#36465c','stroke-width':1.5});
    wg.appendChild(c);el.whls.push(c);
    const h=ce('circle',{r:3,fill:'#4a5a6e'});
    wg.appendChild(h);el.hubs.push(h);
  }
  const hg=document.getElementById('hngG');
  el.hgl=[];el.hpn=[];
  for(let i=0;i<3;i++){
    const gl=ce('circle',{r:R+7,fill:'none',stroke:'#f59e0b','stroke-width':1.5,opacity:0,filter:'url(#glw)'});
    hg.appendChild(gl);el.hgl.push(gl);
    const pn=ce('circle',{r:5,fill:'#36465c',stroke:'#4a5a6e','stroke-width':1});
    hg.appendChild(pn);el.hpn.push(pn);
  }
  const ag=document.getElementById('arcG');
  el.arcs=[];el.albl=[];
  for(let i=0;i<3;i++){
    const ar=ce('path',{fill:'none',stroke:'#f59e0b','stroke-width':1.5,opacity:0});
    ag.appendChild(ar);el.arcs.push(ar);
    const lb=ce('text',{'font-family':'IBM Plex Mono,monospace','font-size':8,fill:'#f59e0b','text-anchor':'middle',opacity:0});
    ag.appendChild(lb);el.albl.push(lb);
  }
  const pg=document.getElementById('pltG');
  el.st1=ce('line',{stroke:'#36465c','stroke-width':2,opacity:.35});
  el.st2=ce('line',{stroke:'#36465c','stroke-width':2,opacity:.35});
  pg.appendChild(el.st1);pg.appendChild(el.st2);
  el.plt=ce('rect',{rx:5,ry:5,fill:'#141c2a',stroke:'#3a5a7a','stroke-width':1.5});
  pg.appendChild(el.plt);
  el.lvl=ce('line',{stroke:'#22d3ee','stroke-width':.8,'stroke-dasharray':'3 2',opacity:.35});
  pg.appendChild(el.lvl);
  const cg=document.getElementById('cgoG');
  el.cgo=ce('rect',{rx:2,ry:2,fill:'url(#cgG)',stroke:'#fbbf24','stroke-width':1,filter:'url(#sglw)'});
  cg.appendChild(el.cgo);
  el.cst=ce('line',{stroke:'#7c2d12','stroke-width':1,opacity:.5});
  cg.appendChild(el.cst);
  el.clb=ce('text',{'font-family':'IBM Plex Mono,monospace','font-size':8,'font-weight':600,fill:'#1a0a02','text-anchor':'middle'});
  el.clb.textContent='货物';
  cg.appendChild(el.clb);
  // Static annotations
  const sa=document.getElementById('stepAnno');
  STEPS.forEach((s,i)=>{
    const nx=i<STEPS.length-1?STEPS[i+1].x:VB_W;
    const dim=ce('line',{x1:s.x-14,y1:GY,x2:s.x-14,y2:GY-s.h,stroke:'#243448','stroke-width':.5,'stroke-dasharray':'2 2'});
    sa.appendChild(dim);
    ce('line',{x1:s.x-17,y1:GY,x2:s.x-11,y2:GY,stroke:'#243448','stroke-width':.5});
    const a2=ce('line',{x1:s.x-17,y1:GY-s.h,x2:s.x-11,y2:GY-s.h,stroke:'#243448','stroke-width':.5});
    sa.appendChild(a2);
    const t=ce('text',{x:s.x-20,y:GY-s.h/2+3,'font-family':'IBM Plex Mono,monospace','font-size':7,fill:'#2e3e52','text-anchor':'end'});
    t.textContent='Δ'+s.h;sa.appendChild(t);
  });
  el.sll=ce('text',{'font-family':'IBM Plex Mono,monospace','font-size':7,fill:'#2e3e52','text-anchor':'middle'});
  el.sll.textContent='L=180mm';
  document.getElementById('annoG').appendChild(el.sll);
}

function renderTerrain(){
  let f=`M 0 ${VB_H} L 0 ${GY}`,e=`M 0 ${GY}`;
  for(let x=0;x<=VB_W;x+=2){const y=tY(x);f+=` L ${x} ${y.toFixed(1)}`;e+=` L ${x} ${y.toFixed(1)}`}
  f+=` L ${VB_W} ${VB_H} Z`;
  document.getElementById('trnF').setAttribute('d',f);
  document.getElementById('trnH').setAttribute('d',f);
  document.getElementById('trnE').setAttribute('d',e);
}

function updateVehicle(w,a,h){
  document.getElementById('belt').setAttribute('d',computeBelt(w));
  for(let i=0;i<NW-1;i++){
    const w1=w[i],w2=w[i+1];
    const mx=(w1.x+w2.x)/2,my=(w1.y+w2.y)/2;
    const ln=Math.hypot(w1.x-w2.x,w1.y-w2.y);
    const ad=(-a[i]*180/Math.PI).toFixed(1);
    const segEl=el.segs[i*2];
    segEl.setAttribute('x',(-ln/2).toFixed(1));
    segEl.setAttribute('y',-8.5);
    segEl.setAttribute('width',ln.toFixed(1));
    segEl.setAttribute('height',17);
    segEl.setAttribute('transform',`translate(${mx.toFixed(1)},${my.toFixed(1)}) rotate(${ad})`);
    // Rivets
    const rv=el.segs[i*2+1];
    const ang=a[i],cs=Math.cos(ang),sn=Math.sin(ang);
    const r1x=w1.x-sn*0, r1y=w1.y-cs*0; // at wheel centers, handled by wheel rendering
    rv.rv1.setAttribute('cx',(mx-ln/3*cs).toFixed(1));
    rv.rv1.setAttribute('cy',(my+ln/3*sn).toFixed(1));
    rv.rv2.setAttribute('cx',(mx+ln/3*cs).toFixed(1));
    rv.rv2.setAttribute('cy',(my-ln/3*sn).toFixed(1));
  }
  for(let i=0;i<NW;i++){
    el.whls[i].setAttribute('cx',w[i].x.toFixed(1));
    el.whls[i].setAttribute('cy',w[i].y.toFixed(1));
    el.hubs[i].setAttribute('cx',w[i].x.toFixed(1));
    el.hubs[i].setAttribute('cy',w[i].y.toFixed(1));
  }
  const mAR=mAD*Math.PI/180;
  const now=Date.now();
  for(let i=0;i<3;i++){
    const wi=w[i+1],ha=Math.abs(h[i]),hd=ha*180/Math.PI;
    const act=hd>2,itn=Math.min(1,hd/28),ov=hd>mAD;
    const pulse=1+.08*Math.sin(now/180+i);
    el.hgl[i].setAttribute('cx',wi.x.toFixed(1));
    el.hgl[i].setAttribute('cy',wi.y.toFixed(1));
    el.hgl[i].setAttribute('r',((R+7)*pulse).toFixed(1));
    el.hgl[i].setAttribute('opacity',act?itn*.45:0);
    el.hgl[i].setAttribute('stroke',ov?'#ef4444':'#f59e0b');
    el.hpn[i].setAttribute('cx',wi.x.toFixed(1));
    el.hpn[i].setAttribute('cy',wi.y.toFixed(1));
    el.hpn[i].setAttribute('fill',act?(ov?'#ef4444':'#f59e0b'):'#36465c');
    el.hpn[i].setAttribute('stroke',act?(ov?'#f87171':'#fbbf24'):'#4a5a6e');
    if(act){
      const sa1=a[i],sa2=a[i+1],aR=26;
      const x1=wi.x+aR*Math.cos(sa2),y1=wi.y-aR*Math.sin(sa2);
      const x2=wi.x+aR*Math.cos(sa1),y2=wi.y-aR*Math.sin(sa1);
      const sw=h[i]>0?1:0;
      el.arcs[i].setAttribute('d',`M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${aR} ${aR} 0 0 ${sw} ${x2.toFixed(1)} ${y2.toFixed(1)}`);
      el.arcs[i].setAttribute('opacity',itn*.65);
      el.arcs[i].setAttribute('stroke',ov?'#ef4444':'#f59e0b');
      const ma=(sa1+sa2)/2,lr=aR+10;
      el.albl[i].setAttribute('x',(wi.x+lr*Math.cos(ma)).toFixed(1));
      el.albl[i].setAttribute('y',(wi.y-lr*Math.sin(ma)+3).toFixed(1));
      el.albl[i].textContent=hd.toFixed(1)+'°';
      el.albl[i].setAttribute('opacity',itn*.8);
      el.albl[i].setAttribute('fill',ov?'#ef4444':'#f59e0b');
    }else{
      el.arcs[i].setAttribute('opacity',0);
      el.albl[i].setAttribute('opacity',0);
    }
  }
  const plat=computePlatform(w,a);
  const ad=(-plat.angle*180/Math.PI).toFixed(1);
  el.plt.setAttribute('x',-PW/2);
  el.plt.setAttribute('y',-PH/2);
  el.plt.setAttribute('width',PW);
  el.plt.setAttribute('height',PH);
  el.plt.setAttribute('transform',`translate(${plat.x.toFixed(1)},${plat.y.toFixed(1)}) rotate(${ad})`);
  el.st1.setAttribute('x1',plat.x.toFixed(1));el.st1.setAttribute('y1',plat.y.toFixed(1));
  el.st1.setAttribute('x2',w[1].x.toFixed(1));el.st1.setAttribute('y2',w[1].y.toFixed(1));
  el.st2.setAttribute('x1',plat.x.toFixed(1));el.st2.setAttribute('y1',plat.y.toFixed(1));
  el.st2.setAttribute('x2',w[2].x.toFixed(1));el.st2.setAttribute('y2',w[2].y.toFixed(1));
  el.lvl.setAttribute('x1',(plat.x-PW/2+8).toFixed(1));el.lvl.setAttribute('y1',plat.y.toFixed(1));
  el.lvl.setAttribute('x2',(plat.x+PW/2-8).toFixed(1));el.lvl.setAttribute('y2',plat.y.toFixed(1));
  const cx=plat.x,cy=plat.y-PH/2-CH/2-2;
  el.cgo.setAttribute('x',-CW/2);el.cgo.setAttribute('y',-CH/2);
  el.cgo.setAttribute('width',CW);el.cgo.setAttribute('height',CH);
  el.cgo.setAttribute('transform',`translate(${cx.toFixed(1)},${cy.toFixed(1)}) rotate(${ad})`);
  el.cst.setAttribute('x1',(-CW/2+6).toFixed(1));el.cst.setAttribute('y1',0);
  el.cst.setAttribute('x2',(CW/2-6).toFixed(1));el.cst.setAttribute('y2',0);
  el.cst.setAttribute('transform',`translate(${cx.toFixed(1)},${cy.toFixed(1)}) rotate(${ad})`);
  el.clb.setAttribute('x',0);el.clb.setAttribute('y',3.5);
  el.clb.setAttribute('transform',`translate(${cx.toFixed(1)},${cy.toFixed(1)}) rotate(${ad})`);
  // Segment length label
  const s2m={x:(w[2].x+w[3].x)/2,y:(w[2].y+w[3].y)/2};
  const s2a=a[2],od={x:-Math.sin(s2a),y:-Math.cos(s2a)};
  el.sll.setAttribute('x',(s2m.x+22*od.x).toFixed(1));
  el.sll.setAttribute('y',(s2m.y+22*od.y+2).toFixed(1));
  return plat;
积分规则:第一轮对话扣减8分,后续每轮扣6分