分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>S-Curve Motion Profile - Fast & Stable</title>
    <style>
        :root {
            --bg-color: #070B14;
            --grid-color: rgba(41, 53, 72, 0.4);
            --text-main: #E2E8F0;
            --text-muted: #64748B;
            --accent-fast: #00F0FF;     /* 快速启动阶段颜色 */
            --accent-stable: #FF9F1C;   /* 阻尼减速阶段颜色 */
            --accent-pos: #8B5CF6;      /* 位移曲线颜色 */
            --panel-bg: rgba(15, 23, 42, 0.7);
            --border-color: rgba(51, 65, 85, 0.8);
        }

        body {
            margin: 0;
            padding: 0;
            background-color: var(--bg-color);
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            overflow: hidden;
            color: var(--text-main);
        }

        #animation-container {
            width: 100%;
            max-width: 1200px;
            aspect-ratio: 16 / 9;
            position: relative;
            background: radial-gradient(circle at 50% 50%, #0F172A 0%, var(--bg-color) 100%);
            box-shadow: 0 0 100px rgba(0, 240, 255, 0.05) inset;
            border: 1px solid rgba(255, 255, 255, 0.05);
        }

        svg {
            width: 100%;
            height: 100%;
            display: block;
        }

        /* 隐藏滚动条 */
        ::-webkit-scrollbar { display: none; }

        /* SVG 样式精调 */
        .text-sm { font-size: 12px; font-weight: 400; letter-spacing: 0.05em; }
        .text-xs { font-size: 10px; font-weight: 300; letter-spacing: 0.05em; }
        .text-mono { font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; }
        
        .glow-fast { filter: drop-shadow(0 0 6px rgba(0, 240, 255, 0.6)); }
        .glow-stable { filter: drop-shadow(0 0 6px rgba(255, 159, 28, 0.6)); }
        .glow-pos { filter: drop-shadow(0 0 6px rgba(139, 92, 246, 0.6)); }

        .data-panel {
            fill: var(--panel-bg);
            stroke: var(--border-color);
            stroke-width: 1;
            rx: 6;
        }
    </style>
</head>
<body>

