分享图
A
动画渲染工坊
就绪
<!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>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;400;600;800&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
:root {
  --bg: #060a12;
  --bg-card: #0c1422;
  --border: #162038;
  --text: #c8d6e5;
  --text-muted: #4a5e78;
  --accent: #f0a500;
  --accent-dim: rgba(240,165,0,0.15);
  --cyan: #00d4ff;
  --red: #ff4757;
  --green: #2ed573;
  --purple: #b06aff;
  --steel: #6b7fa3;
  --steel-dark: #2e3d55;
  --steel-mid: #4a5e78;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
  font-family:'Outfit',sans-serif;
  background:var(--bg);
  color:var(--text);
  min-height:100vh;
  overflow-x:hidden;
  display:flex;flex-direction:column;
}
/* 背景纹理 */
body::before{
  content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(ellipse 80% 50% at 20% 30%, rgba(240,165,0,0.04) 0%, transparent 60%),
    radial-gradient(ellipse 60% 40% at 80% 70%, rgba(0,212,255,0.03) 0%, transparent 60%);
}
body::after{
  content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
  background-image:
    linear-gradient(rgba(22,32,56,0.25) 1px, transparent 1px),
    linear-gradient(90deg, rgba(22,32,56,0.25) 1px, transparent 1px);
  background-size:40px 40px;
}
.page{position:relative;z-index:1;display:flex;flex-direction:column;min-height:100vh}

