分享图
动画工坊
引擎就绪
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>管内探伤机器人 - 最终理想解 (IFR) 自适应机制</title>
    <style>
        :root {
            --bg-color: #060913;
            --grid-color: rgba(0, 243, 255, 0.05);
            --pipe-wall: #111827;
            --pipe-stroke: #1f2937;
            --neon-cyan: #00f3ff;
            --neon-pink: #ff0055;
            --acid-green: #ccff00;
            --text-main: #8be9fd;
            --text-dim: #6272a4;
            --panel-bg: rgba(6, 9, 19, 0.85);
        }

        * { box-sizing: border-box; }
        
        body, html {
            margin: 0; padding: 0; width: 100%; height: 100%;
            background-color: var(--bg-color);
            color: var(--text-main);
            font-family: 'Consolas', 'Courier New', monospace;
            overflow: hidden; /* Prevent scrolling */
        }

        /* Tactical Scanline Overlay */
        .scanlines {
            position: absolute; inset: 0; z-index: 100; pointer-events: none;
            background: linear-gradient(
                to bottom,
                rgba(255, 255, 255, 0),
                rgba(255, 255, 255, 0) 50%,
                rgba(0, 0, 0, 0.25) 50%,
                rgba(0, 0, 0, 0.25)
            );
            background-size: 100% 4px;
        }

        /* Telemetry HUD - Kept to edges/corners */
        .hud {
            position: absolute; z-index: 50; padding: 16px;
            backdrop-filter: blur(8px);
            border: 1px solid rgba(0, 243, 255, 0.2);
            background: var(--panel-bg);
            box-shadow: 0 0 20px rgba(0, 243, 255, 0.05);
        }
        
        .hud-top-left { top: 30px; left: 30px; width: 320px; border-left: 4px solid var(--neon-cyan); }
        .hud-top-right { top: 30px; right: 30px; width: 280px; border-right: 4px solid var(--acid-green); }
        .hud-bottom-right { bottom: 80px; right: 30px; width: 280px; border-right: 4px solid var(--neon-pink); }

        h1 {
            font-size: 16px; margin: 0 0 12px 0;
            color: #fff; text-transform: uppercase;
            letter-spacing: 1px; font-weight: bold;
            display: flex; align-items: center; gap: 8px;
        }
        h1::before { content: ''; display: block; width: 8px; height: 8px; background: var(--neon-cyan); box-shadow: 0 0 10px var(--neon-cyan); }

        .data-row {
            display: flex; justify-content: space-between;
            font-size: 13px; margin-bottom: 8px;
            border-bottom: 1px dashed rgba(139, 233, 253, 0.15); padding-bottom: 4px;
        }
        .data-row span:first-child { color: var(--text-dim); }
        .val-cyan { color: var(--neon-cyan); font-weight: bold; }
        .val-green { color: var(--acid-green); font-weight: bold; }
        .val-pink { color: var(--neon-pink); font-weight: bold; }

        /* Interactive Controls */
        .control-panel {
            position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
            z-index: 60; width: 50%; max-width: 600px;
            background: var(--panel-bg); padding: 12px 24px;
            border: 1px solid rgba(0, 243, 255, 0.2); border-radius: 4px;
            display: flex; flex-direction: column; align-items: center;
        }
        .slider-container { width: 100%; display: flex; align-items: center; gap: 15px; }
        input[type="range"] {
            flex-grow: 1; appearance: none; background: rgba(0,243,255,0.1); height: 4px; border-radius: 2px; outline: none;
        }
        input[type="range"]::-webkit-slider-thumb {
            appearance: none; width: 16px; height: 16px; background: var(--neon-cyan); cursor: ew-resize; box-shadow: 0 0 10px var(--neon-cyan);
        }
        .control-label { font-size: 12px; color: var(--text-dim); margin-top: 8px; letter-spacing: 1px; }

        /* SVG Canvas */
        svg { width: 100%; height: 100%; display: block; }
    </style>
