分享图
动画工坊
引擎就绪
<!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 Solution: Articulated Flexible Track Chassis</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700;800&family=Space+Grotesk:wght@400;700&display=swap');

        :root {
            --bg-color: #050914;
            --grid-color: rgba(0, 229, 255, 0.05);
            --stair-color: #0f1626;
            --stair-stroke: #1e3a5f;
            --primary: #00E5FF;
            --warning: #FF3D00;
            --success: #00E676;
            --text-main: #E0F7FA;
            --text-muted: #4DD0E1;
            --font-mono: 'JetBrains Mono', monospace;
            --font-display: 'Space Grotesk', sans-serif;
        }

        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: var(--bg-color);
            color: var(--text-main);
            overflow: hidden;
            font-family: var(--font-display);
        }

        #canvas-container {
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
        }

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

        /* SVG Element Styling */
        .track-belt {
            fill: none;
            stroke: var(--primary);
            stroke-width: 4;
            stroke-linejoin: round;
            stroke-linecap: round;
            transition: stroke 0.3s;
        }

        .track-inner-dash {
            fill: none;
            stroke: #fff;
            stroke-width: 2;
            stroke-dasharray: 6 12;
            opacity: 0.6;
        }

        .chassis-segment {
            fill: rgba(0, 229, 255, 0.05);
            stroke: rgba(0, 229, 255, 0.4);
            stroke-width: 2;
        }

        .wheel {
            fill: #0a1128;
            stroke: var(--primary);
            stroke-width: 2;
        }

        .wheel-spoke {
            stroke: rgba(0, 229, 255, 0.5);
            stroke-width: 2;
        }

        .stair-polygon {
            fill: var(--stair-color);
            stroke: var(--stair-stroke);
            stroke-width: 3;
        }

        .envelope-path {
            fill: none;
            stroke: rgba(0, 229, 255, 0.2);
            stroke-width: 1;
            stroke-dasharray: 4 4;
        }

        .hinge-joint {
            fill: var(--bg-color);
            stroke: var(--primary);
            stroke-width: 2;
        }

        /* Dynamic Classes applied via JS */
        .active-hinge {
            stroke: var(--warning);
            filter: drop-shadow(0 0 10px var(--warning));
        }

        .active-text {
            fill: var(--warning) !important;
            font-weight: bold;
            text-shadow: 0 0 8px rgba(255, 61, 0, 0.6);
        }

        .hud-title { font-size: 28px; font-weight: 800; fill: #fff; letter-spacing: 2px; }
        .hud-subtitle { font-size: 14px; fill: var(--text-muted); font-family: var(--font-mono); }
        .hud-label { font-size: 12px; fill: var(--text-muted); font-family: var(--font-mono); }
        .hud-value { font-size: 16px; fill: var(--primary); font-family: var(--font-mono); font-weight: 700; }
        .hud-unit { font-size: 10px; fill: rgba(0,229,255,0.5); }

        .callout-line {
            stroke: var(--warning);
            stroke-width: 1;
            stroke-dasharray: 2 4;
            opacity: 0;
            transition: opacity 0.2s;
        }

        .callout-line.visible { opacity: 0.8; }

        /* UI Overlay */
        .overlay {
            position: absolute;
            top: 40px;
            left: 40px;
            pointer-events: none;
        }

        .triz-badge {
            display: inline-block;
            background: rgba(0, 229, 255, 0.1);
            border: 1px solid rgba(0, 229, 255, 0.3);
            color: var(--primary);
            padding: 4px 12px;
            font-family: var(--font-mono);
            font-size: 12px;
            border-radius: 20px;
            margin-bottom: 12px;
            text-transform: uppercase;
            letter-spacing: 1px;
            box-shadow: 0 0 15px rgba(0, 229, 255, 0.1);
        }

        h1 { margin: 0 0 8px 0; font-size: 2.5rem; letter-spacing: -0.5px; }
        p { margin: 0; font-family: var(--font-mono); color: var(--text-muted); font-size: 0.9rem; max-width: 500px; line-height: 1.5; }

        /* Glossy scanline effect */
        .scanline {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.25) 51%);
            background-size: 100% 4px;
            pointer-events: none;
            z-index: 10;
            opacity: 0.3;
        }
    </style>
</head>
<body>

<div id="canvas-container">
    <div class="scanline"></div>
    
    <div class="overlay">
        <div class="triz-badge">TRIZ IFR Solution</div>
        <h1>Articulated Soft-Track</h1>
        <p>Eliminating step-slip contradiction via 3-segment pitch hinges and dynamic 150N track tensioning. The chassis continuously morphs to the terrain envelope.</p>
    </div>

    <svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
        <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="1"/>
            </pattern>
            <pattern id="grid-large" width="200" height="200" patternUnits="userSpaceOnUse">
                <path d="M 200 0 L 0 0 0 200" fill="none" stroke="rgba(0, 229, 255, 0.1)" stroke-width="2"/>
            </pattern>
            <filter id="glow">
                <feGaussianBlur stdDeviation="4" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>
            <filter id="glow-warning">
                <feGaussianBlur stdDeviation="6" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>
        </defs>

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

        <!-- World Group (Camera follows vehicle) -->
        <g id="world" transform="translate(0, 0)">
            
            <!-- Environment / Stairs -->
            <polygon id="stairs" class="stair-polygon" points="" />
            <!-- Kinetmatic Envelope -->
            <path id="envelope-path" class="envelope-path" d="" />

            <!-- Vehicle Assembly -->
            <g id="vehicle">
                <!-- Outer Rubber Track -->
                <path id="main-track" class="track-belt" d="" filter="url(#glow)"/>
                <!-- Track inner visual for motion -->
                <path id="main-track-dash" class="track-inner-dash" d="" />
                
                <!-- Chassis Segments -->
                <line id="seg1" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>
                <line id="seg2" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>
                <line id="seg3" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>

                <!-- Dynamic Tensioner Spring -->
                <path id="tensioner-spring" fill="none" stroke="var(--warning)" stroke-width="3" stroke-linejoin="round" />
                <rect id="tensioner-block" width="16" height="24" fill="var(--bg-color)" stroke="var(--warning)" stroke-width="2" rx="4" y="-12" />

                <!-- Wheels and Joints -->
                <g id="wheels"></g>

                <!-- Torsion Hinge Highlight Arcs -->
                <path id="hinge-arc1" fill="none" stroke-width="4" stroke-linecap="round" />
                <path id="hinge-arc2" fill="none" stroke-width="4" stroke-linecap="round" />
            </g>
        </g>

        <!-- Static HUD Overlays -->
        <g transform="translate(1250, 60)">
            <rect width="300" height="220" fill="rgba(5, 9, 20, 0.8)" stroke="rgba(0,229,255,0.3)" stroke-width="1" rx="8"/>
            <text x="20" y="35" class="hud-label">SYSTEM TELEMETRY</text>
            <line x1="20" y1="45" x2="280" y2="45" stroke="rgba(0,229,255,0.2)" stroke-width="1"/>
            
            <text x="20" y="80" class="hud-label">PITCH HINGE 1 (Front)</text>
            <text x="280" y="80" id="hud-angle2" class="hud-value" text-anchor="end">0.0°</text>
            
            <text x="20" y="115" class="hud-label">PITCH HINGE 2 (Rear)</text>
            <text x="280" y="115" id="hud-angle1" class="hud-value" text-anchor="end">0.0°</text>
            
            <text x="20" y="150" class="hud-label">TRACK PERIMETER</text>
            <text x="280" y="150" id="hud-length" class="hud-value" text-anchor="end">0 <tspan class="hud-unit">mm</tspan></text>
            
            <text x="20" y="185" class="hud-label">DYNAMIC TENSION (K=150N)</text>
            <text x="280" y="185" id="hud-tension" class="hud-value" text-anchor="end">150.0 <tspan class="hud-unit">N</tspan></text>

            <rect x="20" y="200" width="260" height="4" fill="rgba(0,229,255,0.1)" rx="2"/>
            <rect id="hud-tension-bar" x="20" y="200" width="130" height="4" fill="var(--primary)" rx="2" transition="width 0.2s"/>
        </g>

        <!-- Callouts -->
        <g id="callouts">
            <line id="callout-line1" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
            <text id="callout-text1" x="0" y="0" class="hud-label" opacity="0" transition="opacity 0.2s">YIELDING</text>
            
            <line id="callout-line2" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
            <text id="callout-text2" x="0" y="0" class="hud-label" opacity="0">YIELDING</text>
        </g>

    </svg>
