分享图
A
动画渲染工坊
就绪
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V型渐缩滑道被动定向落料原理</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{--bg:#060a12;--fg:#c8d4e0;--muted:#4a5568;--accent:#00e8a0;--accent2:#00b8ff;--shaft:#e89030;--shaft-hi:#f0c858;--metal:#1e2d3d;--metal-lt:#2a3d50;--metal-edge:#3a5068;--danger:#ff4060;--card:#0c1420;--border:#162030}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:'IBM Plex Mono',monospace;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px 16px;overflow-x:hidden}
.hdr{text-align:center;margin-bottom:14px}
.hdr h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(20px,3.2vw,30px);letter-spacing:-.5px;color:var(--fg)}
.hdr .sub{font-size:12px;color:var(--accent);margin-top:4px;font-weight:400;letter-spacing:.5px}
.wrap{width:100%;max-width:1020px;background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden;position:relative}
.wrap svg{display:block;width:100%;height:auto}
.ctrls{display:flex;align-items:center;gap:14px;margin-top:14px;padding:10px 18px;background:var(--card);border:1px solid var(--border);border-radius:10px;flex-wrap:wrap;justify-content:center}
.btn{padding:7px 18px;border:1px solid var(--border);border-radius:6px;background:var(--metal);color:var(--fg);font-family:'IBM Plex Mono',monospace;font-size:12px;cursor:pointer;transition:all .2s;user-select:none}
.btn:hover{background:var(--metal-lt);border-color:var(--accent)}
.btn.on{background:var(--accent);color:var(--bg);border-color:var(--accent)}
.sg{display:flex;align-items:center;gap:6px}
.sg label{font-size:11px;color:var(--muted);white-space:nowrap}
.sg input[type=range]{width:90px;accent-color:var(--accent);cursor:pointer}
.sg .v{font-size:11px;color:var(--accent);min-width:36px}
.pbar{display:flex;gap:3px;margin-top:10px;flex-wrap:wrap;justify-content:center}
.pi{padding:4px 10px;font-size:10px;border-radius:4px;background:var(--metal);color:var(--muted);transition:all .3s;white-space:nowrap}
.pi.on{background:var(--accent);color:var(--bg)}
.pi.warn{background:var(--danger);color:#fff}
.info{margin-top:14px;padding:14px 18px;background:var(--card);border:1px solid var(--border);border-radius:10px;max-width:1020px;width:100%}
.info h3{font-family:'Syne',sans-serif;font-size:14px;color:var(--accent);margin-bottom:6px}
.info p{font-size:11px;color:var(--muted);line-height:1.7}
.tip{font-size:11px;color:var(--danger);margin-top:8px;display:none}
.tip.show{display:block}
@keyframes pulse{0%,100%{opacity:.6}50%{opacity:1}}
</style>
</head>
<body>

<div class="hdr">
  <h1>V型渐缩滑道 · 被动定向落料</h1>
  <div class="sub">IFR 最终理想解 — 以几何约束替代主动翻转,零能耗自动定向</div>
</div>

<div class="wrap">
<svg id="scene" viewBox="0 0 1000 680" xmlns="http://www.w3.org/2000/svg">
<defs>
  <linearGradient id="gMetal" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#3a5068"/><stop offset="50%" stop-color="#283e52"/><stop offset="100%" stop-color="#1c2e40"/>
  </linearGradient>
  <linearGradient id="gMetalH" x1="0" y1="0" x2="1" y2="0">
    <stop offset="0%" stop-color="#2a3d50"/><stop offset="50%" stop-color="#3a5068"/><stop offset="100%" stop-color="#2a3d50"/>
  </linearGradient>
  <linearGradient id="gShaft" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#f0c858"/><stop offset="40%" stop-color="#e89830"/><stop offset="100%" stop-color="#c07018"/>
  </linearGradient>
  <linearGradient id="gShaftS" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="#f8d868"/><stop offset="40%" stop-color="#e8a838"/><stop offset="100%" stop-color="#c87820"/>
  </linearGradient>
  <filter id="glow"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="glowS"><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
  <filter id="shadow"><feDropShadow dx="2" dy="4" stdDeviation="3" flood-color="#000" flood-opacity=".4"/></filter>
  <marker id="arrG" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#00e8a0"/></marker>
  <marker id="arrR" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#ff4060"/></marker>
  <clipPath id="holeClip"><rect x="488" y="370" width="104" height="210"/></clipPath>
</defs>

<!-- 背景 -->
<rect width="1000" height="680" fill="#080e18"/>
<!-- 微网格 -->
<g opacity=".04" stroke="#4a6a8a" stroke-width=".5" id="gridG"></g>

<!-- 标题 -->
<text x="310" y="48" font-family="Syne,sans-serif" font-weight="700" font-size="15" fill="#8a9aaa" text-anchor="middle" letter-spacing="1">纯被动结构:滑移 → 翻转 → 导向</text>

<!-- V型导轨 -->
<g id="vgroove">
  <!-- 主体 -->
  <path d="M80 232 L540 267 L540 288 L80 253 Z" fill="url(#gMetal)" stroke="#3a5068" stroke-width="1"/>
  <!-- 顶面高光 -->
  <line x1="80" y1="232" x2="540" y2="267" stroke="#5a7a90" stroke-width="1.2"/>
  <!-- 左端面 -->
  <line x1="80" y1="232" x2="80" y2="253" stroke="#4a6a80" stroke-width="1"/>
  <!-- 右端面(关键边缘) -->
  <line id="pivotEdgeLine" x1="540" y1="267" x2="540" y2="288" stroke="#6a8a9a" stroke-width="2.5"/>
  <!-- 底部支撑 -->
  <path d="M80 253 L80 310 L65 310 L65 340 L555 340 L555 310 L540 310 L540 288" fill="url(#gMetalH)" stroke="#2a3d50" stroke-width="1"/>
  <path d="M65 340 L65 360 L555 360 L555 340 Z" fill="#141e2a" stroke="#2a3d50" stroke-width="1"/>
  <!-- V槽纹理线 -->
  <g opacity=".15" stroke="#5a7a90" stroke-width=".5">
    <line x1="120" y1="234" x2="120" y2="255"/><line x1="180" y1="237" x2="180" y2="258"/>
    <line x1="240" y1="241" x2="240" y2="261"/><line x1="300" y1="244" x2="300" y2="265"/>
    <line x1="360" y1="248" x2="360" y2="268"/><line x1="420" y1="251" x2="420" y2="272"/>
    <line x1="480" y1="255" x2="480" y2="275"/>
  </g>
</g>

<!-- 间隙虚线 -->
<line x1="540" y1="288" x2="540" y2="370" stroke="#2a3d50" stroke-width="1" stroke-dasharray="4,4"/>

<!-- 落料座 -->
<g id="dropseat">
  <!-- 主体 -->
  <path id="dsBody" d="M470 370 L488 370 L500 378 L500 402 L517 402 L517 572 L563 572 L563 402 L580 402 L580 378 L592 370 L610 370 L610 572 L470 572 Z" fill="url(#gMetal)" stroke="#3a5068" stroke-width="1.5"/>
  <!-- 孔内暗面 -->
  <path d="M500 378 L500 402 L517 402 L517 572 L563 572 L563 402 L580 402 L580 378 Z" fill="#060c16" opacity=".85"/>
  <!-- 倒角高亮 -->
  <path id="chL" d="M488 370 L500 378" stroke="#00e8a0" stroke-width="0" opacity="0"/>
  <path id="chR" d="M592 370 L580 378" stroke="#00e8a0" stroke-width="0" opacity="0"/>
  <!-- 台阶面 -->
  <line x1="500" y1="402" x2="517" y2="402" stroke="#5a7a90" stroke-width="1.5"/>
  <line x1="563" y1="402" x2="580" y2="402" stroke="#5a7a90" stroke-width="1.5"/>
  <!-- 孔壁 -->
  <line x1="500" y1="378" x2="500" y2="402" stroke="#4a6a80" stroke-width="1"/>
  <line x1="580" y1="378" x2="580" y2="402" stroke="#4a6a80" stroke-width="1"/>
  <line x1="517" y1="402" x2="517" y2="572" stroke="#4a6a80" stroke-width="1"/>
  <line x1="563" y1="402" x2="563" y2="572" stroke="#4a6a80" stroke-width="1"/>
</g>

<!-- 尺寸标注 -->
<g id="dims" opacity=".55">
  <text x="46" y="228" font-family="IBM Plex Mono" font-size="10" fill="#5a7a90">V槽 60°</text>
  <text x="618" y="395" font-family="IBM Plex Mono" font-size="10" fill="#5a7a90">Φ3.6</text>
  <text x="568" y="460" font-family="IBM Plex Mono" font-size="10" fill="#5a7a90">Φ2.1</text>
  <text x="618" y="376" font-family="IBM Plex Mono" font-size="9" fill="#00e8a0" opacity=".8">30°倒角</text>
  <!-- 倒角角度标注弧 -->
  <path d="M494 370 L500 370 L500 376" fill="none" stroke="#00e8a0" stroke-width=".8" opacity=".5"/>
</g>

<!-- 轨迹线 -->
<path id="traj" d="" fill="none" stroke="#00b8ff" stroke-width="1.8" opacity=".35" stroke-linecap="round"/>
<path id="trajGlow" d="" fill="none" stroke="#00b8ff" stroke-width="4" opacity=".1" stroke-linecap="round"/>

<!-- 支点指示器 -->
<g id="pivotInd" opacity="0">
  <circle cx="540" cy="267" r="7" fill="none" stroke="#00e8a0" stroke-width="2" filter="url(#glow)"/>
  <circle cx="540" cy="267" r="2.5" fill="#00e8a0"/>
  <text x="555" y="260" font-family="IBM Plex Mono" font-size="10" fill="#00e8a0">支点</text>
</g>

<!-- 重心指示 -->
<g id="cogInd" opacity="0">
  <circle id="cogDot" cx="0" cy="0" r="3.5" fill="#ff4060" filter="url(#glow)"/>
  <line id="cogLine" x1="0" y1="4" x2="0" y2="28" stroke="#ff4060" stroke-width="1" stroke-dasharray="2,2" marker-end="url(#arrR)"/>
  <text id="cogTxt" x="8" y="22" font-family="IBM Plex Mono" font-size="9" fill="#ff4060">G</text>
</g>

<!-- 重力箭头 -->
<g id="gravArr" opacity="0">
  <line id="gravLine" x1="0" y1="0" x2="0" y2="38" stroke="#ff4060" stroke-width="1.8" marker-end="url(#arrR)"/>
</g>

<!-- 台阶轴(动画主体) -->
<g id="shaft" filter="url(#shadow)">
  <!-- 大端 -->
  <rect x="0" y="-70" width="60" height="70" rx="2" fill="url(#gShaft)" stroke="#a06018" stroke-width="1.2"/>
  <!-- 小端 -->
  <rect x="60" y="-40" width="60" height="40" rx="2" fill="url(#gShaftS)" stroke="#a06018" stroke-width="1.2"/>
  <!-- 台阶面 -->
  <line x1="60" y1="-40" x2="60" y2="0" stroke="#a06018" stroke-width="1.5"/>
  <!-- 大端顶面高光 -->
  <line x1="2" y1="-68" x2="58" y2="-68" stroke="#f8d868" stroke-width=".8" opacity=".5"/>
  <!-- 小端顶面高光 -->
  <line x1="62" y1="-38" x2="118" y2="-38" stroke="#f8e078" stroke-width=".8" opacity=".5"/>
  <!-- 标注 -->
  <text x="30" y="-46" font-family="IBM Plex Mono" font-size="9" fill="#a06828" text-anchor="middle" opacity=".75">Φ3.5</text>
  <text x="90" y="-17" font-family="IBM Plex Mono" font-size="9" fill="#a06828" text-anchor="middle" opacity=".75">Φ2.0</text>
</g>

<!-- V槽截面插图 -->
<g transform="translate(850,110)">
  <rect x="-60" y="-20" width="120" height="110" rx="8" fill="#0a1420" stroke="#2a3d50" stroke-width="1"/>
  <text x="0" y="-5" font-family="IBM Plex Mono" font-size="9" fill="#5a7a90" text-anchor="middle">V槽截面</text>
  <line x1="0" y1="18" x2="-32" y2="72" stroke="#4a6a80" stroke-width="2.5" stroke-linecap="round"/>
  <line x1="0" y1="18" x2="32" y2="72" stroke="#4a6a80" stroke-width="2.5" stroke-linecap="round"/>
  <line x1="-32" y1="72" x2="32" y2="72" stroke="#4a6a80" stroke-width="2"/>
  <circle cx="0" cy="44" r="13" fill="url(#gShaft)" stroke="#a06018" stroke-width="1" opacity=".75"/>
  <path d="M-13 24 A16 16 0 0 1 13 24" fill="none" stroke="#00e8a0" stroke-width="1" opacity=".6"/>
  <text x="0" y="19" font-family="IBM Plex Mono" font-size="8" fill="#00e8a0" text-anchor="middle" opacity=".8">60°</text>
</g>

<!-- 阶段标签 -->
<text id="phaseLbl" x="310" y="640" font-family="Syne,sans-serif" font-weight="600" font-size="17" fill="#00e8a0" text-anchor="middle" opacity="0"></text>

<!-- IFR 消息 -->
<g id="ifrMsg" opacity="0">
  <rect x="90" y="510" width="280" height="62" rx="8" fill="#0a1420" stroke="#00e8a0" stroke-width="1.2" opacity=".85"/>
  <text x="230" y="534" font-family="Syne,sans-serif" font-weight="700" font-size="13" fill="#00e8a0" text-anchor="middle">理想解:零能耗自动定向</text>
  <text x="230" y="556" font-family="IBM Plex Mono" font-size="10" fill="#5a7a90" text-anchor="middle">重力 + 几何约束 ≡ 主动翻转机构</text>
</g>

<!-- 状态提示 -->
<text id="statusTxt" x="310" y="618" font-family="IBM Plex Mono" font-size="11" fill="#5a7a90" text-anchor="middle" opacity="0"></text>

</svg>
</div>

<div class="ctrls">
  <button class="btn" id="playBtn">播放</button>
  <button class="btn" id="resetBtn">重置</button>
  <button class="btn" id="loopBtn">循环:关</button>
  <div class="sg"><label>速度</label><input type="range" id="spdR" min="0.2" max="3" step="0.1" value="1"><span class="v" id="spdV">1.0x</span></div>
  <div class="sg"><label>悬空量</label><input type="range" id="ohR" min="30" max="100" step="1" value="72"><span class="v" id="ohV">72</span></div>
  <div class="sg"><label>进度</label><input type="range" id="pgR" min="0" max="1000" step="1" value="0"><span class="v" id="pgV">0%</span></div>
</div>

<div class="pbar" id="pbar">
  <div class="pi" data-p="0">滑入</div>
  <div class="pi" data-p="1">悬空失稳</div>
  <div class="pi" data-p="2">绕支点翻转</div>
  <div class="pi" data-p="3">倒角导向</div>
  <div class="pi" data-p="4">台阶限位落定</div>
</div>

<div class="info">
  <h3>IFR 原理解析</h3>
  <p>删除刚性翻转机械臂,替换为"V型渐缩滑道 + 阶梯落料孔"纯被动结构。台阶轴在V型槽内靠重力下滑,当前端悬空时因重心偏移自动翻转,落料孔口30°导向倒角捕获并扶正轴体,阶梯面限位确保小端优先插入2.1mm孔。全过程无需主动驱动,仅依赖重力与几何约束完成姿态转换。</p>
  <div class="tip" id="tipMsg">⚠ 当前悬空量不足以使重心越过支点,轴不会翻转——请增大悬空量</div>
</div>

<script>
/* ===== 常量 ===== */
const SW=120,SLW=60,SLH=70,SSW=60,SSH=40;
const VSX=100,VSY=232,VEX=540,VEY=267;
const PX=VEX,PY=VEY;
const DST=370;
/* 重心位置(距左端) */
const COG=(30*SLW*SLH+90*SSW*SSH)/(SLW*SLH+SSW*SSH);
/* 临界悬空量 */
const CRIT_OH=SW-COG;

/* ===== 状态 ===== */
let prog=0,playing=false,spd=1,ohLen=72,looping=false,manualPg=false,lastT=null;
let trajPts=[];

/* ===== 工具 ===== */
const lerp=(a,b,t)=>a+(b-a)*t;
const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
function easeIO(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}
function easeIn(t){return t*t*t}
function easeOut(t){return 1-Math.pow(1-t,3)}
function easeOutB(t){const n=7.5625,d=2.75;if(t<1/d)return n*t*t;if(t<2/d)return n*(t-=1.5/d)*t+.75;if(t<2.5/d)return n*(t-=2.25/d)*t+.9375;return n*(t-=2.625/d)*t+.984375}
function vY(x){return VSY+(x-VSX)/(VEX-VSX)*(VEY-VSY)}

/* ===== 变换计算 ===== */
function xformPt(lx,ly,tx,ty,ang,ox,oy){
  const a=ang*Math.PI/180;
  const x1=lx+ox,y1=ly+oy;
  const x2=x1*Math.cos(a)-y1*Math.sin(a);
  const y2=x1*Math.sin(a)+y1*Math.cos(a);
  return{x:x2+tx,y:y2+ty};
}

function getState(p){
  const po=SW-ohLen;
  const canTip=ohLen>CRIT_OH;

  /* 不翻转模式 */
  if(!canTip){
    if(p<.5){
      const t=p/.5,e=easeIO(t);
      const x=lerp(VSX,PX-po,e),y=vY(x);
      return{tf:`translate(${x},${y}) rotate(0)`,ang:0,ph:-1,pn:'重心未过支点 — 稳定',cx:x+COG,cy:y-35};
    }
    const x=PX-po,y=vY(x);
    return{tf:`translate(${x},${y}) rotate(0)`,ang:0,ph:-1,pn:'重心未过支点 — 无法翻转',cx:x+COG,cy:y-35};
  }

  /* 阶段0:滑入 */
  if(p<.25){
    const t=p/.25,e=easeIO(t);
    const x=lerp(VSX,PX-po,e),y=vY(x);
    return{tf:`translate(${x},${y}) rotate(0)`,ang:0,ph:0,pn:'台阶轴沿V型槽滑入',cx:x+COG,cy:y-35};
  }
  /* 阶段1:悬空失稳 */
  if(p<.36){
    const t=(p-.25)/.11;
    const ang=lerp(0,10,easeIn(t));
    const tf=`translate(${PX},${PY}) rotate(${ang}) translate(${-po},0)`;
    const c=xformPt(COG,-35,PX,PY,ang,-po,0);
    return{tf,ang,ph:1,pn:'前端悬空 · 重心偏移',cx:c.x,cy:c.y};
  }
  /* 阶段2:翻转 */
  if(p<.63){
    const t=(p-.36)/.27;
    const ang=lerp(10,90,easeIn(t));
    const tf=`translate(${PX},${PY}) rotate(${ang}) translate(${-po},0)`;
    const c=xformPt(COG,-35,PX,PY,ang,-po,0);
    return{tf,ang,ph:2,pn:'绕支点翻转90°落下',cx:c.x,cy:c.y};
  }
  /* 阶段3:导向 */
  if(p<.81){
    const t=(p-.63)/.18;
    const fd=lerp(0,88,easeOut(t));
    const lat=lerp(0,-22,easeIO(t));
    const tf=`translate(${PX+lat},${PY+fd}) rotate(90) translate(${-po},0)`;
    const c=xformPt(COG,-35,PX+lat,PY+fd,90,-po,0);
    return{tf,ang:90,ph:3,pn:'30°倒角导向 · 自动扶正',cx:c.x,cy:c.y};
  }
  /* 阶段4:落定 */
  if(p<.96){
    const t=(p-.81)/.15;
    const fd=lerp(88,128,easeOutB(t));
    const lat=lerp(-22,-22,t);
    const tf=`translate(${PX+lat},${PY+fd}) rotate(90) translate(${-po},0)`;
    const c=xformPt(COG,-35,PX+lat,PY+fd,90,-po,0);
    return{tf,ang:90,ph:4,pn:'台阶面限位 · 小端插入Φ2.1孔',cx:c.x,cy:c.y};
  }
  /* 完成 */
  const fd=128,lat=-22;
  const tf=`translate(${PX+lat},${PY+fd}) rotate(90) translate(${-po},0)`;
  const c=xformPt(COG,-35,PX+lat,PY+fd,90,-po,0);
  return{tf,ang:90,ph:5,pn:'定向完成',cx:c.x,cy:c.y};
}

/* ===== 场景更新 ===== */
const $=id=>document.getElementById(id);

function updateScene(){
  const s=getState(prog);
  $('shaft').setAttribute('transform',s.tf);

  /* 阶段指示 */
  document.querySelectorAll('.pi').forEach(el=>{
    const pp=parseInt(el.dataset.p);
    el.classList.toggle('on',pp===s.ph);
    el.classList.toggle('warn',s.ph===-1&&pp===1);
  });

  /* 阶段标签 */
  const lbl=$('phaseLbl');
  lbl.textContent=s.pn;
  lbl.setAttribute('opacity',prog>0?'1':'0');

  /* 支点指示器 */
  const showPivot=s.ph===1||s.ph===2;
  $('pivotInd').setAttribute('opacity',showPivot?'1':'0');
  if(showPivot){
    const pulse=.6+.4*Math.sin(Date.now()*.006);
    $('pivotInd').setAttribute('opacity',pulse.toFixed(2));
  }

  /* 重心指示 */
  const showCOG=s.ph>=0&&s.ph<=2;
  const cogG=$('cogInd');
  if(showCOG){
    cogG.setAttribute('opacity','1');
    $('cogDot').setAttribute('cx',s.cx);
    $('cogDot').setAttribute('cy',s.cy);
    $('cogLine').setAttribute('x1',s.cx);$('cogLine').setAttribute('y1',s.cy+5);
    $('cogLine').setAttribute('x2',s.cx);$('cogLine').setAttribute('y2',s.cy+30);
    $('cogTxt').setAttribute('x',s.cx+8);$('cogTxt').setAttribute('y',s.cy+24);
    /* 重心是否过支点 */
    const pastPivot=s.cx>PX;
    $('cogDot').setAttribute('fill',pastPivot?'#ff4060':'#00e8a0');
    $('cogLine').setAttribute('stroke',pastPivot?'#ff4060':'#00e8a0');
    $('cogTxt').setAttribute('fill',pastPivot?'#ff4060':'#00e8a0');
    $('cogTxt').textContent=pastPivot?'G(失稳)':'G';
  }else{
    cogG.setAttribute('opacity','0');
  }

  /* 重力箭头 */
  const showGrav=s.ph===2;
  const gA=$('gravArr');
  if(showGrav){
    gA.setAttribute('opacity','1');
    $('gravLine').setAttribute('x1',s.cx);$('gravLine').setAttribute('y1',s.cy+5);
    $('gravLine').setAttribute('x2',s.cx);$('gravLine').setAttribute('y2',s.cy+42);
  }else{
    gA.setAttribute('opacity','0');
  }

  /* 倒角高亮 */
  const showCh=s.ph===3;
  ['chL','chR'].forEach(id=>{
    $(id).setAttribute('stroke-width',showCh?'3':'0');
    $(id).setAttribute('opacity',showCh?'1':'0');
  });

  /* IFR 消息 */
  $('ifrMsg').setAttribute('opacity',s.ph>=4?'1':'0');

  /* 状态文字 */
  const st=$('statusTxt');
  if(s.ph===-1){
    st.textContent='悬空量 < 临界值 ('+CRIT_OH.toFixed(0)+'px),轴不会翻转';
    st.setAttribute('opacity','1');st.setAttribute('fill','#ff4060');
  }else if(s.ph>=4){
    st.textContent='IFR实现:零主动驱动,纯几何约束完成定向';
    st.setAttribute('opacity','1');st.setAttribute('fill','#00e8a0');
  }else{
    st.setAttribute('opacity','0');
  }

  /* 临界提示 */
  $('tipMsg').classList.toggle('show',ohLen<=CRIT_OH);

  /* 轨迹 */
  if(playing&&!manualPg&&prog>0.02){
    const tip=xformPt(SW,0,s.tf);
    trajPts.push(tip);
    if(trajPts.length>1){
      let d='M'+trajPts[0].x.toFixed(1)+','+trajPts[0].y.toFixed(1);
      for(let i=1;i<trajPts.length;i++){
        d+=' L'+trajPts[i].x.toFixed(1)+','+trajPts[i].y.toFixed(1);
      }
      $('traj').setAttribute('d',d);
      $('trajGlow').setAttribute('d',d);
    }
  }

  /* 进度滑块 */
  if(!manualPg){
    $('pgR').value=Math.round(prog*1000);
    $('pgV').textContent=Math.round(prog*100)+'%';
  }
}

/* ===== 动画循环 ===== */
function tick(ts){
  if(!playing){lastT=null;return}
  if(lastT===null)lastT=ts;
  const dt=(ts-lastT)/1000;
  lastT=ts;
  if(!manualPg){
    prog+=dt*spd*.18;
    if(prog>=1){
      prog=1;playing=false;
      $('playBtn').textContent='重播';
      if(looping)setTimeout(()=>{resetAnim();togglePlay()},1800);
    }
  }
  updateScene();
  if(playing)requestAnimationFrame(tick);
}

function togglePlay(){
  if(prog>=1)resetAnim();
  playing=!playing;
  $('playBtn').textContent=playing?'暂停':'播放';
  $('playBtn').classList.toggle('on',playing);
  if(playing)requestAnimationFrame(tick);
}

function resetAnim(){
  prog=0;playing=false;lastT=null;trajPts=[];
  $('traj').setAttribute('d','');$('trajGlow').setAttribute('d','');
  $('playBtn').textContent='播放';$('playBtn').classList.remove('on');
  updateScene();
}

/* ===== 事件绑定 ===== */
$('playBtn').onclick=togglePlay;
$('resetBtn').onclick=resetAnim;
$('loopBtn').onclick=function(){looping=!looping;this.textContent='循环:'+(looping?'开':'关');this.classList.toggle('on',looping)};

$('spdR').oninput=function(){spd=parseFloat(this.value);$('spdV').textContent=spd.toFixed(1)+'x'};
$('ohR').oninput=function(){ohLen=parseInt(this.value);$('ohV').textContent=ohLen;resetAnim()};
$('pgR').oninput=function(){manualPg=true;prog=parseInt(this.value)/1000;$('pgV').textContent=Math.round(prog*100)+'%';updateScene()};
$('pgR').onchange=function(){manualPg=false};

/* ===== 初始化网格 ===== */
(function(){
  const g=$('gridG');let s='';
  for(let x=0;x<=1000;x+=40)s+=`<line x1="${x}" y1="0" x2="${x}" y2="680"/>`;
  for(let y=0;y<=680;y+=40)s+=`<line x1="0" y1="${y}" x2="1000" y2="${y}"/>`;
  g.innerHTML=s;
})();

/* ===== 启动 ===== */
updateScene();
</script>
</body>
</html>

实现说明:

动画核心逻辑:整个动画分为5个阶段(滑入→悬空失稳→绕支点翻转→倒角导向→台阶限位落定),使用 SVG transform 组合(translate + rotate)精确模拟台阶轴在V型槽内的运动学行为。翻转阶段采用 easeIn 缓动模拟重力加速,落定阶段采用弹跳缓动表现微小弹跳效果。

IFR 思想体现

  • 聚焦理想状态——动画直接展示纯被动结构的工作过程,无需对比旧方案
  • 重心标记(红色/绿色)实时显示失稳临界点,直观揭示"重力即驱动力"的资源利用逻辑
  • 倒角高亮动画突出几何约束如何替代主动扶正机构
  • 完成时显示 IFR 等式:"重力 + 几何约束 ≡ 主动翻转机构"

交互设计

  • 悬空量滑块:可调节轴伸出V槽边缘的长度,低于临界值(≈68px)时轴不会翻转,直观体验关键设计参数的敏感性
  • 进度条:支持手动拖拽逐帧观察
  • 速度控制:0.2x~3x 变速
  • 循环模式:自动重复播放

视觉引导:支点脉冲光晕、重心颜色变化(绿→红表示失稳)、倒角高亮、轨迹线等共同引导关注核心创新点。

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