独立渲染引擎就绪引擎就绪
<!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>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Space+Grotesk:wght@500;700&display=swap');
:root {
--bg-base: #060913;
--grid-line: rgba(74, 102, 160, 0.15);
--pipe-stroke: #2E4057;
--pipe-fill: rgba(46, 64, 87, 0.3);
--accent-cyan: #00F0FF;
--accent-orange: #FF4D00;
--accent-yellow: #FFD600;
--text-main: #E0E6ED;
--text-dim: #8A99A8;
--hud-bg: rgba(6, 9, 19, 0.85);
--hud-border: rgba(0, 240, 255, 0.3);
--glow-cyan: drop-shadow(0 0 8px rgba(0, 240, 255, 0.6));
--glow-orange: drop-shadow(0 0 12px rgba(255, 77, 0, 0.8));
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--bg-base);
font-family: 'Space Grotesk', system-ui, sans-serif;
color: var(--text-main);
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.workspace {
position: relative;
width: 100vw;
height: 100vh;
max-width: 1600px;
max-height: 900px;
display: flex;
justify-content: center;
align-items: center;
background:
linear-gradient(rgba(6,9,19,0.9), rgba(6,9,19,0.9)),
radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.05) 0%, transparent 60%);
}
svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 0 30px rgba(0,0,0,0.5));
}
/* Technical Grid Pattern */
.grid-pattern {
stroke: var(--grid-line);
stroke-width: 1;
}
/* SVG Element Styles */
.pipe-wall {
fill: var(--pipe-fill);
stroke: var(--pipe-stroke);
stroke-width: 4;
stroke-linecap: round;
}
.centerline {
stroke: var(--text-dim);
stroke-width: 1;
stroke-dasharray: 10, 5, 2, 5;
opacity: 0.5;
}
.valve-seat {
fill: #1A2235;
stroke: var(--accent-cyan);
stroke-width: 2;
}
.valve-plate {
fill: rgba(0, 240, 255, 0.1);
stroke: var(--accent-cyan);
stroke-width: 3;
transition: stroke 0.3s ease;
}
.valve-plate.high-torque {
stroke: var(--accent-orange);
fill: rgba(255, 77, 0, 0.15);
filter: var(--glow-orange);
}
.shaft {
fill: var(--bg-base);
stroke: #FFF;
stroke-width: 3;
}
.linkage-crank {
stroke: var(--accent-orange);
stroke-width: 8;
stroke-linecap: round;
stroke-linejoin: round;
}
.linkage-rod {
stroke: #8A99A8;
stroke-width: 6;
stroke-linecap: round;
stroke-linejoin: round;
}
.linkage-rocker {
stroke: var(--accent-orange);
stroke-width: 8;
stroke-linecap: round;
stroke-linejoin: round;
}
.pivot {
fill: #FFF;
stroke: var(--bg-base);
stroke-width: 2;
}
.j-curve {
fill: none;
stroke: var(--accent-yellow);
stroke-width: 2;
stroke-dasharray: 4 4;
opacity: 0.4;
}
.j-curve-active {
fill: none;
stroke: var(--accent-yellow);
stroke-width: 3;
filter: drop-shadow(0 0 5px var(--accent-yellow));
}
/* HUD UI */
.hud-panel {
position: absolute;
background: var(--hud-bg);
border: 1px solid var(--hud-border);
padding: 24px;
border-radius: 8px;
backdrop-filter: blur(10px);
pointer-events: none;
}
.hud-top-left { top: 40px; left: 40px; }
.hud-bottom-right { bottom: 40px; right: 40px; }
.hud-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--accent-cyan);
margin: 0 0 12px 0;
font-weight: 700;
}
.hud-data {
font-family: 'JetBrains Mono', monospace;
font-size: 28px;
font-weight: 800;
color: #FFF;
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
}
.hud-label {
font-family: 'Space Grotesk', sans-serif;
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent-cyan);
margin-right: 10px;
box-shadow: 0 0 10px var(--accent-cyan);
}
.status-indicator.alert {
background: var(--accent-orange);
box-shadow: 0 0 10px var(--accent-orange);
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 12px;
gap: 40px;
}
.phase-text {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
color: var(--accent-yellow);
height: 20px;
}
/* Dynamic Visual Indicators */
.force-arrow {
fill: none;
stroke: var(--accent-orange);
stroke-width: 3;
marker-end: url(#arrowhead-orange);
opacity: 0;
transition: opacity 0.2s;
}
.detach-arrow {
fill: none;
stroke: var(--accent-cyan);
stroke-width: 3;
marker-end: url(#arrowhead-cyan);
opacity: 0;
transition: opacity 0.2s;
}
.visible { opacity: 1; }
</style>
</head>
<body>
<div class="workspace">
<!-- Main SV Canvas -->
<svg id="simulation" viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- Grid Pattern -->
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" class="grid-pattern" />
<circle cx="0" cy="0" r="1" fill="rgba(255,255,255,0.2)"/>
</pattern>
<!-- Arrow Markers -->
<marker id="arrowhead-orange" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--accent-orange)" />
</marker>
<marker id="arrowhead-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--accent-cyan)" />
</marker>
</defs>
<!-- Background Grid -->
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- Reference Lines & Pipe Structure -->
<g id="static-structure">
<!-- Pipe Centerline -->
<line x1="100" y1="400" x2="1100" y2="400" class="centerline" />
<text x="120" y="390" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">PIPE AXIS (Cp)</text>
<!-- Pipe Walls -->
<path d="M 100 200 L 1100 200 M 100 600 L 1100 600" class="pipe-wall" />
<!-- Flow Direction Hint -->
<path d="M 150 300 L 250 300 M 230 280 L 250 300 L 230 320" stroke="rgba(255,255,255,0.1)" stroke-width="2" fill="none"/>
<path d="M 150 500 L 250 500 M 230 480 L 250 500 L 230 520" stroke="rgba(255,255,255,0.1)" stroke-width="2" fill="none"/>
<!-- Valve Seat Rings (Cross Section) -->
<rect x="730" y="180" width="20" height="40" rx="4" class="valve-seat" />
<rect x="730" y="580" width="20" height="40" rx="4" class="valve-seat" />
<!-- Seat Sealing Plane Line -->
<line x1="750" y1="150" x2="750" y2="650" class="centerline" stroke="var(--accent-cyan)" stroke-opacity="0.3" />
<text x="760" y="170" fill="var(--accent-cyan)" font-family="JetBrains Mono" font-size="12" opacity="0.6">SEALING PLANE</text>
<!-- Shaft Centerline (Vertical offset e2) -->
<line x1="830" y1="300" x2="830" y2="550" class="centerline" stroke-dasharray="4,4" />
</g>
<!-- Mechanism & Kinematics -->
<g id="kinematics">
<!-- Full Trajectory Paths (Calculated via JS) -->
<path id="j-curve-full" class="j-curve" d="" />
<!-- Active Trajectory Trace -->
<path id="j-curve-trace" class="j-curve-active" d="" />
<!-- The Valve Assembly (Rotates around O2) -->
<g id="valve-group">
<!-- Valve Plate Profile (Triple Eccentric Section) -->
<!-- Coordinates are relative to Shaft Center O2 -->
<path id="valve-body" class="valve-plate" d="
M -80 -270
C -60 -270, -20 -150, -20 0
C -20 150, -60 210, -80 210
L -120 210
C -100 150, -80 50, -80 0
C -80 -50, -100 -200, -120 -270
Z"
/>
<!-- Center point of valve plate (showing offset) -->
<circle cx="-80" cy="0" r="4" fill="var(--accent-cyan)" />
<line x1="0" y1="0" x2="-80" y2="0" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="2,2"/>
<line x1="0" y1="0" x2="0" y2="-70" stroke="var(--accent-cyan)" stroke-width="1" stroke-dasharray="2,2"/>
<!-- Dynamic Force Vectors -->
<!-- Compressive Force (Closing) -->
<g id="force-compressive" class="force-arrow">
<line x1="-30" y1="-270" x2="-100" y2="-270" />
<text x="-40" y="-285" fill="var(--accent-orange)" font-family="Space Grotesk" font-size="14" stroke="none" font-weight="bold">HIGH TORQUE SEALING</text>
</g>
<!-- Detachment Velocity (Opening) -->
<g id="force-detach" class="detach-arrow">
<line x1="-80" y1="-270" x2="20" y2="-270" />
<text x="-30" y="-285" fill="var(--accent-cyan)" font-family="Space Grotesk" font-size="14" stroke="none" font-weight="bold">INSTANT DETACHMENT</text>
</g>
<!-- Tip tracking point -->
<circle id="valve-tip" cx="-80" cy="-270" r="5" fill="#FFF" filter="drop-shadow(0 0 5px #FFF)" />
</g>
<!-- Shaft O2 -->
<circle cx="830" cy="470" r="18" class="shaft" />
<circle cx="830" cy="470" r="6" fill="var(--bg-base)" />
<text x="850" y="485" fill="#FFF" font-family="JetBrains Mono" font-size="14" font-weight="bold">O₂</text>
<!-- Motor Shaft O1 -->
<circle cx="280" cy="550" r="22" class="shaft" />
<circle cx="280" cy="550" r="8" fill="var(--bg-base)" />
<text x="240" y="580" fill="#FFF" font-family="JetBrains Mono" font-size="14" font-weight="bold">O₁</text>
<!-- Linkages -->
<line id="link-crank" class="linkage-crank" x1="280" y1="550" x2="400" y2="550" />
<line id="link-rocker" class="linkage-rocker" x1="830" y1="470" x2="700" y2="350" />
<line id="link-rod" class="linkage-rod" x1="400" y1="550" x2="700" y2="350" />
<!-- Joint Pivots -->
<circle id="joint-A" cx="400" cy="550" r="8" class="pivot" />
<circle id="joint-B" cx="700" cy="350" r="8" class="pivot" />
<text id="label-A" x="400" y="530" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">A</text>
<text id="label-B" x="700" y="330" fill="var(--text-dim)" font-family="JetBrains Mono" font-size="12">B</text>
</g>
<!-- Eccentricity Dimension Lines -->
<g stroke="var(--text-dim)" stroke-width="1" font-family="JetBrains Mono" font-size="12" fill="var(--text-dim)">
<!-- Radial Eccentricity e1 -->
<line x1="850" y1="400" x2="850" y2="470" />
<line x1="845" y1="400" x2="855" y2="400" />
<line x1="845" y1="470" x2="855" y2="470" />
<text x="860" y="440">e₁: 70mm (Radial)</text>
<!-- Axial Eccentricity e2 -->
<line x1="750" y1="670" x2="830" y2="670" />
<line x1="750" y1="665" x2="750" y2="675" />
<line x1="830" y1="665" x2="830" y2="675" />
<text x="760" y="690">e₂: 80mm (Axial)</text>
</g>
</svg>
<!-- Overlay UI (HUD) -->
<div class="hud-panel hud-top-left">
<h2 class="hud-title">System Telemetry</h2>
<div class="hud-data">
<span id="ui-status-dot" class="status-indicator"></span>
<span id="ui-mode">INITIALIZING</span>
</div>
<div class="phase-text" id="ui-phase-desc">System booting...</div>
<div class="data-row">
<div>
<div class="hud-label">Mechanism State</div>
<div class="hud-data" style="font-size: 20px;"><span id="ui-angle-crank">0</span>°</div>
</div>
<div>
<div class="hud-label">Valve Angle</div>
<div class="hud-data" style="font-size: 20px;"><span id="ui-angle-valve">0</span>°</div>
</div>
</div>
</div>
<div class="hud-panel hud-bottom-right">
<h2 class="hud-title">IFR: Resource Utilization</h2>
<div style="max-width: 300px; color: var(--text-dim); font-size: 13px; line-height: 1.5;">
By integrating an <strong>external crank-rocker mechanism</strong> with a <strong>triple-eccentric geometric core</strong>, uniform actuation is converted into a non-linear <span style="color:var(--accent-yellow)">'J' shaped trajectory</span>.
<br><br>
<span style="color:var(--accent-cyan)">■ Instant Detachment</span><br>
<span style="color:var(--accent-orange)">■ High-Torque Perpendicular Sealing</span>
</div>
<div class="data-row" style="margin-top: 20px;">
<div>
<div class="hud-label">Torque Multiplier (Est.)</div>
<div class="hud-data" style="color: var(--accent-orange);"><span id="ui-torque">1.0</span>x</div>
</div>
</div>
</div>
</div>
<script>
/**
* Kinematic Simulation of Crank-Rocker + Triple Eccentric Butterfly Valve
* Solves exact geometry frame-by-frame for high-fidelity visualization.
*/
// --- Configuration & Parameters ---
const O1 = { x: 280, y: 550 }; // Crank Motor Axis
const O2 = { x: 830, y: 470 }; // Valve Shaft Axis
// Mechanism Link Lengths
const L1 = 140; // Crank length
const L3 = 180; // Rocker length
const L2 = Math.hypot(O2.x - O1.x, O2.y - O1.y) + 20; // Coupler length (calculated for specific geometry)
// Valve Local Geometry (Relative to O2)
// Sealing edge is global x=750. O2 is at x=830. Local x = -80.
const tipLocal = { x: -80, y: -270 }; // Top sealing tip relative to O2
// Animation Timing
const CYCLE_DURATION = 8000; // ms per full open-close cycle
const ANGLE_OPEN = -30 * (Math.PI / 180); // Crank angle when fully open
const ANGLE_CLOSE = 175 * (Math.PI / 180); // Crank angle when fully closed (near dead center)
// Reference state for valve rotation mapping
// We want the valve to be vertically aligned (sealing) when mechanism is closed.
let theta3_closed = 0;
// --- DOM Elements ---
const elCrank = document.getElementById('link-crank');
const elRod = document.getElementById('link-rod');
const elRocker = document.getElementById('link-rocker');
const elJointA = document.getElementById('joint-A');
const elJointB = document.getElementById('joint-B');
const elLabelA = document.getElementById('label-A');
const elLabelB = document.getElementById('label-B');
const elValveGroup = document.getElementById('valve-group');
const elValveBody = document.getElementById('valve-body');
const elJCurveFull = document.getElementById('j-curve-full');
const elJCurveTrace = document.getElementById('j-curve-trace');
// HUD Elements
const uiMode = document.getElementById('ui-mode');
const uiPhaseDesc = document.getElementById('ui-phase-desc');
const uiStatusDot = document.getElementById('ui-status-dot');
const uiAngleCrank = document.getElementById('ui-angle-crank');
const uiAngleValve = document.getElementById('ui-angle-valve');
const uiTorque = document.getElementById('ui-torque');
const forceCompressive = document.getElementById('force-compressive');
const forceDetach = document.getElementById('force-detach');
// --- Core Kinematic Solver ---
function solveKinematics(theta1) {
// Point A (End of Crank)
// SVG coordinate system: y increases downwards. Standard math puts y upwards.
// We use SVG logic directly: x = r*cos, y = -r*sin (for visually standard rotation)
const xA = O1.x + L1 * Math.cos(theta1);
const yA = O1.y - L1 * Math.sin(theta1);
// Distance from A to O2
const d = Math.hypot(O2.x - xA, O2.y - yA);
// Check if mechanism binds (d must be <= L2+L3 and >= |L2-L3|)
if (d > L2 + L3 || d < Math.abs(L2 - L3)) {
console.warn("Mechanism bound/invalid geometry");
return null; // Should not happen with current hardcoded parameters
}
// Law of Cosines to find angle of rocker (theta3)
// Triangle A-O2-B. Sides are d, L2, L3.
let cosGamma = (L3*L3 + d*d - L2*L2) / (2 * L3 * d);
// Prevent precision errors causing NaN
cosGamma = Math.max(-1, Math.min(1, cosGamma));
const gamma = Math.acos(cosGamma);
// Angle of line A-O2
const angleAO2 = Math.atan2(yA - O2.y, xA - O2.x);
// Assembly mode: We choose one of the two intersections.
// Given our layout, we want the "upper" assembly.
let theta3 = angleAO2 - Math.PI + gamma;
// Point B (End of Rocker)
const xB = O2.x + L3 * Math.cos(theta3);
const yB = O2.y - L3 * Math.sin(theta3);
return {
A: { x: xA, y: yA },
B: { x: xB, y: yB },
theta3: theta3
};
}
// Calculate reference theta3 for closed state
const closedState = solveKinematics(ANGLE_CLOSE);
if(closedState) {
theta3_closed = closedState.theta3;
}
// --- Pre-compute Trajectory Path ---
function generateTrajectoryPath() {
let pathD = "";
let steps = 100;
let p = [];
for(let i=0; i<=steps; i++) {
let t = i / steps;
let theta1 = ANGLE_OPEN + (ANGLE_CLOSE - ANGLE_OPEN) * t;
let state = solveKinematics(theta1);
if(!state) continue;
// Map rocker rotation to valve rotation
let dTheta = state.theta3 - theta3_closed;
// Calculate global position of tip
// Rotate local tip coordinate by -dTheta (because SVG rotation is clockwise for positive angles, but we defined math y-up. Let's align with SVG rotate transform)
// Local to global rotation: x' = x*cos(a) - y*sin(a), y' = x*sin(a) + y*cos(a)
// Since we use SVG `transform="rotate(-deg)"`, we apply it here to find points
let angleRad = -dTheta; // Negate because SVG transform rotate is clockwise
let tipX_rot = tipLocal.x * Math.cos(angleRad) - tipLocal.y * Math.sin(angleRad);
let tipY_rot = tipLocal.x * Math.sin(angleRad) + tipLocal.y * Math.cos(angleRad);
let tipGlobalX = O2.x + tipX_rot;
let tipGlobalY = O2.y + tipY_rot;
p.push({x: tipGlobalX, y: tipGlobalY});
}
pathD += `M ${p[0].x} ${p[0].y} `;
for(let i=1; i<p.length; i++) {
pathD += `L ${p[i].x} ${p[i].y} `;
}
elJCurveFull.setAttribute('d', pathD);
return p;
}
const tracePoints = generateTrajectoryPath();
// --- Animation Loop ---
function updateSimulation(time) {
// Calculate normalized progress (0 to 1 back to 0)
let phase = (time % CYCLE_DURATION) / CYCLE_DURATION;
// Easing function to make motion look natural (motor speeds up/slows down slightly, pauses at ends)
// Custom easing: smoothstep with dwells
let t = 0;
if (phase < 0.4) {
// Closing (0 to 0.4)
t = phase / 0.4;
t = t * t * (3 - 2 * t); // Smoothstep
} else if (phase < 0.5) {
// Dwell Closed (0.4 to 0.5)
t = 1;
} else if (phase < 0.9) {
// Opening (0.5 to 0.9)
t = 1 - ((phase - 0.5) / 0.4);
t = t * t * (3 - 2 * t); // Smoothstep
} else {
// Dwell Open (0.9 to 1.0)
t = 0;
}
// Current Crank Angle
const currentTheta1 = ANGLE_OPEN + (ANGLE_CLOSE - ANGLE_OPEN) * t;
// Solve kinematics
const state = solveKinematics(currentTheta1);
if(!state) return;
// Update Linkage SVGs
elCrank.setAttribute('x2', state.A.x);
elCrank.setAttribute('y2', state.A.y);
elRocker.setAttribute('x2', state.B.x);
elRocker.setAttribute('y2', state.B.y);
elRod.setAttribute('x1', state.A.x);
elRod.setAttribute('y1', state.A.y);
elRod.setAttribute('x2', state.B.x);
elRod.setAttribute('y2', state.B.y);
elJointA.setAttribute('cx', state.A.x);
elJointA.setAttribute('cy', state.A.y);
elLabelA.setAttribute('x', state.A.x + 10);
elLabelA.setAttribute('y', state.A.y - 10);
elJointB.setAttribute('cx', state.B.x);
elJointB.setAttribute('cy', state.B.y);
elLabelB.setAttribute('x', state.B.x + 10);
elLabelB.setAttribute('y', state.B.y - 10);
// Update Valve Assembly Rotation
// Calculate required rotation in degrees.
// state.theta3 is in radians. SVG rotate expects degrees clockwise.
let dThetaRad = state.theta3 - theta3_closed;
let dThetaDeg = -(dThetaRad * 180 / Math.PI); // Negative because math y is up, SVG y is down
elValveGroup.setAttribute('transform', `translate(${O2.x}, ${O2.y}) rotate(${dThetaDeg}) translate(${-O2.x}, ${-O2.y})`);
// Update Trace Curve
// Find closest point index based on t
let traceIdx = Math.floor(t * (tracePoints.length - 1));
if (traceIdx > 0) {
let activePath = `M ${tracePoints[0].x} ${tracePoints[0].y} `;
for(let i=1; i<=traceIdx; i++) {
activePath += `L ${tracePoints[i].x} ${tracePoints[i].y} `;
}
elJCurveTrace.setAttribute('d', activePath);
} else {
elJCurveTrace.setAttribute('d', "");
}
// --- Calculate Torque & UI State Logic ---
// Determine instantaneous transmission ratio (kinematic advantage)
// Derivative dt3/dt1 approximated numerically
let delta = 0.01;
let s2 = solveKinematics(currentTheta1 + delta);
let ratio = 1.0;
if(s2) {
ratio = Math.abs(delta / (s2.theta3 - state.theta3));
}
// Format ratio
let displayTorque = ratio > 50 ? "MAX" : ratio.toFixed(2);
uiTorque.innerText = displayTorque;
// State Machine for UI and Visuals
let crankDeg = (currentTheta1 * 180 / Math.PI).toFixed(0);
let valveDeg = Math.abs(dThetaDeg).toFixed(1);
uiAngleCrank.innerText = crankDeg;
uiAngleValve.innerText = valveDeg;
if (t > 0.95 && phase < 0.5) {
// SEALS ENGAGED (High Torque)
uiMode.innerText = "SEALS ENGAGED";
uiMode.style.color = "var(--accent-orange)";
uiPhaseDesc.innerText = "Non-linear leverage maximizing pressure";
uiStatusDot.className = "status-indicator alert";
elValveBody.classList.add("high-torque");
forceCompressive.classList.add("visible");
forceDetach.classList.remove("visible");
} else if (t < 0.05 && phase > 0.5) {
// FULLY OPEN
uiMode.innerText = "FLOW CLEARED";
uiMode.style.color = "var(--accent-cyan)";
uiPhaseDesc.innerText = "Valve parked parallel to flow";
uiStatusDot.className = "status-indicator";
elValveBody.classList.remove("high-torque");
forceCompressive.classList.remove("visible");
forceDetach.classList.remove("visible");
} else if (phase >= 0.5 && phase <= 0.6 && t < 0.9) {
// INITIAL OPENING (Detachment phase)
uiMode.innerText = "OPENING";
uiMode.style.color = "var(--text-main)";
uiPhaseDesc.innerText = "J-Curve execution: Pulling away from seat";
uiStatusDot.className = "status-indicator";
elValveBody.classList.remove("high-torque");
forceCompressive.classList.remove("visible");
forceDetach.classList.add("visible");
} else if (phase < 0.4) {
// CLOSING
uiMode.innerText = "CLOSING";
uiMode.style.color = "var(--text-main)";
uiPhaseDesc.innerText = "High-speed sweep approach";
uiStatusDot.className = "status-indicator";
elValveBody.classList.remove("high-torque");
forceCompressive.classList.remove("visible");
forceDetach.classList.remove("visible");
} else {
// Default transit
elValveBody.classList.remove("high-torque");
forceCompressive.classList.remove("visible");
forceDetach.classList.remove("visible");
}
requestAnimationFrame(updateSimulation);
}
// Start simulation immediately
requestAnimationFrame(updateSimulation);
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