</div>

<script>
/**
 * TRIZ IFR Kinematic Simulation
 * Simulates a 3-segment articulated tracked vehicle traversing irregular stairs.
 */

// --- Configuration & Constants ---
const CHASSIS_L = 140; // Length of each of the 3 segments
const WHEEL_R = 24;    // Radius of wheels/pulleys
const TRACK_R = 28;    // Offset radius for the rubber track path
const BASE_TENSION = 150; // Newtons

// --- Environment Geometry (Stairs) ---
// Coordinates: X forward, Y down (SVG standard)
const stairsData = [
    {x: -1000, y: 500},
    {x: 300, y: 500},
    {x: 300, y: 400}, // Step 1: H=100
    {x: 480, y: 400}, 
    {x: 480, y: 250}, // Step 2: H=150 (Irregular height)
    {x: 600, y: 250},
    {x: 600, y: 120}, // Step 3: H=130
    {x: 1000, y: 120},
    {x: 1000, y: -20}, // Step 4
    {x: 3000, y: -20}
];

// Kinematic envelope: The upper convex hull / smoothed path the wheels follow.
// Expands corners outwards to simulate the track bridging the gaps.
const envelopeData = [
    {x: -1000, y: 476},
    {x: 240,  y: 476},
    {x: 340,  y: 376}, 
    {x: 440,  y: 376},
    {x: 520,  y: 226},
    {x: 560,  y: 226},
    {x: 640,  y: 96},
    {x: 960,  y: 96},
    {x: 1040, y: -44},
    {x: 3000, y: -44}
];

// --- DOM Elements ---
const elStairs = document.getElementById('stairs');
const elEnvelope = document.getElementById('envelope-path');
const elWorld = document.getElementById('world');
const elMainTrack = document.getElementById('main-track');
const elMainTrackDash = document.getElementById('main-track-dash');
const elSeg1 = document.getElementById('seg1');
const elSeg2 = document.getElementById('seg2');
const elSeg3 = document.getElementById('seg3');
const elWheelsGrp = document.getElementById('wheels');
const elHingeArc1 = document.getElementById('hinge-arc1');
const elHingeArc2 = document.getElementById('hinge-arc2');
const elTenSpring = document.getElementById('tensioner-spring');
const elTenBlock = document.getElementById('tensioner-block');

// HUD
const hudAngle1 = document.getElementById('hud-angle1');
const hudAngle2 = document.getElementById('hud-angle2');
const hudLength = document.getElementById('hud-length');
const hudTension = document.getElementById('hud-tension');
const hudTensionBar = document.getElementById('hud-tension-bar');
const cLine1 = document.getElementById('callout-line1');
const cText1 = document.getElementById('callout-text1');
const cLine2 = document.getElementById('callout-line2');
const cText2 = document.getElementById('callout-text2');

// Initialize SVG static geometry
function initGeometry() {
    // Build Stairs Polygon
    let pts = "";
    stairsData.forEach(p => pts += `${p.x},${p.y} `);
    pts += `3000,1000 -1000,1000`; // Close bottom
    elStairs.setAttribute('points', pts.trim());

    // Build Envelope Path
    let d = `M ${envelopeData[0].x} ${envelopeData[0].y}`;
    for(let i=1; i<envelopeData.length; i++) {
        d += ` L ${envelopeData[i].x} ${envelopeData[i].y}`;
    }
    elEnvelope.setAttribute('d', d);

    // Create 4 Wheels
    for(let i=0; i<4; i++) {
        const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        g.setAttribute('id', `wheel${i}`);
        
        const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('r', WHEEL_R);
        circle.setAttribute('class', 'wheel');
        
        const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line1.setAttribute('x1', -WHEEL_R+4); line1.setAttribute('y1', 0);
        line1.setAttribute('x2', WHEEL_R-4); line1.setAttribute('y2', 0);
        line1.setAttribute('class', 'wheel-spoke');
        
        const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line2.setAttribute('x1', 0); line2.setAttribute('y1', -WHEEL_R+4);
        line2.setAttribute('x2', 0); line2.setAttribute('y2', WHEEL_R-4);
        line2.setAttribute('class', 'wheel-spoke');
        
        const hub = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        hub.setAttribute('r', 6);
        hub.setAttribute('class', 'hinge-joint');
        
        g.appendChild(circle);
        g.appendChild(line1);
        g.appendChild(line2);
        g.appendChild(hub);
        elWheelsGrp.appendChild(g);
    }
}

// --- Kinematic Math Solvers ---