/* 顶栏 */
header{
  padding:24px 32px 12px;
  display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;
  border-bottom:1px solid var(--border);
}
header h1{
  font-size:clamp(20px,3vw,28px);font-weight:800;letter-spacing:-0.5px;
  background:linear-gradient(135deg,var(--accent),#ffcc44);
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;
}
header .tag{
  font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:400;
  color:var(--accent);border:1px solid var(--accent-dim);
  padding:2px 10px;border-radius:20px;letter-spacing:0.5px;
}

/* 主区域 */
main{
  flex:1;display:flex;flex-direction:column;align-items:center;
  padding:16px 20px 0;gap:12px;
}
.svg-wrap{
  width:100%;max-width:1200px;flex:1;min-height:0;
  border:1px solid var(--border);border-radius:12px;
  background:var(--bg-card);overflow:hidden;position:relative;
}
.svg-wrap svg{width:100%;height:100%;display:block}

/* 控制面板 */
.controls{
  padding:14px 24px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;
  border-top:1px solid var(--border);background:var(--bg-card);
  max-width:1200px;width:100%;border-radius:0 0 12px 12px;
}
.ctrl-group{display:flex;align-items:center;gap:8px}
.ctrl-group label{
  font-family:'JetBrains Mono',monospace;font-size:11px;
  color:var(--text-muted);white-space:nowrap;
}
.ctrl-group .val{
  font-family:'JetBrains Mono',monospace;font-size:12px;
  color:var(--accent);min-width:28px;text-align:right;
}
input[type=range]{
  -webkit-appearance:none;width:120px;height:4px;border-radius:2px;
  background:var(--steel-dark);outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:14px;height:14px;border-radius:50%;
  background:var(--accent);border:2px solid var(--bg);cursor:pointer;
  box-shadow:0 0 8px rgba(240,165,0,0.4);
}
.btn{
  font-family:'Outfit',sans-serif;font-size:12px;font-weight:600;
  padding:6px 14px;border-radius:6px;border:1px solid var(--border);
  background:var(--steel-dark);color:var(--text);cursor:pointer;
  transition:all 0.2s;display:flex;align-items:center;gap:6px;
}
.btn:hover{background:var(--steel-mid);border-color:var(--steel)}
.btn.active{background:var(--accent);color:#000;border-color:var(--accent)}
.btn i{font-size:11px}

/* 状态指示器 */
.status-bar{
  display:flex;align-items:center;gap:16px;
  padding:10px 24px;max-width:1200px;width:100%;
}
.status-item{
  display:flex;align-items:center;gap:6px;
  font-family:'JetBrains Mono',monospace;font-size:11px;
}
.status-dot{
  width:8px;height:8px;border-radius:50%;
  animation:pulse 1.5s ease-in-out infinite;
}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}

/* 浮动信息卡 */
.info-cards{
  position:fixed;top:80px;right:20px;z-index:10;
  display:flex;flex-direction:column;gap:8px;
  pointer-events:none;
}
.info-card{
  background:rgba(12,20,34,0.92);border:1px solid var(--border);
  border-radius:8px;padding:10px 14px;backdrop-filter:blur(8px);
  pointer-events:auto;max-width:220px;
}
.info-card .ic-title{
  font-size:10px;font-weight:600;text-transform:uppercase;
  letter-spacing:1px;color:var(--text-muted);margin-bottom:4px;
}
.info-card .ic-value{
  font-family:'JetBrains Mono',monospace;font-size:18px;font-weight:500;
}
.info-card .ic-desc{font-size:11px;color:var(--text-muted);margin-top:2px}

/* 响应式 */
@media(max-width:768px){
  header{padding:16px}
  .controls{padding:10px 16px;gap:12px}
  .info-cards{display:none}
  input[type=range]{width:80px}
}
</style>
</head>
<body>
<div class="page">
  <header>
    <h1>30° 楔形自锁螺纹</h1>
    <span class="tag">IFR · 振动能量 → 锁紧动力</span>
  </header>

  <main>
    <div class="svg-wrap">
      <svg id="mainSvg" viewBox="0 0 1200 650" preserveAspectRatio="xMidYMid meet"></svg>
    </div>

    <div class="status-bar">
      <div class="status-item">
        <div class="status-dot" id="lockDot" style="background:var(--green)"></div>
        <span id="lockLabel" style="color:var(--green)">自锁稳定</span>
      </div>
      <div class="status-item">
        <span style="color:var(--text-muted)">摩擦增幅</span>
        <span id="frictionRatio" style="color:var(--accent);font-weight:600">×2.8</span>
      </div>
      <div class="status-item">
        <span style="color:var(--text-muted)">能量转化</span>
        <span id="energyConv" style="color:var(--cyan);font-weight:600">0%</span>
      </div>
    </div>

    <div class="controls">
      <div class="ctrl-group">
        <label>振幅</label>
        <input type="range" id="ampSlider" min="0" max="100" value="50">
        <span class="val" id="ampVal">50</span>
      </div>
      <div class="ctrl-group">
        <label>频率</label>
        <input type="range" id="freqSlider" min="10" max="100" value="40">
        <span class="val" id="freqVal">40</span>
      </div>
      <button class="btn active" id="forceBtn"><i class="fas fa-arrows-alt"></i>力矢量</button>
      <button class="btn" id="playBtn"><i class="fas fa-pause"></i>暂停</button>
      <button class="btn" id="resetBtn"><i class="fas fa-redo"></i>重置</button>
    </div>
  </main>

  <div class="info-cards">
    <div class="info-card">
      <div class="ic-title">楔形斜面角</div>
      <div class="ic-value" style="color:var(--accent)">30°</div>
      <div class="ic-desc">法向力全转化为正压力</div>
    </div>
    <div class="info-card">
      <div class="ic-title">当量摩擦系数</div>
      <div class="ic-value" style="color:var(--cyan)" id="eqFriction">μ' = 2.8μ</div>
      <div class="ic-desc">自锁条件充分满足</div>
    </div>
    <div class="info-card">
      <div class="ic-title">啮合长度</div>
      <div class="ic-value" style="color:var(--green)">≥1.5d</div>
      <div class="ic-desc">确保完整力封闭回路</div>
    </div>
  </div>
</div>

<script>
// ===== 全局状态 =====
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('mainSvg');

let state = {
  playing: true,
  showForces: true,
  amplitude: 50,
  frequency: 40,
  time: 0,
  vibOffset: 0,
  lockStrength: 1,
  frictionMultiplier: 2.8
};

// ===== SVG 辅助 =====
function el(tag, attrs, parent) {
  const e = document.createElementNS(SVG_NS, tag);
  for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
  (parent || svg).appendChild(e);
  return e;
}

function clearSvg() { svg.innerHTML = ''; }

// ===== 定义渐变和滤镜 =====
function defineDefs() {
  const defs = el('defs');

  // 螺栓金属渐变
  const bg1 = el('linearGradient', {id:'boltGrad', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
  el('stop', {offset:'0%', 'stop-color':'#8fa3c4'}, bg1);
  el('stop', {offset:'50%', 'stop-color':'#6b7fa3'}, bg1);
  el('stop', {offset:'100%', 'stop-color':'#4a5e78'}, bg1);

  // 螺母金属渐变
  const bg2 = el('linearGradient', {id:'nutGrad', x1:'0', y1:'0', x2:'0', y2:'1'}, defs);
  el('stop', {offset:'0%', 'stop-color':'#4a5e78'}, bg2);
  el('stop', {offset:'50%', 'stop-color':'#3a4a63'}, bg2);
  el('stop', {offset:'100%', 'stop-color':'#2e3d55'}, bg2);

  // 楔形面发光渐变
  const bg3 = el('linearGradient', {id:'wedgeGrad', x1:'0', y1:'0', x2:'1', y2:'0'}, defs);
  el('stop', {offset:'0%', 'stop-color':'#f0a500'}, bg3);
  el('stop', {offset:'100%', 'stop-color':'#ffcc44'}, bg3);

  // 发光滤镜
  const f1 = el('filter', {id:'glowAmber', x:'-50%', y:'-50%', width:'200%', height:'200%'}, defs);
  el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:'4', result:'blur'}, f1);
  const fm1 = el('feMerge', {}, f1);
  el('feMergeNode', {in:'blur'}, fm1);
  el('feMergeNode', {in:'SourceGraphic'}, fm1);

  const f2 = el('filter', {id:'glowCyan', x:'-50%', y:'-50%', width:'200%', height:'200%'}, defs);
  el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:'3', result:'blur'}, f2);
  const fm2 = el('feMerge', {}, f2);
  el('feMergeNode', {in:'blur'}, fm2);
  el('feMergeNode', {in:'SourceGraphic'}, fm2);

  const f3 = el('filter', {id:'glowRed', x:'-50%', y:'-50%', width:'200%', height:'200%'}, defs);
  el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:'3', result:'blur'}, f3);
  const fm3 = el('feMerge', {}, f3);
  el('feMergeNode', {in:'blur'}, fm3);
  el('feMergeNode', {in:'SourceGraphic'}, fm3);

  // 振动能量粒子发光
  const f4 = el('filter', {id:'glowGreen', x:'-50%', y:'-50%', width:'200%', height:'200%'}, defs);
  el('feGaussianBlur', {in:'SourceGraphic', stdDeviation:'5', result:'blur'}, f4);
  const fm4 = el('feMerge', {}, f4);
  el('feMergeNode', {in:'blur'}, fm4);
  el('feMergeNode', {in:'SourceGraphic'}, fm4);

  // 箭头标记
  const mk1 = el('marker', {id:'arrowCyan', markerWidth:'8', markerHeight:'6', refX:'8', refY:'3', orient:'auto'}, defs);
  el('polygon', {points:'0 0, 8 3, 0 6', fill:'#00d4ff'}, mk1);

  const mk2 = el('marker', {id:'arrowRed', markerWidth:'8', markerHeight:'6', refX:'8', refY:'3', orient:'auto'}, defs);
  el('polygon', {points:'0 0, 8 3, 0 6', fill:'#ff4757'}, mk2);

  const mk3 = el('marker', {id:'arrowAccent', markerWidth:'8', markerHeight:'6', refX:'8', refY:'3', orient:'auto'}, defs);
  el('polygon', {points:'0 0, 8 3, 0 6', fill:'#f0a500'}, mk3);

  const mk4 = el('marker', {id:'arrowPurple', markerWidth:'8', markerHeight:'6', refX:'8', refY:'3', orient:'auto'}, defs);
  el('polygon', {points:'0 0, 8 3, 0 6', fill:'#b06aff'}, mk4);
}