</head>
<body>
    <div class="scanlines"></div>

    <!-- HUD Elements -->
    <div class="hud hud-top-left">
        <h1>最终理想解 (IFR) 架构</h1>
        <div class="data-row"><span>设计目标</span><span class="val-cyan">消解过弯卡死与探头间隙波动</span></div>
        <div class="data-row"><span>核心机制</span><span class="val-cyan">万向节连接 + 摇臂自定心</span></div>
        <div class="data-row"><span>管道规格</span><span class="val-cyan">φ60-120mm / R≥1.5D</span></div>
        <div class="data-row"><span>当前管径适配</span><span class="val-pink">140 mm (展示比例)</span></div>
    </div>

    <div class="hud hud-top-right">
        <h1 style="color:var(--acid-green)"><span style="background:var(--acid-green)"></span>探伤滑靴状态</h1>
        <div class="data-row"><span>壁面贴合度</span><span class="val-green">100% 理想跟踪</span></div>
        <div class="data-row"><span>恒力弹簧下压</span><span class="val-green">2.50 N</span></div>
        <div class="data-row"><span>涡流探头间隙</span><span class="val-green" id="probe-gap">0.50 ±0.0 mm</span></div>
    </div>

    <div class="hud hud-bottom-right">
        <h1 style="color:var(--neon-pink)"><span style="background:var(--neon-pink)"></span>动力学实时遥测</h1>
        <div class="data-row"><span>前摇臂压缩力</span><span class="val-pink" id="force-front">5.5 N</span></div>
        <div class="data-row"><span>后摇臂压缩力</span><span class="val-pink" id="force-rear">5.5 N</span></div>
        <div class="data-row"><span>万向节偏转角度</span><span class="val-cyan" id="joint-angle">0.0°</span></div>
        <div class="data-row"><span>过弯状态</span><span class="val-pink" id="bend-status">直线定心</span></div>
    </div>

    <div class="control-panel">
        <div class="slider-container">
            <span style="font-size: 12px; color: var(--neon-cyan)">START</span>
            <input type="range" id="progress-slider" min="0" max="100" step="0.1" value="0">
            <span style="font-size: 12px; color: var(--neon-cyan)">END</span>
        </div>
        <div class="control-label">动画自动播放中。拖动滑块可手动控制过弯进度 (释放后继续)</div>
    </div>

    <!-- Main SVG Animation Container -->
    <svg id="canvas" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
        <defs>
            <!-- Glow Filters -->
            <filter id="glow-cyan" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur" />
                <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
            <filter id="glow-pink" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
                <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>
            <filter id="glow-green" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" />
                <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
            </filter>

            <!-- Background Grid Pattern -->
            <pattern id="bg-grid" width="60" height="60" patternUnits="userSpaceOnUse">
                <path d="M 60 0 L 0 0 0 60" fill="none" stroke="var(--grid-color)" stroke-width="1"/>
                <circle cx="0" cy="0" r="1.5" fill="rgba(0,243,255,0.2)" />
            </pattern>

            <!-- Pipe Wall Gradient -->
            <linearGradient id="wall-grad" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stop-color="#1f2937" />
                <stop offset="10%" stop-color="#111827" />
                <stop offset="90%" stop-color="#111827" />
                <stop offset="100%" stop-color="#1f2937" />
            </linearGradient>
        </defs>

        <!-- Background -->
        <rect width="100%" height="100%" fill="url(#bg-grid)" />

        <!-- 
          Centerline Path (Invisible, used for JS math): 
          Straight up from bottom, 90 degree curve right (R=300), straight right.
        -->
        <path id="track" d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300" fill="none" stroke="none" />

        <!-- Pipe Visual Rendering (Trick: Thick stroke minus thinner stroke creates parallel offset walls) -->
        <g id="pipe-visuals">
            <!-- Outer Wall Stroke -->
            <path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300" 
                  fill="none" stroke="#2d3748" stroke-width="144" />
            <!-- Inner Cavity Fill -->
            <path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300" 
                  fill="none" stroke="#0a0e17" stroke-width="140" />
            <!-- Centerline Indicator -->
            <path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300" 
                  fill="none" stroke="rgba(0,243,255,0.1)" stroke-width="2" stroke-dasharray="10 10" />
        </g>

        <!-- Dynamic Robot Elements Group -->
        <g id="robot-system">
            <!-- Springs drawn first (underneath bodies) -->
            <path id="spring-f-top" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
            <path id="spring-f-bot" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
            <path id="spring-r-top" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
            <path id="spring-r-bot" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
            <!-- Probe Constant Force Spring -->
            <path id="spring-probe" fill="none" stroke="var(--acid-green)" stroke-width="2" />

            <!-- Front Module Body -->
            <g id="mod-front">
                <rect x="-55" y="-22" width="110" height="44" rx="4" fill="#111" stroke="var(--neon-cyan)" stroke-width="2" />
                <rect x="-45" y="-12" width="90" height="24" fill="#1a1a1a" stroke="#333" />
                <circle cx="40" cy="0" r="4" fill="var(--neon-cyan)" filter="url(#glow-cyan)"/> <!-- Direction Indicator -->
            </g>

            <!-- Rear Module Body -->
            <g id="mod-rear">
                <rect x="-55" y="-22" width="110" height="44" rx="4" fill="#111" stroke="var(--neon-cyan)" stroke-width="2" />
                <rect x="-45" y="-12" width="90" height="24" fill="#1a1a1a" stroke="#333" />
                <!-- Probe guiding pins base inside rear module -->
                <rect x="-40" y="10" width="24" height="14" fill="#222" stroke="var(--acid-green)" stroke-width="1" />
            </g>

            <!-- Wheels (Polyurethane coated) -->
            <circle id="wheel-f-top" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
            <circle id="wheel-f-bot" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
            <circle id="wheel-r-top" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
            <circle id="wheel-r-bot" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />

            <!-- Eddy Current Probe Sliding Shoe -->
            <g id="probe-shoe">
                <path d="M -15,0 L 15,0 L 10,-8 L -10,-8 Z" fill="var(--acid-green)" filter="url(#glow-green)"/>
                <!-- Scanning Wave Effect -->
                <path id="probe-wave-1" d="M -12,4 L 12,4" fill="none" stroke="var(--acid-green)" stroke-width="1.5" stroke-dasharray="2 2" />
                <path id="probe-wave-2" d="M -8,8 L 8,8" fill="none" stroke="var(--acid-green)" stroke-width="1" stroke-dasharray="2 2" />
            </g>

            <!-- Universal Joint (Cross Hinge) -->
            <g id="universal-joint">
                <circle cx="0" cy="0" r="10" fill="#222" stroke="var(--acid-green)" stroke-width="3" filter="url(#glow-green)"/>
                <circle cx="0" cy="0" r="4" fill="var(--acid-green)" />
            </g>
        </g>

        <!-- Dynamic Visual Leaders (Connecting HUD logic to moving parts) -->
        <g id="leaders" stroke="#ffffff" stroke-width="1" stroke-dasharray="4 4" opacity="0.4">
            <line id="leader-joint" x1="600" y1="70" x2="0" y2="0" />
            <line id="leader-probe" x1="1200" y1="800" x2="0" y2="0" />
            <circle id="target-joint" r="3" fill="#fff" />
            <circle id="target-probe" r="3" fill="#fff" />
        </g>
        <text x="605" y="65" fill="#fff" font-size="12" opacity="0.7">万向铰链 (蛇形自适应)</text>
        <text x="1205" y="815" fill="#fff" font-size="12" opacity="0.7">弹性滑靴 (恒力贴壁)</text>
    </svg>

    <script>
        const track = document.getElementById('track');
        const trackLen = track.getTotalLength();
        const slider = document.getElementById('progress-slider');
        
        // Robot parameters
        const PIPE_RADIUS = 70; // Half of 140 width
        const MOD_HALF_LEN = 55; // Half length of rigid module
        const SPRING_NOMINAL = 48; // Nominal distance from module center to wall
        
        // State variables
        let isDragging = false;
        let autoProgress = 100; // Start position (distance along path)
        const SPEED = 1.2; // Pixels per frame
        const MIN_D = 200; // Safe start bound to fit entire robot
        const MAX_D = trackLen - 150; // Safe end bound

        // DOM Elements
        const elJoint = document.getElementById('universal-joint');
        const elModFront = document.getElementById('mod-front');
        const elModRear = document.getElementById('mod-rear');
        const elWFT = document.getElementById('wheel-f-top');
        const elWFB = document.getElementById('wheel-f-bot');
        const elWRT = document.getElementById('wheel-r-top');
        const elWRB = document.getElementById('wheel-r-bot');
        const elProbe = document.getElementById('probe-shoe');
        
        const elSFT = document.getElementById('spring-f-top');
        const elSFB = document.getElementById('spring-f-bot');
        const elSRT = document.getElementById('spring-r-top');
        const elSRB = document.getElementById('spring-r-bot');
        const elSProbe = document.getElementById('spring-probe');

        const leaderJoint = document.getElementById('leader-joint');
        const leaderProbe = document.getElementById('leader-probe');
        const targetJoint = document.getElementById('target-joint');
        const targetProbe = document.getElementById('target-probe');

        // UI Elements
        const uiJointAngle = document.getElementById('joint-angle');
        const uiForceFront = document.getElementById('force-front');
        const uiForceRear = document.getElementById('force-rear');
        const uiBendStatus = document.getElementById('bend-status');
        
        // Helper: Generate a zigzag spring path
        function getSpringPath(x1, y1, x2, y2, coils = 5, width = 8) {
            const dx = x2 - x1, dy = y2 - y1;
            const dist = Math.hypot(dx, dy);
            const nx = -dy / dist, ny = dx / dist;
            const step = dist / (coils * 2 + 1);
            let path = `M ${x1} ${y1} `;
            
            for (let i = 1; i <= coils * 2; i++) {
                const cx = x1 + (dx / dist) * (i * step);
                const cy = y1 + (dy / dist) * (i * step);
                const mult = i % 2 === 0 ? 1 : -1;
                path += `L ${cx + nx * width * mult} ${cy + ny * width * mult} `;
            }
            path += `L ${x2} ${y2}`;
            return path;
        }

        // Helper: Calculate normal vector and position on track
        function getTrackData(d) {
            const p = track.getPointAtLength(d);
            const pNext = track.getPointAtLength(Math.min(d + 1, trackLen));
            const pPrev = track.getPointAtLength(Math.max(d - 1, 0));
            const dx = pNext.x - pPrev.x;
            const dy = pNext.y - pPrev.y;
            const len = Math.hypot(dx, dy);
            return {
                x: p.x, y: p.y,
                dx: dx/len, dy: dy/len,
                nx: -dy/len, ny: dx/len // Left normal
            };
        }

        function updateFrame(d_joint) {
            // Kinematics:
            // Joint is at d_joint
            // Front module attaches to joint, nose extends forward. Midpoint is at d_joint + MOD_HALF_LEN
            // Rear module attaches to joint, tail extends backward. Midpoint is at d_joint - MOD_HALF_LEN

            const pdJoint = getTrackData(d_joint);
            const pdFMid = getTrackData(d_joint + MOD_HALF_LEN);
            const pdRMid = getTrackData(d_joint - MOD_HALF_LEN);
            const pdFNose = getTrackData(d_joint + MOD_HALF_LEN * 2);
            const pdRTail = getTrackData(d_joint - MOD_HALF_LEN * 2);

            // Update Joint
            elJoint.setAttribute('transform', `translate(${pdJoint.x}, ${pdJoint.y})`);
            
            // Calculate Module Rotations (Rigid body vectors)
            const angleFront = Math.atan2(pdFNose.y - pdJoint.y, pdFNose.x - pdJoint.x) * 180 / Math.PI;
            const angleRear = Math.atan2(pdJoint.y - pdRTail.y, pdJoint.x - pdRTail.x) * 180 / Math.PI;
            
            elModFront.setAttribute('transform', `translate(${pdFMid.x}, ${pdFMid.y}) rotate(${angleFront})`);
            elModRear.setAttribute('transform', `translate(${pdRMid.x}, ${pdRMid.y}) rotate(${angleRear})`);

            // Compute relative joint angle for Telemetry
            let relAngle = Math.abs(angleFront - angleRear);
            if (relAngle > 180) relAngle = 360 - relAngle;
            uiJointAngle.innerText = relAngle.toFixed(1) + '°';
            
            if(relAngle > 5) {
                uiBendStatus.innerText = "万向铰链偏转 / 过弯补偿";
                uiBendStatus.className = "val-pink";
            } else {
                uiBendStatus.innerText = "直线段 / 完美自定心";
                uiBendStatus.className = "val-cyan";
            }

            // --- Suspension & Wheels Calculations ---
            
            // Function to attach wheel and spring at a given midpoint
            function applySuspension(midData, elWT, elWB, elST, elSB, cx, cy, angle, isFront) {
                // Top/Bottom wall coords using local track normal
                const wallTX = midData.x + midData.nx * (PIPE_RADIUS - 10); // 10 is wheel radius
                const wallTY = midData.y + midData.ny * (PIPE_RADIUS - 10);
                const wallBX = midData.x - midData.nx * (PIPE_RADIUS - 10);
                const wallBY = midData.y - midData.ny * (PIPE_RADIUS - 10);

                elWT.setAttribute('cx', wallTX); elWT.setAttribute('cy', wallTY);
                elWB.setAttribute('cx', wallBX); elWB.setAttribute('cy', wallBY);

                // Body attachment points (top and bottom of module body)
                const rad = angle * Math.PI / 180;
                // Module normal vector
                const bnx = -Math.sin(rad), bny = Math.cos(rad);
                const attachTopX = cx + bnx * 22; const attachTopY = cy + bny * 22;
                const attachBotX = cx - bnx * 22; const attachBotY = cy - bny * 22;

                elST.setAttribute('d', getSpringPath(attachTopX, attachTopY, wallTX, wallTY, 4, 6));
                elSB.setAttribute('d', getSpringPath(attachBotX, attachBotY, wallBX, wallBY, 4, 6));

                // Force telemetry simulation (Spring compression)
                const distT = Math.hypot(wallTX - attachTopX, wallTY - attachTopY);
                const force = (5.5 + (SPRING_NOMINAL - distT) * 0.1).toFixed(1);
                if(isFront) uiForceFront.innerText = `${force} N`;
                else uiForceRear.innerText = `${force} N`;
                
                // Color intensity based on compression
                const compressionVal = Math.max(0, SPRING_NOMINAL - distT);
                if (compressionVal > 2) {
                    elST.setAttribute('stroke', '#ffcc00'); // Warning/Compression color
                    elSB.setAttribute('stroke', '#ffcc00');
                } else {
                    elST.setAttribute('stroke', 'var(--neon-pink)');
                    elSB.setAttribute('stroke', 'var(--neon-pink)');
                }
            }

            applySuspension(pdFMid, elWFT, elWFB, elSFT, elSFB, pdFMid.x, pdFMid.y, angleFront, true);
            applySuspension(pdRMid, elWRT, elWRB, elSRT, elSRB, pdRMid.x, pdRMid.y, angleRear, false);

            // --- Probe Sliding Shoe ---
            // Probe is mounted on the rear module, near the tail
            const probeD = d_joint - MOD_HALF_LEN * 1.5;
            const pdProbe = getTrackData(probeD);
            
            // Probe sits on outer (bottom) wall
            const probeWallX = pdProbe.x - pdProbe.nx * (PIPE_RADIUS - 2); // 2px visual gap tolerance
            const probeWallY = pdProbe.y - pdProbe.ny * (PIPE_RADIUS - 2);
            
            // Align probe shoe with wall tangent
            const probeAngle = Math.atan2(pdProbe.dy, pdProbe.dx) * 180 / Math.PI;
            elProbe.setAttribute('transform', `translate(${probeWallX}, ${probeWallY}) rotate(${probeAngle})`);

            // Probe constant force spring connects from rear body tail to shoe
            const radRear = angleRear * Math.PI / 180;
            const rbnx = -Math.sin(radRear), rbny = Math.cos(radRear);
            // Probe anchor on module body (near tail, bottom side)
            const tailBodyX = pdRTail.x + pdProbe.dx * 10 + rbnx * (-22);
            const tailBodyY = pdRTail.y + pdProbe.dy * 10 + rbny * (-22);
            
            elSProbe.setAttribute('d', getSpringPath(tailBodyX, tailBodyY, probeWallX, probeWallY, 3, 4));

            // Update UI Leaders
            targetJoint.setAttribute('cx', pdJoint.x); targetJoint.setAttribute('cy', pdJoint.y);
            leaderJoint.setAttribute('x2', pdJoint.x); leaderJoint.setAttribute('y2', pdJoint.y);
            
            targetProbe.setAttribute('cx', probeWallX); targetProbe.setAttribute('cy', probeWallY);
            leaderProbe.setAttribute('x2', probeWallX); leaderProbe.setAttribute('y2', probeWallY);

            // Simple wave animation based on time
            const waveScale = (Date.now() % 1000) / 1000;
            document.getElementById('probe-wave-1').setAttribute('transform', `translate(0, ${waveScale * 6})`);
            document.getElementById('probe-wave-1').setAttribute('opacity', 1 - waveScale);
            document.getElementById('probe-wave-2').setAttribute('transform', `translate(0, ${waveScale * 12})`);
            document.getElementById('probe-wave-2').setAttribute('opacity', 1 - waveScale);
        }

        // Main Animation Loop
        function animate() {
            if (!isDragging) {
                autoProgress += SPEED;
                if (autoProgress > MAX_D) {
                    autoProgress = MIN_D; // Loop back
                }
                // Sync slider with auto progress
                const percent = ((autoProgress - MIN_D) / (MAX_D - MIN_D)) * 100;
                slider.value = percent;
            } else {
                // If dragging, calculate autoProgress from slider value
                const percent = slider.value;
                autoProgress = MIN_D + (percent / 100) * (MAX_D - MIN_D);
            }

            updateFrame(autoProgress);
            requestAnimationFrame(animate);
        }

        // Slider Interaction Setup
        slider.addEventListener('mousedown', () => isDragging = true);
        slider.addEventListener('touchstart', () => isDragging = true, {passive: true});
        slider.addEventListener('mouseup', () => isDragging = false);
        slider.addEventListener('touchend', () => isDragging = false);
        
        slider.addEventListener('input', (e) => {
            const percent = e.target.value;
            autoProgress = MIN_D + (percent / 100) * (MAX_D - MIN_D);
            updateFrame(autoProgress); // Force immediate visual update
        });

        // Initialize immediately
        updateFrame(MIN_D);
        requestAnimationFrame(animate);

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