function getEnvY(x) {
    for(let i=0; i<envelopeData.length-1; i++) {
        if(x >= envelopeData[i].x && x <= envelopeData[i+1].x) {
            let t = (x - envelopeData[i].x) / (envelopeData[i+1].x - envelopeData[i].x);
            return envelopeData[i].y + t * (envelopeData[i+1].y - envelopeData[i].y);
        }
    }
    return envelopeData[envelopeData.length-1].y;
}

// Solves for the next joint point maintaining segment length L and resting on envelope
function solveNextPoint(x_prev, y_prev, L) {
    let bestX = x_prev + L;
    let minError = Infinity;
    
    // Forward scanline approximation
    for(let scan = x_prev; scan <= x_prev + L; scan += 0.5) {
        let y = getEnvY(scan);
        let dist = Math.hypot(scan - x_prev, y - y_prev);
        let err = Math.abs(dist - L);
        if (err < minError) {
            minError = err;
            bestX = scan;
        }
    }
    
    // Binary search refinement
    let left = bestX - 1.0;
    let right = bestX + 1.0;
    for(let i=0; i<12; i++) {
        let mid = (left + right) / 2;
        let y = getEnvY(mid);
        let dist = Math.hypot(mid - x_prev, y - y_prev);
        if (dist > L) right = mid;
        else left = mid;
    }
    return {x: right, y: getEnvY(right)};
}

// Track Offset Math (Miter joint handling)
function getOffsetPoints(pA, pB, pC, R) {
    let dx1 = pB.x - pA.x, dy1 = pB.y - pA.y;
    let l1 = Math.hypot(dx1, dy1);
    let n1x = -dy1/l1, n1y = dx1/l1;

    let dx2 = pC.x - pB.x, dy2 = pC.y - pB.y;
    let l2 = Math.hypot(dx2, dy2);
    let n2x = -dy2/l2, n2y = dx2/l2;

    let nx = n1x + n2x, ny = n1y + n2y;
    let len = Math.hypot(nx, ny);

    if(len < 0.01) return { U: {x: pB.x - n1x*R, y: pB.y - n1y*R}, D: {x: pB.x + n1x*R, y: pB.y + n1y*R} };
    
    nx /= len; ny /= len;
    let dot = n1x*nx + n1y*ny;
    let miter = R / Math.max(0.2, dot);

    return {
        U: {x: pB.x - nx*miter, y: pB.y - ny*miter},
        D: {x: pB.x + nx*miter, y: pB.y + ny*miter}
    };
}

function getNormal(p1, p2) {
    let dx = p2.x - p1.x, dy = p2.y - p1.y;
    let l = Math.hypot(dx, dy);
    return {nx: -dy/l, ny: dx/l}; // Points 'down' (positive Y direction in SVG)
}

// --- Main Animation Loop ---
let basePerimeter = 0;
let isFirstFrame = true;

