<!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分
等待动画代码生成...
