独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRIZ IFR Solution: Articulated Flexible Track Chassis</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700;800&family=Space+Grotesk:wght@400;700&display=swap');
:root {
--bg-color: #050914;
--grid-color: rgba(0, 229, 255, 0.05);
--stair-color: #0f1626;
--stair-stroke: #1e3a5f;
--primary: #00E5FF;
--warning: #FF3D00;
--success: #00E676;
--text-main: #E0F7FA;
--text-muted: #4DD0E1;
--font-mono: 'JetBrains Mono', monospace;
--font-display: 'Space Grotesk', sans-serif;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--bg-color);
color: var(--text-main);
overflow: hidden;
font-family: var(--font-display);
}
#canvas-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG Element Styling */
.track-belt {
fill: none;
stroke: var(--primary);
stroke-width: 4;
stroke-linejoin: round;
stroke-linecap: round;
transition: stroke 0.3s;
}
.track-inner-dash {
fill: none;
stroke: #fff;
stroke-width: 2;
stroke-dasharray: 6 12;
opacity: 0.6;
}
.chassis-segment {
fill: rgba(0, 229, 255, 0.05);
stroke: rgba(0, 229, 255, 0.4);
stroke-width: 2;
}
.wheel {
fill: #0a1128;
stroke: var(--primary);
stroke-width: 2;
}
.wheel-spoke {
stroke: rgba(0, 229, 255, 0.5);
stroke-width: 2;
}
.stair-polygon {
fill: var(--stair-color);
stroke: var(--stair-stroke);
stroke-width: 3;
}
.envelope-path {
fill: none;
stroke: rgba(0, 229, 255, 0.2);
stroke-width: 1;
stroke-dasharray: 4 4;
}
.hinge-joint {
fill: var(--bg-color);
stroke: var(--primary);
stroke-width: 2;
}
/* Dynamic Classes applied via JS */
.active-hinge {
stroke: var(--warning);
filter: drop-shadow(0 0 10px var(--warning));
}
.active-text {
fill: var(--warning) !important;
font-weight: bold;
text-shadow: 0 0 8px rgba(255, 61, 0, 0.6);
}
.hud-title { font-size: 28px; font-weight: 800; fill: #fff; letter-spacing: 2px; }
.hud-subtitle { font-size: 14px; fill: var(--text-muted); font-family: var(--font-mono); }
.hud-label { font-size: 12px; fill: var(--text-muted); font-family: var(--font-mono); }
.hud-value { font-size: 16px; fill: var(--primary); font-family: var(--font-mono); font-weight: 700; }
.hud-unit { font-size: 10px; fill: rgba(0,229,255,0.5); }
.callout-line {
stroke: var(--warning);
stroke-width: 1;
stroke-dasharray: 2 4;
opacity: 0;
transition: opacity 0.2s;
}
.callout-line.visible { opacity: 0.8; }
/* UI Overlay */
.overlay {
position: absolute;
top: 40px;
left: 40px;
pointer-events: none;
}
.triz-badge {
display: inline-block;
background: rgba(0, 229, 255, 0.1);
border: 1px solid rgba(0, 229, 255, 0.3);
color: var(--primary);
padding: 4px 12px;
font-family: var(--font-mono);
font-size: 12px;
border-radius: 20px;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.1);
}
h1 { margin: 0 0 8px 0; font-size: 2.5rem; letter-spacing: -0.5px; }
p { margin: 0; font-family: var(--font-mono); color: var(--text-muted); font-size: 0.9rem; max-width: 500px; line-height: 1.5; }
/* Glossy scanline effect */
.scanline {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.25) 51%);
background-size: 100% 4px;
pointer-events: none;
z-index: 10;
opacity: 0.3;
}
</style>
</head>
<body>
<div id="canvas-container">
<div class="scanline"></div>
<div class="overlay">
<div class="triz-badge">TRIZ IFR Solution</div>
<h1>Articulated Soft-Track</h1>
<p>Eliminating step-slip contradiction via 3-segment pitch hinges and dynamic 150N track tensioning. The chassis continuously morphs to the terrain envelope.</p>
</div>
<svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--grid-color)" stroke-width="1"/>
</pattern>
<pattern id="grid-large" width="200" height="200" patternUnits="userSpaceOnUse">
<path d="M 200 0 L 0 0 0 200" fill="none" stroke="rgba(0, 229, 255, 0.1)" stroke-width="2"/>
</pattern>
<filter id="glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-warning">
<feGaussianBlur stdDeviation="6" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
<rect width="100%" height="100%" fill="url(#grid-large)" />
<!-- World Group (Camera follows vehicle) -->
<g id="world" transform="translate(0, 0)">
<!-- Environment / Stairs -->
<polygon id="stairs" class="stair-polygon" points="" />
<!-- Kinetmatic Envelope -->
<path id="envelope-path" class="envelope-path" d="" />
<!-- Vehicle Assembly -->
<g id="vehicle">
<!-- Outer Rubber Track -->
<path id="main-track" class="track-belt" d="" filter="url(#glow)"/>
<!-- Track inner visual for motion -->
<path id="main-track-dash" class="track-inner-dash" d="" />
<!-- Chassis Segments -->
<line id="seg1" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>
<line id="seg2" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>
<line id="seg3" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" stroke-width="24" stroke-linecap="round"/>
<!-- Dynamic Tensioner Spring -->
<path id="tensioner-spring" fill="none" stroke="var(--warning)" stroke-width="3" stroke-linejoin="round" />
<rect id="tensioner-block" width="16" height="24" fill="var(--bg-color)" stroke="var(--warning)" stroke-width="2" rx="4" y="-12" />
<!-- Wheels and Joints -->
<g id="wheels"></g>
<!-- Torsion Hinge Highlight Arcs -->
<path id="hinge-arc1" fill="none" stroke-width="4" stroke-linecap="round" />
<path id="hinge-arc2" fill="none" stroke-width="4" stroke-linecap="round" />
</g>
</g>
<!-- Static HUD Overlays -->
<g transform="translate(1250, 60)">
<rect width="300" height="220" fill="rgba(5, 9, 20, 0.8)" stroke="rgba(0,229,255,0.3)" stroke-width="1" rx="8"/>
<text x="20" y="35" class="hud-label">SYSTEM TELEMETRY</text>
<line x1="20" y1="45" x2="280" y2="45" stroke="rgba(0,229,255,0.2)" stroke-width="1"/>
<text x="20" y="80" class="hud-label">PITCH HINGE 1 (Front)</text>
<text x="280" y="80" id="hud-angle2" class="hud-value" text-anchor="end">0.0°</text>
<text x="20" y="115" class="hud-label">PITCH HINGE 2 (Rear)</text>
<text x="280" y="115" id="hud-angle1" class="hud-value" text-anchor="end">0.0°</text>
<text x="20" y="150" class="hud-label">TRACK PERIMETER</text>
<text x="280" y="150" id="hud-length" class="hud-value" text-anchor="end">0 <tspan class="hud-unit">mm</tspan></text>
<text x="20" y="185" class="hud-label">DYNAMIC TENSION (K=150N)</text>
<text x="280" y="185" id="hud-tension" class="hud-value" text-anchor="end">150.0 <tspan class="hud-unit">N</tspan></text>
<rect x="20" y="200" width="260" height="4" fill="rgba(0,229,255,0.1)" rx="2"/>
<rect id="hud-tension-bar" x="20" y="200" width="130" height="4" fill="var(--primary)" rx="2" transition="width 0.2s"/>
</g>
<!-- Callouts -->
<g id="callouts">
<line id="callout-line1" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
<text id="callout-text1" x="0" y="0" class="hud-label" opacity="0" transition="opacity 0.2s">YIELDING</text>
<line id="callout-line2" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
<text id="callout-text2" x="0" y="0" class="hud-label" opacity="0">YIELDING</text>
</g>
</svg>
</div>
<script>
/**
* TRIZ IFR Kinematic Simulation
* Simulates a 3-segment articulated tracked vehicle traversing irregular stairs.
*/
// --- Configuration & Constants ---
const CHASSIS_L = 140; // Length of each of the 3 segments
const WHEEL_R = 24; // Radius of wheels/pulleys
const TRACK_R = 28; // Offset radius for the rubber track path
const BASE_TENSION = 150; // Newtons
// --- Environment Geometry (Stairs) ---
// Coordinates: X forward, Y down (SVG standard)
const stairsData = [
{x: -1000, y: 500},
{x: 300, y: 500},
{x: 300, y: 400}, // Step 1: H=100
{x: 480, y: 400},
{x: 480, y: 250}, // Step 2: H=150 (Irregular height)
{x: 600, y: 250},
{x: 600, y: 120}, // Step 3: H=130
{x: 1000, y: 120},
{x: 1000, y: -20}, // Step 4
{x: 3000, y: -20}
];
// Kinematic envelope: The upper convex hull / smoothed path the wheels follow.
// Expands corners outwards to simulate the track bridging the gaps.
const envelopeData = [
{x: -1000, y: 476},
{x: 240, y: 476},
{x: 340, y: 376},
{x: 440, y: 376},
{x: 520, y: 226},
{x: 560, y: 226},
{x: 640, y: 96},
{x: 960, y: 96},
{x: 1040, y: -44},
{x: 3000, y: -44}
];
// --- DOM Elements ---
const elStairs = document.getElementById('stairs');
const elEnvelope = document.getElementById('envelope-path');
const elWorld = document.getElementById('world');
const elMainTrack = document.getElementById('main-track');
const elMainTrackDash = document.getElementById('main-track-dash');
const elSeg1 = document.getElementById('seg1');
const elSeg2 = document.getElementById('seg2');
const elSeg3 = document.getElementById('seg3');
const elWheelsGrp = document.getElementById('wheels');
const elHingeArc1 = document.getElementById('hinge-arc1');
const elHingeArc2 = document.getElementById('hinge-arc2');
const elTenSpring = document.getElementById('tensioner-spring');
const elTenBlock = document.getElementById('tensioner-block');
// HUD
const hudAngle1 = document.getElementById('hud-angle1');
const hudAngle2 = document.getElementById('hud-angle2');
const hudLength = document.getElementById('hud-length');
const hudTension = document.getElementById('hud-tension');
const hudTensionBar = document.getElementById('hud-tension-bar');
const cLine1 = document.getElementById('callout-line1');
const cText1 = document.getElementById('callout-text1');
const cLine2 = document.getElementById('callout-line2');
const cText2 = document.getElementById('callout-text2');
// Initialize SVG static geometry
function initGeometry() {
// Build Stairs Polygon
let pts = "";
stairsData.forEach(p => pts += `${p.x},${p.y} `);
pts += `3000,1000 -1000,1000`; // Close bottom
elStairs.setAttribute('points', pts.trim());
// Build Envelope Path
let d = `M ${envelopeData[0].x} ${envelopeData[0].y}`;
for(let i=1; i<envelopeData.length; i++) {
d += ` L ${envelopeData[i].x} ${envelopeData[i].y}`;
}
elEnvelope.setAttribute('d', d);
// Create 4 Wheels
for(let i=0; i<4; i++) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('id', `wheel${i}`);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', WHEEL_R);
circle.setAttribute('class', 'wheel');
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('x1', -WHEEL_R+4); line1.setAttribute('y1', 0);
line1.setAttribute('x2', WHEEL_R-4); line1.setAttribute('y2', 0);
line1.setAttribute('class', 'wheel-spoke');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('x1', 0); line2.setAttribute('y1', -WHEEL_R+4);
line2.setAttribute('x2', 0); line2.setAttribute('y2', WHEEL_R-4);
line2.setAttribute('class', 'wheel-spoke');
const hub = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
hub.setAttribute('r', 6);
hub.setAttribute('class', 'hinge-joint');
g.appendChild(circle);
g.appendChild(line1);
g.appendChild(line2);
g.appendChild(hub);
elWheelsGrp.appendChild(g);
}
}
// --- Kinematic Math Solvers ---
function getEnvY(x) {
for(let i=0; i<envelopeData.length-1; i++) {
if(x >= envelopeData[i].x && x <= envelopeData[i+1].x) {
let t = (x - envelopeData[i].x) / (envelopeData[i+1].x - envelopeData[i].x);
return envelopeData[i].y + t * (envelopeData[i+1].y - envelopeData[i].y);
}
}
return envelopeData[envelopeData.length-1].y;
}
// Solves for the next joint point maintaining segment length L and resting on envelope
function solveNextPoint(x_prev, y_prev, L) {
let bestX = x_prev + L;
let minError = Infinity;
// Forward scanline approximation
for(let scan = x_prev; scan <= x_prev + L; scan += 0.5) {
let y = getEnvY(scan);
let dist = Math.hypot(scan - x_prev, y - y_prev);
let err = Math.abs(dist - L);
if (err < minError) {
minError = err;
bestX = scan;
}
}
// Binary search refinement
let left = bestX - 1.0;
let right = bestX + 1.0;
for(let i=0; i<12; i++) {
let mid = (left + right) / 2;
let y = getEnvY(mid);
let dist = Math.hypot(mid - x_prev, y - y_prev);
if (dist > L) right = mid;
else left = mid;
}
return {x: right, y: getEnvY(right)};
}
// Track Offset Math (Miter joint handling)
function getOffsetPoints(pA, pB, pC, R) {
let dx1 = pB.x - pA.x, dy1 = pB.y - pA.y;
let l1 = Math.hypot(dx1, dy1);
let n1x = -dy1/l1, n1y = dx1/l1;
let dx2 = pC.x - pB.x, dy2 = pC.y - pB.y;
let l2 = Math.hypot(dx2, dy2);
let n2x = -dy2/l2, n2y = dx2/l2;
let nx = n1x + n2x, ny = n1y + n2y;
let len = Math.hypot(nx, ny);
if(len < 0.01) return { U: {x: pB.x - n1x*R, y: pB.y - n1y*R}, D: {x: pB.x + n1x*R, y: pB.y + n1y*R} };
nx /= len; ny /= len;
let dot = n1x*nx + n1y*ny;
let miter = R / Math.max(0.2, dot);
return {
U: {x: pB.x - nx*miter, y: pB.y - ny*miter},
D: {x: pB.x + nx*miter, y: pB.y + ny*miter}
};
}
function getNormal(p1, p2) {
let dx = p2.x - p1.x, dy = p2.y - p1.y;
let l = Math.hypot(dx, dy);
return {nx: -dy/l, ny: dx/l}; // Points 'down' (positive Y direction in SVG)
}
// --- Main Animation Loop ---
let basePerimeter = 0;
let isFirstFrame = true;
function animate(time) {
// 1. Compute global driving variable X
// Cycle every ~14 seconds
const duration = 14000;
const progress = (time % duration) / duration;
const startX = -100;
const endX = 1300;
let wx0 = startX + progress * (endX - startX);
// 2. Kinematic Forward Solver
let p0 = {x: wx0, y: getEnvY(wx0)};
let p1 = solveNextPoint(p0.x, p0.y, CHASSIS_L);
let p2 = solveNextPoint(p1.x, p1.y, CHASSIS_L);
let p3 = solveNextPoint(p2.x, p2.y, CHASSIS_L);
let points = [p0, p1, p2, p3];
// 3. Update Chassis Segments
[elSeg1, elSeg2, elSeg3].forEach((el, i) => {
el.setAttribute('x1', points[i].x); el.setAttribute('y1', points[i].y);
el.setAttribute('x2', points[i+1].x); el.setAttribute('y2', points[i+1].y);
});
// 4. Update Wheels
let wheelDist = wx0; // simple approximation for rotation
let wheelAngle = (wheelDist / WHEEL_R) * (180/Math.PI);
for(let i=0; i<4; i++) {
let w = document.getElementById(`wheel${i}`);
w.setAttribute('transform', `translate(${points[i].x}, ${points[i].y}) rotate(${wheelAngle})`);
}
// 5. Compute Track Path & Perimeter
let n0 = getNormal(p0, p1);
let n3 = getNormal(p2, p3);
let D0 = {x: p0.x + n0.nx*TRACK_R, y: p0.y + n0.ny*TRACK_R};
let U0 = {x: p0.x - n0.nx*TRACK_R, y: p0.y - n0.ny*TRACK_R};
let D3 = {x: p3.x + n3.nx*TRACK_R, y: p3.y + n3.ny*TRACK_R};
let U3 = {x: p3.x - n3.nx*TRACK_R, y: p3.y - n3.ny*TRACK_R};
let j1 = getOffsetPoints(p0, p1, p2, TRACK_R);
let j2 = getOffsetPoints(p1, p2, p3, TRACK_R);
// Build SVG Path
let trackPath = `M ${U0.x} ${U0.y} L ${j1.U.x} ${j1.U.y} L ${j2.U.x} ${j2.U.y} L ${U3.x} ${U3.y} `;
trackPath += `A ${TRACK_R} ${TRACK_R} 0 0 1 ${D3.x} ${D3.y} `;
trackPath += `L ${j2.D.x} ${j2.D.y} L ${j1.D.x} ${j1.D.y} L ${D0.x} ${D0.y} `;
trackPath += `A ${TRACK_R} ${TRACK_R} 0 0 1 ${U0.x} ${U0.y} Z`;
elMainTrack.setAttribute('d', trackPath);
elMainTrackDash.setAttribute('d', trackPath);
elMainTrackDash.style.strokeDashoffset = -wheelDist; // Animate inner belt
// Perimeter calculation for dynamic tensioning
let perim = 0;
perim += Math.hypot(j1.U.x - U0.x, j1.U.y - U0.y);
perim += Math.hypot(j2.U.x - j1.U.x, j2.U.y - j1.U.y);
perim += Math.hypot(U3.x - j2.U.x, U3.y - j2.U.y);
perim += Math.PI * TRACK_R; // half circle front
perim += Math.hypot(j2.D.x - D3.x, j2.D.y - D3.y);
perim += Math.hypot(j1.D.x - j2.D.x, j1.D.y - j2.D.y);
perim += Math.hypot(D0.x - j1.D.x, D0.y - j1.D.y);
perim += Math.PI * TRACK_R; // half circle back
if (isFirstFrame) {
basePerimeter = perim;
isFirstFrame = false;
}
// 6. Articulation Angles & Visual Feedback (Torsion Springs)
function getAngle(pA, pB) { return Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI; }
let a1 = getAngle(p0, p1);
let a2 = getAngle(p1, p2);
let a3 = getAngle(p2, p3);
let flex1 = a2 - a1;
let flex2 = a3 - a2;
// Normalize flex degrees
if (flex1 > 180) flex1 -= 360; if (flex1 < -180) flex1 += 360;
if (flex2 > 180) flex2 -= 360; if (flex2 < -180) flex2 += 360;
hudAngle1.textContent = Math.abs(flex1).toFixed(1) + '°';
hudAngle2.textContent = Math.abs(flex2).toFixed(1) + '°';
// Highlight logic
const THRESHOLD = 3.0;
function drawHingeArc(elArc, pCtr, pPrev, angleDelta, isActive) {
if(Math.abs(angleDelta) < THRESHOLD) {
elArc.setAttribute('d', '');
elArc.classList.remove('active-hinge');
return false;
}
let r = 35;
let baseA = getAngle(pPrev, pCtr) * Math.PI / 180;
let deltaA = angleDelta * Math.PI / 180;
let startX = pCtr.x + r * Math.cos(baseA);
let startY = pCtr.y + r * Math.sin(baseA);
let endX = pCtr.x + r * Math.cos(baseA + deltaA);
let endY = pCtr.y + r * Math.sin(baseA + deltaA);
let sweep = angleDelta > 0 ? 1 : 0;
elArc.setAttribute('d', `M ${pCtr.x} ${pCtr.y} L ${startX} ${startY} A ${r} ${r} 0 0 ${sweep} ${endX} ${endY} Z`);
elArc.classList.add('active-hinge');
return true;
}
let active1 = drawHingeArc(elHingeArc1, p1, p0, flex1, true);
let active2 = drawHingeArc(elHingeArc2, p2, p1, flex2, true);
// Callout lines
function updateCallout(active, px, py, textEl, lineEl, offsetX, offsetY, text) {
if(active) {
lineEl.setAttribute('x1', px); lineEl.setAttribute('y1', py);
lineEl.setAttribute('x2', px + offsetX); lineEl.setAttribute('y2', py + offsetY);
textEl.setAttribute('x', px + offsetX + (offsetX>0?5:-70));
textEl.setAttribute('y', py + offsetY + 4);
textEl.textContent = text + " " + Math.abs(flex1>flex2?flex1:flex2).toFixed(0) + "°";
lineEl.classList.add('visible');
textEl.classList.add('active-text');
textEl.style.opacity = 1;
} else {
lineEl.classList.remove('visible');
textEl.classList.remove('active-text');
textEl.style.opacity = 0;
}
}
updateCallout(active1, p1.x, p1.y, cText1, cLine1, -50, -80, "TORSION YIELD");
updateCallout(active2, p2.x, p2.y, cText2, cLine2, 50, -80, "TORSION YIELD");
// 7. Dynamic Tensioner Visualization
let deltaP = perim - basePerimeter;
let force = BASE_TENSION + Math.max(0, deltaP * 1.5);
hudLength.innerHTML = perim.toFixed(1) + ' <tspan class="hud-unit">mm</tspan>';
hudTension.innerHTML = force.toFixed(1) + ' <tspan class="hud-unit">N</tspan>';
// Animate HUD Bar
let barW = Math.min(260, (force / 300) * 260);
hudTensionBar.setAttribute('width', barW);
if(force > 160) {
hudTension.style.fill = 'var(--warning)';
hudTensionBar.style.fill = 'var(--warning)';
} else {
hudTension.style.fill = 'var(--primary)';
hudTensionBar.style.fill = 'var(--primary)';
}
// Draw Spring at Rear Wheel (p0)
// Spring stretches horizontally relative to segment 1 angle
let a1_rad = a1 * Math.PI / 180;
let ext = Math.max(0, deltaP * 1.5); // Visual stretch amount
let sprPts = "";
let coils = 5;
let coilW = (20 + ext) / coils;
// Transform spring to lie along segment 1, anchored slightly behind P0
let bx = p0.x - Math.cos(a1_rad)*10;
let by = p0.y - Math.sin(a1_rad)*10;
for(let i=0; i<=coils; i++) {
let x = bx + Math.cos(a1_rad)*(i*coilW);
let y = by + Math.sin(a1_rad)*(i*coilW);
let perpX = -Math.sin(a1_rad) * (i%2===0 ? 8 : -8);
let perpY = Math.cos(a1_rad) * (i%2===0 ? 8 : -8);
sprPts += `${x+perpX},${y+perpY} `;
}
elTenSpring.setAttribute('d', `M ${bx},${by} L ${sprPts.trim()}`);
// Position tensioner block
elTenBlock.setAttribute('transform', `translate(${bx + Math.cos(a1_rad)*(20+ext)}, ${by + Math.sin(a1_rad)*(20+ext)}) rotate(${a1})`);
// 8. Camera Tracking
// Keep vehicle centered. Center is ~p1
let camTargetX = p1.x - 600;
let camTargetY = p1.y - 600;
// Apply soft transform
elWorld.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
// Update callouts to track camera
cLine1.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
cText1.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
cLine2.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
cText2.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
requestAnimationFrame(animate);
}
// Bootstrap
initGeometry();
requestAnimationFrame(animate);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRIZ IFR Solution: Articulated Flexible Track Chassis</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700;800&family=Space+Grotesk:wght@400;700&display=swap');
:root {
--bg-color: #050914;
--grid-color: rgba(0, 229, 255, 0.08);
--stair-color: #0c1222;
--stair-stroke: #1e3a5f;
--primary: #00E5FF;
--warning: #FF3D00;
--success: #00E676;
--text-main: #E0F7FA;
--text-muted: #4DD0E1;
--font-mono: 'JetBrains Mono', monospace;
--font-display: 'Space Grotesk', sans-serif;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--bg-color);
color: var(--text-main);
overflow: hidden;
font-family: var(--font-display);
}
#canvas-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG Element Styling */
.track-belt {
fill: rgba(0, 229, 255, 0.05);
stroke: var(--primary);
stroke-width: 5;
stroke-linejoin: round;
stroke-linecap: round;
}
.track-inner-dash {
fill: none;
stroke: #fff;
stroke-width: 2.5;
stroke-dasharray: 8 16;
opacity: 0.7;
}
.chassis-segment {
fill: none;
stroke: rgba(0, 229, 255, 0.25);
stroke-width: 28;
stroke-linecap: round;
}
.chassis-rod {
fill: none;
stroke: var(--primary);
stroke-width: 10;
stroke-linecap: round;
}
.wheel {
fill: #0a1128;
stroke: var(--primary);
stroke-width: 3;
}
.wheel-spoke {
stroke: rgba(0, 229, 255, 0.6);
stroke-width: 3;
stroke-linecap: round;
}
.stair-polygon {
fill: var(--stair-color);
stroke: var(--stair-stroke);
stroke-width: 4;
stroke-linejoin: round;
}
.envelope-path {
fill: none;
stroke: rgba(0, 229, 255, 0.15);
stroke-width: 2;
stroke-dasharray: 6 6;
}
.hinge-joint {
fill: var(--bg-color);
stroke: var(--primary);
stroke-width: 3;
}
.hinge-ring {
fill: none;
stroke: rgba(0, 229, 255, 0.5);
stroke-width: 3;
}
/* Dynamic Classes applied via JS */
.active-hinge {
stroke: var(--warning);
filter: drop-shadow(0 0 12px var(--warning));
}
.active-text {
fill: var(--warning) !important;
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 61, 0, 0.8);
}
.hud-label { font-size: 13px; fill: var(--text-muted); font-family: var(--font-mono); letter-spacing: 1px;}
.hud-value { font-size: 18px; fill: var(--primary); font-family: var(--font-mono); font-weight: 800; }
.hud-unit { font-size: 11px; fill: rgba(0,229,255,0.6); }
.callout-line {
stroke: var(--warning);
stroke-width: 2;
stroke-dasharray: 3 5;
opacity: 0;
transition: opacity 0.2s;
}
.callout-line.visible { opacity: 0.9; }
/* UI Overlay */
.overlay {
position: absolute;
top: 40px;
left: 50px;
pointer-events: none;
z-index: 100;
}
.triz-badge {
display: inline-block;
background: rgba(0, 229, 255, 0.15);
border: 1px solid var(--primary);
color: var(--primary);
padding: 6px 16px;
font-family: var(--font-mono);
font-size: 13px;
border-radius: 4px;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 2px;
box-shadow: 0 0 20px rgba(0, 229, 255, 0.2);
}
h1 { margin: 0 0 12px 0; font-size: 2.8rem; letter-spacing: -1px; text-shadow: 0 2px 10px rgba(0,0,0,0.5); }
p { margin: 0; font-family: var(--font-mono); color: var(--text-muted); font-size: 0.95rem; max-width: 550px; line-height: 1.6; background: rgba(5,9,20,0.6); padding: 15px; border-left: 3px solid var(--primary); border-radius: 0 8px 8px 0; backdrop-filter: blur(4px); }
.scanline {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.3) 51%);
background-size: 100% 4px;
pointer-events: none;
z-index: 900;
opacity: 0.4;
}
</style>
</head>
<body>
<div id="canvas-container">
<div class="scanline"></div>
<div class="overlay">
<div class="triz-badge">TRIZ IFR Solution</div>
<h1>Articulated Soft-Track</h1>
<p>Eliminating step-slip via segmented pitch hinges and dynamic telescopic tensioning. The parts stay mechanically interlinked while continuously morphing to the irregular terrain.</p>
</div>
<svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="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"/>
</pattern>
<pattern id="grid-large" width="300" height="300" patternUnits="userSpaceOnUse">
<path d="M 300 0 L 0 0 0 300" fill="none" stroke="rgba(0, 229, 255, 0.15)" stroke-width="2"/>
</pattern>
<filter id="glow">
<feGaussianBlur stdDeviation="5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- World Group (Camera follows vehicle, grids move with world) -->
<g id="world" transform="translate(0, 0)">
<!-- Infinite Background Grids fixed to World -->
<rect x="-5000" y="-5000" width="15000" height="10000" fill="url(#grid)" />
<rect x="-5000" y="-5000" width="15000" height="10000" fill="url(#grid-large)" />
<!-- Environment / Stairs -->
<polygon id="stairs" class="stair-polygon" points="" />
<!-- Kinetmatic Envelope -->
<path id="envelope-path" class="envelope-path" d="" />
<!-- Vehicle Assembly -->
<g id="vehicle">
<!-- Chassis Modules -->
<!-- Seg 1: Telescopic Tensioner Module -->
<line id="seg1-tube" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />
<line id="seg1-rod" class="chassis-rod" x1="0" y1="0" x2="0" y2="0" />
<path id="tensioner-spring" fill="none" stroke="var(--warning)" stroke-width="4" stroke-linejoin="round" />
<!-- Seg 2 & 3: Rigid Modules -->
<line id="seg2" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />
<line id="seg3" class="chassis-segment" x1="0" y1="0" x2="0" y2="0" />
<!-- Outer Rubber Track & Dashes -->
<path id="main-track" class="track-belt" d="" filter="url(#glow)"/>
<path id="main-track-dash" class="track-inner-dash" d="" />
<!-- Wheels and Mechanical Joints -->
<g id="wheels"></g>
<!-- Torsion Hinge Highlight Arcs -->
<path id="hinge-arc1" fill="none" stroke-width="5" stroke-linecap="round" />
<path id="hinge-arc2" fill="none" stroke-width="5" stroke-linecap="round" />
</g>
</g>
<!-- Static HUD Overlays -->
<g transform="translate(1220, 60)">
<rect width="320" height="230" fill="rgba(5, 9, 20, 0.85)" stroke="rgba(0,229,255,0.4)" stroke-width="1" rx="8"/>
<text x="25" y="35" class="hud-label">SYSTEM KINEMATICS</text>
<line x1="25" y1="48" x2="295" y2="48" stroke="rgba(0,229,255,0.2)" stroke-width="2"/>
<text x="25" y="85" class="hud-label">FRONT HINGE ANGLE</text>
<text x="295" y="85" id="hud-angle2" class="hud-value" text-anchor="end">0.0°</text>
<text x="25" y="125" class="hud-label">REAR HINGE ANGLE</text>
<text x="295" y="125" id="hud-angle1" class="hud-value" text-anchor="end">0.0°</text>
<text x="25" y="165" class="hud-label">TENSION CYLINDER LOAD</text>
<text x="295" y="165" id="hud-tension" class="hud-value" text-anchor="end">150.0 <tspan class="hud-unit">N</tspan></text>
<rect x="25" y="195" width="270" height="6" fill="rgba(0,229,255,0.1)" rx="3"/>
<rect id="hud-tension-bar" x="25" y="195" width="135" height="6" fill="var(--primary)" rx="3" style="transition: width 0.1s linear, fill 0.2s;"/>
</g>
<!-- Dynamic Callouts (Positioned in Screen Space) -->
<g id="callouts">
<line id="callout-line1" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
<text id="callout-text1" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">TORSION YIELD</text>
<line id="callout-line2" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
<text id="callout-text2" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">TORSION YIELD</text>
<line id="callout-line3" class="callout-line" x1="0" y1="0" x2="0" y2="0" />
<text id="callout-text3" x="0" y="0" class="hud-label" opacity="0" style="transition: opacity 0.2s">DYNAMIC TENSION (150N)</text>
</g>
</svg>
</div>
<script>
/**
* TRIZ IFR Kinematic Simulation - Highly Robust Physics & Rendering
*/
const CHASSIS_L = 140;
const WHEEL_R = 26;
const TRACK_R = 32;
const BASE_TENSION = 150;
// --- Environment Geometry (Stairs) ---
const stairsData = [
{x: -1500, y: 500},
{x: 300, y: 500},
{x: 300, y: 400}, // Step 1
{x: 480, y: 400},
{x: 480, y: 250}, // Step 2 (Irregular)
{x: 600, y: 250},
{x: 600, y: 120}, // Step 3
{x: 1000, y: 120},
{x: 1000, y: -20}, // Step 4
{x: 4000, y: -20}
];
// Kinematic envelope ensures wheels perfectly track the terrain peaks
const envelopeData = [
{x: -1500, y: 474},
{x: 240, y: 474},
{x: 340, y: 374},
{x: 440, y: 374},
{x: 520, y: 224},
{x: 560, y: 224},
{x: 640, y: 94},
{x: 960, y: 94},
{x: 1040, y: -46},
{x: 4000, y: -46}
];
// --- DOM Elements ---
const elStairs = document.getElementById('stairs');
const elEnvelope = document.getElementById('envelope-path');
const elWorld = document.getElementById('world');
const elMainTrack = document.getElementById('main-track');
const elMainTrackDash = document.getElementById('main-track-dash');
const elSeg1Tube = document.getElementById('seg1-tube');
const elSeg1Rod = document.getElementById('seg1-rod');
const elTenSpring = document.getElementById('tensioner-spring');
const elSeg2 = document.getElementById('seg2');
const elSeg3 = document.getElementById('seg3');
const elWheelsGrp = document.getElementById('wheels');
const elHingeArc1 = document.getElementById('hinge-arc1');
const elHingeArc2 = document.getElementById('hinge-arc2');
const hudAngle1 = document.getElementById('hud-angle1');
const hudAngle2 = document.getElementById('hud-angle2');
const hudTension = document.getElementById('hud-tension');
const hudTensionBar = document.getElementById('hud-tension-bar');
const callouts = [
{ line: document.getElementById('callout-line1'), text: document.getElementById('callout-text1') },
{ line: document.getElementById('callout-line2'), text: document.getElementById('callout-text2') },
{ line: document.getElementById('callout-line3'), text: document.getElementById('callout-text3') }
];
// Initialize SVG static geometry
function initGeometry() {
let pts = "";
stairsData.forEach(p => pts += `${p.x},${p.y} `);
pts += `4000,1200 -1500,1200`;
elStairs.setAttribute('points', pts.trim());
let d = `M ${envelopeData[0].x} ${envelopeData[0].y}`;
for(let i=1; i<envelopeData.length; i++) {
d += ` L ${envelopeData[i].x} ${envelopeData[i].y}`;
}
elEnvelope.setAttribute('d', d);
for(let i=0; i<4; i++) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('id', `wheel${i}`);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', WHEEL_R);
circle.setAttribute('class', 'wheel');
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('x1', -WHEEL_R+5); line1.setAttribute('y1', 0);
line1.setAttribute('x2', WHEEL_R-5); line1.setAttribute('y2', 0);
line1.setAttribute('class', 'wheel-spoke');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('x1', 0); line2.setAttribute('y1', -WHEEL_R+5);
line2.setAttribute('x2', 0); line2.setAttribute('y2', WHEEL_R-5);
line2.setAttribute('class', 'wheel-spoke');
const ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
ring.setAttribute('r', 12);
ring.setAttribute('class', 'hinge-ring');
const hub = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
hub.setAttribute('r', 5);
hub.setAttribute('class', 'hinge-joint');
g.appendChild(circle);
g.appendChild(line1);
g.appendChild(line2);
g.appendChild(ring);
g.appendChild(hub);
elWheelsGrp.appendChild(g);
}
}
// --- Kinematic Math Solvers ---
function getEnvY(x) {
for(let i=0; i<envelopeData.length-1; i++) {
if(x >= envelopeData[i].x && x <= envelopeData[i+1].x) {
let t = (x - envelopeData[i].x) / (envelopeData[i+1].x - envelopeData[i].x);
return envelopeData[i].y + t * (envelopeData[i+1].y - envelopeData[i].y);
}
}
return envelopeData[envelopeData.length-1].y;
}
// Guaranteed binary search solver to prevent part detachment
function solveNextPoint(x_prev, y_prev, L) {
let left = x_prev;
let right = x_prev + L;
for(let i=0; i<20; i++) {
let mid = (left + right) / 2;
let y = getEnvY(mid);
let dist = Math.hypot(mid - x_prev, y - y_prev);
if (dist > L) right = mid;
else left = mid;
}
return {x: right, y: getEnvY(right)};
}
// Vector math
function sub(pA, pB) { return {x: pA.x - pB.x, y: pA.y - pB.y}; }
function add(pA, v) { return {x: pA.x + v.x, y: pA.y + v.y}; }
function getNormal(pA, pB) {
let dx = pB.x - pA.x, dy = pB.y - pA.y;
let l = Math.hypot(dx, dy);
return {nx: -dy/l, ny: dx/l};
}
function getMiterPoint(p, nA, nB, sign) {
let nx = nA.nx + nB.nx, ny = nA.ny + nB.ny;
let len = Math.hypot(nx, ny);
if (len < 0.01) return {x: p.x + sign*nA.nx*TRACK_R, y: p.y + sign*nA.ny*TRACK_R};
nx /= len; ny /= len;
let dot = nA.nx*nx + nA.ny*ny;
let miter = TRACK_R / Math.max(0.1, dot);
return {x: p.x + sign*nx*miter, y: p.y + sign*ny*miter};
}
let basePerimeter = 0;
function animate(time) {
// 1. Drive variable
const duration = 14000;
const progress = (time % duration) / duration;
const startX = -300;
const endX = 1400;
let wx0 = startX + progress * (endX - startX);
// 2. Base Kinematics
let p0_math = {x: wx0, y: getEnvY(wx0)};
let p1 = solveNextPoint(p0_math.x, p0_math.y, CHASSIS_L);
let p2 = solveNextPoint(p1.x, p1.y, CHASSIS_L);
let p3 = solveNextPoint(p2.x, p2.y, CHASSIS_L);
// 3. Compute Slack & Tension
let n0_raw = getNormal(p0_math, p1);
let n1_raw = getNormal(p1, p2);
let n2_raw = getNormal(p2, p3);
// Rough perimeter estimate to find slack
let rawPerim = Math.hypot(p1.x-p0_math.x, p1.y-p0_math.y) +
Math.hypot(p2.x-p1.x, p2.y-p1.y) +
Math.hypot(p3.x-p2.x, p3.y-p2.y);
rawPerim = rawPerim * 2 + 2 * Math.PI * TRACK_R;
if (basePerimeter === 0) basePerimeter = rawPerim;
let slack = Math.max(0, basePerimeter - rawPerim);
let stretchDist = slack / 2; // Rear wheel moves back to take up slack
// Real visual p0 shifted backwards to simulate telescoping tensioner
let v01 = sub(p0_math, p1);
let len01 = Math.hypot(v01.x, v01.y);
let u01 = {x: v01.x/len01, y: v01.y/len01};
let p0 = {x: p0_math.x + u01.x * stretchDist, y: p0_math.y + u01.y * stretchDist};
let points = [p0, p1, p2, p3];
// 4. Draw Telescopic Segment 1 (Tensioner Assembly)
let tube_len = 80;
let tube_end = {x: p1.x + u01.x * tube_len, y: p1.y + u01.y * tube_len};
elSeg1Tube.setAttribute('x1', p1.x); elSeg1Tube.setAttribute('y1', p1.y);
elSeg1Tube.setAttribute('x2', tube_end.x); elSeg1Tube.setAttribute('y2', tube_end.y);
elSeg1Rod.setAttribute('x1', tube_end.x); elSeg1Rod.setAttribute('y1', tube_end.y);
elSeg1Rod.setAttribute('x2', p0.x); elSeg1Rod.setAttribute('y2', p0.y);
// Zigzag Spring on Rod
let sprPts = "";
let coils = 7;
let distRod = Math.hypot(p0.x - tube_end.x, p0.y - tube_end.y);
let coilW = distRod / coils;
for(let i=0; i<=coils; i++) {
let cx = tube_end.x + u01.x * (i*coilW);
let cy = tube_end.y + u01.y * (i*coilW);
let px = -u01.y * (i%2===0 ? 14 : -14);
let py = u01.x * (i%2===0 ? 14 : -14);
if(i===0 || i===coils) sprPts += `${cx},${cy} `;
else sprPts += `${cx+px},${cy+py} `;
}
elTenSpring.setAttribute('d', `M ${tube_end.x},${tube_end.y} L ${sprPts.trim()} L ${p0.x},${p0.y}`);
// Rigid Segments 2 & 3
elSeg2.setAttribute('x1', p1.x); elSeg2.setAttribute('y1', p1.y);
elSeg2.setAttribute('x2', p2.x); elSeg2.setAttribute('y2', p2.y);
elSeg3.setAttribute('x1', p2.x); elSeg3.setAttribute('y1', p2.y);
elSeg3.setAttribute('x2', p3.x); elSeg3.setAttribute('y2', p3.y);
// 5. Update Wheels
let wheelAngle = (wx0 / WHEEL_R) * (180/Math.PI);
for(let i=0; i<4; i++) {
let w = document.getElementById(`wheel${i}`);
w.setAttribute('transform', `translate(${points[i].x}, ${points[i].y}) rotate(${wheelAngle})`);
}
// 6. Robust Track Path Generation (Ensuring perfect wrap and connection)
let n0 = getNormal(p0, p1);
let n1 = getNormal(p1, p2);
let n2 = getNormal(p2, p3);
let U0 = {x: p0.x - n0.nx*TRACK_R, y: p0.y - n0.ny*TRACK_R};
let U1_in = {x: p1.x - n0.nx*TRACK_R, y: p1.y - n0.ny*TRACK_R};
let U1_out = {x: p1.x - n1.nx*TRACK_R, y: p1.y - n1.ny*TRACK_R};
let U2_in = {x: p2.x - n1.nx*TRACK_R, y: p2.y - n1.ny*TRACK_R};
let U2_out = {x: p2.x - n2.nx*TRACK_R, y: p2.y - n2.ny*TRACK_R};
let U3 = {x: p3.x - n2.nx*TRACK_R, y: p3.y - n2.ny*TRACK_R};
let D0 = {x: p0.x + n0.nx*TRACK_R, y: p0.y + n0.ny*TRACK_R};
let D1_in = {x: p1.x + n0.nx*TRACK_R, y: p1.y + n0.ny*TRACK_R};
let D1_out = {x: p1.x + n1.nx*TRACK_R, y: p1.y + n1.ny*TRACK_R};
let D2_in = {x: p2.x + n1.nx*TRACK_R, y: p2.y + n1.ny*TRACK_R};
let D2_out = {x: p2.x + n2.nx*TRACK_R, y: p2.y + n2.ny*TRACK_R};
let D3 = {x: p3.x + n2.nx*TRACK_R, y: p3.y + n2.ny*TRACK_R};
let j1_U = getMiterPoint(p1, n0, n1, -1);
let j1_D = getMiterPoint(p1, n0, n1, 1);
let j2_U = getMiterPoint(p2, n1, n2, -1);
let j2_D = getMiterPoint(p2, n1, n2, 1);
let cross1 = (p1.x - p0.x)*(p2.y - p1.y) - (p1.y - p0.y)*(p2.x - p1.x);
let cross2 = (p2.x - p1.x)*(p3.y - p2.y) - (p2.y - p1.y)*(p3.x - p2.x);
let tPath = `M ${U0.x} ${U0.y} `;
if (cross1 > 0) tPath += `L ${U1_in.x} ${U1_in.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U1_out.x} ${U1_out.y} `;
else tPath += `L ${j1_U.x} ${j1_U.y} `;
if (cross2 > 0) tPath += `L ${U2_in.x} ${U2_in.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U2_out.x} ${U2_out.y} `;
else tPath += `L ${j2_U.x} ${j2_U.y} `;
tPath += `L ${U3.x} ${U3.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D3.x} ${D3.y} `;
if (cross2 < 0) tPath += `L ${D2_out.x} ${D2_out.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D2_in.x} ${D2_in.y} `;
else tPath += `L ${j2_D.x} ${j2_D.y} `;
if (cross1 < 0) tPath += `L ${D1_out.x} ${D1_out.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${D1_in.x} ${D1_in.y} `;
else tPath += `L ${j1_D.x} ${j1_D.y} `;
tPath += `L ${D0.x} ${D0.y} A ${TRACK_R} ${TRACK_R} 0 0 1 ${U0.x} ${U0.y} Z`;
elMainTrack.setAttribute('d', tPath);
elMainTrackDash.setAttribute('d', tPath);
elMainTrackDash.style.strokeDashoffset = -wx0;
// 7. Angles & HUD
function getAngle(pA, pB) { return Math.atan2(pB.y - pA.y, pB.x - pA.x) * 180 / Math.PI; }
let a1 = getAngle(p0, p1);
let a2 = getAngle(p1, p2);
let a3 = getAngle(p2, p3);
let flex1 = a2 - a1;
let flex2 = a3 - a2;
if (flex1 > 180) flex1 -= 360; if (flex1 < -180) flex1 += 360;
if (flex2 > 180) flex2 -= 360; if (flex2 < -180) flex2 += 360;
hudAngle1.textContent = Math.abs(flex1).toFixed(1) + '°';
hudAngle2.textContent = Math.abs(flex2).toFixed(1) + '°';
// HUD Tension
let force = BASE_TENSION + slack * 4; // visual multiplier
hudTension.innerHTML = force.toFixed(1) + ' <tspan class="hud-unit">N</tspan>';
let barW = Math.min(270, (force / 350) * 270);
hudTensionBar.setAttribute('width', barW);
if(force > 170) {
hudTension.style.fill = 'var(--warning)';
hudTensionBar.style.fill = 'var(--warning)';
} else {
hudTension.style.fill = 'var(--primary)';
hudTensionBar.style.fill = 'var(--primary)';
}
// Hinge Arcs
function drawHingeArc(elArc, pCtr, pPrev, angleDelta) {
if(Math.abs(angleDelta) < 3.0) {
elArc.setAttribute('d', '');
elArc.classList.remove('active-hinge');
return false;
}
let r = 40;
let baseA = getAngle(pPrev, pCtr) * Math.PI / 180;
let deltaA = angleDelta * Math.PI / 180;
let startX = pCtr.x + r * Math.cos(baseA);
let startY = pCtr.y + r * Math.sin(baseA);
let endX = pCtr.x + r * Math.cos(baseA + deltaA);
let endY = pCtr.y + r * Math.sin(baseA + deltaA);
let sweep = angleDelta > 0 ? 1 : 0;
elArc.setAttribute('d', `M ${pCtr.x} ${pCtr.y} L ${startX} ${startY} A ${r} ${r} 0 0 ${sweep} ${endX} ${endY} Z`);
elArc.classList.add('active-hinge');
return true;
}
let act1 = drawHingeArc(elHingeArc1, p1, p0, flex1);
let act2 = drawHingeArc(elHingeArc2, p2, p1, flex2);
// 8. Camera Tracking (World Translation)
let camTargetX = p1.x - 700;
let camTargetY = p1.y - 650;
elWorld.setAttribute('transform', `translate(${-camTargetX}, ${-camTargetY})`);
// Callout Sync to World Space
function updateCallout(active, px, py, item, offsetX, offsetY, text) {
if(active) {
let sx = px - camTargetX;
let sy = py - camTargetY;
item.line.setAttribute('x1', sx); item.line.setAttribute('y1', sy);
item.line.setAttribute('x2', sx + offsetX); item.line.setAttribute('y2', sy + offsetY);
item.text.setAttribute('x', sx + offsetX + (offsetX>0?8:-90));
item.text.setAttribute('y', sy + offsetY + 5);
item.text.textContent = text;
item.line.classList.add('visible');
item.text.classList.add('active-text');
item.text.style.opacity = 1;
} else {
item.line.classList.remove('visible');
item.text.classList.remove('active-text');
item.text.style.opacity = 0;
}
}
updateCallout(act1, p1.x, p1.y, callouts[0], -60, -90, `TORSION: ${Math.abs(flex1).toFixed(0)}°`);
updateCallout(act2, p2.x, p2.y, callouts[1], 60, -90, `TORSION: ${Math.abs(flex2).toFixed(0)}°`);
// Tensioner Callout on Rod
let rodMidX = (tube_end.x + p0.x)/2;
let rodMidY = (tube_end.y + p0.y)/2;
updateCallout(slack > 2, rodMidX, rodMidY, callouts[2], -80, 80, `TENSION: ${force.toFixed(0)}N`);
requestAnimationFrame(animate);
}
initGeometry();
requestAnimationFrame(animate);
</script>
</body>
</html>
积分规则:第一轮对话扣减8分,后续每轮扣6分
等待动画代码生成...