function animate(time) {
    // 1. Compute global driving variable X
    // Cycle every ~14 seconds
    const duration = 14000;
    const progress = (time % duration) / duration; 
    const startX = -100;
    const endX = 1300;
    let wx0 = startX + progress * (endX - startX);

    // 2. Kinematic Forward Solver
    let p0 = {x: wx0, y: getEnvY(wx0)};
    let p1 = solveNextPoint(p0.x, p0.y, CHASSIS_L);
    let p2 = solveNextPoint(p1.x, p1.y, CHASSIS_L);
    let p3 = solveNextPoint(p2.x, p2.y, CHASSIS_L);
    let points = [p0, p1, p2, p3];

    // 3. Update Chassis Segments
    [elSeg1, elSeg2, elSeg3].forEach((el, i) => {
        el.setAttribute('x1', points[i].x); el.setAttribute('y1', points[i].y);
        el.setAttribute('x2', points[i+1].x); el.setAttribute('y2', points[i+1].y);
    });

    // 4. Update Wheels
    let wheelDist = wx0; // simple approximation for rotation
    let wheelAngle = (wheelDist / WHEEL_R) * (180/Math.PI);
    for(let i=0; i<4; i++) {
        let w = document.getElementById(`wheel${i}`);
        w.setAttribute('transform', `translate(${points[i].x}, ${points[i].y}) rotate(${wheelAngle})`);
    }

    // 5. Compute Track Path & Perimeter
    let n0 = getNormal(p0, p1);
    let n3 = getNormal(p2, p3);
    
    let D0 = {x: p0.x + n0.nx*TRACK_R, y: p0.y + n0.ny*TRACK_R};
    let U0 = {x: p0.x - n0.nx*TRACK_R, y: p0.y - n0.ny*TRACK_R};
    let D3 = {x: p3.x + n3.nx*TRACK_R, y: p3.y + n3.ny*TRACK_R};
    let U3 = {x: p3.x - n3.nx*TRACK_R, y: p3.y - n3.ny*TRACK_R};

    let j1 = getOffsetPoints(p0, p1, p2, TRACK_R);
    let j2 = getOffsetPoints(p1, p2, p3, TRACK_R);

    // Build SVG Path
    let trackPath = `M ${U0.x} ${U0.y} L ${j1.U.x} ${j1.U.y} L ${j2.U.x} ${j2.U.y} L ${U3.x} ${U3.y} `;
    trackPath += `A ${TRACK_R} ${TRACK_R} 0 0 1 ${D3.x} ${D3.y} `;
    trackPath += `L ${j2.D.x} ${j2.D.y} L ${j1.D.x} ${j1.D.y} L ${D0.x} ${D0.y} `;
    trackPath += `A ${TRACK_R} ${TRACK_R} 0 0 1 ${U0.x} ${U0.y} Z`;

    elMainTrack.setAttribute('d', trackPath);
    elMainTrackDash.setAttribute('d', trackPath);
    elMainTrackDash.style.strokeDashoffset = -wheelDist; // Animate inner belt

    // Perimeter calculation for dynamic tensioning
    let perim = 0;
    perim += Math.hypot(j1.U.x - U0.x, j1.U.y - U0.y);
    perim += Math.hypot(j2.U.x - j1.U.x, j2.U.y - j1.U.y);
    perim += Math.hypot(U3.x - j2.U.x, U3.y - j2.U.y);
    perim += Math.PI * TRACK_R; // half circle front
    perim += Math.hypot(j2.D.x - D3.x, j2.D.y - D3.y);
    perim += Math.hypot(j1.D.x - j2.D.x, j1.D.y - j2.D.y);
    perim += Math.hypot(D0.x - j1.D.x, D0.y - j1.D.y);
    perim += Math.PI * TRACK_R; // half circle back

    if (isFirstFrame) {
        basePerimeter = perim;
        isFirstFrame = false;
    }

    // 6. Articulation Angles & Visual Feedback (Torsion Springs)
    function getAngle(pA, pB) { return Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI; }
    let a1 = getAngle(p0, p1);
    let a2 = getAngle(p1, p2);
    let a3 = getAngle(p2, p3);

    let flex1 = a2 - a1;
    let flex2 = a3 - a2;
    
    // Normalize flex degrees
    if (flex1 > 180) flex1 -= 360; if (flex1 < -180) flex1 += 360;
    if (flex2 > 180) flex2 -= 360; if (flex2 < -180) flex2 += 360;

    hudAngle1.textContent = Math.abs(flex1).toFixed(1) + '°';
    hudAngle2.textContent = Math.abs(flex2).toFixed(1) + '°';

    // Highlight logic
    const THRESHOLD = 3.0;
    
    function drawHingeArc(elArc, pCtr, pPrev, angleDelta, isActive) {
        if(Math.abs(angleDelta) < THRESHOLD) {
            elArc.setAttribute('d', '');
            elArc.classList.remove('active-hinge');
            return false;
        }
        let r = 35;
        let baseA = getAngle(pPrev, pCtr) * Math.PI / 180;
        let deltaA = angleDelta * Math.PI / 180;
        let startX = pCtr.x + r * Math.cos(baseA);
        let startY = pCtr.y + r * Math.sin(baseA);
        let endX = pCtr.x + r * Math.cos(baseA + deltaA);
        let endY = pCtr.y + r * Math.sin(baseA + deltaA);
        let sweep = angleDelta > 0 ? 1 : 0;
        elArc.setAttribute('d', `M ${pCtr.x} ${pCtr.y} L ${startX} ${startY} A ${r} ${r} 0 0 ${sweep} ${endX} ${endY} Z`);
        elArc.classList.add('active-hinge');
        return true;
    }

    let active1 = drawHingeArc(elHingeArc1, p1, p0, flex1, true);
    let active2 = drawHingeArc(elHingeArc2, p2, p1, flex2, true);

    // Callout lines
    function updateCallout(active, px, py, textEl, lineEl, offsetX, offsetY, text) {
        if(active) {
            lineEl.setAttribute('x1', px); lineEl.setAttribute('y1', py);
            lineEl.setAttribute('x2', px + offsetX); lineEl.setAttribute('y2', py + offsetY);
            textEl.setAttribute('x', px + offsetX + (offsetX>0?5:-70)); 
            textEl.setAttribute('y', py + offsetY + 4);
            textEl.textContent = text + " " + Math.abs(flex1>flex2?flex1:flex2).toFixed(0) + "°";
            lineEl.classList.add('visible');
            textEl.classList.add('active-text');
            textEl.style.opacity = 1;
        } else {
            lineEl.classList.remove('visible');
            textEl.classList.remove('active-text');
            textEl.style.opacity = 0;
        }
    }
    updateCallout(active1, p1.x, p1.y, cText1, cLine1, -50, -80, "TORSION YIELD");
    updateCallout(active2, p2.x, p2.y, cText2, cLine2, 50, -80, "TORSION YIELD");


    // 7. Dynamic Tensioner Visualization
    let deltaP = perim - basePerimeter;
    let force = BASE_TENSION + Math.max(0, deltaP * 1.5);
    hudLength.innerHTML = perim.toFixed(1) + ' <tspan class="hud-unit">mm</tspan>';
    hudTension.innerHTML = force.toFixed(1) + ' <tspan class="hud-unit">N</tspan>';
    
    // Animate HUD Bar
    let barW = Math.min(260, (force / 300) * 260);
    hudTensionBar.setAttribute('width', barW);
    if(force > 160) {
        hudTension.style.fill = 'var(--warning)';
        hudTensionBar.style.fill = 'var(--warning)';
    } else {
        hudTension.style.fill = 'var(--primary)';
        hudTensionBar.style.fill = 'var(--primary)';
    }

    // Draw Spring at Rear Wheel (p0)
    // Spring stretches horizontally relative to segment 1 angle
    let a1_rad = a1 * Math.PI / 180;
    let ext = Math.max(0, deltaP * 1.5); // Visual stretch amount
    let sprPts = "";
    let coils = 5;
    let coilW = (20 + ext) / coils;
    
    // Transform spring to lie along segment 1, anchored slightly behind P0
    let bx = p0.x - Math.cos(a1_rad)*10;
    let by = p0.y - Math.sin(a1_rad)*10;
    
    for(let i=0; i<=coils; i++) {
        let x = bx + Math.cos(a1_rad)*(i*coilW);
        let y = by + Math.sin(a1_rad)*(i*coilW);
        let perpX = -Math.sin(a1_rad) * (i%2===0 ? 8 : -8);
        let perpY = Math.cos(a1_rad) * (i%2===0 ? 8 : -8);
        sprPts += `${x+perpX},${y+perpY} `;
    }
    elTenSpring.setAttribute('d', `M ${bx},${by} L ${sprPts.trim()}`);
    
    // Position tensioner block
    elTenBlock.setAttribute('transform', `translate(${bx + Math.cos(a1_rad)*(20+ext)}, ${by + Math.sin(a1_rad)*(20+ext)}) rotate(${a1})`);

    // 8. Camera Tracking
    // Keep vehicle centered. Center is ~p1
    let camTargetX = p1.x - 600;
    let camTargetY = p1.y - 600;
    // Apply soft transform
    elWorld.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
    
    // Update callouts to track camera
    cLine1.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
    cText1.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
    cLine2.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
    cText2.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);


    requestAnimationFrame(animate);
}

// Bootstrap
initGeometry();
requestAnimationFrame(animate);

