分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TRIZ 理想最终解:纯机械自适应修鼓机</title>
    <style>
        :root {
            --bg-color: #0b0f19;
            --grid-color: rgba(30, 58, 138, 0.2);
            --accent-cyan: #00f0ff;
            --accent-amber: #ffb700;
            --accent-red: #ff3366;
            --text-main: #e2e8f0;
            --text-dim: #94a3b8;
            --panel-bg: rgba(11, 15, 25, 0.8);
            --panel-border: rgba(0, 240, 255, 0.3);
            --font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            user-select: none;
        }

        body {
            background-color: var(--bg-color);
            color: var(--text-main);
            font-family: var(--font-family);
            height: 100vh;
            width: 100vw;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 30px 30px;
            background-position: center center;
        }

        /* 核心动画容器 */
        #animation-container {
            position: relative;
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        svg {
            width: 90%;
            max-width: 1400px;
            height: auto;
            max-height: 85vh;
            filter: drop-shadow(0 0 30px rgba(0, 240, 255, 0.1));
            overflow: visible;
        }

        /* 信息面板 - 绝对定位在角落,严格控制尺寸与位置 */
        .info-panel {
            position: absolute;
            background: var(--panel-bg);
            border: 1px solid var(--panel-border);
            border-radius: 8px;
            padding: 16px;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            z-index: 10;
            max-width: 320px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
        }

        .panel-top-left { top: 30px; left: 30px; }
        .panel-top-right { top: 30px; right: 30px; }
        .panel-bottom-left { bottom: 30px; left: 30px; }

        .info-title {
            font-size: 14px;
            font-weight: 700;
            color: var(--accent-cyan);
            margin-bottom: 8px;
            letter-spacing: 1px;
            text-transform: uppercase;
        }

        .info-text {
            font-size: 12px;
            line-height: 1.6;
            color: var(--text-dim);
            text-align: justify;
        }

        .info-text strong {
            color: var(--text-main);
        }

        .highlight-triz {
            display: inline-block;
            background: rgba(255, 183, 0, 0.15);
            color: var(--accent-amber);
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            margin-top: 6px;
            border: 1px solid rgba(255, 183, 0, 0.3);
        }

        /* 状态指示器 */
        .status-indicator {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-top: 12px;
            font-size: 12px;
            font-family: monospace;
            color: var(--accent-cyan);
        }

        .status-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background-color: var(--accent-cyan);
            box-shadow: 0 0 8px var(--accent-cyan);
            transition: background-color 0.3s, box-shadow 0.3s;
        }

        /* 底部交互控制区 */
        .control-panel {
            position: absolute;
            bottom: 30px;
            left: 50%;
            transform: translateX(-50%);
            background: var(--panel-bg);
            border: 1px solid var(--panel-border);
            border-radius: 20px;
            padding: 10px 24px;
            display: flex;
            align-items: center;
            gap: 16px;
            z-index: 10;
            backdrop-filter: blur(10px);
        }

        .control-btn {
            background: transparent;
            border: 1px solid var(--accent-cyan);
            color: var(--accent-cyan);
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .control-btn:hover, .control-btn.active {
            background: var(--accent-cyan);
            color: var(--bg-color);
        }

        .slider-container {
            display: flex;
            align-items: center;
            gap: 12px;
            width: 300px;
        }

        .time-slider {
            -webkit-appearance: none;
            width: 100%;
            height: 4px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 2px;
            outline: none;
            cursor: pointer;
        }

        .time-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: var(--accent-cyan);
            box-shadow: 0 0 10px var(--accent-cyan);
            cursor: pointer;
            transition: transform 0.1s;
        }

        .time-slider::-webkit-slider-thumb:hover {
            transform: scale(1.3);
        }

        .slider-label {
            font-size: 11px;
            color: var(--text-dim);
            font-family: monospace;
            min-width: 45px;
        }

        /* 持续旋转动画 */
        .spin-infinite {
            animation: spin 3s linear infinite;
            transform-origin: center;
        }

        @keyframes spin {
            100% { transform: rotate(360deg); }
        }

        /* 动态发光类 */
        .glow-cyan { filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.6)); }
        .glow-amber { filter: drop-shadow(0 0 10px rgba(255, 183, 0, 0.8)); }
        .glow-red { filter: drop-shadow(0 0 12px rgba(255, 51, 102, 0.9)); }
    </style>