<div id="animation-container">
    <svg viewBox="0 0 1200 675" preserveAspectRatio="xMidYMid meet" id="main-svg">
        <defs>
            <!-- 背景网格图案 -->
            <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
                <path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--grid-color)" stroke-width="0.5"/>
            </pattern>
            <pattern id="subgrid" width="10" height="10" patternUnits="userSpaceOnUse">
                <path d="M 10 0 L 0 0 0 10" fill="none" stroke="var(--grid-color)" stroke-width="0.15"/>
            </pattern>

            <!-- 渐变定义 -->
            <linearGradient id="cylinder-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="#334155" />
                <stop offset="50%" stop-color="#94A3B8" />
                <stop offset="100%" stop-color="#1E293B" />
            </linearGradient>
            
            <linearGradient id="rod-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="#94A3B8" />
                <stop offset="50%" stop-color="#F8FAFC" />
                <stop offset="100%" stop-color="#64748B" />
            </linearGradient>

            <linearGradient id="vel-area-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="var(--accent-fast)" stop-opacity="0.3" />
                <stop offset="100%" stop-color="var(--accent-fast)" stop-opacity="0.0" />
            </linearGradient>

            <linearGradient id="vel-area-stable-grad" x1="0%" y1="0%" x2="0%" y2="100%">
                <stop offset="0%" stop-color="var(--accent-stable)" stop-opacity="0.3" />
                <stop offset="100%" stop-color="var(--accent-stable)" stop-opacity="0.0" />
            </linearGradient>

            <!-- 滤镜发光效果 -->
            <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
                <feGaussianBlur stdDeviation="3" result="blur" />
                <feMerge>
                    <feMergeNode in="blur" />
                    <feMergeNode in="SourceGraphic" />
                </feMerge>
            </filter>
        </defs>

        <!-- 1. 背景网格层 -->
        <rect width="100%" height="100%" fill="url(#subgrid)" />
        <rect width="100%" height="100%" fill="url(#grid)" />

        <!-- ==================== 顶部:执行器与物理模型 ==================== -->
        <g id="actuator-group" transform="translate(100, 120)">
            <!-- 导轨背景 -->
            <rect x="0" y="-5" width="1000" height="10" fill="#0F172A" stroke="#1E293B" rx="2" />
            <line x1="0" y1="0" x2="1000" y2="0" stroke="rgba(255,255,255,0.1)" stroke-dasharray="4 4" />

            <!-- 刻度线 -->
            <g stroke="#475569" stroke-width="1" opacity="0.6">
                <line x1="0" y1="15" x2="0" y2="25" />
                <text x="0" y="40" fill="#94A3B8" class="text-xs" text-anchor="middle">0mm</text>
                
                <line x1="900" y1="15" x2="900" y2="25" />
                <text x="900" y="40" fill="#94A3B8" class="text-xs" text-anchor="middle">120mm</text>
            </g>

            <!-- 执行缸体 (左侧固定) -->
            <rect x="-60" y="-30" width="80" height="60" fill="url(#cylinder-grad)" rx="4" stroke="#0F172A" stroke-width="2"/>
            <!-- 伺服/阻尼阀控制模块 (概念化展示) -->
            <rect x="-40" y="-45" width="40" height="15" fill="#1E293B" rx="2" stroke="#475569" />
            <circle id="valve-indicator" cx="-20" cy="-37.5" r="3" fill="var(--accent-fast)" class="glow-fast" />
            <text x="-50" y="-55" fill="#94A3B8" class="text-xs">伺服控制 / 阻尼调节阀</text>

            <!-- 动态运动部分 (推杆 + 负载) -->
            <g id="moving-parts" transform="translate(0, 0)">
                <!-- 活塞杆 -->
                <rect x="-50" y="-8" width="100" height="16" fill="url(#rod-grad)" id="piston-rod" />
                <!-- 负载滑块 -->
                <rect x="0" y="-25" width="60" height="50" fill="rgba(30, 41, 59, 0.9)" stroke="var(--accent-fast)" stroke-width="2" rx="4" id="payload-block" class="glow-fast" />
                
                <!-- 负载上的受力/状态指示 -->
                <path d="M 15 -10 L 45 -10 L 45 10 L 15 10 Z" fill="rgba(0,240,255,0.1)" id="payload-inner-glow" />
                <text x="30" y="4" fill="var(--text-main)" class="text-xs text-mono" text-anchor="middle" font-weight="bold">LOAD</text>
            </g>
        </g>

        <!-- ==================== 底部:时序与状态曲线 ==================== -->
        <g id="graph-group" transform="translate(100, 520)">
            <!-- 坐标轴 -->
            <line x1="0" y1="0" x2="1000" y2="0" stroke="#334155" stroke-width="2" /> <!-- X 轴 (时间) -->
            <line x1="0" y1="0" x2="0" y2="-280" stroke="#334155" stroke-width="2" /> <!-- Y 轴 (幅值) -->
            
            <!-- Y轴标签 (极简) -->
            <text x="-15" y="-270" fill="var(--text-muted)" class="text-xs" text-anchor="end">Max</text>
            <text x="-15" y="-5" fill="var(--text-muted)" class="text-xs" text-anchor="end">0</text>
            
            <!-- X轴标签 -->
            <text x="1000" y="20" fill="var(--text-muted)" class="text-xs" text-anchor="end">Time (t)</text>
            <text x="900" y="20" fill="var(--text-muted)" class="text-xs" text-anchor="middle">目标位</text>

            <!-- 目标位指示虚线 -->
            <line x1="900" y1="0" x2="900" y2="-280" stroke="rgba(255,255,255,0.1)" stroke-dasharray="4 4" />
            <line x1="0" y1="-250" x2="1000" y2="-250" stroke="rgba(139, 92, 246, 0.2)" stroke-dasharray="2 4" /> <!-- 位移上限基准 -->

            <!-- 动态绘制的曲线 -->
            <!-- 速度填充区域 (分为加速和减速两段颜色,通过JS遮罩或双路径实现,此处采用叠加路径方式简化) -->
            <path id="vel-area-path" d="" fill="url(#vel-area-grad)" opacity="0.8"/>
            <path id="vel-area-stable-path" d="" fill="url(#vel-area-stable-grad)" opacity="0.8"/>
            
            <!-- 速度曲线 v(t) - 钟形 -->
            <path id="vel-path" d="" fill="none" stroke="var(--accent-fast)" stroke-width="3" class="glow-fast" stroke-linecap="round" stroke-linejoin="round"/>
            
            <!-- 位移曲线 s(t) - S型 -->
            <path id="pos-path" d="" fill="none" stroke="var(--accent-pos)" stroke-width="2" class="glow-pos" stroke-dasharray="4 4"/>

            <!-- 当前时间游标 -->
            <g id="time-cursor" transform="translate(0, 0)">
                <line x1="0" y1="10" x2="0" y2="-280" stroke="rgba(255,255,255,0.3)" stroke-width="1" />
                <circle cx="0" cy="0" r="4" fill="var(--text-main)" id="cursor-dot-vel" />
                <circle cx="0" cy="0" r="3" fill="var(--accent-pos)" id="cursor-dot-pos" />
            </g>
        </g>

        <!-- ==================== 信息面板与数据 ==================== -->
        <!-- 左上角:系统参数 -->
        <g transform="translate(40, 40)">
            <rect width="220" height="70" class="data-panel" />
            <text x="15" y="25" fill="var(--text-main)" class="text-sm font-bold">最终理想解 (IFR) 状态</text>
            <text x="15" y="45" fill="var(--text-muted)" class="text-xs text-mono">V_avg : 30 mm/s</text>
            <text x="110" y="45" fill="var(--text-muted)" class="text-xs text-mono">A_max : 0.5 g</text>
            <text x="15" y="60" fill="var(--text-muted)" class="text-xs">无冲击 · 自适应相变协调</text>
        </g>

        <!-- 右上角:实时状态监控 -->
        <g transform="translate(940, 40)">
            <rect width="220" height="90" class="data-panel" />
            <text x="15" y="25" fill="var(--text-muted)" class="text-xs">当前系统相位</text>
            <text x="15" y="48" fill="var(--accent-fast)" class="text-sm font-bold text-mono glow-fast" id="phase-text">STARTUP_ACCEL</text>
            
            <!-- 进度条 -->
            <rect x="15" y="65" width="190" height="4" fill="#1E293B" rx="2" />
            <rect x="15" y="65" width="0" height="4" fill="var(--accent-fast)" rx="2" id="phase-progress" class="glow-fast" />
        </g>

        <!-- 图例 (极简, 置于右下角附近) -->
        <g transform="translate(960, 600)">
            <line x1="0" y1="0" x2="20" y2="0" stroke="var(--accent-pos)" stroke-width="2" stroke-dasharray="4 4" />
            <text x="30" y="4" fill="var(--text-muted)" class="text-xs">位移 S(t)</text>
            
            <line x1="0" y1="20" x2="20" y2="20" stroke="var(--accent-fast)" stroke-width="2" />
            <text x="30" y="24" fill="var(--text-muted)" class="text-xs">速度 V(t)</text>
        </g>

    </svg>