</script>
</body>
</html>
<!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 Solution: Articulated Flexible Track Chassis</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700;800&family=Space+Grotesk:wght@400;700&display=swap');

        :root {
            --bg-color: #050914;
            --grid-color: rgba(0, 229, 255, 0.08);
            --stair-color: #0c1222;
            --stair-stroke: #1e3a5f;
            --primary: #00E5FF;
            --warning: #FF3D00;
            --success: #00E676;
            --text-main: #E0F7FA;
            --text-muted: #4DD0E1;
            --font-mono: 'JetBrains Mono', monospace;
            --font-display: 'Space Grotesk', sans-serif;
        }

        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: var(--bg-color);
            color: var(--text-main);
            overflow: hidden;
            font-family: var(--font-display);
        }

        #canvas-container {
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
        }

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

        /* SVG Element Styling */
        .track-belt {
            fill: rgba(0, 229, 255, 0.05);
            stroke: var(--primary);
            stroke-width: 5;
            stroke-linejoin: round;
            stroke-linecap: round;
        }

        .track-inner-dash {
            fill: none;
            stroke: #fff;
            stroke-width: 2.5;
            stroke-dasharray: 8 16;
            opacity: 0.7;
        }

        .chassis-segment {
            fill: none;
            stroke: rgba(0, 229, 255, 0.25);
            stroke-width: 28;
            stroke-linecap: round;
        }
        
        .chassis-rod {
            fill: none;
            stroke: var(--primary);
            stroke-width: 10;
            stroke-linecap: round;
        }

        .wheel {
            fill: #0a1128;
            stroke: var(--primary);
            stroke-width: 3;
        }

        .wheel-spoke {
            stroke: rgba(0, 229, 255, 0.6);
            stroke-width: 3;
            stroke-linecap: round;
        }

        .stair-polygon {
            fill: var(--stair-color);
            stroke: var(--stair-stroke);
            stroke-width: 4;
            stroke-linejoin: round;
        }

        .envelope-path {
            fill: none;
            stroke: rgba(0, 229, 255, 0.15);
            stroke-width: 2;
            stroke-dasharray: 6 6;
        }

        .hinge-joint {
            fill: var(--bg-color);
            stroke: var(--primary);
            stroke-width: 3;
        }
        
        .hinge-ring {
            fill: none;
            stroke: rgba(0, 229, 255, 0.5);
            stroke-width: 3;
        }

        /* Dynamic Classes applied via JS */
        .active-hinge {
            stroke: var(--warning);
            filter: drop-shadow(0 0 12px var(--warning));
        }

        .active-text {
            fill: var(--warning) !important;
            font-weight: bold;
            text-shadow: 0 0 10px rgba(255, 61, 0, 0.8);
        }

        .hud-label { font-size: 13px; fill: var(--text-muted); font-family: var(--font-mono); letter-spacing: 1px;}
        .hud-value { font-size: 18px; fill: var(--primary); font-family: var(--font-mono); font-weight: 800; }
        .hud-unit { font-size: 11px; fill: rgba(0,229,255,0.6); }

        .callout-line {
            stroke: var(--warning);
            stroke-width: 2;
            stroke-dasharray: 3 5;
            opacity: 0;
            transition: opacity 0.2s;
        }

        .callout-line.visible { opacity: 0.9; }

        /* UI Overlay */
        .overlay {
            position: absolute;
            top: 40px;
            left: 50px;
            pointer-events: none;
            z-index: 100;
        }

        .triz-badge {
            display: inline-block;
            background: rgba(0, 229, 255, 0.15);
            border: 1px solid var(--primary);
            color: var(--primary);
            padding: 6px 16px;
            font-family: var(--font-mono);
            font-size: 13px;
            border-radius: 4px;
            margin-bottom: 16px;
            text-transform: uppercase;
            letter-spacing: 2px;
            box-shadow: 0 0 20px rgba(0, 229, 255, 0.2);
        }

        h1 { margin: 0 0 12px 0; font-size: 2.8rem; letter-spacing: -1px; text-shadow: 0 2px 10px rgba(0,0,0,0.5); }
        p { margin: 0; font-family: var(--font-mono); color: var(--text-muted); font-size: 0.95rem; max-width: 550px; line-height: 1.6; background: rgba(5,9,20,0.6); padding: 15px; border-left: 3px solid var(--primary); border-radius: 0 8px 8px 0; backdrop-filter: blur(4px); }

        .scanline {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 51%);
            background-size: 100% 4px;
            pointer-events: none;
            z-index: 900;
            opacity: 0.4;
        }
    </style>
</head>
<body>

<div id="canvas-container">
    <div class="scanline"></div>
    
    <div class="overlay">
        <div class="triz-badge">TRIZ IFR Solution</div>
        <h1>Articulated Soft-Track</h1>
        <p>Eliminating step-slip via segmented pitch hinges and dynamic telescopic tensioning. The parts stay mechanically interlinked while continuously morphing to the irregular terrain.</p>
    </div>

    <svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
        <defs>
            <pattern id="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"/>
            </pattern>
            <pattern id="grid-large" width="300" height="300" patternUnits="userSpaceOnUse">
                <path d="M 300 0 L 0 0 0 300" fill="none" stroke="rgba(0, 229, 255, 0.15)" stroke-width="2"/>
            </pattern>
            <filter id="glow">
                <feGaussianBlur stdDeviation="5" result="coloredBlur"/>
                <feMerge>
                    <feMergeNode in="coloredBlur"/>
                    <feMergeNode in="SourceGraphic"/>
                </feMerge>
            </filter>
        </defs>

        <!-- World Group (Camera follows vehicle, grids move with world) -->
        <g id="world" transform="translate(0, 0)">
            
            <!-- Infinite Background Grids fixed to World -->
            <rect x="-5000" y="-5000" width="15000" height="10000" fill="url(#grid)" />
            <rect x="-5000" y="-5000" width="15000" height="10000" fill="url(#grid-large)" />

            <!-- Environment / Stairs -->
            <polygon id="stairs" class="stair-polygon" points="" />
            <!-- Kinetmatic Envelope -->
            <path id="envelope-path" class="envelope-path" d="" />

            <!-- Vehicle Assembly -->
            <g id="vehicle">
                
                <!-- Chassis Modules -->
                <!-- Seg 1: Telescopic Tensioner Module -->
                <line id="seg1-tube" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />
                <line id="seg1-rod" class="chassis-rod" x1="0" y1="0" x2="0" y2="0" />
                <path id="tensioner-spring" fill="none" stroke="var(--warning)" stroke-width="4" stroke-linejoin="round" />
                
                <!-- Seg 2 & 3: Rigid Modules -->
                <line id="seg2" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />
                <line id="seg3" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />

                <!-- Outer Rubber Track & Dashes -->
                <path id="main-track" class="track-belt" d="" filter="url(#glow)"/>
                <path id="main-track-dash" class="track-inner-dash" d="" />

                <!-- Wheels and Mechanical Joints -->
                <g id="wheels"></g>

                <!-- Torsion Hinge Highlight Arcs -->
                <path id="hinge-arc1" fill="none" stroke-width="5" stroke-linecap="round" />
                <path id="hinge-arc2" fill="none" stroke-width="5" stroke-linecap="round" />
            </g>
        </g>

        <!-- Static HUD Overlays -->
        <g transform="translate(1220, 60)">
            <rect width="320" height="230" fill="rgba(5, 9, 20, 0.85)" stroke="rgba(0,229,255,0.4)" stroke-width="1" rx="8"/>
            <text x="25" y="35" class="hud-label">SYSTEM KINEMATICS</text>
            <line x1="25" y1="48" x2="295" y2="48" stroke="rgba(0,229,255,0.2)" stroke-width="2"/>
            
            <text x="25" y="85" class="hud-label">FRONT HINGE ANGLE</text>
            <text x="295" y="85" id="hud-angle2" class="hud-value" text-anchor="end">0.0°</text>
            
            <text x="25" y="125" class="hud-label">REAR HINGE ANGLE</text>
            <text x="295" y="125" id="hud-angle1" class="hud-value" text-anchor="end">0.0°</text>
            
            <text x="25" y="165" class="hud-label">TENSION CYLINDER LOAD</text>
            <text x="295" y="165" id="hud-tension" class="hud-value" text-anchor="end">150.0 <tspan class="hud-unit">N</tspan></text>

            <rect x="25" y="195" width="270" height="6" fill="rgba(0,229,255,0.1)" rx="3"/>
            <rect id="hud-tension-bar" x="25" y="195" width="135" height="6" fill="var(--primary)" rx="3" style="transition: width 0.1s linear, fill 0.2s;"/>
        </g>

        <!-- Dynamic Callouts (Positioned in Screen Space) -->
        <g id="callouts">
            <line id="callout-line1" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
            <text id="callout-text1" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">TORSION YIELD</text>
            
            <line id="callout-line2" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
            <text id="callout-text2" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">TORSION YIELD</text>
            
            <line id="callout-line3" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
            <text id="callout-text3" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">DYNAMIC TENSION (150N)</text>
        </g>

    </svg>
