独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRIZ 理想最终解:纯机械自适应修鼓机</title>
<style>
:root {
--bg-color: #0b0f19;
--grid-color: rgba(30, 58, 138, 0.2);
--accent-cyan: #00f0ff;
--accent-amber: #ffb700;
--accent-red: #ff3366;
--text-main: #e2e8f0;
--text-dim: #94a3b8;
--panel-bg: rgba(11, 15, 25, 0.8);
--panel-border: rgba(0, 240, 255, 0.3);
--font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: var(--font-family);
height: 100vh;
width: 100vw;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 30px 30px;
background-position: center center;
}
/* 核心动画容器 */
#animation-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
svg {
width: 90%;
max-width: 1400px;
height: auto;
max-height: 85vh;
filter: drop-shadow(0 0 30px rgba(0, 240, 255, 0.1));
overflow: visible;
}
/* 信息面板 - 绝对定位在角落,严格控制尺寸与位置 */
.info-panel {
position: absolute;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 16px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 10;
max-width: 320px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.panel-top-left { top: 30px; left: 30px; }
.panel-top-right { top: 30px; right: 30px; }
.panel-bottom-left { bottom: 30px; left: 30px; }
.info-title {
font-size: 14px;
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 8px;
letter-spacing: 1px;
text-transform: uppercase;
}
.info-text {
font-size: 12px;
line-height: 1.6;
color: var(--text-dim);
text-align: justify;
}
.info-text strong {
color: var(--text-main);
}
.highlight-triz {
display: inline-block;
background: rgba(255, 183, 0, 0.15);
color: var(--accent-amber);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
margin-top: 6px;
border: 1px solid rgba(255, 183, 0, 0.3);
}
/* 状态指示器 */
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 12px;
font-family: monospace;
color: var(--accent-cyan);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--accent-cyan);
box-shadow: 0 0 8px var(--accent-cyan);
transition: background-color 0.3s, box-shadow 0.3s;
}
/* 底部交互控制区 */
.control-panel {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 20px;
padding: 10px 24px;
display: flex;
align-items: center;
gap: 16px;
z-index: 10;
backdrop-filter: blur(10px);
}
.control-btn {
background: transparent;
border: 1px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover, .control-btn.active {
background: var(--accent-cyan);
color: var(--bg-color);
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
width: 300px;
}
.time-slider {
-webkit-appearance: none;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.time-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-cyan);
box-shadow: 0 0 10px var(--accent-cyan);
cursor: pointer;
transition: transform 0.1s;
}
.time-slider::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.slider-label {
font-size: 11px;
color: var(--text-dim);
font-family: monospace;
min-width: 45px;
}
/* 持续旋转动画 */
.spin-infinite {
animation: spin 3s linear infinite;
transform-origin: center;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
/* 动态发光类 */
.glow-cyan { filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.6)); }
.glow-amber { filter: drop-shadow(0 0 10px rgba(255, 183, 0, 0.8)); }
.glow-red { filter: drop-shadow(0 0 12px rgba(255, 51, 102, 0.9)); }
</style>
</head>
<body>
<div id="animation-container">
<!-- 左上角:设计原理 -->
<div class="info-panel panel-top-left">
<div class="info-title">最终理想解 (IFR) 架构</div>
<div class="info-text">
本机构通过<strong>大扭矩单电机</strong>与内置的<strong>“螺距离合器”</strong>相配合,彻底去除了传统方案中复杂的电动推缸与传感器。
<br><br>
巧妙利用用户手持球杆向内推压的<strong>轴向进给力</strong>,在遇到内部硬限位产生“扭矩/推力阶跃”时,自动触发顶刀旋出,实现侧面与顶部切削的时序协同。
</div>
<div class="highlight-triz">TRIZ 原理:利用现有资源 (用户推力) 替代外部复杂系统 (推缸)</div>
</div>
<!-- 右上角:组件图例 -->
<div class="info-panel panel-top-right">
<div class="info-title">系统核心组件</div>
<div class="info-text" style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 12px; height: 12px; border: 2px solid var(--accent-cyan); border-radius: 2px;"></span> 侧切刀盘 (受弹簧约束)
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 12px; height: 12px; background: rgba(0,240,255,0.2); border: 1px solid var(--accent-cyan); border-radius: 50%;"></span> 顶弧切刀 (随螺纹旋出)
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 12px; height: 2px; background: var(--accent-amber); box-shadow: 0 0 5px var(--accent-amber);"></span> 高压阻尼弹簧 (预紧力30N)
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 12px; height: 12px; border-right: 3px solid var(--accent-red);"></span> 物理硬限位
</div>
</div>
</div>
<!-- 左下角:实时状态 -->
<div class="info-panel panel-bottom-left">
<div class="info-title">实时力学状态监测</div>
<div class="info-text">当前时序阶段解析:</div>
<div class="status-indicator">
<div class="status-dot" id="status-dot"></div>
<span id="status-text">待机 / 准备插入</span>
</div>
<div style="margin-top: 10px; font-size: 11px; color: var(--text-dim); display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<span>轴向推力: <span id="val-thrust" style="color:var(--text-main);">0 N</span></span>
<span>弹簧反力: <span id="val-spring" style="color:var(--text-main);">30 N</span></span>
<span>丝杠位移: <span id="val-disp" style="color:var(--text-main);">0 mm</span></span>
<span>切削模式: <span id="val-mode" style="color:var(--accent-cyan);">空转</span></span>
</div>
</div>
<!-- 底部交互控制 -->
<div class="control-panel">
<button class="control-btn active" id="btn-auto">自动演示</button>
<div class="slider-container">
<span class="slider-label">0%</span>
<input type="range" class="time-slider" id="timeline-slider" min="0" max="1000" value="0">
<span class="slider-label">100%</span>
</div>
</div>
<!-- 核心 SVG 动画区 -->
<svg viewBox="0 0 1000 500" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 滤镜与渐变定义 -->
<filter id="neon-cyan" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="neon-amber" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="neon-red" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="6" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<linearGradient id="metal-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#475569" />
<stop offset="50%" stop-color="#94a3b8" />
<stop offset="100%" stop-color="#334155" />
</linearGradient>
<linearGradient id="wood-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#92400e" />
<stop offset="50%" stop-color="#d97706" />
<stop offset="100%" stop-color="#78350f" />
</linearGradient>
<linearGradient id="tip-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#1e3a8a" />
<stop offset="50%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#1e40af" />
</linearGradient>
<pattern id="screw-pattern" width="10" height="20" patternUnits="userSpaceOnUse">
<line x1="0" y1="20" x2="10" y2="0" stroke="#00f0ff" stroke-width="2" opacity="0.4"/>
</pattern>
<clipPath id="tip-clip">
<!-- 动态裁剪路径,用于表现皮头被切削成圆弧形 -->
<path id="clip-path-shape" d="M 0 -25 L 50 -25 L 50 25 L 0 25 Z" />
</clipPath>
</defs>
<!-- 极简背景线条 -->
<g stroke="rgba(255,255,255,0.05)" stroke-width="1">
<line x1="0" y1="250" x2="1000" y2="250" stroke-dasharray="10,5" />
<line x1="500" y1="0" x2="500" y2="500" stroke-dasharray="10,5" />
</g>
<!-- 旋转方向指示 (电机抽象) -->
<g transform="translate(850, 250)">
<circle cx="0" cy="0" r="140" fill="none" stroke="rgba(0, 240, 255, 0.1)" stroke-width="1" />
<path class="spin-infinite" d="M -150 0 A 150 150 0 0 1 0 -150" fill="none" stroke="var(--accent-cyan)" stroke-width="2" stroke-linecap="round">
<animate attributeName="opacity" values="0.2;0.8;0.2" dur="2s" repeatCount="indefinite"/>
</path>
<polygon points="2 -155, 12 -150, 2 -145" fill="var(--accent-cyan)" transform="rotate(0) translate(-3,0)" class="spin-infinite"/>
<text x="0" y="170" fill="var(--accent-cyan)" font-size="12" text-anchor="middle" font-family="monospace" opacity="0.6">单向连续主轴驱动</text>
</g>
<!-- 机器外壳剖面 -->
<path d="M 400 100 L 900 100 L 900 400 L 400 400 Z" fill="none" stroke="#334155" stroke-width="4" stroke-linejoin="round" />
<path d="M 380 150 L 400 150 L 400 350 L 380 350" fill="none" stroke="#475569" stroke-width="4" stroke-linecap="round" />
<!-- 内部螺距机构 (固定座) -->
<rect x="750" y="180" width="100" height="140" fill="url(#metal-grad)" rx="4" />
<text x="800" y="255" fill="#0b0f19" font-size="12" font-weight="bold" text-anchor="middle">主轴座</text>
<!-- 动态系统组:螺距离合器、弹簧、刀盘 -->
<!-- 螺距丝杠 -->
<rect id="screw-shaft" x="600" y="225" width="150" height="50" fill="url(#screw-pattern)" stroke="#00f0ff" stroke-width="1" />
<!-- 阻尼弹簧 -->
<g id="spring-group" transform="translate(680, 250)">
<!-- 弹簧通过缩放X轴来表现压缩,transform-origin 在右侧 -->
<path id="spring-path" d="M 0 0 L -15 -30 L -30 30 L -45 -30 L -60 30 L -75 -30 L -90 30 L -105 -30 L -120 30 L -135 -30 L -150 0"
fill="none" stroke="var(--accent-amber)" stroke-width="6" stroke-linejoin="round" filter="url(#neon-amber)"/>
</g>
<!-- 顶弧切刀盘 (内部隐藏,被丝杠推出) -->
<g id="top-blade-group" transform="translate(600, 250)">
<path d="M 0 -40 L 40 -40 L 40 40 L 0 40 Z" fill="rgba(0, 240, 255, 0.1)" stroke="var(--accent-cyan)" stroke-width="2" />
<!-- 顶刀刃口 - 弧形 -->
<path d="M -10 -25 Q -30 0 -10 25 L 0 25 L 0 -25 Z" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
<line x1="-30" y1="0" x2="-60" y2="0" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="4,4" opacity="0.5"/>
</g>
<!-- 侧刀盘组 (受弹簧推力,遇限位后相对位移导致张开) -->
<!-- 上侧刀 -->
<g id="side-blade-top" transform="translate(480, 150)">
<path d="M 0 0 L 120 0 L 120 90 L 40 90 L 0 50 Z" fill="url(#metal-grad)" stroke="#94a3b8" stroke-width="2" />
<path d="M 0 50 L 40 90" stroke="var(--accent-cyan)" stroke-width="4" filter="url(#neon-cyan)" /> <!-- 侧切刃 -->
</g>
<!-- 下侧刀 -->
<g id="side-blade-bottom" transform="translate(480, 350)">
<path d="M 0 0 L 120 0 L 120 -90 L 40 -90 L 0 -50 Z" fill="url(#metal-grad)" stroke="#94a3b8" stroke-width="2" />
<path d="M 0 -50 L 40 -90" stroke="var(--accent-cyan)" stroke-width="4" filter="url(#neon-cyan)" /> <!-- 侧切刃 -->
</g>
<!-- 物理硬限位 -->
<g transform="translate(580, 250)">
<rect x="-10" y="-80" width="20" height="40" fill="#1e293b" stroke="#334155" stroke-width="2" />
<rect x="-10" y="40" width="20" height="40" fill="#1e293b" stroke="#334155" stroke-width="2" />
<line id="hard-limit-line-top" x1="-10" y1="-40" x2="-10" y2="-80" stroke="var(--accent-red)" stroke-width="4" />
<line id="hard-limit-line-bot" x1="-10" y1="40" x2="-10" y2="80" stroke="var(--accent-red)" stroke-width="4" />
<text x="0" y="-95" fill="var(--accent-red)" font-size="10" text-anchor="middle" font-family="monospace">硬限位</text>
</g>
<!-- 球杆及皮头组 (由用户推力驱动进入) -->
<g id="cue-stick-group" transform="translate(100, 250)">
<!-- 动能/推力指示箭头 -->
<g id="force-arrow" transform="translate(-150, 0)">
<path d="M 0 -15 L 40 -15 L 40 -30 L 70 0 L 40 30 L 40 15 L 0 15 Z" fill="rgba(255,255,255,0.1)" stroke="var(--text-main)" stroke-width="2"/>
<text x="35" y="5" fill="var(--text-main)" font-size="14" font-weight="bold" text-anchor="middle" id="force-text">进给推力</text>
</g>
<!-- 球杆先角与木质部分 -->
<path d="M -300 -20 L -10 -15 L -10 15 L -300 20 Z" fill="url(#wood-grad)" />
<rect x="-10" y="-15" width="20" height="30" fill="#e2e8f0" />
<!-- 皮头 (应用裁剪路径实现变形切削效果) -->
<g clip-path="url(#tip-clip)">
<rect id="cue-tip-rect" x="10" y="-16" width="30" height="32" fill="url(#tip-grad)" rx="2" />
</g>
<!-- 辅助对齐线 -->
<line x1="-300" y1="0" x2="60" y2="0" stroke="rgba(255,255,255,0.2)" stroke-dasharray="4,4" />
</g>
<!-- 切削火花/碎屑特效 (隐藏态) -->
<g id="sparks-side" opacity="0" transform="translate(480, 200)">
<circle cx="0" cy="0" r="2" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
<circle cx="-10" cy="-10" r="1.5" fill="var(--accent-cyan)" />
<circle cx="-5" cy="15" r="1" fill="var(--accent-cyan)" />
</g>
<g id="sparks-top" opacity="0" transform="translate(560, 250)">
<circle cx="0" cy="-10" r="2" fill="var(--accent-cyan)" filter="url(#neon-cyan)" />
<circle cx="-5" cy="10" r="1.5" fill="var(--accent-cyan)" />
</g>
</svg>
</div>
<script>
/**
* 动画时序控制器 (State Machine & Timeline)
* 基于 requestAnimationFrame 实现平滑的高保真工程动画
*/
const DOM = {
cueGroup: document.getElementById('cue-stick-group'),
cueTip: document.getElementById('cue-tip-rect'),
clipShape: document.getElementById('clip-path-shape'),
springGroup: document.getElementById('spring-group'),
springPath: document.getElementById('spring-path'),
sideTop: document.getElementById('side-blade-top'),
sideBot: document.getElementById('side-blade-bottom'),
topBlade: document.getElementById('top-blade-group'),
screwShaft: document.getElementById('screw-shaft'),
limitLineTop: document.getElementById('hard-limit-line-top'),
limitLineBot: document.getElementById('hard-limit-line-bot'),
forceArrow: document.getElementById('force-arrow'),
forceText: document.getElementById('force-text'),
sparksSide: document.getElementById('sparks-side'),
sparksTop: document.getElementById('sparks-top'),
// UI 面板
statusDot: document.getElementById('status-dot'),
statusText: document.getElementById('status-text'),
valThrust: document.getElementById('val-thrust'),
valSpring: document.getElementById('val-spring'),
valDisp: document.getElementById('val-disp'),
valMode: document.getElementById('val-mode'),
// 交互
btnAuto: document.getElementById('btn-auto'),
slider: document.getElementById('timeline-slider')
};
// 动画配置
const CONFIG = {
duration: 9000, // 总周期 9 秒
isAuto: true,
progress: 0,
// 关键帧时间点 (百分比)
t_insert: 0.15, // 插入阶段结束
t_sideCut: 0.35, // 侧切结束,碰到硬限位
t_limit: 0.45, // 抵住限位,推力蓄积
t_topCut: 0.65, // 压缩弹簧,顶刀切削完成
t_hold: 0.80, // 保持展示
t_reset: 1.0 // 拔出重置
};
// 插值辅助函数
const lerp = (start, end, t) => start + (end - start) * Math.max(0, Math.min(1, t));
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
// 核心渲染逻辑:根据进度(0~1)更新所有DOM元素的状态
function renderFrame(p) {
// 1. 初始化 / 待机态
let cueX = 100;
let tipHeight = 32; // 原始高度
let tipY = -16;
let clipD = "M 0 -25 L 50 -25 L 50 25 L 0 25 Z"; // 初始平头裁剪
let springScaleX = 1.0;
let springColor = "var(--accent-amber)";
let sideBladeYOffset = 0; // 侧刀张开量
let topBladeX = 600; // 顶刀位置
let screwShiftX = 600;
let forceScale = 1.0;
let forceColor = "rgba(255,255,255,0.1)";
let forceStroke = "var(--text-main)";
let limitGlow = "none";
let sparksSideOp = 0;
let sparksTopOp = 0;
// UI 状态文本
let sText = "待机 / 准备插入";
let sColor = "var(--text-dim)";
let vThrust = "5 N";
let vSpring = "30 N";
let vDisp = "0.0 mm";
let vMode = "空转";
// 阶段 1: 插入并进行侧面切削 (0 -> t_sideCut)
if (p <= CONFIG.t_sideCut) {
let localP = p / CONFIG.t_sideCut;
cueX = lerp(100, 440, localP); // 推进至刀盘内
if (localP > 0.4) { // 开始接触侧刀
let cutP = (localP - 0.4) / 0.6;
tipHeight = lerp(32, 28, cutP); // 侧面被削去 4mm
tipY = -tipHeight / 2;
sparksSideOp = Math.random() > 0.3 ? 0.8 : 0.2;
sText = "轴向进给:削减侧面余量";
sColor = "var(--accent-cyan)";
vThrust = "15 N";
vMode = "侧面修边";
} else {
sText = "轴向进给:插入球杆";
sColor = "var(--text-main)";
}
}
// 阶段 2: 遭遇硬限位,蓄力 (t_sideCut -> t_limit)
else if (p <= CONFIG.t_limit) {
cueX = 440; // 停在限位处
tipHeight = 28;
tipY = -14;
let localP = (p - CONFIG.t_sideCut) / (CONFIG.t_limit - CONFIG.t_sideCut);
forceScale = lerp(1.0, 1.3, localP);
forceColor = `rgba(255, 51, 102, ${lerp(0.1, 0.4, localP)})`;
forceStroke = "var(--accent-red)";
limitGlow = localP > 0.5 ? "url(#neon-red)" : "none";
sText = "遭遇硬限位:推力阶跃激增";
sColor = "var(--accent-red)";
vThrust = `${(15 + localP * 25).toFixed(1)} N`; // 15N -> 40N
vMode = "进给受阻";
}
// 阶段 3: 克服弹簧预紧力,机构相对位移,顶刀切削 (t_limit -> t_topCut)
else if (p <= CONFIG.t_topCut) {
cueX = 440; // 杆子本体不再前进
tipHeight = 28;
tipY = -14;
forceScale = 1.3;
forceColor = "rgba(255, 51, 102, 0.4)";
forceStroke = "var(--accent-red)";
limitGlow = "url(#neon-red)";
let localP = (p - CONFIG.t_limit) / (CONFIG.t_topCut - CONFIG.t_limit);
// IFR 核心动作:位移与协同
springScaleX = lerp(1.0, 0.4, localP); // 弹簧压缩
springColor = "var(--accent-red)"; // 弹簧受高压变红
sideBladeYOffset = lerp(0, 15, localP); // 侧刀被迫上下张开避让
topBladeX = lerp(600, 540, localP); // 顶刀旋出向前压入
screwShiftX = lerp(600, 630, localP); // 丝杠相对位移
// 顶刀切削,改变皮头前端形状(裁剪路径从矩形变为圆弧)
let curveX = lerp(50, 25, localP);
clipD = `M 0 -25 L ${curveX} -25 Q 50 0 ${curveX} 25 L 0 25 Z`;
sparksTopOp = Math.random() > 0.3 ? 0.9 : 0.4;
sText = "克服预紧:离合动作触发!";
sColor = "var(--accent-amber)";
vThrust = "40 N (克服弹簧)";
vSpring = "40 N (压缩状态)";
vDisp = `${(localP * 8.0).toFixed(1)} mm`;
vMode = "顶弧修整";
}
// 阶段 4: 保持状态展示 (t_topCut -> t_hold)
else if (p <= CONFIG.t_hold) {
cueX = 440;
tipHeight = 28;
tipY = -14;
forceScale = 1.3;
forceColor = "rgba(255, 51, 102, 0.4)";
forceStroke = "var(--accent-red)";
limitGlow = "url(#neon-red)";
springScaleX = 0.4;
springColor = "var(--accent-red)";
sideBladeYOffset = 15;
topBladeX = 540;
screwShiftX = 630;
clipD = `M 0 -25 L 25 -25 Q 50 0 25 25 L 0 25 Z`;
sText = "修整完成:IFR 状态维持";
sColor = "var(--accent-cyan)";
vThrust = "40 N";
vSpring = "40 N";
vDisp = "8.0 mm";
vMode = "修整完毕";
}
// 阶段 5: 拔出球杆,系统复位 (t_hold -> 1.0)
else {
let localP = (p - CONFIG.t_hold) / (1.0 - CONFIG.t_hold);
cueX = lerp(440, 100, localP); // 拔出
tipHeight = 28;
tipY = -14;
clipD = `M 0 -25 L 25 -25 Q 50 0 25 25 L 0 25 Z`; // 保持切削后的形状
// 机构迅速回弹
let resetP = Math.min(1.0, localP * 3); // 快速回弹
springScaleX = lerp(0.4, 1.0, resetP);
springColor = "var(--accent-amber)";
sideBladeYOffset = lerp(15, 0, resetP);
topBladeX = lerp(540, 600, resetP);
screwShiftX = lerp(630, 600, resetP);
sText = "退出:弹簧复位刀组";
sColor = "var(--text-dim)";
vThrust = "0 N";
vSpring = "30 N (恢复预紧)";
vDisp = `${(8.0 - resetP * 8.0).toFixed(1)} mm`;
vMode = "复位脱离";
}
// --- 实际应用 DOM 属性 ---
// 1. 球杆位置及皮头形状
DOM.cueGroup.setAttribute('transform', `translate(${cueX}, 250)`);
DOM.cueTip.setAttribute('y', tipY);
DOM.cueTip.setAttribute('height', tipHeight);
DOM.clipShape.setAttribute('d', clipD);
// 2. 弹簧压缩与颜色
DOM.springGroup.setAttribute('transform', `translate(680, 250) scale(${springScaleX}, 1)`);
DOM.springPath.setAttribute('stroke', springColor);
if(springScaleX < 0.9) {
DOM.springPath.setAttribute('filter', 'url(#neon-red)');
} else {
DOM.springPath.setAttribute('filter', 'url(#neon-amber)');
}
// 3. 刀组位移
DOM.sideTop.setAttribute('transform', `translate(480, ${150 - sideBladeYOffset})`);
DOM.sideBot.setAttribute('transform', `translate(480, ${350 + sideBladeYOffset})`);
DOM.topBlade.setAttribute('transform', `translate(${topBladeX}, 250)`);
DOM.screwShaft.setAttribute('x', screwShiftX);
// 4. 力学指示与特效
DOM.forceArrow.setAttribute('transform', `translate(-150, 0) scale(${forceScale})`);
DOM.forceArrow.firstElementChild.setAttribute('fill', forceColor);
DOM.forceArrow.firstElementChild.setAttribute('stroke', forceStroke);
DOM.forceText.setAttribute('fill', forceStroke);
DOM.limitLineTop.setAttribute('filter', limitGlow);
DOM.limitLineBot.setAttribute('filter', limitGlow);
DOM.sparksSide.setAttribute('opacity', sparksSideOp);
DOM.sparksTop.setAttribute('opacity', sparksTopOp);
// 5. UI 文本更新
DOM.statusDot.style.backgroundColor = sColor;
DOM.statusDot.style.boxShadow = `0 0 8px ${sColor}`;
DOM.statusText.textContent = sText;
DOM.statusText.style.color = sColor;
DOM.valThrust.textContent = vThrust;
DOM.valSpring.textContent = vSpring;
DOM.valDisp.textContent = vDisp;
DOM.valMode.textContent = vMode;
}
// 动画主循环
let startTimestamp = null;
function animationLoop(timestamp) {
if (!startTimestamp) startTimestamp = timestamp;
if (CONFIG.isAuto) {
// 计算循环进度
let elapsed = (timestamp - startTimestamp) % CONFIG.duration;
CONFIG.progress = elapsed / CONFIG.duration;
// 同步更新滑块 UI
DOM.slider.value = CONFIG.progress * 1000;
}
renderFrame(CONFIG.progress);
requestAnimationFrame(animationLoop);
}
// 交互事件绑定
DOM.btnAuto.addEventListener('click', () => {
CONFIG.isAuto = !CONFIG.isAuto;
if (CONFIG.isAuto) {
DOM.btnAuto.classList.add('active');
DOM.btnAuto.textContent = "自动演示";
// 重新同步时间戳,避免跳变
startTimestamp = performance.now() - (CONFIG.progress * CONFIG.duration);
} else {
DOM.btnAuto.classList.remove('active');
DOM.btnAuto.textContent = "手动控制";
}
});
DOM.slider.addEventListener('input', (e) => {
CONFIG.isAuto = false;
DOM.btnAuto.classList.remove('active');
DOM.btnAuto.textContent = "手动控制";
CONFIG.progress = e.target.value / 1000;
});
// 启动动画 (满足重开即播要求)
window.addEventListener('DOMContentLoaded', () => {
requestAnimationFrame(animationLoop);
});
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
