分享图
动画工坊
引擎就绪
<!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>
        @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;500;700&display=swap');

        :root {
            --bg-color: #030712;
            --grid-color: rgba(30, 58, 138, 0.2);
            --text-primary: #94a3b8;
            --text-highlight: #38bdf8;
            --spring-comp: #f97316; /* 压缩:橙色 */
            --spring-tens: #0ea5e9; /* 拉伸:青色 */
            --flange-stroke: #38bdf8;
            --flange-fill: rgba(8, 47, 73, 0.6);
        }

        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color: var(--bg-color);
            background-image: 
                linear-gradient(var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 40px 40px;
            background-position: center center;
            font-family: 'JetBrains Mono', monospace;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        /* 绝对定位的说明性文本,严格遵守角落布局和小字号要求 */
        .overlay {
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            padding: 24px;
            z-index: 10;
        }

        .header, .footer, .sidebar {
            background: rgba(3, 7, 18, 0.7);
            backdrop-filter: blur(4px);
            border: 1px solid rgba(56, 189, 248, 0.2);
            padding: 12px 16px;
            border-radius: 8px;
        }

        .header {
            align-self: flex-start;
            max-width: 320px;
        }

        .header h1 {
            margin: 0 0 8px 0;
            font-size: 14px;
            color: var(--text-highlight);
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        .header p {
            margin: 0;
            font-size: 11px;
            color: var(--text-primary);
            line-height: 1.5;
        }

        .ifr-tag {
            display: inline-block;
            background: rgba(56, 189, 248, 0.1);
            color: var(--text-highlight);
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 10px;
            margin-bottom: 6px;
            border: 1px solid rgba(56, 189, 248, 0.3);
        }

        .sidebar {
            position: absolute;
            right: 24px;
            top: 50%;
            transform: translateY(-50%);
            display: flex;
            flex-direction: column;
            gap: 12px;
            width: 160px;
        }

        .spring-data {
            font-size: 10px;
            color: var(--text-primary);
        }

        .spring-bar-container {
            width: 100%;
            height: 4px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 2px;
            margin-top: 4px;
            position: relative;
            overflow: hidden;
        }

        .spring-bar {
            position: absolute;
            height: 100%;
            width: 50%;
            left: 50%;
            transform-origin: left;
            border-radius: 2px;
            transition: transform 0.1s linear, background-color 0.1s linear;
        }

        .footer {
            align-self: flex-start;
        }

        .status-display {
            font-size: 12px;
            color: #fff;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .status-dot {
            width: 8px;
            height: 8px;
            background: #22c55e;
            border-radius: 50%;
            box-shadow: 0 0 8px #22c55e;
        }

        /* SVG 主体容器 */
        #render-container {
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        svg {
            width: 100%;
            height: 100%;
            max-width: 1000px;
            max-height: 1000px;
            filter: drop-shadow(0 0 20px rgba(56, 189, 248, 0.1));
        }

        /* 强制动画焦点在中央,避免被遮挡 */
    </style>
</head>
<body>

    <div class="overlay">
        <div class="header">
            <div class="ifr-tag">TRIZ / IFR 最终理想解</div>
            <h1>六维弹性解耦基座</h1>
            <p>通过3对空间斜置力线构成的“灯笼骨架”,巧妙利用6根标准弹簧实现六维空间的弹性刚度对称。完全消除独立的导向机构,外力作用下弹簧通过协同形变自动平衡,实现结构极简与功能解耦。</p>
        </div>

        <div class="sidebar" id="data-panel">
            <!-- 弹簧数据通过 JS 动态生成 -->
        </div>

        <div class="footer">
            <div class="status-display">
                <div class="status-dot" id="status-dot"></div>
                <span id="status-text">系统初始化...</span>
            </div>
        </div>
    </div>

    <div id="render-container">
        <svg id="stage" viewBox="-400 -400 800 800" preserveAspectRatio="xMidYMid meet">
            <defs>
                <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
                    <feGaussianBlur stdDeviation="4" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                <filter id="glow-strong" x="-50%" y="-50%" width="200%" height="200%">
                    <feGaussianBlur stdDeviation="8" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                </filter>
                <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                    <polygon points="0 0, 10 3.5, 0 7" fill="#fff" />
                </marker>
            </defs>
            <g id="scene"></g>
        </svg>
    </div>

<script>
/**
 * 3D 渲染与物理引擎内核
 */

// 核心参数
const R = 150; // 法兰分布半径
const H_IDLE = 160; // 上下锚点基础高度差
const BASE_Z = -80; // 基础平面Z坐标
const TOP_Z_IDLE = 80; // 顶部平面Z坐标

// 视图相机参数 (等轴测/透视投影混配)
const CAMERA = {
    pitch: 0.6, // 绕X轴俯仰角
    yaw: 0.785  // 绕Z轴偏航角 (45度)
};

// 预定义状态序列 (自动播放动画轨迹)
const ANIMATION_SEQUENCE = [
    { duration: 2000, state: {x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0}, label: '零位状态 (预紧)' },
    { duration: 3000, state: {x: 0, y: 0, z: -50, rx: 0, ry: 0, rz: 0}, label: 'Z向受力: 轴向压缩 (Fz)' },
    { duration: 3000, state: {x: 0, y: 0, z: 30, rx: 0, ry: 0, rz: 0}, label: 'Z向受力: 轴向拉伸 (Fz)' },
    { duration: 3000, state: {x: 60, y: 0, z: 0, rx: 0, ry: 0, rz: 0}, label: 'X向受力: 径向剪切 (Fx)' },
    { duration: 3000, state: {x: 0, y: 60, z: 0, rx: 0, ry: 0, rz: 0}, label: 'Y向受力: 径向剪切 (Fy)' },
    { duration: 3000, state: {x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0.4}, label: '绕Z轴受力: 扭矩 (Mz)' },
    { duration: 3000, state: {x: 0, y: 0, z: 0, rx: -0.3, ry: 0, rz: 0}, label: '绕X轴受力: 弯矩 (Mx)' },
    { duration: 3000, state: {x: 0, y: 0, z: 0, rx: 0, ry: 0.3, rz: 0}, label: '绕Y轴受力: 弯矩 (My)' }
];

// 初始化锚点角度
const BASE_ANGLES = [60, 180, 300].map(a => a * Math.PI / 180);
const TOP_ANGLES = [0, 120, 240].map(a => a * Math.PI / 180);

// 生成法兰多边形的基础点位
function generateFlangePoints(angles) {
    const points = [];
    const rIn = 45;
    angles.forEach(ang => {
        points.push({x: rIn * Math.cos(ang - 0.4), y: rIn * Math.sin(ang - 0.4), z: 0});
        points.push({x: R * Math.cos(ang - 0.1), y: R * Math.sin(ang - 0.1), z: 0});
        points.push({x: R * Math.cos(ang + 0.1), y: R * Math.sin(ang + 0.1), z: 0});
        points.push({x: rIn * Math.cos(ang + 0.4), y: rIn * Math.sin(ang + 0.4), z: 0});
    });
    return points;
}
const baseFlangeLocal = generateFlangePoints(BASE_ANGLES);
const topFlangeLocal = generateFlangePoints(TOP_ANGLES);

// 弹簧连接关系: 上法兰的每个吊耳连接下法兰相邻的两个支座
const SPRING_CONNECTIONS = [
    { topIdx: 0, baseIdx: 2 }, // Top 0° to Base 300°
    { topIdx: 0, baseIdx: 0 }, // Top 0° to Base 60°
    { topIdx: 1, baseIdx: 0 }, // Top 120° to Base 60°
    { topIdx: 1, baseIdx: 1 }, // Top 120° to Base 180°
    { topIdx: 2, baseIdx: 1 }, // Top 240° to Base 180°
    { topIdx: 2, baseIdx: 2 }  // Top 240° to Base 300°
];

// 计算初始静止弹簧长度作为基准
const L0 = Math.hypot(
    R * Math.cos(TOP_ANGLES[0]) - R * Math.cos(BASE_ANGLES[2]),
    R * Math.sin(TOP_ANGLES[0]) - R * Math.sin(BASE_ANGLES[2]),
    TOP_Z_IDLE - BASE_Z
);

// 3D 旋转及平移变换
function transform3D(point, pos) {
    // 旋转 (Tait-Bryan angles: X-Y-Z)
    let cx = Math.cos(pos.rx), sx = Math.sin(pos.rx);
    let cy = Math.cos(pos.ry), sy = Math.sin(pos.ry);
    let cz = Math.cos(pos.rz), sz = Math.sin(pos.rz);

    let x1 = point.x, y1 = point.y, z1 = point.z;

    // Rx
    let x2 = x1, y2 = y1*cx - z1*sx, z2 = y1*sx + z1*cx;
    // Ry
    let x3 = x2*cy + z2*sy, y3 = y2, z3 = -x2*sy + z2*cy;
    // Rz
    let x4 = x3*cz - y3*sz, y4 = x3*sz + y3*cz, z4 = z3;

    return {
        x: x4 + pos.x,
        y: y4 + pos.y,
        z: z4 + pos.z
    };
}

// 投影到 2D 平面
function project3D(point) {
    // Yaw 旋转 (绕Z)
    let x1 = point.x * Math.cos(CAMERA.yaw) - point.y * Math.sin(CAMERA.yaw);
    let y1 = point.x * Math.sin(CAMERA.yaw) + point.y * Math.cos(CAMERA.yaw);
    let z1 = point.z;

    // Pitch 旋转 (绕X)
    let x2 = x1;
    let y2 = y1 * Math.cos(CAMERA.pitch) - z1 * Math.sin(CAMERA.pitch);
    let z2 = y1 * Math.sin(CAMERA.pitch) + z1 * Math.cos(CAMERA.pitch);

    // 返回 2D 坐标和深度 (深度值越小越靠后)
    return { x: x2, y: -y2, depth: -z2 }; 
}

// 插值函数 (缓动效果)
function easeInOutCubic(t) {
    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

// 颜色混合
function interpolateColor(color1, color2, factor) {
    const hex2rgb = hex => [
        parseInt(hex.substring(1, 3), 16),
        parseInt(hex.substring(3, 5), 16),
        parseInt(hex.substring(5, 7), 16)
    ];
    const rgb1 = hex2rgb(color1);
    const rgb2 = hex2rgb(color2);
    const rgb = rgb1.map((c, i) => Math.round(c + factor * (rgb2[i] - c)));
    return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
}

// 构建弹簧 SVG 路径 (波浪形)
function createSpringPath(p1, p2, coils = 10, width = 14) {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const dist = Math.hypot(dx, dy);
    
    // 如果极度压缩,限制最小距离防崩溃
    if(dist < 10) return `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;

    const nx = -dy / dist * width;
    const ny = dx / dist * width;

    let d = `M ${p1.x},${p1.y} `;
    
    // 两端留出直线段
    d += `L ${p1.x + dx*0.05},${p1.y + dy*0.05} `;

    for(let i = 1; i <= coils; i++) {
        const t = 0.05 + 0.9 * (i - 0.5) / coils;
        const px = p1.x + dx * t + nx * (i%2 ? 1 : -1);
        const py = p1.y + dy * t + ny * (i%2 ? 1 : -1);
        d += `L ${px},${py} `;
    }
    
    d += `L ${p1.x + dx*0.95},${p1.y + dy*0.95} `;
    d += `L ${p2.x},${p2.y}`;
    return d;
}

// 状态管理
let currentStateIdx = 0;
let nextStateIdx = 1;
let animProgress = 0; // 0 to 1
let lastTime = 0;

const scene = document.getElementById('scene');
const statusText = document.getElementById('status-text');
const statusDot = document.getElementById('status-dot');
const dataPanel = document.getElementById('data-panel');

// 初始化侧边栏数据条
for (let i = 0; i < 6; i++) {
    dataPanel.innerHTML += `
        <div class="spring-data">
            <div style="display:flex; justify-content:space-between; margin-bottom:2px;">
                <span>斜置弹簧 ${i+1}</span>
                <span id="s-val-${i}">0%</span>
            </div>
            <div class="spring-bar-container">
                <div class="spring-bar" id="s-bar-${i}"></div>
            </div>
        </div>
    `;
}

// 主渲染循环
function animate(time) {
    if(!lastTime) lastTime = time;
    const deltaTime = time - lastTime;
    lastTime = time;

    const currentSeq = ANIMATION_SEQUENCE[currentStateIdx];
    const nextSeq = ANIMATION_SEQUENCE[nextStateIdx];

    animProgress += deltaTime / currentSeq.duration;
    
    if (animProgress >= 1) {
        animProgress = 0;
        currentStateIdx = nextStateIdx;
        nextStateIdx = (nextStateIdx + 1) % ANIMATION_SEQUENCE.length;
        statusText.textContent = ANIMATION_SEQUENCE[currentStateIdx].label;
        
        // 状态指示灯闪烁
        statusDot.style.background = '#38bdf8';
        statusDot.style.boxShadow = '0 0 10px #38bdf8';
        setTimeout(() => {
            statusDot.style.background = '#22c55e';
            statusDot.style.boxShadow = '0 0 8px #22c55e';
        }, 300);
    }

    const easeT = easeInOutCubic(animProgress);

    // 当前六维位姿
    const pos = {
        x: currentSeq.state.x + (nextSeq.state.x - currentSeq.state.x) * easeT,
        y: currentSeq.state.y + (nextSeq.state.y - currentSeq.state.y) * easeT,
        z: currentSeq.state.z + (nextSeq.state.z - currentSeq.state.z) * easeT + TOP_Z_IDLE,
        rx: currentSeq.state.rx + (nextSeq.state.rx - currentSeq.state.rx) * easeT,
        ry: currentSeq.state.ry + (nextSeq.state.ry - currentSeq.state.ry) * easeT,
        rz: currentSeq.state.rz + (nextSeq.state.rz - currentSeq.state.rz) * easeT
    };

    renderScene(pos);

    requestAnimationFrame(animate);
}

function renderScene(topPos) {
    // 1. 计算基座节点 (固定)
    const baseNodesGlobal = BASE_ANGLES.map(ang => ({
        x: R * Math.cos(ang), y: R * Math.sin(ang), z: BASE_Z
    }));
    
    // 2. 计算顶部节点 (运动)
    const topNodesGlobal = TOP_ANGLES.map(ang => {
        const local = {x: R * Math.cos(ang), y: R * Math.sin(ang), z: 0};
        return transform3D(local, topPos);
    });

    // 收集所有要绘制的图元并计算深度以进行 Z-Sorting
    const drawables = [];

    // --- 底座法兰 ---
    const baseFlangeProj = baseFlangeLocal.map(p => project3D({x: p.x, y: p.y, z: BASE_Z}));
    const baseFlangeDepth = baseFlangeProj.reduce((sum, p) => sum + p.depth, 0) / baseFlangeProj.length;
    let dBase = `M ${baseFlangeProj[0].x} ${baseFlangeProj[0].y} `;
    for(let i=1; i<baseFlangeProj.length; i++) dBase += `L ${baseFlangeProj[i].x} ${baseFlangeProj[i].y} `;
    dBase += 'Z';
    drawables.push({
        type: 'polygon',
        depth: baseFlangeDepth,
        html: `<path d="${dBase}" fill="var(--flange-fill)" stroke="var(--flange-stroke)" stroke-width="2" stroke-linejoin="round" />`
    });

    // --- 顶部法兰 ---
    const topFlangeProj = topFlangeLocal.map(p => project3D(transform3D(p, topPos)));
    const topFlangeDepth = topFlangeProj.reduce((sum, p) => sum + p.depth, 0) / topFlangeProj.length;
    let dTop = `M ${topFlangeProj[0].x} ${topFlangeProj[0].y} `;
    for(let i=1; i<topFlangeProj.length; i++) dTop += `L ${topFlangeProj[i].x} ${topFlangeProj[i].y} `;
    dTop += 'Z';
    drawables.push({
        type: 'polygon',
        depth: topFlangeDepth,
        html: `<path d="${dTop}" fill="rgba(6, 182, 212, 0.4)" stroke="#22d3ee" stroke-width="3" stroke-linejoin="round" filter="url(#glow)"/>`
    });

    // --- 轴心引导线 (虚拟) ---
    const centerBase = project3D({x:0, y:0, z:BASE_Z});
    const centerTop = project3D(transform3D({x:0, y:0, z:0}, topPos));
    drawables.push({
        type: 'line',
        depth: (centerBase.depth + centerTop.depth)/2,
        html: `<line x1="${centerBase.x}" y1="${centerBase.y}" x2="${centerTop.x}" y2="${centerTop.y}" stroke="rgba(255,255,255,0.1)" stroke-dasharray="5,5" stroke-width="2" />`
    });

    // --- 6 根斜置弹簧 ---
    SPRING_CONNECTIONS.forEach((conn, idx) => {
        const pTopGlobal = topNodesGlobal[conn.topIdx];
        const pBaseGlobal = baseNodesGlobal[conn.baseIdx];
        
        // 计算实际物理长度与形变量
        const len = Math.hypot(pTopGlobal.x - pBaseGlobal.x, pTopGlobal.y - pBaseGlobal.y, pTopGlobal.z - pBaseGlobal.z);
        const strain = (len - L0) / L0; // 负为压缩,正为拉伸

        // 更新侧边栏 UI
        const bar = document.getElementById(`s-bar-${idx}`);
        const valText = document.getElementById(`s-val-${idx}`);
        
        // 映射颜色: strain < 0 趋向橙色, strain > 0 趋向青色
        let springColor;
        if (strain < 0) {
            springColor = interpolateColor('#94a3b8', '#f97316', Math.min(-strain * 5, 1));
            bar.style.backgroundColor = 'var(--spring-comp)';
        } else {
            springColor = interpolateColor('#94a3b8', '#0ea5e9', Math.min(strain * 5, 1));
            bar.style.backgroundColor = 'var(--spring-tens)';
        }
        
        // 更新长度条位置: 左侧压缩,右侧拉伸
        const percent = Math.max(-50, Math.min(50, strain * 150));
        bar.style.transform = `translateX(${percent}%) scaleX(${Math.abs(percent)/50 || 0.05})`;
        valText.textContent = `${strain > 0 ? '+' : ''}${(strain * 100).toFixed(1)}%`;
        valText.style.color = springColor;

        // 投影渲染
        const pTop = project3D(pTopGlobal);
        const pBase = project3D(pBaseGlobal);
        const depth = (pTop.depth + pBase.depth) / 2;

        const pathD = createSpringPath(pTop, pBase);
        
        // 内置导向杆
        const guideHtml = `<line x1="${pTop.x}" y1="${pTop.y}" x2="${pBase.x}" y2="${pBase.y}" stroke="rgba(255,255,255,0.15)" stroke-width="4" stroke-linecap="round"/>`;
        
        // 弹簧本体
        const springHtml = `<path d="${pathD}" fill="none" stroke="${springColor}" stroke-width="6" stroke-linejoin="round" filter="url(#glow)"/>`;

        drawables.push({
            type: 'spring',
            depth: depth,
            html: guideHtml + springHtml
        });
    });

    // 排序渲染 (Painter's Algorithm)
    drawables.sort((a, b) => a.depth - b.depth);

    // 生成外力矢量指示(仅在偏离零位时显示)
    let forceArrow = '';
    const isDeformed = Math.abs(topPos.x)>1 || Math.abs(topPos.y)>1 || Math.abs(topPos.z - TOP_Z_IDLE)>1 || Math.abs(topPos.rx)>0.01 || Math.abs(topPos.ry)>0.01 || Math.abs(topPos.rz)>0.01;
    
    if (isDeformed) {
        // 简化表达,直接在顶部中心添加指示标识
        forceArrow = `<circle cx="${centerTop.x}" cy="${centerTop.y}" r="15" fill="none" stroke="#fff" stroke-width="2" stroke-dasharray="4,4">
            <animateTransform attributeName="transform" type="rotate" from="0 ${centerTop.x} ${centerTop.y}" to="360 ${centerTop.x} ${centerTop.y}" dur="2s" repeatCount="indefinite"/>
        </circle>
        <circle cx="${centerTop.x}" cy="${centerTop.y}" r="4" fill="#fff" filter="url(#glow-strong)"/>`;
    }

    // 批量更新 DOM
    scene.innerHTML = drawables.map(d => d.html).join('') + forceArrow;
}

// 启动
window.addEventListener('DOMContentLoaded', () => {
    statusText.textContent = ANIMATION_SEQUENCE[0].label;
    requestAnimationFrame(animate);
});
</script>
</body>
</html>
积分规则:每轮对话扣10分