</div>

<script>
/**
 * TRIZ IFR Kinematic Simulation - Highly Robust Physics & Rendering
 */

const CHASSIS_L = 140; 
const WHEEL_R = 26;    
const TRACK_R = 32;    
const BASE_TENSION = 150; 

// --- Environment Geometry (Stairs) ---
const stairsData = [
    {x: -1500, y: 500},
    {x: 300, y: 500},
    {x: 300, y: 400}, // Step 1
    {x: 480, y: 400}, 
    {x: 480, y: 250}, // Step 2 (Irregular)
    {x: 600, y: 250},
    {x: 600, y: 120}, // Step 3
    {x: 1000, y: 120},
    {x: 1000, y: -20}, // Step 4
    {x: 4000, y: -20}
];

// Kinematic envelope ensures wheels perfectly track the terrain peaks
const envelopeData = [
    {x: -1500, y: 474},
    {x: 240,  y: 474},
    {x: 340,  y: 374}, 
    {x: 440,  y: 374},
    {x: 520,  y: 224},
    {x: 560,  y: 224},
    {x: 640,  y: 94},
    {x: 960,  y: 94},
    {x: 1040, y: -46},
    {x: 4000, y: -46}
];

// --- DOM Elements ---
const elStairs = document.getElementById('stairs');
const elEnvelope = document.getElementById('envelope-path');
const elWorld = document.getElementById('world');
const elMainTrack = document.getElementById('main-track');
const elMainTrackDash = document.getElementById('main-track-dash');

const elSeg1Tube = document.getElementById('seg1-tube');
const elSeg1Rod = document.getElementById('seg1-rod');
const elTenSpring = document.getElementById('tensioner-spring');

const elSeg2 = document.getElementById('seg2');
const elSeg3 = document.getElementById('seg3');

const elWheelsGrp = document.getElementById('wheels');
const elHingeArc1 = document.getElementById('hinge-arc1');
const elHingeArc2 = document.getElementById('hinge-arc2');

const hudAngle1 = document.getElementById('hud-angle1');
const hudAngle2 = document.getElementById('hud-angle2');
const hudTension = document.getElementById('hud-tension');
const hudTensionBar = document.getElementById('hud-tension-bar');

const callouts = [
    { line: document.getElementById('callout-line1'), text: document.getElementById('callout-text1') },
    { line: document.getElementById('callout-line2'), text: document.getElementById('callout-text2') },
    { line: document.getElementById('callout-line3'), text: document.getElementById('callout-text3') }
];

// Initialize SVG static geometry
function initGeometry() {
    let pts = "";
    stairsData.forEach(p => pts += `${p.x},${p.y} `);
    pts += `4000,1200 -1500,1200`; 
    elStairs.setAttribute('points', pts.trim());

    let d = `M ${envelopeData[0].x} ${envelopeData[0].y}`;
    for(let i=1; i<envelopeData.length; i++) {
        d += ` L ${envelopeData[i].x} ${envelopeData[i].y}`;
    }
    elEnvelope.setAttribute('d', d);

    for(let i=0; i<4; i++) {
        const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        g.setAttribute('id', `wheel${i}`);
        
        const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        circle.setAttribute('r', WHEEL_R);
        circle.setAttribute('class', 'wheel');
        
        const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line1.setAttribute('x1', -WHEEL_R+5); line1.setAttribute('y1', 0);
        line1.setAttribute('x2', WHEEL_R-5); line1.setAttribute('y2', 0);
        line1.setAttribute('class', 'wheel-spoke');
        
        const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line2.setAttribute('x1', 0); line2.setAttribute('y1', -WHEEL_R+5);
        line2.setAttribute('x2', 0); line2.setAttribute('y2', WHEEL_R-5);
        line2.setAttribute('class', 'wheel-spoke');

        const ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        ring.setAttribute('r', 12);
        ring.setAttribute('class', 'hinge-ring');
        
        const hub = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        hub.setAttribute('r', 5);
        hub.setAttribute('class', 'hinge-joint');
        
        g.appendChild(circle);
        g.appendChild(line1);
        g.appendChild(line2);
        g.appendChild(ring);
        g.appendChild(hub);
        elWheelsGrp.appendChild(g);
    }
}

// --- Kinematic Math Solvers ---
function getEnvY(x) {
    for(let i=0; i<envelopeData.length-1; i++) {
        if(x >= envelopeData[i].x && x <= envelopeData[i+1].x) {
            let t = (x - envelopeData[i].x) / (envelopeData[i+1].x - envelopeData[i].x);
            return envelopeData[i].y + t * (envelopeData[i+1].y - envelopeData[i].y);
        }
    }
    return envelopeData[envelopeData.length-1].y;
}

