feat: rebuild Local G2 algorithm visualization
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Local G2 五次 Hermite 局部平滑|算法演示</title>
|
||||
<style>
|
||||
:root { color-scheme: light; --ink:#15243a; --muted:#617086; --paper:#f6f8fc; --line:#9aa6b5; --blue:#1976d2; --blue-pale:#dbeeff; --green:#1d8a5b; --amber:#bd7900; --red:#c33b3b; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:linear-gradient(135deg,#eef4fb,#fafbfd); color:var(--ink); font:16px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif; }
|
||||
main { width:min(1160px,100%); margin:auto; padding:clamp(18px,4vw,48px); }
|
||||
header { display:grid; gap:8px; margin-bottom:14px; }
|
||||
h1 { font-size:clamp(1.7rem,4vw,2.75rem); line-height:1.15; margin:0; }
|
||||
h1 + p, .evidence-note { margin:0; color:var(--muted); }
|
||||
.boundary { color:#805700; font-weight:700; }
|
||||
.evidence-note { border-left:4px solid var(--amber); padding-left:10px; }
|
||||
.stepper { display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:8px; margin:22px 0 14px; }
|
||||
button { min-height:44px; border:1px solid #b9c7d8; border-radius:10px; padding:8px 10px; color:var(--ink); background:white; font:inherit; cursor:pointer; }
|
||||
button:hover { border-color:var(--blue); } button:focus-visible { outline:3px solid #ffbd3c; outline-offset:2px; } button[aria-pressed="true"] { color:white; background:var(--blue); border-color:var(--blue); font-weight:700; } button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
.diagram-shell { background:white; border:1px solid #d7e0ec; border-radius:18px; overflow:hidden; box-shadow:0 14px 34px #233c6012; }
|
||||
.step-copy { padding:clamp(16px,3vw,28px) clamp(16px,4vw,40px) 4px; } #step-status { color:var(--blue); font-weight:800; letter-spacing:.08em; } h2 { margin:4px 0; font-size:clamp(1.35rem,3vw,2rem); } #step-description { margin:0; color:var(--muted); }
|
||||
svg { width:100%; height:auto; display:block; }
|
||||
.scene-layer { opacity:.04; transition:opacity .32s ease; } [data-step="0"] .scene-0, [data-step="1"] .scene-1, [data-step="2"] .scene-2, [data-step="3"] .scene-3, [data-step="4"] .scene-4, [data-step="5"] .scene-5 { opacity:1; }
|
||||
.step-controls { display:flex; justify-content:space-between; gap:12px; margin-top:16px; } .step-controls button { width:110px; }
|
||||
@media (max-width:700px) { .stepper { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
@media (prefers-reduced-motion:reduce) { .scene-layer { transition:none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="local-g2-algorithm-demo" data-step="0">
|
||||
<header>
|
||||
<h1>Local G2:五次 Hermite 局部路径平滑</h1>
|
||||
<p>候选层已实现;<span class="boundary">正式发布流水线尚未接入</span>。本页说明候选构造和质量门,不表示正式发布结果。</p>
|
||||
<p class="evidence-note">典型示例:用于解释机制,不代表某次测试的精确坐标。</p>
|
||||
</header>
|
||||
<nav class="stepper" aria-label="Local G2 平滑步骤">
|
||||
<button type="button" class="step-button" data-step="0" aria-pressed="true">0 粗路径</button><button type="button" class="step-button" data-step="1" aria-pressed="false">1 局部窗口</button><button type="button" class="step-button" data-step="2" aria-pressed="false">2 Hermite 约束</button><button type="button" class="step-button" data-step="3" aria-pressed="false">3 局部替换</button><button type="button" class="step-button" data-step="4" aria-pressed="false">4 连续性效果</button><button type="button" class="step-button" data-step="5" aria-pressed="false">5 安全质量门</button>
|
||||
</nav>
|
||||
<section class="diagram-shell" aria-live="polite">
|
||||
<div class="step-copy"><span id="step-status"></span><h2 id="step-title"></h2><p id="step-description"></p></div>
|
||||
<svg id="algorithm-diagram" viewBox="0 0 1200 720" role="img" aria-labelledby="algorithm-svg-title algorithm-svg-desc">
|
||||
<title id="algorithm-svg-title">Local G2 五次 Hermite 局部路径平滑步骤</title><desc id="algorithm-svg-desc">典型粗路径在局部曲率跳变处被五次 Hermite 曲线安全替换的六步示意。</desc>
|
||||
<defs>
|
||||
<marker id="arrow-blue" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10z" fill="#1976d2"/></marker>
|
||||
<marker id="arrow-green" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10z" fill="#1d8a5b"/></marker>
|
||||
<marker id="arrow-red" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0,0 L10,5 L0,10z" fill="#c33b3b"/></marker>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="1200" height="720" fill="#fbfcff"/>
|
||||
<g fill="none" stroke="#dce4ee" stroke-width="1"><path d="M80 100H1120M80 220H1120M80 340H1120M80 460H1120M80 580H1120"/><path d="M120 70V610M300 70V610M480 70V610M660 70V610M840 70V610M1020 70V610"/></g>
|
||||
<g aria-label="始终可见的典型原始路径"><path d="M120 525 L290 462 L420 462 L520 350 L690 258 L885 220 L1080 170" fill="none" stroke="#9aa6b5" stroke-width="12" stroke-linejoin="round"/><path d="M120 525 L290 462 L420 462 L520 350 L690 258 L885 220 L1080 170" fill="none" stroke="#fff" stroke-width="3" stroke-dasharray="4 11"/>
|
||||
<g fill="#617086"><circle cx="120" cy="525" r="8"/><circle cx="290" cy="462" r="8"/><circle cx="420" cy="462" r="8"/><circle cx="520" cy="350" r="8"/><circle cx="690" cy="258" r="8"/><circle cx="885" cy="220" r="8"/><circle cx="1080" cy="170" r="8"/></g>
|
||||
<text x="92" y="565" fill="#15243a" font-weight="700">起点</text><text x="1050" y="148" fill="#15243a" font-weight="700">终点</text><path d="M145 515 l70 -26" stroke="#1976d2" stroke-width="4" marker-end="url(#arrow-blue)"/><text x="148" y="490" fill="#1976d2">行驶方向</text></g>
|
||||
<g class="scene-layer scene-0"><circle cx="420" cy="462" r="22" fill="none" stroke="#c33b3b" stroke-width="4"/><path d="M398 431 L439 485 M441 431 L400 485" stroke="#c33b3b" stroke-width="4"/><text x="446" y="454" fill="#c33b3b" font-weight="700">κ 跳变</text><text x="84" y="90" fill="#617086" font-size="18">离散点 + 原始折线:局部接口存在方向/曲率突变</text></g>
|
||||
<g class="scene-layer scene-1"><rect x="290" y="184" width="400" height="340" rx="18" fill="#dbeeff" opacity=".7"/><line x1="290" y1="180" x2="290" y2="535" stroke="#1976d2" stroke-width="4" stroke-dasharray="10 8"/><line x1="690" y1="180" x2="690" y2="535" stroke="#1976d2" stroke-width="4" stroke-dasharray="10 8"/><text x="300" y="210" fill="#1976d2" font-weight="700">局部窗口:只处理跳变附近</text><text x="252" y="555" fill="#1976d2">窗口边界</text><text x="650" y="555" fill="#1976d2">窗口边界</text></g>
|
||||
<g class="scene-layer scene-2"><path d="M290 462 h180 M690 258 l150 -96" stroke="#1976d2" stroke-width="4" marker-end="url(#arrow-blue)"/><path d="M310 432 Q350 400 388 438 M660 290 Q700 315 730 270" fill="none" stroke="#1976d2" stroke-width="3" stroke-dasharray="8 7"/><text x="334" y="401" fill="#1976d2">二阶趋势</text><text x="737" y="253" fill="#1976d2">端点切向</text><path id="quintic-candidate" fill="none" stroke="#1976d2" stroke-width="7" stroke-dasharray="13 8"/><text x="430" y="300" fill="#1976d2" font-weight="700">五次 Hermite 候选(位置 + 切向 + 二阶趋势)</text></g>
|
||||
<g class="scene-layer scene-3"><path id="replacement-candidate" fill="none" stroke="#1d8a5b" stroke-width="10"/><circle cx="290" cy="462" r="13" fill="white" stroke="#1d8a5b" stroke-width="5"/><circle cx="690" cy="258" r="13" fill="white" stroke="#1d8a5b" stroke-width="5"/><text x="245" y="430" fill="#1d8a5b" font-weight="700">替换开始</text><text x="650" y="235" fill="#1d8a5b" font-weight="700">替换结束</text><text x="410" y="520" fill="#1d8a5b" font-weight="700">绿色候选覆盖灰色原折线</text></g>
|
||||
<g class="scene-layer scene-4"><rect x="70" y="70" width="1060" height="245" rx="16" fill="#f0faf5" stroke="#b8dfca"/><path id="continuity-candidate" fill="none" stroke="#1d8a5b" stroke-width="9"/><text x="90" y="105" fill="#1d8a5b" font-weight="700">替换后路径:窗口外不变,接口趋势连续</text><g transform="translate(110 500)"><path d="M0 0V150M0 150H850" stroke="#617086" stroke-width="3"/><text x="-20" y="5" fill="#617086">κ</text><text x="855" y="160" fill="#617086">s</text><path d="M60 118 L290 118 L300 38 L520 38" stroke="#c33b3b" stroke-width="6" fill="none"/><path d="M60 118 C230 118 280 106 355 84 S465 46 520 38" stroke="#1d8a5b" stroke-width="6" fill="none"/><text x="535" y="43" fill="#c33b3b">原始跳变</text><text x="535" y="73" fill="#1d8a5b">候选连续过渡</text><text x="520" y="130" fill="#617086">趋势示意,非实测数据</text></g></g>
|
||||
<g class="scene-layer scene-5"><path id="safety-candidate" fill="none" stroke="#1d8a5b" stroke-width="36" opacity=".18"/><path id="accepted-candidate" fill="none" stroke="#1d8a5b" stroke-width="8"/><g fill="#fff" stroke="#1d8a5b" stroke-width="4"><circle cx="300" cy="462" r="13"/><circle cx="500" cy="382" r="13"/><circle cx="680" cy="266" r="13"/></g><g fill="#1d8a5b" font-weight="700"><text x="720" y="370">✓ 完整车体安全</text><text x="720" y="402">✓ 净空</text><text x="720" y="434">✓ 曲率</text><text x="720" y="466">✓ 偏移</text></g><g transform="translate(320 435) rotate(0)"><rect x="-28" y="-15" width="56" height="30" rx="6" fill="#fff" stroke="#1d8a5b" stroke-width="4"/><circle cx="-17" cy="18" r="5"/><circle cx="17" cy="18" r="5"/></g><g transform="translate(500 348) rotate(-30)"><rect x="-28" y="-15" width="56" height="30" rx="6" fill="#fff" stroke="#1d8a5b" stroke-width="4"/><circle cx="-17" cy="18" r="5"/><circle cx="17" cy="18" r="5"/></g><g transform="translate(660 278) rotate(-28)"><rect x="-28" y="-15" width="56" height="30" rx="6" fill="#fff" stroke="#1d8a5b" stroke-width="4"/><circle cx="-17" cy="18" r="5"/><circle cx="17" cy="18" r="5"/></g><text x="90" y="90" fill="#1d8a5b" font-size="18">通过质量门后才接受候选;任一门阻断则回退原路径</text></g>
|
||||
<g transform="translate(80 675)"><rect width="22" height="12" fill="#9aa6b5"/><text x="30" y="12" fill="#617086">原始粗路径</text><rect x="185" width="22" height="12" fill="#1976d2"/><text x="215" y="12" fill="#617086">窗口/约束</text><rect x="340" width="22" height="12" fill="#1d8a5b"/><text x="370" y="12" fill="#617086">已接受候选</text><rect x="535" width="22" height="12" fill="#bd7900"/><text x="565" y="12" fill="#617086">发布边界</text><rect x="705" width="22" height="12" fill="#c33b3b"/><text x="735" y="12" fill="#617086">拒绝/阻断</text></g>
|
||||
</svg>
|
||||
</section>
|
||||
<p class="boundary">事实边界:正式发布流水线尚未接入;图中的绿色仅代表通过本演示中的典型质量门。</p>
|
||||
<div class="step-controls"><button id="prev-step" type="button">上一步</button><button id="next-step" type="button">下一步</button></div>
|
||||
</main>
|
||||
<script>
|
||||
const hermite = {
|
||||
p0: { x: 290, y: 462 }, p1: { x: 690, y: 258 },
|
||||
d0: { x: 180, y: 0 }, d1: { x: 210, y: -135 },
|
||||
a0: { x: 0, y: -18 }, a1: { x: 22, y: -12 }
|
||||
};
|
||||
function quinticBasis(t) {
|
||||
const t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t;
|
||||
return [
|
||||
1 - 10 * t3 + 15 * t4 - 6 * t5,
|
||||
t - 6 * t3 + 8 * t4 - 3 * t5,
|
||||
0.5 * (t2 - 3 * t3 + 3 * t4 - t5),
|
||||
10 * t3 - 15 * t4 + 6 * t5,
|
||||
-4 * t3 + 7 * t4 - 3 * t5,
|
||||
0.5 * (t3 - 2 * t4 + t5)
|
||||
];
|
||||
}
|
||||
function buildQuinticPath() {
|
||||
const points = [];
|
||||
for (let i = 0; i <= 48; i += 1) {
|
||||
const [h00, h10, h20, h01, h11, h21] = quinticBasis(i / 48);
|
||||
points.push({
|
||||
x: h00 * hermite.p0.x + h10 * hermite.d0.x + h20 * hermite.a0.x + h01 * hermite.p1.x + h11 * hermite.d1.x + h21 * hermite.a1.x,
|
||||
y: h00 * hermite.p0.y + h10 * hermite.d0.y + h20 * hermite.a0.y + h01 * hermite.p1.y + h11 * hermite.d1.y + h21 * hermite.a1.y
|
||||
});
|
||||
}
|
||||
return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' ');
|
||||
}
|
||||
const pathData=buildQuinticPath(); ['quintic-candidate','replacement-candidate','continuity-candidate','safety-candidate','accepted-candidate'].forEach((id)=>document.getElementById(id).setAttribute('d',pathData));
|
||||
const steps=[['0 / 5','原始 Hybrid A* 粗路径','可行离散路径在局部接口处仍可能有曲率跳变。'],['1 / 5','检测并框定局部窗口','只处理跳变附近,不重新搜索整条路径。'],['2 / 5','由端点约束构造五次 Hermite 候选','位置、切向和曲率趋势共同确定局部曲线。'],['3 / 5','替换窗口内部的原始几何','窗口外路径保持不变,接缝需要连续。'],['4 / 5','观察曲率—弧长连续性','目标是消除接口处的趋势跳变,而不是只让外形更圆。'],['5 / 5','经安全与质量门决定接受或回退','候选必须同时满足车体安全、净空、曲率和偏移约束。']];
|
||||
const stepButtons=[...document.querySelectorAll('.step-button')];
|
||||
function setStep(index) { const next=Math.max(0,Math.min(steps.length-1,index)); const [status,title,description]=steps[next]; document.getElementById('local-g2-algorithm-demo').dataset.step=String(next); document.getElementById('step-status').textContent=`步骤 ${status}`; document.getElementById('step-title').textContent=title; document.getElementById('step-description').textContent=description; stepButtons.forEach((button)=>button.setAttribute('aria-pressed',String(Number(button.dataset.step)===next))); document.getElementById('prev-step').disabled=next===0; document.getElementById('next-step').disabled=next===steps.length-1; }
|
||||
stepButtons.forEach((button)=>button.addEventListener('click',()=>setStep(Number(button.dataset.step)))); document.getElementById('prev-step').addEventListener('click',()=>setStep(Number(document.getElementById('local-g2-algorithm-demo').dataset.step)-1)); document.getElementById('next-step').addEventListener('click',()=>setStep(Number(document.getElementById('local-g2-algorithm-demo').dataset.step)+1));
|
||||
document.addEventListener('keydown',(event)=>{ const target=event.target; if(event.altKey||event.ctrlKey||event.metaKey||event.shiftKey||target.matches('input, textarea, select, button, [contenteditable="true"]')) return; if(event.key==='ArrowLeft'||event.key==='ArrowRight') { event.preventDefault(); setStep(Number(document.getElementById('local-g2-algorithm-demo').dataset.step)+(event.key==='ArrowLeft'?-1:1)); } }); setStep(0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user