分享图
A
动画渲染工坊
就绪
<!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 href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#060a12;--surface:#0d1420;--border:#1e293b;
  --text:#e2e8f0;--muted:#64748b;--accent:#f59e0b;
  --teal:#14b8a6;--rose:#f43f5e;--green:#4ade80;
  --shaft:#e8a838;--disc:#3a4f66;--disc-light:#506880;
}
body{
  background:var(--bg);color:var(--text);
  font-family:'Noto Sans SC','Rajdhani',sans-serif;
  min-height:100vh;display:flex;flex-direction:column;align-items:center;
  overflow-x:hidden;
}
.page-header{
  text-align:center;padding:24px 20px 8px;width:100%;max-width:960px;
}
.page-header h1{
  font-family:'Rajdhani','Noto Sans SC',sans-serif;
  font-weight:700;font-size:clamp(22px,3.2vw,32px);
  letter-spacing:2px;color:#f1f5f9;
  background:linear-gradient(135deg,#f1f5f9 30%,var(--accent));
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
.page-header .subtitle{
  font-size:13px;color:var(--muted);margin-top:4px;letter-spacing:1px;
}
.animation-container{
  width:100%;max-width:960px;aspect-ratio:960/580;
  background:var(--surface);border:1px solid var(--border);
  border-radius:12px;overflow:hidden;position:relative;
  box-shadow:0 0 60px rgba(20,184,166,0.06),0 0 120px rgba(245,158,11,0.04);
  margin:12px auto 0;
}
.animation-container svg{width:100%;height:100%;display:block}
.controls-bar{
  width:100%;max-width:960px;display:flex;align-items:center;
  gap:12px;padding:14px 20px;flex-wrap:wrap;
  background:var(--surface);border:1px solid var(--border);
  border-radius:10px;margin:12px auto 0;
}
.ctrl-btn{
  background:#1a2332;border:1px solid var(--border);color:var(--text);
  border-radius:8px;padding:8px 16px;cursor:pointer;font-size:13px;
  font-family:'Rajdhani','Noto Sans SC',sans-serif;font-weight:600;
  transition:all .2s;display:flex;align-items:center;gap:6px;
  user-select:none;
}
.ctrl-btn:hover{background:#243044;border-color:#334155}
.ctrl-btn.active{background:var(--accent);color:#000;border-color:var(--accent)}
.ctrl-btn i{font-size:12px}
.phase-indicator{
  flex:1;min-width:200px;display:flex;gap:4px;align-items:center;
}
.phase-dot{
  width:10px;height:10px;border-radius:50%;background:#1e293b;
  border:1.5px solid #334155;transition:all .3s;position:relative;
}
.phase-dot.active{background:var(--accent);border-color:var(--accent);box-shadow:0 0 8px rgba(245,158,11,0.5)}
.phase-dot.done{background:#334155;border-color:#475569}
.phase-dot::after{
  content:attr(data-label);position:absolute;top:14px;left:50%;
  transform:translateX(-50%);font-size:9px;color:var(--muted);
  white-space:nowrap;pointer-events:none;
}
.speed-control{
  display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);
}
.speed-control input[type=range]{
  width:80px;accent-color:var(--accent);height:4px;
}
.info-row{
  width:100%;max-width:960px;display:grid;
  grid-template-columns:1fr 1fr;gap:12px;margin:12px auto 0;
}
.info-card{
  background:var(--surface);border:1px solid var(--border);
  border-radius:10px;padding:16px 18px;
}
.info-card h3{
  font-family:'Rajdhani',sans-serif;font-size:14px;font-weight:700;
  color:var(--accent);letter-spacing:1px;margin-bottom:8px;
}
.info-card p{font-size:12.5px;line-height:1.7;color:#94a3b8}
.info-card .param{
  display:inline-block;background:#1a2332;border:1px solid #253345;
  border-radius:4px;padding:1px 7px;font-size:11px;color:var(--teal);
  font-family:'Rajdhani',monospace;margin:2px 2px;
}
.phase-desc{
  font-size:13px;color:var(--text);min-height:40px;line-height:1.6;
  padding:10px 0;
}
.phase-desc .highlight{color:var(--accent);font-weight:700}
.phase-desc .teal{color:var(--teal);font-weight:700}
@media(max-width:640px){
  .info-row{grid-template-columns:1fr}
  .controls-bar{gap:8px;padding:10px 12px}
  .phase-dot::after{display:none}
}
</style>
</head>
<body>

<div class="page-header">
  <h1>ROTARY TRAY & WEDGE BLOCK</h1>
  <div class="subtitle">旋转托板与固定挡块 · 极狭小空间上料立起机构 · IFR 原理演示</div>
</div>

<div class="animation-container">
  <svg id="mainSvg" viewBox="0 0 960 580" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <!-- 背景网格 -->
      <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
        <path d="M40 0L0 0 0 40" fill="none" stroke="rgba(148,163,184,0.04)" stroke-width="0.5"/>
      </pattern>
      <!-- 圆盘渐变 -->
      <linearGradient id="discGrad" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#506880"/>
        <stop offset="50%" stop-color="#3a4f66"/>
        <stop offset="100%" stop-color="#2a3a4c"/>
      </linearGradient>
      <!-- 轴件渐变 -->
      <linearGradient id="shaftGrad" x1="0" y1="0" x2="1" y2="0">
        <stop offset="0%" stop-color="#d4922e"/>
        <stop offset="50%" stop-color="#e8a838"/>
        <stop offset="100%" stop-color="#f0be50"/>
      </linearGradient>
      <!-- 楔形挡块渐变 -->
      <linearGradient id="wedgeGrad" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="#0d9488"/>
        <stop offset="100%" stop-color="#14b8a6"/>
      </linearGradient>
      <!-- 发光效果 -->
      <filter id="glowShaft" x="-50%" y="-50%" width="200%" height="200%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
        <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <filter id="glowTeal" x="-50%" y="-50%" width="200%" height="200%">
        <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur"/>
        <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
        <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
        <feOffset dx="2" dy="3" result="off"/>
        <feFlood flood-color="#000" flood-opacity="0.35" result="color"/>
        <feComposite in="color" in2="off" operator="in" result="shadow"/>
        <feMerge><feMergeNode in="shadow"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
      <!-- 目标孔渐变 -->
      <linearGradient id="holeGrad" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="#1e293b"/>
        <stop offset="100%" stop-color="#0f172a"/>
      </linearGradient>
    </defs>

    <!-- 背景网格 -->
    <rect width="960" height="580" fill="#0a0f1a"/>
    <rect width="960" height="580" fill="url(#grid)"/>

    <!-- 所有动态元素将由 JS 创建 -->
  </svg>
</div>

<div class="controls-bar">
  <button class="ctrl-btn" id="btnPlay"><i class="fas fa-play"></i> 播放</button>
  <button class="ctrl-btn" id="btnStep"><i class="fas fa-step-forward"></i> 单步</button>
  <button class="ctrl-btn" id="btnReset"><i class="fas fa-redo"></i> 重置</button>
  <div class="phase-indicator" id="phaseDots"></div>
  <div class="speed-control">
    <span>速度</span>
    <input type="range" id="speedSlider" min="0.2" max="3" step="0.1" value="1">
    <span id="speedVal">1.0x</span>
  </div>
</div>

<div class="info-row">
  <div class="info-card">
    <h3>IFR 核心思想</h3>
    <p>理想解中,机构<em>自身</em>完成翻转——无需额外翻转动力源。托板旋转同时携带轴改变姿态,楔形挡块<em>被动</em>推出轴件,重力<em>自主</em>完成落料。系统未增加复杂执行器,却同时实现了"空间转向"与"姿态确定"。</p>
  </div>
  <div class="info-card">
    <h3>关键参数</h3>
    <p>
      托板厚度 <span class="param">1.5mm</span>
      凹槽外端 <span class="param">3.6×1mm</span>
      凹槽内端 <span class="param">2.1×8mm</span>
      楔形斜角 <span class="param">45°</span>
      旋转行程 <span class="param">0°→90°→~135°</span>
    </p>
  </div>
</div>

<script>
(function(){
  const SVG_NS='http://www.w3.org/2000/svg';
  const svg=document.getElementById('mainSvg');

  /* ============ 常量与尺寸 ============ */
  const CX=370, CY=195;            // 圆盘旋转中心
  const DISC_HW=105, DISC_HH=14;   // 圆盘半宽/半高(截面视图)
  const SHAFT_HEAD_L=13, SHAFT_HEAD_W=30;  // 轴头长度/宽度
  const SHAFT_BODY_L=58, SHAFT_BODY_W=17;  // 轴身长度/宽度
  const SHAFT_TOTAL=SHAFT_HEAD_L+SHAFT_BODY_L;
  const WEDGE_X=CX+8, WEDGE_Y=CY+DISC_HW+SHAFT_TOTAL*0.52;
  const HOLE_CX=CX, HOLE_CY=WEDGE_Y+68, HOLE_W=22, HOLE_D=38;
  const EJECT_ANGLE=135;  // 排出终止角度

  /* ============ 阶段定义 ============ */
  const PHASES=[
    {name:'推入',  dur:1.4, desc:'台阶轴水平推入旋转托板边缘凹槽,<span class="teal">大端朝外</span>,轴平躺在槽内'},
    {name:'旋转',  dur:2.0, desc:'托板旋转<span class="highlight">90°</span>,凹槽从水平转向垂直朝下,轴随槽<span class="highlight">竖起</span>'},
    {name:'阻挡',  dur:1.2, desc:'轴受重力欲下落,被固定<span class="teal">楔形挡块</span>挡住——资源巧妙利用'},
    {name:'推出',  dur:2.0, desc:'托板继续旋转,<span class="teal">楔形斜面</span>将轴逐渐推出凹槽,轴<span class="highlight">保持竖直</span>'},
    {name:'落料',  dur:1.2, desc:'轴脱出凹槽,<span class="highlight">小端向下</span>直接落入目标孔——理想解实现'},
    {name:'完成',  dur:1.5, desc:'落料完成,机构复位准备下一循环'},
  ];

  /* ============ 状态 ============ */
  let phase=0, progress=0, playing=false, speed=1, lastTime=0;
  let discAngle=0, shaftEjected=false, shaftGlobalY=0, shaftGlobalX=0;

  /* ============ SVG 辅助 ============ */
  function el(tag,attrs,parent){
    const e=document.createElementNS(SVG_NS,tag);
    for(const[k,v]of Object.entries(attrs||{}))e.setAttribute(k,v);
    if(parent)parent.appendChild(e);
    return e;
  }
  function setAttrs(e,attrs){for(const[k,v]of Object.entries(attrs))e.setAttribute(k,v)}

  /* ============ 创建静态元素 ============ */
  // 轨迹弧线(轴尖端路径)
  const trajGroup=el('g',{opacity:'0.15'},svg);
  const trajPath=el('path',{
    fill:'none',stroke:'#f59e0b','stroke-width':'1.5',
    'stroke-dasharray':'4 3',
  },trajGroup);

  // 目标孔
  const holeGroup=el('g',{filter:'url(#softShadow)'},svg);
  el('rect',{
    x:HOLE_CX-HOLE_W/2-4,y:HOLE_CY-HOLE_D/2-4,
    width:HOLE_W+8,height:HOLE_D+8,rx:3,
    fill:'#1e293b',stroke:'#334155','stroke-width':'1',
  },holeGroup);
  el('rect',{
    x:HOLE_CX-HOLE_W/2,y:HOLE_CY-HOLE_D/2,
    width:HOLE_W,height:HOLE_D,rx:2,
    fill:'url(#holeGrad)',stroke:var_green(),'stroke-width':'1.5',
    'stroke-dasharray':'3 2',
  },holeGroup);
  // 孔标签
  el('text',{
    x:HOLE_CX,y:HOLE_CY+HOLE_D/2+16,
    fill:'#4ade80','font-size':'10','text-anchor':'middle',
    'font-family':'Rajdhani, sans-serif','font-weight':'600',
  },holeGroup).textContent='TARGET HOLE';

  function var_green(){return '#4ade80'}

  // 楔形挡块
  const wedgeGroup=el('g',{filter:'url(#softShadow)'},svg);
  const wedgeW=36, wedgeH=44;
  // 楔形:右侧45°斜面
  const wPath=`M${WEDGE_X-wedgeW/2},${WEDGE_Y-wedgeH/2}
               L${WEDGE_X+wedgeW/2},${WEDGE_Y-wedgeH/2}
               L${WEDGE_X+wedgeW/2},${WEDGE_Y+wedgeH/2}
               L${WEDGE_X-wedgeW/2},${WEDGE_Y+wedgeH/2}Z`;
  el('path',{d:wPath,fill:'url(#wedgeGrad)',stroke:'#0d9488','stroke-width':'1'},wedgeGroup);
  // 45°斜面线
  const wsX=WEDGE_X+wedgeW/2, wsY=WEDGE_Y-wedgeH/2;
  const weX=wsX-wedgeH, weY=WEDGE_Y+wedgeH/2;
  el('line',{x1:wsX,y1:wsY,x2:Math.max(wsX-wedgeH,WEDGE_X-wedgeW/2),y2:weY,
    stroke:'#5eead4','stroke-width':'1.5','stroke-dasharray':'3 2'},wedgeGroup);
  // 斜面角度标注
  el('text',{
    x:WEDGE_X+wedgeW/2+8,y:WEDGE_Y,
    fill:'#5eead4','font-size':'10','font-family':'Rajdhani, sans-serif',
    'font-weight':'600',
  },wedgeGroup).textContent='45°';
  // 挡块标签
  el('text',{
    x:WEDGE_X,y:WEDGE_Y-wedgeH/2-8,
    fill:'#14b8a6','font-size':'10','text-anchor':'middle',
    'font-family':'Rajdhani, sans-serif','font-weight':'600',
  },wedgeGroup).textContent='WEDGE BLOCK';

  // 旋转中心标记
  el('circle',{cx:CX,cy:CY,r:4,fill:'#334155',stroke:'#64748b','stroke-width':'1'},svg);
  el('circle',{cx:CX,cy:CY,r:1.5,fill:'#94a3b8'},svg);
  // 中心标签
  el('text',{
    x:CX-16,y:CY-10,fill:'#64748b','font-size':'9',
    'font-family':'Rajdhani, sans-serif',
  },svg).textContent='AXIS';

  /* ============ 创建动态元素 ============ */
  // 圆盘组(旋转)
  const discGroup=el('g',{},svg);
  // 圆盘本体
  const discRect=el('rect',{
    x:-DISC_HW,y:-DISC_HH,width:DISC_HW*2,height:DISC_HH*2,rx:3,
    fill:'url(#discGrad)',stroke:'#5a7088','stroke-width':'1',
  },discGroup);
  // 凹槽(在圆盘上,右侧边缘)
  const grooveGroup=el('g',{},discGroup);
  // 凹槽外端(宽而浅)
  el('rect',{
    x:DISC_HW-SHAFT_HEAD_L,y:-SHAFT_HEAD_W/2,
    width:SHAFT_HEAD_L,height:SHAFT_HEAD_W,rx:1,
    fill:'#1a2535',stroke:'#2a3a4c','stroke-width':'0.5',
  },grooveGroup);
  // 凹槽内端(窄而深)
  el('rect',{
    x:DISC_HW-SHAFT_HEAD_L-SHAFT_BODY_L,y:-SHAFT_BODY_W/2,
    width:SHAFT_BODY_L,height:SHAFT_BODY_W,rx:1,
    fill:'#151e2d',stroke:'#2a3a4c','stroke-width':'0.5',
  },grooveGroup);

  // 轴件组(在圆盘内时随圆盘旋转,排出后独立)
  const shaftInDisc=el('g',{filter:'url(#glowShaft)'},discGroup);
  // 轴头
  el('rect',{
    x:DISC_HW-SHAFT_HEAD_L,y:-SHAFT_HEAD_W/2,
    width:SHAFT_HEAD_L,height:SHAFT_HEAD_W,rx:2,
    fill:'url(#shaftGrad)',stroke:'#b8860b','stroke-width':'0.8',
  },shaftInDisc);
  // 轴身
  el('rect',{
    x:DISC_HW-SHAFT_HEAD_L-SHAFT_BODY_L,y:-SHAFT_BODY_W/2,
    width:SHAFT_BODY_L,height:SHAFT_BODY_W,rx:1,
    fill:'url(#shaftGrad)',stroke:'#b8860b','stroke-width':'0.8',
  },shaftInDisc);
  // 小端标记
  el('circle',{
    cx:DISC_HW-SHAFT_HEAD_L-SHAFT_BODY_L+4,cy:0,r:2.5,
    fill:'#f43f5e',opacity:'0.9',
  },shaftInDisc);

  // 独立轴件(排出后使用)
  const shaftFree=el('g',{filter:'url(#glowShaft)',opacity:'0'},svg);
  const sfHead=el('rect',{x:0,y:0,width:SHAFT_HEAD_L,height:SHAFT_HEAD_W,rx:2,
    fill:'url(#shaftGrad)',stroke:'#b8860b','stroke-width':'0.8'},shaftFree);
  const sfBody=el('rect',{x:SHAFT_HEAD_L,y:(SHAFT_HEAD_W-SHAFT_BODY_W)/2,
    width:SHAFT_BODY_L,height:SHAFT_BODY_W,rx:1,
    fill:'url(#shaftGrad)',stroke:'#b8860b','stroke-width':'0.8'},shaftFree);
  const sfMark=el('circle',{cx:SHAFT_HEAD_L+SHAFT_BODY_L-4,
    cy:SHAFT_HEAD_W/2,r:2.5,fill:'#f43f5e',opacity:'0.9'},shaftFree);

  // 高亮环(关键事件反馈)
  const highlightRing=el('circle',{
    cx:0,cy:0,r:20,fill:'none',stroke:'#f59e0b','stroke-width':'2',
    opacity:'0','pointer-events':'none',
  },svg);

  // 角度指示弧
  const angleArc=el('path',{
    fill:'none',stroke:'rgba(245,158,11,0.3)','stroke-width':'1.5',
    'stroke-dasharray':'4 3',
  },svg);
  const angleText=el('text',{
    x:0,y:0,fill:'#f59e0b','font-size':'12',
    'font-family':'Rajdhani, sans-serif','font-weight':'700',
    'text-anchor':'middle',
  },svg);

  // 阶段描述文字
  const phaseLabel=el('text',{
    x:700,y:40,fill:'#f1f5f9','font-size':'14',
    'font-family':'Noto Sans SC, Rajdhani, sans-serif','font-weight':'400',
  },svg);
  const phaseTitle=el('text',{
    x:700,y:22,fill:var_accent(),'font-size':'16',
    'font-family':'Rajdhani, sans-serif','font-weight':'700',
    'letter-spacing':'1',
  },svg);
  function var_accent(){return '#f59e0b'}

  // 运动方向箭头
  const arrowGroup=el('g',{opacity:'0'},svg);

  // 推入方向箭头
  const pushArrow=el('g',{},arrowGroup);
  el('line',{x1:CX+DISC_HW+SHAFT_TOTAL+40,y2:CY,x2:CX+DISC_HW+10,y2:CY,
    stroke:'#f59e0b','stroke-width':'2','marker-end':'url(#arrowHead)'},pushArrow);
  el('text',{x:CX+DISC_HW+SHAFT_TOTAL+45,y:CY+4,fill:'#f59e0b',
    'font-size':'11','font-family':'Rajdhani, sans-serif','font-weight':'600'},
    pushArrow).textContent='PUSH';

  // 旋转方向箭头
  const rotArrow=el('g',{},arrowGroup);

  // 重力箭头
  const gravArrow=el('g',{},arrowGroup);

  // 箭头标记定义
  const arrowMarker=el('marker',{
    id:'arrowHead',markerWidth:'8',markerHeight:'6',
    refX:'8',refY:'3',orient:'auto',
  },el('defs',{},svg));
  el('path',{d:'M0,0 L8,3 L0,6 Z',fill:'#f59e0b'},arrowMarker);

  const arrowMarkerTeal=el('marker',{
    id:'arrowHeadTeal',markerWidth:'8',markerHeight:'6',
    refX:'8',refY:'3',orient:'auto',
  },el('defs',{},svg));
  el('path',{d:'M0,0 L8,3 L0,6 Z',fill:'#14b8a6'},arrowMarkerTeal);

  /* ============ 截面细节插图 ============ */
  const insetGroup=el('g',{transform:'translate(750,140)'},svg);
  el('rect',{x:-90,y:-70,width:180,height:150,rx:6,
    fill:'rgba(13,20,32,0.9)',stroke:'#1e293b','stroke-width':'1'},insetGroup);
  el('text',{x:0,y:-54,fill:'#94a3b8','font-size':'10',
    'text-anchor':'middle','font-family':'Rajdhani, sans-serif','font-weight':'600',
    'letter-spacing':'1'},insetGroup).textContent='GROOVE CROSS-SECTION';

  // 截面:凹槽 + 轴
  const insetScale=3.5;
  // 圆盘边缘截面
  el('rect',{x:-60,y:-30,width:120,height:50,rx:2,
    fill:'#2a3a4c',stroke:'#5a7088','stroke-width':'0.8'},insetGroup);
  // 凹槽外端
  el('rect',{x:30,y:-10,width:1*insetScale,height:3.6*insetScale,rx:0.5,
    fill:'#151e2d',stroke:'#2a3a4c','stroke-width':'0.5'},insetGroup);
  // 凹槽内端
  el('rect',{x:30-8*insetScale,y:(3.6-2.1)/2*insetScale-10,
    width:8*insetScale,height:2.1*insetScale,rx:0.5,
    fill:'#111827',stroke:'#2a3a4c','stroke-width':'0.5'},insetGroup);
  // 轴件(半剖面)
  el('rect',{x:30,y:-10,width:1*insetScale,height:3.6*insetScale*0.5,rx:0.5,
    fill:'#e8a838',opacity:'0.8'},insetGroup);
  el('rect',{x:30-8*insetScale,y:(3.6-2.1)/2*insetScale-10,
    width:8*insetScale,height:2.1*insetScale*0.5,rx:0.5,
    fill:'#e8a838',opacity:'0.8'},insetGroup);
  // 标注
  el('text',{x:0,y:30,fill:'#64748b','font-size':'8',
    'text-anchor':'middle','font-family':'Rajdhani, sans-serif'},
    insetGroup).textContent='3.6×1  |  2.1×8 (mm)';

  /* ============ 轨迹计算 ============ */
  function calcTrajectory(){
    let d='';
    for(let a=0;a<=EJECT_ANGLE;a+=2){
      const rad=a*Math.PI/180;
      const r=DISC_HW+SHAFT_TOTAL;
      const px=CX+r*Math.cos(rad);
      const py=CY+r*Math.sin(rad);
      d+=(a===0?'M':'L')+px.toFixed(1)+','+py.toFixed(1);
    }
    // 继续到落料位置
    const ejectRad=EJECT_ANGLE*Math.PI/180;
    const tipX=CX+(DISC_HW+SHAFT_TOTAL)*Math.cos(ejectRad);
    const tipY=CY+(DISC_HW+SHAFT_TOTAL)*Math.sin(ejectRad);
    d+='L'+tipX.toFixed(1)+','+(HOLE_CY-HOLE_D/2).toFixed(1);
    trajPath.setAttribute('d',d);
  }
  calcTrajectory();

  /* ============ 角度弧线 ============ */
  function updateAngleArc(angle){
    if(angle<1){angleArc.setAttribute('d','');angleText.textContent='';return}
    const r=50;
    const startRad=0,endRad=angle*Math.PI/180;
    const sx=CX+r*Math.cos(startRad),sy=CY+r*Math.sin(startRad);
    const ex=CX+r*Math.cos(endRad),ey=CY+r*Math.sin(endRad);
    const largeArc=angle>180?1:0;
    const d=`M${sx},${sy} A${r},${r} 0 ${largeArc} 1 ${ex},${ey}`;
    angleArc.setAttribute('d',d);
    const midRad=(startRad+endRad)/2;
    angleText.setAttribute('x',CX+(r+16)*Math.cos(midRad));
    angleText.setAttribute('y',CY+(r+16)*Math.sin(midRad)+4);
    angleText.textContent=Math.round(angle)+'°';
  }

  /* ============ 旋转方向箭头 ============ */
  function drawRotArrow(){
    while(rotArrow.firstChild)rotArrow.removeChild(rotArrow.firstChild);
    const r=DISC_HW+30;
    const a1=-10*Math.PI/180, a2=80*Math.PI/180;
    const sx=CX+r*Math.cos(a1),sy=CY+r*Math.sin(a1);
    const ex=CX+r*Math.cos(a2),ey=CY+r*Math.sin(a2);
    el('path',{
      d:`M${sx},${sy} A${r},${r} 0 0 1 ${ex},${ey}`,
      fill:'none',stroke:'#f59e0b','stroke-width':'1.5',
      'stroke-dasharray':'5 3','marker-end':'url(#arrowHead)',
    },rotArrow);
    el('text',{
      x:CX+r+10,y:CY+r/2+5,fill:'#f59e0b','font-size':'10',
      'font-family':'Rajdhani, sans-serif','font-weight':'600',
    },rotArrow).textContent='ROTATE';
  }
  drawRotArrow();

  /* ============ 重力箭头 ============ */
  function drawGravArrow(yPos){
    while(gravArrow.firstChild)gravArrow.removeChild(gravArrow.firstChild);
    const ax=CX+DISC_HW+SHAFT_TOTAL/2+20;
    el('line',{x1:ax,y1:yPos-15,x2:ax,y2:yPos+20,
      stroke:'#f43f5e','stroke-width':'2','marker-end':'url(#arrowHeadRed)'},gravArrow);
    el('text',{x:ax+6,y:yPos+5,fill:'#f43f5e','font-size':'10',
      'font-family':'Rajdhani, sans-serif','font-weight':'600'},gravArrow).textContent='G';
  }
  // 红色箭头
  const arrowMarkerRed=el('marker',{
    id:'arrowHeadRed',markerWidth:'8',markerHeight:'6',
    refX:'8',refY:'3',orient:'auto',
  },el('defs',{},svg));
  el('path',{d:'M0,0 L8,3 L0,6 Z',fill:'#f43f5e'},arrowMarkerRed);

  /* ============ 高亮环动画 ============ */
  let ringAnim={active:false,x:0,y:0,t:0};
  function triggerRing(x,y){
    ringAnim={active:true,x,y,t:0};
    highlightRing.setAttribute('cx',x);
    highlightRing.setAttribute('cy',y);
    highlightRing.setAttribute('opacity','1');
    highlightRing.setAttribute('r','10');
  }
  function updateRing(dt){
    if(!ringAnim.active)return;
    ringAnim.t+=dt*2;
    if(ringAnim.t>1){ringAnim.active=false;highlightRing.setAttribute('opacity','0');return}
    const r=10+ringAnim.t*40;
    const op=1-ringAnim.t;
    highlightRing.setAttribute('r',r);
    highlightRing.setAttribute('opacity',op);
    highlightRing.setAttribute('stroke-width',2*(1-ringAnim.t));
  }

  /* ============ 阶段指示器 ============ */
  const dotsContainer=document.getElementById('phaseDots');
  PHASES.forEach((p,i)=>{
    const dot=document.createElement('div');
    dot.className='phase-dot';
    dot.setAttribute('data-label',p.name);
    dot.addEventListener('click',()=>{phase=i;progress=0;updatePhaseDots()});
    dotsContainer.appendChild(dot);
  });
  function updatePhaseDots(){
    const dots=dotsContainer.children;
    for(let i=0;i<dots.length;i++){
      dots[i].classList.toggle('active',i===phase);
      dots[i].classList.toggle('done',i<phase);
    }
  }
  updatePhaseDots();

  /* ============ 缓动函数 ============ */
  function easeInOut(t){return t<0.5?2*t*t:-1+(4-2*t)*t}
  function easeOut(t){return 1-Math.pow(1-t,3)}
  function easeIn(t){return t*t*t}

  /* ============ 主更新逻辑 ============ */
  let prevPhase=-1;

  function update(dt){
    if(!playing && phase===prevPhase) return;

    // 阶段描述
    if(phase!==prevPhase){
      prevPhase=phase;
      updatePhaseDots();
      if(phase<PHASES.length){
        phaseTitle.textContent='PHASE '+(phase+1)+' / '+PHASES.length;
        phaseLabel.textContent=PHASES[phase].name;
      }
    }

    const p=easeInOut(Math.min(1,Math.max(0,progress)));

    // 圆盘组旋转
    discGroup.setAttribute('transform',`translate(${CX},${CY}) rotate(${discAngle})`);

    // 根据阶段计算状态
    switch(phase){
      case 0:{ // 推入
        discAngle=0;
        // 轴件从右侧滑入
        const slideX=SHAFT_TOTAL*(1-p);
        shaftInDisc.setAttribute('transform',`translate(${slideX},0)`);
        shaftInDisc.setAttribute('opacity','1');
        shaftFree.setAttribute('opacity','0');
        arrowGroup.setAttribute('opacity',p<0.9?'1':'0');
        // 显示推入箭头
        while(arrowGroup.childNodes.length>2)arrowGroup.removeChild(arrowGroup.lastChild);
        if(p<0.85){
          const ax=CX+DISC_HW+SHAFT_TOTAL*(1-p)+20;
          el('line',{x1:ax+35,y1:CY,x2:ax+5,y2:CY,
            stroke:'#f59e0b','stroke-width':'2','marker-end':'url(#arrowHead)'},
            arrowGroup);
        }
        break;
      }
      case 1:{ // 旋转 0→90°
        discAngle=p*90;
        shaftInDisc.setAttribute('transform','translate(0,0)');
        shaftInDisc.setAttribute('opacity','1');
        shaftFree.setAttribute('opacity','0');
        arrowGroup.setAttribute('opacity',p<0.8?'0.7':'0');
        break;
      }
      case 2:{ // 阻挡
        discAngle=90;
        shaftInDisc.setAttribute('transform','translate(0,0)');
        shaftInDisc.setAttribute('opacity','1');
        shaftFree.setAttribute('opacity','0');
        arrowGroup.setAttribute('opacity','0.8');
        // 显示重力箭头
        drawGravArrow(CY+DISC_HW+SHAFT_TOTAL*0.5);
        // 楔形挡块高亮脉冲
        const pulse=0.6+0.4*Math.sin(progress*Math.PI*4);
        wedgeGroup.setAttribute('opacity',pulse.toFixed(2));
        if(progress>0.01 && progress<0.05) triggerRing(WEDGE_X,WEDGE_Y);
        break;
      }
      case 3:{ // 推出 90°→135°
        discAngle=90+p*45;
        wedgeGroup.setAttribute('opacity','1');
        // 轴件逐渐脱离凹槽
        const ejectP=easeOut(p);
        // 在圆盘坐标系中,轴件向外滑动
        const slideOut=ejectP*SHAFT_TOTAL*0.7;
        shaftInDisc.setAttribute('transform',`translate(${slideOut},0)`);
        shaftInDisc.setAttribute('opacity',(1-ejectP*1.2).toFixed(2));

        // 独立轴件逐渐出现(保持在垂直姿态)
        const sAngle=(90)*Math.PI/180;
        const shaftCenterR=DISC_HW+SHAFT_TOTAL*0.5+slideOut*0.3;
        const sfx=CX+shaftCenterR*Math.cos(sAngle)-SHAFT_TOTAL/2;
        const sfy=CY+shaftCenterR*Math.sin(sAngle)-SHAFT_HEAD_W/2;
        shaftFree.setAttribute('transform',
          `translate(${sfx.toFixed(1)},${sfy.toFixed(1)}) rotate(90,${(SHAFT_TOTAL/2).toFixed(1)},${(SHAFT_HEAD_W/2).toFixed(1)})`);
        shaftFree.setAttribute('opacity',(ejectP*1.3).toFixed(2));
        arrowGroup.setAttribute('opacity','0');

        // 楔形挡块推力箭头
        if(p>0.1 && p<0.9){
          while(arrowGroup.childNodes.length>2)arrowGroup.removeChild(arrowGroup.lastChild);
          const waY=CY+DISC_HW+SHAFT_TOTAL*0.5;
          el('line',{x1:WEDGE_X-wedgeW/2-5,y1:waY,x2:WEDGE_X-wedgeW/2-25,y2:waY,
            stroke:'#14b8a6','stroke-width':'2','marker-end':'url(#arrowHeadTeal)'},
            arrowGroup);
          arrowGroup.setAttribute('opacity','0.8');
        }
        break;
      }
      case 4:{ // 落料
        discAngle=EJECT_ANGLE;
        shaftInDisc.setAttribute('opacity','0');
        // 轴件从排出位置下落到孔中
        const startFY=CY+DISC_HW+SHAFT_TOTAL*0.5;
        const endFY=HOLE_CY-SHAFT_TOTAL/2;
        const curFY=startFY+(endFY-startFY)*easeIn(p);
        const curFX=CX;
        shaftFree.setAttribute('transform',
          `translate(${(curFX-SHAFT_TOTAL/2).toFixed(1)},${(curFY-SHAFT_HEAD_W/2).toFixed(1)}) rotate(90,${(SHAFT_TOTAL/2).toFixed(1)},${(SHAFT_HEAD_W/2).toFixed(1)})`);
        shaftFree.setAttribute('opacity','1');
        arrowGroup.setAttribute('opacity','0');
        if(progress>0.01&&progress<0.05) triggerRing(curFX,curFY+SHAFT_TOTAL/2);
        break;
      }
      case 5:{ // 完成
        discAngle=EJECT_ANGLE-(p*EJECT_ANGLE); // 复位旋转
        shaftInDisc.setAttribute('opacity','0');
        // 轴件在孔中
        const finalFX=CX;
        const finalFY=HOLE_CY-SHAFT_TOTAL/2;
        shaftFree.setAttribute('transform',
          `translate(${(finalFX-SHAFT_TOTAL/2).toFixed(1)},${(finalFY-SHAFT_HEAD_W/2).toFixed(1)}) rotate(90,${(SHAFT_TOTAL/2).toFixed(1)},${(SHAFT_HEAD_W/2).toFixed(1)})`);
        shaftFree.setAttribute('opacity',(1-p*0.5).toFixed(2));
        arrowGroup.setAttribute('opacity','0');
        // 复位后显示轴件在原位
        if(p>0.7){
          shaftInDisc.setAttribute('opacity',((p-0.7)/0.3).toFixed(2));
          shaftInDisc.setAttribute('transform','translate(0,0)');
        }
        break;
      }
    }

    updateAngleArc(discAngle);
    updateRing(dt);

    // 进度推进
    if(playing){
      const dur=PHASES[phase]?PHASES[phase].dur:1;
      progress+=dt*speed/dur;
      if(progress>=1){
        progress=0;
        phase++;
        if(phase>=PHASES.length){
          phase=0;
        }
        updatePhaseDots();
      }
    }
  }

  /* ============ 动画循环 ============ */
  function loop(time){
    const dt=Math.min(0.05,(time-lastTime)/1000);
    lastTime=time;
    update(dt);
    requestAnimationFrame(loop);
  }
  lastTime=performance.now();
  requestAnimationFrame(loop);

  /* ============ 控件事件 ============ */
  const btnPlay=document.getElementById('btnPlay');
  const btnStep=document.getElementById('btnStep');
  const btnReset=document.getElementById('btnReset');
  const speedSlider=document.getElementById('speedSlider');
  const speedVal=document.getElementById('speedVal');

  btnPlay.addEventListener('click',()=>{
    playing=!playing;
    btnPlay.classList.toggle('active',playing);
    btnPlay.innerHTML=playing?'<i class="fas fa-pause"></i> 暂停':'<i class="fas fa-play"></i> 播放';
  });
  btnStep.addEventListener('click',()=>{
    playing=false;
    btnPlay.classList.remove('active');
    btnPlay.innerHTML='<i class="fas fa-play"></i> 播放';
    progress=0;
    phase=(phase+1)%PHASES.length;
    prevPhase=-1;
    updatePhaseDots();
  });
  btnReset.addEventListener('click',()=>{
    playing=false;
    phase=0;progress=0;discAngle=0;prevPhase=-1;
    btnPlay.classList.remove('active');
    btnPlay.innerHTML='<i class="fas fa-play"></i> 播放';
    shaftInDisc.setAttribute('opacity','1');
    shaftInDisc.setAttribute('transform','translate(0,0)');
    shaftFree.setAttribute('opacity','0');
    arrowGroup.setAttribute('opacity','0');
    wedgeGroup.setAttribute('opacity','1');
    updatePhaseDots();
  });
  speedSlider.addEventListener('input',()=>{
    speed=parseFloat(speedSlider.value);
    speedVal.textContent=speed.toFixed(1)+'x';
  });

  /* ============ 描述面板更新 ============ */
  const descEl=document.createElement('div');
  descEl.className='phase-desc';
  descEl.style.cssText='max-width:960px;width:100%;text-align:center;padding:8px 20px;';
  document.querySelector('.info-row').after(descEl);
  function updateDesc(){
    if(phase<PHASES.length){
      descEl.innerHTML=PHASES[phase].desc;
    }
  }
  setInterval(updateDesc,200);

  /* ============ 额外装饰:微粒背景 ============ */
  const particles=[];
  for(let i=0;i<25;i++){
    const c=el('circle',{
      cx:Math.random()*960,cy:Math.random()*580,
      r:Math.random()*1.2+0.3,
      fill:'rgba(148,163,184,0.15)',
    },svg);
    particles.push({el:c,vx:(Math.random()-0.5)*8,vy:(Math.random()-0.5)*5});
  }
  function animParticles(dt){
    particles.forEach(p=>{
      let cx=parseFloat(p.el.getAttribute('cx'))+p.vx*dt;
      let cy=parseFloat(p.el.getAttribute('cy'))+p.vy*dt;
      if(cx<0||cx>960)p.vx*=-1;
      if(cy<0||cy>580)p.vy*=-1;
      p.el.setAttribute('cx',cx.toFixed(1));
      p.el.setAttribute('cy',cy.toFixed(1));
    });
  }
  // 添加到主循环
  const origUpdate=update;
  update=function(dt){
    origUpdate(dt);
    animParticles(dt);
  };

  /* ============ 鼠标悬停交互 ============ */
  svg.addEventListener('mousemove',(e)=>{
    const rect=svg.getBoundingClientRect();
    const scaleX=960/rect.width;
    const scaleY=580/rect.height;
    const mx=(e.clientX-rect.left)*scaleX;
    const my=(e.clientY-rect.top)*scaleY;
    // 检查是否悬停在楔形挡块上
    const dx=mx-WEDGE_X, dy=my-WEDGE_Y;
    if(Math.abs(dx)<wedgeW&&Math.abs(dy)<wedgeH){
      wedgeGroup.setAttribute('filter','url(#glowTeal)');
    }else{
      wedgeGroup.removeAttribute('filter');
    }
  });

})();
</script>
</body>
</html>

实现说明

这个动画以侧截面视图为核心,清晰呈现了"旋转托板与固定挡块"机构的完整工作原理:

视觉设计

  • 暗色工程蓝图风格:深邃背景配微光网格,营造精密仪器感
  • 轴件采用琥珀色渐变+辉光滤镜,始终是视觉焦点
  • 楔形挡块用青绿色区分,悬停时触发辉光反馈
  • 目标孔用虚线绿色边框标注,暗示接纳位
  • 右上角嵌入凹槽截面细节图,展示 3.6×1 / 2.1×8mm 的台阶形构造

动画六阶段

  1. 推入 → 轴件水平滑入凹槽,方向箭头引导
  2. 旋转 → 托板旋转 0°→90°,角度弧线实时标注
  3. 阻挡 → 重力箭头+楔形挡块脉冲高亮,展示"被动资源利用"
  4. 推出 → 托板继续旋转至 ~135°,轴件脱离凹槽保持竖直
  5. 落料 → 轴件小端向下自由落入目标孔
  6. 完成 → 机构自动复位循环

IFR 思想体现

  • 无额外翻转执行器——旋转本身改变空间维度
  • 重力作为免费资源完成落料
  • 楔形挡块零能耗被动推出轴件
  • 关键时刻(阻挡、脱出)有高亮环动画强化注意

交互控制

  • 播放/暂停、单步跳转、重置按钮
  • 可点击阶段指示器直接跳转
  • 速度滑块 0.2x ~ 3.0x 调节
积分规则:第一轮对话扣减6分,后续每轮扣4分