// Guaranteed binary search solver to prevent part detachment
function solveNextPoint(x_prev, y_prev, L) {
    let left = x_prev;
    let right = x_prev + L; 
    for(let i=0; i<20; i++) {
        let mid = (left + right) / 2;
        let y = getEnvY(mid);
        let dist = Math.hypot(mid - x_prev, y - y_prev);
        if (dist > L) right = mid;
        else left = mid;
    }
    return {x: right, y: getEnvY(right)};
}

// Vector math
function sub(pA, pB) { return {x: pA.x - pB.x, y: pA.y - pB.y}; }
function add(pA, v) { return {x: pA.x + v.x, y: pA.y + v.y}; }
function getNormal(pA, pB) {
    let dx = pB.x - pA.x, dy = pB.y - pA.y;
    let l = Math.hypot(dx, dy);
    return {nx: -dy/l, ny: dx/l}; 
}
function getMiterPoint(p, nA, nB, sign) {
    let nx = nA.nx + nB.nx, ny = nA.ny + nB.ny;
    let len = Math.hypot(nx, ny);
    if (len < 0.01) return {x: p.x + sign*nA.nx*TRACK_R, y: p.y + sign*nA.ny*TRACK_R};
    nx /= len; ny /= len;
    let dot = nA.nx*nx + nA.ny*ny;
    let miter = TRACK_R / Math.max(0.1, dot);
    return {x: p.x + sign*nx*miter, y: p.y + sign*ny*miter};
}

let basePerimeter = 0;

