<!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 的台阶形构造
动画六阶段
- 推入 → 轴件水平滑入凹槽,方向箭头引导
- 旋转 → 托板旋转 0°→90°,角度弧线实时标注
- 阻挡 → 重力箭头+楔形挡块脉冲高亮,展示"被动资源利用"
- 推出 → 托板继续旋转至 ~135°,轴件脱离凹槽保持竖直
- 落料 → 轴件小端向下自由落入目标孔
- 完成 → 机构自动复位循环
IFR 思想体现
- 无额外翻转执行器——旋转本身改变空间维度
- 重力作为免费资源完成落料
- 楔形挡块零能耗被动推出轴件
- 关键时刻(阻挡、脱出)有高亮环动画强化注意
交互控制
- 播放/暂停、单步跳转、重置按钮
- 可点击阶段指示器直接跳转
- 速度滑块 0.2x ~ 3.0x 调节
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