// ===== 绘制螺纹截面 =====
// 螺纹参数
const PX = 80;       // 起始X
const PY_BOLT = 290;  // 螺栓体顶面Y
const PY_NUT = 140;   // 螺母体底面Y
const PITCH = 48;     // 螺距
const NUM_TEETH = 9;  // 齿数
const TOOTH_H = 32;   // 齿高

// 标准螺纹面角(从法线方向量):30°
// 楔形面角(从法线方向量):约55°,视觉上夸张以示区别
const STD_ANGLE = 30;
const WEDGE_ANGLE = 55;

// 生成螺栓外螺纹路径(对称60°)
function boltThreadPath(yBase, dir) {
  // dir: -1=向上, 1=向下
  let d = '';
  const tanA = Math.tan(STD_ANGLE * Math.PI / 180);
  for (let i = 0; i < NUM_TEETH; i++) {
    const x0 = PX + i * PITCH;
    const xCrest = x0 + TOOTH_H * tanA;
    const xMid = x0 + PITCH / 2;
    const yCrest = yBase + dir * TOOTH_H;
    const xCrest2 = xMid + TOOTH_H * tanA;
    d += `M${x0},${yBase} L${xCrest},${yCrest} L${xMid},${yBase} `;
  }
  return d;
}

// 生成螺母内螺纹路径(非对称,左侧为楔形面)
function nutThreadPath(yBase, dir) {
  let d = '';
  const tanStd = Math.tan(STD_ANGLE * Math.PI / 180);
  const tanWedge = Math.tan(WEDGE_ANGLE * Math.PI / 180);
  for (let i = 0; i < NUM_TEETH; i++) {
    const x0 = PX + i * PITCH;
    const xCrest = x0 + TOOTH_H * tanWedge;  // 楔形面水平跨度更大
    const xMid = x0 + PITCH / 2;
    const yCrest = yBase + dir * TOOTH_H;
    // 从根部到齿顶(楔形面 - 陡面)
    d += `M${x0},${yBase} L${xCrest},${yCrest} `;
    // 从齿顶回根部(标准面)
    d += `L${xMid},${yBase} `;
  }
  return d;
}

// 仅楔形面路径(高亮用)
function wedgeFacesPath(yBase, dir) {
  let d = '';
  const tanWedge = Math.tan(WEDGE_ANGLE * Math.PI / 180);
  for (let i = 0; i < NUM_TEETH; i++) {
    const x0 = PX + i * PITCH;
    const xCrest = x0 + TOOTH_H * tanWedge;
    const yCrest = yBase + dir * TOOTH_H;
    d += `M${x0},${yBase} L${xCrest},${yCrest} `;
  }
  return d;
}

// 绘制截面图
let dynamicGroup, forceGroup, particleGroup, vibIndicatorGroup;

function drawCrossSection() {
  const g = el('g', {id: 'crossSection'});

  // === 螺母体(上方) ===
  el('rect', {
    x: PX - 20, y: PY_NUT - 60, width: NUM_TEETH * PITCH + 40, height: 60,
    fill: 'url(#nutGrad)', stroke: '#4a5e78', 'stroke-width': 1.5, rx: 3
  }, g);

  // 螺母标签
  const nutLabel = el('text', {
    x: PX + NUM_TEETH * PITCH / 2, y: PY_NUT - 25,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '13',
    fill: '#8fa3c4', 'text-anchor': 'middle', 'font-weight': 600
  }, g);
  nutLabel.textContent = '非对称内螺纹螺母';

  // 螺母内螺纹(向下延伸)
  el('path', {
    d: nutThreadPath(PY_NUT, 1),
    fill: 'none', stroke: '#4a5e78', 'stroke-width': 2.5,
    'stroke-linecap': 'round'
  }, g);

  // 楔形面高亮
  el('path', {
    d: wedgeFacesPath(PY_NUT, 1),
    fill: 'none', stroke: 'url(#wedgeGrad)', 'stroke-width': 3.5,
    'stroke-linecap': 'round', filter: 'url(#glowAmber)',
    class: 'wedge-surface'
  }, g);

  // === 螺栓体(下方) ===
  const boltRect = el('rect', {
    x: PX - 20, y: PY_BOLT, width: NUM_TEETH * PITCH + 40, height: 70,
    fill: 'url(#boltGrad)', stroke: '#6b7fa3', 'stroke-width': 1.5, rx: 3,
    class: 'bolt-body'
  }, g);

  // 螺栓标签
  const boltLabel = el('text', {
    x: PX + NUM_TEETH * PITCH / 2, y: PY_BOLT + 42,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '13',
    fill: '#8fa3c4', 'text-anchor': 'middle', 'font-weight': 600
  }, g);
  boltLabel.textContent = '标准公螺纹螺栓';

  // 螺栓外螺纹(向上延伸)
  el('path', {
    d: boltThreadPath(PY_BOLT, -1),
    fill: 'none', stroke: '#6b7fa3', 'stroke-width': 2.5,
    'stroke-linecap': 'round'
  }, g);

  // === 角度标注 ===
  drawAngleAnnotation(g);

  return g;
}

