this repo has no description

Initial commit

+745
+745
tline_viz.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width,initial-scale=1" /> 6 + <title>Transmission Line Step + Reflection (Figure 6.3-style)</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: repeat(12, 1fr); gap: 10px; align-items: end; } 25 + .controls .btns { grid-column: span 4; display: flex; gap: 8px; flex-wrap: wrap; } 26 + button { 27 + background: #162234; border: 1px solid #24364e; color: var(--ink); 28 + padding: 8px 10px; border-radius: 10px; cursor: pointer; font-weight: 600; 29 + } 30 + button:hover { border-color: #35557a; } 31 + button:active { transform: translateY(1px); } 32 + .pill { 33 + display: inline-flex; gap: 8px; align-items: center; 34 + background: #101a28; border: 1px solid #24364e; border-radius: 999px; 35 + padding: 6px 10px; color: var(--muted); font-size: 12px; 36 + } 37 + .pill b { color: var(--ink); font-weight: 650; } 38 + .controls .readouts { grid-column: span 8; display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } 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 + .note { color: var(--muted); font-size: 12px; line-height: 1.35; margin-top: 10px; } 52 + canvas { width: 100%; height: auto; display: block; border-radius: 12px; } 53 + .legend { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; } 54 + .key { display: inline-flex; gap: 8px; align-items: center; font-size: 12px; color: var(--muted); } 55 + .swatch { width: 12px; height: 12px; border-radius: 3px; background: var(--accent); } 56 + .swatch.ref { background: var(--accent2); } 57 + .swatch.sum { background: var(--ok); } 58 + .divider { height: 1px; background: #1b2736; margin: 10px 0; } 59 + .small { font-size: 12px; color: var(--muted); } 60 + .toggle { display:flex; align-items:center; gap:8px; margin-left: 4px; } 61 + input[type="checkbox"]{ width: 16px; height: 16px; accent-color: var(--accent); } 62 + .footer { margin-top: 10px; color: var(--muted); font-size: 12px; } 63 + .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-variant-numeric: tabular-nums; } 64 + </style> 65 + </head> 66 + <body> 67 + <div class="wrap"> 68 + <h1>EM propagation on a transmission line (Figure 6.3-style: incident step + load reflection)</h1> 69 + 70 + <div class="row"> 71 + <div class="panel"> 72 + <div class="controls"> 73 + <div class="btns"> 74 + <button id="startBtn">Start</button> 75 + <button id="pauseBtn">Pause</button> 76 + <button id="resetBtn">Reset</button> 77 + <div class="toggle pill" title="When paused, move your mouse over the plot or the line to scrub time."> 78 + <input id="scrubToggle" type="checkbox" /> 79 + <span>Scrub</span> 80 + </div> 81 + </div> 82 + <div class="readouts"> 83 + <div class="pill"><span>t / τ<sub>d</sub>:</span> <b class="mono" id="tRead">0.000</b></div> 84 + <div class="pill"><span>Γ<sub>L</sub>:</span> <b class="mono" id="gLRead">0.000</b></div> 85 + <div class="pill"><span>V₁ (incident):</span> <b class="mono" id="v1Read">0.000 V</b></div> 86 + <div class="pill"><span>V₂ (reflected):</span> <b class="mono" id="v2Read">0.000 V</b></div> 87 + <div class="pill"><span>V<sub>S</sub>:</span> <b class="mono" id="vsRead">0.000 V</b></div> 88 + <div class="pill"><span>V<sub>L</sub>:</span> <b class="mono" id="vlRead">0.000 V</b></div> 89 + </div> 90 + </div> 91 + 92 + <div class="cfg"> 93 + <label> 94 + V<sub>g</sub> (V) 95 + <input id="Vg" type="number" step="0.1" value="5.0" /> 96 + </label> 97 + <label> 98 + R<sub>g</sub> (Ω) 99 + <input id="Rg" type="number" step="0.1" value="50.0" /> 100 + </label> 101 + <label> 102 + Z<sub>0</sub> (Ω) 103 + <input id="Z0" type="number" step="0.1" value="50.0" /> 104 + </label> 105 + <label> 106 + R<sub>L</sub> (Ω) 107 + <input id="RL" type="number" step="0.1" value="30.0" /> 108 + </label> 109 + 110 + <label class="wide"> 111 + Time scale (speed) — seconds per τ<sub>d</sub> 112 + <input id="secPerTau" type="range" min="0.5" max="6" step="0.1" value="2.5" /> 113 + </label> 114 + <label class="wide"> 115 + Show only one reflection (matched source, Γ<sub>S</sub>=0) 116 + <input id="singleBounce" type="checkbox" checked /> 117 + </label> 118 + </div> 119 + 120 + <div class="note"> 121 + Model: at <span class="mono">t=0</span> the switch closes, the source initially “sees” <span class="mono">Z0</span>, launching 122 + <span class="mono">V1 = Vg·Z0/(Rg+Z0)</span>. When the step reaches the load at <span class="mono">t=τd</span>, it reflects with 123 + <span class="mono">ΓL = (RL−Z0)/(RL+Z0)</span> giving <span class="mono">V2 = ΓL·V1</span>. The plot below shows the spatial waveform vs <span class="mono">z</span>, 124 + and the circuit highlight shows the moving wavefront(s). :contentReference[oaicite:1]{index=1} 125 + </div> 126 + 127 + <div class="divider"></div> 128 + 129 + <!-- Circuit canvas (top) --> 130 + <canvas id="circuit" width="1100" height="260" aria-label="Circuit with wavefront highlight"></canvas> 131 + 132 + <div class="divider"></div> 133 + 134 + <!-- Plot canvas (bottom) --> 135 + <canvas id="plot" width="1100" height="360" aria-label="Voltage vs z plot with incident and reflected waves"></canvas> 136 + 137 + <div class="legend"> 138 + <div class="key"><span class="swatch"></span> incident</div> 139 + <div class="key"><span class="swatch ref"></span> reflected</div> 140 + <div class="key"><span class="swatch sum"></span> total (incident + reflected)</div> 141 + <div class="small">Axes: horizontal is position <span class="mono">z</span> (0 → ℓ). Vertical is voltage.</div> 142 + </div> 143 + 144 + <div class="footer"> 145 + Implementation note: code is structured so you can later replace the time-evolution with direct cursor control of the wavefront, 146 + or add multiple bounces (Γ<sub>S</sub> ≠ 0) via a bounce-diagram style event list. 147 + </div> 148 + </div> 149 + </div> 150 + </div> 151 + 152 + <script> 153 + (() => { 154 + // ----------------------------- 155 + // Utilities 156 + // ----------------------------- 157 + const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); 158 + const fmt = (x, d=3) => Number.isFinite(x) ? x.toFixed(d) : "NaN"; 159 + const sign = (x) => (x > 0) - (x < 0); 160 + 161 + function getDPR() { return Math.max(1, Math.floor(window.devicePixelRatio || 1)); } 162 + 163 + function resizeCanvasToCSS(canvas) { 164 + // Canvas is already given width/height attributes for a default, 165 + // but we ensure crisp rendering on high-DPI screens. 166 + const dpr = getDPR(); 167 + const cssW = canvas.clientWidth; 168 + const cssH = Math.round(cssW * (canvas.height / canvas.width)); 169 + // Keep CSS height "auto"; only scale internal buffer. 170 + canvas.width = Math.round(cssW * dpr); 171 + canvas.height = Math.round(cssH * dpr); 172 + const ctx = canvas.getContext("2d"); 173 + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 174 + return { ctx, w: cssW, h: cssH, dpr }; 175 + } 176 + 177 + // ----------------------------- 178 + // Parameters / State 179 + // ----------------------------- 180 + const el = { 181 + circuit: document.getElementById("circuit"), 182 + plot: document.getElementById("plot"), 183 + startBtn: document.getElementById("startBtn"), 184 + pauseBtn: document.getElementById("pauseBtn"), 185 + resetBtn: document.getElementById("resetBtn"), 186 + scrubToggle: document.getElementById("scrubToggle"), 187 + singleBounce: document.getElementById("singleBounce"), 188 + Vg: document.getElementById("Vg"), 189 + Rg: document.getElementById("Rg"), 190 + Z0: document.getElementById("Z0"), 191 + RL: document.getElementById("RL"), 192 + secPerTau: document.getElementById("secPerTau"), 193 + tRead: document.getElementById("tRead"), 194 + gLRead: document.getElementById("gLRead"), 195 + v1Read: document.getElementById("v1Read"), 196 + v2Read: document.getElementById("v2Read"), 197 + vsRead: document.getElementById("vsRead"), 198 + vlRead: document.getElementById("vlRead"), 199 + }; 200 + 201 + const theme = { 202 + bg: getCSS("--bg"), 203 + panel: getCSS("--panel"), 204 + ink: getCSS("--ink"), 205 + muted: getCSS("--muted"), 206 + grid: getCSS("--grid"), 207 + accent: getCSS("--accent"), 208 + accent2: getCSS("--accent2"), 209 + warn: getCSS("--warn"), 210 + ok: getCSS("--ok"), 211 + }; 212 + function getCSS(varName) { 213 + return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); 214 + } 215 + 216 + let running = false; 217 + let tNorm = 0; // time in units of tau_d (dimensionless) 218 + let lastTS = null; 219 + 220 + const model = { 221 + // Spatial normalization: z in [0,1] corresponds to [0, ℓ] 222 + // The wave speed is normalized so the front reaches z=1 at tNorm=1. 223 + // So front positions: 224 + // incident front z_i = clamp(tNorm, 0, 1) 225 + // reflected front z_r = clamp(1 - (tNorm - 1), 0, 1) for tNorm >= 1 226 + // 227 + // Voltage levels: 228 + Vg: 5, 229 + Rg: 50, 230 + Z0: 50, 231 + RL: 30, 232 + secPerTau: 2.5, 233 + singleBounce: true, // if true, assume matched source => no re-reflection 234 + }; 235 + 236 + function recomputeModelFromInputs() { 237 + model.Vg = parseFloat(el.Vg.value); 238 + model.Rg = parseFloat(el.Rg.value); 239 + model.Z0 = parseFloat(el.Z0.value); 240 + model.RL = parseFloat(el.RL.value); 241 + model.secPerTau = parseFloat(el.secPerTau.value); 242 + model.singleBounce = !!el.singleBounce.checked; 243 + } 244 + 245 + function computeWaveParams() { 246 + const { Vg, Rg, Z0, RL } = model; 247 + const V1 = Vg * Z0 / (Rg + Z0); // Eq. (6.3) in the excerpt around Fig. 6.3 248 + const gL = (RL - Z0) / (RL + Z0); // Eq. (6.7) reflection coefficient at load 249 + const V2 = gL * V1; 250 + // If later you want source re-reflection: 251 + const gS = (Rg - Z0) / (Rg + Z0); 252 + return { V1, V2, gL, gS }; 253 + } 254 + 255 + // For this first draft, show VS/VL consistent with a *single* load reflection and matched source (ΓS=0), 256 + // which matches the “clean” narrative used in the examples immediately after Fig. 6.3. 257 + function computeEndVoltages(tn, V1, V2) { 258 + const eps = 1e-9; 259 + // Load: 260 + let VL = (tn < 1 - eps) ? 0 : (V1 + V2); 261 + // Source: 262 + let VS; 263 + if (model.singleBounce) { 264 + VS = (tn < 2 - eps) ? V1 : (V1 + V2); // after reflected wave returns (t=2τd), source sees updated level 265 + } else { 266 + // If not single-bounce, we still *display* something reasonable: 267 + // keep VS at V1 for now to avoid implying correctness for multi-bounce without implementing it. 268 + VS = V1; 269 + } 270 + return { VS, VL }; 271 + } 272 + 273 + // ----------------------------- 274 + // Rendering: Circuit 275 + // ----------------------------- 276 + function drawCircuit(ctx, w, h, tn, waves) { 277 + ctx.clearRect(0, 0, w, h); 278 + ctx.fillStyle = theme.panel; 279 + ctx.fillRect(0, 0, w, h); 280 + 281 + const pad = 18; 282 + const yTop = 70; 283 + const yBot = 190; 284 + 285 + // Key x positions 286 + const xSourceL = pad + 60; 287 + const xSwitch = pad + 170; 288 + const xTL0 = pad + 240; 289 + const xTL1 = w - pad - 210; 290 + const xLoad = w - pad - 120; 291 + const xRight = w - pad - 40; 292 + 293 + // Helper lines 294 + ctx.lineWidth = 2; 295 + ctx.strokeStyle = theme.ink; 296 + 297 + // Wires top/bottom 298 + line(ctx, xSourceL, yTop, xTL0, yTop); 299 + line(ctx, xTL1, yTop, xLoad, yTop); 300 + line(ctx, xLoad, yTop, xRight, yTop); 301 + 302 + line(ctx, xSourceL, yBot, xRight, yBot); 303 + 304 + // Left vertical (source) 305 + line(ctx, xSourceL, yTop, xSourceL, yBot); 306 + 307 + // Voltage source symbol (circle) 308 + const vsx = xSourceL, vsy = (yTop + yBot) / 2; 309 + circle(ctx, vsx, vsy, 20, theme.ink); 310 + // +/- inside 311 + ctx.fillStyle = theme.ink; 312 + ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 313 + ctx.fillText("+", vsx - 4, vsy - 6); 314 + ctx.fillText("−", vsx - 4, vsy + 14); 315 + label(ctx, "Vg", vsx - 10, vsy - 30, theme.muted); 316 + 317 + // Source resistor Rg on top wire (zigzag) 318 + const r0 = xSourceL + 20, r1 = xSwitch - 20; 319 + drawResistor(ctx, r0, yTop, r1, yTop, 8, 8); 320 + label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted); 321 + 322 + // Switch 323 + drawSwitch(ctx, xSwitch, yTop, tn); 324 + 325 + // Transmission line rectangle 326 + ctx.strokeStyle = theme.ink; 327 + ctx.lineWidth = 2; 328 + ctx.strokeRect(xTL0, yTop - 16, xTL1 - xTL0, 32); 329 + label(ctx, "Z0", (xTL0 + xTL1) / 2 - 10, yTop - 26, theme.muted); 330 + 331 + // Load resistor RL (vertical on right) 332 + drawResistor(ctx, xLoad, yTop, xLoad, yBot, 8, 8); 333 + label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 334 + 335 + // VS and VL polarity markers 336 + drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS"); 337 + drawVoltageProbe(ctx, xLoad + 18, yTop, yBot, "VL"); 338 + 339 + // Ground-ish reference on far right (just a node) 340 + ctx.fillStyle = theme.ink; 341 + ctx.beginPath(); 342 + ctx.arc(xRight, yBot, 3.5, 0, Math.PI * 2); 343 + ctx.fill(); 344 + 345 + // Wavefront highlight in the transmission line rectangle: 346 + // incident: small bar moving right; reflected: small bar moving left (after tn>=1) 347 + const tlW = (xTL1 - xTL0); 348 + const barH = 24; 349 + const barW = 8; // intentionally small: "not wider than the transmission line symbol" 350 + const zy = yTop - barH / 2; 351 + 352 + // Incident front position 353 + const zi = clamp(tn, 0, 1); 354 + const xi = xTL0 + zi * tlW; 355 + 356 + if (tn >= 0) { 357 + ctx.fillStyle = theme.accent; 358 + ctx.globalAlpha = 0.95; 359 + ctx.fillRect(xi - barW / 2, zy, barW, barH); 360 + ctx.globalAlpha = 1; 361 + } 362 + 363 + // Reflected front position 364 + if (tn >= 1) { 365 + const zr = clamp(1 - (tn - 1), 0, 1); // starts at 1 and moves left 366 + const xr = xTL0 + zr * tlW; 367 + ctx.fillStyle = theme.accent2; 368 + ctx.globalAlpha = 0.95; 369 + ctx.fillRect(xr - barW / 2, zy, barW, barH); 370 + ctx.globalAlpha = 1; 371 + 372 + // Little curved arrow near the load to emphasize reversal (like Fig. 6.3) 373 + ctx.strokeStyle = theme.accent2; 374 + ctx.lineWidth = 2; 375 + curvedArrow(ctx, xTL1 + 10, yTop - 22, xTL1 - 40, yTop - 22); 376 + } 377 + 378 + // z=0 and z=l labels 379 + label(ctx, "z = 0", xTL0 - 20, yBot - 8, theme.muted); 380 + label(ctx, "z = ℓ", xTL1 - 18, yBot - 8, theme.muted); 381 + } 382 + 383 + function drawSwitch(ctx, x, y, tn) { 384 + // Simple open/closed depiction based on whether tn>0 385 + const closed = tn > 0; 386 + ctx.strokeStyle = theme.ink; 387 + ctx.lineWidth = 2; 388 + 389 + const a = { x: x - 18, y }; 390 + const b = { x: x + 18, y }; 391 + 392 + // left contact 393 + circleFill(ctx, a.x, a.y, 3.2, theme.ink); 394 + // right contact 395 + circleFill(ctx, b.x, b.y, 3.2, theme.ink); 396 + 397 + if (closed) { 398 + line(ctx, a.x, a.y, b.x, b.y); 399 + } else { 400 + // angled blade 401 + line(ctx, a.x, a.y, b.x - 2, b.y - 14); 402 + } 403 + label(ctx, "t = 0", x - 18, y + 26, theme.muted); 404 + } 405 + 406 + function drawVoltageProbe(ctx, x, yTop, yBot, name) { 407 + // + at top, - at bottom with a brace line 408 + ctx.strokeStyle = theme.muted; 409 + ctx.lineWidth = 1.5; 410 + line(ctx, x, yTop + 6, x, yBot - 6); 411 + ctx.fillStyle = theme.muted; 412 + ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 413 + ctx.fillText("+", x + 6, yTop + 12); 414 + ctx.fillText("−", x + 6, yBot - 4); 415 + label(ctx, name, x + 10, (yTop + yBot) / 2 - 6, theme.muted); 416 + } 417 + 418 + function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp) { 419 + // Draw a zigzag resistor between two points (supports horizontal or vertical). 420 + ctx.strokeStyle = theme.ink; 421 + ctx.lineWidth = 2; 422 + 423 + const dx = x1 - x0, dy = y1 - y0; 424 + const len = Math.hypot(dx, dy); 425 + if (len < 1e-6) return; 426 + const ux = dx / len, uy = dy / len; 427 + const px = -uy, py = ux; // perpendicular 428 + 429 + const lead = 10; 430 + const start = { x: x0 + ux * lead, y: y0 + uy * lead }; 431 + const end = { x: x1 - ux * lead, y: y1 - uy * lead }; 432 + 433 + // Leads 434 + line(ctx, x0, y0, start.x, start.y); 435 + line(ctx, end.x, end.y, x1, y1); 436 + 437 + // Zigzag path 438 + const segs = zigZagCount * 2; 439 + const segLen = (len - 2 * lead) / segs; 440 + 441 + ctx.beginPath(); 442 + ctx.moveTo(start.x, start.y); 443 + for (let i = 1; i < segs; i++) { 444 + const s = i * segLen; 445 + const flip = (i % 2 === 0) ? -1 : 1; 446 + const xx = start.x + ux * s + px * amp * flip; 447 + const yy = start.y + uy * s + py * amp * flip; 448 + ctx.lineTo(xx, yy); 449 + } 450 + ctx.lineTo(end.x, end.y); 451 + ctx.stroke(); 452 + } 453 + 454 + function curvedArrow(ctx, x0, y0, x1, y1) { 455 + // simple quadratic curve with arrowhead 456 + const mx = (x0 + x1) / 2; 457 + const my = (y0 + y1) / 2 - 16; 458 + ctx.beginPath(); 459 + ctx.moveTo(x0, y0); 460 + ctx.quadraticCurveTo(mx, my, x1, y1); 461 + ctx.stroke(); 462 + 463 + // Arrowhead at (x1,y1) 464 + const ang = Math.atan2(y1 - my, x1 - mx); 465 + const ah = 8; 466 + ctx.beginPath(); 467 + ctx.moveTo(x1, y1); 468 + ctx.lineTo(x1 - ah * Math.cos(ang - 0.45), y1 - ah * Math.sin(ang - 0.45)); 469 + ctx.moveTo(x1, y1); 470 + ctx.lineTo(x1 - ah * Math.cos(ang + 0.45), y1 - ah * Math.sin(ang + 0.45)); 471 + ctx.stroke(); 472 + } 473 + 474 + function line(ctx, x0, y0, x1, y1) { 475 + ctx.beginPath(); 476 + ctx.moveTo(x0, y0); 477 + ctx.lineTo(x1, y1); 478 + ctx.stroke(); 479 + } 480 + function circle(ctx, x, y, r, strokeStyle) { 481 + ctx.strokeStyle = strokeStyle; 482 + ctx.lineWidth = 2; 483 + ctx.beginPath(); 484 + ctx.arc(x, y, r, 0, Math.PI * 2); 485 + ctx.stroke(); 486 + } 487 + function circleFill(ctx, x, y, r, fillStyle) { 488 + ctx.fillStyle = fillStyle; 489 + ctx.beginPath(); 490 + ctx.arc(x, y, r, 0, Math.PI * 2); 491 + ctx.fill(); 492 + } 493 + function label(ctx, text, x, y, color) { 494 + ctx.fillStyle = color; 495 + ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 496 + ctx.fillText(text, x, y); 497 + } 498 + 499 + // ----------------------------- 500 + // Rendering: Plot (V vs z) 501 + // ----------------------------- 502 + function drawPlot(ctx, w, h, tn, waves) { 503 + ctx.clearRect(0, 0, w, h); 504 + ctx.fillStyle = theme.panel; 505 + ctx.fillRect(0, 0, w, h); 506 + 507 + const padL = 58, padR = 18, padT = 22, padB = 44; 508 + const plotW = w - padL - padR; 509 + const plotH = h - padT - padB; 510 + 511 + // Determine vertical range a bit intelligently 512 + const Vmin = Math.min(0, waves.V1 + Math.min(0, waves.V2)) - 0.25 * Math.max(1e-6, Math.abs(waves.V1)); 513 + const Vmax = Math.max(0, waves.V1 + Math.max(0, waves.V2)) + 0.25 * Math.max(1e-6, Math.abs(waves.V1)); 514 + const vLo = (Number.isFinite(Vmin) ? Vmin : -1); 515 + const vHi = (Number.isFinite(Vmax) ? Vmax : 1); 516 + 517 + const xOfZ = (z) => padL + z * plotW; 518 + const yOfV = (V) => padT + (vHi - V) / (vHi - vLo) * plotH; 519 + 520 + // Grid 521 + ctx.strokeStyle = theme.grid; 522 + ctx.lineWidth = 1; 523 + const nGridX = 10, nGridY = 6; 524 + for (let i = 0; i <= nGridX; i++) { 525 + const x = padL + (i / nGridX) * plotW; 526 + line(ctx, x, padT, x, padT + plotH); 527 + } 528 + for (let j = 0; j <= nGridY; j++) { 529 + const y = padT + (j / nGridY) * plotH; 530 + line(ctx, padL, y, padL + plotW, y); 531 + } 532 + 533 + // Axes 534 + ctx.strokeStyle = theme.ink; 535 + ctx.lineWidth = 1.8; 536 + line(ctx, padL, padT, padL, padT + plotH); 537 + line(ctx, padL, padT + plotH, padL + plotW, padT + plotH); 538 + 539 + // Axis labels 540 + ctx.fillStyle = theme.muted; 541 + ctx.font = "13px ui-sans-serif, system-ui"; 542 + ctx.fillText("Voltage", 12, padT + 14); 543 + ctx.fillText("z (position)", padL + plotW - 85, padT + plotH + 34); 544 + 545 + // Tick labels (just endpoints + 0V) 546 + ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 547 + ctx.fillStyle = theme.muted; 548 + ctx.fillText("0", padL - 10, padT + plotH + 18); 549 + ctx.fillText("ℓ", padL + plotW - 10, padT + plotH + 18); 550 + const y0 = yOfV(0); 551 + ctx.fillText("0V", 18, y0 + 4); 552 + 553 + // Compute spatial waveforms at time tn 554 + // Incident: step from z=0 to z=zi with amplitude V1 555 + // Reflected (after tn>=1): step from z=1 down to z=zr with amplitude V2 556 + const zi = clamp(tn, 0, 1); 557 + const zr = (tn >= 1) ? clamp(1 - (tn - 1), 0, 1) : 1; // when tn=1 -> zr=1 558 + 559 + // Draw incident waveform 560 + drawStep(ctx, xOfZ, yOfV, 0, zi, waves.V1, theme.accent); 561 + 562 + // Draw reflected waveform 563 + if (tn >= 1) { 564 + drawStep(ctx, xOfZ, yOfV, zr, 1, waves.V2, theme.accent2); 565 + } 566 + 567 + // Draw total waveform (superposition) 568 + // Total is V1 on [0, zi]; plus V2 on [zr, 1] (when reflected exists). 569 + // Overlap region exists when zr < zi (late times): total becomes V1+V2 there. 570 + const segments = []; 571 + // Build segments on z axis with constant total voltage 572 + // Casework: 573 + // - Before reflection: [0,zi] = V1; [zi,1]=0 574 + // - After reflection: add V2 on [zr,1] 575 + if (tn < 1) { 576 + segments.push({ a: 0, b: zi, V: waves.V1 }); 577 + segments.push({ a: zi, b: 1, V: 0 }); 578 + } else { 579 + // Partition at sorted breakpoints 580 + const pts = [0, zi, zr, 1].sort((a,b)=>a-b); 581 + for (let k=0;k<pts.length-1;k++){ 582 + const a = pts[k], b = pts[k+1]; 583 + const mid = (a+b)/2; 584 + let V = 0; 585 + if (mid <= zi) V += waves.V1; 586 + if (mid >= zr) V += waves.V2; 587 + segments.push({ a, b, V }); 588 + } 589 + } 590 + drawPiecewise(ctx, xOfZ, yOfV, segments, theme.ok, 2.8); 591 + 592 + // Wavefront markers (vertical dotted lines at zi and zr) 593 + ctx.strokeStyle = theme.warn; 594 + ctx.lineWidth = 1.4; 595 + ctx.setLineDash([5, 5]); 596 + line(ctx, xOfZ(zi), padT, xOfZ(zi), padT + plotH); 597 + if (tn >= 1) line(ctx, xOfZ(zr), padT, xOfZ(zr), padT + plotH); 598 + ctx.setLineDash([]); 599 + 600 + // Caption-ish 601 + ctx.fillStyle = theme.muted; 602 + ctx.font = "12px ui-sans-serif, system-ui"; 603 + ctx.fillText("Wavefront(s) shown as dashed lines; total is the physically relevant waveform along the line.", padL, padT - 6); 604 + } 605 + 606 + function drawStep(ctx, xOfZ, yOfV, zA, zB, V, color) { 607 + // Draw a step that is V on [zA,zB], 0 elsewhere. 608 + // We draw as a polyline: (0,0)->(zA,0)->(zA,V)->(zB,V)->(zB,0)->(1,0) 609 + ctx.strokeStyle = color; 610 + ctx.lineWidth = 2.2; 611 + ctx.beginPath(); 612 + ctx.moveTo(xOfZ(0), yOfV(0)); 613 + ctx.lineTo(xOfZ(zA), yOfV(0)); 614 + ctx.lineTo(xOfZ(zA), yOfV(V)); 615 + ctx.lineTo(xOfZ(zB), yOfV(V)); 616 + ctx.lineTo(xOfZ(zB), yOfV(0)); 617 + ctx.lineTo(xOfZ(1), yOfV(0)); 618 + ctx.stroke(); 619 + } 620 + 621 + function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 622 + ctx.strokeStyle = color; 623 + ctx.lineWidth = width; 624 + ctx.beginPath(); 625 + // Start at z=0 626 + ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V)); 627 + for (const seg of segments) { 628 + ctx.lineTo(xOfZ(seg.a), yOfV(seg.V)); 629 + ctx.lineTo(xOfZ(seg.b), yOfV(seg.V)); 630 + } 631 + ctx.stroke(); 632 + } 633 + 634 + // ----------------------------- 635 + // Animation loop 636 + // ----------------------------- 637 + function render() { 638 + recomputeModelFromInputs(); 639 + const waves = computeWaveParams(); 640 + const ends = computeEndVoltages(tNorm, waves.V1, waves.V2); 641 + 642 + el.tRead.textContent = fmt(tNorm, 3); 643 + el.gLRead.textContent = fmt(waves.gL, 3); 644 + el.v1Read.textContent = `${fmt(waves.V1, 3)} V`; 645 + el.v2Read.textContent = `${fmt(waves.V2, 3)} V`; 646 + el.vsRead.textContent = `${fmt(ends.VS, 3)} V`; 647 + el.vlRead.textContent = `${fmt(ends.VL, 3)} V`; 648 + 649 + // Circuit 650 + const c = resizeCanvasToCSS(el.circuit); 651 + drawCircuit(c.ctx, c.w, c.h, tNorm, waves); 652 + 653 + // Plot 654 + const p = resizeCanvasToCSS(el.plot); 655 + drawPlot(p.ctx, p.w, p.h, tNorm, waves); 656 + } 657 + 658 + function tick(ts) { 659 + if (!running) return; 660 + if (lastTS == null) lastTS = ts; 661 + const dt = (ts - lastTS) / 1000; 662 + lastTS = ts; 663 + 664 + const secPerTau = Math.max(0.2, model.secPerTau); 665 + const dtn = dt / secPerTau; 666 + tNorm += dtn; 667 + 668 + // Stop condition: 669 + // - If singleBounce: run a bit past 2τd so you see the return to the source. 670 + // - Else: just loop. 671 + if (model.singleBounce) { 672 + if (tNorm > 2.4) { // let it linger a moment 673 + running = false; 674 + lastTS = null; 675 + } 676 + } else { 677 + if (tNorm > 3.0) tNorm = 0; // placeholder loop for now 678 + } 679 + 680 + render(); 681 + requestAnimationFrame(tick); 682 + } 683 + 684 + function start() { 685 + tNorm = 0; 686 + running = true; 687 + lastTS = null; 688 + render(); 689 + requestAnimationFrame(tick); 690 + } 691 + 692 + function pause() { 693 + running = false; 694 + lastTS = null; 695 + render(); 696 + } 697 + 698 + function reset() { 699 + running = false; 700 + lastTS = null; 701 + tNorm = 0; 702 + render(); 703 + } 704 + 705 + // ----------------------------- 706 + // Scrubbing (optional) 707 + // ----------------------------- 708 + function scrubFromEvent(ev) { 709 + if (!el.scrubToggle.checked) return; 710 + if (running) return; 711 + 712 + // Map x-position over either canvas to tNorm in [0,2.2] for single-bounce view 713 + const rect = ev.target.getBoundingClientRect(); 714 + const x = clamp(ev.clientX - rect.left, 0, rect.width); 715 + const u = x / rect.width; 716 + 717 + // Focus on 0..2τd timeline (so you can sweep incident to reflection-to-return) 718 + tNorm = u * 2.2; 719 + render(); 720 + } 721 + 722 + // ----------------------------- 723 + // Wiring events 724 + // ----------------------------- 725 + el.startBtn.addEventListener("click", start); 726 + el.pauseBtn.addEventListener("click", pause); 727 + el.resetBtn.addEventListener("click", reset); 728 + 729 + for (const inp of [el.Vg, el.Rg, el.Z0, el.RL, el.secPerTau, el.singleBounce]) { 730 + inp.addEventListener("input", () => { if (!running) render(); }); 731 + inp.addEventListener("change", () => { if (!running) render(); }); 732 + } 733 + 734 + el.plot.addEventListener("mousemove", scrubFromEvent); 735 + el.circuit.addEventListener("mousemove", scrubFromEvent); 736 + 737 + window.addEventListener("resize", () => render()); 738 + 739 + // First paint 740 + render(); 741 + 742 + })(); 743 + </script> 744 + </body> 745 + </html>