this repo has no description
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 ${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})();