// 角度标注
function drawAngleAnnotation(parent) {
  // 选取中间一个齿来标注
  const idx = 3;
  const x0 = PX + idx * PITCH;
  const yBase = PY_NUT;
  const tanWedge = Math.tan(WEDGE_ANGLE * Math.PI / 180);
  const xCrest = x0 + TOOTH_H * tanWedge;
  const yCrest = yBase + TOOTH_H;

  // 楔形面角度弧线
  const arcR = 22;
  // 法线方向(垂直向下 = 90°),楔形面从法线偏转55°
  // SVG角度:法线向下 = 90°,楔形面方向 = 90° - 55° = 35°... 
  // 让我画一个小弧线表示角度
  const perpAngle = Math.PI / 2; // 法线朝下
  const faceAngle = Math.PI / 2 - WEDGE_ANGLE * Math.PI / 180; // 楔形面方向

  const ax1 = x0 + arcR * Math.cos(perpAngle);
  const ay1 = yBase + arcR * Math.sin(perpAngle);
  const ax2 = x0 + arcR * Math.cos(faceAngle);
  const ay2 = yBase + arcR * Math.sin(faceAngle);

  // 角度弧
  const largeArc = WEDGE_ANGLE > 180 ? 1 : 0;
  el('path', {
    d: `M${ax1},${ay1} A${arcR},${arcR} 0 ${largeArc} 0 ${ax2},${ay2}`,
    fill: 'none', stroke: '#f0a500', 'stroke-width': 1.5, 'stroke-dasharray': '3,2'
  }, parent);

  // 角度文字
  const midAngle = (perpAngle + faceAngle) / 2;
  const tx = x0 + (arcR + 14) * Math.cos(midAngle);
  const ty = yBase + (arcR + 14) * Math.sin(midAngle);
  const angleText = el('text', {
    x: tx, y: ty,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '11',
    fill: '#f0a500', 'text-anchor': 'middle', 'font-weight': 500
  }, parent);
  angleText.textContent = '55°';

  // 法线虚线
  el('line', {
    x1: x0, y1: yBase, x2: x0, y2: yBase + arcR + 6,
    stroke: '#f0a500', 'stroke-width': 0.8, 'stroke-dasharray': '3,3', opacity: 0.5
  }, parent);

  // 标注:"楔形面"
  const wLabel = el('text', {
    x: x0 + TOOTH_H * tanWedge / 2 - 8, y: yCrest + 16,
    'font-family': "'Outfit', sans-serif", 'font-size': '11',
    fill: '#f0a500', 'text-anchor': 'middle', 'font-weight': 600
  }, parent);
  wLabel.textContent = '楔形面';

  // 标准面角度标注(另一侧)
  const xMid = x0 + PITCH / 2;
  const tanStd = Math.tan(STD_ANGLE * Math.PI / 180);
  const xStdCrest = xMid - TOOTH_H * tanStd;

  const arcR2 = 18;
  const perpAngle2 = Math.PI / 2;
  const faceAngle2 = Math.PI / 2 + STD_ANGLE * Math.PI / 180;

  const bx1 = xMid + arcR2 * Math.cos(perpAngle2);
  const by1 = yBase + arcR2 * Math.sin(perpAngle2);
  const bx2 = xMid + arcR2 * Math.cos(faceAngle2);
  const by2 = yBase + arcR2 * Math.sin(faceAngle2);

  el('path', {
    d: `M${bx1},${by1} A${arcR2},${arcR2} 0 0 1 ${bx2},${by2}`,
    fill: 'none', stroke: '#6b7fa3', 'stroke-width': 1, 'stroke-dasharray': '3,2', opacity: 0.6
  }, parent);

  const midAngle2 = (perpAngle2 + faceAngle2) / 2;
  const tx2 = xMid + (arcR2 + 14) * Math.cos(midAngle2);
  const ty2 = yBase + (arcR2 + 14) * Math.sin(midAngle2);
  const angleText2 = el('text', {
    x: tx2, y: ty2,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '10',
    fill: '#6b7fa3', 'text-anchor': 'middle', opacity: 0.7
  }, parent);
  angleText2.textContent = '30°';
}