function animate(time) {
    // 1. Drive variable
    const duration = 14000;
    const progress = (time % duration) / duration; 
    const startX = -300;
    const endX = 1400;
    let wx0 = startX + progress * (endX - startX);

    // 2. Base Kinematics
    let p0_math = {x: wx0, y: getEnvY(wx0)};
    let p1 = solveNextPoint(p0_math.x, p0_math.y, CHASSIS_L);
    let p2 = solveNextPoint(p1.x, p1.y, CHASSIS_L);
    let p3 = solveNextPoint(p2.x, p2.y, CHASSIS_L);

    // 3. Compute Slack & Tension
    let n0_raw = getNormal(p0_math, p1);
    let n1_raw = getNormal(p1, p2);
    let n2_raw = getNormal(p2, p3);
    
    // Rough perimeter estimate to find slack
    let rawPerim = Math.hypot(p1.x-p0_math.x, p1.y-p0_math.y) + 
                   Math.hypot(p2.x-p1.x, p2.y-p1.y) + 
                   Math.hypot(p3.x-p2.x, p3.y-p2.y);
    rawPerim = rawPerim * 2 + 2 * Math.PI * TRACK_R;

    if (basePerimeter === 0) basePerimeter = rawPerim;
    
    let slack = Math.max(0, basePerimeter - rawPerim);
    let stretchDist = slack / 2; // Rear wheel moves back to take up slack

    // Real visual p0 shifted backwards to simulate telescoping tensioner
    let v01 = sub(p0_math, p1);
    let len01 = Math.hypot(v01.x, v01.y);
    let u01 = {x: v01.x/len01, y: v01.y/len01};
    let p0 = {x: p0_math.x + u01.x * stretchDist, y: p0_math.y + u01.y * stretchDist};

    let points = [p0, p1, p2, p3];

    // 4. Draw Telescopic Segment 1 (Tensioner Assembly)
    let tube_len = 80;
    let tube_end = {x: p1.x + u01.x * tube_len, y: p1.y + u01.y * tube_len};
    
    elSeg1Tube.setAttribute('x1', p1.x); elSeg1Tube.setAttribute('y1', p1.y);
    elSeg1Tube.setAttribute('x2', tube_end.x); elSeg1Tube.setAttribute('y2', tube_end.y);
    
    elSeg1Rod.setAttribute('x1', tube_end.x); elSeg1Rod.setAttribute('y1', tube_end.y);
    elSeg1Rod.setAttribute('x2', p0.x); elSeg1Rod.setAttribute('y2', p0.y);

    // Zigzag Spring on Rod
    let sprPts = "";
    let coils = 7;
    let distRod = Math.hypot(p0.x - tube_end.x, p0.y - tube_end.y);
    let coilW = distRod / coils;
    for(let i=0; i<=coils; i++) {
        let cx = tube_end.x + u01.x * (i*coilW);
        let cy = tube_end.y + u01.y * (i*coilW);
        let px = -u01.y * (i%2===0 ? 14 : -14);
        let py = u01.x * (i%2===0 ? 14 : -14);
        if(i===0 || i===coils) sprPts += `${cx},${cy} `;
        else sprPts += `${cx+px},${cy+py} `;
    }
    elTenSpring.setAttribute('d', `M ${tube_end.x},${tube_end.y} L ${sprPts.trim()} L ${p0.x},${p0.y}`);

    // Rigid Segments 2 & 3
    elSeg2.setAttribute('x1', p1.x); elSeg2.setAttribute('y1', p1.y);
    elSeg2.setAttribute('x2', p2.x); elSeg2.setAttribute('y2', p2.y);
    elSeg3.setAttribute('x1', p2.x); elSeg3.setAttribute('y1', p2.y);
    elSeg3.setAttribute('x2', p3.x); elSeg3.setAttribute('y2', p3.y);

    // 5. Update Wheels
    let wheelAngle = (wx0 / WHEEL_R) * (180/Math.PI);
    for(let i=0; i<4; i++) {
        let w = document.getElementById(`wheel${i}`);
        w.setAttribute('transform', `translate(${points[i].x}, ${points[i].y}) rotate(${wheelAngle})`);
    }

    // 6. Robust Track Path Generation (Ensuring perfect wrap and connection)
    let n0 = getNormal(p0, p1);
    let n1 = getNormal(p1, p2);
    let n2 = getNormal(p2, p3);

    let U0 = {x: p0.x - n0.nx*TRACK_R, y: p0.y - n0.ny*TRACK_R};
    let U1_in = {x: p1.x - n0.nx*TRACK_R, y: p1.y - n0.ny*TRACK_R};
    let U1_out = {x: p1.x - n1.nx*TRACK_R, y: p1.y - n1.ny*TRACK_R};
    let U2_in = {x: p2.x - n1.nx*TRACK_R, y: p2.y - n1.ny*TRACK_R};
    let U2_out = {x: p2.x - n2.nx*TRACK_R, y: p2.y - n2.ny*TRACK_R};
    let U3 = {x: p3.x - n2.nx*TRACK_R, y: p3.y - n2.ny*TRACK_R};

    let D0 = {x: p0.x + n0.nx*TRACK_R, y: p0.y + n0.ny*TRACK_R};
    let D1_in = {x: p1.x + n0.nx*TRACK_R, y: p1.y + n0.ny*TRACK_R};
    let D1_out = {x: p1.x + n1.nx*TRACK_R, y: p1.y + n1.ny*TRACK_R};
    let D2_in = {x: p2.x + n1.nx*TRACK_R, y: p2.y + n1.ny*TRACK_R};
    let D2_out = {x: p2.x + n2.nx*TRACK_R, y: p2.y + n2.ny*TRACK_R};
    let D3 = {x: p3.x + n2.nx*TRACK_R, y: p3.y + n2.ny*TRACK_R};

    let j1_U = getMiterPoint(p1, n0, n1, -1);
    let j1_D = getMiterPoint(p1, n0, n1, 1);
    let j2_U = getMiterPoint(p2, n1, n2, -1);
    let j2_D = getMiterPoint(p2, n1, n2, 1);

    let cross1 = (p1.x - p0.x)*(p2.y - p1.y) - (p1.y - p0.y)*(p2.x - p1.x);
    let cross2 = (p2.x - p1.x)*(p3.y - p2.y) - (p2.y - p1.y)*(p3.x - p2.x);

    let tPath = `M ${U0.x} ${U0.y} `;
    
    if (cross1 > 0) tPath += `L ${U1_in.x} ${U1_in.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U1_out.x} ${U1_out.y} `;
    else tPath += `L ${j1_U.x} ${j1_U.y} `;

    if (cross2 > 0) tPath += `L ${U2_in.x} ${U2_in.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U2_out.x} ${U2_out.y} `;
    else tPath += `L ${j2_U.x} ${j2_U.y} `;

    tPath += `L ${U3.x} ${U3.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D3.x} ${D3.y} `;

    if (cross2 < 0) tPath += `L ${D2_out.x} ${D2_out.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D2_in.x} ${D2_in.y} `;
    else tPath += `L ${j2_D.x} ${j2_D.y} `;

    if (cross1 < 0) tPath += `L ${D1_out.x} ${D1_out.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D1_in.x} ${D1_in.y} `;
    else tPath += `L ${j1_D.x} ${j1_D.y} `;

    tPath += `L ${D0.x} ${D0.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U0.x} ${U0.y} Z`;

    elMainTrack.setAttribute('d', tPath);
    elMainTrackDash.setAttribute('d', tPath);
    elMainTrackDash.style.strokeDashoffset = -wx0;

    // 7. Angles & HUD
    function getAngle(pA, pB) { return Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI; }
    let a1 = getAngle(p0, p1);
    let a2 = getAngle(p1, p2);
    let a3 = getAngle(p2, p3);

    let flex1 = a2 - a1;
    let flex2 = a3 - a2;
    if (flex1 > 180) flex1 -= 360; if (flex1 < -180) flex1 += 360;
    if (flex2 > 180) flex2 -= 360; if (flex2 < -180) flex2 += 360;

    hudAngle1.textContent = Math.abs(flex1).toFixed(1) + '°';
    hudAngle2.textContent = Math.abs(flex2).toFixed(1) + '°';

    // HUD Tension
    let force = BASE_TENSION + slack * 4; // visual multiplier
    hudTension.innerHTML = force.toFixed(1) + ' <tspan class="hud-unit">N</tspan>';
    
    let barW = Math.min(270, (force / 350) * 270);
    hudTensionBar.setAttribute('width', barW);
    if(force > 170) {
        hudTension.style.fill = 'var(--warning)';
        hudTensionBar.style.fill = 'var(--warning)';
    } else {
        hudTension.style.fill = 'var(--primary)';
        hudTensionBar.style.fill = 'var(--primary)';
    }

    // Hinge Arcs
    function drawHingeArc(elArc, pCtr, pPrev, angleDelta) {
        if(Math.abs(angleDelta) < 3.0) {
            elArc.setAttribute('d', '');
            elArc.classList.remove('active-hinge');
            return false;
        }
        let r = 40;
        let baseA = getAngle(pPrev, pCtr) * Math.PI / 180;
        let deltaA = angleDelta * Math.PI / 180;
        let startX = pCtr.x + r * Math.cos(baseA);
        let startY = pCtr.y + r * Math.sin(baseA);
        let endX = pCtr.x + r * Math.cos(baseA + deltaA);
        let endY = pCtr.y + r * Math.sin(baseA + deltaA);
        let sweep = angleDelta > 0 ? 1 : 0;
        elArc.setAttribute('d', `M ${pCtr.x} ${pCtr.y} L ${startX} ${startY} A ${r} ${r} 0 0 ${sweep} ${endX} ${endY} Z`);
        elArc.classList.add('active-hinge');
        return true;
    }
    let act1 = drawHingeArc(elHingeArc1, p1, p0, flex1);
    let act2 = drawHingeArc(elHingeArc2, p2, p1, flex2);

    // 8. Camera Tracking (World Translation)
    let camTargetX = p1.x - 700;
    let camTargetY = p1.y - 650;
    elWorld.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);

    // Callout Sync to World Space
    function updateCallout(active, px, py, item, offsetX, offsetY, text) {
        if(active) {
            let sx = px - camTargetX;
            let sy = py - camTargetY;
            item.line.setAttribute('x1', sx); item.line.setAttribute('y1', sy);
            item.line.setAttribute('x2', sx + offsetX); item.line.setAttribute('y2', sy + offsetY);
            item.text.setAttribute('x', sx + offsetX + (offsetX>0?8:-90)); 
            item.text.setAttribute('y', sy + offsetY + 5);
            item.text.textContent = text;
            item.line.classList.add('visible');
            item.text.classList.add('active-text');
            item.text.style.opacity = 1;
        } else {
            item.line.classList.remove('visible');
            item.text.classList.remove('active-text');
            item.text.style.opacity = 0;
        }
    }

    updateCallout(act1, p1.x, p1.y, callouts[0], -60, -90, `TORSION: ${Math.abs(flex1).toFixed(0)}°`);
    updateCallout(act2, p2.x, p2.y, callouts[1], 60, -90, `TORSION: ${Math.abs(flex2).toFixed(0)}°`);
    
    // Tensioner Callout on Rod
    let rodMidX = (tube_end.x + p0.x)/2;
    let rodMidY = (tube_end.y + p0.y)/2;
    updateCallout(slack > 2, rodMidX, rodMidY, callouts[2], -80, 80, `TENSION: ${force.toFixed(0)}N`);

    requestAnimationFrame(animate);
}

initGeometry();
requestAnimationFrame(animate);

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