</head>
<body>

    <div id="animation-container">
        
        <!-- 左上角:设计原理 -->
        <div class="info-panel panel-top-left">
            <div class="info-title">最终理想解 (IFR) 架构</div>
            <div class="info-text">
                本机构通过<strong>大扭矩单电机</strong>与内置的<strong>“螺距离合器”</strong>相配合,彻底去除了传统方案中复杂的电动推缸与传感器。
                <br><br>
                巧妙利用用户手持球杆向内推压的<strong>轴向进给力</strong>,在遇到内部硬限位产生“扭矩/推力阶跃”时,自动触发顶刀旋出,实现侧面与顶部切削的时序协同。
            </div>
            <div class="highlight-triz">TRIZ 原理:利用现有资源 (用户推力) 替代外部复杂系统 (推缸)</div>
        </div>

        <!-- 右上角:组件图例 -->
        <div class="info-panel panel-top-right">
            <div class="info-title">系统核心组件</div>
            <div class="info-text" style="display: flex; flex-direction: column; gap: 8px;">
                <div style="display: flex; align-items: center; gap: 8px;">
                    <span style="width: 12px; height: 12px; border: 2px solid var(--accent-cyan); border-radius: 2px;"></span> 侧切刀盘 (受弹簧约束)
                </div>
                <div style="display: flex; align-items: center; gap: 8px;">
                    <span style="width: 12px; height: 12px; background: rgba(0,240,255,0.2); border: 1px solid var(--accent-cyan); border-radius: 50%;"></span> 顶弧切刀 (随螺纹旋出)
                </div>
                <div style="display: flex; align-items: center; gap: 8px;">
                    <span style="width: 12px; height: 2px; background: var(--accent-amber); box-shadow: 0 0 5px var(--accent-amber);"></span> 高压阻尼弹簧 (预紧力30N)
                </div>
                <div style="display: flex; align-items: center; gap: 8px;">
                    <span style="width: 12px; height: 12px; border-right: 3px solid var(--accent-red);"></span> 物理硬限位
                </div>
            </div>
        </div>

        <!-- 左下角:实时状态 -->
        <div class="info-panel panel-bottom-left">
            <div class="info-title">实时力学状态监测</div>
            <div class="info-text">当前时序阶段解析:</div>
            <div class="status-indicator">
                <div class="status-dot" id="status-dot"></div>
                <span id="status-text">待机 / 准备插入</span>
            </div>
            <div style="margin-top: 10px; font-size: 11px; color: var(--text-dim); display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
                <span>轴向推力: <span id="val-thrust" style="color:var(--text-main);">0 N</span></span>
                <span>弹簧反力: <span id="val-spring" style="color:var(--text-main);">30 N</span></span>
                <span>丝杠位移: <span id="val-disp" style="color:var(--text-main);">0 mm</span></span>
                <span>切削模式: <span id="val-mode" style="color:var(--accent-cyan);">空转</span></span>
            </div>
        </div>

        <!-- 底部交互控制 -->
        <div class="control-panel">
            <button class="control-btn active" id="btn-auto">自动演示</button>
            <div class="slider-container">
                <span class="slider-label">0%</span>
                <input type="range" class="time-slider" id="timeline-slider" min="0" max="1000" value="0">
                <span class="slider-label">100%</span>
            </div>
        </div>

        <!-- 核心 SVG 动画区 -->
        <svg viewBox="0 0 1000 500" preserveAspectRatio="xMidYMid meet">
            <defs>
                <!-- 滤镜与渐变定义 -->
                <filter id="neon-cyan" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="5" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                <filter id="neon-amber" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="4" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                <filter id="neon-red" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="6" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>

                <linearGradient id="metal-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#475569" />
                    <stop offset="50%" stop-color="#94a3b8" />
                    <stop offset="100%" stop-color="#334155" />
                </linearGradient>

                <linearGradient id="wood-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#92400e" />
                    <stop offset="50%" stop-color="#d97706" />
                    <stop offset="100%" stop-color="#78350f" />
                </linearGradient>

                <linearGradient id="tip-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                    <stop offset="0%" stop-color="#1e3a8a" />
                    <stop offset="50%" stop-color="#3b82f6" />
                    <stop offset="100%" stop-color="#1e40af" />
                </linearGradient>

                <pattern id="screw-pattern" width="10" height="20" patternUnits="userSpaceOnUse">
                    <line x1="0" y1="20" x2="10" y2="0" stroke="#00f0ff" stroke-width="2" opacity="0.4"/>
                </pattern>
                
                <clipPath id="tip-clip">
                    <!-- 动态裁剪路径,用于表现皮头被切削成圆弧形 -->
                    <path id="clip-path-shape" d="M 0 -25 L 50 -25 L 50 25 L 0 25 Z" />
                </clipPath>
            </defs>

            <!-- 极简背景线条 -->
            <g stroke="rgba(255,255,255,0.05)" stroke-width="1">
                <line x1="0" y1="250" x2="1000" y2="250" stroke-dasharray="10,5" />
                <line x1="500" y1="0" x2="500" y2="500" stroke-dasharray="10,5" />
            </g>

            <!-- 旋转方向指示 (电机抽象) -->
            <g transform="translate(850, 250)">
                <circle cx="0" cy="0" r="140" fill="none" stroke="rgba(0, 240, 255, 0.1)" stroke-width="1" />
                <path class="spin-infinite" d="M -150 0 A 150 150 0 0 1 0 -150" fill="none" stroke="var(--accent-cyan)" stroke-width="2" stroke-linecap="round">
                    <animate attributeName="opacity" values="0.2;0.8;0.2" dur="2s" repeatCount="indefinite"/>
                </path>
                <polygon points="2 -155, 12 -150, 2 -145" fill="var(--accent-cyan)" transform="rotate(0) translate(-3,0)" class="spin-infinite"/>
                <text x="0" y="170" fill="var(--accent-cyan)" font-size="12" text-anchor="middle" font-family="monospace" opacity="0.6">单向连续主轴驱动</text>
            </g>

            <!-- 机器外壳剖面 -->
            <path d="M 400 100 L 900 100 L 900 400 L 400 400 Z" fill="none" stroke="#334155" stroke-width="4" stroke-linejoin="round" />
            <path d="M 380 150 L 400 150 L 400 350 L 380 350" fill="none" stroke="#475569" stroke-width="4" stroke-linecap="round" />

            <!-- 内部螺距机构 (固定座) -->
            <rect x="750" y="180" width="100" height="140" fill="url(#metal-grad)" rx="4" />
            <text x="800" y="255" fill="#0b0f19" font-size="12" font-weight="bold" text-anchor="middle">主轴座</text>

            <!-- 动态系统组:螺距离合器、弹簧、刀盘 -->
            <!-- 螺距丝杠 -->
            <rect id="screw-shaft" x="600" y="225" width="150" height="50" fill="url(#screw-pattern)" stroke="#00f0ff" stroke-width="1" />
            
            <!-- 阻尼弹簧 -->
            <g id="spring-group" transform="translate(680, 250)">
                <!-- 弹簧通过缩放X轴来表现压缩,transform-origin 在右侧 -->
                <path id="spring-path" d="M 0 0 L -15 -30 L -30 30 L -45 -30 L -60 30 L -75 -30 L -90 30 L -105 -30 L -120 30 L -135 -30 L -150 0" 
                      fill="none" stroke="var(--accent-amber)" stroke-width="6" stroke-linejoin="round" filter="url(#neon-amber)"/>
            </g>

            <!-- 顶弧切刀盘 (内部隐藏,被丝杠推出) -->
            <g id="top-blade-group" transform="translate(600, 250)">
                <path d="M 0 -40 L 40 -40 L 40 40 L 0 40 Z" fill="rgba(0, 240, 255, 0.1)" stroke="var(--accent-cyan)" stroke-width="2" />
                <!-- 顶刀刃口 - 弧形 -->
                <path d="M -10 -25 Q -30 0 -10 25 L 0 25 L 0 -25 Z" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
                <line x1="-30" y1="0" x2="-60" y2="0" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="4,4" opacity="0.5"/>
            </g>

            <!-- 侧刀盘组 (受弹簧推力,遇限位后相对位移导致张开) -->
            <!-- 上侧刀 -->
            <g id="side-blade-top" transform="translate(480, 150)">
                <path d="M 0 0 L 120 0 L 120 90 L 40 90 L 0 50 Z" fill="url(#metal-grad)" stroke="#94a3b8" stroke-width="2" />
                <path d="M 0 50 L 40 90" stroke="var(--accent-cyan)" stroke-width="4" filter="url(#neon-cyan)" /> <!-- 侧切刃 -->
            </g>
            <!-- 下侧刀 -->
            <g id="side-blade-bottom" transform="translate(480, 350)">
                <path d="M 0 0 L 120 0 L 120 -90 L 40 -90 L 0 -50 Z" fill="url(#metal-grad)" stroke="#94a3b8" stroke-width="2" />
                <path d="M 0 -50 L 40 -90" stroke="var(--accent-cyan)" stroke-width="4" filter="url(#neon-cyan)" /> <!-- 侧切刃 -->
            </g>

            <!-- 物理硬限位 -->
            <g transform="translate(580, 250)">
                <rect x="-10" y="-80" width="20" height="40" fill="#1e293b" stroke="#334155" stroke-width="2" />
                <rect x="-10" y="40" width="20" height="40" fill="#1e293b" stroke="#334155" stroke-width="2" />
                <line id="hard-limit-line-top" x1="-10" y1="-40" x2="-10" y2="-80" stroke="var(--accent-red)" stroke-width="4" />
                <line id="hard-limit-line-bot" x1="-10" y1="40" x2="-10" y2="80" stroke="var(--accent-red)" stroke-width="4" />
                <text x="0" y="-95" fill="var(--accent-red)" font-size="10" text-anchor="middle" font-family="monospace">硬限位</text>
            </g>

            <!-- 球杆及皮头组 (由用户推力驱动进入) -->
            <g id="cue-stick-group" transform="translate(100, 250)">
                <!-- 动能/推力指示箭头 -->
                <g id="force-arrow" transform="translate(-150, 0)">
                    <path d="M 0 -15 L 40 -15 L 40 -30 L 70 0 L 40 30 L 40 15 L 0 15 Z" fill="rgba(255,255,255,0.1)" stroke="var(--text-main)" stroke-width="2"/>
                    <text x="35" y="5" fill="var(--text-main)" font-size="14" font-weight="bold" text-anchor="middle" id="force-text">进给推力</text>
                </g>

                <!-- 球杆先角与木质部分 -->
                <path d="M -300 -20 L -10 -15 L -10 15 L -300 20 Z" fill="url(#wood-grad)" />
                <rect x="-10" y="-15" width="20" height="30" fill="#e2e8f0" />
                
                <!-- 皮头 (应用裁剪路径实现变形切削效果) -->
                <g clip-path="url(#tip-clip)">
                    <rect id="cue-tip-rect" x="10" y="-16" width="30" height="32" fill="url(#tip-grad)" rx="2" />
                </g>
                
                <!-- 辅助对齐线 -->
                <line x1="-300" y1="0" x2="60" y2="0" stroke="rgba(255,255,255,0.2)" stroke-dasharray="4,4" />
            </g>

            <!-- 切削火花/碎屑特效 (隐藏态) -->
            <g id="sparks-side" opacity="0" transform="translate(480, 200)">
                <circle cx="0" cy="0" r="2" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
                <circle cx="-10" cy="-10" r="1.5" fill="var(--accent-cyan)" />
                <circle cx="-5" cy="15" r="1" fill="var(--accent-cyan)" />
            </g>
            <g id="sparks-top" opacity="0" transform="translate(560, 250)">
                <circle cx="0" cy="-10" r="2" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
                <circle cx="-5" cy="10" r="1.5" fill="var(--accent-cyan)" />
            </g>

        </svg>
    </div>

    <script>
        /**
         * 动画时序控制器 (State Machine & Timeline)
         * 基于 requestAnimationFrame 实现平滑的高保真工程动画
         */
        const DOM = {
            cueGroup: document.getElementById('cue-stick-group'),
            cueTip: document.getElementById('cue-tip-rect'),
            clipShape: document.getElementById('clip-path-shape'),
            springGroup: document.getElementById('spring-group'),
            springPath: document.getElementById('spring-path'),
            sideTop: document.getElementById('side-blade-top'),
            sideBot: document.getElementById('side-blade-bottom'),
            topBlade: document.getElementById('top-blade-group'),
            screwShaft: document.getElementById('screw-shaft'),
            limitLineTop: document.getElementById('hard-limit-line-top'),
            limitLineBot: document.getElementById('hard-limit-line-bot'),
            forceArrow: document.getElementById('force-arrow'),
            forceText: document.getElementById('force-text'),
            sparksSide: document.getElementById('sparks-side'),
            sparksTop: document.getElementById('sparks-top'),
            
            // UI 面板
            statusDot: document.getElementById('status-dot'),
            statusText: document.getElementById('status-text'),
            valThrust: document.getElementById('val-thrust'),
            valSpring: document.getElementById('val-spring'),
            valDisp: document.getElementById('val-disp'),
            valMode: document.getElementById('val-mode'),
            
            // 交互
            btnAuto: document.getElementById('btn-auto'),
            slider: document.getElementById('timeline-slider')
        };

        // 动画配置
        const CONFIG = {
            duration: 9000, // 总周期 9 秒
            isAuto: true,
            progress: 0,
            
            // 关键帧时间点 (百分比)
            t_insert: 0.15,   // 插入阶段结束
            t_sideCut: 0.35,  // 侧切结束,碰到硬限位
            t_limit: 0.45,    // 抵住限位,推力蓄积
            t_topCut: 0.65,   // 压缩弹簧,顶刀切削完成
            t_hold: 0.80,     // 保持展示
            t_reset: 1.0      // 拔出重置
        };

        // 插值辅助函数
        const lerp = (start, end, t) => start + (end - start) * Math.max(0, Math.min(1, t));
        const clamp = (val, min, max) => Math.max(min, Math.min(max, val));

        // 核心渲染逻辑:根据进度(0~1)更新所有DOM元素的状态
        function renderFrame(p) {
            // 1. 初始化 / 待机态
            let cueX = 100;
            let tipHeight = 32; // 原始高度
            let tipY = -16;
            let clipD = "M 0 -25 L 50 -25 L 50 25 L 0 25 Z"; // 初始平头裁剪
            
            let springScaleX = 1.0;
            let springColor = "var(--accent-amber)";
            
            let sideBladeYOffset = 0; // 侧刀张开量
            let topBladeX = 600;      // 顶刀位置
            let screwShiftX = 600;
            
            let forceScale = 1.0;
            let forceColor = "rgba(255,255,255,0.1)";
            let forceStroke = "var(--text-main)";
            let limitGlow = "none";
            
            let sparksSideOp = 0;
            let sparksTopOp = 0;
            
            // UI 状态文本
            let sText = "待机 / 准备插入";
            let sColor = "var(--text-dim)";
            let vThrust = "5 N";
            let vSpring = "30 N";
            let vDisp = "0.0 mm";
            let vMode = "空转";

            // 阶段 1: 插入并进行侧面切削 (0 -> t_sideCut)
            if (p <= CONFIG.t_sideCut) {
                let localP = p / CONFIG.t_sideCut;
                cueX = lerp(100, 440, localP); // 推进至刀盘内
                
                if (localP > 0.4) { // 开始接触侧刀
                    let cutP = (localP - 0.4) / 0.6;
                    tipHeight = lerp(32, 28, cutP); // 侧面被削去 4mm
                    tipY = -tipHeight / 2;
                    sparksSideOp = Math.random() > 0.3 ? 0.8 : 0.2;
                    
                    sText = "轴向进给:削减侧面余量";
                    sColor = "var(--accent-cyan)";
                    vThrust = "15 N";
                    vMode = "侧面修边";
                } else {
                    sText = "轴向进给:插入球杆";
                    sColor = "var(--text-main)";
                }
            }
            // 阶段 2: 遭遇硬限位,蓄力 (t_sideCut -> t_limit)
            else if (p <= CONFIG.t_limit) {
                cueX = 440; // 停在限位处
                tipHeight = 28;
                tipY = -14;
                
                let localP = (p - CONFIG.t_sideCut) / (CONFIG.t_limit - CONFIG.t_sideCut);
                forceScale = lerp(1.0, 1.3, localP);
                forceColor = `rgba(255, 51, 102, ${lerp(0.1, 0.4, localP)})`;
                forceStroke = "var(--accent-red)";
                limitGlow = localP > 0.5 ? "url(#neon-red)" : "none";
                
                sText = "遭遇硬限位:推力阶跃激增";
                sColor = "var(--accent-red)";
                vThrust = `${(15 + localP * 25).toFixed(1)} N`; // 15N -> 40N
                vMode = "进给受阻";
            }
            // 阶段 3: 克服弹簧预紧力,机构相对位移,顶刀切削 (t_limit -> t_topCut)
            else if (p <= CONFIG.t_topCut) {
                cueX = 440; // 杆子本体不再前进
                tipHeight = 28;
                tipY = -14;
                forceScale = 1.3;
                forceColor = "rgba(255, 51, 102, 0.4)";
                forceStroke = "var(--accent-red)";
                limitGlow = "url(#neon-red)";
                
                let localP = (p - CONFIG.t_limit) / (CONFIG.t_topCut - CONFIG.t_limit);
                
                // IFR 核心动作:位移与协同
                springScaleX = lerp(1.0, 0.4, localP); // 弹簧压缩
                springColor = "var(--accent-red)"; // 弹簧受高压变红
                
                sideBladeYOffset = lerp(0, 15, localP); // 侧刀被迫上下张开避让
                topBladeX = lerp(600, 540, localP);     // 顶刀旋出向前压入
                screwShiftX = lerp(600, 630, localP);   // 丝杠相对位移
                
                // 顶刀切削,改变皮头前端形状(裁剪路径从矩形变为圆弧)
                let curveX = lerp(50, 25, localP);
                clipD = `M 0 -25 L ${curveX} -25 Q 50 0 ${curveX} 25 L 0 25 Z`;
                
                sparksTopOp = Math.random() > 0.3 ? 0.9 : 0.4;
                
                sText = "克服预紧:离合动作触发!";
                sColor = "var(--accent-amber)";
                vThrust = "40 N (克服弹簧)";
                vSpring = "40 N (压缩状态)";
                vDisp = `${(localP * 8.0).toFixed(1)} mm`;
                vMode = "顶弧修整";
            }
            // 阶段 4: 保持状态展示 (t_topCut -> t_hold)
            else if (p <= CONFIG.t_hold) {
                cueX = 440;
                tipHeight = 28;
                tipY = -14;
                forceScale = 1.3;
                forceColor = "rgba(255, 51, 102, 0.4)";
                forceStroke = "var(--accent-red)";
                limitGlow = "url(#neon-red)";
                
                springScaleX = 0.4;
                springColor = "var(--accent-red)";
                sideBladeYOffset = 15;
                topBladeX = 540;
                screwShiftX = 630;
                clipD = `M 0 -25 L 25 -25 Q 50 0 25 25 L 0 25 Z`;
                
                sText = "修整完成:IFR 状态维持";
                sColor = "var(--accent-cyan)";
                vThrust = "40 N";
                vSpring = "40 N";
                vDisp = "8.0 mm";
                vMode = "修整完毕";
            }
            // 阶段 5: 拔出球杆,系统复位 (t_hold -> 1.0)
            else {
                let localP = (p - CONFIG.t_hold) / (1.0 - CONFIG.t_hold);
                cueX = lerp(440, 100, localP); // 拔出
                tipHeight = 28;
                tipY = -14;
                clipD = `M 0 -25 L 25 -25 Q 50 0 25 25 L 0 25 Z`; // 保持切削后的形状
                
                // 机构迅速回弹
                let resetP = Math.min(1.0, localP * 3); // 快速回弹
                springScaleX = lerp(0.4, 1.0, resetP);
                springColor = "var(--accent-amber)";
                sideBladeYOffset = lerp(15, 0, resetP);
                topBladeX = lerp(540, 600, resetP);
                screwShiftX = lerp(630, 600, resetP);
                
                sText = "退出:弹簧复位刀组";
                sColor = "var(--text-dim)";
                vThrust = "0 N";
                vSpring = "30 N (恢复预紧)";
                vDisp = `${(8.0 - resetP * 8.0).toFixed(1)} mm`;
                vMode = "复位脱离";
            }

            // --- 实际应用 DOM 属性 ---
            
            // 1. 球杆位置及皮头形状
            DOM.cueGroup.setAttribute('transform', `translate(${cueX}, 250)`);
            DOM.cueTip.setAttribute('y', tipY);
            DOM.cueTip.setAttribute('height', tipHeight);
            DOM.clipShape.setAttribute('d', clipD);
            
            // 2. 弹簧压缩与颜色
            DOM.springGroup.setAttribute('transform', `translate(680, 250) scale(${springScaleX}, 1)`);
            DOM.springPath.setAttribute('stroke', springColor);
            if(springScaleX < 0.9) {
                DOM.springPath.setAttribute('filter', 'url(#neon-red)');
            } else {
                DOM.springPath.setAttribute('filter', 'url(#neon-amber)');
            }
            
            // 3. 刀组位移
            DOM.sideTop.setAttribute('transform', `translate(480, ${150 - sideBladeYOffset})`);
            DOM.sideBot.setAttribute('transform', `translate(480, ${350 + sideBladeYOffset})`);
            DOM.topBlade.setAttribute('transform', `translate(${topBladeX}, 250)`);
            DOM.screwShaft.setAttribute('x', screwShiftX);
            
            // 4. 力学指示与特效
            DOM.forceArrow.setAttribute('transform', `translate(-150, 0) scale(${forceScale})`);
            DOM.forceArrow.firstElementChild.setAttribute('fill', forceColor);
            DOM.forceArrow.firstElementChild.setAttribute('stroke', forceStroke);
            DOM.forceText.setAttribute('fill', forceStroke);
            
            DOM.limitLineTop.setAttribute('filter', limitGlow);
            DOM.limitLineBot.setAttribute('filter', limitGlow);
            
            DOM.sparksSide.setAttribute('opacity', sparksSideOp);
            DOM.sparksTop.setAttribute('opacity', sparksTopOp);
            
            // 5. UI 文本更新
            DOM.statusDot.style.backgroundColor = sColor;
            DOM.statusDot.style.boxShadow = `0 0 8px ${sColor}`;
            DOM.statusText.textContent = sText;
            DOM.statusText.style.color = sColor;
            
            DOM.valThrust.textContent = vThrust;
            DOM.valSpring.textContent = vSpring;
            DOM.valDisp.textContent = vDisp;
            DOM.valMode.textContent = vMode;
        }

        // 动画主循环
        let startTimestamp = null;
        
        function animationLoop(timestamp) {
            if (!startTimestamp) startTimestamp = timestamp;
            
            if (CONFIG.isAuto) {
                // 计算循环进度
                let elapsed = (timestamp - startTimestamp) % CONFIG.duration;
                CONFIG.progress = elapsed / CONFIG.duration;
                
                // 同步更新滑块 UI
                DOM.slider.value = CONFIG.progress * 1000;
            }
            
            renderFrame(CONFIG.progress);
            requestAnimationFrame(animationLoop);
        }

        // 交互事件绑定
        DOM.btnAuto.addEventListener('click', () => {
            CONFIG.isAuto = !CONFIG.isAuto;
            if (CONFIG.isAuto) {
                DOM.btnAuto.classList.add('active');
                DOM.btnAuto.textContent = "自动演示";
                // 重新同步时间戳,避免跳变
                startTimestamp = performance.now() - (CONFIG.progress * CONFIG.duration);
            } else {
                DOM.btnAuto.classList.remove('active');
                DOM.btnAuto.textContent = "手动控制";
            }
        });

        DOM.slider.addEventListener('input', (e) => {
            CONFIG.isAuto = false;
            DOM.btnAuto.classList.remove('active');
            DOM.btnAuto.textContent = "手动控制";
            CONFIG.progress = e.target.value / 1000;
        });

        // 启动动画 (满足重开即播要求)
        window.addEventListener('DOMContentLoaded', () => {
            requestAnimationFrame(animationLoop);
        });

    </script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分