// ===== 绘制力分析斜面模型 =====
let inclineGroup;
function drawInclineModel() {
  const g = el('g', {id: 'inclineModel', transform: 'translate(760, 60)'});

  // 背景框
  el('rect', {
    x: 0, y: 0, width: 400, height: 280, rx: 10,
    fill: 'rgba(12,20,34,0.6)', stroke: 'var(--border)', 'stroke-width': 1
  }, g);

  // 标题
  const title = el('text', {
    x: 200, y: 28,
    'font-family': "'Outfit', sans-serif", 'font-size': '14',
    fill: '#c8d6e5', 'text-anchor': 'middle', 'font-weight': 600
  }, g);
  title.textContent = '楔形面力学模型 · 自锁分析';

  // 斜面(从左下到右上)
  const slopeLen = 220;
  const slopeAngle = 30; // 楔形面与水平方向的夹角(30° 从轴线方向看)
  const rad = slopeAngle * Math.PI / 180;
  const sx = 60, sy = 240;
  const ex = sx + slopeLen * Math.cos(rad);
  const ey = sy - slopeLen * Math.sin(rad);

  // 斜面底色
  el('polygon', {
    points: `${sx},${sy} ${ex},${ey} ${ex},${sy}`,
    fill: 'rgba(240,165,0,0.08)', stroke: 'none'
  }, g);

  // 斜面线
  el('line', {
    x1: sx, y1: sy, x2: ex, y2: ey,
    stroke: '#f0a500', 'stroke-width': 3, filter: 'url(#glowAmber)'
  }, g);

  // 水平基线
  el('line', {
    x1: sx, y1: sy, x2: ex + 20, y2: sy,
    stroke: '#4a5e78', 'stroke-width': 1, 'stroke-dasharray': '4,3'
  }, g);

  // 角度弧
  const arcR3 = 40;
  const aStart = 0;
  const aEnd = -slopeAngle;
  const ax1 = sx + arcR3;
  const ay1 = sy;
  const ax2 = sx + arcR3 * Math.cos(rad);
  const ay2 = sy - arcR3 * Math.sin(rad);
  el('path', {
    d: `M${ax1},${ay1} A${arcR3},${arcR3} 0 0 0 ${ax2},${ay2}`,
    fill: 'none', stroke: '#f0a500', 'stroke-width': 1.5
  }, g);
  const aText = el('text', {
    x: sx + arcR3 + 14, y: sy - 10,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '12',
    fill: '#f0a500', 'font-weight': 600
  }, g);
  aText.textContent = '30°';

  // 方块(代表螺栓螺纹)——可动画
  const blockW = 50, blockH = 36;
  const blockCx = (sx + ex) / 2;
  const blockCy = (sy + ey) / 2;
  // 方块中心在斜面上
  const bCx = sx + 110 * Math.cos(rad);
  const bCy = sy - 110 * Math.sin(rad);

  // 方块组(可沿斜面微振)
  const blockG = el('g', {id: 'blockGroup'}, g);

  // 方块需旋转以贴合斜面
  const rotAngle = -slopeAngle;
  const transformStr = `translate(${bCx},${bCy}) rotate(${rotAngle}) translate(${-blockW/2},${-blockH})`;
  blockG.setAttribute('transform', transformStr);

  // 方块本体
  el('rect', {
    x: 0, y: 0, width: blockW, height: blockH, rx: 4,
    fill: 'url(#boltGrad)', stroke: '#8fa3c4', 'stroke-width': 1.5
  }, blockG);

  const bLabel = el('text', {
    x: blockW / 2, y: blockH / 2 + 4,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '9',
    fill: '#c8d6e5', 'text-anchor': 'middle', 'font-weight': 500
  }, blockG);
  bLabel.textContent = '螺栓螺纹';

  // 力矢量组(在方块上绘制,会随之旋转)
  const forceG = el('g', {id: 'forceVectors'}, blockG);

  // 法向力 N(垂直于斜面向外 = 远离斜面方向)
  // 在旋转后的坐标系中,法向力向上(-y)
  const nLen = 50;
  el('line', {
    x1: blockW / 2, y1: 0, x2: blockW / 2, y2: -nLen,
    stroke: '#00d4ff', 'stroke-width': 2.5, filter: 'url(#glowCyan)',
    'marker-end': 'url(#arrowCyan)', class: 'force-N'
  }, forceG);
  const nLabel = el('text', {
    x: blockW / 2 + 14, y: -nLen + 8,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '11',
    fill: '#00d4ff', 'font-weight': 600
  }, forceG);
  nLabel.textContent = 'N';

  // 摩擦力 f(沿斜面向上 = 阻止下滑)
  // 在旋转坐标系中,向左上 = -x 方向
  const fLen = 45;
  el('line', {
    x1: 0, y1: blockH / 2, x2: -fLen, y2: blockH / 2,
    stroke: '#ff4757', 'stroke-width': 2.5, filter: 'url(#glowRed)',
    'marker-end': 'url(#arrowRed)', class: 'force-f'
  }, forceG);
  const fLabel = el('text', {
    x: -fLen - 4, y: blockH / 2 - 6,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '11',
    fill: '#ff4757', 'font-weight': 600, 'text-anchor': 'end'
  }, forceG);
  fLabel.textContent = 'f=μ\'N';

  // 预紧力/重力分量(沿斜面向下 = 促使松动)
  const gLen = 28;
  el('line', {
    x1: blockW, y1: blockH / 2, x2: blockW + gLen, y2: blockH / 2,
    stroke: '#b06aff', 'stroke-width': 2, filter: 'url(#glowRed)',
    'marker-end': 'url(#arrowPurple)', class: 'force-Fp'
  }, forceG);
  const fpLabel = el('text', {
    x: blockW + gLen + 4, y: blockH / 2 - 6,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '11',
    fill: '#b06aff', 'font-weight': 600
  }, forceG);
  fpLabel.textContent = 'Fp';

  // 自锁条件文字
  const lockCond = el('text', {
    x: 200, y: 268,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '11',
    fill: '#2ed573', 'text-anchor': 'middle', 'font-weight': 500
  }, g);
  lockCond.textContent = 'f ≫ Fp → 自锁成立';

  return g;
}

