分享图
A
动画渲染工坊
就绪

这是为您生成的一个展示机械蛇“变刚度推进”原理的高保真 SVG 交互式动画网页源码,用于直观演示 TRIZ 最终理想解(IFR)的核心机制。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>机械蛇变刚度推进 - TRIZ IFR 原理动画</title>
    <style>
        :root {
            --bg: #080d16;
            --bg-panel: #0d1422;
            --border: #1a2540;
            --text: #c8d6e5;
            --text-dim: #6b7d99;
            --accent-warm: #f08c40;
            --accent-hot: #ff5c35;
            --accent-cold: #4a7db5;
            --accent-rigid: #3b5f85;
            --glow-warm: rgba(240, 140, 64, 0.5);
            --glow-hot: rgba(255, 92, 53, 0.6);
            --gold: #e2b04a;
            --cyan: #3cc6d0;
            --font-display: 'Georgia', 'Times New Roman', serif;
            --font-body: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            --font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', 'Monaco', monospace;
        }

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

        body {
            background: var(--bg);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: var(--font-body);
            color: var(--text);
            overflow-x: hidden;
            background-image:
                radial-gradient(ellipse at 50% 30%, rgba(30, 60, 110, 0.25) 0%, transparent 70%),
                radial-gradient(ellipse at 85% 65%, rgba(200, 100, 40, 0.08) 0%, transparent 55%),
                radial-gradient(ellipse at 15% 60%, rgba(60, 140, 200, 0.06) 0%, transparent 50%);
        }

        .main-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
            max-width: 1050px;
            padding: 18px 20px 28px;
            gap: 16px;
        }

        /* 标题区域 */
        .header {
            text-align: center;
            width: 100%;
        }
        .header .tag {
            display: inline-block;
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            color: var(--gold);
            border: 1px solid rgba(226, 176, 74, 0.35);
            padding: 4px 14px;
            border-radius: 20px;
            margin-bottom: 8px;
            background: rgba(226, 176, 74, 0.05);
        }
        .header h1 {
            font-family: var(--font-display);
            font-size: 1.55rem;
            font-weight: 400;
            letter-spacing: 0.03em;
            color: #e8edf5;
            line-height: 1.35;
            max-width: 680px;
            margin: 0 auto;
        }
        .header h1 .highlight {
            color: var(--accent-warm);
            font-style: italic;
        }

        /* Canvas 容器 */
        .canvas-wrapper {
            position: relative;
            width: 100%;
            max-width: 950px;
            aspect-ratio: 950 / 520;
            background: var(--bg-panel);
            border-radius: 16px;
            border: 1px solid var(--border);
            box-shadow:
                0 0 0 1px rgba(255, 255, 255, 0.02),
                0 8px 40px rgba(0, 0, 0, 0.45),
                inset 0 1px 0 rgba(255, 255, 255, 0.015);
            overflow: hidden;
            cursor: default;
        }
        .canvas-wrapper canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        /* 截面图叠加层 */
        .cross-section-overlay {
            position: absolute;
            top: 16px;
            right: 18px;
            width: 130px;
            height: 130px;
            pointer-events: none;
            z-index: 5;
        }
        .cross-section-overlay svg {
            width: 100%;
            height: 100%;
        }
        .cross-section-label {
            position: absolute;
            bottom: 2px;
            right: 0;
            font-family: var(--font-mono);
            font-size: 0.58rem;
            letter-spacing: 0.08em;
            color: var(--text-dim);
            text-align: right;
            white-space: nowrap;
        }

        /* 刚度分布条 */
        .stiffness-bar-container {
            position: absolute;
            bottom: 20px;
            left: 30px;
            right: 170px;
            height: 8px;
            border-radius: 4px;
            background: rgba(255, 255, 255, 0.04);
            pointer-events: none;
            z-index: 4;
            overflow: visible;
        }
        .stiffness-bar-label {
            position: absolute;
            top: -18px;
            left: 0;
            font-family: var(--font-mono);
            font-size: 0.55rem;
            letter-spacing: 0.06em;
            color: var(--text-dim);
        }
        .stiffness-bar-fill {
            height: 100%;
            border-radius: 4px;
            transition: width 0.3s ease, background 0.4s ease;
            background: linear-gradient(90deg, var(--accent-rigid) 0%, var(--accent-cold) 40%, var(--accent-warm) 70%, var(--accent-hot) 100%);
            opacity: 0.8;
        }

        /* 控制面板 */
        .controls {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: center;
            gap: 22px;
            width: 100%;
            max-width: 950px;
            padding: 14px 20px;
            background: var(--bg-panel);
            border-radius: 12px;
            border: 1px solid var(--border);
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        }
        .control-group {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .control-group label {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.05em;
            color: var(--text-dim);
            white-space: nowrap;
            min-width: 52px;
            text-align: right;
        }
        .control-group input[type="range"] {
            -webkit-appearance: none;
            width: 120px;
            height: 5px;
            border-radius: 3px;
            background: rgba(255, 255, 255, 0.1);
            outline: none;
            cursor: pointer;
        }
        .control-group input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 22px;
            height: 22px;
            border-radius: 50%;
            background: var(--accent-warm);
            cursor: pointer;
            border: 2px solid rgba(255, 255, 255, 0.3);
            box-shadow: 0 0 12px var(--glow-warm);
            transition: all 0.2s ease;
        }
        .control-group input[type="range"]::-webkit-slider-thumb:hover {
            box-shadow: 0 0 20px var(--glow-hot);
            transform: scale(1.1);
        }
        .control-value {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            color: var(--text);
            min-width: 36px;
            text-align: left;
        }
        .btn {
            font-family: var(--font-mono);
            font-size: 0.7rem;
            letter-spacing: 0.06em;
            padding: 8px 18px;
            border-radius: 22px;
            cursor: pointer;
            border: 1px solid var(--border);
            background: rgba(255, 255, 255, 0.04);
            color: var(--text);
            transition: all 0.25s ease;
            white-space: nowrap;
            position: relative;
            overflow: hidden;
        }
        .btn:hover {
            background: rgba(255, 255, 255, 0.1);
            border-color: rgba(255, 255, 255, 0.3);
            box-shadow: 0 0 18px rgba(240, 140, 64, 0.2);
        }
        .btn.active {
            background: rgba(240, 140, 64, 0.18);
            border-color: var(--accent-warm);
            color: var(--accent-warm);
            box-shadow: 0 0 14px rgba(240, 140, 64, 0.3);
        }
        .btn-play {
            min-width: 55px;
            text-align: center;
            font-size: 0.85rem;
        }

        /* 图例 */
        .legend-row {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            justify-content: center;
            align-items: center;
            font-family: var(--font-mono);
            font-size: 0.62rem;
            letter-spacing: 0.05em;
            color: var(--text-dim);
        }
        .legend-dot {
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            margin-right: 5px;
            vertical-align: middle;
        }
        .legend-dot.warm {
            background: var(--accent-warm);
            box-shadow: 0 0 8px var(--glow-warm);
        }
        .legend-dot.cold {
            background: var(--accent-rigid);
            box-shadow: 0 0 4px rgba(74, 125, 181, 0.4);
        }
        .legend-dot.gold {
            background: var(--gold);
            box-shadow: 0 0 6px rgba(226, 176, 74, 0.5);
        }

        /* 响应式 */
        @media (max-width: 768px) {
            .main-container {
                padding: 10px 8px 16px;
                gap: 10px;
            }
            .header h1 {
                font-size: 1.2rem;
                max-width: 95%;
            }
            .canvas-wrapper {
                border-radius: 10px;
                aspect-ratio: 4/3;
            }
            .cross-section-overlay {
                width: 90px;
                height: 90px;
                top: 8px;
                right: 8px;
            }
            .stiffness-bar-container {
                left: 14px;
                right: 110px;
                bottom: 10px;
                height: 5px;
            }
            .controls {
                gap: 10px;
                padding: 10px 12px;
            }
            .control-group input[type="range"] {
                width: 70px;
            }
            .btn {
                padding: 6px 12px;
                font-size: 0.62rem;
            }
        }
    </style>
