<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>曲柄滑块推力放大机构 | IFR 原理动画</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700;900&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root{--bg:#060a12;--bg2:#0c1220;--fg:#d8e2f0;--muted:#5a6b82;--accent:#00d4e8;--accent-g:rgba(0,212,232,.25);--force:#ff6633;--force-g:rgba(255,102,51,.35);--torque:#3ddc84;--torque-g:rgba(61,220,132,.25);--amber:#fbbf24;--card:rgba(12,18,32,.88);--bdr:rgba(80,100,130,.18);--grid:rgba(25,38,58,.45)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Rajdhani',sans-serif;background:var(--bg);color:var(--fg);min-height:100vh;overflow-x:hidden;display:flex;flex-direction:column}
.mono{font-family:'IBM Plex Mono',monospace}
body::before{content:'';position:fixed;inset:0;background:linear-gradient(rgba(0,212,232,.018)1px,transparent 1px),linear-gradient(90deg,rgba(0,212,232,.018)1px,transparent 1px);background-size:48px 48px;pointer-events:none;z-index:0}
header{position:relative;z-index:2;border-bottom:1px solid var(--bdr);background:var(--card);backdrop-filter:blur(12px);padding:10px 24px;display:flex;align-items:center;gap:16px}
header .logo{width:28px;height:28px;border:2px solid var(--accent);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px;color:var(--accent);font-weight:700}
header h1{font-size:1.15rem;font-weight:700;letter-spacing:.02em}
header h1 span{color:var(--accent)}
.ifr-badge{background:rgba(0,212,232,.08);border:1px solid rgba(0,212,232,.25);border-radius:4px;padding:2px 10px;font-size:.68rem;color:var(--accent);letter-spacing:.06em;text-transform:uppercase}
.main-wrap{flex:1;display:flex;flex-direction:column;position:relative;z-index:1;overflow:hidden}
.svg-container{flex:1;display:flex;align-items:center;justify-content:center;padding:8px 12px;min-height:0}
.svg-container svg{width:100%;max-height:100%;display:block}
.params-row{display:flex;gap:8px;padding:0 16px 8px;flex-wrap:wrap;justify-content:center}
.p-card{background:var(--card);border:1px solid var(--bdr);border-radius:8px;padding:8px 14px;min-width:130px;backdrop-filter:blur(8px);transition:border-color .3s,box-shadow .3s}
.p-card:hover{border-color:var(--accent);box-shadow:0 0 16px var(--accent-g)}
.p-card .lbl{font-size:.62rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.p-card .val{font-family:'IBM Plex Mono',monospace;font-size:1.2rem;font-weight:600;line-height:1.2}
.p-card .val.accent{color:var(--accent)}.p-card .val.force{color:var(--force)}.p-card .val.torque{color:var(--torque)}.p-card .val.amber{color:var(--amber)}
.graph-wrap{padding:0 16px 8px;display:flex;gap:12px;align-items:stretch}
.graph-canvas-wrap{flex:1;background:var(--bg2);border:1px solid var(--bdr);border-radius:8px;overflow:hidden;position:relative;min-height:140px}
.graph-canvas-wrap canvas{width:100%;height:100%;display:block}
.ifr-panel{width:260px;flex-shrink:0;background:var(--card);border:1px solid var(--bdr);border-radius:8px;padding:14px 16px;backdrop-filter:blur(8px);display:flex;flex-direction:column;gap:10px}
.ifr-panel h3{font-size:.82rem;font-weight:700;color:var(--accent);letter-spacing:.04em;text-transform:uppercase}
.ifr-panel p{font-size:.74rem;color:var(--muted);line-height:1.55}
.ifr-panel .key{color:var(--amber);font-weight:600}
.ctrl-bar{background:var(--card);border-top:1px solid var(--bdr);padding:10px 20px;display:flex;align-items:center;gap:16px;flex-wrap:wrap;backdrop-filter:blur(12px)}
.ctrl-btn{background:transparent;border:1px solid var(--bdr);color:var(--fg);width:36px;height:36px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .2s}
.ctrl-btn:hover,.ctrl-btn.active{border-color:var(--accent);color:var(--accent);box-shadow:0 0 10px var(--accent-g)}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{font-size:.7rem;color:var(--muted);white-space:nowrap}
.ctrl-group input[type=range]{-webkit-appearance:none;appearance:none;height:4px;background:var(--bdr);border-radius:2px;outline:none;width:120px}
.ctrl-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 6px var(--accent-g)}
.ctrl-group .val-display{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--accent);min-width:36px;text-align:right}
.sep{width:1px;height:24px;background:var(--bdr)}
@keyframes pulse-zone{0%,100%{opacity:.15}50%{opacity:.4}}
.dead-pulse{animation:pulse-zone 2.5s ease-in-out infinite}
@keyframes dash-flow{to{stroke-dashoffset:-24}}
.dash-flow{animation:dash-flow .8s linear infinite}
@media(prefers-reduced-motion:reduce){.dead-pulse,.dash-flow{animation:none}}
@media(max-width:768px){.ifr-panel{display:none}.graph-wrap{flex-direction:column}.params-row{gap:4px}.p-card{min-width:90px;padding:6px 8px}.p-card .val{font-size:1rem}.ctrl-group input[type=range]{width:80px}}
</style>
</head>
<body>
<header>
<div class="logo"><i class="fa-solid fa-gears" style="font-size:13px"></i></div>
<h1>曲柄滑块<span>推力放大</span>机构</h1>
<div class="ifr-badge">IFR 最终理想解</div>
</header>
<div class="main-wrap">
<div class="svg-container">
<svg id="mechSVG" viewBox="0 0 960 420" preserveAspectRatio="xMidYMid meet">
<defs>
<filter id="glowCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowForce" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glowGreen" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<marker id="arrowForce" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
<polygon points="0,0 10,4 0,8" fill="#ff6633"/>
</marker>
<marker id="arrowTorque" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0,0 8,3 0,6" fill="#3ddc84"/>
</marker>
<marker id="arrowLoad" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0,0 8,3 0,6" fill="#8899aa"/>
</marker>
<linearGradient id="forceGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ff6633" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#ff6633" stop-opacity="0.3"/>
</linearGradient>
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="6" stroke="rgba(80,100,130,0.12)" stroke-width="1"/>
</pattern>
</defs>
<!-- 背景网格 -->
<rect width="960" height="420" fill="url(#hatch)" opacity=".5"/>
<!-- 死点高亮区域 -->
<rect id="deadZone" x="0" y="0" width="0" height="0" rx="4" fill="rgba(255,102,51,0.06)" class="dead-pulse"/>
<text id="deadLabel" x="0" y="0" fill="#ff6633" font-size="10" opacity=".55" text-anchor="middle" font-family="IBM Plex Mono,monospace">死点区域</text>
<!-- 滑轨 -->
<line x1="200" y1="240" x2="820" y2="240" stroke="#1e293b" stroke-width="2.5"/>
<line x1="200" y1="222" x2="820" y2="222" stroke="#1e293b" stroke-width=".6" stroke-dasharray="4,6"/>
<line x1="200" y1="258" x2="820" y2="258" stroke="#1e293b" stroke-width=".6" stroke-dasharray="4,6"/>
<!-- 滑轨刻度 -->
<g id="trackMarks" fill="none" stroke="#1e293b" stroke-width=".5"></g>
<!-- 曲柄销轨迹(虚线圆) -->
<circle id="crankCircle" cx="0" cy="0" r="0" fill="none" stroke="rgba(0,212,232,0.12)" stroke-width="1" stroke-dasharray="4,4"/>
<!-- 电机 -->
<g id="motorGroup" transform="translate(0,0)">
<rect x="-52" y="-28" width="50" height="56" rx="4" fill="#111827" stroke="#475569" stroke-width="1.2"/>
<circle cx="-27" cy="0" r="16" fill="none" stroke="#64748b" stroke-width="1"/>
<text x="-27" y="4" text-anchor="middle" fill="#64748b" font-size="11" font-weight="700" font-family="Rajdhani,sans-serif">M</text>
<line x1="-11" y1="0" x2="0" y2="0" stroke="#64748b" stroke-width="2"/>
</g>
<!-- 减速机 -->
<g id="gearGroup" transform="translate(0,0)">
<rect x="-16" y="-18" width="30" height="36" rx="3" fill="#0f172a" stroke="#475569" stroke-width="1"/>
<circle cx="0" cy="-6" r="5" fill="none" stroke="#475569" stroke-width=".8"/>
<circle cx="0" cy="6" r="7" fill="none" stroke="#475569" stroke-width=".8"/>
<line x1="14" y1="0" x2="22" y2="0" stroke="#64748b" stroke-width="2"/>
</g>
<!-- 曲柄臂 -->
<line id="crankArm" x1="0" y1="0" x2="0" y2="0" stroke="#00d4e8" stroke-width="7" stroke-linecap="round" filter="url(#glowCyan)"/>
<!-- 曲柄中心 -->
<circle id="crankCenter" cx="0" cy="0" r="6" fill="#0c1220" stroke="#00d4e8" stroke-width="2"/>
<line id="ccH" x1="0" y1="0" x2="0" y2="0" stroke="#00d4e8" stroke-width=".8" opacity=".4"/>
<line id="ccV" x1="0" y1="0" x2="0" y2="0" stroke="#00d4e8" stroke-width=".8" opacity=".4"/>
<!-- 曲柄销 -->
<circle id="crankPin" cx="0" cy="0" r="9" fill="#00d4e8" stroke="#0c1220" stroke-width="2.5" cursor="pointer" filter="url(#glowCyan)"/>
<!-- 连杆 -->
<line id="connRod" x1="0" y1="0" x2="0" y2="0" stroke="#7a8da6" stroke-width="5" stroke-linecap="round"/>
<!-- 连杆销(滑块端) -->
<circle id="sliderPin" cx="0" cy="0" r="6" fill="#7a8da6" stroke="#0c1220" stroke-width="1.5"/>
<!-- 滑块 -->
<rect id="sliderBody" x="0" y="0" width="56" height="36" rx="5" fill="#1e293b" stroke="#64748b" stroke-width="1.5"/>
<!-- 滑块花纹 -->
<line id="sl1" x1="0" y1="0" x2="0" y2="0" stroke="#475569" stroke-width="1"/>
<line id="sl2" x1="0" y1="0" x2="0" y2="0" stroke="#475569" stroke-width="1"/>
<!-- 负载块 -->
<rect id="loadBlock" x="0" y="0" width="40" height="50" rx="3" fill="#111827" stroke="#475569" stroke-width="1"/>
<text id="loadLabel" x="0" y="0" text-anchor="middle" fill="#64748b" font-size="9" font-family="Rajdhani,sans-serif">负载</text>
<!-- 负载阻力箭头 -->
<line id="loadArrow" x1="0" y1="0" x2="0" y2="0" stroke="#8899aa" stroke-width="2" marker-end="url(#arrowLoad)" opacity=".6"/>
<!-- 推力箭头 -->
<g id="forceGroup">
<line id="forceLine" x1="0" y1="0" x2="0" y2="0" stroke="url(#forceGrad)" stroke-width="5" marker-end="url(#arrowForce)" filter="url(#glowForce)"/>
<rect id="forceBg" x="0" y="0" width="0" height="0" rx="3" fill="rgba(255,102,51,0.12)"/>
<text id="forceText" x="0" y="0" text-anchor="middle" fill="#ff6633" font-size="15" font-weight="700" font-family="IBM Plex Mono,monospace"></text>
</g>
<!-- 扭矩箭头 -->
<path id="torqueArc" d="" fill="none" stroke="#3ddc84" stroke-width="2.2" marker-end="url(#arrowTorque)" filter="url(#glowGreen)"/>
<text id="torqueText" x="0" y="0" fill="#3ddc84" font-size="10" font-family="IBM Plex Mono,monospace" opacity=".85"></text>
<!-- 角度标注 -->
<path id="alphaArc" d="" fill="none" stroke="#fbbf24" stroke-width="1.5" stroke-dasharray="3,3" class="dash-flow"/>
<text id="alphaText" x="0" y="0" fill="#fbbf24" font-size="12" font-weight="600" font-family="IBM Plex Mono,monospace"></text>
<!-- 曲柄半径标注 -->
<g id="dimR" opacity=".5">
<line id="dimRLine" x1="0" y1="0" x2="0" y2="0" stroke="#00d4e8" stroke-width=".7" stroke-dasharray="3,3"/>
<text id="dimRText" x="0" y="0" fill="#00d4e8" font-size="9" text-anchor="middle" font-family="IBM Plex Mono,monospace">r=60</text>
</g>
<!-- 连杆长度标注 -->
<g id="dimL" opacity=".4">
<line id="dimLLine" x1="0" y1="0" x2="0" y2="0" stroke="#7a8da6" stroke-width=".6" stroke-dasharray="3,3"/>
<text id="dimLText" x="0" y="0" fill="#7a8da6" font-size="9" text-anchor="middle" font-family="IBM Plex Mono,monospace">L=200</text>
</g>
<!-- 运动方向提示 -->
<text id="motionHint" x="0" y="0" fill="var(--accent)" font-size="11" font-family="Rajdhani,sans-serif" opacity=".6"></text>
<!-- 锁定机构示意 -->
<g id="lockGroup" opacity="0">
<rect id="lockRect" x="0" y="0" width="12" height="20" rx="2" fill="#1e293b" stroke="#fbbf24" stroke-width="1"/>
<line id="lockPin" x1="0" y1="0" x2="0" y2="0" stroke="#fbbf24" stroke-width="1.5"/>
</g>
</svg>
</div>
<div class="params-row">
<div class="p-card"><div class="lbl">推力放大倍数</div><div class="val accent" id="pMA">3.7x</div></div>
<div class="p-card"><div class="lbl">输出推力</div><div class="val force" id="pForce">4000 N</div></div>
<div class="p-card"><div class="lbl">电机扭矩</div><div class="val torque" id="pTorque">1081 N·mm</div></div>
<div class="p-card"><div class="lbl">连杆夹角 α</div><div class="val amber" id="pAlpha">15.0°</div></div>
<div class="p-card"><div class="lbl">曲柄偏离死点</div><div class="val" id="pOffset">0.0°</div></div>
<div class="p-card"><div class="lbl">滑块位移</div><div class="val" id="pDisp">0 mm</div></div>
</div>
<div class="graph-wrap">
<div class="graph-canvas-wrap">
<canvas id="forceGraph"></canvas>
</div>
<div class="ifr-panel">
<h3><i class="fa-solid fa-bullseye" style="margin-right:6px"></i>IFR 最终理想解</h3>
<p><span class="key">物理矛盾:</span>大推力(4000N)要求大电机,但空间受限只能用小电机。</p>
<p><span class="key">理想解:</span>用曲柄滑块在<span class="key">死点附近</span>的几何非线性增益,将小扭矩放大为巨大推力——以<span class="key">机构构型</span>替代电机体积。</p>
<p><span class="key">关键资源:</span>连杆与滑轨夹角 α。α→0 时,切向力被极度放大为轴向推力,放大倍数→∞。</p>
<p style="color:var(--accent);font-weight:600;font-size:.78rem;margin-top:4px">系统复杂度仅微增,矛盾被彻底消解。</p>
</div>
</div>
<div class="ctrl-bar">
<button class="ctrl-btn active" id="btnPlay" title="播放/暂停"><i class="fa-solid fa-pause"></i></button>
<button class="ctrl-btn" id="btnReset" title="重置"><i class="fa-solid fa-rotate-left"></i></button>
<div class="sep"></div>
<div class="ctrl-group">
<label>速度</label>
<input type="range" id="sliderSpeed" min="0.1" max="3" step="0.1" value="1">
<span class="val-display" id="valSpeed">1.0x</span>
</div>
<div class="sep"></div>
<div class="ctrl-group">
<label>曲柄角度</label>
<input type="range" id="sliderAngle" min="0" max="100" step="0.5" value="0">
<span class="val-display" id="valAngle">0°</span>
</div>
<div class="sep"></div>
<div class="ctrl-group">
<label>连杆长度</label>
<input type="range" id="sliderRod" min="140" max="280" step="5" value="200">
<span class="val-display" id="valRod">200</span>
</div>
<div class="sep"></div>
<button class="ctrl-btn" id="btnLock" title="锁定机构"><i class="fa-solid fa-lock-open"></i></button>
<span style="font-size:.65rem;color:var(--muted);margin-left:auto">拖拽曲柄销可手动控制</span>
</div>
</div>
<script>
/* ========== 配置 ========== */
const CFG = {
crankR: 65,
rodL: 200,
cx: 240, // 曲柄中心X
cy: 240, // 曲柄中心Y
baseForce: 4000,
// 推程角度范围: θ从 π+startOff 到 2π-endOff
startOff: 0.06,
endOff: 0.12,
};
/* ========== 状态 ========== */
const S = {
theta: Math.PI + CFG.startOff,
playing: true,
speed: 1,
phase: 'push', // push | return
dragging: false,
manualAngle: false,
locked: false,
rodL: CFG.rodL,
history: [], // 力曲线历史
maxHistory: 300,
};
/* ========== DOM引用 ========== */
const $ = id => document.getElementById(id);
const svg = $('mechSVG');
/* ========== 机构计算 ========== */
function calcMech(theta, rodL) {
const r = CFG.crankR, L = rodL || S.rodL;
const cx = CFG.cx, cy = CFG.cy;
const cpx = cx + r * Math.cos(theta);
const cpy = cy - r * Math.sin(theta);
const dy = cpy - cy;
const hDist = Math.sqrt(Math.max(0.01, L * L - dy * dy));
const sx = cpx + hDist;
const alpha = Math.atan2(Math.abs(dy), hDist);
// 数值微分求力放大
const dt = 0.0004;
const t2 = theta + dt;
const cpx2 = cx + r * Math.cos(t2);
const cpy2 = cy - r * Math.sin(t2);
const dy2 = cpy2 - cy;
const hDist2 = Math.sqrt(Math.max(0.01, L * L - dy2 * dy2));
const sx2 = cpx2 + hDist2;
const dxdt = (sx2 - sx) / dt;
const MA = Math.abs(r / (dxdt || 0.0001));
const clampedMA = Math.min(MA, 40);
const minExt = cx - r + L;
const displacement = sx - minExt;
const maxDisp = 2 * r;
// 推力(归一化到 baseForce)
const peakMA = calcPeakMA(L);
const forceNorm = clampedMA / peakMA;
const force = CFG.baseForce * forceNorm;
return { cpx, cpy, sx, sy: cy, alpha, MA: clampedMA, displacement, maxDisp, theta, force, forceNorm, hDist, minExt };
}
function calcPeakMA(rodL) {
// 在起始角度处的MA
const d = calcMechInternal(Math.PI + CFG.startOff, rodL);
return d.MA;
}
function calcMechInternal(theta, rodL) {
const r = CFG.crankR, L = rodL;
const cx = CFG.cx, cy = CFG.cy;
const cpx = cx + r * Math.cos(theta);
const cpy = cy - r * Math.sin(theta);
const dy = cpy - cy;
const hDist = Math.sqrt(Math.max(0.01, L * L - dy * dy));
const sx = cpx + hDist;
const alpha = Math.atan2(Math.abs(dy), hDist);
const dt = 0.0004;
const t2 = theta + dt;
const cpx2 = cx + r * Math.cos(t2);
const cpy2 = cy - r * Math.sin(t2);
const dy2 = cpy2 - cy;
const hDist2 = Math.sqrt(Math.max(0.01, L * L - dy2 * dy2));
const sx2 = cpx2 + hDist2;
const dxdt = (sx2 - sx) / dt;
const MA = Math.min(Math.abs(r / (dxdt || 0.0001)), 40);
return { MA, alpha, sx, displacement: sx - (cx - r + L) };
}
/* ========== SVG更新 ========== */
function updateSVG(d) {
const cx = CFG.cx, cy = CFG.cy, r = CFG.crankR;
// 曲柄销轨迹圆
const cc = $('crankCircle');
cc.setAttribute('cx', cx); cc.setAttribute('cy', cy); cc.setAttribute('r', r);
// 电机
const mg = $('motorGroup');
mg.setAttribute('transform', `translate(${cx - 40},${cy})`);
// 减速机
const gg = $('gearGroup');
gg.setAttribute('transform', `translate(${cx - 18},${cy})`);
// 曲柄臂
const ca = $('crankArm');
ca.setAttribute('x1', cx); ca.setAttribute('y1', cy);
ca.setAttribute('x2', d.cpx); ca.setAttribute('y2', d.cpy);
// 曲柄中心
const ccEl = $('crankCenter');
ccEl.setAttribute('cx', cx); ccEl.setAttribute('cy', cy);
$('ccH').setAttribute('x1', cx - 10); $('ccH').setAttribute('y1', cy);
$('ccH').setAttribute('x2', cx + 10); $('ccH').setAttribute('y2', cy);
$('ccV').setAttribute('x1', cx); $('ccV').setAttribute('y1', cy - 10);
$('ccV').setAttribute('x2', cx); $('ccV').setAttribute('y2', cy + 10);
// 曲柄销
const cp = $('crankPin');
cp.setAttribute('cx', d.cpx); cp.setAttribute('cy', d.cpy);
// 连杆
const cr = $('connRod');
cr.setAttribute('x1', d.cpx); cr.setAttribute('y1', d.cpy);
cr.setAttribute('x2', d.sx); cr.setAttribute('y2', d.sy);
// 连杆颜色根据力放大变化
const intensity = Math.min(d.forceNorm * 1.5, 1);
const rodColor = `rgb(${122 + 133 * intensity},${141 - 39 * intensity},${166 - 115 * intensity})`;
cr.setAttribute('stroke', rodColor);
// 滑块销
const sp = $('sliderPin');
sp.setAttribute('cx', d.sx); sp.setAttribute('cy', d.sy);
// 滑块体
const sb = $('sliderBody');
const sw = 56, sh = 36;
sb.setAttribute('x', d.sx - 8); sb.setAttribute('y', d.sy - sh / 2);
sb.setAttribute('width', sw); sb.setAttribute('height', sh);
// 滑块花纹
const sl1 = $('sl1'), sl2 = $('sl2');
sl1.setAttribute('x1', d.sx + 8); sl1.setAttribute('y1', d.sy - 12);
sl1.setAttribute('x2', d.sx + 8); sl1.setAttribute('y2', d.sy + 12);
sl2.setAttribute('x1', d.sx + 18); sl2.setAttribute('y1', d.sy - 12);
sl2.setAttribute('x2', d.sx + 18); sl2.setAttribute('y2', d.sy + 12);
// 负载块
const lb = $('loadBlock');
const lbx = d.sx + 52;
lb.setAttribute('x', lbx); lb.setAttribute('y', d.sy - 25);
$('loadLabel').setAttribute('x', lbx + 20); $('loadLabel').setAttribute('y', d.sy + 4);
// 负载阻力箭头
const la = $('loadArrow');
const loadForce = 4000 * Math.exp(-d.displacement / 55) + 400;
const loadArrowLen = Math.max(15, Math.min(80, loadForce / 60));
la.setAttribute('x1', lbx + 48 + loadArrowLen); la.setAttribute('y1', d.sy);
la.setAttribute('x2', lbx + 48); la.setAttribute('y2', d.sy);
// 推力箭头
const forceLen = Math.max(10, Math.min(180, d.force / 25));
const fl = $('forceLine');
fl.setAttribute('x1', d.sx + 50); fl.setAttribute('y1', d.sy);
fl.setAttribute('x2', d.sx + 50 + forceLen); fl.setAttribute('y2', d.sy);
const ft = $('forceText');
ft.textContent = Math.round(d.force) + ' N';
const ftx = d.sx + 50 + forceLen / 2;
ft.setAttribute('x', ftx); ft.setAttribute('y', d.sy - 14);
const fb = $('forceBg');
const ftWidth = ft.textContent.length * 9 + 12;
fb.setAttribute('x', ftx - ftWidth / 2); fb.setAttribute('y', d.sy - 28);
fb.setAttribute('width', ftWidth); fb.setAttribute('height', 18);
// 扭矩箭头
const ta = $('torqueArc');
const taR = 28;
const taStart = -Math.PI * 0.3;
const taEnd = Math.PI * 0.3;
const taX1 = cx + taR * Math.cos(taStart);
const taY1 = cy + taR * Math.sin(taStart);
const taX2 = cx + taR * Math.cos(taEnd);
const taY2 = cy + taR * Math.sin(taEnd);
ta.setAttribute('d', `M ${taX1},${taY1} A ${taR},${taR} 0 0,1 ${taX2},${taY2}`);
const tt = $('torqueText');
const motorTorque = Math.round(d.force / d.MA);
tt.textContent = motorTorque + ' N·mm';
tt.setAttribute('x', cx - 28); tt.setAttribute('y', cy - 34);
// 角度弧(连杆夹角 α)
const aa = $('alphaArc');
if (d.alpha > 0.02) {
const arcR = 40;
const startA = 0; // 水平方向
const endA = -d.alpha; // 连杆方向(向上为负)
const ax1 = d.sx + arcR * Math.cos(startA);
const ay1 = d.sy + arcR * Math.sin(startA);
const ax2 = d.sx + arcR * Math.cos(endA);
const ay2 = d.sy + arcR * Math.sin(endA);
// 从水平到连杆方向的弧
const rodAngle = Math.atan2(d.cpy - d.sy, d.cpx - d.sx);
const ax1b = d.sx + arcR;
const ay1b = d.sy;
const ax2b = d.sx + arcR * Math.cos(rodAngle);
const ay2b = d.sy + arcR * Math.sin(rodAngle);
const largeArc = Math.abs(d.alpha) > Math.PI ? 1 : 0;
const sweep = rodAngle < 0 ? 1 : 0;
aa.setAttribute('d', `M ${ax1b},${ay1b} A ${arcR},${arcR} 0 ${largeArc},${sweep} ${ax2b},${ay2b}`);
} else {
aa.setAttribute('d', '');
}
const at = $('alphaText');
const alphaDeg = (d.alpha * 180 / Math.PI).toFixed(1);
at.textContent = 'α=' + alphaDeg + '°';
const labelAngle = Math.atan2(d.cpy - d.sy, d.cpx - d.sx) / 2;
at.setAttribute('x', d.sx + 52 * Math.cos(labelAngle) - 10);
at.setAttribute('y', d.sy + 52 * Math.sin(labelAngle) - 4);
// 死点区域
const dz = $('deadZone');
const dzLeft = cx - r + S.rodL - 15;
const dzRight = dzLeft + 30;
dz.setAttribute('x', dzLeft); dz.setAttribute('y', cy - 30);
dz.setAttribute('width', 30); dz.setAttribute('height', 60);
const dl = $('deadLabel');
dl.setAttribute('x', dzLeft + 15); dl.setAttribute('y', cy - 36);
// 曲柄半径标注
const drl = $('dimRLine');
const drMidX = (cx + d.cpx) / 2;
const drMidY = (cy + d.cpy) / 2 + 16;
drl.setAttribute('x1', cx); drl.setAttribute('y1', cy + 14);
drl.setAttribute('x2', d.cpx); drl.setAttribute('y2', d.cpy + 14);
const drt = $('dimRText');
drt.setAttribute('x', drMidX); drt.setAttribute('y', drMidY + 10);
// 连杆长度标注
const dll = $('dimLLine');
const dlMidX = (d.cpx + d.sx) / 2;
const dlMidY = (d.cpy + d.sy) / 2 - 16;
dll.setAttribute('x1', d.cpx); dll.setAttribute('y1', d.cpy - 10);
dll.setAttribute('x2', d.sx); dll.setAttribute('y2', d.sy - 10);
const dlt = $('dimLText');
dlt.setAttribute('x', dlMidX); dlt.setAttribute('y', dlMidY - 6);
// 运动方向提示
const mh = $('motionHint');
if (S.phase === 'push') {
mh.textContent = '推程 →';
mh.setAttribute('x', d.sx + 20); mh.setAttribute('y', d.sy + 40);
} else {
mh.textContent = '← 回程';
mh.setAttribute('x', d.sx + 20); mh.setAttribute('y', d.sy + 40);
}
// 锁定机构
const lg = $('lockGroup');
if (S.locked) {
lg.setAttribute('opacity', '1');
const lr = $('lockRect');
lr.setAttribute('x', d.sx - 14); lr.setAttribute('y', d.sy - 42);
const lp = $('lockPin');
lp.setAttribute('x1', d.sx - 8); lp.setAttribute('y1', d.sy - 22);
lp.setAttribute('x2', d.sx - 8); lp.setAttribute('y2', d.sy - 18);
} else {
lg.setAttribute('opacity', '0');
}
}
/* ========== 参数卡片更新 ========== */
function updateParams(d) {
const peakMA = calcPeakMA(S.rodL);
const maDisplay = (d.MA / peakMA * 3.7).toFixed(1);
$('pMA').textContent = maDisplay + 'x';
$('pForce').textContent = Math.round(d.force) + ' N';
const motorT = Math.round(d.force / Math.max(d.MA, 0.1));
$('pTorque').textContent = motorT + ' N·mm';
$('pAlpha').textContent = (d.alpha * 180 / Math.PI).toFixed(1) + '°';
const offset = ((d.theta - Math.PI) * 180 / Math.PI);
$('pOffset').textContent = offset.toFixed(1) + '°';
$('pDisp').textContent = Math.round(d.displacement * 120 / (2 * CFG.crankR)) + ' mm';
// 力值颜色强度
const fi = Math.min(d.forceNorm * 1.5, 1);
$('pForce').style.textShadow = `0 0 ${8 + 12 * fi}px rgba(255,102,51,${0.3 + 0.5 * fi})`;
}
/* ========== 力曲线图绘制 ========== */
const graphCanvas = $('forceGraph');
const gCtx = graphCanvas.getContext('2d');
function resizeGraph() {
const wrap = graphCanvas.parentElement;
const rect = wrap.getBoundingClientRect();
graphCanvas.width = rect.width * window.devicePixelRatio;
graphCanvas.height = rect.height * window.devicePixelRatio;
gCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
function drawGraph(currentDisp, currentForce) {
const w = graphCanvas.width / window.devicePixelRatio;
const h = graphCanvas.height / window.devicePixelRatio;
gCtx.clearRect(0, 0, w, h);
const pad = { l: 50, r: 20, t: 16, b: 30 };
const gw = w - pad.l - pad.r;
const gh = h - pad.t - pad.b;
const maxDisp = 130; // mm
const maxForce = 5500;
// 坐标轴
gCtx.strokeStyle = '#1e293b';
gCtx.lineWidth = 1;
gCtx.beginPath();
gCtx.moveTo(pad.l, pad.t);
gCtx.lineTo(pad.l, pad.t + gh);
gCtx.lineTo(pad.l + gw, pad.t + gh);
gCtx.stroke();
// 网格
gCtx.strokeStyle = 'rgba(30,41,59,0.5)';
gCtx.lineWidth = 0.5;
for (let i = 1; i <= 4; i++) {
const y = pad.t + gh - (i / 4) * gh;
gCtx.beginPath(); gCtx.moveTo(pad.l, y); gCtx.lineTo(pad.l + gw, y); gCtx.stroke();
}
for (let i = 1; i <= 5; i++) {
const x = pad.l + (i / 5) * gw;
gCtx.beginPath(); gCtx.moveTo(x, pad.t); gCtx.lineTo(x, pad.t + gh); gCtx.stroke();
}
// Y轴标签
gCtx.fillStyle = '#5a6b82';
gCtx.font = '10px IBM Plex Mono, monospace';
gCtx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const val = Math.round(maxForce * i / 4);
const y = pad.t + gh - (i / 4) * gh;
gCtx.fillText(val + '', pad.l - 6, y + 3);
}
// X轴标签
gCtx.textAlign = 'center';
for (let i = 0; i <= 5; i++) {
const val = Math.round(maxDisp * i / 5);
const x = pad.l + (i / 5) * gw;
gCtx.fillText(val + 'mm', x, pad.t + gh + 16);
}
// 轴标题
gCtx.fillStyle = '#5a6b82';
gCtx.font = '10px Rajdhani, sans-serif';
gCtx.textAlign = 'center';
gCtx.fillText('滑块位移', pad.l + gw / 2, h - 2);
gCtx.save();
gCtx.translate(12, pad.t + gh / 2);
gCtx.rotate(-Math.PI / 2);
gCtx.fillText('推力 (N)', 0, 0);
gCtx.restore();
// 负载阻力曲线(灰色虚线)
gCtx.strokeStyle = '#64748b';
gCtx.lineWidth = 1.5;
gCtx.setLineDash([5, 4]);
gCtx.beginPath();
for (let i = 0; i <= 100; i++) {
const disp = (i / 100) * maxDisp;
const load = 4000 * Math.exp(-disp / 55) + 400;
const x = pad.l + (disp / maxDisp) * gw;
const y = pad.t + gh - (load / maxForce) * gh;
if (i === 0) gCtx.moveTo(x, y); else gCtx.lineTo(x, y);
}
gCtx.stroke();
gCtx.setLineDash([]);
// 负载标签
gCtx.fillStyle = '#64748b';
gCtx.font = '9px Rajdhani, sans-serif';
gCtx.fillText('负载阻力', pad.l + gw * 0.65, pad.t + gh * 0.35);
// 机构推力曲线(橙色)
gCtx.strokeStyle = '#ff6633';
gCtx.lineWidth = 2.5;
gCtx.shadowColor = 'rgba(255,102,51,0.3)';
gCtx.shadowBlur = 8;
gCtx.beginPath();
const peakMA = calcPeakMA(S.rodL);
for (let i = 0; i <= 200; i++) {
const t = i / 200;
const theta = Math.PI + CFG.startOff + t * (Math.PI - CFG.startOff - CFG.endOff);
const md = calcMechInternal(theta, S.rodL);
const disp = md.displacement * 120 / (2 * CFG.crankR);
const force = CFG.baseForce * (md.MA / peakMA);
if (disp < 0 || disp > maxDisp) continue;
const x = pad.l + (disp / maxDisp) * gw;
const y = pad.t + gh - (Math.min(force, maxForce) / maxForce) * gh;
if (i === 0) gCtx.moveTo(x, y); else gCtx.lineTo(x, y);
}
gCtx.stroke();
gCtx.shadowBlur = 0;
// 推力标签
gCtx.fillStyle = '#ff6633';
gCtx.font = '9px Rajdhani, sans-serif';
gCtx.fillText('机构推力', pad.l + gw * 0.12, pad.t + 14);
// 当前位置标记
const dispMM = currentDisp * 120 / (2 * CFG.crankR);
if (dispMM >= 0 && dispMM <= maxDisp) {
const cx = pad.l + (dispMM / maxDisp) * gw;
const cy = pad.t + gh - (Math.min(currentForce, maxForce) / maxForce) * gh;
// 竖线
gCtx.strokeStyle = 'rgba(0,212,232,0.3)';
gCtx.lineWidth = 1;
gCtx.setLineDash([3, 3]);
gCtx.beginPath(); gCtx.moveTo(cx, pad.t); gCtx.lineTo(cx, pad.t + gh); gCtx.stroke();
gCtx.setLineDash([]);
// 圆点
gCtx.fillStyle = '#00d4e8';
gCtx.shadowColor = 'rgba(0,212,232,0.5)';
gCtx.shadowBlur = 10;
gCtx.beginPath(); gCtx.arc(cx, cy, 5, 0, Math.PI * 2); gCtx.fill();
gCtx.shadowBlur = 0;
// 外圈
gCtx.strokeStyle = '#00d4e8';
gCtx.lineWidth = 1.5;
gCtx.beginPath(); gCtx.arc(cx, cy, 9, 0, Math.PI * 2); gCtx.stroke();
}
}
/* ========== 动画循环 ========== */
let lastTime = 0;
function animate(time) {
if (!lastTime) lastTime = time;
const dt = (time - lastTime) / 1000;
lastTime = time;
if (S.playing && !S.dragging && !S.locked) {
const baseSpeed = 0.6;
const dTheta = baseSpeed * S.speed * dt;
if (S.phase === 'push') {
S.theta += dTheta;
if (S.theta >= Math.PI * 2 - CFG.endOff) {
S.theta = Math.PI * 2 - CFG.endOff;
S.phase = 'return';
}
} else {
S.theta -= dTheta * 2.5; // 回程更快
if (S.theta <= Math.PI + CFG.startOff) {
S.theta = Math.PI + CFG.startOff;
S.phase = 'push';
}
}
}
const d = calcMech(S.theta, S.rodL);
updateSVG(d);
updateParams(d);
drawGraph(d.displacement, d.force);
// 更新角度滑块
if (!S.dragging) {
const anglePct = ((S.theta - Math.PI) / Math.PI) * 100;
$('sliderAngle').value = Math.max(0, Math.min(100, anglePct));
$('valAngle').textContent = ((S.theta - Math.PI) * 180 / Math.PI).toFixed(0) + '°';
}
requestAnimationFrame(animate);
}
/* ========== 交互 ========== */
// 播放/暂停
$('btnPlay').addEventListener('click', () => {
S.playing = !S.playing;
$('btnPlay').innerHTML = S.playing ? '<i class="fa-solid fa-pause"></i>' : '<i class="fa-solid fa-play"></i>';
$('btnPlay').classList.toggle('active', S.playing);
});
// 重置
$('btnReset').addEventListener('click', () => {
S.theta = Math.PI + CFG.startOff;
S.phase = 'push';
S.playing = true;
$('btnPlay').innerHTML = '<i class="fa-solid fa-pause"></i>';
$('btnPlay').classList.add('active');
});
// 速度
$('sliderSpeed').addEventListener('input', e => {
S.speed = parseFloat(e.target.value);
$('valSpeed').textContent = S.speed.toFixed(1) + 'x';
});
// 角度
$('sliderAngle').addEventListener('input', e => {
const pct = parseFloat(e.target.value) / 100;
S.theta = Math.PI + pct * Math.PI;
S.phase = pct > 0.9 ? 'return' : 'push';
$('valAngle').textContent = (pct * 180).toFixed(0) + '°';
});
// 连杆长度
$('sliderRod').addEventListener('input', e => {
S.rodL = parseInt(e.target.value);
$('valRod').textContent = S.rodL;
});
// 锁定
$('btnLock').addEventListener('click', () => {
S.locked = !S.locked;
$('btnLock').innerHTML = S.locked ? '<i class="fa-solid fa-lock"></i>' : '<i class="fa-solid fa-lock-open"></i>';
$('btnLock').classList.toggle('active', S.locked);
});
// 曲柄销拖拽
function getSVGCoords(evt) {
const pt = svg.createSVGPoint();
const src = evt.touches ? evt.touches[0] : evt;
pt.x = src.clientX; pt.y = src.clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
function startDrag(evt) {
evt.preventDefault();
S.dragging = true;
S.playing = false;
$('btnPlay').innerHTML = '<i class="fa-solid fa-play"></i>';
$('btnPlay').classList.remove('active');
}
function moveDrag(evt) {
if (!S.dragging) return;
evt.preventDefault();
const pt = getSVGCoords(evt);
const dx = pt.x - CFG.cx;
const dy = -(pt.y - CFG.cy); // 翻转Y
let angle = Math.atan2(dy, dx);
// 限制在推程范围
if (angle < 0) angle += Math.PI * 2;
if (angle < Math.PI + CFG.startOff) angle = Math.PI + CFG.startOff;
if (angle > Math.PI * 2 - CFG.endOff) angle = Math.PI * 2 - CFG.endOff;
S.theta = angle;
}
function endDrag() {
S.dragging = false;
}
$('crankPin').addEventListener('mousedown', startDrag);
$('crankPin').addEventListener('touchstart', startDrag, { passive: false });
svg.addEventListener('mousemove', moveDrag);
svg.addEventListener('touchmove', moveDrag, { passive: false });
window.addEventListener('mouseup', endDrag);
window.addEventListener('touchend', endDrag);
// 窗口大小
function handleResize() {
resizeGraph();
}
window.addEventListener('resize', handleResize);
/* ========== 初始化 ========== */
function init() {
resizeGraph();
// 生成滑轨刻度
const marks = $('trackMarks');
for (let i = 0; i <= 12; i++) {
const x = 300 + i * 40;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', 237);
line.setAttribute('x2', x); line.setAttribute('y2', 243);
marks.appendChild(line);
}
requestAnimationFrame(animate);
}
init();
</script>
</body>
</html>
实现说明
这是一个完整的曲柄滑块推力放大机构 IFR 原理动画,核心亮点如下:
机构仿真
- 精确计算曲柄滑块运动学:通过数值微分求解力放大倍数(MA),在死点附近 MA 趋近无穷大,随曲柄转角增大自然衰减
- 推力归一化到 4000N 基准,实时显示输出推力、电机扭矩、连杆夹角 α、滑块位移等参数
IFR 视觉叙事
- 死点区域高亮:脉冲发光标注死点区域,引导关注核心创新点
- 力箭头动态缩放:橙色推力箭头长度随力值变化,死点附近箭头最长、发光最强
- 连杆颜色渐变:连杆颜色随力放大强度从蓝灰渐变为橙红,直观反映力学状态
- 角度弧线:琥珀色虚线弧标注连杆夹角 α,带流动动画暗示力的传递方向
- 对比箭头:绿色小扭矩弧 vs 橙色大推力箭头,视觉上直接呈现"以小撬大"
力曲线图
- 橙色实线:机构推力随位移的衰减曲线(初始极高,自然下降)
- 灰色虚线:负载阻力曲线(同样衰减),两者匹配说明方案可行性
- 青色圆点:实时跟踪当前工作点
交互控制
- 拖拽曲柄销:直接用鼠标/触摸拖拽曲柄销旋转,手动探索不同角度的力放大效果
- 角度/速度/连杆长度滑块:调节关键变量,观察力放大特性变化
- 锁定机构:模拟滑块到达位置后的机械锁定
- 播放/暂停/重置:完整的动画控制
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
