chihsing's picture
Update index.html
2a1ec4d verified
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaiScience CSD-EWS: Pole & Damping Ratio Simulator</title>
<style>
body { background-color: #121212; color: #e0e0e0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
.container { max-width: 1200px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; position: relative; }
.header h1 { margin: 0; color: #4dabf7; font-size: 28px; font-weight: 600; letter-spacing: 1px; }
.header p { color: #aaa; font-size: 15px; margin-top: 8px; max-width: 900px; margin-left: auto; margin-right: auto; }
.lang-switch { position: absolute; top: 0; right: 0; display: flex; gap: 6px; }
.lang-switch button {
background: #252525; color: #aaa; border: 1px solid #444; border-radius: 6px;
padding: 6px 12px; cursor: pointer; font-size: 13px; transition: all 0.2s;
}
.lang-switch button:hover { border-color: #4dabf7; color: #e0e0e0; }
.lang-switch button.active { background: #4dabf7; color: #121212; border-color: #4dabf7; font-weight: 600; }
.grid { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; }
.controls { background: #1e1e1e; padding: 25px; border-radius: 8px; border: 1px solid #333; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
.visuals { display: flex; flex-direction: column; gap: 20px; }
.panel { background: #1e1e1e; padding: 20px; border-radius: 8px; border: 1px solid #333; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
h3 { margin-top: 0; color: #fff; font-size: 18px; border-bottom: 1px solid #444; padding-bottom: 10px; margin-bottom: 20px; }
label { display: block; margin-top: 20px; margin-bottom: 8px; font-weight: 500; font-size: 15px; color: #ccc; }
span.val { float: right; color: #4dabf7; font-weight: bold; }
input[type=range] { width: 100%; cursor: pointer; accent-color: #4dabf7; }
canvas { width: 100%; height: 260px; background: #0a0a0a; border: 1px solid #444; border-radius: 6px; }
.metric { font-size: 1.3em; font-weight: bold; margin-top: 30px; padding: 20px; background: #252525; border-radius: 8px; text-align: center; border: 2px solid #333; transition: border-color 0.3s; }
.metric .zeta-row { margin-bottom: 10px; }
.metric .status-label { font-size: 0.8em; color: #888; }
.safe { color: #51cf66; }
.warning { color: #fcc419; }
.danger { color: #ff6b6b; }
.math-notes { color: #888; font-size: 13px; margin-top: 30px; background: #1a1a1a; padding: 15px; border-radius: 6px; border-left: 3px solid #4dabf7; }
.math-notes .disclaimer { display: block; margin-top: 12px; color: #666; font-style: italic; }
.time-caption { font-size: 12px; color: #666; margin: -12px 0 10px 0; }
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
.lang-switch { position: static; justify-content: center; margin-bottom: 12px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="lang-switch">
<button type="button" id="lang-zh" class="active" aria-pressed="true">中文</button>
<button type="button" id="lang-en" aria-pressed="false">English</button>
</div>
<h1 id="title-main">TaiScience CSD-EWS</h1>
<p id="title-sub">動態系統極點與阻尼比實時模擬器 | Jacobian 特徵值代理特徵(教學示範)</p>
</div>
<div class="grid">
<div class="controls">
<h3 id="controls-heading">系統特徵值參數 (System Poles)</h3>
<label id="label-sigma">實部 (σ, 衰減率/生長率): <span class="val" id="sigma-val">-0.50</span></label>
<input type="range" id="sigma" min="-2.0" max="0.5" step="0.01" value="-0.5">
<label id="label-omega">虛部 (ω, 振盪頻率): <span class="val" id="omega-val">5.0</span></label>
<input type="range" id="omega" min="1.0" max="15.0" step="0.1" value="5.0">
<div class="metric" id="status-box">
<div class="zeta-row"><span id="zeta-label">阻尼比 (ζ):</span> <span id="zeta-val" style="font-size: 1.2em;">0.1000</span></div>
<div class="status-label" id="status-label">狀態判別 (ζ 閾值 5%):</div>
<div id="status-text" class="safe" style="margin-top: 5px;"></div>
</div>
<div class="math-notes" id="math-notes"></div>
</div>
<div class="visuals">
<div class="panel">
<h3 id="splane-heading">複數平面 (s-plane) 映射</h3>
<canvas id="s-plane"></canvas>
</div>
<div class="panel">
<h3 id="time-heading">時域響應(示意波形)</h3>
<p class="time-caption" id="time-caption"></p>
<canvas id="time-domain"></canvas>
</div>
</div>
</div>
</div>
<script>
const ZETA_WEAK_THRESHOLD = 0.05;
const I18N = {
'zh-TW': {
pageTitle: 'TaiScience CSD-EWS: 動態系統極點與阻尼比分析',
titleMain: 'TaiScience CSD-EWS',
titleSub: '動態系統極點與阻尼比實時模擬器 | Jacobian 特徵值代理特徵(教學示範)',
controlsHeading: '系統特徵值參數 (System Poles)',
labelSigma: '實部 (σ, 衰減率/生長率):',
labelOmega: '虛部 (ω, 振盪頻率):',
zetaLabel: '阻尼比 (ζ):',
statusLabel: '狀態判別 (ζ 閾值 5%):',
statusDanger: '不穩定 / 負阻尼 (σ > 0)',
statusWarning: '弱阻尼 (ζ < 5%)',
statusSafe: '安全阻尼 (ζ ≥ 5%)',
mathNotes: '<strong>核心公式(共軛極點假設):</strong><br><br>'
+ '1. <strong>特徵值:</strong> s = σ ± jω<br>'
+ '2. <strong>阻尼比:</strong> ζ = −σ / √(σ² + ω²)<br>'
+ '3. <strong>臨界邊界:</strong> σ 越過 0 進入右半平面時 ζ &lt; 0,系統發散。<br>'
+ '4. <strong>告警閾值:</strong> 極點顏色、狀態與時域曲線均以同一規則判斷:σ &gt; 0 → 危險;0 ≤ ζ &lt; 0.05 → 警告;ζ ≥ 0.05 → 安全。<br>'
+ '<span class="disclaimer">本頁為手動調參的互動教具,非 PMU / Toeplitz 即時辨識輸出;時域圖僅示範 e<sup>σt</sup>cos(ωt),不含模態振幅與相位。</span>',
splaneHeading: '複數平面 (s-plane) 映射',
timeHeading: '時域響應(示意波形)',
timeCaption: 'y(t) = e<sup>σt</sup> cos(ωt) — 固定單位振幅,僅供直覺理解衰減與頻率',
axisRe: 'Re (σ)',
axisIm: 'Im (jω)',
polePos: 's = σ + jω',
poleNeg: 's = σ − jω',
collapseWarn: '⚠ 振幅超出顯示範圍'
},
en: {
pageTitle: 'TaiScience CSD-EWS: Pole & Damping Ratio Simulator',
titleMain: 'TaiScience CSD-EWS',
titleSub: 'Real-time pole & damping ratio simulator | Jacobian eigenvalue proxy (educational demo)',
controlsHeading: 'System Eigenvalue Parameters (Poles)',
labelSigma: 'Real part (σ, decay/growth rate):',
labelOmega: 'Imaginary part (ω, oscillation frequency):',
zetaLabel: 'Damping ratio (ζ):',
statusLabel: 'Status (ζ threshold 5%):',
statusDanger: 'Unstable / negative damping (σ > 0)',
statusWarning: 'Weak damping (ζ < 5%)',
statusSafe: 'Adequate damping (ζ ≥ 5%)',
mathNotes: '<strong>Core formulas (conjugate-pole assumption):</strong><br><br>'
+ '1. <strong>Eigenvalues:</strong> s = σ ± jω<br>'
+ '2. <strong>Damping ratio:</strong> ζ = −σ / √(σ² + ω²)<br>'
+ '3. <strong>Stability boundary:</strong> when σ crosses 0 into the RHP, ζ &lt; 0 and the mode diverges.<br>'
+ '4. <strong>Alert rule:</strong> pole color, status, and waveform share one rule: σ &gt; 0 → danger; 0 ≤ ζ &lt; 0.05 → warning; ζ ≥ 0.05 → safe.<br>'
+ '<span class="disclaimer">This page is a manual teaching demo, not live PMU / Toeplitz identification. The waveform shows e<sup>σt</sup>cos(ωt) only—no modal amplitude or phase.</span>',
splaneHeading: 'Complex s-plane map',
timeHeading: 'Time-domain response (illustrative)',
timeCaption: 'y(t) = e<sup>σt</sup> cos(ωt) — unit amplitude fixed; for intuition only',
axisRe: 'Re (σ)',
axisIm: 'Im (jω)',
polePos: 's = σ + jω',
poleNeg: 's = σ − jω',
collapseWarn: '⚠ Amplitude exceeds plot range'
}
};
let lang = localStorage.getItem('csd-sim-lang') || 'zh-TW';
if (!I18N[lang]) lang = 'zh-TW';
const sigmaSlider = document.getElementById('sigma');
const omegaSlider = document.getElementById('omega');
const sigmaVal = document.getElementById('sigma-val');
const omegaVal = document.getElementById('omega-val');
const zetaVal = document.getElementById('zeta-val');
const statusText = document.getElementById('status-text');
const statusBox = document.getElementById('status-box');
const splaneCanvas = document.getElementById('s-plane');
const ctxS = splaneCanvas.getContext('2d');
const timeCanvas = document.getElementById('time-domain');
const ctxT = timeCanvas.getContext('2d');
function t(key) {
return I18N[lang][key];
}
/** Unified status: danger | warning | safe */
function getStatus(sigma, zeta) {
if (sigma > 0 || zeta < 0) {
return { level: 'danger', color: '#ff6b6b', message: t('statusDanger') };
}
if (zeta < ZETA_WEAK_THRESHOLD) {
return { level: 'warning', color: '#fcc419', message: t('statusWarning') };
}
return { level: 'safe', color: '#51cf66', message: t('statusSafe') };
}
function setLabelHtml(id, text) {
const el = document.getElementById(id);
const valSpan = el.querySelector('.val');
const valId = valSpan ? valSpan.id : null;
const valText = valSpan ? valSpan.textContent : '';
el.innerHTML = text + (valSpan ? ' <span class="val" id="' + valId + '">' + valText + '</span>' : '');
}
function applyLanguage() {
const L = I18N[lang];
document.documentElement.lang = lang === 'en' ? 'en' : 'zh-TW';
document.title = L.pageTitle;
document.getElementById('title-main').textContent = L.titleMain;
document.getElementById('title-sub').textContent = L.titleSub;
document.getElementById('controls-heading').textContent = L.controlsHeading;
setLabelHtml('label-sigma', L.labelSigma);
setLabelHtml('label-omega', L.labelOmega);
document.getElementById('zeta-label').textContent = L.zetaLabel;
document.getElementById('status-label').textContent = L.statusLabel;
document.getElementById('math-notes').innerHTML = L.mathNotes;
document.getElementById('splane-heading').textContent = L.splaneHeading;
document.getElementById('time-heading').textContent = L.timeHeading;
document.getElementById('time-caption').innerHTML = L.timeCaption;
document.getElementById('lang-zh').classList.toggle('active', lang === 'zh-TW');
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
document.getElementById('lang-zh').setAttribute('aria-pressed', lang === 'zh-TW');
document.getElementById('lang-en').setAttribute('aria-pressed', lang === 'en');
localStorage.setItem('csd-sim-lang', lang);
draw();
}
function resizeCanvases() {
splaneCanvas.width = splaneCanvas.clientWidth * window.devicePixelRatio;
splaneCanvas.height = splaneCanvas.clientHeight * window.devicePixelRatio;
timeCanvas.width = timeCanvas.clientWidth * window.devicePixelRatio;
timeCanvas.height = timeCanvas.clientHeight * window.devicePixelRatio;
ctxS.setTransform(1, 0, 0, 1, 0, 0);
ctxT.setTransform(1, 0, 0, 1, 0, 0);
ctxS.scale(window.devicePixelRatio, window.devicePixelRatio);
ctxT.scale(window.devicePixelRatio, window.devicePixelRatio);
draw();
}
window.addEventListener('resize', resizeCanvases);
function drawSPlane(sigma, omega, poleColor) {
const w = splaneCanvas.clientWidth;
const h = splaneCanvas.clientHeight;
if (w <= 0 || h <= 0) return;
ctxS.clearRect(0, 0, w, h);
const originX = w * 0.75;
const originY = h / 2;
ctxS.fillStyle = 'rgba(43, 138, 62, 0.15)';
ctxS.fillRect(0, 0, originX, h);
ctxS.fillStyle = 'rgba(201, 42, 42, 0.15)';
ctxS.fillRect(originX, 0, w - originX, h);
ctxS.strokeStyle = '#333';
ctxS.lineWidth = 1;
ctxS.beginPath();
for (let i = 0; i < w; i += 40) { ctxS.moveTo(i, 0); ctxS.lineTo(i, h); }
for (let i = 0; i < h; i += 40) { ctxS.moveTo(0, i); ctxS.lineTo(w, i); }
ctxS.stroke();
ctxS.beginPath();
ctxS.moveTo(0, originY); ctxS.lineTo(w, originY);
ctxS.moveTo(originX, 0); ctxS.lineTo(originX, h);
ctxS.strokeStyle = '#888';
ctxS.lineWidth = 2;
ctxS.stroke();
ctxS.fillStyle = '#aaa';
ctxS.font = '12px sans-serif';
ctxS.fillText(t('axisRe'), w - 40, originY - 10);
ctxS.fillText(t('axisIm'), originX + 10, 20);
const scaleX = 80;
const scaleY = 7;
const px = originX + sigma * scaleX;
const py1 = originY - omega * scaleY;
const py2 = originY + omega * scaleY;
function drawCross(x, y, color) {
ctxS.beginPath();
ctxS.moveTo(x - 6, y - 6); ctxS.lineTo(x + 6, y + 6);
ctxS.moveTo(x - 6, y + 6); ctxS.lineTo(x + 6, y - 6);
ctxS.strokeStyle = color;
ctxS.lineWidth = 3;
ctxS.stroke();
}
drawCross(px, py1, poleColor);
drawCross(px, py2, poleColor);
ctxS.fillStyle = '#fff';
ctxS.font = '14px sans-serif';
ctxS.fillText(t('polePos'), px + 15, py1 + 5);
ctxS.fillText(t('poleNeg'), px + 15, py2 + 5);
ctxS.beginPath();
ctxS.setLineDash([5, 5]);
ctxS.moveTo(originX, originY);
ctxS.lineTo(px, py1);
ctxS.moveTo(originX, originY);
ctxS.lineTo(px, py2);
ctxS.strokeStyle = poleColor;
ctxS.lineWidth = 1.5;
ctxS.stroke();
ctxS.setLineDash([]);
}
function drawTimeDomain(sigma, omega, strokeColor) {
const w = timeCanvas.clientWidth;
const h = timeCanvas.clientHeight;
if (w <= 0 || h <= 0) return;
ctxT.clearRect(0, 0, w, h);
const originY = h / 2;
ctxT.strokeStyle = '#222';
ctxT.lineWidth = 1;
ctxT.beginPath();
for (let i = 0; i < w; i += 50) { ctxT.moveTo(i, 0); ctxT.lineTo(i, h); }
for (let i = 0; i < h; i += 40) { ctxT.moveTo(0, i); ctxT.lineTo(w, i); }
ctxT.stroke();
ctxT.beginPath();
ctxT.moveTo(0, originY); ctxT.lineTo(w, originY);
ctxT.strokeStyle = '#666';
ctxT.lineWidth = 2;
ctxT.stroke();
const timeDuration = 10;
let outOfBounds = false;
ctxT.beginPath();
for (let px = 0; px <= w; px++) {
const tSec = (px / w) * timeDuration;
const y = Math.exp(sigma * tSec) * Math.cos(omega * tSec);
const py = originY - (y * (h / 4));
if (py < -h || py > h * 2) outOfBounds = true;
if (px === 0) ctxT.moveTo(px, py);
else ctxT.lineTo(px, py);
}
ctxT.strokeStyle = strokeColor;
ctxT.lineWidth = 3;
ctxT.stroke();
if (outOfBounds) {
ctxT.fillStyle = 'rgba(255, 107, 107, 0.8)';
ctxT.font = 'bold 16px sans-serif';
ctxT.fillText(t('collapseWarn'), 20, 30);
}
}
function draw() {
const sigma = parseFloat(sigmaSlider.value);
const omega = parseFloat(omegaSlider.value);
const mag = Math.sqrt(sigma * sigma + omega * omega);
const zeta = mag > 0 ? -sigma / mag : 0;
const status = getStatus(sigma, zeta);
document.getElementById('sigma-val').textContent = sigma.toFixed(2);
document.getElementById('omega-val').textContent = omega.toFixed(1);
zetaVal.textContent = zeta.toFixed(4);
zetaVal.style.color = status.color;
statusText.textContent = status.message;
statusText.className = status.level;
statusBox.style.borderColor = status.color;
drawSPlane(sigma, omega, status.color);
drawTimeDomain(sigma, omega, status.color);
}
sigmaSlider.addEventListener('input', draw);
omegaSlider.addEventListener('input', draw);
document.getElementById('lang-zh').addEventListener('click', () => {
if (lang !== 'zh-TW') { lang = 'zh-TW'; applyLanguage(); }
});
document.getElementById('lang-en').addEventListener('click', () => {
if (lang !== 'en') { lang = 'en'; applyLanguage(); }
});
applyLanguage();
requestAnimationFrame(() => {
requestAnimationFrame(resizeCanvases);
});
</script>
</body>
</html>