// ===== 振动模拟区域 =====
let vibGroup;
function drawVibSection() {
  const g = el('g', {id: 'vibSection', transform: 'translate(760, 360)'});

  // 背景
  el('rect', {
    x: 0, y: 0, width: 400, height: 250, rx: 10,
    fill: 'rgba(12,20,34,0.6)', stroke: 'var(--border)', 'stroke-width': 1
  }, g);

  // 标题
  const title = el('text', {
    x: 200, y: 24,
    'font-family': "'Outfit', sans-serif", 'font-size': '14',
    fill: '#c8d6e5', 'text-anchor': 'middle', 'font-weight': 600
  }, g);
  title.textContent = '横向振动响应 · 能量转化';

  // 简化的螺栓-螺母侧视图
  const cG = el('g', {id: 'vibAssembly', transform: 'translate(200, 120)'}, g);

  // 螺母(固定,画为矩形)
  el('rect', {
    x: -80, y: -55, width: 160, height: 30, rx: 4,
    fill: 'url(#nutGrad)', stroke: '#4a5e78', 'stroke-width': 1.5
  }, cG);
  const nutL = el('text', {
    x: 0, y: -37,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '10',
    fill: '#8fa3c4', 'text-anchor': 'middle'
  }, cG);
  nutL.textContent = '螺母 (固定)';

  // 螺栓(可横向振动)
  const boltG = el('g', {id: 'vibBolt'}, cG);
  el('rect', {
    x: -80, y: 25, width: 160, height: 28, rx: 4,
    fill: 'url(#boltGrad)', stroke: '#6b7fa3', 'stroke-width': 1.5
  }, boltG);
  // 螺栓头
  el('rect', {
    x: -95, y: 22, width: 18, height: 34, rx: 3,
    fill: '#6b7fa3', stroke: '#8fa3c4', 'stroke-width': 1
  }, boltG);
  const boltL = el('text', {
    x: 0, y: 43,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '10',
    fill: '#c8d6e5', 'text-anchor': 'middle'
  }, boltG);
  boltL.textContent = '螺栓 (振动)';

  // 楔形面接触指示(两侧小三角形)
  const wedgeLeft = el('polygon', {
    points: '-50,-25 -42,-25 -42,-18',
    fill: '#f0a500', opacity: 0.9, filter: 'url(#glowAmber)',
    class: 'wedge-contact'
  }, cG);
  const wedgeRight = el('polygon', {
    points: '50,-25 42,-25 42,-18',
    fill: '#f0a500', opacity: 0.9, filter: 'url(#glowAmber)',
    class: 'wedge-contact'
  }, cG);

  // 接触面标签
  const cLabel = el('text', {
    x: 0, y: -8,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '9',
    fill: '#f0a500', 'text-anchor': 'middle', 'font-weight': 500
  }, cG);
  cLabel.textContent = '楔形面接触 · 摩擦锁死';

  // 振动方向箭头
  const vibArrowG = el('g', {id: 'vibArrows', transform: 'translate(0, 39)'}, cG);
  // 左箭头
  el('line', {
    x1: -110, y1: 0, x2: -95, y2: 0,
    stroke: '#b06aff', 'stroke-width': 1.5, 'marker-end': 'url(#arrowPurple)'
  }, vibArrowG);
  // 右箭头
  el('line', {
    x1: 110, y1: 0, x2: 95, y2: 0,
    stroke: '#b06aff', 'stroke-width': 1.5, 'marker-end': 'url(#arrowPurple)'
  }, vibArrowG);
  const vaLabel = el('text', {
    x: 0, y: 14,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '9',
    fill: '#b06aff', 'text-anchor': 'middle'
  }, vibArrowG);
  vaLabel.textContent = '横向振动';

  // 锁紧状态指示灯
  const lockIndicator = el('circle', {
    cx: 0, cy: 65, r: 8,
    fill: '#2ed573', filter: 'url(#glowGreen)',
    id: 'lockLight'
  }, cG);
  const lockText = el('text', {
    x: 0, y: 82,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '10',
    fill: '#2ed573', 'text-anchor': 'middle', 'font-weight': 600,
    id: 'lockText'
  }, cG);
  lockText.textContent = 'LOCK';

  // 能量转化箭头(振动能量 → 摩擦锁紧力)
  const energyG = el('g', {transform: 'translate(0, 220)'}, g);
  const eLabel = el('text', {
    x: 200, y: 0,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '12',
    fill: '#c8d6e5', 'text-anchor': 'middle', 'font-weight': 500
  }, energyG);
  eLabel.textContent = '振动能量 ──→ 摩擦锁紧力';
  const eSubLabel = el('text', {
    x: 200, y: 16,
    'font-family': "'Outfit', sans-serif", 'font-size': '10',
    fill: '#4a5e78', 'text-anchor': 'middle'
  }, energyG);
  eSubLabel.textContent = '楔形面将法向力全部转化为正压力,当量摩擦系数成倍增大';

  return g;
}

// ===== 粒子系统(能量转化可视化) =====
let particles = [];
function initParticles() {
  particles = [];
  for (let i = 0; i < 20; i++) {
    particles.push({
      x: 0, y: 0,
      vx: 0, vy: 0,
      life: 0, maxLife: 60 + Math.random() * 40,
      size: 2 + Math.random() * 3,
      active: false
    });
  }
}

function updateParticles(vibIntensity) {
  // 激活粒子
  const activationChance = vibIntensity / 100;
  particles.forEach(p => {
    if (!p.active && Math.random() < activationChance * 0.1) {
      p.active = true;
      p.life = 0;
      // 从接触点出发
      p.x = 760 + 200 + (Math.random() - 0.5) * 60;
      p.y = 360 + 120 + (Math.random() - 0.5) * 20;
      p.vx = (Math.random() - 0.5) * 1.5;
      p.vy = -1 - Math.random() * 2;
    }
    if (p.active) {
      p.life++;
      p.x += p.vx;
      p.y += p.vy;
      p.vy -= 0.02; // 轻微上升加速
      if (p.life > p.maxLife) p.active = false;
    }
  });
}

// ===== 主动画循环 =====
let lastTime = 0;

function animate(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  if (state.playing) {
    state.time += dt;
  }

  const vibAmp = state.amplitude / 100;
  const vibFreq = state.frequency / 10;
  const t = state.time;

  // 计算振动偏移
  state.vibOffset = vibAmp * 12 * Math.sin(t * vibFreq);
  const absVib = Math.abs(state.vibOffset);

  // 更新锁紧强度(振动越大,锁紧越强——这就是IFR的精髓)
  state.lockStrength = 1 + absVib / 12 * 1.8;
  state.frictionMultiplier = 1 + (state.lockStrength - 1) * 2.8;

  // 更新螺母截面图中的动态效果
  updateDynamicEffects(absVib, vibAmp);

  // 更新斜面模型中方块的微振
  updateInclineBlock(vibAmp, t, vibFreq);

  // 更新振动模拟区域
  updateVibSimulation(vibAmp, t, vibFreq);

  // 更新粒子
  updateParticles(state.amplitude);
  renderParticles();

  // 更新UI状态
  updateStatusUI();

  requestAnimationFrame(animate);
}

