独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管内探伤机器人 - 最终理想解 (IFR) 自适应机制</title>
<style>
:root {
--bg-color: #060913;
--grid-color: rgba(0, 243, 255, 0.05);
--pipe-wall: #111827;
--pipe-stroke: #1f2937;
--neon-cyan: #00f3ff;
--neon-pink: #ff0055;
--acid-green: #ccff00;
--text-main: #8be9fd;
--text-dim: #6272a4;
--panel-bg: rgba(6, 9, 19, 0.85);
}
* { box-sizing: border-box; }
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
background-color: var(--bg-color);
color: var(--text-main);
font-family: 'Consolas', 'Courier New', monospace;
overflow: hidden; /* Prevent scrolling */
}
/* Tactical Scanline Overlay */
.scanlines {
position: absolute; inset: 0; z-index: 100; pointer-events: none;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.25) 50%,
rgba(0, 0, 0, 0.25)
);
background-size: 100% 4px;
}
/* Telemetry HUD - Kept to edges/corners */
.hud {
position: absolute; z-index: 50; padding: 16px;
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 243, 255, 0.2);
background: var(--panel-bg);
box-shadow: 0 0 20px rgba(0, 243, 255, 0.05);
}
.hud-top-left { top: 30px; left: 30px; width: 320px; border-left: 4px solid var(--neon-cyan); }
.hud-top-right { top: 30px; right: 30px; width: 280px; border-right: 4px solid var(--acid-green); }
.hud-bottom-right { bottom: 80px; right: 30px; width: 280px; border-right: 4px solid var(--neon-pink); }
h1 {
font-size: 16px; margin: 0 0 12px 0;
color: #fff; text-transform: uppercase;
letter-spacing: 1px; font-weight: bold;
display: flex; align-items: center; gap: 8px;
}
h1::before { content: ''; display: block; width: 8px; height: 8px; background: var(--neon-cyan); box-shadow: 0 0 10px var(--neon-cyan); }
.data-row {
display: flex; justify-content: space-between;
font-size: 13px; margin-bottom: 8px;
border-bottom: 1px dashed rgba(139, 233, 253, 0.15); padding-bottom: 4px;
}
.data-row span:first-child { color: var(--text-dim); }
.val-cyan { color: var(--neon-cyan); font-weight: bold; }
.val-green { color: var(--acid-green); font-weight: bold; }
.val-pink { color: var(--neon-pink); font-weight: bold; }
/* Interactive Controls */
.control-panel {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
z-index: 60; width: 50%; max-width: 600px;
background: var(--panel-bg); padding: 12px 24px;
border: 1px solid rgba(0, 243, 255, 0.2); border-radius: 4px;
display: flex; flex-direction: column; align-items: center;
}
.slider-container { width: 100%; display: flex; align-items: center; gap: 15px; }
input[type="range"] {
flex-grow: 1; appearance: none; background: rgba(0,243,255,0.1); height: 4px; border-radius: 2px; outline: none;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none; width: 16px; height: 16px; background: var(--neon-cyan); cursor: ew-resize; box-shadow: 0 0 10px var(--neon-cyan);
}
.control-label { font-size: 12px; color: var(--text-dim); margin-top: 8px; letter-spacing: 1px; }
/* SVG Canvas */
svg { width: 100%; height: 100%; display: block; }
</style>
</head>
<body>
<div class="scanlines"></div>
<!-- HUD Elements -->
<div class="hud hud-top-left">
<h1>最终理想解 (IFR) 架构</h1>
<div class="data-row"><span>设计目标</span><span class="val-cyan">消解过弯卡死与探头间隙波动</span></div>
<div class="data-row"><span>核心机制</span><span class="val-cyan">万向节连接 + 摇臂自定心</span></div>
<div class="data-row"><span>管道规格</span><span class="val-cyan">φ60-120mm / R≥1.5D</span></div>
<div class="data-row"><span>当前管径适配</span><span class="val-pink">140 mm (展示比例)</span></div>
</div>
<div class="hud hud-top-right">
<h1 style="color:var(--acid-green)"><span style="background:var(--acid-green)"></span>探伤滑靴状态</h1>
<div class="data-row"><span>壁面贴合度</span><span class="val-green">100% 理想跟踪</span></div>
<div class="data-row"><span>恒力弹簧下压</span><span class="val-green">2.50 N</span></div>
<div class="data-row"><span>涡流探头间隙</span><span class="val-green" id="probe-gap">0.50 ±0.0 mm</span></div>
</div>
<div class="hud hud-bottom-right">
<h1 style="color:var(--neon-pink)"><span style="background:var(--neon-pink)"></span>动力学实时遥测</h1>
<div class="data-row"><span>前摇臂压缩力</span><span class="val-pink" id="force-front">5.5 N</span></div>
<div class="data-row"><span>后摇臂压缩力</span><span class="val-pink" id="force-rear">5.5 N</span></div>
<div class="data-row"><span>万向节偏转角度</span><span class="val-cyan" id="joint-angle">0.0°</span></div>
<div class="data-row"><span>过弯状态</span><span class="val-pink" id="bend-status">直线定心</span></div>
</div>
<div class="control-panel">
<div class="slider-container">
<span style="font-size: 12px; color: var(--neon-cyan)">START</span>
<input type="range" id="progress-slider" min="0" max="100" step="0.1" value="0">
<span style="font-size: 12px; color: var(--neon-cyan)">END</span>
</div>
<div class="control-label">动画自动播放中。拖动滑块可手动控制过弯进度 (释放后继续)</div>
</div>
<!-- Main SVG Animation Container -->
<svg id="canvas" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
<defs>
<!-- Glow Filters -->
<filter id="glow-cyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur" />
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-pink" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="glow-green" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur" />
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<!-- Background Grid Pattern -->
<pattern id="bg-grid" width="60" height="60" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 60" fill="none" stroke="var(--grid-color)" stroke-width="1"/>
<circle cx="0" cy="0" r="1.5" fill="rgba(0,243,255,0.2)" />
</pattern>
<!-- Pipe Wall Gradient -->
<linearGradient id="wall-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1f2937" />
<stop offset="10%" stop-color="#111827" />
<stop offset="90%" stop-color="#111827" />
<stop offset="100%" stop-color="#1f2937" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg-grid)" />
<!--
Centerline Path (Invisible, used for JS math):
Straight up from bottom, 90 degree curve right (R=300), straight right.
-->
<path id="track" d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300" fill="none" stroke="none" />
<!-- Pipe Visual Rendering (Trick: Thick stroke minus thinner stroke creates parallel offset walls) -->
<g id="pipe-visuals">
<!-- Outer Wall Stroke -->
<path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300"
fill="none" stroke="#2d3748" stroke-width="144" />
<!-- Inner Cavity Fill -->
<path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300"
fill="none" stroke="#0a0e17" stroke-width="140" />
<!-- Centerline Indicator -->
<path d="M 350 1100 L 350 600 A 300 300 0 0 1 650 300 L 1900 300"
fill="none" stroke="rgba(0,243,255,0.1)" stroke-width="2" stroke-dasharray="10 10" />
</g>
<!-- Dynamic Robot Elements Group -->
<g id="robot-system">
<!-- Springs drawn first (underneath bodies) -->
<path id="spring-f-top" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
<path id="spring-f-bot" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
<path id="spring-r-top" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
<path id="spring-r-bot" fill="none" stroke="var(--neon-pink)" stroke-width="2" filter="url(#glow-pink)"/>
<!-- Probe Constant Force Spring -->
<path id="spring-probe" fill="none" stroke="var(--acid-green)" stroke-width="2" />
<!-- Front Module Body -->
<g id="mod-front">
<rect x="-55" y="-22" width="110" height="44" rx="4" fill="#111" stroke="var(--neon-cyan)" stroke-width="2" />
<rect x="-45" y="-12" width="90" height="24" fill="#1a1a1a" stroke="#333" />
<circle cx="40" cy="0" r="4" fill="var(--neon-cyan)" filter="url(#glow-cyan)"/> <!-- Direction Indicator -->
</g>
<!-- Rear Module Body -->
<g id="mod-rear">
<rect x="-55" y="-22" width="110" height="44" rx="4" fill="#111" stroke="var(--neon-cyan)" stroke-width="2" />
<rect x="-45" y="-12" width="90" height="24" fill="#1a1a1a" stroke="#333" />
<!-- Probe guiding pins base inside rear module -->
<rect x="-40" y="10" width="24" height="14" fill="#222" stroke="var(--acid-green)" stroke-width="1" />
</g>
<!-- Wheels (Polyurethane coated) -->
<circle id="wheel-f-top" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
<circle id="wheel-f-bot" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
<circle id="wheel-r-top" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
<circle id="wheel-r-bot" r="10" fill="var(--neon-pink)" stroke="#fff" stroke-width="2" />
<!-- Eddy Current Probe Sliding Shoe -->
<g id="probe-shoe">
<path d="M -15,0 L 15,0 L 10,-8 L -10,-8 Z" fill="var(--acid-green)" filter="url(#glow-green)"/>
<!-- Scanning Wave Effect -->
<path id="probe-wave-1" d="M -12,4 L 12,4" fill="none" stroke="var(--acid-green)" stroke-width="1.5" stroke-dasharray="2 2" />
<path id="probe-wave-2" d="M -8,8 L 8,8" fill="none" stroke="var(--acid-green)" stroke-width="1" stroke-dasharray="2 2" />
</g>
<!-- Universal Joint (Cross Hinge) -->
<g id="universal-joint">
<circle cx="0" cy="0" r="10" fill="#222" stroke="var(--acid-green)" stroke-width="3" filter="url(#glow-green)"/>
<circle cx="0" cy="0" r="4" fill="var(--acid-green)" />
</g>
</g>
<!-- Dynamic Visual Leaders (Connecting HUD logic to moving parts) -->
<g id="leaders" stroke="#ffffff" stroke-width="1" stroke-dasharray="4 4" opacity="0.4">
<line id="leader-joint" x1="600" y1="70" x2="0" y2="0" />
<line id="leader-probe" x1="1200" y1="800" x2="0" y2="0" />
<circle id="target-joint" r="3" fill="#fff" />
<circle id="target-probe" r="3" fill="#fff" />
</g>
<text x="605" y="65" fill="#fff" font-size="12" opacity="0.7">万向铰链 (蛇形自适应)</text>
<text x="1205" y="815" fill="#fff" font-size="12" opacity="0.7">弹性滑靴 (恒力贴壁)</text>
</svg>
<script>
const track = document.getElementById('track');
const trackLen = track.getTotalLength();
const slider = document.getElementById('progress-slider');
// Robot parameters
const PIPE_RADIUS = 70; // Half of 140 width
const MOD_HALF_LEN = 55; // Half length of rigid module
const SPRING_NOMINAL = 48; // Nominal distance from module center to wall
// State variables
let isDragging = false;
let autoProgress = 100; // Start position (distance along path)
const SPEED = 1.2; // Pixels per frame
const MIN_D = 200; // Safe start bound to fit entire robot
const MAX_D = trackLen - 150; // Safe end bound
// DOM Elements
const elJoint = document.getElementById('universal-joint');
const elModFront = document.getElementById('mod-front');
const elModRear = document.getElementById('mod-rear');
const elWFT = document.getElementById('wheel-f-top');
const elWFB = document.getElementById('wheel-f-bot');
const elWRT = document.getElementById('wheel-r-top');
const elWRB = document.getElementById('wheel-r-bot');
const elProbe = document.getElementById('probe-shoe');
const elSFT = document.getElementById('spring-f-top');
const elSFB = document.getElementById('spring-f-bot');
const elSRT = document.getElementById('spring-r-top');
const elSRB = document.getElementById('spring-r-bot');
const elSProbe = document.getElementById('spring-probe');
const leaderJoint = document.getElementById('leader-joint');
const leaderProbe = document.getElementById('leader-probe');
const targetJoint = document.getElementById('target-joint');
const targetProbe = document.getElementById('target-probe');
// UI Elements
const uiJointAngle = document.getElementById('joint-angle');
const uiForceFront = document.getElementById('force-front');
const uiForceRear = document.getElementById('force-rear');
const uiBendStatus = document.getElementById('bend-status');
// Helper: Generate a zigzag spring path
function getSpringPath(x1, y1, x2, y2, coils = 5, width = 8) {
const dx = x2 - x1, dy = y2 - y1;
const dist = Math.hypot(dx, dy);
const nx = -dy / dist, ny = dx / dist;
const step = dist / (coils * 2 + 1);
let path = `M ${x1} ${y1} `;
for (let i = 1; i <= coils * 2; i++) {
const cx = x1 + (dx / dist) * (i * step);
const cy = y1 + (dy / dist) * (i * step);
const mult = i % 2 === 0 ? 1 : -1;
path += `L ${cx + nx * width * mult} ${cy + ny * width * mult} `;
}
path += `L ${x2} ${y2}`;
return path;
}
// Helper: Calculate normal vector and position on track
function getTrackData(d) {
const p = track.getPointAtLength(d);
const pNext = track.getPointAtLength(Math.min(d + 1, trackLen));
const pPrev = track.getPointAtLength(Math.max(d - 1, 0));
const dx = pNext.x - pPrev.x;
const dy = pNext.y - pPrev.y;
const len = Math.hypot(dx, dy);
return {
x: p.x, y: p.y,
dx: dx/len, dy: dy/len,
nx: -dy/len, ny: dx/len // Left normal
};
}
function updateFrame(d_joint) {
// Kinematics:
// Joint is at d_joint
// Front module attaches to joint, nose extends forward. Midpoint is at d_joint + MOD_HALF_LEN
// Rear module attaches to joint, tail extends backward. Midpoint is at d_joint - MOD_HALF_LEN
const pdJoint = getTrackData(d_joint);
const pdFMid = getTrackData(d_joint + MOD_HALF_LEN);
const pdRMid = getTrackData(d_joint - MOD_HALF_LEN);
const pdFNose = getTrackData(d_joint + MOD_HALF_LEN * 2);
const pdRTail = getTrackData(d_joint - MOD_HALF_LEN * 2);
// Update Joint
elJoint.setAttribute('transform', `translate(${pdJoint.x}, ${pdJoint.y})`);
// Calculate Module Rotations (Rigid body vectors)
const angleFront = Math.atan2(pdFNose.y - pdJoint.y, pdFNose.x - pdJoint.x) * 180 / Math.PI;
const angleRear = Math.atan2(pdJoint.y - pdRTail.y, pdJoint.x - pdRTail.x) * 180 / Math.PI;
elModFront.setAttribute('transform', `translate(${pdFMid.x}, ${pdFMid.y}) rotate(${angleFront})`);
elModRear.setAttribute('transform', `translate(${pdRMid.x}, ${pdRMid.y}) rotate(${angleRear})`);
// Compute relative joint angle for Telemetry
let relAngle = Math.abs(angleFront - angleRear);
if (relAngle > 180) relAngle = 360 - relAngle;
uiJointAngle.innerText = relAngle.toFixed(1) + '°';
if(relAngle > 5) {
uiBendStatus.innerText = "万向铰链偏转 / 过弯补偿";
uiBendStatus.className = "val-pink";
} else {
uiBendStatus.innerText = "直线段 / 完美自定心";
uiBendStatus.className = "val-cyan";
}
// --- Suspension & Wheels Calculations ---
// Function to attach wheel and spring at a given midpoint
function applySuspension(midData, elWT, elWB, elST, elSB, cx, cy, angle, isFront) {
// Top/Bottom wall coords using local track normal
const wallTX = midData.x + midData.nx * (PIPE_RADIUS - 10); // 10 is wheel radius
const wallTY = midData.y + midData.ny * (PIPE_RADIUS - 10);
const wallBX = midData.x - midData.nx * (PIPE_RADIUS - 10);
const wallBY = midData.y - midData.ny * (PIPE_RADIUS - 10);
elWT.setAttribute('cx', wallTX); elWT.setAttribute('cy', wallTY);
elWB.setAttribute('cx', wallBX); elWB.setAttribute('cy', wallBY);
// Body attachment points (top and bottom of module body)
const rad = angle * Math.PI / 180;
// Module normal vector
const bnx = -Math.sin(rad), bny = Math.cos(rad);
const attachTopX = cx + bnx * 22; const attachTopY = cy + bny * 22;
const attachBotX = cx - bnx * 22; const attachBotY = cy - bny * 22;
elST.setAttribute('d', getSpringPath(attachTopX, attachTopY, wallTX, wallTY, 4, 6));
elSB.setAttribute('d', getSpringPath(attachBotX, attachBotY, wallBX, wallBY, 4, 6));
// Force telemetry simulation (Spring compression)
const distT = Math.hypot(wallTX - attachTopX, wallTY - attachTopY);
const force = (5.5 + (SPRING_NOMINAL - distT) * 0.1).toFixed(1);
if(isFront) uiForceFront.innerText = `${force} N`;
else uiForceRear.innerText = `${force} N`;
// Color intensity based on compression
const compressionVal = Math.max(0, SPRING_NOMINAL - distT);
if (compressionVal > 2) {
elST.setAttribute('stroke', '#ffcc00'); // Warning/Compression color
elSB.setAttribute('stroke', '#ffcc00');
} else {
elST.setAttribute('stroke', 'var(--neon-pink)');
elSB.setAttribute('stroke', 'var(--neon-pink)');
}
}
applySuspension(pdFMid, elWFT, elWFB, elSFT, elSFB, pdFMid.x, pdFMid.y, angleFront, true);
applySuspension(pdRMid, elWRT, elWRB, elSRT, elSRB, pdRMid.x, pdRMid.y, angleRear, false);
// --- Probe Sliding Shoe ---
// Probe is mounted on the rear module, near the tail
const probeD = d_joint - MOD_HALF_LEN * 1.5;
const pdProbe = getTrackData(probeD);
// Probe sits on outer (bottom) wall
const probeWallX = pdProbe.x - pdProbe.nx * (PIPE_RADIUS - 2); // 2px visual gap tolerance
const probeWallY = pdProbe.y - pdProbe.ny * (PIPE_RADIUS - 2);
// Align probe shoe with wall tangent
const probeAngle = Math.atan2(pdProbe.dy, pdProbe.dx) * 180 / Math.PI;
elProbe.setAttribute('transform', `translate(${probeWallX}, ${probeWallY}) rotate(${probeAngle})`);
// Probe constant force spring connects from rear body tail to shoe
const radRear = angleRear * Math.PI / 180;
const rbnx = -Math.sin(radRear), rbny = Math.cos(radRear);
// Probe anchor on module body (near tail, bottom side)
const tailBodyX = pdRTail.x + pdProbe.dx * 10 + rbnx * (-22);
const tailBodyY = pdRTail.y + pdProbe.dy * 10 + rbny * (-22);
elSProbe.setAttribute('d', getSpringPath(tailBodyX, tailBodyY, probeWallX, probeWallY, 3, 4));
// Update UI Leaders
targetJoint.setAttribute('cx', pdJoint.x); targetJoint.setAttribute('cy', pdJoint.y);
leaderJoint.setAttribute('x2', pdJoint.x); leaderJoint.setAttribute('y2', pdJoint.y);
targetProbe.setAttribute('cx', probeWallX); targetProbe.setAttribute('cy', probeWallY);
leaderProbe.setAttribute('x2', probeWallX); leaderProbe.setAttribute('y2', probeWallY);
// Simple wave animation based on time
const waveScale = (Date.now() % 1000) / 1000;
document.getElementById('probe-wave-1').setAttribute('transform', `translate(0, ${waveScale * 6})`);
document.getElementById('probe-wave-1').setAttribute('opacity', 1 - waveScale);
document.getElementById('probe-wave-2').setAttribute('transform', `translate(0, ${waveScale * 12})`);
document.getElementById('probe-wave-2').setAttribute('opacity', 1 - waveScale);
}
// Main Animation Loop
function animate() {
if (!isDragging) {
autoProgress += SPEED;
if (autoProgress > MAX_D) {
autoProgress = MIN_D; // Loop back
}
// Sync slider with auto progress
const percent = ((autoProgress - MIN_D) / (MAX_D - MIN_D)) * 100;
slider.value = percent;
} else {
// If dragging, calculate autoProgress from slider value
const percent = slider.value;
autoProgress = MIN_D + (percent / 100) * (MAX_D - MIN_D);
}
updateFrame(autoProgress);
requestAnimationFrame(animate);
}
// Slider Interaction Setup
slider.addEventListener('mousedown', () => isDragging = true);
slider.addEventListener('touchstart', () => isDragging = true, {passive: true});
slider.addEventListener('mouseup', () => isDragging = false);
slider.addEventListener('touchend', () => isDragging = false);
slider.addEventListener('input', (e) => {
const percent = e.target.value;
autoProgress = MIN_D + (percent / 100) * (MAX_D - MIN_D);
updateFrame(autoProgress); // Force immediate visual update
});
// Initialize immediately
updateFrame(MIN_D);
requestAnimationFrame(animate);
</script>
</body>
</html>
积分规则:每轮对话扣10分
等待动画代码生成...
