<!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>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;600;700&family=IBM+Plex+Mono:wght@300;400;500&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: #070c18;
--fg: #dce4f0;
--muted: #3e5068;
--accent: #ff9500;
--accent2: #00e5a0;
--accent3: #ff3860;
--card: #0d1528;
--border: #162240;
--frame1: #2c4466;
--frame2: #3a5a80;
}
* { 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;
overflow-x: hidden;
}
/* 背景装饰 */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 60% 40% at 30% 20%, rgba(0,229,160,0.04) 0%, transparent 70%),
radial-gradient(ellipse 50% 50% at 75% 70%, rgba(255,149,0,0.04) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 1440px;
margin: 0 auto;
width: 100%;
}
/* 顶部标题 */
header {
padding: 18px 30px 10px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: baseline;
gap: 20px;
flex-wrap: wrap;
background: linear-gradient(180deg, rgba(13,21,40,0.95) 0%, transparent 100%);
}
header h1 {
font-family: 'Rajdhani', sans-serif;
font-weight: 700;
font-size: clamp(20px, 3vw, 30px);
letter-spacing: 2px;
color: var(--fg);
text-transform: uppercase;
}
header h1 span { color: var(--accent); }
header .subtitle {
font-size: 12px;
color: var(--accent2);
letter-spacing: 1px;
font-weight: 400;
}
/* 主体布局 */
.main-area {
flex: 1;
display: flex;
gap: 0;
min-height: 0;
}
/* SVG 容器 */
.svg-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
min-width: 0;
}
.svg-wrap svg {
width: 100%;
max-width: 660px;
height: auto;
max-height: calc(100vh - 170px);
}
/* 右侧信息面板 */
.info-panel {
width: 280px;
min-width: 240px;
padding: 18px 16px;
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 14px;
overflow-y: auto;
background: var(--card);
}
.info-section {
background: rgba(22,34,64,0.5);
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px 14px;
}
.info-section .label {
font-size: 10px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 6px;
}
.info-section .value {
font-family: 'Rajdhani', sans-serif;
font-size: 22px;
font-weight: 600;
color: var(--accent);
}
.info-section .value.green { color: var(--accent2); }
.info-section .value.red { color: var(--accent3); }
.info-section .desc {
font-size: 11px;
color: #7a8fa8;
line-height: 1.6;
margin-top: 4px;
}
/* 阶段指示器 */
.phase-indicator {
display: flex;
gap: 6px;
align-items: center;
}
.phase-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--muted);
transition: all 0.3s;
}
.phase-dot.active {
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
}
.phase-dot.done {
background: var(--accent2);
}
.phase-line {
flex: 1;
height: 2px;
background: var(--muted);
transition: background 0.3s;
}
.phase-line.done { background: var(--accent2); }
/* 参数标签 */
.param-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
border-bottom: 1px solid rgba(30,48,80,0.5);
}
.param-row:last-child { border-bottom: none; }
.param-name { font-size: 11px; color: #7a8fa8; }
.param-val { font-size: 13px; font-weight: 500; color: var(--fg); }
.param-val .unit { font-size: 10px; color: var(--muted); margin-left: 2px; }
/* IFR 标注 */
.ifr-box {
border-left: 3px solid var(--accent2);
padding-left: 10px;
}
.ifr-box .title {
font-size: 11px;
font-weight: 500;
color: var(--accent2);
margin-bottom: 4px;
}
.ifr-box .text {
font-size: 10.5px;
color: #8a9eb8;
line-height: 1.6;
}
/* 底部控制栏 */
.controls {
padding: 12px 24px 16px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
background: var(--card);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 18px;
border: 1px solid var(--border);
border-radius: 5px;
background: rgba(22,34,64,0.6);
color: var(--fg);
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:active { transform: scale(0.96); }
.btn.primary {
background: rgba(255,149,0,0.15);
border-color: var(--accent);
color: var(--accent);
}
.btn.primary:hover { background: rgba(255,149,0,0.25); }
.slider-wrap {
flex: 1;
min-width: 120px;
display: flex;
align-items: center;
gap: 10px;
}
.slider-wrap label {
font-size: 10px;
color: var(--muted);
white-space: nowrap;
letter-spacing: 1px;
text-transform: uppercase;
}
.slider-wrap input[type="range"] {
flex: 1;
-webkit-appearance: none;
height: 4px;
background: var(--border);
border-radius: 2px;
outline: none;
}
.slider-wrap input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 6px rgba(255,149,0,0.4);
}
.speed-group {
display: flex;
gap: 4px;
}
.speed-btn {
padding: 5px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
color: var(--muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.speed-btn.active {
border-color: var(--accent2);
color: var(--accent2);
background: rgba(0,229,160,0.08);
}
.speed-btn:hover { border-color: var(--accent2); color: var(--accent2); }
/* 响应式 */
@media (max-width: 900px) {
.main-area { flex-direction: column; }
.info-panel {
width: 100%;
min-width: 0;
border-left: none;
border-top: 1px solid var(--border);
flex-direction: row;
flex-wrap: wrap;
padding: 12px;
gap: 10px;
}
.info-section { flex: 1; min-width: 200px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>单动力源<span>绳轮联动</span>多级伸缩机构</h1>
<span class="subtitle">IFR 理想解原理动画演示</span>
</header>
<div class="main-area">
<div class="svg-wrap">
<svg id="mechSvg" viewBox="0 0 660 820" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 绳索发光滤镜 -->
<filter id="ropeGlow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3.5" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- 脉冲发光 -->
<filter id="pulseGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- 框架渐变 -->
<linearGradient id="f1Grad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3550"/>
<stop offset="35%" stop-color="#2c4a6a"/>
<stop offset="65%" stop-color="#2c4a6a"/>
<stop offset="100%" stop-color="#1e3550"/>
</linearGradient>
<linearGradient id="f2Grad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#264060"/>
<stop offset="35%" stop-color="#38608a"/>
<stop offset="65%" stop-color="#38608a"/>
<stop offset="100%" stop-color="#264060"/>
</linearGradient>
<linearGradient id="baseGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a2840"/>
<stop offset="100%" stop-color="#0e1a2c"/>
</linearGradient>
<!-- 网格图案 -->
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
<path d="M 30 0 L 0 0 0 30" fill="none" stroke="rgba(30,50,80,0.25)" stroke-width="0.5"/>
</pattern>
<!-- 限位标记渐变 -->
<linearGradient id="limitGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgba(255,56,96,0.6)"/>
<stop offset="100%" stop-color="rgba(255,56,96,0)"/>
</linearGradient>
</defs>
<!-- 背景 -->
<rect width="660" height="820" fill="#070c18"/>
<rect width="660" height="820" fill="url(#grid)"/>
<!-- 地面 -->
<rect x="40" y="700" width="580" height="4" fill="#1a2a42" rx="1"/>
<line x1="40" y1="704" x2="620" y2="704" stroke="#0e1825" stroke-width="8"/>
<!-- 底座 -->
<rect x="160" y="650" width="340" height="50" rx="4" fill="url(#baseGrad)" stroke="#2a4060" stroke-width="1.5"/>
<text x="330" y="680" text-anchor="middle" fill="#4a6a8a" font-size="11" font-family="IBM Plex Mono">底座 BASE</text>
<!-- 导轨(固定) -->
<line x1="215" y1="140" x2="215" y2="650" stroke="#152540" stroke-width="2" stroke-dasharray="6,4"/>
<line x1="445" y1="140" x2="445" y2="650" stroke="#152540" stroke-width="2" stroke-dasharray="6,4"/>
<line x1="215" y1="140" x2="445" y2="140" stroke="#152540" stroke-width="2" stroke-dasharray="6,4"/>
<!-- 限位标记 -->
<g id="limitMark">
<rect x="210" y="145" width="6" height="30" rx="2" fill="url(#limitGrad)"/>
<rect x="444" y="145" width="6" height="30" rx="2" fill="url(#limitGrad)"/>
<text x="200" y="165" text-anchor="end" fill="rgba(255,56,96,0.6)" font-size="9" font-family="IBM Plex Mono">限位</text>
</g>
<!-- 顶部定滑轮(固定在导轨顶部) -->
<g id="topPulley">
<circle cx="330" cy="142" r="14" fill="#0b1825" stroke="#00e5a0" stroke-width="2"/>
<circle cx="330" cy="142" r="3" fill="#00e5a0"/>
<line id="topPulleyMark" x1="330" y1="130" x2="330" y2="135" stroke="#00e5a0" stroke-width="1.5"/>
<text x="330" y="122" text-anchor="middle" fill="#00e5a0" font-size="9" font-family="IBM Plex Mono" opacity="0.7">顶部定滑轮</text>
</g>
<!-- 一级框架 -->
<g id="frame1Group">
<rect id="f1Body" x="230" y="420" width="200" height="280" rx="3" fill="url(#f1Grad)" stroke="#3a6090" stroke-width="1.5"/>
<!-- 顶部横梁 -->
<rect id="f1Cap" x="225" y="416" width="210" height="8" rx="2" fill="#2a4a6e" stroke="#4a7aaa" stroke-width="1"/>
<text id="f1Label" x="330" y="560" text-anchor="middle" fill="#6a9ac0" font-size="12" font-family="IBM Plex Mono" font-weight="500">一级框架</text>
<!-- 左定滑轮 -->
<g id="fpLeft">
<circle cx="248" cy="440" r="10" fill="#0b1825" stroke="#00e5a0" stroke-width="1.8"/>
<circle cx="248" cy="440" r="2.5" fill="#00e5a0"/>
<line id="fpLeftMark" x1="248" y1="432" x2="248" y2="435" stroke="#00e5a0" stroke-width="1.2"/>
</g>
<!-- 右定滑轮 -->
<g id="fpRight">
<circle cx="412" cy="440" r="10" fill="#0b1825" stroke="#00e5a0" stroke-width="1.8"/>
<circle cx="412" cy="440" r="2.5" fill="#00e5a0"/>
<line id="fpRightMark" x1="412" y1="432" x2="412" y2="435" stroke="#00e5a0" stroke-width="1.2"/>
</g>
</g>
<!-- 二级框架 -->
<g id="frame2Group">
<rect id="f2Body" x="265" y="460" width="130" height="200" rx="3" fill="url(#f2Grad)" stroke="#4a80b0" stroke-width="1.5"/>
<rect id="f2Cap" x="260" y="456" width="140" height="8" rx="2" fill="#305878" stroke="#5a9ac0" stroke-width="1"/>
<text id="f2Label" x="330" y="565" text-anchor="middle" fill="#8abce0" font-size="12" font-family="IBM Plex Mono" font-weight="500">二级框架</text>
<!-- 动滑轮 -->
<g id="mpGroup">
<circle cx="330" cy="648" r="10" fill="#0b1825" stroke="#ff9500" stroke-width="2"/>
<circle cx="330" cy="648" r="2.5" fill="#ff9500"/>
<line id="mpMark" x1="330" y1="640" x2="330" y2="643" stroke="#ff9500" stroke-width="1.2"/>
</g>
</g>
<!-- 电机 -->
<g id="motorGroup">
<rect x="295" y="660" width="70" height="36" rx="4" fill="#1a1020" stroke="#ff3860" stroke-width="1.5"/>
<circle cx="330" cy="678" r="13" fill="#120a18" stroke="#ff3860" stroke-width="1.5"/>
<line id="motorMark1" x1="330" y1="667" x2="330" y2="673" stroke="#ff3860" stroke-width="2" stroke-linecap="round"/>
<line id="motorMark2" x1="330" y1="683" x2="330" y2="689" stroke="#ff3860" stroke-width="2" stroke-linecap="round"/>
<text x="330" y="710" text-anchor="middle" fill="#ff3860" font-size="10" font-family="IBM Plex Mono">电机</text>
</g>
<!-- 绳索 A(提升绳) -->
<path id="ropeA" d="" fill="none" stroke="#ff9500" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" filter="url(#ropeGlow)"/>
<!-- 绳索 B(伸缩绳 - 动滑轮组) -->
<path id="ropeB" d="" fill="none" stroke="#ffb840" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" filter="url(#ropeGlow)"/>
<!-- 绳索流动粒子 -->
<circle id="dotA1" r="3" fill="#ffcc00" opacity="0.9" filter="url(#pulseGlow)"/>
<circle id="dotA2" r="3" fill="#ffcc00" opacity="0.9" filter="url(#pulseGlow)"/>
<circle id="dotB1" r="3" fill="#ffdd55" opacity="0.9" filter="url(#pulseGlow)"/>
<circle id="dotB2" r="3" fill="#ffdd55" opacity="0.9" filter="url(#pulseGlow)"/>
<!-- 速度/力标注 -->
<g id="speedForceLabels" opacity="0">
<g id="f1SpeedLabel">
<rect x="448" y="0" width="70" height="28" rx="4" fill="rgba(0,229,160,0.12)" stroke="rgba(0,229,160,0.4)" stroke-width="1"/>
<text x="483" y="0" text-anchor="middle" fill="#00e5a0" font-size="13" font-family="Rajdhani" font-weight="600">v</text>
</g>
<g id="f2SpeedLabel">
<rect x="448" y="0" width="70" height="28" rx="4" fill="rgba(255,149,0,0.12)" stroke="rgba(255,149,0,0.4)" stroke-width="1"/>
<text x="483" y="0" text-anchor="middle" fill="#ff9500" font-size="13" font-family="Rajdhani" font-weight="600">2v</text>
</g>
<g id="forceLabel">
<rect x="80" y="0" width="80" height="28" rx="4" fill="rgba(255,56,96,0.12)" stroke="rgba(255,56,96,0.4)" stroke-width="1"/>
<text x="120" y="0" text-anchor="middle" fill="#ff3860" font-size="12" font-family="Rajdhani" font-weight="600">F → F/2</text>
</g>
</g>
<!-- 碰撞闪光 -->
<rect id="limitFlash" x="210" y="130" width="240" height="40" rx="4" fill="rgba(255,56,96,0.3)" opacity="0"/>
<!-- IFR 原理标注 -->
<g id="ifrAnnotations">
<g id="ifrSingleSource" opacity="0">
<rect x="16" y="640" width="155" height="42" rx="4" fill="rgba(0,229,160,0.08)" stroke="rgba(0,229,160,0.3)" stroke-width="1"/>
<text x="24" y="656" fill="#00e5a0" font-size="10" font-family="IBM Plex Mono" font-weight="500">IFR: 单一动力源</text>
<text x="24" y="672" fill="#6a9aaa" font-size="9" font-family="IBM Plex Mono">消除级联复杂度</text>
</g>
<g id="ifrPulley" opacity="0">
<rect x="460" y="0" width="170" height="42" rx="4" fill="rgba(255,149,0,0.08)" stroke="rgba(255,149,0,0.3)" stroke-width="1"/>
<text x="468" y="0" fill="#ff9500" font-size="10" font-family="IBM Plex Mono" font-weight="500">动滑轮倍率 2:1</text>
<text x="468" y="0" fill="#aa8a5a" font-size="9" font-family="IBM Plex Mono">速度倍增 / 推力减半</text>
</g>
<g id="ifrFlexible" opacity="0">
<rect x="16" y="0" width="165" height="42" rx="4" fill="rgba(255,184,64,0.08)" stroke="rgba(255,184,64,0.3)" stroke-width="1"/>
<text x="24" y="0" fill="#ffb840" font-size="10" font-family="IBM Plex Mono" font-weight="500">柔性绳索传动</text>
<text x="24" y="0" fill="#8a8a6a" font-size="9" font-family="IBM Plex Mono">替代刚性级联丝杠</text>
</g>
</g>
<!-- 力箭头 -->
<g id="forceArrows" opacity="0">
<line id="arrowMotor" x1="330" y1="650" x2="330" y2="640" stroke="#ff3860" stroke-width="2" marker-end="url(#arrowHead)"/>
<line id="arrowF1" x1="0" y1="0" x2="0" y2="0" stroke="#00e5a0" stroke-width="2"/>
<line id="arrowF2" x1="0" y1="0" x2="0" y2="0" stroke="#ff9500" stroke-width="2"/>
</g>
<!-- 箭头标记 -->
<defs>
<marker id="arrowHeadR" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#ff3860"/>
</marker>
<marker id="arrowHeadG" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#00e5a0"/>
</marker>
<marker id="arrowHeadO" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#ff9500"/>
</marker>
</defs>
</svg>
</div>
<!-- 右侧信息面板 -->
<aside class="info-panel">
<!-- 阶段指示器 -->
<div class="info-section">
<div class="label">当前阶段</div>
<div class="phase-indicator">
<div class="phase-dot" id="pd0"></div>
<div class="phase-line" id="pl01"></div>
<div class="phase-dot" id="pd1"></div>
<div class="phase-line" id="pl12"></div>
<div class="phase-dot" id="pd2"></div>
<div class="phase-line" id="pl23"></div>
<div class="phase-dot" id="pd3"></div>
</div>
<div class="desc" id="phaseText">就绪 - 点击播放开始演示</div>
</div>
<!-- 核心参数 -->
<div class="info-section">
<div class="label">核心参数</div>
<div class="param-row">
<span class="param-name">绳轮倍率</span>
<span class="param-val">2<span class="unit">:1</span></span>
</div>
<div class="param-row">
<span class="param-name">安全系数</span>
<span class="param-val">≥4<span class="unit">倍</span></span>
</div>
<div class="param-row">
<span class="param-name">动力源数量</span>
<span class="param-val">1<span class="unit">个</span></span>
</div>
<div class="param-row">
<span class="param-name">二级速度比</span>
<span class="param-val" id="speedRatioVal">1<span class="unit">x</span></span>
</div>
</div>
<!-- 实时数据 -->
<div class="info-section">
<div class="label">实时状态</div>
<div class="param-row">
<span class="param-name">一级框架位移</span>
<span class="param-val" id="f1Disp">0<span class="unit">mm</span></span>
</div>
<div class="param-row">
<span class="param-name">二级框架位移</span>
<span class="param-val" id="f2Disp">0<span class="unit">mm</span></span>
</div>
<div class="param-row">
<span class="param-name">电机输出力</span>
<span class="param-val" id="motorForce">0<span class="unit">N</span></span>
</div>
</div>
<!-- IFR 原理 -->
<div class="info-section ifr-box">
<div class="title">IFR 最终理想解</div>
<div class="text">
系统自行消除矛盾:以单一动力源 + 柔性绳轮组,取代多级级联刚性传动。
动滑轮将速度倍增、推力减半,用最少资源实现多级顺序伸缩。
</div>
</div>
<!-- 技术风险 -->
<div class="info-section">
<div class="label">技术风险</div>
<div class="desc" style="color:#aa6a5a;">
绳索绕线需防干涉脱槽;需配自锁减速电机或制动器防坠落;长期使用需张紧调节。
</div>
</div>
</aside>
</div>
<!-- 控制栏 -->
<div class="controls">
<button class="btn primary" id="btnPlay" aria-label="播放/暂停">
<i class="fas fa-play" id="playIcon"></i> 播放
</button>
<button class="btn" id="btnReset" aria-label="重置">
<i class="fas fa-undo"></i> 重置
</button>
<div class="slider-wrap">
<label>进度</label>
<input type="range" id="progressSlider" min="0" max="1000" value="0" aria-label="动画进度">
</div>
<div class="speed-group">
<button class="speed-btn" data-speed="0.3">0.3x</button>
<button class="speed-btn active" data-speed="1">1x</button>
<button class="speed-btn" data-speed="2">2x</button>
</div>
</div>
</div>
<script>
(function() {
'use strict';
/* ===== 常量定义 ===== */
const CX = 330; // SVG 中心 X
const GROUND_Y = 700;
const BASE_TOP = 650;
const MAST_TOP = 140;
// 一级框架参数
const F1 = {
left: 230, width: 200, height: 280,
collapsedTop: 420, // 收缩时顶部 y
extendedTop: 180, // 完全伸出时顶部 y
capHeight: 8
};
F1.right = F1.left + F1.width;
F1.travel = F1.collapsedTop - F1.extendedTop;
// 二级框架参数
const F2 = {
left: 265, width: 130, height: 200,
relTopCollapsed: 40, // 相对一级框架顶部的偏移(收缩时)
extensionAbove: 80, // 相对一级框架向上伸出的距离
};
F2.right = F2.left + F2.width;
// 滑轮参数
const PULLEY_R = 10;
const TOP_PULLEY_Y = 142;
const FP_LEFT_X = 248; // 一级框架左定滑轮 X
const FP_RIGHT_X = 412; // 一级框架右定滑轮 X
const FP_OFFSET_Y = 22; // 定滑轮相对框架顶部 Y 偏移
const MP_X = CX; // 动滑轮 X(居中)
const MP_OFFSET_Y = -22; // 动滑轮相对二级框架底部 Y 偏移
// 电机
const MOTOR_Y = 678;
/* ===== 动画状态 ===== */
let state = {
progress: 0, // 0 ~ 1
playing: false,
speed: 1,
lastTime: 0
};
/* ===== DOM 引用 ===== */
const svg = document.getElementById('mechSvg');
const slider = document.getElementById('progressSlider');
const btnPlay = document.getElementById('btnPlay');
const btnReset = document.getElementById('btnReset');
const playIcon = document.getElementById('playIcon');
// SVG 元素
const f1Body = document.getElementById('f1Body');
const f1Cap = document.getElementById('f1Cap');
const f1Label = document.getElementById('f1Label');
const fpLeft = document.getElementById('fpLeft');
const fpRight = document.getElementById('fpRight');
const fpLeftMark = document.getElementById('fpLeftMark');
const fpRightMark = document.getElementById('fpRightMark');
const f2Body = document.getElementById('f2Body');
const f2Cap = document.getElementById('f2Cap');
const f2Label = document.getElementById('f2Label');
const mpGroup = document.getElementById('mpGroup');
const mpMark = document.getElementById('mpMark');
const motorMark1 = document.getElementById('motorMark1');
const motorMark2 = document.getElementById('motorMark2');
const topPulleyMark = document.getElementById('topPulleyMark');
const ropeA = document.getElementById('ropeA');
const ropeB = document.getElementById('ropeB');
const dotA1 = document.getElementById('dotA1');
const dotA2 = document.getElementById('dotA2');
const dotB1 = document.getElementById('dotB1');
const dotB2 = document.getElementById('dotB2');
const limitFlash = document.getElementById('limitFlash');
// 标注
const ifrSingleSource = document.getElementById('ifrSingleSource');
const ifrPulley = document.getElementById('ifrPulley');
const ifrFlexible = document.getElementById('ifrFlexible');
const speedForceLabels = document.getElementById('speedForceLabels');
const phaseText = document.getElementById('phaseText');
const f1DispEl = document.getElementById('f1Disp');
const f2DispEl = document.getElementById('f2Disp');
const motorForceEl = document.getElementById('motorForce');
const speedRatioVal = document.getElementById('speedRatioVal');
/* ===== 工具函数 ===== */
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeInOut(t) { return t < 0.5 ? 2*t*t : -1+(4-2*t)*t; }
/* ===== 核心:更新场景 ===== */
function updateScene(progress) {
const p = clamp(progress, 0, 1);
// 阶段计算
// 阶段1 (0~0.45): 一级框架上升
// 阶段2 (0.45~0.55): 一级框架触限位,短暂停顿
// 阶段3 (0.55~1.0): 二级框架伸出
const p1Raw = clamp(p / 0.45, 0, 1);
const p1 = easeInOut(p1Raw);
const p3Raw = clamp((p - 0.55) / 0.45, 0, 1);
const p3 = easeInOut(p3Raw);
// 一级框架位置
const f1Top = lerp(F1.collapsedTop, F1.extendedTop, p1);
const f1Bottom = f1Top + F1.height;
// 二级框架位置(相对一级框架)
const f2RelTopCollapsed = F2.relTopCollapsed;
const f2RelTopExtended = -F2.extensionAbove; // 负值表示伸出一级框架上方
const f2RelTop = lerp(f2RelTopCollapsed, f2RelTopExtended, p3);
const f2Top = f1Top + f2RelTop;
const f2Bottom = f2Top + F2.height;
// ===== 更新一级框架 =====
f1Body.setAttribute('y', f1Top);
f1Cap.setAttribute('y', f1Top - F1.capHeight);
f1Label.setAttribute('y', f1Top + F1.height / 2);
// 定滑轮位置
const fpY = f1Top + FP_OFFSET_Y;
fpLeft.setAttribute('transform', `translate(0, ${fpY - 440})`);
fpRight.setAttribute('transform', `translate(0, ${fpY - 440})`);
// 旋转标记
const fpAngle1 = p1 * 360 * 2;
const fpR1 = PULLEY_R - 5;
fpLeftMark.setAttribute('transform', `rotate(${fpAngle1}, 248, ${fpY})`);
fpRightMark.setAttribute('transform', `rotate(${-fpAngle1}, 412, ${fpY})`);
// ===== 更新二级框架 =====
f2Body.setAttribute('y', f2Top);
f2Cap.setAttribute('y', f2Top - F2.capHeight);
f2Label.setAttribute('y', f2Top + F2.height / 2);
// 动滑轮位置
const mpY = f2Bottom + MP_OFFSET_Y;
mpGroup.setAttribute('transform', `translate(0, ${mpY - 648})`);
const mpAngle = p3 * 360 * 4;
mpMark.setAttribute('transform', `rotate(${mpAngle}, 330, ${mpY})`);
// ===== 电机旋转 =====
const motorAngle = p * 360 * 6;
motorMark1.setAttribute('transform', `rotate(${motorAngle}, 330, ${MOTOR_Y})`);
motorMark2.setAttribute('transform', `rotate(${motorAngle + 90}, 330, ${MOTOR_Y})`);
// 顶部滑轮旋转
const topAngle = p1 * 360 * 2;
topPulleyMark.setAttribute('transform', `rotate(${topAngle}, 330, ${TOP_PULLEY_Y})`);
// ===== 绳索 A(提升绳):电机 → 左侧上行 → 顶部滑轮 → 下行到一级框架顶部 =====
const ropeAPath = [
`M ${CX - 15} ${MOTOR_Y}`, // 电机左侧
`L ${FP_LEFT_X - 18} ${BASE_TOP + 5}`, // 底座左导轨
`L ${FP_LEFT_X - 18} ${TOP_PULLEY_Y - 10}`, // 沿左侧上行
`L ${CX} ${TOP_PULLEY_Y}`, // 到顶部滑轮
`L ${CX} ${f1Top - 2}` // 下行到一级框架顶
].join(' ');
ropeA.setAttribute('d', ropeAPath);
// ===== 绳索 B(伸缩绳):左定滑轮 → 动滑轮 → 右定滑轮 → 下行到电机 =====
const ropeBPath = [
`M ${FP_LEFT_X} ${fpY}`, // 左定滑轮
`L ${MP_X} ${mpY}`, // 动滑轮
`L ${FP_RIGHT_X} ${fpY}`, // 右定滑轮
`L ${FP_RIGHT_X + 18} ${BASE_TOP + 5}`, // 右侧下行
`L ${CX + 15} ${MOTOR_Y}` // 到电机右侧
].join(' ');
ropeB.setAttribute('d', ropeBPath);
// ===== 绳索流动粒子 =====
if (state.playing && p < 1) {
const t = Date.now() / 400;
// 绳索 A 上的粒子
const aPt1 = getPointOnRopeA(ropeAPath, (t % 1));
const aPt2 = getPointOnRopeA(ropeAPath, ((t + 0.5) % 1));
dotA1.setAttribute('cx', aPt1.x);
dotA1.setAttribute('cy', aPt1.y);
dotA2.setAttribute('cx', aPt2.x);
dotA2.setAttribute('cy', aPt2.y);
dotA1.setAttribute('opacity', '0.9');
dotA2.setAttribute('opacity', '0.9');
// 绳索 B 上的粒子
const bPt1 = getPointOnRopeB(ropeBPath, (t % 1));
const bPt2 = getPointOnRopeB(ropeBPath, ((t + 0.5) % 1));
dotB1.setAttribute('cx', bPt1.x);
dotB1.setAttribute('cy', bPt1.y);
dotB2.setAttribute('cx', bPt2.x);
dotB2.setAttribute('cy', bPt2.y);
dotB1.setAttribute('opacity', p > 0.5 ? '0.9' : '0.3');
dotB2.setAttribute('opacity', p > 0.5 ? '0.9' : '0.3');
} else {
dotA1.setAttribute('opacity', '0');
dotA2.setAttribute('opacity', '0');
dotB1.setAttribute('opacity', '0');
dotB2.setAttribute('opacity', '0');
}
// ===== 限位闪光 =====
const flashIntensity = (p > 0.42 && p < 0.58) ? Math.max(0, 1 - Math.abs(p - 0.48) * 10) : 0;
limitFlash.setAttribute('opacity', flashIntensity * 0.6);
// ===== IFR 标注淡入淡出 =====
const ifrSingleOp = (p > 0.05 && p < 0.6) ? clamp(Math.min((p - 0.05) * 8, (0.6 - p) * 5), 0, 1) : 0;
ifrSingleSource.setAttribute('opacity', ifrSingleOp);
const ifrPulleyOp = (p > 0.5 && p < 1.0) ? clamp(Math.min((p - 0.5) * 6, (1.0 - p) * 5), 0, 1) : 0;
ifrPulley.setAttribute('opacity', ifrPulleyOp);
// 动态定位
ifrPulley.querySelector('rect').setAttribute('y', mpY - 55);
ifrPulley.querySelectorAll('text')[0].setAttribute('y', mpY - 39);
ifrPulley.querySelectorAll('text')[1].setAttribute('y', mpY - 23);
const ifrFlexOp = (p > 0.1 && p < 0.7) ? clamp(Math.min((p - 0.1) * 6, (0.7 - p) * 5), 0, 1) : 0;
ifrFlexible.setAttribute('opacity', ifrFlexOp);
ifrFlexible.querySelector('rect').setAttribute('y', f1Top + F1.height / 2 - 50);
ifrFlexible.querySelectorAll('text')[0].setAttribute('y', f1Top + F1.height / 2 - 34);
ifrFlexible.querySelectorAll('text')[1].setAttribute('y', f1Top + F1.height / 2 - 18);
// ===== 速度/力标注 =====
const sfOp = p > 0.15 ? clamp((p - 0.15) * 4, 0, 1) : 0;
speedForceLabels.setAttribute('opacity', sfOp);
// 一级框架速度标签
const f1SpeedG = document.getElementById('f1SpeedLabel');
f1SpeedG.querySelector('rect').setAttribute('y', f1Top - 5);
f1SpeedG.querySelector('text').setAttribute('y', f1Top + 15);
// 二级框架速度标签
const f2SpeedG = document.getElementById('f2SpeedLabel');
f2SpeedG.querySelector('rect').setAttribute('y', f2Top - 5);
f2SpeedG.querySelector('text').setAttribute('y', f2Top + 15);
// 力标签
const forceG = document.getElementById('forceLabel');
forceG.querySelector('rect').setAttribute('y', MOTOR_Y - 35);
forceG.querySelector('text').setAttribute('y', MOTOR_Y - 17);
// ===== 更新信息面板 =====
updateInfoPanel(p, p1, p3, f1Top, f2Top);
}
/* ===== 简易路径点计算 ===== */
function parsePathD(d) {
const points = [];
const cmds = d.split(/(?=[ML])/);
for (const cmd of cmds) {
const parts = cmd.trim().split(/[\s,]+/);
if (parts[0] === 'M' || parts[0] === 'L') {
points.push({ x: parseFloat(parts[1]), y: parseFloat(parts[2]) });
}
}
return points;
}
function getPointOnPath(points, t) {
// 计算总长度
let totalLen = 0;
const segs = [];
for (let i = 1; i < points.length; i++) {
const dx = points[i].x - points[i-1].x;
const dy = points[i].y - points[i-1].y;
const len = Math.sqrt(dx*dx + dy*dy);
segs.push({ start: points[i-1], end: points[i], len });
totalLen += len;
}
let targetLen = t * totalLen;
for (const seg of segs) {
if (targetLen <= seg.len) {
const ratio = seg.len > 0 ? targetLen / seg.len : 0;
return {
x: lerp(seg.start.x, seg.end.x, ratio),
y: lerp(seg.start.y, seg.end.y, ratio)
};
}
targetLen -= seg.len;
}
return points[points.length - 1];
}
function getPointOnRopeA(d, t) {
return getPointOnPath(parsePathD(d), t);
}
function getPointOnRopeB(d, t) {
return getPointOnPath(parsePathD(d), t);
}
/* ===== 信息面板更新 ===== */
function updateInfoPanel(p, p1, p3, f1Top, f2Top) {
// 阶段指示器
const dots = ['pd0','pd1','pd2','pd3'].map(id => document.getElementById(id));
const lines = ['pl01','pl12','pl23'].map(id => document.getElementById(id));
dots.forEach(d => { d.className = 'phase-dot'; });
lines.forEach(l => { l.className = 'phase-line'; });
let phaseStr = '';
if (p <= 0.01) {
dots[0].classList.add('active');
phaseStr = '就绪 - 点击播放开始演示';
} else if (p < 0.45) {
dots[0].classList.add('done');
lines[0].classList.add('done');
dots[1].classList.add('active');
phaseStr = '阶段1: 电机收绳 → 一级框架上升';
} else if (p < 0.55) {
dots[0].classList.add('done');
lines[0].classList.add('done');
dots[1].classList.add('done');
lines[1].classList.add('done');
dots[2].classList.add('active');
phaseStr = '过渡: 一级框架触及限位节点';
} else if (p < 0.99) {
dots[0].classList.add('done');
dots[1].classList.add('done');
lines[0].classList.add('done');
lines[1].classList.add('done');
dots[2].classList.add('done');
lines[2].classList.add('done');
dots[3].classList.add('active');
phaseStr = '阶段2: 动滑轮组联动 → 二级框架伸出';
} else {
dots.forEach(d => d.classList.add('done'));
lines.forEach(l => l.classList.add('done'));
phaseStr = '完成: 双级全伸出 - 到达顶点';
}
phaseText.textContent = phaseStr;
// 实时数据
const f1Disp = Math.round(p1 * 240);
const f2Disp = Math.round(p3 * 120);
const totalF2 = f1Disp + f2Disp;
f1DispEl.innerHTML = `${f1Disp}<span class="unit">mm</span>`;
f2DispEl.innerHTML = `${totalF2}<span class="unit">mm</span>`;
motorForceEl.innerHTML = `${Math.round(lerp(0, 500, p > 0.02 ? 1 : 0))}<span class="unit">N</span>`;
const ratio = p > 0.55 ? 2 : 1;
speedRatioVal.innerHTML = `${ratio}<span class="unit">x</span>`;
// 更新滑块
slider.value = Math.round(p * 1000);
}
/* ===== 动画循环 ===== */
function animate(timestamp) {
if (!state.playing) return;
if (state.lastTime === 0) state.lastTime = timestamp;
const delta = (timestamp - state.lastTime) / 1000;
state.lastTime = timestamp;
state.progress += delta * state.speed * 0.12;
if (state.progress >= 1) {
state.progress = 1;
state.playing = false;
updatePlayButton();
}
updateScene(state.progress);
if (state.playing) {
requestAnimationFrame(animate);
}
}
function updatePlayButton() {
if (state.playing) {
playIcon.className = 'fas fa-pause';
btnPlay.innerHTML = '<i class="fas fa-pause"></i> 暂停';
} else {
playIcon.className = 'fas fa-play';
btnPlay.innerHTML = '<i class="fas fa-play"></i> 播放';
}
}
/* ===== 事件绑定 ===== */
btnPlay.addEventListener('click', () => {
if (state.progress >= 1) {
state.progress = 0;
}
state.playing = !state.playing;
state.lastTime = 0;
updatePlayButton();
if (state.playing) {
requestAnimationFrame(animate);
}
});
btnReset.addEventListener('click', () => {
state.playing = false;
state.progress = 0;
state.lastTime = 0;
updatePlayButton();
updateScene(0);
});
slider.addEventListener('input', (e) => {
state.progress = parseInt(e.target.value) / 1000;
state.playing = false;
state.lastTime = 0;
updatePlayButton();
updateScene(state.progress);
});
// 速度按钮
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.speed = parseFloat(btn.dataset.speed);
});
});
/* ===== 绳索发光动画(CSS 无法驱动 SVG filter,用 JS 周期性变化透明度) ===== */
let glowPhase = 0;
function animateGlow() {
glowPhase += 0.03;
const intensity = 0.7 + 0.3 * Math.sin(glowPhase);
ropeA.style.opacity = state.playing ? intensity : 0.85;
ropeB.style.opacity = state.playing ? intensity : 0.85;
requestAnimationFrame(animateGlow);
}
/* ===== 初始化 ===== */
updateScene(0);
animateGlow();
// 5秒后自动播放一次
setTimeout(() => {
if (state.progress <= 0.01 && !state.playing) {
state.playing = true;
state.lastTime = 0;
updatePlayButton();
requestAnimationFrame(animate);
}
}, 2500);
})();
</script>
</body>
</html>
实现说明
动画原理与核心设计:
IFR 理想解聚焦:动画直接展示消除级联复杂度后的最终理想状态——单一电机通过绳轮组驱动双级伸缩,没有前后对比的冗余,而是通过阶段性标注("单一动力源→消除级联复杂度""动滑轮倍率2:1""柔性绳索→替代刚性丝杠")在关键时刻淡入淡出,引导用户关注核心创新。
双绳系统可视化:
- 绳索 A(提升绳):从电机左侧出发,沿左侧导轨上行,经顶部定滑轮换向,下行连接一级框架顶部——展示直接提升原理。
- 绳索 B(伸缩绳):经一级框架左定滑轮→二级框架动滑轮→右定滑轮→回电机——展示动滑轮2倍率原理。流动粒子沿绳索路径运动,直观呈现力的传递方向。
顺序动作时序:进度 0~45% 为一级框架上升阶段;45%~55% 为触限位过渡(红色闪光反馈);55%~100% 为二级框架伸出阶段。二级框架速度标签从
v变为2v,力标签显示F → F/2。交互控制:播放/暂停、重置、进度拖动滑块(可手动逐帧 scrub 观察任意时刻状态)、三档速度切换(0.3x 慢放观察细节 / 1x 正常 / 2x 快进)。
视觉设计:深色工程蓝图风格,琥珀色绳索带发光滤镜,青绿色定滑轮与橙色动滑轮形成功能色对比,红色电机与限位闪光提供警示性视觉锚点。所有滑轮和电机滚筒带旋转标记动画,增强动态感知。
如何下载动画?
下载方法
这段动画是纯前端 HTML 文件,无需服务器即可运行,保存方式很简单:
方法一:直接复制保存(推荐)
- 在我的回复中,找到
```html代码块 - 点击代码块右上角的复制按钮
- 打开任意文本编辑器(记事本、VS Code、Sublime 等)
- 粘贴全部内容
- 保存为
mechanism.html(注意扩展名必须是.html,而非.txt) - 双击该文件,浏览器会自动打开并运行动画
方法二:浏览器直接保存
如果你已经在浏览器中看到了运行效果:
- 按
Ctrl + S(Mac 为Cmd + S) - 选择保存类型为 "网页,全部" 或 "网页,仅HTML"
- 保存到本地即可
方法三:一键命令行(适合开发者)
# 将代码写入文件
cat > mechanism.html << 'EOF'
# 此处粘贴完整 HTML 代码
EOF
# macOS 直接打开
open mechanism.html
# Linux
xdg-open mechanism.html
# Windows
start mechanism.html
注意事项
- 该文件完全自包含,仅引用了 Google Fonts 和 Font Awesome 的 CDN 资源,需要联网加载字体和图标
- 如果离线使用,字体回退为系统字体,动画本身不受影响
- 不依赖任何本地服务器、构建工具或后端,任何现代浏览器直接打开即可运行
积分规则:第一轮对话扣减6分,后续每轮扣4分
等待动画代码生成...
