this repo has no description

Refactor

oscillatory.net f28b5e48 fc464ab7

verified
+862 -846
+215
app.js
··· 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, ensurePlotCanvasHeight } = TLRender; 9 + 10 + // ---- DOM references ---- 11 + const el = { 12 + circuit: document.getElementById("circuit"), 13 + plot: document.getElementById("plot"), 14 + startBtn: document.getElementById("startBtn"), 15 + pauseBtn: document.getElementById("pauseBtn"), 16 + resetBtn: document.getElementById("resetBtn"), 17 + scrubToggle: document.getElementById("scrubToggle"), 18 + singleBounce: document.getElementById("singleBounce"), 19 + Vg: document.getElementById("Vg"), 20 + Rg: document.getElementById("Rg"), 21 + Z0: document.getElementById("Z0"), 22 + RL: document.getElementById("RL"), 23 + secPerTau: document.getElementById("secPerTau"), 24 + reflectTol: document.getElementById("reflectTol"), 25 + tRead: document.getElementById("tRead"), 26 + gLRead: document.getElementById("gLRead"), 27 + vsRead: document.getElementById("vsRead"), 28 + vlRead: document.getElementById("vlRead"), 29 + derivedValues: document.getElementById("derivedValues"), 30 + waveValues: document.getElementById("waveValues"), 31 + }; 32 + 33 + // ---- model (physics parameters) ---- 34 + // All entries here correspond to user-adjustable controls. 35 + // riseTimeTau: placeholder for future finite-rise-time feature (0 = instantaneous step). 36 + const model = { 37 + Vg: 5, 38 + Rg: 50, 39 + Z0: 50, 40 + RL: 30, 41 + secPerTau: 2.5, // animation speed: real seconds per τ_d 42 + reflectTol: 1, // % of |V1| below which reflections are dropped 43 + singleBounce: true, // when true, treat Γ_S = 0 (suppress re-reflection at source) 44 + // riseTimeTau: 0, // future: RC rise time in units of τ_d 45 + }; 46 + 47 + // ---- animation state ---- 48 + let running = false; 49 + let hasStarted = false; 50 + let tNorm = 0; // current time in units of τ_d 51 + let lastTS = null; // timestamp from previous rAF tick 52 + let timeHorizon = 2.2; // animation end time (updated each frame from bounce series) 53 + 54 + let mathjaxTypesetDone = false; 55 + let theme = getTheme(); 56 + 57 + // ---- model sync ---- 58 + function syncModelFromInputs() { 59 + model.Vg = parseFloat(el.Vg.value); 60 + model.Rg = parseFloat(el.Rg.value); 61 + model.Z0 = parseFloat(el.Z0.value); 62 + model.RL = parseFloat(el.RL.value); 63 + model.secPerTau = parseFloat(el.secPerTau.value); 64 + model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 65 + model.singleBounce = !!el.singleBounce.checked; 66 + } 67 + 68 + // ---- derived-value readout panel ---- 69 + function updateDerivedDisplays(waves, bounce) { 70 + const gSEffective = bounce.gSEffective; 71 + const V2calc = waves.gL * waves.V1; 72 + const V3calc = gSEffective * V2calc; 73 + const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 74 + const v3Reason = model.singleBounce 75 + ? "suppressed because single-bounce mode forces Γ_S = 0" 76 + : (v3Suppressed 77 + ? `suppressed since |V_3| ≤ ε (ε=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V_1|)` 78 + : "nonzero, so re-reflection should appear"); 79 + 80 + el.derivedValues.innerHTML = [ 81 + `Γ_L = ${fmt(waves.gL, 6)}`, 82 + `Γ_S = ${fmt(gSEffective, 6)}${model.singleBounce ? " (forced by single-bounce mode)" : ""}`, 83 + `V_1 = ${fmt(waves.V1, 6)} V`, 84 + `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V_1| = ${fmt(bounce.ampTol, 6)} V`, 85 + `V_2 = Γ_L·V_1 = (${fmt(waves.gL, 6)})·(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, 86 + `V_3 = Γ_S·V_2 = (${fmt(gSEffective, 6)})·(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, 87 + `V_3 status: ${v3Reason}`, 88 + `Total generated waves: ${bounce.series.length}`, 89 + ].map((x) => `<div>${x}</div>`).join(""); 90 + 91 + const waveLines = bounce.series.map((w) => { 92 + const kind = w.dir > 0 ? "incident (→ load)" : "reflected (→ source)"; 93 + return `<div>V<sub>${w.n}</sub> [${kind}] = ${fmt(w.A, 6)} V</div>`; 94 + }); 95 + if (bounce.series.length < 3) { 96 + waveLines.push(`<div>V<sub>3</sub> not launched: ${v3Reason}</div>`); 97 + } 98 + el.waveValues.innerHTML = waveLines.join(""); 99 + } 100 + 101 + // ---- render frame ---- 102 + function render() { 103 + syncModelFromInputs(); 104 + const waves = computeWaveParams(model); 105 + const bounce = buildBounceSeries(model, waves); 106 + const dyn = computeDynamicState(tNorm, bounce); 107 + timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 108 + 109 + el.tRead.textContent = fmt(tNorm, 3); 110 + el.gLRead.textContent = fmt(waves.gL, 3); 111 + el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 112 + el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 113 + updateDerivedDisplays(waves, bounce); 114 + 115 + if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 116 + mathjaxTypesetDone = true; 117 + window.MathJax.typesetPromise(); 118 + } 119 + 120 + const c = resizeCanvasToCSS(el.circuit); 121 + drawCircuit(c.ctx, c.w, c.h, tNorm, dyn, theme); 122 + 123 + ensurePlotCanvasHeight(el.plot, 2); 124 + const p = resizeCanvasToCSS(el.plot); 125 + drawPlot(p.ctx, p.w, p.h, tNorm, dyn, theme); 126 + 127 + if (!hasStarted) { 128 + el.pauseBtn.textContent = "Pause"; 129 + } else { 130 + el.pauseBtn.textContent = running ? "Pause" : "Resume"; 131 + } 132 + el.pauseBtn.disabled = !hasStarted; 133 + } 134 + 135 + // ---- animation loop ---- 136 + function tick(ts) { 137 + if (!running) return; 138 + if (lastTS == null) lastTS = ts; 139 + const dt = (ts - lastTS) / 1000; 140 + lastTS = ts; 141 + 142 + tNorm += dt / Math.max(0.2, model.secPerTau); 143 + 144 + if (tNorm > timeHorizon) { 145 + if (model.singleBounce) { 146 + running = false; 147 + lastTS = null; 148 + } else { 149 + tNorm = 0; 150 + } 151 + } 152 + 153 + render(); 154 + requestAnimationFrame(tick); 155 + } 156 + 157 + // ---- transport controls ---- 158 + function start() { 159 + hasStarted = true; 160 + tNorm = 0; 161 + running = true; 162 + lastTS = null; 163 + render(); 164 + requestAnimationFrame(tick); 165 + } 166 + 167 + function pause() { 168 + if (!hasStarted) return; 169 + if (running) { 170 + running = false; 171 + lastTS = null; 172 + render(); 173 + return; 174 + } 175 + // Resume; if already at the end, wrap back to t=0. 176 + if (tNorm >= timeHorizon - 1e-9) tNorm = 0; 177 + running = true; 178 + lastTS = null; 179 + render(); 180 + requestAnimationFrame(tick); 181 + } 182 + 183 + function reset() { 184 + running = false; 185 + hasStarted = false; 186 + lastTS = null; 187 + tNorm = 0; 188 + render(); 189 + } 190 + 191 + // ---- scrubbing ---- 192 + function scrubFromEvent(ev) { 193 + if (!el.scrubToggle.checked || running) return; 194 + const rect = ev.target.getBoundingClientRect(); 195 + tNorm = clamp(ev.clientX - rect.left, 0, rect.width) / rect.width * timeHorizon; 196 + render(); 197 + } 198 + 199 + // ---- event wiring ---- 200 + el.startBtn.addEventListener("click", start); 201 + el.pauseBtn.addEventListener("click", pause); 202 + el.resetBtn.addEventListener("click", reset); 203 + 204 + for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.reflectTol, el.singleBounce]) { 205 + inp.addEventListener("input", () => { if (!running) render(); }); 206 + inp.addEventListener("change", () => { if (!running) render(); }); 207 + } 208 + 209 + el.plot.addEventListener("mousemove", scrubFromEvent); 210 + el.circuit.addEventListener("mousemove", scrubFromEvent); 211 + window.addEventListener("resize", () => { theme = getTheme(); render(); }); 212 + 213 + // Initial render 214 + render(); 215 + })();
+136
physics.js
··· 1 + "use strict"; 2 + 3 + // Transmission line physics — pure functions, no DOM or canvas dependencies. 4 + // 5 + // Spatial convention: z ∈ [0, 1] maps to [0, ℓ]. 6 + // Time convention: tNorm is dimensionless time in units of τ_d (one-way delay). 7 + // A wave launched at tNorm=launch reaches the far end at tNorm=launch+1. 8 + // 9 + // Future extension points: 10 + // • Rise time (RC/finite bandwidth): model.riseTimeTau > 0 means each wave front has a 11 + // smooth leading edge V = V_final * (1 − exp(−Δt / riseTimeTau)) rather than a hard step. 12 + // Δt for a rightward wave at position z is (tNorm − launch − z); for leftward, (tNorm − launch − (1−z)). 13 + // When implemented, totalSegmentsForWaves() in render.js would be replaced by a sampled 14 + // continuous V(z) function evaluated per-pixel. 15 + // 16 + // • Multi-segment lines: replace single Z0 with an array of segments [{Z0, length}]. 17 + // Each impedance boundary generates both a reflected wave (Γ) and a transmitted wave (T = 1+Γ). 18 + // buildBounceSeries() would be replaced by a segment-aware lattice diagram solver. 19 + 20 + const TLPhysics = (() => { 21 + const BOUNCE_EPS = 1e-6; 22 + const MAX_BOUNCES = 5000; // safety cap; ordinary use stays well under 50 23 + 24 + // ---- reflection coefficients and initial voltage step ---- 25 + // Returns { V1, gL, gS } 26 + function computeWaveParams(model) { 27 + const { Vg, Rg, Z0, RL } = model; 28 + const V1 = Vg * Z0 / (Rg + Z0); // voltage of the first incident wave 29 + const gL = (RL - Z0) / (RL + Z0); // reflection coefficient at load 30 + const gS = (Rg - Z0) / (Rg + Z0); // reflection coefficient at source 31 + return { V1, gL, gS }; 32 + } 33 + 34 + // ---- bounce series ---- 35 + // Builds the sequence of travelling waves produced by successive reflections. 36 + // Returns { 37 + // series: [{n, A, dir, launch}] — all generated waves in order 38 + // srcEvents: [{t, dV}] — cumulative voltage steps at z=0 (VS) 39 + // loadEvents: [{t, dV}] — cumulative voltage steps at z=ℓ (VL) 40 + // gSEffective, tolPct, ampTol, tEnd 41 + // } 42 + function buildBounceSeries(model, waves) { 43 + const gS = model.singleBounce ? 0 : waves.gS; 44 + const tolPct = Number.isFinite(model.reflectTol) ? model.reflectTol : 1; 45 + const ampTol = Math.max(BOUNCE_EPS, Math.abs(waves.V1) * (tolPct / 100)); 46 + 47 + // wave n=1: first incident, rightward, launched at tNorm=0 48 + const series = [{ n: 1, A: waves.V1, dir: +1, launch: 0 }]; 49 + 50 + let A = waves.V1; 51 + let launch = 1; // each successive wave launches one τ_d later 52 + let reflectAtLoad = true; // V2 is the first reflection, from the load 53 + 54 + while (series.length < MAX_BOUNCES) { 55 + const g = reflectAtLoad ? waves.gL : gS; 56 + const nextA = A * g; 57 + if (Math.abs(nextA) <= ampTol) break; 58 + series.push({ n: series.length + 1, A: nextA, dir: reflectAtLoad ? -1 : +1, launch }); 59 + A = nextA; 60 + launch++; 61 + reflectAtLoad = !reflectAtLoad; 62 + } 63 + 64 + // Node voltage histories (cumulative step events). 65 + // VS: V source-side = sum of (1+Γ_S)·V_k for every left-going wave that arrives at source, 66 + // plus V1 at t=0 (the initial step seen at z=0 when the switch closes). 67 + // VL: V load-side = sum of (1+Γ_L)·V_k for every right-going wave that arrives at load. 68 + const srcEvents = [{ t: 0, dV: waves.V1 }]; 69 + const loadEvents = []; 70 + for (const w of series) { 71 + const arrive = w.launch + 1; 72 + if (w.dir > 0) { 73 + loadEvents.push({ t: arrive, dV: (1 + waves.gL) * w.A }); 74 + } else { 75 + srcEvents.push({ t: arrive, dV: (1 + gS) * w.A }); 76 + } 77 + } 78 + 79 + return { 80 + series, 81 + srcEvents, 82 + loadEvents, 83 + gSEffective: gS, 84 + tolPct, 85 + ampTol, 86 + tEnd: series.length > 0 ? series[series.length - 1].launch + 1 : 2, 87 + }; 88 + } 89 + 90 + // ---- time query: node voltage ---- 91 + // Sum all voltage step events that have occurred by time tn. 92 + function sumEventsAtTime(events, tn) { 93 + const eps = 1e-9; 94 + let v = 0; 95 + for (const e of events) { 96 + if (tn >= e.t - eps) v += e.dV; 97 + } 98 + return v; 99 + } 100 + 101 + // ---- dynamic state at a given tNorm ---- 102 + // Returns the set of waves currently propagating and the node voltages. 103 + // Each wave in launchedWaves gets two extra fields: 104 + // u: time elapsed since launch (tNorm − launch) 105 + // front: current position of the leading edge, ∈ [0,1] 106 + function computeDynamicState(tn, bounce) { 107 + const { clamp } = TLUtils; 108 + const launchedWaves = []; 109 + const activeWaves = []; // waves whose front is still in transit 110 + 111 + for (const w of bounce.series) { 112 + const u = tn - w.launch; 113 + if (u < 0) continue; 114 + const front = w.dir > 0 ? clamp(u, 0, 1) : clamp(1 - u, 0, 1); 115 + const ww = { ...w, u, front }; 116 + launchedWaves.push(ww); 117 + if (u < 1) activeWaves.push(ww); 118 + } 119 + 120 + return { 121 + launchedWaves, 122 + activeWaves, 123 + VS: sumEventsAtTime(bounce.srcEvents, tn), 124 + VL: sumEventsAtTime(bounce.loadEvents, tn), 125 + }; 126 + } 127 + 128 + return { 129 + BOUNCE_EPS, 130 + MAX_BOUNCES, 131 + computeWaveParams, 132 + buildBounceSeries, 133 + sumEventsAtTime, 134 + computeDynamicState, 135 + }; 136 + })();
+381
render.js
··· 1 + "use strict"; 2 + 3 + // Canvas rendering — circuit diagram and V(z) plot. 4 + // All draw functions are pure with respect to app state; they receive 5 + // everything they need as parameters (ctx, dimensions, dynamic state, theme). 6 + // 7 + // Future extension points: 8 + // • drawPlot: when riseTimeTau > 0, replace piecewise-constant segment drawing 9 + // with a sampled continuous curve per wave (see physics.js comments). 10 + // • drawCircuit: for multi-segment lines, the single T-line box becomes N 11 + // abutting boxes, each labelled with its own Z0. 12 + 13 + const TLRender = (() => { 14 + // ---- plot layout constants ---- 15 + const PLOT_PANEL_H = 150; 16 + const PLOT_PANEL_GAP = 10; 17 + const PLOT_PAD_T = 14; 18 + const PLOT_PAD_B = 26; 19 + 20 + // ---- theme ---- 21 + // Read CSS custom properties into a plain object. 22 + // Call this once at init and again on resize/theme-change. 23 + function getTheme() { 24 + const get = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim(); 25 + return { 26 + bg: get("--bg"), 27 + panel: get("--panel"), 28 + ink: get("--ink"), 29 + muted: get("--muted"), 30 + grid: get("--grid"), 31 + accent: get("--accent"), 32 + accent2: get("--accent2"), 33 + warn: get("--warn"), 34 + ok: get("--ok"), 35 + }; 36 + } 37 + 38 + // ---- primitive helpers ---- 39 + function line(ctx, x0, y0, x1, y1) { 40 + ctx.beginPath(); 41 + ctx.moveTo(x0, y0); 42 + ctx.lineTo(x1, y1); 43 + ctx.stroke(); 44 + } 45 + 46 + function circle(ctx, x, y, r, strokeStyle) { 47 + ctx.strokeStyle = strokeStyle; 48 + ctx.lineWidth = 2; 49 + ctx.beginPath(); 50 + ctx.arc(x, y, r, 0, Math.PI * 2); 51 + ctx.stroke(); 52 + } 53 + 54 + function circleFill(ctx, x, y, r, fillStyle) { 55 + ctx.fillStyle = fillStyle; 56 + ctx.beginPath(); 57 + ctx.arc(x, y, r, 0, Math.PI * 2); 58 + ctx.fill(); 59 + } 60 + 61 + function label(ctx, text, x, y, color) { 62 + ctx.fillStyle = color; 63 + ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 64 + ctx.fillText(text, x, y); 65 + } 66 + 67 + // ---- circuit component primitives ---- 68 + function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp, theme) { 69 + ctx.strokeStyle = theme.ink; 70 + ctx.lineWidth = 2; 71 + const dx = x1 - x0, dy = y1 - y0; 72 + const len = Math.hypot(dx, dy); 73 + if (len < 1e-6) return; 74 + const ux = dx / len, uy = dy / len; 75 + const px = -uy, py = ux; // unit perpendicular 76 + 77 + const lead = 10; 78 + const start = { x: x0 + ux * lead, y: y0 + uy * lead }; 79 + const end = { x: x1 - ux * lead, y: y1 - uy * lead }; 80 + 81 + line(ctx, x0, y0, start.x, start.y); 82 + line(ctx, end.x, end.y, x1, y1); 83 + 84 + const segs = zigZagCount * 2; 85 + const segLen = (len - 2 * lead) / segs; 86 + ctx.beginPath(); 87 + ctx.moveTo(start.x, start.y); 88 + for (let i = 1; i < segs; i++) { 89 + const s = i * segLen; 90 + const flip = (i % 2 === 0) ? -1 : 1; 91 + ctx.lineTo(start.x + ux * s + px * amp * flip, 92 + start.y + uy * s + py * amp * flip); 93 + } 94 + ctx.lineTo(end.x, end.y); 95 + ctx.stroke(); 96 + } 97 + 98 + function drawSwitch(ctx, x, y, tn, theme) { 99 + const closed = tn > 0; 100 + ctx.strokeStyle = theme.ink; 101 + ctx.lineWidth = 2; 102 + const a = { x: x - 18, y }; 103 + const b = { x: x + 18, y }; 104 + circleFill(ctx, a.x, a.y, 3.2, theme.ink); 105 + circleFill(ctx, b.x, b.y, 3.2, theme.ink); 106 + if (closed) { 107 + line(ctx, a.x, a.y, b.x, b.y); 108 + } else { 109 + line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14); 110 + } 111 + label(ctx, "t = 0", x - 18, y + 26, theme.muted); 112 + } 113 + 114 + function drawVoltageProbe(ctx, x, yTop, yBot, name, theme) { 115 + ctx.fillStyle = theme.muted; 116 + ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 117 + ctx.fillText("+", x + 6, yTop + 12); 118 + ctx.fillText("−", x + 6, yBot - 4); 119 + label(ctx, name, x + 10, (yTop + yBot) / 2 - 6, theme.muted); 120 + } 121 + 122 + // ---- circuit canvas ---- 123 + function drawCircuit(ctx, w, h, tn, dyn, theme) { 124 + ctx.clearRect(0, 0, w, h); 125 + ctx.fillStyle = theme.panel; 126 + ctx.fillRect(0, 0, w, h); 127 + 128 + const pad = 18; 129 + const yTop = 70; 130 + const yBot = 190; 131 + 132 + // x key-points — keep these consistent with xPlot0/xPlot1 in drawPlot 133 + const xSourceL = pad + 60; 134 + const xSwitch = pad + 170; 135 + const xTL0 = pad + 240; 136 + const xTL1 = w - pad - 210; 137 + const xLoad = w - pad - 120; 138 + const xRight = w - pad - 40; 139 + 140 + ctx.lineWidth = 2; 141 + ctx.strokeStyle = theme.ink; 142 + 143 + // Top and bottom wires 144 + line(ctx, xSourceL, yTop, xSwitch - 18, yTop); 145 + line(ctx, xSwitch + 18, yTop, xTL0, yTop); 146 + line(ctx, xTL1, yTop, xLoad, yTop); 147 + line(ctx, xLoad, yTop, xRight, yTop); 148 + line(ctx, xSourceL, yBot, xTL0, yBot); 149 + line(ctx, xTL1, yBot, xRight, yBot); 150 + 151 + // Voltage source (circle + +/−) 152 + const vsx = xSourceL, vsy = (yTop + yBot) / 2; 153 + line(ctx, xSourceL, yTop, xSourceL, vsy - 20); 154 + line(ctx, xSourceL, vsy + 20, xSourceL, yBot); 155 + circle(ctx, vsx, vsy, 20, theme.ink); 156 + ctx.fillStyle = theme.ink; 157 + ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 158 + ctx.fillText("+", vsx - 4, vsy - 6); 159 + ctx.fillText("−", vsx - 4, vsy + 14); 160 + label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted); 161 + 162 + // Source resistor Rg 163 + const r0 = xSourceL + 20, r1 = xSwitch - 20; 164 + drawResistor(ctx, r0, yTop, r1, yTop, 8, 8, theme); 165 + label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted); 166 + 167 + drawSwitch(ctx, xSwitch, yTop, tn, theme); 168 + 169 + // Transmission line boxes (top and bottom conductors) 170 + ctx.strokeStyle = theme.ink; 171 + ctx.lineWidth = 2; 172 + ctx.strokeRect(xTL0, yTop - 16, xTL1 - xTL0, 32); 173 + label(ctx, "Z0", (xTL0 + xTL1) / 2 - 10, yTop - 26, theme.muted); 174 + ctx.strokeRect(xTL0, yBot - 16, xTL1 - xTL0, 32); 175 + 176 + // Load resistor RL (vertical) 177 + const rlTop = yTop + 22, rlBot = yBot - 22; 178 + ctx.strokeStyle = theme.ink; 179 + ctx.lineWidth = 2; 180 + line(ctx, xLoad, yTop, xLoad, rlTop); 181 + drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 6, 8, theme); 182 + line(ctx, xLoad, rlBot, xLoad, yBot); 183 + label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 184 + 185 + // Voltage probes 186 + drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS", theme); 187 + drawVoltageProbe(ctx, xLoad + 18, yTop, yBot, "VL", theme); 188 + 189 + // Ground reference node 190 + ctx.fillStyle = theme.ink; 191 + ctx.beginPath(); 192 + ctx.arc(xRight, yBot, 3.5, 0, Math.PI * 2); 193 + ctx.fill(); 194 + 195 + // Wavefront markers (dashed vertical lines inside T-line boxes) 196 + const tlW = xTL1 - xTL0; 197 + const wfY0 = yTop - 16; 198 + const wfY1 = yBot + 16; 199 + for (const wf of dyn.activeWaves) { 200 + const xw = xTL0 + wf.front * tlW; 201 + ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 202 + ctx.lineWidth = 3; 203 + ctx.setLineDash([9, 7]); 204 + line(ctx, xw, wfY0, xw, wfY1); 205 + ctx.setLineDash([]); 206 + } 207 + 208 + label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted); 209 + label(ctx, "z = ℓ", xTL1 - 18, yBot + 34, theme.muted); 210 + } 211 + 212 + // ---- wave-shape helpers (piecewise-constant — used until rise time is implemented) ---- 213 + 214 + // Voltage profile of a single wave: non-zero on the region behind the front. 215 + function segmentsForWave(wf) { 216 + if (wf.dir > 0) return [ 217 + { a: 0, b: wf.front, V: wf.A }, 218 + { a: wf.front, b: 1, V: 0 }, 219 + ]; 220 + return [ 221 + { a: 0, b: wf.front, V: 0 }, 222 + { a: wf.front, b: 1, V: wf.A }, 223 + ]; 224 + } 225 + 226 + // Sum of all launched waves, broken into piecewise-constant segments. 227 + function totalSegmentsForWaves(waves) { 228 + const breakpoints = [0, 1]; 229 + for (const wf of waves) breakpoints.push(wf.front); 230 + breakpoints.sort((a, b) => a - b); 231 + 232 + const pts = []; 233 + for (const x of breakpoints) { 234 + if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x); 235 + } 236 + 237 + const segs = []; 238 + for (let i = 0; i < pts.length - 1; i++) { 239 + const a = pts[i], b = pts[i + 1]; 240 + const m = (a + b) / 2; // midpoint probe 241 + let V = 0; 242 + for (const wf of waves) { 243 + if (wf.dir > 0 && m <= wf.front) V += wf.A; 244 + if (wf.dir < 0 && m >= wf.front) V += wf.A; 245 + } 246 + segs.push({ a, b, V }); 247 + } 248 + return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 249 + } 250 + 251 + // Draw a piecewise-constant voltage profile. 252 + // (When rise time is added, this becomes drawContinuousWave with sampled V(z).) 253 + function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 254 + ctx.strokeStyle = color; 255 + ctx.lineWidth = width; 256 + ctx.beginPath(); 257 + ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V)); 258 + for (const seg of segments) { 259 + ctx.lineTo(xOfZ(seg.a), yOfV(seg.V)); 260 + ctx.lineTo(xOfZ(seg.b), yOfV(seg.V)); 261 + } 262 + ctx.stroke(); 263 + } 264 + 265 + // ---- plot canvas ---- 266 + // xPlot0/xPlot1 must match the T-line box extents in drawCircuit so that 267 + // z=0 and z=ℓ align vertically between the two canvases. 268 + function drawPlot(ctx, w, h, tn, dyn, theme) { 269 + ctx.clearRect(0, 0, w, h); 270 + ctx.fillStyle = theme.panel; 271 + ctx.fillRect(0, 0, w, h); 272 + 273 + const circuitPad = 18; 274 + const xPlot0 = circuitPad + 240; 275 + const xPlot1 = w - circuitPad - 210; 276 + const plotW = xPlot1 - xPlot0; 277 + const xOfZ = (z) => xPlot0 + z * plotW; 278 + 279 + const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 280 + const shownWaves = launched.slice(0, 6); // limit component panel to 6 curves 281 + const sumSegs = totalSegmentsForWaves(launched); 282 + 283 + // Shared vertical scale: symmetric, padded, fixed for the current parameter set. 284 + const maxAbsWave = Math.max(1e-6, ...shownWaves.map((wf) => Math.abs(wf.A))); 285 + const maxAbsSum = Math.max(1e-6, sumSegs.reduce((m, s) => Math.max(m, Math.abs(s.V)), 0)); 286 + const vScale = Math.max(maxAbsWave, maxAbsSum); 287 + const vLo = -1.15 * vScale; 288 + const vHi = 1.15 * vScale; 289 + 290 + ctx.fillStyle = theme.muted; 291 + ctx.font = "12px ui-sans-serif, system-ui"; 292 + ctx.fillText("Voltage along the T-line", 12, 14); 293 + 294 + // Inner helper: draw axes/border for a panel 295 + function drawPanelFrame(top, bot, yOfV, labelText) { 296 + ctx.strokeStyle = theme.grid; 297 + ctx.lineWidth = 1; 298 + line(ctx, xPlot0, top, xPlot1, top); 299 + line(ctx, xPlot0, bot, xPlot1, bot); 300 + line(ctx, xPlot0, top, xPlot0, bot); 301 + line(ctx, xPlot1, top, xPlot1, bot); 302 + ctx.strokeStyle = theme.muted; 303 + ctx.lineWidth = 1.2; 304 + line(ctx, xPlot0, yOfV(0), xPlot1, yOfV(0)); 305 + ctx.fillStyle = theme.muted; 306 + ctx.font = "12px ui-sans-serif, system-ui"; 307 + ctx.fillText(labelText, xPlot0 + 8, top + 14); 308 + ctx.fillText("0", xPlot0 - 14, yOfV(0) + 4); 309 + ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4); 310 + } 311 + 312 + // Inner helper: dashed front-position markers 313 + function drawFrontMarkers(top, bot, fronts) { 314 + if (!fronts.length) return; 315 + ctx.strokeStyle = theme.warn; 316 + ctx.lineWidth = 1.2; 317 + ctx.setLineDash([4, 5]); 318 + for (const zf of fronts) line(ctx, xOfZ(zf), top, xOfZ(zf), bot); 319 + ctx.setLineDash([]); 320 + } 321 + 322 + const panelH = PLOT_PANEL_H; 323 + 324 + // Panel 1 — sum of all waves 325 + const top0 = PLOT_PAD_T; 326 + const bot0 = top0 + panelH; 327 + const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 328 + drawPanelFrame(top0, bot0, y0, "Sum (all waves)"); 329 + drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 330 + drawFrontMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front)); 331 + 332 + // Panel 2 — individual component waves 333 + const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP; 334 + const bot1 = top1 + panelH; 335 + const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 336 + const truncated = launched.length > shownWaves.length; 337 + drawPanelFrame(top1, bot1, y1, truncated ? "Components (V1..V6 shown)" : "Components (all waves)"); 338 + 339 + const waveStyles = [ 340 + { color: theme.accent, dash: [] }, // solid blue 341 + { color: theme.accent2, dash: [] }, // solid pink 342 + { color: theme.accent, dash: [9, 6] }, // dashed blue 343 + { color: theme.accent2, dash: [9, 6] }, // dashed pink 344 + { color: theme.accent, dash: [2, 5] }, // dotted blue 345 + { color: theme.accent2, dash: [2, 5] }, // dotted pink 346 + ]; 347 + 348 + for (let i = 0; i < shownWaves.length; i++) { 349 + const style = waveStyles[i]; 350 + ctx.setLineDash(style.dash); 351 + drawPiecewise(ctx, xOfZ, y1, segmentsForWave(shownWaves[i]), style.color, 2.0); 352 + ctx.setLineDash([]); 353 + } 354 + drawFrontMarkers(top1, bot1, shownWaves.filter((wf) => wf.u < 1).map((wf) => wf.front)); 355 + 356 + // z-axis endpoint labels (below both panels) 357 + ctx.fillStyle = theme.muted; 358 + ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 359 + ctx.fillText("0", xPlot0 - 4, h - 6); 360 + ctx.fillText("ℓ", xPlot1 - 4, h - 6); 361 + } 362 + 363 + // Resize the plot canvas height attribute to fit the given number of panels. 364 + function ensurePlotCanvasHeight(plotEl, nPanels) { 365 + const targetH = Math.round( 366 + PLOT_PAD_T + PLOT_PAD_B + 367 + nPanels * PLOT_PANEL_H + 368 + (nPanels - 1) * PLOT_PANEL_GAP 369 + ); 370 + const curr = parseInt(plotEl.getAttribute("height") || "0", 10); 371 + if (curr !== targetH) plotEl.setAttribute("height", String(targetH)); 372 + } 373 + 374 + return { 375 + PLOT_PANEL_H, PLOT_PANEL_GAP, PLOT_PAD_T, PLOT_PAD_B, 376 + getTheme, 377 + drawCircuit, 378 + drawPlot, 379 + ensurePlotCanvasHeight, 380 + }; 381 + })();
+98
style.css
··· 1 + :root { 2 + --bg: #0b0f14; 3 + --panel: #0f1620; 4 + --ink: #e8eef7; 5 + --muted: #9bb0c7; 6 + --grid: #223042; 7 + --accent: #5ad1ff; 8 + --accent2: #ff6ad5; 9 + --warn: #ffd166; 10 + --ok: #7CFF9B; 11 + } 12 + html, body { height: 100%; background: var(--bg); color: var(--ink); margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } 13 + .wrap { max-width: 1100px; margin: 0 auto; padding: 18px 14px 28px; } 14 + h1 { font-size: 18px; margin: 0 0 10px; font-weight: 650; letter-spacing: 0.2px; } 15 + .row { display: grid; grid-template-columns: 1fr; gap: 12px; } 16 + .panel { background: var(--panel); border: 1px solid #1b2736; border-radius: 14px; padding: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.25); } 17 + .controls { display: grid; grid-template-columns: 1fr; gap: 10px; align-items: end; } 18 + button { 19 + background: #162234; border: 1px solid #24364e; color: var(--ink); 20 + padding: 8px 10px; border-radius: 10px; cursor: pointer; font-weight: 600; 21 + } 22 + button:hover { border-color: #35557a; } 23 + button:active { transform: translateY(1px); } 24 + .pill { 25 + display: inline-flex; gap: 8px; align-items: center; 26 + background: #101a28; border: 1px solid #24364e; border-radius: 999px; 27 + padding: 6px 10px; color: var(--muted); font-size: 12px; 28 + } 29 + .pill b { color: var(--ink); font-weight: 650; } 30 + .controls .readouts { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } 31 + .transport { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; } 32 + label { font-size: 12px; color: var(--muted); display: grid; gap: 6px; } 33 + input[type="number"], input[type="range"] { 34 + width: 100%; 35 + background: #0c1420; color: var(--ink); 36 + border: 1px solid #24364e; border-radius: 10px; 37 + padding: 8px 10px; box-sizing: border-box; 38 + font-variant-numeric: tabular-nums; 39 + } 40 + input[type="range"] { padding: 0; height: 36px; } 41 + .cfg { display: grid; grid-template-columns: repeat(12, 1fr); gap: 10px; margin-top: 10px; } 42 + .cfg > label { grid-column: span 3; } 43 + .cfg > label.wide { grid-column: span 6; } 44 + .top-layout { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; gap: 12px; margin-top: 10px; } 45 + .cfg-vert { display: grid; gap: 8px; align-content: start; } 46 + .cfg-row { 47 + display: grid; 48 + grid-template-columns: auto 1fr auto; 49 + gap: 8px; 50 + align-items: center; 51 + font-size: 13px; 52 + color: var(--muted); 53 + background: #101a28; 54 + border: 1px solid #24364e; 55 + border-radius: 10px; 56 + padding: 8px 10px; 57 + } 58 + .cfg-extra { 59 + display: grid; 60 + gap: 8px; 61 + background: #101a28; 62 + border: 1px solid #24364e; 63 + border-radius: 10px; 64 + padding: 8px 10px; 65 + color: var(--muted); 66 + font-size: 12px; 67 + } 68 + .eq-panel { 69 + background: #101a28; 70 + border: 1px solid #24364e; 71 + border-radius: 10px; 72 + padding: 10px; 73 + } 74 + .eq-title { font-size: 12px; color: var(--ink); font-weight: 650; margin: 2px 0 6px; } 75 + .eq-lines { color: var(--muted); font-size: 13px; line-height: 1.5; } 76 + .calc-values { color: var(--muted); font-size: 13px; line-height: 1.45; margin-top: 6px; } 77 + .wave-values { 78 + margin-top: 6px; 79 + max-height: 170px; 80 + overflow: auto; 81 + border-top: 1px solid #24364e; 82 + padding-top: 6px; 83 + color: var(--muted); 84 + font-size: 12px; 85 + line-height: 1.35; 86 + } 87 + .note { color: var(--muted); font-size: 12px; line-height: 1.35; margin-top: 10px; } 88 + canvas { width: 100%; height: auto; display: block; border-radius: 12px; } 89 + .legend { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; } 90 + .key { display: inline-flex; gap: 8px; align-items: center; font-size: 12px; color: var(--muted); } 91 + .swatch { width: 12px; height: 12px; border-radius: 3px; background: var(--accent); } 92 + .swatch.ref { background: var(--accent2); } 93 + .swatch.sum { background: var(--ok); } 94 + .divider { height: 1px; background: #1b2736; margin: 10px 0; } 95 + .small { font-size: 12px; color: var(--muted); } 96 + .toggle { display:flex; align-items:center; gap:8px; margin-left: 4px; } 97 + input[type="checkbox"]{ width: 16px; height: 16px; accent-color: var(--accent); } 98 + .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-variant-numeric: tabular-nums; }
+5 -846
tline_viz.html
··· 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width,initial-scale=1" /> 6 6 <title>Transmission Line reflections</title> 7 - <style> 8 - :root { 9 - --bg: #0b0f14; 10 - --panel: #0f1620; 11 - --ink: #e8eef7; 12 - --muted: #9bb0c7; 13 - --grid: #223042; 14 - --accent: #5ad1ff; 15 - --accent2: #ff6ad5; 16 - --warn: #ffd166; 17 - --ok: #7CFF9B; 18 - } 19 - html, body { height: 100%; background: var(--bg); color: var(--ink); margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } 20 - .wrap { max-width: 1100px; margin: 0 auto; padding: 18px 14px 28px; } 21 - h1 { font-size: 18px; margin: 0 0 10px; font-weight: 650; letter-spacing: 0.2px; } 22 - .row { display: grid; grid-template-columns: 1fr; gap: 12px; } 23 - .panel { background: var(--panel); border: 1px solid #1b2736; border-radius: 14px; padding: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.25); } 24 - .controls { display: grid; grid-template-columns: 1fr; gap: 10px; align-items: end; } 25 - button { 26 - background: #162234; border: 1px solid #24364e; color: var(--ink); 27 - padding: 8px 10px; border-radius: 10px; cursor: pointer; font-weight: 600; 28 - } 29 - button:hover { border-color: #35557a; } 30 - button:active { transform: translateY(1px); } 31 - .pill { 32 - display: inline-flex; gap: 8px; align-items: center; 33 - background: #101a28; border: 1px solid #24364e; border-radius: 999px; 34 - padding: 6px 10px; color: var(--muted); font-size: 12px; 35 - } 36 - .pill b { color: var(--ink); font-weight: 650; } 37 - .controls .readouts { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } 38 - .transport { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; } 39 - label { font-size: 12px; color: var(--muted); display: grid; gap: 6px; } 40 - input[type="number"], input[type="range"] { 41 - width: 100%; 42 - background: #0c1420; color: var(--ink); 43 - border: 1px solid #24364e; border-radius: 10px; 44 - padding: 8px 10px; box-sizing: border-box; 45 - font-variant-numeric: tabular-nums; 46 - } 47 - input[type="range"] { padding: 0; height: 36px; } 48 - .cfg { display: grid; grid-template-columns: repeat(12, 1fr); gap: 10px; margin-top: 10px; } 49 - .cfg > label { grid-column: span 3; } 50 - .cfg > label.wide { grid-column: span 6; } 51 - .top-layout { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; gap: 12px; margin-top: 10px; } 52 - .cfg-vert { display: grid; gap: 8px; align-content: start; } 53 - .cfg-row { 54 - display: grid; 55 - grid-template-columns: auto 1fr auto; 56 - gap: 8px; 57 - align-items: center; 58 - font-size: 13px; 59 - color: var(--muted); 60 - background: #101a28; 61 - border: 1px solid #24364e; 62 - border-radius: 10px; 63 - padding: 8px 10px; 64 - } 65 - .cfg-extra { 66 - display: grid; 67 - gap: 8px; 68 - background: #101a28; 69 - border: 1px solid #24364e; 70 - border-radius: 10px; 71 - padding: 8px 10px; 72 - color: var(--muted); 73 - font-size: 12px; 74 - } 75 - .eq-panel { 76 - background: #101a28; 77 - border: 1px solid #24364e; 78 - border-radius: 10px; 79 - padding: 10px; 80 - } 81 - .eq-title { font-size: 12px; color: var(--ink); font-weight: 650; margin: 2px 0 6px; } 82 - .eq-lines { color: var(--muted); font-size: 13px; line-height: 1.5; } 83 - .calc-values { color: var(--muted); font-size: 13px; line-height: 1.45; margin-top: 6px; } 84 - .wave-values { 85 - margin-top: 6px; 86 - max-height: 170px; 87 - overflow: auto; 88 - border-top: 1px solid #24364e; 89 - padding-top: 6px; 90 - color: var(--muted); 91 - font-size: 12px; 92 - line-height: 1.35; 93 - } 94 - .note { color: var(--muted); font-size: 12px; line-height: 1.35; margin-top: 10px; } 95 - canvas { width: 100%; height: auto; display: block; border-radius: 12px; } 96 - .legend { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; } 97 - .key { display: inline-flex; gap: 8px; align-items: center; font-size: 12px; color: var(--muted); } 98 - .swatch { width: 12px; height: 12px; border-radius: 3px; background: var(--accent); } 99 - .swatch.ref { background: var(--accent2); } 100 - .swatch.sum { background: var(--ok); } 101 - .divider { height: 1px; background: #1b2736; margin: 10px 0; } 102 - .small { font-size: 12px; color: var(--muted); } 103 - .toggle { display:flex; align-items:center; gap:8px; margin-left: 4px; } 104 - input[type="checkbox"]{ width: 16px; height: 16px; accent-color: var(--accent); } 105 - .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-variant-numeric: tabular-nums; } 106 - </style> 7 + <link rel="stylesheet" type="text/css" href="style.css" /> 107 8 </head> 108 9 <body> 109 10 <div class="wrap"> ··· 188 89 }; 189 90 </script> 190 91 <script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script> 191 - <script> 192 - (() => { 193 - // ----------------------------- 194 - // Utilities 195 - // ----------------------------- 196 - const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); 197 - const fmt = (x, d=3) => Number.isFinite(x) ? x.toFixed(d) : "NaN"; 198 - const sign = (x) => (x > 0) - (x < 0); 199 - 200 - function getDPR() { return Math.max(1, Math.floor(window.devicePixelRatio || 1)); } 201 - 202 - function resizeCanvasToCSS(canvas) { 203 - // Canvas is already given width/height attributes for a default, 204 - // but we ensure crisp rendering on high-DPI screens. 205 - const dpr = getDPR(); 206 - const cssW = canvas.clientWidth; 207 - const cssH = Math.round(cssW * (canvas.height / canvas.width)); 208 - // Keep CSS height "auto"; only scale internal buffer. 209 - canvas.width = Math.round(cssW * dpr); 210 - canvas.height = Math.round(cssH * dpr); 211 - const ctx = canvas.getContext("2d"); 212 - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 213 - return { ctx, w: cssW, h: cssH, dpr }; 214 - } 215 - 216 - // ----------------------------- 217 - // Parameters / State 218 - // ----------------------------- 219 - const el = { 220 - circuit: document.getElementById("circuit"), 221 - plot: document.getElementById("plot"), 222 - startBtn: document.getElementById("startBtn"), 223 - pauseBtn: document.getElementById("pauseBtn"), 224 - resetBtn: document.getElementById("resetBtn"), 225 - scrubToggle: document.getElementById("scrubToggle"), 226 - singleBounce: document.getElementById("singleBounce"), 227 - Vg: document.getElementById("Vg"), 228 - Rg: document.getElementById("Rg"), 229 - Z0: document.getElementById("Z0"), 230 - RL: document.getElementById("RL"), 231 - secPerTau: document.getElementById("secPerTau"), 232 - reflectTol: document.getElementById("reflectTol"), 233 - tRead: document.getElementById("tRead"), 234 - gLRead: document.getElementById("gLRead"), 235 - vsRead: document.getElementById("vsRead"), 236 - vlRead: document.getElementById("vlRead"), 237 - derivedValues: document.getElementById("derivedValues"), 238 - waveValues: document.getElementById("waveValues"), 239 - }; 240 - 241 - const theme = { 242 - bg: getCSS("--bg"), 243 - panel: getCSS("--panel"), 244 - ink: getCSS("--ink"), 245 - muted: getCSS("--muted"), 246 - grid: getCSS("--grid"), 247 - accent: getCSS("--accent"), 248 - accent2: getCSS("--accent2"), 249 - warn: getCSS("--warn"), 250 - ok: getCSS("--ok"), 251 - }; 252 - function getCSS(varName) { 253 - return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); 254 - } 255 - 256 - let running = false; 257 - let hasStarted = false; 258 - let tNorm = 0; // time in units of tau_d (dimensionless) 259 - let lastTS = null; 260 - 261 - const model = { 262 - // Spatial normalization: z in [0,1] corresponds to [0, ℓ] 263 - // The wave speed is normalized so the front reaches z=1 at tNorm=1. 264 - // So front positions: 265 - // incident front z_i = clamp(tNorm, 0, 1) 266 - // reflected front z_r = clamp(1 - (tNorm - 1), 0, 1) for tNorm >= 1 267 - // 268 - // Voltage levels: 269 - Vg: 5, 270 - Rg: 50, 271 - Z0: 50, 272 - RL: 30, 273 - secPerTau: 2.5, 274 - reflectTol: 1, 275 - singleBounce: true, // if true, assume matched source => no re-reflection 276 - }; 277 - 278 - const BOUNCE_EPS = 1e-6; 279 - const MAX_BOUNCES = 5000; // safety guard if a user picks non-convergent parameters/tolerance. 280 - let timeHorizon = 2.2; 281 - let mathjaxTypesetDone = false; 282 - const PLOT_PANEL_H = 150; 283 - const PLOT_PANEL_GAP = 10; 284 - const PLOT_PAD_T = 14; 285 - const PLOT_PAD_B = 26; 286 - 287 - function recomputeModelFromInputs() { 288 - model.Vg = parseFloat(el.Vg.value); 289 - model.Rg = parseFloat(el.Rg.value); 290 - model.Z0 = parseFloat(el.Z0.value); 291 - model.RL = parseFloat(el.RL.value); 292 - model.secPerTau = parseFloat(el.secPerTau.value); 293 - model.reflectTol = Math.max(0, parseFloat(el.reflectTol.value)); 294 - model.singleBounce = !!el.singleBounce.checked; 295 - } 296 - 297 - function computeWaveParams() { 298 - const { Vg, Rg, Z0, RL } = model; 299 - const V1 = Vg * Z0 / (Rg + Z0); // Eq. (6.3) in the excerpt around Fig. 6.3 300 - const gL = (RL - Z0) / (RL + Z0); // Eq. (6.7) reflection coefficient at load 301 - const V2 = gL * V1; 302 - // If later you want source re-reflection: 303 - const gS = (Rg - Z0) / (Rg + Z0); 304 - return { V1, V2, gL, gS }; 305 - } 306 - 307 - function buildBounceSeries(waves) { 308 - const gS = model.singleBounce ? 0 : waves.gS; 309 - const tolPct = Number.isFinite(model.reflectTol) ? model.reflectTol : 1; 310 - const ampTol = Math.max(BOUNCE_EPS, Math.abs(waves.V1) * (tolPct / 100)); 311 - const series = [{ n: 1, A: waves.V1, dir: +1, launch: 0 }]; 312 - 313 - let A = waves.V1; 314 - let launch = 1; 315 - let reflectAtLoad = true; // V2 is generated at the load from V1. 316 - while (series.length < MAX_BOUNCES) { 317 - const g = reflectAtLoad ? waves.gL : gS; 318 - const nextA = A * g; 319 - if (Math.abs(nextA) <= ampTol) break; 320 - series.push({ 321 - n: series.length + 1, 322 - A: nextA, 323 - dir: reflectAtLoad ? -1 : +1, 324 - launch 325 - }); 326 - A = nextA; 327 - launch += 1; 328 - reflectAtLoad = !reflectAtLoad; 329 - } 330 - 331 - const srcEvents = [{ t: 0, dV: waves.V1 }]; 332 - const loadEvents = []; 333 - for (const w of series) { 334 - const arrive = w.launch + 1; 335 - if (w.dir > 0) { 336 - // Right-going wave arrives at load and creates a reflected left-going wave. 337 - loadEvents.push({ t: arrive, dV: (1 + waves.gL) * w.A }); 338 - } else { 339 - // Left-going wave arrives at source and may re-reflect right-going wave. 340 - srcEvents.push({ t: arrive, dV: (1 + gS) * w.A }); 341 - } 342 - } 343 - 344 - return { 345 - series, 346 - srcEvents, 347 - loadEvents, 348 - gSEffective: gS, 349 - tolPct, 350 - ampTol, 351 - tEnd: (series.length > 0 ? series[series.length - 1].launch + 1 : 2) 352 - }; 353 - } 354 - 355 - function sumEventsAtTime(events, tn) { 356 - const eps = 1e-9; 357 - let v = 0; 358 - for (const e of events) { 359 - if (tn >= e.t - eps) v += e.dV; 360 - } 361 - return v; 362 - } 363 - 364 - function computeDynamicState(tn, bounce) { 365 - const launchedWaves = []; 366 - const activeWaves = []; 367 - for (const w of bounce.series) { 368 - const u = tn - w.launch; 369 - if (u < 0) continue; // not launched yet 370 - const front = (w.dir > 0) ? clamp(u, 0, 1) : clamp(1 - u, 0, 1); 371 - const ww = { ...w, u, front }; 372 - launchedWaves.push(ww); 373 - if (u < 1) activeWaves.push(ww); // front still propagating along the line 374 - } 375 - 376 - return { 377 - launchedWaves, 378 - activeWaves, 379 - VS: sumEventsAtTime(bounce.srcEvents, tn), 380 - VL: sumEventsAtTime(bounce.loadEvents, tn) 381 - }; 382 - } 383 - 384 - function updateDerivedDisplays(waves, bounce) { 385 - const gSEffective = bounce.gSEffective; 386 - const V2calc = waves.gL * waves.V1; 387 - const V3calc = gSEffective * V2calc; 388 - const v3Suppressed = Math.abs(V3calc) <= bounce.ampTol; 389 - const v3Reason = model.singleBounce 390 - ? "suppressed because single-bounce mode forces Γ_S = 0" 391 - : (v3Suppressed ? `suppressed since |V_3| ≤ ε (ε=${fmt(bounce.ampTol, 6)} V = ${fmt(bounce.tolPct, 3)}% of |V_1|)` : "nonzero, so re-reflection should appear"); 392 - 393 - el.derivedValues.innerHTML = [ 394 - `Γ_L = ${fmt(waves.gL, 6)}`, 395 - `Γ_S = ${fmt(gSEffective, 6)}${model.singleBounce ? " (forced by single-bounce mode)" : ""}`, 396 - `V_1 = ${fmt(waves.V1, 6)} V`, 397 - `Termination threshold = ${fmt(bounce.tolPct, 3)}% of |V_1| = ${fmt(bounce.ampTol, 6)} V`, 398 - `V_2 = Γ_L·V_1 = (${fmt(waves.gL, 6)})·(${fmt(waves.V1, 6)}) = ${fmt(V2calc, 6)} V`, 399 - `V_3 = Γ_S·V_2 = (${fmt(gSEffective, 6)})·(${fmt(V2calc, 6)}) = ${fmt(V3calc, 6)} V`, 400 - `V_3 status: ${v3Reason}`, 401 - `Total generated waves: ${bounce.series.length}` 402 - ].map((x) => `<div>${x}</div>`).join(""); 403 - 404 - const waveLines = bounce.series.map((w) => { 405 - const kind = (w.dir > 0) ? "incident (→ load)" : "reflected (→ source)"; 406 - return `<div>V<sub>${w.n}</sub> [${kind}] = ${fmt(w.A, 6)} V</div>`; 407 - }); 408 - if (bounce.series.length < 3) { 409 - waveLines.push(`<div>V<sub>3</sub> not launched: ${v3Reason}</div>`); 410 - } 411 - el.waveValues.innerHTML = waveLines.join(""); 412 - } 413 - 414 - // ----------------------------- 415 - // Rendering: Circuit 416 - // ----------------------------- 417 - function drawCircuit(ctx, w, h, tn, dyn) { 418 - ctx.clearRect(0, 0, w, h); 419 - ctx.fillStyle = theme.panel; 420 - ctx.fillRect(0, 0, w, h); 421 - 422 - const pad = 18; 423 - const yTop = 70; 424 - const yBot = 190; 425 - 426 - // Key x positions 427 - const xSourceL = pad + 60; 428 - const xSwitch = pad + 170; 429 - const xTL0 = pad + 240; 430 - const xTL1 = w - pad - 210; 431 - const xLoad = w - pad - 120; 432 - const xRight = w - pad - 40; 433 - 434 - // Helper lines 435 - ctx.lineWidth = 2; 436 - ctx.strokeStyle = theme.ink; 437 - 438 - // Wires top/bottom 439 - const swA = xSwitch - 18; 440 - const swB = xSwitch + 18; 441 - line(ctx, xSourceL, yTop, swA, yTop); 442 - line(ctx, swB, yTop, xTL0, yTop); 443 - line(ctx, xTL1, yTop, xLoad, yTop); 444 - line(ctx, xLoad, yTop, xRight, yTop); 445 - 446 - // Bottom return path with transmission-line symbol 447 - line(ctx, xSourceL, yBot, xTL0, yBot); 448 - line(ctx, xTL1, yBot, xRight, yBot); 449 - 450 - // Voltage source symbol (circle) 451 - const vsx = xSourceL, vsy = (yTop + yBot) / 2; 452 - // Source lead wires stop at the source body so no line passes through the symbol. 453 - line(ctx, xSourceL, yTop, xSourceL, vsy - 20); 454 - line(ctx, xSourceL, vsy + 20, xSourceL, yBot); 455 - circle(ctx, vsx, vsy, 20, theme.ink); 456 - // +/- inside 457 - ctx.fillStyle = theme.ink; 458 - ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 459 - ctx.fillText("+", vsx - 4, vsy - 6); 460 - ctx.fillText("−", vsx - 4, vsy + 14); 461 - label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted); 462 - 463 - // Source resistor Rg on top wire (zigzag) 464 - const r0 = xSourceL + 20, r1 = xSwitch - 20; 465 - drawResistor(ctx, r0, yTop, r1, yTop, 8, 8); 466 - label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted); 467 - 468 - // Switch 469 - drawSwitch(ctx, xSwitch, yTop, tn); 470 - 471 - // Transmission line rectangle 472 - ctx.strokeStyle = theme.ink; 473 - ctx.lineWidth = 2; 474 - ctx.strokeRect(xTL0, yTop - 16, xTL1 - xTL0, 32); 475 - label(ctx, "Z0", (xTL0 + xTL1) / 2 - 10, yTop - 26, theme.muted); 476 - 477 - // Bottom transmission line rectangle 478 - ctx.strokeRect(xTL0, yBot - 16, xTL1 - xTL0, 32); 479 - 480 - // Load resistor RL (vertical on right) 481 - const rlTop = yTop + 22; 482 - const rlBot = yBot - 22; 483 - ctx.strokeStyle = theme.ink; 484 - ctx.lineWidth = 2; 485 - line(ctx, xLoad, yTop, xLoad, rlTop); 486 - drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 6, 8); 487 - line(ctx, xLoad, rlBot, xLoad, yBot); 488 - label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 489 - 490 - // VS and VL polarity markers 491 - drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS"); 492 - drawVoltageProbe(ctx, xLoad + 18, yTop, yBot, "VL"); 493 - 494 - // Ground-ish reference on far right (just a node) 495 - ctx.fillStyle = theme.ink; 496 - ctx.beginPath(); 497 - ctx.arc(xRight, yBot, 3.5, 0, Math.PI * 2); 498 - ctx.fill(); 499 - 500 - // Wavefront highlights: dashed vertical lines crossing both T-line symbols. 501 - const tlW = (xTL1 - xTL0); 502 - const wfY0 = yTop - 16; 503 - const wfY1 = yBot + 16; 504 - 505 - for (const wf of dyn.activeWaves) { 506 - const xw = xTL0 + wf.front * tlW; 507 - ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 508 - ctx.lineWidth = 3; 509 - ctx.setLineDash([9, 7]); 510 - line(ctx, xw, wfY0, xw, wfY1); 511 - ctx.setLineDash([]); 512 - } 513 - 514 - // z=0 and z=l labels 515 - label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted); 516 - label(ctx, "z = ℓ", xTL1 - 18, yBot + 34, theme.muted); 517 - } 518 - 519 - function drawSwitch(ctx, x, y, tn) { 520 - // Open at t=0 with angled blade; close by rotating down onto right contact. 521 - const closed = tn > 0; 522 - ctx.strokeStyle = theme.ink; 523 - ctx.lineWidth = 2; 524 - 525 - const a = { x: x - 18, y }; 526 - const b = { x: x + 18, y }; 527 - 528 - // left contact 529 - circleFill(ctx, a.x, a.y, 3.2, theme.ink); 530 - // right contact 531 - circleFill(ctx, b.x, b.y, 3.2, theme.ink); 532 - 533 - if (closed) { 534 - // Closed switch: hard short contact-to-contact. 535 - line(ctx, a.x, a.y, b.x, b.y); 536 - } else { 537 - line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14); 538 - } 539 - label(ctx, "t = 0", x - 18, y + 26, theme.muted); 540 - } 541 - 542 - function drawVoltageProbe(ctx, x, yTop, yBot, name) { 543 - // + at top, - at bottom with a brace line 544 - ctx.strokeStyle = theme.muted; 545 - //ctx.lineWidth = 1.5; 546 - //line(ctx, x, yTop + 6, x, yBot - 6); 547 - ctx.fillStyle = theme.muted; 548 - ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 549 - ctx.fillText("+", x + 6, yTop + 12); 550 - ctx.fillText("−", x + 6, yBot - 4); 551 - label(ctx, name, x + 10, (yTop + yBot) / 2 - 6, theme.muted); 552 - } 553 - 554 - function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp) { 555 - // Draw a zigzag resistor between two points (supports horizontal or vertical). 556 - ctx.strokeStyle = theme.ink; 557 - ctx.lineWidth = 2; 558 - 559 - const dx = x1 - x0, dy = y1 - y0; 560 - const len = Math.hypot(dx, dy); 561 - if (len < 1e-6) return; 562 - const ux = dx / len, uy = dy / len; 563 - const px = -uy, py = ux; // perpendicular 564 - 565 - const lead = 10; 566 - const start = { x: x0 + ux * lead, y: y0 + uy * lead }; 567 - const end = { x: x1 - ux * lead, y: y1 - uy * lead }; 568 - 569 - // Leads 570 - line(ctx, x0, y0, start.x, start.y); 571 - line(ctx, end.x, end.y, x1, y1); 572 - 573 - // Zigzag path 574 - const segs = zigZagCount * 2; 575 - const segLen = (len - 2 * lead) / segs; 576 - 577 - ctx.beginPath(); 578 - ctx.moveTo(start.x, start.y); 579 - for (let i = 1; i < segs; i++) { 580 - const s = i * segLen; 581 - const flip = (i % 2 === 0) ? -1 : 1; 582 - const xx = start.x + ux * s + px * amp * flip; 583 - const yy = start.y + uy * s + py * amp * flip; 584 - ctx.lineTo(xx, yy); 585 - } 586 - ctx.lineTo(end.x, end.y); 587 - ctx.stroke(); 588 - } 589 - 590 - function curvedArrow(ctx, x0, y0, x1, y1) { 591 - // simple quadratic curve with arrowhead 592 - const mx = (x0 + x1) / 2; 593 - const my = (y0 + y1) / 2 - 16; 594 - ctx.beginPath(); 595 - ctx.moveTo(x0, y0); 596 - ctx.quadraticCurveTo(mx, my, x1, y1); 597 - ctx.stroke(); 598 - 599 - // Arrowhead at (x1,y1) 600 - const ang = Math.atan2(y1 - my, x1 - mx); 601 - const ah = 8; 602 - ctx.beginPath(); 603 - ctx.moveTo(x1, y1); 604 - ctx.lineTo(x1 - ah * Math.cos(ang - 0.45), y1 - ah * Math.sin(ang - 0.45)); 605 - ctx.moveTo(x1, y1); 606 - ctx.lineTo(x1 - ah * Math.cos(ang + 0.45), y1 - ah * Math.sin(ang + 0.45)); 607 - ctx.stroke(); 608 - } 609 - 610 - function line(ctx, x0, y0, x1, y1) { 611 - ctx.beginPath(); 612 - ctx.moveTo(x0, y0); 613 - ctx.lineTo(x1, y1); 614 - ctx.stroke(); 615 - } 616 - function circle(ctx, x, y, r, strokeStyle) { 617 - ctx.strokeStyle = strokeStyle; 618 - ctx.lineWidth = 2; 619 - ctx.beginPath(); 620 - ctx.arc(x, y, r, 0, Math.PI * 2); 621 - ctx.stroke(); 622 - } 623 - function circleFill(ctx, x, y, r, fillStyle) { 624 - ctx.fillStyle = fillStyle; 625 - ctx.beginPath(); 626 - ctx.arc(x, y, r, 0, Math.PI * 2); 627 - ctx.fill(); 628 - } 629 - function label(ctx, text, x, y, color) { 630 - ctx.fillStyle = color; 631 - ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 632 - ctx.fillText(text, x, y); 633 - } 634 - 635 - // ----------------------------- 636 - // Rendering: Plot (V vs z) 637 - // ----------------------------- 638 - function drawPlot(ctx, w, h, tn, dyn) { 639 - ctx.clearRect(0, 0, w, h); 640 - ctx.fillStyle = theme.panel; 641 - ctx.fillRect(0, 0, w, h); 642 - 643 - const padT = PLOT_PAD_T, padB = PLOT_PAD_B, panelGap = PLOT_PANEL_GAP; 644 - // Match circuit T-line x extents so z=0 and z=ℓ align vertically with the circuit view. 645 - const circuitPad = 18; 646 - const xPlot0 = circuitPad + 240; 647 - const xPlot1 = w - circuitPad - 210; 648 - const plotW = xPlot1 - xPlot0; 649 - const xOfZ = (z) => xPlot0 + z * plotW; 650 - 651 - const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 652 - const shownWaves = launched.slice(0, 6); 653 - 654 - function totalSegmentsForWaves(waves) { 655 - const breakpoints = [0, 1]; 656 - for (const wf of waves) breakpoints.push(wf.front); 657 - breakpoints.sort((a, b) => a - b); 658 - const pts = []; 659 - for (const x of breakpoints) { 660 - if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x); 661 - } 662 - const segs = []; 663 - for (let i = 0; i < pts.length - 1; i++) { 664 - const a = pts[i], b = pts[i + 1]; 665 - const m = (a + b) / 2; 666 - let V = 0; 667 - for (const wf of waves) { 668 - if (wf.dir > 0 && m <= wf.front) V += wf.A; 669 - if (wf.dir < 0 && m >= wf.front) V += wf.A; 670 - } 671 - segs.push({ a, b, V }); 672 - } 673 - return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 674 - } 675 - 676 - function segmentsForWave(wf) { 677 - if (wf.dir > 0) return [ 678 - { a: 0, b: wf.front, V: wf.A }, 679 - { a: wf.front, b: 1, V: 0 } 680 - ]; 681 - return [ 682 - { a: 0, b: wf.front, V: 0 }, 683 - { a: wf.front, b: 1, V: wf.A } 684 - ]; 685 - } 686 - 687 - const sumSegments = totalSegmentsForWaves(launched); 688 - const nPanels = 2; 689 - const panelH = PLOT_PANEL_H; 690 - 691 - // Fixed vertical scale for all panels/time for the current parameter set. 692 - const maxAbsWave = Math.max(1e-6, ...shownWaves.map((wf) => Math.abs(wf.A))); 693 - const maxAbsSum = Math.max(1e-6, sumSegments.reduce((m, seg) => Math.max(m, Math.abs(seg.V)), 0)); 694 - const vScale = Math.max(maxAbsWave, maxAbsSum); 695 - const vLo = -1.15 * vScale; 696 - const vHi = 1.15 * vScale; 697 - 698 - ctx.fillStyle = theme.muted; 699 - ctx.font = "12px ui-sans-serif, system-ui"; 700 - ctx.fillText("Voltage along the T-line", 12, 14); 701 - 702 - function drawPanelFrame(top, bottom, yOfV, labelText) { 703 - const midY = (top + bottom) / 2; 704 - ctx.strokeStyle = theme.grid; 705 - ctx.lineWidth = 1; 706 - line(ctx, xPlot0, top, xPlot1, top); 707 - line(ctx, xPlot0, bottom, xPlot1, bottom); 708 - line(ctx, xPlot0, top, xPlot0, bottom); 709 - line(ctx, xPlot1, top, xPlot1, bottom); 710 - 711 - ctx.strokeStyle = theme.muted; 712 - ctx.lineWidth = 1.2; 713 - line(ctx, xPlot0, yOfV(0), xPlot1, yOfV(0)); 714 - 715 - ctx.fillStyle = theme.muted; 716 - ctx.font = "12px ui-sans-serif, system-ui"; 717 - ctx.fillText(labelText, xPlot0 + 8, top + 14); 718 - ctx.fillText("0", xPlot0 - 14, yOfV(0) + 4); 719 - ctx.fillText("z", xPlot1 + 6, midY + 4); 720 - } 721 - 722 - function drawMarkers(top, bottom, fronts) { 723 - if (!fronts.length) return; 724 - ctx.strokeStyle = theme.warn; 725 - ctx.lineWidth = 1.2; 726 - ctx.setLineDash([4, 5]); 727 - for (const zf of fronts) line(ctx, xOfZ(zf), top, xOfZ(zf), bottom); 728 - ctx.setLineDash([]); 729 - } 730 - 731 - // Panel 1: sum only. 732 - const top0 = padT; 733 - const bot0 = top0 + panelH; 734 - const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 735 - drawPanelFrame(top0, bot0, y0, "Sum (all waves)"); 736 - drawPiecewise(ctx, xOfZ, y0, sumSegments, theme.ok, 2.4); 737 - drawMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front)); 738 - 739 - // Panel 2: all component waves overlaid with style sequence. 740 - const top1 = padT + panelH + panelGap; 741 - const bot1 = top1 + panelH; 742 - const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 743 - const truncated = launched.length > shownWaves.length; 744 - drawPanelFrame(top1, bot1, y1, truncated ? "Components (V1..V6 shown)" : "Components (all waves)"); 745 - 746 - const waveStyles = [ 747 - { color: theme.accent, dash: [] }, // solid blue 748 - { color: theme.accent2, dash: [] }, // solid pink 749 - { color: theme.accent, dash: [9, 6] }, // dashed blue 750 - { color: theme.accent2, dash: [9, 6] }, // dashed pink 751 - { color: theme.accent, dash: [2, 5] }, // dotted blue 752 - { color: theme.accent2, dash: [2, 5] }, // dotted pink 753 - ]; 754 - 755 - for (let i = 0; i < shownWaves.length; i++) { 756 - const wf = shownWaves[i]; 757 - const style = waveStyles[i]; 758 - ctx.setLineDash(style.dash); 759 - drawPiecewise(ctx, xOfZ, y1, segmentsForWave(wf), style.color, 2.0); 760 - ctx.setLineDash([]); 761 - } 762 - drawMarkers(top1, bot1, shownWaves.filter((wf) => wf.u < 1).map((wf) => wf.front)); 763 - 764 - // Shared z-axis endpoint labels 765 - ctx.fillStyle = theme.muted; 766 - ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 767 - ctx.fillText("0", xPlot0 - 4, h - 6); 768 - ctx.fillText("ℓ", xPlot1 - 4, h - 6); 769 - } 770 - 771 - function ensurePlotCanvasHeight(nPanels) { 772 - const targetH = Math.round(PLOT_PAD_T + PLOT_PAD_B + nPanels * PLOT_PANEL_H + (nPanels - 1) * PLOT_PANEL_GAP); 773 - const curr = parseInt(el.plot.getAttribute("height") || "0", 10); 774 - if (curr !== targetH) el.plot.setAttribute("height", String(targetH)); 775 - } 776 - 777 - function drawStep(ctx, xOfZ, yOfV, zA, zB, V, color) { 778 - // Draw a step that is V on [zA,zB], 0 elsewhere. 779 - // We draw as a polyline: (0,0)->(zA,0)->(zA,V)->(zB,V)->(zB,0)->(1,0) 780 - ctx.strokeStyle = color; 781 - ctx.lineWidth = 2.2; 782 - ctx.beginPath(); 783 - ctx.moveTo(xOfZ(0), yOfV(0)); 784 - ctx.lineTo(xOfZ(zA), yOfV(0)); 785 - ctx.lineTo(xOfZ(zA), yOfV(V)); 786 - ctx.lineTo(xOfZ(zB), yOfV(V)); 787 - ctx.lineTo(xOfZ(zB), yOfV(0)); 788 - ctx.lineTo(xOfZ(1), yOfV(0)); 789 - ctx.stroke(); 790 - } 791 - 792 - function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 793 - ctx.strokeStyle = color; 794 - ctx.lineWidth = width; 795 - ctx.beginPath(); 796 - // Start at z=0 797 - ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V)); 798 - for (const seg of segments) { 799 - ctx.lineTo(xOfZ(seg.a), yOfV(seg.V)); 800 - ctx.lineTo(xOfZ(seg.b), yOfV(seg.V)); 801 - } 802 - ctx.stroke(); 803 - } 804 - 805 - // ----------------------------- 806 - // Animation loop 807 - // ----------------------------- 808 - function render() { 809 - recomputeModelFromInputs(); 810 - const waves = computeWaveParams(); 811 - const bounce = buildBounceSeries(waves); 812 - const dyn = computeDynamicState(tNorm, bounce); 813 - timeHorizon = Math.max(2.2, Math.min(16, bounce.tEnd + 0.4)); 814 - 815 - el.tRead.textContent = fmt(tNorm, 3); 816 - el.gLRead.textContent = fmt(waves.gL, 3); 817 - el.vsRead.textContent = `${fmt(dyn.VS, 3)} V`; 818 - el.vlRead.textContent = `${fmt(dyn.VL, 3)} V`; 819 - updateDerivedDisplays(waves, bounce); 820 - 821 - if (!mathjaxTypesetDone && window.MathJax?.typesetPromise) { 822 - mathjaxTypesetDone = true; 823 - window.MathJax.typesetPromise(); 824 - } 825 - 826 - // Circuit 827 - const c = resizeCanvasToCSS(el.circuit); 828 - drawCircuit(c.ctx, c.w, c.h, tNorm, dyn); 829 - 830 - // Plot 831 - ensurePlotCanvasHeight(2); 832 - const p = resizeCanvasToCSS(el.plot); 833 - drawPlot(p.ctx, p.w, p.h, tNorm, dyn); 834 - if (!hasStarted) { 835 - el.pauseBtn.textContent = "Pause"; 836 - } else { 837 - el.pauseBtn.textContent = running ? "Pause" : "Resume"; 838 - } 839 - el.pauseBtn.disabled = !hasStarted; 840 - } 841 - 842 - function tick(ts) { 843 - if (!running) return; 844 - if (lastTS == null) lastTS = ts; 845 - const dt = (ts - lastTS) / 1000; 846 - lastTS = ts; 847 - 848 - const secPerTau = Math.max(0.2, model.secPerTau); 849 - const dtn = dt / secPerTau; 850 - tNorm += dtn; 851 - 852 - // Stop/loop at the current model-dependent horizon. 853 - if (tNorm > timeHorizon) { 854 - if (model.singleBounce) { 855 - running = false; 856 - lastTS = null; 857 - } else { 858 - tNorm = 0; 859 - } 860 - } 861 - 862 - render(); 863 - requestAnimationFrame(tick); 864 - } 865 - 866 - function start() { 867 - hasStarted = true; 868 - tNorm = 0; 869 - running = true; 870 - lastTS = null; 871 - render(); 872 - requestAnimationFrame(tick); 873 - } 874 - 875 - function pause() { 876 - if (!hasStarted) return; 877 - if (running) { 878 - running = false; 879 - lastTS = null; 880 - render(); 881 - return; 882 - } 883 - // Resume from current time; if already at the end, restart from t=0. 884 - if (tNorm >= timeHorizon - 1e-9) tNorm = 0; 885 - running = true; 886 - lastTS = null; 887 - render(); 888 - requestAnimationFrame(tick); 889 - } 890 - 891 - function reset() { 892 - running = false; 893 - hasStarted = false; 894 - lastTS = null; 895 - tNorm = 0; 896 - render(); 897 - } 898 - 899 - // ----------------------------- 900 - // Scrubbing 901 - // ----------------------------- 902 - function scrubFromEvent(ev) { 903 - if (!el.scrubToggle.checked) return; 904 - if (running) return; 905 - 906 - // Map x-position over either canvas to tNorm in [0,timeHorizon]. 907 - const rect = ev.target.getBoundingClientRect(); 908 - const x = clamp(ev.clientX - rect.left, 0, rect.width); 909 - const u = x / rect.width; 910 - 911 - tNorm = u * timeHorizon; 912 - render(); 913 - } 914 - 915 - // ----------------------------- 916 - // Wiring events 917 - // ----------------------------- 918 - el.startBtn.addEventListener("click", start); 919 - el.pauseBtn.addEventListener("click", pause); 920 - el.resetBtn.addEventListener("click", reset); 921 - 922 - for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.reflectTol, el.singleBounce]) { 923 - inp.addEventListener("input", () => { if (!running) render(); }); 924 - inp.addEventListener("change", () => { if (!running) render(); }); 925 - } 926 - 927 - el.plot.addEventListener("mousemove", scrubFromEvent); 928 - el.circuit.addEventListener("mousemove", scrubFromEvent); 929 - 930 - window.addEventListener("resize", () => render()); 931 - 932 - // First paint 933 - render(); 934 - 935 - })(); 936 - </script> 92 + <script src="utils.js"></script> 93 + <script src="physics.js"></script> 94 + <script src="render.js"></script> 95 + <script src="app.js"></script> 937 96 </body> 938 97 </html>
+27
utils.js
··· 1 + "use strict"; 2 + 3 + // Pure math and canvas utilities shared across modules. 4 + const TLUtils = (() => { 5 + const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); 6 + const fmt = (x, d = 3) => Number.isFinite(x) ? x.toFixed(d) : "NaN"; 7 + const sign = (x) => (x > 0) - (x < 0); 8 + 9 + function getDPR() { 10 + return Math.max(1, Math.floor(window.devicePixelRatio || 1)); 11 + } 12 + 13 + // Scale canvas internal buffer to match CSS size × device pixel ratio, 14 + // returning a logical-pixel coordinate system via ctx.setTransform. 15 + function resizeCanvasToCSS(canvas) { 16 + const dpr = getDPR(); 17 + const cssW = canvas.clientWidth; 18 + const cssH = Math.round(cssW * (canvas.height / canvas.width)); 19 + canvas.width = Math.round(cssW * dpr); 20 + canvas.height = Math.round(cssH * dpr); 21 + const ctx = canvas.getContext("2d"); 22 + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 23 + return { ctx, w: cssW, h: cssH, dpr }; 24 + } 25 + 26 + return { clamp, fmt, sign, getDPR, resizeCanvasToCSS }; 27 + })();