</div>

<script>
    document.addEventListener("DOMContentLoaded", () => {
        // --- 核心参数配置 ---
        const totalDistance = 900;       // 对应 120mm 的像素缩放距离
        const animationDuration = 2500;  // 单次运动时长 (ms)
        const holdDuration = 1000;       // 到达终点后的保持时间 (ms)
        const resetDuration = 800;       // 快速复位时长 (ms)
        const cycleDuration = animationDuration + holdDuration + resetDuration;
        
        const graphWidth = 900;          // 图表X轴宽度
        const maxVelY = 180;             // 速度曲线最大Y高度 (像素)
        const maxPosY = 250;             // 位移曲线最大Y高度 (像素)

        // --- DOM 元素引用 ---
        const movingParts = document.getElementById('moving-parts');
        const payloadBlock = document.getElementById('payload-block');
        const pistonRod = document.getElementById('piston-rod');
        const valveIndicator = document.getElementById('valve-indicator');
        const payloadInnerGlow = document.getElementById('payload-inner-glow');
        
        const velPath = document.getElementById('vel-path');
        const velAreaPath = document.getElementById('vel-area-path');
        const velAreaStablePath = document.getElementById('vel-area-stable-path');
        const posPath = document.getElementById('pos-path');
        
        const timeCursor = document.getElementById('time-cursor');
        const cursorDotVel = document.getElementById('cursor-dot-vel');
        const cursorDotPos = document.getElementById('cursor-dot-pos');
        
        const phaseText = document.getElementById('phase-text');
        const phaseProgress = document.getElementById('phase-progress');

        // --- 曲线数学模型 (最小加加速度的五次多项式 S 曲线) ---
        // t 范围 [0, 1]
        // S(t) = 10t^3 - 15t^4 + 6t^5 (归一化位移)
        // V(t) = dS/dt = 30t^2 - 60t^3 + 30t^4 (归一化速度)
        function getPosition(t) {
            return 10 * Math.pow(t, 3) - 15 * Math.pow(t, 4) + 6 * Math.pow(t, 5);
        }

        function getVelocity(t) {
            // 返回归一化的速度,最大值出现在 t=0.5 处,最大值为 1.875
            // 为了映射到最大高度,需要除以最大值
            const maxNormVel = 1.875; 
            const normVel = 30 * Math.pow(t, 2) - 60 * Math.pow(t, 3) + 30 * Math.pow(t, 4);
            return normVel / maxNormVel;
        }

        // --- 状态变量 ---
        let startTime = null;
        let velPoints = [];
        let velStablePoints = [];
        let posPoints = [];
        
        const ACCENT_FAST = '#00F0FF';
        const ACCENT_STABLE = '#FF9F1C';

        // --- 主动画循环 ---
        function animate(timestamp) {
            if (!startTime) startTime = timestamp;
            const elapsed = (timestamp - startTime) % cycleDuration;
            
            // 阶段 1: S曲线运动 (0 -> 120mm)
            if (elapsed <= animationDuration) {
                const t = elapsed / animationDuration;
                
                // 计算当前归一化数据
                const s_t = getPosition(t);
                const v_t = getVelocity(t);
                
                // 像素映射
                const currentX = t * graphWidth;
                const currentPosY = -s_t * maxPosY;
                const currentVelY = -v_t * maxVelY;
                
                // 1. 更新物理模型位置
                const actX = s_t * totalDistance;
                movingParts.setAttribute('transform', `translate(${actX}, 0)`);
                // 动态拉长活塞杆的左侧 (保持视觉连贯)
                pistonRod.setAttribute('x', -50 - actX);
                pistonRod.setAttribute('width', 100 + actX);

                // 2. 状态逻辑:减速区间判断 (t > 0.6 作为阻尼介入点)
                const isDecelerating = t > 0.6;
                const currentColor = isDecelerating ? ACCENT_STABLE : ACCENT_FAST;
                const currentGlowClass = isDecelerating ? 'glow-stable' : 'glow-fast';

                // 更新UI颜色和文本
                if (isDecelerating) {
                    phaseText.textContent = "DAMPING_DECEL";
                    phaseText.setAttribute('fill', ACCENT_STABLE);
                    phaseText.className.baseVal = "text-sm font-bold text-mono glow-stable";
                    
                    valveIndicator.setAttribute('fill', ACCENT_STABLE);
                    valveIndicator.className.baseVal = "glow-stable";
                    
                    payloadBlock.setAttribute('stroke', ACCENT_STABLE);
                    payloadBlock.className.baseVal = "glow-stable";
                    payloadInnerGlow.setAttribute('fill', 'rgba(255, 159, 28, 0.15)');
                    
                    velPath.setAttribute('stroke', ACCENT_STABLE);
                    velPath.className.baseVal = "glow-stable";
                    
                    phaseProgress.setAttribute('fill', ACCENT_STABLE);
                    phaseProgress.className.baseVal = "glow-stable";
                } else {
                    phaseText.textContent = t < 0.4 ? "STARTUP_ACCEL" : "STEADY_TRANSIT";
                    phaseText.setAttribute('fill', ACCENT_FAST);
                    phaseText.className.baseVal = "text-sm font-bold text-mono glow-fast";
                    
                    valveIndicator.setAttribute('fill', ACCENT_FAST);
                    valveIndicator.className.baseVal = "glow-fast";
                    
                    payloadBlock.setAttribute('stroke', ACCENT_FAST);
                    payloadBlock.className.baseVal = "glow-fast";
                    payloadInnerGlow.setAttribute('fill', 'rgba(0, 240, 255, 0.1)');
                    
                    velPath.setAttribute('stroke', ACCENT_FAST);
                    velPath.className.baseVal = "glow-fast";
                    
                    phaseProgress.setAttribute('fill', ACCENT_FAST);
                    phaseProgress.className.baseVal = "glow-fast";
                }

                // 更新进度条
                phaseProgress.setAttribute('width', t * 190);

                // 3. 动态绘制图表曲线
                posPoints.push(`${currentX},${currentPosY}`);
                
                // 将速度曲线分段推入数组以实现双色填充
                if (isDecelerating) {
                    velStablePoints.push(`${currentX},${currentVelY}`);
                } else {
                    velPoints.push(`${currentX},${currentVelY}`);
                    // 确保平滑过渡,减速段的起点连接加速段的终点
                    if (velStablePoints.length === 0) {
                        velStablePoints.push(`${currentX},${currentVelY}`);
                    }
                }

                // 生成 SVG Path D 字符串
                posPath.setAttribute('d', 'M 0,0 L ' + posPoints.join(' L '));
                
                // 速度曲线线框 (合并数组绘制以保持连贯)
                const fullVelPath = [...velPoints, ...velStablePoints];
                if (fullVelPath.length > 0) {
                    velPath.setAttribute('d', 'M 0,0 L ' + fullVelPath.join(' L '));
                }

                // 速度填充区域
                if (velPoints.length > 0) {
                    velAreaPath.setAttribute('d', `M 0,0 L ${velPoints.join(' L ')} L ${velPoints[velPoints.length-1].split(',')[0]},0 Z`);
                }
                if (velStablePoints.length > 0) {
                    const startX = velStablePoints[0].split(',')[0];
                    velAreaStablePath.setAttribute('d', `M ${startX},0 L ${velStablePoints.join(' L ')} L ${currentX},0 Z`);
                }

                // 更新游标位置
                timeCursor.setAttribute('transform', `translate(${currentX}, 0)`);
                cursorDotVel.setAttribute('cy', currentVelY);
                cursorDotPos.setAttribute('cy', currentPosY);

            } 
            // 阶段 2: 保持状态 (Hold)
            else if (elapsed <= animationDuration + holdDuration) {
                phaseText.textContent = "TARGET_REACHED";
                phaseText.setAttribute('fill', var(--text-main));
                phaseText.className.baseVal = "text-sm font-bold text-mono";
                
                valveIndicator.setAttribute('fill', '#475569');
                valveIndicator.className.baseVal = "";
                
                payloadBlock.setAttribute('stroke', '#64748B');
                payloadBlock.className.baseVal = "";
                payloadInnerGlow.setAttribute('fill', 'rgba(255, 255, 255, 0.05)');
                
                velPath.setAttribute('stroke', '#64748B');
                velPath.className.baseVal = "";
                phaseProgress.setAttribute('fill', '#64748B');
                phaseProgress.className.baseVal = "";
            } 
            // 阶段 3: 快速复位 (重置数据准备下一轮)
            else {
                phaseText.textContent = "SYSTEM_RESET";
                const resetT = (elapsed - animationDuration - holdDuration) / resetDuration;
                // 使用缓出曲线复位
                const easeOut = 1 - Math.pow(1 - resetT, 3);
                
                const actX = (1 - easeOut) * totalDistance;
                movingParts.setAttribute('transform', `translate(${actX}, 0)`);
                pistonRod.setAttribute('x', -50 - actX);
                pistonRod.setAttribute('width', 100 + actX);
                
                phaseProgress.setAttribute('width', (1 - easeOut) * 190);

                // 在复位快结束时清空路径数据
                if (resetT > 0.9) {
                    velPoints = [];
                    velStablePoints = [];
                    posPoints = [];
                    velPath.setAttribute('d', '');
                    posPath.setAttribute('d', '');
                    velAreaPath.setAttribute('d', '');
                    velAreaStablePath.setAttribute('d', '');
                    timeCursor.setAttribute('transform', `translate(0, 0)`);
                }
            }

            requestAnimationFrame(animate);
        }

        // 启动动画
        requestAnimationFrame(animate);
    });
</script>

</body>
</html>
积分规则:每轮对话扣10分