function updateDynamicEffects(absVib, vibAmp) {
  // 楔形面发光强度随振动增强
  const wedgePaths = svg.querySelectorAll('.wedge-surface');
  const glowOpacity = 0.5 + Math.min(absVib / 10, 1) * 0.5;
  wedgePaths.forEach(p => {
    p.style.opacity = glowOpacity + 0.5;
    p.style.strokeWidth = (3 + absVib / 5) + 'px';
  });

  // 螺栓体横向振动
  const boltBody = svg.querySelector('.bolt-body');
  if (boltBody) {
    boltBody.setAttribute('transform', `translate(${state.vibOffset * 0.3}, 0)`);
  }
}

function updateInclineBlock(vibAmp, t, vibFreq) {
  const blockG = document.getElementById('blockGroup');
  if (!blockG) return;

  const slopeAngle = 30;
  const rad = slopeAngle * Math.PI / 180;

  // 方块沿斜面微振(振动试图推动它,但摩擦力阻止)
  const vibPush = vibAmp * 4 * Math.sin(t * vibFreq);
  // 锁定效应:实际位移远小于推动力(约1/5)
  const actualSlide = vibPush * 0.15;

  const bCx = 60 + 110 * Math.cos(rad);
  const bCy = 240 - 110 * Math.sin(rad);

  // 沿斜面方向的位移
  const slideX = actualSlide * Math.cos(rad);
  const slideY = -actualSlide * Math.sin(rad);

  const blockW = 50, blockH = 36;
  const transformStr = `translate(${bCx + slideX},${bCy + slideY}) rotate(${-slopeAngle}) translate(${-blockW/2},${-blockH})`;
  blockG.setAttribute('transform', transformStr);

  // 力矢量大小随振动变化
  const forceG = document.getElementById('forceVectors');
  if (forceG && state.showForces) {
    forceG.style.opacity = '1';
    // 法向力长度不变(由预紧力决定)
    // 摩擦力长度随振动增大
    const fLine = forceG.querySelector('.force-f');
    if (fLine) {
      const fLen = 45 + Math.abs(vibPush) * 1.5;
      fLine.setAttribute('x2', -fLen);
    }
  } else if (forceG) {
    forceG.style.opacity = '0';
  }
}

function updateVibSimulation(vibAmp, t, vibFreq) {
  const vibBolt = document.getElementById('vibBolt');
  if (vibBolt) {
    const offsetX = vibAmp * 10 * Math.sin(t * vibFreq);
    vibBolt.setAttribute('transform', `translate(${offsetX}, 0)`);
  }

  // 楔形接触点发光
  const contacts = svg.querySelectorAll('.wedge-contact');
  const glowIntensity = 0.5 + Math.min(vibAmp * Math.abs(Math.sin(t * vibFreq)), 1) * 0.5;
  contacts.forEach(c => {
    c.style.opacity = glowIntensity + 0.3;
  });

  // 锁定指示灯
  const lockLight = document.getElementById('lockLight');
  const lockText = document.getElementById('lockText');
  if (lockLight && lockText) {
    if (vibAmp > 0.1) {
      const pulseVal = 0.7 + 0.3 * Math.sin(t * 6);
      lockLight.style.opacity = pulseVal;
      lockLight.setAttribute('fill', '#2ed573');
      lockText.textContent = 'LOCK';
      lockText.setAttribute('fill', '#2ed573');
    } else {
      lockLight.style.opacity = '0.4';
      lockLight.setAttribute('fill', '#4a5e78');
      lockText.textContent = 'IDLE';
      lockText.setAttribute('fill', '#4a5e78');
    }
  }
}

function renderParticles() {
  // 移除旧粒子
  svg.querySelectorAll('.energy-particle').forEach(p => p.remove());

  particles.forEach(p => {
    if (!p.active) return;
    const alpha = 1 - p.life / p.maxLife;
    const color = p.life < p.maxLife * 0.5 ? '#f0a500' : '#2ed573';
    el('circle', {
      cx: p.x, cy: p.y, r: p.size * alpha,
      fill: color, opacity: alpha * 0.8,
      class: 'energy-particle'
    });
  });
}

function updateStatusUI() {
  const ampPct = state.amplitude;
  const fricMul = state.frictionMultiplier.toFixed(1);
  const energyPct = Math.min(Math.round((state.lockStrength - 1) / 2 * 100), 100);

  document.getElementById('frictionRatio').textContent = `×${fricMul}`;
  document.getElementById('energyConv').textContent = `${energyPct}%`;
  document.getElementById('eqFriction').textContent = `μ' = ${fricMul}μ`;

  const lockDot = document.getElementById('lockDot');
  const lockLabel = document.getElementById('lockLabel');
  if (state.amplitude > 10) {
    lockDot.style.background = 'var(--green)';
    lockLabel.textContent = '自锁稳定';
    lockLabel.style.color = 'var(--green)';
  } else {
    lockDot.style.background = 'var(--text-muted)';
    lockLabel.textContent = '待载状态';
    lockLabel.style.color = 'var(--text-muted)';
  }
}

