"use strict"; // Application entry point — state, animation loop, event wiring, UI updates. // Depends on: TLUtils (utils.js), TLPhysics (physics.js), TLRender (render.js) (() => { const { clamp, fmt, resizeCanvasToCSS } = TLUtils; const { computeWaveParams, buildBounceSeries, computeDynamicState } = TLPhysics; const { getTheme, drawCircuit, drawPlot, drawTDR, ensurePlotCanvasHeight } = TLRender; // ---- DOM references ---- const el = { tdr: document.getElementById("tdr"), circuit: document.getElementById("circuit"), plot: document.getElementById("plot"), startBtn: document.getElementById("startBtn"), pauseBtn: document.getElementById("pauseBtn"), resetBtn: document.getElementById("resetBtn"), Vg: document.getElementById("Vg"), Rg: document.getElementById("Rg"), RL: document.getElementById("RL"), RLOpen: document.getElementById("RLOpen"), segCount: document.getElementById("segCount"), segZ0List: document.getElementById("segZ0List"), secPerTau: document.getElementById("secPerTau"), secPerTauRead: document.getElementById("secPerTauRead"), reflectTol: document.getElementById("reflectTol"), tRead: document.getElementById("tRead"), gLRead: document.getElementById("gLRead"), vsRead: document.getElementById("vsRead"), vlRead: document.getElementById("vlRead"), derivedValues: document.getElementById("derivedValues"), waveValues: document.getElementById("waveValues"), riseMode: document.getElementById("riseMode"), riseTau: document.getElementById("riseTau"), }; // ---- model (physics parameters) ---- const model = { Vg: 5, Rg: 20, segments: [{ Z0: 50 }], // array of N equal-length segments, each with a Z0 RL: 30, secPerTau: 2.5, reflectTol: 1, riseTimeTr: 0, riseShape: "step", // "step" | "linear" | "erf" }; // ---- segment input management ---- let segZ0Inputs = []; function buildSegmentInputs(n) { // Preserve existing values where possible. const prev = segZ0Inputs.map((inp) => parseFloat(inp.value) || 50); el.segZ0List.innerHTML = ""; segZ0Inputs = []; for (let i = 0; i < n; i++) { const val = (prev[i] != null) ? prev[i] : 50; const inp = document.createElement("input"); inp.type = "number"; inp.min = "0.1"; inp.step = "0.1"; inp.value = val; inp.title = `Z\u2080 of segment ${i + 1} (\u03A9)`; inp.addEventListener("input", () => { if (!running) render(); }); inp.addEventListener("change", () => { if (!running) render(); }); el.segZ0List.appendChild(inp); segZ0Inputs.push(inp); } } // Initialise with N=1. buildSegmentInputs(1); // ---- open-circuit toggle state ---- let rlIsOpen = false; // ---- animation state ---- let running = false; let hasStarted = false; let tNorm = 0; let lastTS = null; let timeHorizon = 2.2; let mathjaxTypesetDone = false; let theme = getTheme(); // ---- model sync ---- function syncModelFromInputs() { model.Vg = parseFloat(el.Vg.value); model.Rg = parseFloat(el.Rg.value); model.RL = rlIsOpen ? Infinity : parseFloat(el.RL.value); model.secPerTau = parseFloat(el.secPerTau.value); model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); model.riseShape = el.riseMode.value === "step" ? "step" : el.riseMode.value === "linear" ? "linear" : "erf"; model.riseTimeTr = model.riseShape !== "step" ? Math.max(0.001, parseFloat(el.riseTau.value) || 0.1) : 0; model.segments = segZ0Inputs.map((inp) => ({ Z0: Math.max(0.1, parseFloat(inp.value) || 50), })); } // ---- derived-value readout panel ---- function updateDerivedDisplays(model, waves, bounce) { const N = model.segments.length; const gS = bounce.gSEffective; const lines = [ `\u0393S = ${fmt(gS, 6)}`, `\u0393L = ${fmt(waves.gL, 6)}`, ]; if (N > 1) { for (let i = 0; i < N - 1; i++) { const Zl = model.segments[i].Z0; const Zr = model.segments[i + 1].Z0; const g = (Zr - Zl) / (Zr + Zl); lines.push(`\u0393${i + 1}\u2192${i + 2} = ${fmt(g, 6)} (${Zl}\u03A9\u2192${Zr}\u03A9)`); } } lines.push(`V1 = ${fmt(waves.V1, 6)} V`); if (N === 1) { // Single-segment detail: show V2, V3, suppression reason. const V2calc = waves.gL * waves.V1; const V3calc = gS * V2calc; const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; const v3Reason = v3Suppressed ? `suppressed since |V3| \u2264 \u03B5 (\u03B5=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V1|)` : "nonzero, so re-reflection should appear"; lines.push( `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`, `V2 = \u0393L\u00B7V1 = (${fmt(waves.gL, 6)})\u00B7(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, `V3 = \u0393S\u00B7V2 = (${fmt(gS, 6)})\u00B7(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, `V3 status: ${v3Reason}`, ); } else { lines.push(`Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V1| = ${fmt(bounce.ampTol, 6)} V`); } lines.push(`Total generated waves: ${bounce.series.length}`); el.derivedValues.innerHTML = lines.map((x) => `
${x}
`).join(""); el.waveValues.innerHTML = bounce.series.map((w) => { const arrow = w.dir > 0 ? "\u2192" : "\u2190"; const segStr = N > 1 ? ` [seg ${w.segIdx + 1}]` : ""; return `
V${w.n} ${arrow}${segStr} = ${fmt(w.A, 6)} V
`; }).join(""); } // ---- render frame ---- function render() { syncModelFromInputs(); const waves = computeWaveParams(model); const bounce = buildBounceSeries(model, waves); const dyn = computeDynamicState(tNorm, bounce, model.riseTimeTr, model.riseShape); timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); el.secPerTauRead.textContent = fmt(model.secPerTau, 1); el.tRead.textContent = fmt(tNorm, 3); el.gLRead.textContent = fmt(waves.gL, 3); el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; updateDerivedDisplays(model, waves, bounce); if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { mathjaxTypesetDone = true; window.MathJax.typesetPromise(); } const d = resizeCanvasToCSS(el.tdr); drawTDR(d.ctx, d.w, d.h, tNorm, bounce, model.riseTimeTr, model.riseShape, timeHorizon, theme); const c = resizeCanvasToCSS(el.circuit); drawCircuit(c.ctx, c.w, c.h, tNorm, dyn, theme, model.segments, model.RL); ensurePlotCanvasHeight(el.plot, 2); const p = resizeCanvasToCSS(el.plot); drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme, model.riseTimeTr, model.riseShape, model.segments); if (!hasStarted) { el.pauseBtn.textContent = "Pause"; } else { el.pauseBtn.textContent = running ? "Pause" : "Resume"; } el.pauseBtn.disabled = !hasStarted; } // ---- animation loop ---- function tick(ts) { if (!running) return; if (lastTS == null) lastTS = ts; const dt = (ts - lastTS) / 1000; lastTS = ts; tNorm += dt / Math.max(0.2, model.secPerTau); if (tNorm > timeHorizon) { running = false; lastTS = null; } render(); requestAnimationFrame(tick); } // ---- transport controls ---- function start() { hasStarted = true; tNorm = 0; running = true; lastTS = null; render(); requestAnimationFrame(tick); } function pause() { if (!hasStarted) return; if (running) { running = false; lastTS = null; render(); return; } if (tNorm >= timeHorizon - 1e-9) tNorm = 0; running = true; lastTS = null; render(); requestAnimationFrame(tick); } function reset() { running = false; hasStarted = false; lastTS = null; tNorm = 0; render(); } // ---- event wiring ---- el.RLOpen.addEventListener("click", () => { rlIsOpen = !rlIsOpen; el.RL.disabled = rlIsOpen; el.RLOpen.classList.toggle("active", rlIsOpen); if (!running) render(); }); el.startBtn.addEventListener("click", start); el.pauseBtn.addEventListener("click", pause); el.resetBtn.addEventListener("click", reset); for (const inp of [el.Vg, el.Rg, el.RL, el.secPerTau, el.reflectTol, el.riseTau]) { inp.addEventListener("input", () => { if (!running) render(); }); inp.addEventListener("change", () => { if (!running) render(); }); } el.segCount.addEventListener("input", () => { const n = Math.max(1, Math.min(10, parseInt(el.segCount.value) || 1)); el.segCount.value = n; buildSegmentInputs(n); if (!running) render(); }); el.riseMode.addEventListener("change", () => { el.riseTau.disabled = el.riseMode.value === "step"; if (!running) render(); }); window.addEventListener("resize", () => { theme = getTheme(); render(); }); // Initial render render(); })();