独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>颗粒阻塞自适应履带系统 - 最终理想解</title>
<style>
:root {
/* 核心调色板:深邃工业风与高反差提示色 */
--bg-color: #050810;
--panel-bg: rgba(13, 20, 36, 0.75);
--border-color: #1E2D4A;
--text-main: #E2E8F0;
--text-dim: #94A3B8;
/* 状态颜色:柔性(青绿) -> 刚性(亮珀) */
--soft-main: #10B981;
--soft-glow: rgba(16, 185, 129, 0.3);
--rigid-main: #F59E0B;
--rigid-glow: rgba(245, 158, 11, 0.4);
--rock-color: #1A233A;
--rock-stroke: #3B82F6;
--font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", 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-ui);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-image:
radial-gradient(circle at 50% 50%, #0d1424 0%, #050810 100%),
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 100% 100%, 40px 40px, 40px 40px;
}
/* 主容器,约束最大尺寸,保持核心区域主角地位 */
#app-container {
position: relative;
width: 100%;
max-width: 1200px;
aspect-ratio: 16 / 9;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
box-shadow: 0 0 60px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(13, 20, 36, 0.8);
border-radius: 8px;
overflow: hidden;
}
svg {
display: block;
width: 100%;
height: 100%;
}
/* 绝对定位的 UI 悬浮面板,字号极小,靠边放置以免遮挡 */
.hud-panel {
position: absolute;
background: var(--panel-bg);
border: 1px solid var(--border-color);
backdrop-filter: blur(8px);
border-radius: 4px;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none;
z-index: 10;
}
.hud-top-left { top: 20px; left: 20px; }
.hud-top-right { top: 20px; right: 20px; text-align: right; }
.hud-bottom-right { bottom: 80px; right: 20px; width: 220px; }
.title-primary {
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
color: #fff;
text-transform: uppercase;
}
.title-secondary {
font-size: 11px;
color: var(--soft-main);
letter-spacing: 0.5px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-family: monospace;
border-bottom: 1px solid rgba(255,255,255,0.05);
padding-bottom: 4px;
}
.data-label { color: var(--text-dim); }
.data-value { font-weight: 600; transition: color 0.3s; }
.value-soft { color: var(--soft-main); }
.value-rigid { color: var(--rigid-main); text-shadow: 0 0 8px var(--rigid-glow); }
/* 底部交互控制栏 */
.control-bar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 40px;
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 20px;
display: flex;
align-items: center;
padding: 0 20px;
gap: 15px;
backdrop-filter: blur(8px);
z-index: 20;
pointer-events: auto;
}
.play-btn {
background: none;
border: none;
color: var(--text-main);
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
transition: color 0.2s;
}
.play-btn:hover { color: var(--soft-main); }
.timeline-container {
flex-grow: 1;
height: 100%;
display: flex;
align-items: center;
position: relative;
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: var(--text-main);
border-radius: 50%;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
transition: background 0.2s, transform 0.1s;
}
input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.3);
background: var(--soft-main);
}
.phase-indicator {
position: absolute;
top: -25px;
left: 0;
font-size: 11px;
color: var(--rigid-main);
font-weight: 600;
white-space: nowrap;
pointer-events: none;
transition: left 0.1s linear;
}
/* SVG 动画与特效类 */
.glow-pulse { animation: pulse 2s infinite; }
@keyframes pulse {
0% { filter: drop-shadow(0 0 2px var(--soft-main)); }
50% { filter: drop-shadow(0 0 12px var(--soft-main)); }
100% { filter: drop-shadow(0 0 2px var(--soft-main)); }
}
.dash-anim {
stroke-dasharray: 6 6;
animation: dashMove 1s linear infinite;
}
@keyframes dashMove { to { stroke-dashoffset: -12; } }
</style>
</head>
<body>
<div id="app-container">
<!-- UI 面板 -->
<div class="hud-panel hud-top-left">
<div class="title-primary">颗粒阻塞自适应履带系统</div>
<div class="title-secondary">TRIZ 最终理想解: 柔性接触 · 刚性咬合</div>
</div>
<div class="hud-panel hud-top-right">
<div class="title-primary" style="font-size: 12px; color: var(--text-dim);">真空负压泵监控</div>
<div style="font-family: monospace; font-size: 24px; font-weight: 700; margin-top: 4px;" id="hud-pressure">
0.0 <span style="font-size: 12px; color: var(--text-dim);">kPa</span>
</div>
</div>
<div class="hud-panel hud-bottom-right">
<div class="data-row">
<span class="data-label">当前阶段</span>
<span class="data-value" id="hud-phase">复位准备</span>
</div>
<div class="data-row">
<span class="data-label">介质状态</span>
<span class="data-value value-soft" id="hud-state">常压 / 柔软流动</span>
</div>
<div class="data-row">
<span class="data-label">形变包裹深度</span>
<span class="data-value" id="hud-depth">0 mm</span>
</div>
<div class="data-row">
<span class="data-label">扭矩滑移率</span>
<span class="data-value" id="hud-slip">0.0 %</span>
</div>
</div>
<!-- 底部控制栏 -->
<div class="control-bar">
<button class="play-btn" id="btn-play">
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
<!-- 初始显示暂停图标(因为自动播放) -->
<path d="M6 4h4v16H6zm8 0h4v16h-4z" id="icon-play-state"/>
</svg>
</button>
<div class="timeline-container">
<div class="phase-indicator" id="timeline-indicator"></div>
<input type="range" id="timeline-slider" min="0" max="1000" value="0">
</div>
</div>
<!-- 核心可视化 SVG 容器 -->
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" id="main-canvas">
<defs>
<!-- 滤镜:用于刚性状态的发光效果 -->
<filter id="glow-rigid" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="glow-soft" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<!-- 台阶截面纹理 -->
<pattern id="rock-pattern" width="20" height="20" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="20" stroke="#1E293B" stroke-width="2" />
</pattern>
</defs>
<!-- 背景刻度网格线 -->
<g stroke="rgba(255,255,255,0.05)" stroke-width="1">
<line x1="100" y1="0" x2="100" y2="600" /><line x1="200" y1="0" x2="200" y2="600" />
<line x1="300" y1="0" x2="300" y2="600" /><line x1="400" y1="0" x2="400" y2="600" />
<line x1="500" y1="0" x2="500" y2="600" /><line x1="600" y1="0" x2="600" y2="600" />
<line x1="700" y1="0" x2="700" y2="600" /><line x1="800" y1="0" x2="800" y2="600" />
<line x1="900" y1="0" x2="900" y2="600" />
<line x1="0" y1="100" x2="1000" y2="100" /><line x1="0" y1="200" x2="1000" y2="200" />
<line x1="0" y1="300" x2="1000" y2="300" /><line x1="0" y1="400" x2="1000" y2="400" />
<line x1="0" y1="500" x2="1000" y2="500" />
</g>
<!-- 复杂不规则台阶轮廓 -->
<g id="ground-system">
<path id="rock-surface" fill="url(#rock-pattern)" stroke="var(--rock-stroke)" stroke-width="3" filter="url(#glow-soft)" />
</g>
<!-- 履带底盘机构与颗粒包 -->
<g id="chassis-system">
<!-- 履带基板/气室通道 -->
<path d="M 320 80 L 680 80 L 700 120 L 300 120 Z" fill="#1E293B" stroke="#334155" stroke-width="2"/>
<!-- 气阀与管路 -->
<rect x="480" y="30" width="40" height="50" fill="#0F172A" stroke="#475569" rx="4"/>
<path id="vacuum-flow-line" d="M 500 80 L 500 120" stroke="var(--soft-main)" stroke-width="4" stroke-linecap="round" class="dash-anim"/>
<text x="500" y="20" fill="var(--text-dim)" font-size="10" text-anchor="middle" font-family="monospace">VACUUM PUMP</text>
<!-- 弹性阻塞包外壳 (路径由 JS 动态生成控制) -->
<path id="bag-membrane" stroke-width="3" fill-opacity="0.1" />
<!-- 颗粒网络系统:连接线 (仅刚性状态显示) -->
<g id="particle-network" opacity="0"></g>
<!-- 内部颗粒物群体 -->
<g id="particles-group"></g>
</g>
<!-- 物理受力指示箭头 -->
<g id="force-vectors" opacity="0" transform="translate(0, 0)">
<!-- 推进力 -->
<path d="M 400 100 L 600 100" stroke="#EF4444" stroke-width="6" marker-end="url(#arrow-red)"/>
<polygon points="600,90 620,100 600,110" fill="#EF4444"/>
<text x="500" y="85" fill="#EF4444" font-size="14" font-weight="bold" text-anchor="middle">履带驱动力向导</text>
<!-- 反作用力/咬合受力 -->
<path id="reaction-arrow" d="M 500 450 L 350 450" stroke="#3B82F6" stroke-width="4"/>
<polygon points="350,442 335,450 350,458" fill="#3B82F6"/>
<text x="420" y="440" fill="#3B82F6" font-size="12" text-anchor="middle">无滑移刚性支点</text>
</g>
</svg>
</div>
<script>
/**
* 核心逻辑控制:动画与状态机
* 遵循最终理想解 (IFR):去除繁冗中间件,直接展示 柔软包裹 -> 瞬间固化 的核心破局点。
*/
// --- 配置参数 ---
const DURATION = 10000; // 完整周期 10 秒
const NUM_PARTICLES = 160; // 内部阻塞颗粒数量
const BAG_WIDTH = 400;
const BAG_MAX_DEPTH = 220;
const CENTER_X = 500;
// 台阶表面生成:使用一段无限循环的不规则波纹代表复杂地形
const rockHeights = [
450, 430, 480, 400, 390, 460, 490, 420, 380, 450,
410, 470, 480, 420, 400, 440, 460, 430, 450, 430,
// 循环补偿
450, 430, 480, 400, 390, 460, 490, 420, 380, 450
];
const ROCK_SEG_WIDTH = 60; // 每段控制点的水平间距
// 状态元素引用
const dom = {
slider: document.getElementById('timeline-slider'),
btnPlay: document.getElementById('btn-play'),
iconPlay: document.getElementById('icon-play-state'),
indicator: document.getElementById('timeline-indicator'),
membrane: document.getElementById('bag-membrane'),
particles: document.getElementById('particles-group'),
network: document.getElementById('particle-network'),
flowLine: document.getElementById('vacuum-flow-line'),
forceVectors: document.getElementById('force-vectors'),
rockSurface: document.getElementById('rock-surface'),
chassis: document.getElementById('chassis-system'),
// HUD
phase: document.getElementById('hud-phase'),
state: document.getElementById('hud-state'),
pressure: document.getElementById('hud-pressure'),
depth: document.getElementById('hud-depth'),
slip: document.getElementById('hud-slip')
};
// --- 数据初始化 ---
let isPlaying = true;
let manualTime = 0;
let lastFrameTime = performance.now();
let animTime = 0; // 0 到 DURATION
// 生成颗粒物初始坐标 (相对于包裹器中心点的位置)
const particleData = [];
for (let i = 0; i < NUM_PARTICLES; i++) {
// 在半圆范围内随机生成点
let rx, ry, isValid = false;
while (!isValid) {
rx = (Math.random() - 0.5) * (BAG_WIDTH - 20);
ry = Math.random() * BAG_MAX_DEPTH;
// 判断是否在椭圆边界内
let maxRy = Math.sqrt(1 - Math.pow(rx / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
if (ry < maxRy - 10) {
isValid = true;
}
}
particleData.push({
rx, ry,
el: null, // DOM 引用
jitterX: Math.random() * 2000, // 柏林噪声相位
jitterY: Math.random() * 2000,
size: 3 + Math.random() * 4
});
}
// 预计算颗粒间的连接线(用于刚化状态的网络可视化)
const networkLinks = [];
for (let i = 0; i < NUM_PARTICLES; i++) {
for (let j = i + 1; j < NUM_PARTICLES; j++) {
let dx = particleData[i].rx - particleData[j].rx;
let dy = particleData[i].ry - particleData[j].ry;
if (dx*dx + dy*dy < 1200) { // 距离小于 34 像素则连接
networkLinks.push({i, j, el: null});
}
}
}
// --- DOM 初始化 ---
function initSVG() {
// 创建颗粒物
particleData.forEach(p => {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', p.size);
dom.particles.appendChild(circle);
p.el = circle;
});
// 创建网络连接线
networkLinks.forEach(link => {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('stroke-width', '1');
dom.network.appendChild(line);
link.el = line;
});
}
// --- 核心数学与运动学函数 ---
// 获取台阶在指定 x 坐标处的高度
function getRockY(x, offset) {
let globalX = x - offset;
// 映射到无限循环的波纹数组
let maxIndex = (rockHeights.length - 10) * ROCK_SEG_WIDTH;
while (globalX < 0) globalX += maxIndex;
globalX = globalX % maxIndex;
let index = Math.floor(globalX / ROCK_SEG_WIDTH);
let t = (globalX % ROCK_SEG_WIDTH) / ROCK_SEG_WIDTH;
// 缓动插值,使山峰圆滑
let smoothT = t * t * (3 - 2 * t);
return rockHeights[index] * (1 - smoothT) + rockHeights[index + 1] * smoothT;
}
// 渲染帧
function render(time) {
// 时间归一化到 0~1 周期
const progress = (time % DURATION) / DURATION;
// --- 阶段定义与时间线插值 ---
// 0.00-0.15 : 底盘下压接触 (BaseY 增加)
// 0.15-0.25 : 台阶挤压,完美共形 (包裹形变)
// 0.25-0.30 : 抽真空,瞬间固化 (属性变化)
// 0.30-0.65 : 电机发力,驱动车体 (台阶后移,显示抓地力)
// 0.65-0.70 : 泄压解除固化
// 0.70-0.85 : 底盘抬起复位
// 0.85-1.00 : 台阶滚动刷新地形
let phaseText = "复位准备";
let stateText = "常压 / 柔软流动";
let isRigid = false;
let rigidRatio = 0; // 0 到 1 的过渡
// 运动学变量
let baseY = 80; // 底盘基准高度
let rockOffset = 0; // 台阶相对移动量
let displayPressure = 0;
let displayDepth = 0;
if (progress < 0.15) {
phaseText = "柔顺接触地形";
baseY = 80 + (progress / 0.15) * 120; // 80 -> 200
} else if (progress < 0.25) {
phaseText = "自适应不规则轮廓";
baseY = 200;
displayDepth = ((progress - 0.15) / 0.1) * 50; // 形变深度 0->50
} else if (progress < 0.30) {
phaseText = "负压瞬间固化";
baseY = 200;
rigidRatio = (progress - 0.25) / 0.05;
isRigid = true;
displayDepth = 50;
displayPressure = -80 * rigidRatio; // 压力 0 -> -80
} else if (progress < 0.65) {
phaseText = "刚性咬合传递大扭矩";
baseY = 200;
rigidRatio = 1;
isRigid = true;
displayDepth = 50;
displayPressure = -80;
// 驱动地形后移模拟履带前进
rockOffset = - ((progress - 0.30) / 0.35) * 400;
} else if (progress < 0.70) {
phaseText = "气阀泄压复位";
baseY = 200;
rigidRatio = 1 - ((progress - 0.65) / 0.05);
displayDepth = 50;
displayPressure = -80 * rigidRatio;
rockOffset = -400;
} else if (progress < 0.85) {
phaseText = "脱离台阶障碍";
baseY = 200 - ((progress - 0.70) / 0.15) * 120; // 200 -> 80
displayDepth = 50 * (1 - (progress - 0.70) / 0.15);
rockOffset = -400;
} else {
phaseText = "寻找下一支点";
baseY = 80;
// 刷新地形到下一个位置(无缝连接)
rockOffset = -400 - ((progress - 0.85) / 0.15) * 200;
}
// --- 渲染视觉更新 ---
// 1. 生成包裹器外壳路径
let pathD = `M ${CENTER_X - BAG_WIDTH/2} ${baseY + 40}`; // 起点左上
let rightBound = CENTER_X + BAG_WIDTH/2;
// 分段计算底部曲线以贴合地形
const segmentCount = 30;
const bagBottomPoints = [];
for (let i = 0; i <= segmentCount; i++) {
let cx = (CENTER_X - BAG_WIDTH/2) + (BAG_WIDTH / segmentCount) * i;
// 自由悬垂状态下的理论高度 (半椭圆)
let freeY = (baseY + 40) + Math.sqrt(1 - Math.pow((cx - CENTER_X) / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
// 地形限制高度
let rY = getRockY(cx, rockOffset);
// IFR核心:包裹器完美被动顺应地形限制
let actualY = Math.min(freeY, rY - 5);
bagBottomPoints.push({x: cx, y: actualY, freeY: freeY, rockY: rY});
pathD += ` L ${cx} ${actualY}`;
}
pathD += ` L ${rightBound} ${baseY + 40} Z`; // 闭合回到右上
dom.membrane.setAttribute('d', pathD);
// 2. 更新地形面 SVG 路径
let rockPathD = `M 0 600 `;
for (let x = 0; x <= 1000; x += 20) {
rockPathD += `L ${x} ${getRockY(x, rockOffset)} `;
}
rockPathD += `L 1000 600 Z`;
dom.rockSurface.setAttribute('d', rockPathD);
// 3. 渲染内部颗粒动态
// 软态颜色:深青;刚态颜色:亮珀
const rC = isRigid ? [245, 158, 11] : [16, 185, 129];
// 混合颜色计算
const currentR = 16 + (245 - 16) * rigidRatio;
const currentG = 185 + (158 - 185) * rigidRatio;
const currentB = 129 + (11 - 129) * rigidRatio;
const rgbStr = `rgb(${currentR}, ${currentG}, ${currentB})`;
dom.membrane.setAttribute('fill', `rgba(${currentR}, ${currentG}, ${currentB}, ${0.1 + rigidRatio*0.3})`);
dom.membrane.setAttribute('stroke', rgbStr);
if (rigidRatio < 0.5) {
dom.membrane.classList.add('dash-anim');
dom.membrane.setAttribute('filter', 'none');
} else {
dom.membrane.classList.remove('dash-anim');
dom.membrane.setAttribute('stroke-dasharray', 'none');
dom.membrane.setAttribute('filter', 'url(#glow-rigid)');
}
// 时间变量用于布朗运动 (软态才运动)
let tSlow = isRigid ? 0 : time * 0.002;
particleData.forEach((p, idx) => {
let px = CENTER_X + p.rx;
let theoryMaxY = Math.sqrt(1 - Math.pow(p.rx / (BAG_WIDTH/2), 2)) * BAG_MAX_DEPTH;
let py_free = baseY + 40 + p.ry;
// 空间挤压计算 (体积等比压缩)
let rockLimY = getRockY(px, rockOffset) - 10;
let actualY = py_free;
// 当前 x 处可用最大深度
let availDepth = rockLimY - (baseY + 40);
if (availDepth < theoryMaxY && availDepth > 0) {
// 等比例压缩颗粒位置
let compressRatio = availDepth / theoryMaxY;
actualY = baseY + 40 + (p.ry * compressRatio);
} else if (availDepth <= 0) {
actualY = baseY + 40;
}
// 添加柔软状态下的流体颤动
let jitterX = isRigid ? 0 : Math.sin(p.jitterX + tSlow) * 2;
let jitterY = isRigid ? 0 : Math.cos(p.jitterY + tSlow) * 2;
p.currentPx = px + jitterX;
p.currentPy = actualY + jitterY;
p.el.setAttribute('cx', p.currentPx);
p.el.setAttribute('cy', p.currentPy);
p.el.setAttribute('fill', isRigid ? rgbStr : 'var(--soft-main)');
p.el.setAttribute('opacity', 0.6 + rigidRatio*0.4);
});
// 4. 更新刚化网络 (IFR: 微观力链形成)
if (rigidRatio > 0.1) {
dom.network.setAttribute('opacity', rigidRatio * 0.8);
dom.network.setAttribute('stroke', rgbStr);
networkLinks.forEach(link => {
let p1 = particleData[link.i];
let p2 = particleData[link.j];
link.el.setAttribute('x1', p1.currentPx);
link.el.setAttribute('y1', p1.currentPy);
link.el.setAttribute('x2', p2.currentPx);
link.el.setAttribute('y2', p2.currentPy);
});
} else {
dom.network.setAttribute('opacity', 0);
}
// 5. 驱动阶段附加效果
if (progress > 0.30 && progress < 0.65) {
dom.forceVectors.setAttribute('opacity', 1);
// 箭头轻微震动表现强劲扭矩
let forceJitter = Math.sin(time * 0.05) * 2;
dom.forceVectors.setAttribute('transform', `translate(${forceJitter}, 0)`);
} else {
dom.forceVectors.setAttribute('opacity', 0);
}
// 真空管颜色变化
dom.flowLine.setAttribute('stroke', isRigid ? varCol('rigid-main') : varCol('soft-main'));
// 6. 更新 UI 数据与指示器
dom.phase.innerText = phaseText;
dom.state.innerText = isRigid ? "负压 / 刚性咬合" : "常压 / 柔软流动";
dom.state.className = isRigid ? "data-value value-rigid" : "data-value value-soft";
dom.pressure.innerHTML = `${displayPressure.toFixed(1)} <span style="font-size: 12px; color: var(--text-dim);">kPa</span>`;
dom.pressure.style.color = isRigid ? varCol('rigid-main') : '#fff';
dom.depth.innerText = `${displayDepth.toFixed(1)} mm`;
dom.slip.innerText = isRigid ? "0.0 % (无打滑)" : "0.0 %";
dom.slip.style.color = isRigid ? varCol('soft-main') : varCol('text-dim');
// 更新进度条位置
if (isPlaying) {
dom.slider.value = progress * 1000;
}
dom.indicator.innerText = `${(progress * 10).toFixed(1)}s`;
dom.indicator.style.left = `${progress * 100}%`;
}
// 辅助获取 CSS 变量
function varCol(name) {
return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim();
}
// --- 主循环 ---
function loop(timestamp) {
if (isPlaying) {
// 计算流逝时间并累加
let delta = timestamp - lastFrameTime;
// 限制最大 delta 防止休眠唤醒跳跃过大
if (delta > 100) delta = 16;
animTime += delta;
}
lastFrameTime = timestamp;
render(animTime);
requestAnimationFrame(loop);
}
// --- 交互事件绑定 ---
dom.btnPlay.addEventListener('click', () => {
isPlaying = !isPlaying;
lastFrameTime = performance.now();
// 切换图标 (播放/暂停)
dom.iconPlay.setAttribute('d', isPlaying
? "M6 4h4v16H6zm8 0h4v16h-4z" // 暂停图标
: "M8 5v14l11-7z" // 播放图标
);
});
dom.slider.addEventListener('input', (e) => {
isPlaying = false;
animTime = (e.target.value / 1000) * DURATION;
render(animTime);
dom.iconPlay.setAttribute('d', "M8 5v14l11-7z");
});
dom.slider.addEventListener('change', () => {
// 如果需要拖动松开后自动播放,可取消下方注释
// isPlaying = true;
// lastFrameTime = performance.now();
// dom.iconPlay.setAttribute('d', "M6 4h4v16H6zm8 0h4v16h-4z");
});
// 页面加载完成后立即初始化并运行
window.addEventListener('DOMContentLoaded', () => {
initSVG();
lastFrameTime = performance.now();
requestAnimationFrame(loop);
});
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