// ===== 初始化 =====
function init() {
  clearSvg();
  defineDefs();
  drawCrossSection();
  drawInclineModel();
  drawVibSection();
  initParticles();

  // 绘制分隔线
  el('line', {
    x1: 730, y1: 40, x2: 730, y2: 610,
    stroke: 'var(--border)', 'stroke-width': 1, 'stroke-dasharray': '4,4'
  });

  // 标注文字
  const noteText = el('text', {
    x: 400, y: 430,
    'font-family': "'Outfit', sans-serif", 'font-size': '13',
    fill: '#4a5e78', 'text-anchor': 'middle'
  });
  noteText.textContent = '← 截面图:非对称内螺纹楔形面(金色)与标准公螺纹啮合 →';

  // IFR原则标注
  const ifrG = el('g', {transform: 'translate(400, 590)'});
  el('rect', {
    x: -180, y: -16, width: 360, height: 32, rx: 6,
    fill: 'rgba(240,165,0,0.06)', stroke: 'rgba(240,165,0,0.2)', 'stroke-width': 1
  }, ifrG);
  const ifrText = el('text', {
    x: 0, y: 5,
    'font-family': "'JetBrains Mono', monospace", 'font-size': '12',
    fill: '#f0a500', 'text-anchor': 'middle', 'font-weight': 500
  }, ifrG);
  ifrText.textContent = 'IFR:振动能量 ──→ 锁紧动力 · 无附加元件 · 自行维持';

  // 底部参数说明
  const paramG = el('g', {transform: 'translate(100, 470)'});
  const params = [
    {label: '楔形斜面角', value: '30°', color: '#f0a500'},
    {label: '对称面角', value: '30°', color: '#6b7fa3'},
    {label: '啮合长度', value: '≥1.5d', color: '#2ed573'},
    {label: '当量摩擦增幅', value: '×2.8', color: '#00d4ff'},
  ];
  params.forEach((p, i) => {
    const px = i * 140;
    el('circle', {cx: px, cy: 0, r: 4, fill: p.color}, paramG);
    const t1 = el('text', {
      x: px + 10, y: -2,
      'font-family': "'JetBrains Mono', monospace", 'font-size': '10',
      fill: '#4a5e78'
    }, paramG);
    t1.textContent = p.label;
    const t2 = el('text', {
      x: px + 10, y: 12,
      'font-family': "'JetBrains Mono', monospace", 'font-size': '13',
      fill: p.color, 'font-weight': 600
    }, paramG);
    t2.textContent = p.value;
  });

  // 机理说明文字
  const mechG = el('g', {transform: 'translate(100, 520)'});
  const lines = [
    '机理:横向振动 → 螺栓螺纹压向楔形斜面 → 法向力全转化为正压力',
    '效果:当量摩擦系数成倍增大 → 摩擦力瞬间超过退扣力 → 自锁',
    '优势:无需胶水/弹垫/开口销,振动越强锁紧越牢,装拆工序不变'
  ];
  lines.forEach((line, i) => {
    const t = el('text', {
      x: 0, y: i * 18,
      'font-family': "'Outfit', sans-serif", 'font-size': '11',
      fill: i === 0 ? '#c8d6e5' : '#4a5e78'
    }, mechG);
    t.textContent = line;
  });

  requestAnimationFrame(animate);
}

// ===== 交互绑定 =====
document.getElementById('ampSlider').addEventListener('input', e => {
  state.amplitude = parseInt(e.target.value);
  document.getElementById('ampVal').textContent = state.amplitude;
});

document.getElementById('freqSlider').addEventListener('input', e => {
  state.frequency = parseInt(e.target.value);
  document.getElementById('freqVal').textContent = state.frequency;
});

document.getElementById('forceBtn').addEventListener('click', e => {
  state.showForces = !state.showForces;
  e.currentTarget.classList.toggle('active', state.showForces);
});

document.getElementById('playBtn').addEventListener('click', e => {
  state.playing = !state.playing;
  const icon = e.currentTarget.querySelector('i');
  if (state.playing) {
    icon.className = 'fas fa-pause';
  } else {
    icon.className = 'fas fa-play';
  }
});

document.getElementById('resetBtn').addEventListener('click', () => {
  state.time = 0;
  state.amplitude = 50;
  state.frequency = 40;
  state.playing = true;
  state.showForces = true;
  document.getElementById('ampSlider').value = 50;
  document.getElementById('freqSlider').value = 40;
  document.getElementById('ampVal').textContent = '50';
  document.getElementById('freqVal').textContent = '40';
  document.getElementById('forceBtn').classList.add('active');
  document.getElementById('playBtn').querySelector('i').className = 'fas fa-pause';
  initParticles();
});

// 启动
init();
</script>
</body>
</html>

实现说明

这个动画完整呈现了30°楔形自锁螺纹的IFR(最终理想解)工作原理,核心设计思路如下:

视觉架构

  • 左侧截面图:绘制了非对称内螺纹螺母与标准公螺纹螺栓的啮合关系,金色高亮标识楔形面(55°从法线量起的大倾角斜面),与标准30°面形成鲜明对比,配有角度标注弧线
  • 右上力学模型:经典的斜面自锁分析图——方块(螺栓螺纹)置于30°楔形斜面上,展示法向力N、摩擦力f=μ'N、预紧力分量Fp三者的动态关系,摩擦力箭头会随振动强度实时伸缩
  • 右下振动响应:螺栓-螺母装配体的横向振动模拟,实时展示螺栓振动偏移、楔形面接触发光、锁定状态指示灯

IFR核心表达

动画突出"振动能量→锁紧动力"这一理想解本质——振幅越大,楔形面摩擦增幅越高(×1.0→×4.6),锁定越稳固。状态栏的摩擦增幅和能量转化百分比实时反映这一转化关系。

交互控制

  • 振幅滑块:调节横向振动强度(0-100),直接影响锁紧效果和力矢量大小
  • 频率滑块:调节振动频率
  • 力矢量按钮:切换力学模型中的力矢量显示
  • 暂停/重置:控制动画播放
积分规则:第一轮对话扣减6分,后续每轮扣4分