分享图
A
动画渲染工坊
就绪
<!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>

实现说明

动画原理与核心设计:

  1. IFR 理想解聚焦:动画直接展示消除级联复杂度后的最终理想状态——单一电机通过绳轮组驱动双级伸缩,没有前后对比的冗余,而是通过阶段性标注("单一动力源→消除级联复杂度""动滑轮倍率2:1""柔性绳索→替代刚性丝杠")在关键时刻淡入淡出,引导用户关注核心创新。

  2. 双绳系统可视化

    • 绳索 A(提升绳):从电机左侧出发,沿左侧导轨上行,经顶部定滑轮换向,下行连接一级框架顶部——展示直接提升原理。
    • 绳索 B(伸缩绳):经一级框架左定滑轮→二级框架动滑轮→右定滑轮→回电机——展示动滑轮2倍率原理。流动粒子沿绳索路径运动,直观呈现力的传递方向。
  3. 顺序动作时序:进度 0~45% 为一级框架上升阶段;45%~55% 为触限位过渡(红色闪光反馈);55%~100% 为二级框架伸出阶段。二级框架速度标签从 v 变为 2v,力标签显示 F → F/2

  4. 交互控制:播放/暂停、重置、进度拖动滑块(可手动逐帧 scrub 观察任意时刻状态)、三档速度切换(0.3x 慢放观察细节 / 1x 正常 / 2x 快进)。

  5. 视觉设计:深色工程蓝图风格,琥珀色绳索带发光滤镜,青绿色定滑轮与橙色动滑轮形成功能色对比,红色电机与限位闪光提供警示性视觉锚点。所有滑轮和电机滚筒带旋转标记动画,增强动态感知。

如何下载动画?

下载方法

这段动画是纯前端 HTML 文件,无需服务器即可运行,保存方式很简单:

方法一:直接复制保存(推荐)

  1. 在我的回复中,找到 ```html 代码块
  2. 点击代码块右上角的复制按钮
  3. 打开任意文本编辑器(记事本、VS Code、Sublime 等)
  4. 粘贴全部内容
  5. 保存为 mechanism.html(注意扩展名必须是 .html,而非 .txt
  6. 双击该文件,浏览器会自动打开并运行动画

方法二:浏览器直接保存

如果你已经在浏览器中看到了运行效果:

  1. Ctrl + S(Mac 为 Cmd + S
  2. 选择保存类型为 "网页,全部""网页,仅HTML"
  3. 保存到本地即可

方法三:一键命令行(适合开发者)

# 将代码写入文件
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分