this repo has no description
at canon 276 lines 9.5 kB view raw
1"use strict"; 2 3// Application entry point — state, animation loop, event wiring, UI updates. 4// Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) 5(() => { 6 const { clamp, fmt, resizeCanvasToCSS } = TLUtils; 7 const { computeWaveParams, buildBounceSeries, computeDynamicState } = TLPhysics; 8 const { getTheme, drawCircuit, drawPlot, drawTDR, ensurePlotCanvasHeight } = TLRender; 9 10 // ---- DOM references ---- 11 const el = { 12 tdr: document.getElementById("tdr"), 13 circuit: document.getElementById("circuit"), 14 plot: document.getElementById("plot"), 15 startBtn: document.getElementById("startBtn"), 16 pauseBtn: document.getElementById("pauseBtn"), 17 resetBtn: document.getElementById("resetBtn"), 18 Vg: document.getElementById("Vg"), 19 Rg: document.getElementById("Rg"), 20 RL: document.getElementById("RL"), 21 RLOpen: document.getElementById("RLOpen"), 22 segCount: document.getElementById("segCount"), 23 segZ0List: document.getElementById("segZ0List"), 24 secPerTau: document.getElementById("secPerTau"), 25 secPerTauRead: document.getElementById("secPerTauRead"), 26 reflectTol: document.getElementById("reflectTol"), 27 tRead: document.getElementById("tRead"), 28 gLRead: document.getElementById("gLRead"), 29 vsRead: document.getElementById("vsRead"), 30 vlRead: document.getElementById("vlRead"), 31 derivedValues: document.getElementById("derivedValues"), 32 waveValues: document.getElementById("waveValues"), 33 riseMode: document.getElementById("riseMode"), 34 riseTau: document.getElementById("riseTau"), 35 }; 36 37 // ---- model (physics parameters) ---- 38 const model = { 39 Vg: 5, 40 Rg: 20, 41 segments: [{ Z0: 50 }], // array of N equal-length segments, each with a Z0 42 RL: 30, 43 secPerTau: 2.5, 44 reflectTol: 1, 45 riseTimeTr: 0, 46 riseShape: "step", // "step" | "linear" | "erf" 47 }; 48 49 // ---- segment input management ---- 50 let segZ0Inputs = []; 51 52 function buildSegmentInputs(n) { 53 // Preserve existing values where possible. 54 const prev = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); 55 el.segZ0List.innerHTML = ""; 56 segZ0Inputs = []; 57 for (let i = 0; i < n; i++) { 58 const val = (prev[i] != null) ? prev[i] : 50; 59 const inp = document.createElement("input"); 60 inp.type = "number"; 61 inp.min = "0.1"; 62 inp.step = "0.1"; 63 inp.value = val; 64 inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; 65 inp.addEventListener("input", () => { if (!running) render(); }); 66 inp.addEventListener("change", () => { if (!running) render(); }); 67 el.segZ0List.appendChild(inp); 68 segZ0Inputs.push(inp); 69 } 70 } 71 72 // Initialise with N=1. 73 buildSegmentInputs(1); 74 75 // ---- open-circuit toggle state ---- 76 let rlIsOpen = false; 77 78 // ---- animation state ---- 79 let running = false; 80 let hasStarted = false; 81 let tNorm = 0; 82 let lastTS = null; 83 let timeHorizon = 2.2; 84 85 let mathjaxTypesetDone = false; 86 let theme = getTheme(); 87 88 // ---- model sync ---- 89 function syncModelFromInputs() { 90 model.Vg = parseFloat(el.Vg.value); 91 model.Rg = parseFloat(el.Rg.value); 92 model.RL = rlIsOpen ? Infinity : parseFloat(el.RL.value); 93 model.secPerTau = parseFloat(el.secPerTau.value); 94 model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 95 model.riseShape = el.riseMode.value === "step" ? "step" 96 : el.riseMode.value === "linear" ? "linear" : "erf"; 97 model.riseTimeTr = model.riseShape !== "step" 98 ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) 99 : 0; 100 model.segments = segZ0Inputs.map((inp) => ({ 101 Z0: Math.max(0.1, parseFloat(inp.value) || 50), 102 })); 103 } 104 105 // ---- derived-value readout panel ---- 106 function updateDerivedDisplays(model, waves, bounce) { 107 const N = model.segments.length; 108 const gS = bounce.gSEffective; 109 110 const lines = [ 111 `\u0393<sub>S</sub> = ${fmt(gS, 6)}`, 112 `\u0393<sub>L</sub> = ${fmt(waves.gL, 6)}`, 113 ]; 114 115 if (N > 1) { 116 for (let i = 0; i < N - 1; i++) { 117 const Zl = model.segments[i].Z0; 118 const Zr = model.segments[i + 1].Z0; 119 const g = (Zr - Zl) / (Zr + Zl); 120 lines.push(`\u0393<sub>${i + 1}\u2192${i + 2}</sub> = ${fmt(g, 6)} (${Zl}\u03A9\u2192${Zr}\u03A9)`); 121 } 122 } 123 124 lines.push(`V<sub>1</sub> = ${fmt(waves.V1, 6)} V`); 125 126 if (N === 1) { 127 // Single-segment detail: show V2, V3, suppression reason. 128 const V2calc = waves.gL * waves.V1; 129 const V3calc = gS * V2calc; 130 const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 131 const v3Reason = v3Suppressed 132 ? `suppressed since |V<sub>3</sub>| \u2264 \u03B5 (\u03B5=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>|)` 133 : "nonzero, so re-reflection should appear"; 134 lines.push( 135 `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>| = ${fmt(bounce.ampTol, 6)} V`, 136 `V<sub>2</sub> = \u0393<sub>L</sub>\u00B7V<sub>1</sub> = (${fmt(waves.gL, 6)})\u00B7(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, 137 `V<sub>3</sub> = \u0393<sub>S</sub>\u00B7V<sub>2</sub> = (${fmt(gS, 6)})\u00B7(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, 138 `V<sub>3</sub> status: ${v3Reason}`, 139 ); 140 } else { 141 lines.push(`Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V<sub>1</sub>| = ${fmt(bounce.ampTol, 6)} V`); 142 } 143 144 lines.push(`Total generated waves: ${bounce.series.length}`); 145 el.derivedValues.innerHTML = lines.map((x) => `<div>${x}</div>`).join(""); 146 147 el.waveValues.innerHTML = bounce.series.map((w) => { 148 const arrow = w.dir > 0 ? "\u2192" : "\u2190"; 149 const segStr = N > 1 ? ` [seg&nbsp;${w.segIdx + 1}]` : ""; 150 return `<div>V<sub>${w.n}</sub> ${arrow}${segStr} = ${fmt(w.A, 6)} V</div>`; 151 }).join(""); 152 } 153 154 // ---- render frame ---- 155 function render() { 156 syncModelFromInputs(); 157 const waves = computeWaveParams(model); 158 const bounce = buildBounceSeries(model, waves); 159 const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); 160 timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 161 162 el.secPerTauRead.textContent = fmt(model.secPerTau, 1); 163 el.tRead.textContent = fmt(tNorm, 3); 164 el.gLRead.textContent = fmt(waves.gL, 3); 165 el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 166 el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 167 updateDerivedDisplays(model, waves, bounce); 168 169 if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 170 mathjaxTypesetDone = true; 171 window.MathJax.typesetPromise(); 172 } 173 174 const d = resizeCanvasToCSS(el.tdr); 175 drawTDR(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); 176 177 const c = resizeCanvasToCSS(el.circuit); 178 drawCircuit(c.ctx, c.w, c.h, tNorm, dyn, theme, model.segments, model.RL); 179 180 ensurePlotCanvasHeight(el.plot, 2); 181 const p = resizeCanvasToCSS(el.plot); 182 drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); 183 184 if (!hasStarted) { 185 el.pauseBtn.textContent = "Pause"; 186 } else { 187 el.pauseBtn.textContent = running ? "Pause" : "Resume"; 188 } 189 el.pauseBtn.disabled = !hasStarted; 190 } 191 192 // ---- animation loop ---- 193 function tick(ts) { 194 if (!running) return; 195 if (lastTS == null) lastTS = ts; 196 const dt = (ts - lastTS) / 1000; 197 lastTS = ts; 198 199 tNorm += dt / Math.max(0.2, model.secPerTau); 200 201 if (tNorm > timeHorizon) { 202 running = false; 203 lastTS = null; 204 } 205 206 render(); 207 requestAnimationFrame(tick); 208 } 209 210 // ---- transport controls ---- 211 function start() { 212 hasStarted = true; 213 tNorm = 0; 214 running = true; 215 lastTS = null; 216 render(); 217 requestAnimationFrame(tick); 218 } 219 220 function pause() { 221 if (!hasStarted) return; 222 if (running) { 223 running = false; 224 lastTS = null; 225 render(); 226 return; 227 } 228 if (tNorm >= timeHorizon - 1e-9) tNorm = 0; 229 running = true; 230 lastTS = null; 231 render(); 232 requestAnimationFrame(tick); 233 } 234 235 function reset() { 236 running = false; 237 hasStarted = false; 238 lastTS = null; 239 tNorm = 0; 240 render(); 241 } 242 243 // ---- event wiring ---- 244 el.RLOpen.addEventListener("click", () => { 245 rlIsOpen = !rlIsOpen; 246 el.RL.disabled = rlIsOpen; 247 el.RLOpen.classList.toggle("active", rlIsOpen); 248 if (!running) render(); 249 }); 250 251 el.startBtn.addEventListener("click", start); 252 el.pauseBtn.addEventListener("click", pause); 253 el.resetBtn.addEventListener("click", reset); 254 255 for (const inp of [el.Vg, el.Rg, el.RL, el.secPerTau, el.reflectTol, el.riseTau]) { 256 inp.addEventListener("input", () => { if (!running) render(); }); 257 inp.addEventListener("change", () => { if (!running) render(); }); 258 } 259 260 el.segCount.addEventListener("input", () => { 261 const n = Math.max(1, Math.min(10, parseInt(el.segCount.value) || 1)); 262 el.segCount.value = n; 263 buildSegmentInputs(n); 264 if (!running) render(); 265 }); 266 267 el.riseMode.addEventListener("change", () => { 268 el.riseTau.disabled = el.riseMode.value === "step"; 269 if (!running) render(); 270 }); 271 272 window.addEventListener("resize", () => { theme = getTheme(); render(); }); 273 274 // Initial render 275 render(); 276})();