</head>
<body>
    <div class="main-container">
        <!-- 标题 -->
        <div class="header">
            <span class="tag">TRIZ · 最终理想解 IFR</span>
            <h1>
                变刚度机械蛇:<span class="highlight">空间分离</span>柔性与刚性段,
                无需变摩擦机构即可<span class="highlight">自发推进</span>
            </h1>
        </div>

        <!-- Canvas 动画容器 -->
        <div class="canvas-wrapper" id="canvasWrapper">
            <canvas id="mainCanvas"></canvas>
            <!-- 截面图叠加 -->
            <div class="cross-section-overlay" id="crossSectionOverlay">
                <svg viewBox="0 0 130 130" xmlns="http://www.w3.org/2000/svg">
                    <defs>
                        <radialGradient id="csBg" cx="50%" cy="50%" r="55%">
                            <stop offset="0%" stop-color="#1a2540" stop-opacity="0.95" />
                            <stop offset="100%" stop-color="#0d1422" stop-opacity="0.98" />
                        </radialGradient>
                        <filter id="csGlow">
                            <feGaussianBlur stdDeviation="1.2" result="blur" />
                            <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
                        </filter>
                    </defs>
                    <!-- 背景 -->
                    <circle cx="65" cy="65" r="60" fill="url(#csBg)" stroke="#1e3050" stroke-width="1.2" />
                    <!-- 最外层:硅胶表皮 1mm (半径约27px对应实际) -->
                    <circle cx="65" cy="65" r="48" fill="none" stroke="#6b7d99" stroke-width="5" opacity="0.7" />
                    <!-- 横向纹路 -->
                    <circle cx="65" cy="65" r="47" fill="none" stroke="#8899b0" stroke-width="0.5" stroke-dasharray="2.5 3.5" opacity="0.5" />
                    <circle cx="65" cy="65" r="44.5" fill="none" stroke="#8899b0" stroke-width="0.4" stroke-dasharray="1.8 4" opacity="0.4" />
                    <!-- 中间层:SMP 可变刚度层 3mm -->
                    <circle cx="65" cy="65" r="41" fill="none" stroke="#e8a860" stroke-width="9" opacity="0.75" filter="url(#csGlow)" />
                    <!-- 电热丝缠绕示意(螺旋投影为虚线环) -->
                    <circle cx="65" cy="65" r="37" fill="none" stroke="#ff7043" stroke-width="0.7" stroke-dasharray="1.5 2.5" opacity="0.9" />
                    <circle cx="65" cy="65" r="34" fill="none" stroke="#ff7043" stroke-width="0.5" stroke-dasharray="1.2 3" opacity="0.6" />
                    <!-- 最内层:铝合金芯轴 12mm直径 -->
                    <circle cx="65" cy="65" r="21" fill="#3a4a60" stroke="#8899b0" stroke-width="1.5" />
                    <circle cx="65" cy="65" r="16" fill="#2a3545" stroke="#556880" stroke-width="1" />
                    <circle cx="65" cy="65" r="6" fill="#1a2535" stroke="#445566" stroke-width="0.8" />
                    <!-- 走线孔 -->
                    <circle cx="65" cy="65" r="4" fill="#0a0f18" />
                    <!-- 标注线 -->
                    <line x1="65" y1="17" x2="95" y2="6" stroke="#6b7d99" stroke-width="0.5" />
                    <text x="98" y="9" fill="#6b7d99" font-size="7" font-family="'SF Mono','Consolas',monospace">硅胶表皮</text>
                    <line x1="65" y1="26" x2="100" y2="22" stroke="#e8a860" stroke-width="0.5" />
                    <text x="103" y="25" fill="#e8a860" font-size="7" font-family="'SF Mono','Consolas',monospace">SMP层</text>
                    <line x1="65" y1="44" x2="90" y2="52" stroke="#8899b0" stroke-width="0.5" />
                    <text x="93" y="55" fill="#8899b0" font-size="6.5" font-family="'SF Mono','Consolas',monospace">Al芯轴</text>
                </svg>
                <span class="cross-section-label">截面:三层同心管</span>
            </div>
            <!-- 刚度分布指示条 -->
            <div class="stiffness-bar-container" id="stiffnessBarContainer">
                <span class="stiffness-bar-label">沿体刚度分布</span>
                <div class="stiffness-bar-fill" id="stiffnessBarFill" style="width:65%;"></div>
            </div>
        </div>

        <!-- 图例 -->
        <div class="legend-row">
            <span><span class="legend-dot warm"></span> 加热软化段(柔性·SMP &gt; Tg)</span>
            <span><span class="legend-dot cold"></span> 冷却刚性段(支撑自重)</span>
            <span><span class="legend-dot gold"></span> 推进力方向</span>
            <span style="color:var(--cyan);">●</span> 拉线肌腱示意
        </div>

        <!-- 控制面板 -->
        <div class="controls" id="controlsPanel">
            <div class="control-group">
                <label for="speedSlider">波速</label>
                <input type="range" id="speedSlider" min="0.2" max="2.5" step="0.1" value="1.0" title="蛇形波传播速度">
                <span class="control-value" id="speedValue">1.0×</span>
            </div>
            <div class="control-group">
                <label for="amplitudeSlider">振幅</label>
                <input type="range" id="amplitudeSlider" min="0.3" max="1.8" step="0.05" value="1.0" title="弯曲幅度">
                <span class="control-value" id="amplitudeValue">1.0×</span>
            </div>
            <div class="control-group">
                <label for="preheatSlider">预热提前</label>
                <input type="range" id="preheatSlider" min="0" max="3.5" step="0.1" value="2.0" title="加热提前量(秒)">
                <span class="control-value" id="preheatValue">2.0s</span>
            </div>
            <button class="btn btn-play active" id="btnPlay" title="播放/暂停">⏯ 播放</button>
            <button class="btn" id="btnHighlight" title="高亮显示加热段">🔆 高亮模式</button>
            <button class="btn" id="btnReset" title="重置视角">↺ 重置</button>
        </div>
    </div>

    <script>
        (function() {
            // ==================== DOM 元素 ====================
            const canvas = document.getElementById('mainCanvas');
            const ctx = canvas.getContext('2d');
            const wrapper = document.getElementById('canvasWrapper');
            const stiffnessBarFill = document.getElementById('stiffnessBarFill');
            const speedSlider = document.getElementById('speedSlider');
            const amplitudeSlider = document.getElementById('amplitudeSlider');
            const preheatSlider = document.getElementById('preheatSlider');
            const speedValue = document.getElementById('speedValue');
            const amplitudeValue = document.getElementById('amplitudeValue');
            const preheatValue = document.getElementById('preheatValue');
            const btnPlay = document.getElementById('btnPlay');
            const btnHighlight = document.getElementById('btnHighlight');
            const btnReset = document.getElementById('btnReset');

            // ==================== 状态变量 ====================
            let isPlaying = true;
            let highlightMode = false;
            let waveSpeed = 1.0;
            let waveAmplitude = 1.0;
            let preheatSeconds = 2.0;
            const WAVE_PERIOD = 5.0; // 基础周期(秒)
            const NUM_SEGMENTS = 14;
            const SEGMENT_LENGTH = 40; // 每个体节在身体弧长上的跨度(px)
            const BASE_AMPLITUDE = 52; // 基础振幅(px)
            const WAVELENGTH_PX = 340; // 波长(px)
            const BODY_Y_CENTER = 240; // 蛇身体中心Y坐标(在Canvas内)
            const BODY_X_START = 60; // 蛇身体起始X
            const GROUND_Y = 340; // 地面Y坐标

            // ==================== Canvas 尺寸适配 ====================
            function resizeCanvas() {
                const rect = wrapper.getBoundingClientRect();
                const dpr = Math.min(window.devicePixelRatio || 1, 2);
                const w = rect.width;
                const h = rect.height;
                if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
                    canvas.width = w * dpr;
                    canvas.height = h * dpr;
                    canvas.style.width = w + 'px';
                    canvas.style.height = h + 'px';
                    ctx.setTransform(1, 0, 0, 1, 0, 0);
                    ctx.scale(dpr, dpr);
                }
                return { w, h };
            }

            // ==================== 粒子系统 ====================
            class HeatParticle {
                constructor(x, y) {
                    this.x = x;
                    this.y = y;
                    this.vx = (Math.random() - 0.5) * 0.6;
                    this.vy = -(Math.random() * 1.5 + 0.8);
                    this.life = 1.0;
                    this.decay = 0.012 + Math.random() * 0.025;
                    this.size = 1.2 + Math.random() * 3.5;
                    this.opacity = 0.5 + Math.random() * 0.5;
                }
                update() {
                    this.x += this.vx;
                    this.y += this.vy;
                    this.life -= this.decay;
                    this.opacity *= 0.985;
                }
                draw(ctx) {
                    if (this.life <= 0) return;
                    const alpha = this.life * this.opacity;
                    const grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
                    grad.addColorStop(0, `rgba(255,160,60,${alpha})`);
                    grad.addColorStop(0.5, `rgba(255,110,35,${alpha * 0.7})`);
                    grad.addColorStop(1, `rgba(255,70,20,0)`);
                    ctx.fillStyle = grad;
                    ctx.beginPath();
                    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                    ctx.fill();
                }
                get dead() { return this.life <= 0; }
            }
            let particles = [];

            function spawnParticles(x, y, count) {
                for (let i = 0; i < count; i++) {
                    particles.push(new HeatParticle(x + (Math.random() - 0.5) * 14, y + (Math.random() - 0.5) * 8));
                }
            }

            function updateAndDrawParticles(ctx) {
                particles = particles.filter(p => !p.dead);
                for (const p of particles) {
                    p.update();
                    p.draw(ctx);
                }
                // 限制粒子总数
                if (particles.length > 300) {
                    particles.splice(0, particles.length - 300);
                }
            }

            // ==================== 蛇形波计算 ====================
            function getSnakeCurveY(s, t) {
                // s: 弧长参数(沿身体)
                // t: 当前时间(秒)
                const amp = BASE_AMPLITUDE * waveAmplitude;
                const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
                return BODY_Y_CENTER + amp * Math.sin((2 * Math.PI * s) / WAVELENGTH_PX - omega * t);
            }

            function getSnakeCurveDerivative(s, t) {
                const amp = BASE_AMPLITUDE * waveAmplitude;
                const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
                return amp * (2 * Math.PI / WAVELENGTH_PX) * Math.cos((2 * Math.PI * s) / WAVELENGTH_PX - omega * t);
            }

            function getCurvatureApprox(s, t) {
                // 曲率近似 = |d²y/ds²| (小斜率近似)
                const amp = BASE_AMPLITUDE * waveAmplitude;
                const omega = (2 * Math.PI) / WAVE_PERIOD * waveSpeed;
                const phase = (2 * Math.PI * s) / WAVELENGTH_PX - omega * t;
                return amp * Math.pow(2 * Math.PI / WAVELENGTH_PX, 2) * Math.abs(Math.sin(phase));
            }

            // ==================== 绘制函数 ====================
            function drawGridBackground(ctx, w, h) {
                ctx.strokeStyle = 'rgba(255,255,255,0.025)';
                ctx.lineWidth = 0.5;
                const spacing = 32;
                for (let x = spacing; x < w; x += spacing) {
                    ctx.beginPath();
                    ctx.moveTo(x, 0);
                    ctx.lineTo(x, h);
                    ctx.stroke();
                }
                for (let y = spacing; y < h; y += spacing) {
                    ctx.beginPath();
                    ctx.moveTo(0, y);
                    ctx.lineTo(w, y);
                    ctx.stroke();
                }
                // 主参考线
                ctx.strokeStyle = 'rgba(255,255,255,0.06)';
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(0, BODY_Y_CENTER);
                ctx.lineTo(w, BODY_Y_CENTER);
                ctx.setLineDash([4, 16]);
                ctx.stroke();
                ctx.setLineDash([]);
            }

            function drawGround(ctx, w) {
                // 地面主体
                const gy = GROUND_Y;
                const groundGrad = ctx.createLinearGradient(0, gy, 0, gy + 60);
                groundGrad.addColorStop(0, 'rgba(40,50,65,0.7)');
                groundGrad.addColorStop(0.3, 'rgba(30,38,50,0.6)');
                groundGrad.addColorStop(1, 'rgba(15,20,30,0)');
                ctx.fillStyle = groundGrad;
                ctx.fillRect(0, gy, w, 60);

                // 地面线
                ctx.strokeStyle = 'rgba(180,190,210,0.35)';
                ctx.lineWidth = 1.2;
                ctx.beginPath();
                ctx.moveTo(0, gy);
                ctx.lineTo(w, gy);
                ctx.stroke();

                // 地面纹理点
                ctx.fillStyle = 'rgba(180,190,210,0.25)';
                const seed = 42;
                for (let i = 0; i < w; i += 7) {
                    const pseudoRandom = ((Math.sin(i * 0.37 + seed) * 0.5 + 0.5));
                    if (pseudoRandom > 0.55) {
                        const tx = i;
                        const ty = gy + 4 + pseudoRandom * 10;
                        ctx.beginPath();
                        ctx.arc(tx, ty, 0.6 + pseudoRandom * 1.2, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }

                // 标签
                ctx.fillStyle = 'rgba(180,190,210,0.5)';
                ctx.font = '9px "SF Mono","Consolas",monospace';
                ctx.fillText('地面 · 无需主动变摩擦机构', w - 200, gy - 8);
            }

            function drawSegment(ctx, cx, cy, angle, segLength, stiffnessRatio, index) {
                // stiffnessRatio: 0=完全刚性(冷), 1=完全柔性(热)
                const halfLen = segLength / 2;
                const thickness = 16;
                const halfThick = thickness / 2;

                // 颜色插值:刚性(冷蓝灰) → 柔性(暖橙)
                const rRigid = 58,
                    gRigid = 90,
                    bRigid = 130; // 冷蓝灰
                const rFlex = 235,
                    gFlex = 130,
                    bFlex = 50; // 暖橙
                const r = Math.round(rRigid + (rFlex - rRigid) * stiffnessRatio);
                const g = Math.round(gRigid + (gFlex - gRigid) * stiffnessRatio);
                const b = Math.round(bRigid + (bFlex - bRigid) * stiffnessRatio);

                ctx.save();
                ctx.translate(cx, cy);
                ctx.rotate(angle);

                // 发光光晕(加热段)
                if (stiffnessRatio > 0.25) {
                    const glowAlpha = stiffnessRatio * 0.55;
                    const glowGrad = ctx.createRadialGradient(0, 0, halfThick * 0.6, 0, 0, halfThick * 2.5);
                    glowGrad.addColorStop(0, `rgba(255,150,50,${glowAlpha})`);
                    glowGrad.addColorStop(0.5, `rgba(255,100,30,${glowAlpha * 0.5})`);
                    glowGrad.addColorStop(1, 'rgba(255,60,20,0)');
                    ctx.fillStyle = glowGrad;
                    ctx.fillRect(-halfLen - 6, -halfThick * 2.2, segLength + 12, halfThick * 4.4);
                }

                // 体节主体
                const bodyGrad = ctx.createLinearGradient(0, -halfThick, 0, halfThick);
                bodyGrad.addColorStop(0, `rgba(${Math.min(r+30,255)},${Math.min(g+25,255)},${Math.min(b+20,255)},0.9)`);
                bodyGrad.addColorStop(0.35, `rgba(${r},${g},${b},0.95)`);
                bodyGrad.addColorStop(0.65, `rgba(${Math.max(r-25,0)},${Math.max(g-20,0)},${Math.max(b-15,0)},0.9)`);
                bodyGrad.addColorStop(1, `rgba(${Math.max(r-40,0)},${Math.max(g-35,0)},${Math.max(b-30,0)},0.85)`);
                ctx.fillStyle = bodyGrad;
                const radius = 6;
                ctx.beginPath();
                ctx.moveTo(-halfLen + radius, -halfThick);
                ctx.lineTo(halfLen - radius, -halfThick);
                ctx.arcTo(halfLen, -halfThick, halfLen, -halfThick + radius, radius);
                ctx.lineTo(halfLen, halfThick - radius);
                ctx.arcTo(halfLen, halfThick, halfLen - radius, halfThick, radius);
                ctx.lineTo(-halfLen + radius, halfThick);
                ctx.arcTo(-halfLen, halfThick, -halfLen, halfThick - radius, radius);
                ctx.lineTo(-halfLen, -halfThick + radius);
                ctx.arcTo(-halfLen, -halfThick, -halfLen + radius, -halfThick, radius);
                ctx.closePath();
                ctx.fill();

                // 边框
                ctx.strokeStyle = `rgba(255,255,255,${0.15 + stiffnessRatio * 0.25})`;
                ctx.lineWidth = 1;
                ctx.stroke();

                // 横向防滑纹路(硅胶表皮纹理)
                ctx.strokeStyle = `rgba(255,255,255,${0.08 + stiffnessRatio * 0.12})`;
                ctx.lineWidth = 0.5;
                const纹路间距 = 4.5;
                for (let dx = -halfLen + 8; dx < halfLen - 6; dx += 纹路间距) {
                    ctx.beginPath();
                    ctx.moveTo(dx, -halfThick + 1.5);
                    ctx.lineTo(dx, halfThick - 1.5);
                    ctx.stroke();
                }

                // 关节连接点标记(两端)
                const jointDotR = 2.5;
                [-halfLen, halfLen].forEach(jx => {
                    ctx.fillStyle = 'rgba(200,200,210,0.7)';
                    ctx.beginPath();
                    ctx.arc(jx, 0, jointDotR, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.strokeStyle = 'rgba(255,255,255,0.4)';
                    ctx.lineWidth = 0.7;
                    ctx.stroke();
                });

                ctx.restore();
            }

            function drawTendonLine(ctx, x1, y1, x2, y2, side) {
                // side: 'left' or 'right' (relative to segment direction)
                const alpha = 0.55;
                ctx.strokeStyle = `rgba(60,200,210,${alpha})`;
                ctx.lineWidth = 0.8;
                ctx.setLineDash([3, 5]);
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.stroke();
                ctx.setLineDash([]);

                // 拉线端点小圆
                ctx.fillStyle = `rgba(60,200,210,${alpha + 0.2})`;
                ctx.beginPath();
                ctx.arc(x1, y1, 1.8, 0, Math.PI * 2);
                ctx.fill();
                ctx.beginPath();
                ctx.arc(x2, y2, 1.8, 0, Math.PI * 2);
                ctx.fill();
            }

            function drawForceArrow(ctx, x, y, dirX, dirY, alpha) {
                const len = 16;
                const headLen = 7;
                const headAngle = Math.PI / 7;
                const angle = Math.atan2(dirY, dirX);
                const ex = x + dirX * len;
                const ey = y + dirY * len;

                ctx.strokeStyle = `rgba(226,176,74,${alpha})`;
                ctx.lineWidth = 1.8;
                ctx.lineCap = 'round';
                ctx.beginPath();
                ctx.moveTo(x, y);
                ctx.lineTo(ex, ey);
                ctx.stroke();

                // 箭头头部
                ctx.fillStyle = `rgba(226,176,74,${alpha})`;
                ctx.beginPath();
                ctx.moveTo(ex, ey);
                ctx.lineTo(
                    ex - headLen * Math.cos(angle - headAngle),
                    ey - headLen * Math.sin(angle - headAngle)
                );
                ctx.lineTo(
                    ex - headLen * Math.cos(angle + headAngle),
                    ey - headLen * Math.sin(angle + headAngle)
                );
                ctx.closePath();
                ctx.fill();
            }

            // ==================== 主渲染循环 ====================
            let animTime = 0; // 动画累计时间(秒)
            let lastFrameTime = null;

            function render(timestamp) {
                const { w, h } = resizeCanvas();
                if (!lastFrameTime) lastFrameTime = timestamp;
                let dt = (timestamp - lastFrameTime) / 1000;
                if (dt > 0.1) dt = 0.1; // 防止大帧跳跃
                if (dt <= 0) dt = 0.016;
                lastFrameTime = timestamp;

                if (isPlaying) {
                    animTime += dt;
                }

                ctx.clearRect(0, 0, w, h);

                // 背景
                const bgGrad = ctx.createRadialGradient(w * 0.5, h * 0.4, w * 0.15, w * 0.5, h * 0.5, w * 0.9);
                bgGrad.addColorStop(0, '#111c2e');
                bgGrad.addColorStop(1, '#080d16');
                ctx.fillStyle = bgGrad;
                ctx.fillRect(0, 0, w, h);

                drawGridBackground(ctx, w, h);
                drawGround(ctx, w);

                // 计算蛇身体总弧长
                const totalArcLength = NUM_SEGMENTS * SEGMENT_LENGTH;
                const bodyStartS = BODY_X_START;

                // 存储每个体节的信息
                const segments = [];
                for (let i = 0; i < NUM_SEGMENTS; i++) {
                    const s = bodyStartS + i * SEGMENT_LENGTH + SEGMENT_LENGTH / 2;
                    const y = getSnakeCurveY(s, animTime);
                    const dyDs = getSnakeCurveDerivative(s, animTime);
                    const angle = Math.atan2(dyDs, 1);
                    // 曲率(用于判断是否需要加热)
                    const curvature = getCurvatureApprox(s, animTime);
                    // 提前加热:使用未来时间的曲率
                    const futureS = s + (preheatSeconds / WAVE_PERIOD) * WAVELENGTH_PX * waveSpeed;
                    const futureCurvature = getCurvatureApprox(futureS, animTime);
                    // 刚度比率:曲率大→需要柔性→加热→stiffnessRatio高
                    const maxCurv = BASE_AMPLITUDE * waveAmplitude * Math.pow(2 * Math.PI / WAVELENGTH_PX, 2);
                    const stiffnessRatio = Math.min(1, Math.max(0, futureCurvature / (maxCurv * 0.55)));
                    // 平滑过渡
                    const smoothRatio = stiffnessRatio < 0.05 ? 0 :
                        stiffnessRatio > 0.95 ? 1 :
                        0.5 - 0.5 * Math.cos(Math.PI * (stiffnessRatio - 0.05) / 0.9);

                    segments.push({
                        index: i,
                        s: s,
                        x: s, // x坐标近似等于弧长参数
                        y: y,
                        angle: angle,
                        curvature: curvature,
                        stiffnessRatio: smoothRatio,
                        isHeated: smoothRatio > 0.35,
                        isHighlyFlexible: smoothRatio > 0.7,
                    });
                }

                // 绘制拉线肌腱(在体节下方)
                for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
                    const segA = segments[i];
                    const segB = segments[i + 1];
                    const mx = (segA.x + segB.x) / 2;
                    const my = (segA.y + segB.y) / 2;
                    const dx = segB.x - segA.x;
                    const dy = segB.y - segA.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    const nx = -dy / dist;
                    const ny = dx / dist;
                    const offsetDist = 10;
                    // 两侧拉线
                    drawTendonLine(ctx,
                        segA.x + nx * offsetDist, segA.y + ny * offsetDist,
                        segB.x + nx * offsetDist, segB.y + ny * offsetDist,
                        'left');
                    drawTendonLine(ctx,
                        segA.x - nx * offsetDist, segA.y - ny * offsetDist,
                        segB.x - nx * offsetDist, segB.y - ny * offsetDist,
                        'right');
                }

                // 绘制体节
                for (const seg of segments) {
                    drawSegment(ctx, seg.x, seg.y, seg.angle, SEGMENT_LENGTH, seg.stiffnessRatio, seg.index);
                }

                // 绘制关节高亮(弯曲大的关节)
                for (let i = 0; i < NUM_SEGMENTS - 1; i++) {
                    const segA = segments[i];
                    const segB = segments[i + 1];
                    const angleDiff = Math.abs(segB.angle - segA.angle);
                    if (angleDiff > 0.08 && segA.stiffnessRatio > 0.3) {
                        const jx = (segA.x + segB.x) / 2;
                        const jy = (segA.y + segB.y) / 2;
                        const glowGrad = ctx.createRadialGradient(jx, jy, 2, jx, jy, 10);
                        glowGrad.addColorStop(0, 'rgba(255,180,80,0.7)');
                        glowGrad.addColorStop(1, 'rgba(255,100,30,0)');
                        ctx.fillStyle = glowGrad;
                        ctx.beginPath();
                        ctx.arc(jx, jy, 10, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }

                // 绘制推进力箭头(波峰/波谷与地面接触点)
                const contactThreshold = 28;
                for (const seg of segments) {
                    const distToGround = Math.abs(seg.y - GROUND_Y);
                    if (distToGround < contactThreshold && seg.stiffnessRatio > 0.3) {
                        const contactAlpha = (1 - distToGround / contactThreshold) * 0.7;
                        // 推进力方向:在波峰处向前,波谷处也向前(蛇形波特征)
                        const pushDirX = 1;
                        const pushDirY = 0;
                        drawForceArrow(ctx, seg.x, GROUND_Y - 2, pushDirX, pushDirY, contactAlpha);
                    }
                }

                // 加热段粒子
                if (isPlaying) {
                    for (const seg of segments) {
                        if (seg.isHighlyFlexible && Math.random() < 0.35) {
                            spawnParticles(seg.x, seg.y - 10, 2);
                        } else if (seg.isHeated && Math.random() < 0.12) {
                            spawnParticles(seg.x, seg.y - 6, 1);
                        }
                    }
                }
                updateAndDrawParticles(ctx);

                // 高亮模式:在加热段绘制轮廓
                if (highlightMode) {
                    for (const seg of segments) {
                        if (seg.isHeated) {
                            ctx.strokeStyle = 'rgba(255,200,80,0.7)';
                            ctx.lineWidth = 2.5;
                            ctx.setLineDash([6, 3]);
                            ctx.beginPath();
                            const hlRad = SEGMENT_LENGTH / 2 + 5;
                            ctx.arc(seg.x, seg.y, hlRad, 0, Math.PI * 2);
                            ctx.stroke();
                            ctx.setLineDash([]);
                        }
                    }
                }

                // 蛇头部标记
                if (segments.length > 0) {
                    const head = segments[0];
                    const headX = head.x - SEGMENT_LENGTH / 2 - 6;
                    const headY = head.y;
                    ctx.fillStyle = '#e8c060';
                    ctx.beginPath();
                    ctx.moveTo(headX - 10, headY);
                    ctx.lineTo(headX + 2, headY - 8);
                    ctx.lineTo(headX + 2, headY + 8);
                    ctx.closePath();
                    ctx.fill();
                    ctx.strokeStyle = 'rgba(255,220,140,0.8)';
                    ctx.lineWidth = 1.2;
                    ctx.stroke();
                    // 头部光晕
                    const headGlow = ctx.createRadialGradient(headX - 3, headY, 2, headX - 3, headY, 16);
                    headGlow.addColorStop(0, 'rgba(255,200,100,0.6)');
                    headGlow.addColorStop(1, 'rgba(255,150,50,0)');
                    ctx.fillStyle = headGlow;
                    ctx.beginPath();
                    ctx.arc(headX - 3, headY, 16, 0, Math.PI * 2);
                    ctx.fill();
                }

                // 尾部标记
                if (segments.length > 0) {
                    const tail = segments[NUM_SEGMENTS - 1];
                    const tailX = tail.x + SEGMENT_LENGTH / 2 + 4;
                    const tailY = tail.y;
                    ctx.fillStyle = '#7a8da0';
                    ctx.beginPath();
                    ctx.moveTo(tailX + 6, tailY);
                    ctx.lineTo(tailX - 2, tailY - 5);
                    ctx.lineTo(tailX - 2, tailY + 5);
                    ctx.closePath();
                    ctx.fill();
                }

                // 更新刚度分布条
                const heatedCount = segments.filter(s => s.isHeated).length;
                const heatedRatio = heatedCount / NUM_SEGMENTS;
                stiffnessBarFill.style.width = (heatedRatio * 100) + '%';
                if (heatedRatio > 0.6) {
                    stiffnessBarFill.style.background =
                        'linear-gradient(90deg, #3b5f85 0%, #e8a040 60%, #ff5c35 100%)';
                } else if (heatedRatio > 0.3) {
                    stiffnessBarFill.style.background =
                        'linear-gradient(90deg, #3b5f85 0%, #4a7db5 45%, #e8a040 100%)';
                } else {
                    stiffnessBarFill.style.background =
                        'linear-gradient(90deg, #3b5f85 0%, #4a7db5 70%, #c88050 100%)';
                }
                stiffnessBarFill.style.opacity = '0.75';

                // 顶部标注文字
                ctx.fillStyle = 'rgba(200,210,225,0.7)';
                ctx.font = '11px "Georgia","Times New Roman",serif';
                ctx.fillText('← 推进方向', 28, 42);
                const labelX = BODY_X_START + totalArcLength + 10;
                ctx.fillText('蛇形波传播 →', labelX - 30, BODY_Y_CENTER - BASE_AMPLITUDE * waveAmplitude - 30);

                // 继续动画循环
                requestAnimationFrame(render);
            }

            // ==================== 事件处理 ====================
            btnPlay.addEventListener('click', () => {
                isPlaying = !isPlaying;
                if (isPlaying) {
                    lastFrameTime = null;
                    btnPlay.classList.add('active');
                    btnPlay.textContent = '⏯ 播放';
                } else {
                    btnPlay.classList.remove('active');
                    btnPlay.textContent = '▶ 暂停中';
                }
            });

            btnHighlight.addEventListener('click', () => {
                highlightMode = !highlightMode;
                if (highlightMode) {
                    btnHighlight.classList.add('active');
                    btnHighlight.textContent = '🔆 高亮:开';
                } else {
                    btnHighlight.classList.remove('active');
                    btnHighlight.textContent = '🔆 高亮模式';
                }
            });

            btnReset.addEventListener('click', () => {
                waveSpeed = 1.0;
                waveAmplitude = 1.0;
                preheatSeconds = 2.0;
                animTime = 0;
                lastFrameTime = null;
                isPlaying = true;
                highlightMode = false;
                particles = [];
                speedSlider.value = 1.0;
                amplitudeSlider.value = 1.0;
                preheatSlider.value = 2.0;
                speedValue.textContent = '1.0×';
                amplitudeValue.textContent = '1.0×';
                preheatValue.textContent = '2.0s';
                btnPlay.classList.add('active');
                btnPlay.textContent = '⏯ 播放';
                btnHighlight.classList.remove('active');
                btnHighlight.textContent = '🔆 高亮模式';
                stiffnessBarFill.style.width = '65%';
                stiffnessBarFill.style.background =
                    'linear-gradient(90deg, #3b5f85 0%, #4a7db5 40%, #e8a040 70%, #ff5c35 100%)';
                stiffnessBarFill.style.opacity = '0.8';
            });

            speedSlider.addEventListener('input', () => {
                waveSpeed = parseFloat(speedSlider.value);
                speedValue.textContent = waveSpeed.toFixed(1) + '×';
            });

            amplitudeSlider.addEventListener('input', () => {
                waveAmplitude = parseFloat(amplitudeSlider.value);
                amplitudeValue.textContent = waveAmplitude.toFixed(1) + '×';
            });

            preheatSlider.addEventListener('input', () => {
                preheatSeconds = parseFloat(preheatSlider.value);
                preheatValue.textContent = preheatSeconds.toFixed(1) + 's';
            });

            // 键盘快捷键
            document.addEventListener('keydown', (e) => {
                switch (e.key.toLowerCase()) {
                    case ' ':
                        e.preventDefault();
                        btnPlay.click();
                        break;
                    case 'h':
                        if (!e.ctrlKey && !e.metaKey && !e.altKey) {
                            btnHighlight.click();
                        }
                        break;
                    case 'r':
                        if (!e.ctrlKey && !e.metaKey && !e.altKey) {
                            btnReset.click();
                        }
                        break;
                    case 'arrowright':
                        e.preventDefault();
                        waveSpeed = Math.min(2.5, waveSpeed + 0.1);
                        speedSlider.value = waveSpeed;
                        speedValue.textContent = waveSpeed.toFixed(1) + '×';
                        break;
                    case 'arrowleft':
                        e.preventDefault();
                        waveSpeed = Math.max(0.2, waveSpeed - 0.1);
                        speedSlider.value = waveSpeed;
                        speedValue.textContent = waveSpeed.toFixed(1) + '×';
                        break;
                    case 'arrowup':
                        e.preventDefault();
                        waveAmplitude = Math.min(1.8, waveAmplitude + 0.05);
                        amplitudeSlider.value = waveAmplitude;
                        amplitudeValue.textContent = waveAmplitude.toFixed(2) + '×';
                        break;
                    case 'arrowdown':
                        e.preventDefault();
                        waveAmplitude = Math.max(0.3, waveAmplitude - 0.05);
                        amplitudeSlider.value = waveAmplitude;
                        amplitudeValue.textContent = waveAmplitude.toFixed(2) + '×';
                        break;
                }
            });

            // 窗口大小变化时重设
            window.addEventListener('resize', () => {
                lastFrameTime = null;
            });

            // ==================== 启动 ====================
            resizeCanvas();
            requestAnimationFrame(render);

            console.log('🐍 机械蛇变刚度推进动画已就绪');
            console.log('   TRIZ IFR 核心:空间分离柔性段与刚性段');
            console.log('   ⌨ 快捷键:空格=播放/暂停 | H=高亮 | R=重置 | 方向键=调参');
            console.log('   🖱 拖动底部滑块调节波速/振幅/预热提前量');
        })();
    </script>
</body>
</html>

动画机制

这个动画将TRIZ“最终理想解”思想进行了演示,让您可以直观看到变刚度蛇身结构如何自行将弯曲运动转换为推力,而不依赖反馈控制和变摩擦机构。

操作与反馈

  • 核心机制:蛇身动态划分为“刚性支撑段”(冷蓝色)和“柔性弯曲段”(暖橙色)。弯曲段会提前加热软化,拉线肌腱轻松拉动关节形成蛇形波;直线段保持高刚性抵抗重力下垂。动画通过色彩和光晕生动展示了这种空间上的刚度分离。
  • 交互方式:您可以通过拖拽滑块实时调节“波速”、“振幅”和“预热提前量”,直观感受参数变化对运动形态的影响。点击按钮可播放/暂停动画或开启高亮模式,键盘方向键和空格键也支持快速操作。
  • 视觉风格:采用深色科技感主题,蛇身带有横向防滑纹路和三层结构截面图。推进力箭头、拉线肌腱轨迹和加热粒子效果,共同勾勒出完整的运动与力传递链条。

优化建议: 您可以在代码开头的<style>标签中修改CSS变量(如--bg--accent-warm)来调整主题色;核心运动参数(如WAVE_PERIODBASE_AMPLITUDE)位于<script>标签内顶部,可根据演示需求微调。

积分规则:第一轮对话扣减6分,后续每轮扣4分