this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v2 1062 lines 39 kB view raw
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// Multi-segment support: 8// drawCircuit takes a `segments` array and draws N abutting T-line boxes. 9// Wave voltage helpers respect each packet's z-range [zStart, zEnd]. 10// drawPlot draws light vertical tick marks at internal segment boundaries. 11 12const TLRender = (() => { 13 // ---- plot layout constants ---- 14 const PLOT_PANEL_H = 150; 15 const PLOT_PANEL_GAP = 10; 16 const PLOT_PAD_T = 14; 17 const PLOT_PAD_B = 26; 18 19 // ---- theme ---- 20 function getTheme() { 21 const get = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim(); 22 return { 23 bg: get("--bg"), 24 panel: get("--panel"), 25 ink: get("--ink"), 26 muted: get("--muted"), 27 grid: get("--grid"), 28 accent: get("--accent"), 29 accent2: get("--accent2"), 30 warn: get("--warn"), 31 ok: get("--ok"), 32 tdr: get("--tdr"), 33 probe2: get("--probe2"), 34 }; 35 } 36 37 // ---- primitive helpers ---- 38 function line(ctx, x0, y0, x1, y1) { 39 ctx.beginPath(); 40 ctx.moveTo(x0, y0); 41 ctx.lineTo(x1, y1); 42 ctx.stroke(); 43 } 44 45 function circle(ctx, x, y, r, strokeStyle) { 46 ctx.strokeStyle = strokeStyle; 47 ctx.lineWidth = 2; 48 ctx.beginPath(); 49 ctx.arc(x, y, r, 0, Math.PI * 2); 50 ctx.stroke(); 51 } 52 53 function circleFill(ctx, x, y, r, fillStyle) { 54 ctx.fillStyle = fillStyle; 55 ctx.beginPath(); 56 ctx.arc(x, y, r, 0, Math.PI * 2); 57 ctx.fill(); 58 } 59 60 function label(ctx, text, x, y, color) { 61 ctx.fillStyle = color; 62 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 63 ctx.fillText(text, x, y); 64 } 65 66 // Return a "nice" tick step for a given range and target number of ticks. 67 function niceStep(range, targetCount) { 68 const raw = range / targetCount; 69 const mag = Math.pow(10, Math.floor(Math.log10(raw))); 70 const norm = raw / mag; 71 const nice = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10; 72 return nice * mag; 73 } 74 75 // ---- circuit component primitives ---- 76 function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp, theme) { 77 ctx.strokeStyle = theme.ink; 78 ctx.lineWidth = 2; 79 const dx = x1 - x0, dy = y1 - y0; 80 const len = Math.hypot(dx, dy); 81 if (len < 1e-6) return; 82 const ux = dx / len, uy = dy / len; 83 const px = -uy, py = ux; 84 85 const lead = 10; 86 const start = { x: x0 + ux * lead, y: y0 + uy * lead }; 87 const end = { x: x1 - ux * lead, y: y1 - uy * lead }; 88 89 line(ctx, x0, y0, start.x, start.y); 90 line(ctx, end.x, end.y, x1, y1); 91 92 const segs = zigZagCount * 2 - 1; 93 const segLen = (len - 2 * lead) / segs; 94 ctx.beginPath(); 95 ctx.moveTo(start.x, start.y); 96 for (let i = 1; i < segs; i++) { 97 const s = i * segLen; 98 const flip = (i % 2 === 0) ? -1 : 1; 99 ctx.lineTo(start.x + ux * s + px * amp * flip, 100 start.y + uy * s + py * amp * flip); 101 } 102 ctx.lineTo(end.x, end.y); 103 ctx.stroke(); 104 } 105 106 function drawSwitch(ctx, x, y, tn, theme) { 107 const closed = tn > 0; 108 ctx.strokeStyle = theme.ink; 109 ctx.lineWidth = 2; 110 const a = { x: x - 18, y }; 111 const b = { x: x + 18, y }; 112 circleFill(ctx, a.x, a.y, 3.2, theme.ink); 113 circleFill(ctx, b.x, b.y, 3.2, theme.ink); 114 if (closed) { 115 line(ctx, a.x, a.y, b.x, b.y); 116 } else { 117 line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14); 118 } 119 label(ctx, "t = 0", x - 18, y + 26, theme.muted); 120 } 121 122 function drawVoltageProbe(ctx, x, yTop, yBot, name, theme) { 123 ctx.fillStyle = theme.muted; 124 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 125 ctx.fillText("+", x + 6, yTop + 12); 126 ctx.fillText("\u2212", x + 6, yBot - 4); 127 label(ctx, name, x + 5, (yTop + yBot) / 2 - 6, theme.muted); 128 } 129 130 // ---- shunt element symbols ---- 131 // All draw vertically from (x, yA) down to (x, yB), connecting signal to GND. 132 133 function drawShuntR(ctx, x, yA, yB, theme) { 134 ctx.strokeStyle = theme.ink; 135 ctx.lineWidth = 2; 136 drawResistor(ctx, x, yA, x, yB, 4, 7, theme); 137 } 138 139 function drawShuntC(ctx, x, yA, yB, theme) { 140 const midY = (yA + yB) / 2; 141 const plateHalfW = 13; 142 const plateGap = 9; 143 ctx.strokeStyle = theme.ink; 144 ctx.lineWidth = 2; 145 line(ctx, x, yA, x, midY - plateGap / 2); 146 line(ctx, x - plateHalfW, midY - plateGap / 2, x + plateHalfW, midY - plateGap / 2); 147 line(ctx, x - plateHalfW, midY + plateGap / 2, x + plateHalfW, midY + plateGap / 2); 148 line(ctx, x, midY + plateGap / 2, x, yB); 149 } 150 151 function drawShuntL(ctx, x, yA, yB, theme) { 152 const nBumps = 4; 153 const bumpR = 7; 154 const bumpSpan = nBumps * bumpR * 2; 155 const midY = (yA + yB) / 2; 156 const bumpTop = midY - bumpSpan / 2; 157 ctx.strokeStyle = theme.ink; 158 ctx.lineWidth = 2; 159 line(ctx, x, yA, x, bumpTop); 160 for (let i = 0; i < nBumps; i++) { 161 const cy = bumpTop + (i + 0.5) * bumpR * 2; 162 ctx.beginPath(); 163 ctx.arc(x, cy, bumpR, Math.PI, 0); 164 ctx.stroke(); 165 } 166 line(ctx, x, bumpTop + bumpSpan, x, yB); 167 } 168 169 function drawShuntShort(ctx, x, yA, yB, theme) { 170 ctx.strokeStyle = theme.ink; 171 ctx.lineWidth = 3; 172 line(ctx, x, yA, x, yB); 173 ctx.lineWidth = 2; 174 } 175 176 // Draw a shunt element of given type between (x, yA) and (x, yB), with dots at both nodes. 177 function drawShuntElement(ctx, x, yA, yB, type, theme) { 178 circleFill(ctx, x, yA, 4, theme.ink); 179 circleFill(ctx, x, yB, 4, theme.ink); 180 if (type === 'R') drawShuntR(ctx, x, yA, yB, theme); 181 else if (type === 'C') drawShuntC(ctx, x, yA, yB, theme); 182 else if (type === 'L') drawShuntL(ctx, x, yA, yB, theme); 183 else if (type === 'short') drawShuntShort(ctx, x, yA, yB, theme); 184 } 185 186 // ---- circuit canvas ---- 187 // segments = [{Z0}, …] (N entries) 188 // terminal = {type: 'R'|'C'|'L'|'open'|'short', value?} 189 // junctions = [{type: 'none'|'R'|'C'|'L'|'short'}] length N-1 (internal junctions) 190 // wavefronts = [{z, dir}] (optional; positions in [0,1] from active waves) 191 function drawCircuit(ctx, w, h, tn, theme, segments, terminal, junctions, wavefronts) { 192 terminal = terminal || { type: 'open' }; 193 junctions = junctions || []; 194 wavefronts = wavefronts || []; 195 const rlIsOpen = terminal.type === 'open'; 196 const rlIsShort = terminal.type === 'short'; 197 ctx.clearRect(0, 0, w, h); 198 ctx.fillStyle = theme.panel; 199 ctx.fillRect(0, 0, w, h); 200 201 const pad = 18; 202 const yTop = 70; 203 const yBot = 190; 204 205 const xSourceL = pad + 60; 206 const xSwitch = pad + 170; 207 const xTL0 = pad + 240; 208 const xTL1 = w - pad - 210; 209 const xLoad = w - pad - 160; 210 const xRight = w - pad - 90; 211 212 ctx.lineWidth = 2; 213 ctx.strokeStyle = theme.ink; 214 215 // Top and bottom wires — always run to xRight regardless of termination 216 line(ctx, xSwitch + 18, yTop, xTL0, yTop); 217 line(ctx, xTL1, yTop, xRight, yTop); 218 line(ctx, xSourceL, yBot, xTL0, yBot); 219 line(ctx, xTL1, yBot, xRight, yBot); 220 221 // Voltage source 222 const vsx = xSourceL, vsy = (yTop + yBot) / 2; 223 line(ctx, xSourceL, yTop, xSourceL, vsy - 20); 224 line(ctx, xSourceL, vsy + 20, xSourceL, yBot); 225 circle(ctx, vsx, vsy, 20, theme.ink); 226 ctx.fillStyle = theme.ink; 227 ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 228 ctx.fillText("+", vsx - 4, vsy - 6); 229 ctx.fillText("\u2212", vsx - 4, vsy + 14); 230 label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted); 231 232 // Source resistor Rg 233 const r0 = xSourceL + 20, r1 = xSwitch - 30; 234 line(ctx, xSourceL, yTop, r0, yTop); 235 drawResistor(ctx, r0, yTop, r1, yTop, 5, 8, theme); 236 line(ctx, r1, yTop, xSwitch - 18, yTop); 237 label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted); 238 239 drawSwitch(ctx, xSwitch, yTop, tn, theme); 240 241 // Transmission line boxes — N abutting pairs (top + bottom conductor) 242 const N = segments.length; 243 const tlW = xTL1 - xTL0; 244 const segPxW = tlW / N; 245 ctx.lineWidth = 2; 246 ctx.strokeStyle = theme.ink; 247 248 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 249 for (let i = 0; i < N; i++) { 250 const bx = xTL0 + i * segPxW; 251 ctx.strokeRect(bx, yTop - 16, segPxW, 32); 252 ctx.strokeRect(bx, yBot - 16, segPxW, 32); 253 254 // Label above top box: "Z₀" for single segment, "50Ω" etc. for multi 255 const lbl = N === 1 256 ? "Z\u2080" 257 : `${segments[i].Z0}\u03A9`; 258 const lblX = bx + segPxW / 2 - ctx.measureText(lbl).width / 2; 259 label(ctx, lbl, lblX, yTop - 22, theme.muted); 260 } 261 262 // Shunt junction elements (between segments) 263 const shuntYA = yTop + 16; // just below top box 264 const shuntYB = yBot - 16; // just above bottom box 265 for (let i = 0; i < N - 1; i++) { 266 const junc = junctions[i]; 267 if (!junc || junc.type === 'none') continue; 268 const xj = xTL0 + (i + 1) * segPxW; 269 drawShuntElement(ctx, xj, shuntYA, shuntYB, junc.type, theme); 270 } 271 272 // Load — resistor, capacitor, inductor, short, or open 273 const rlTop = yTop + 22, rlBot = yBot - 22; 274 ctx.strokeStyle = theme.ink; 275 ctx.lineWidth = 2; 276 if (rlIsOpen) { 277 circleFill(ctx, xRight, yTop, 4.5, theme.ink); 278 circleFill(ctx, xRight, yBot, 4.5, theme.ink); 279 } else if (rlIsShort) { 280 ctx.lineWidth = 3; 281 line(ctx, xLoad, yTop, xLoad, yBot); 282 ctx.lineWidth = 2; 283 } else if (terminal.type === 'R') { 284 line(ctx, xLoad, yTop, xLoad, rlTop); 285 drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 5, 8, theme); 286 line(ctx, xLoad, rlBot, xLoad, yBot); 287 label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 288 } else if (terminal.type === 'C') { 289 line(ctx, xLoad, yTop, xLoad, rlTop); 290 drawShuntC(ctx, xLoad, rlTop, rlBot, theme); 291 line(ctx, xLoad, rlBot, xLoad, yBot); 292 label(ctx, "CL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 293 } else if (terminal.type === 'L') { 294 line(ctx, xLoad, yTop, xLoad, rlTop); 295 drawShuntL(ctx, xLoad, rlTop, rlBot, theme); 296 line(ctx, xLoad, rlBot, xLoad, yBot); 297 label(ctx, "LL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 298 } 299 300 // Voltage probes 301 drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS", theme); 302 drawVoltageProbe(ctx, xLoad + 55, yTop, yBot, "VL", theme); 303 304 // Terminal dots at the VL port — top and bottom 305 if (!rlIsOpen) { 306 circleFill(ctx, xRight, yTop, 3.5, theme.ink); 307 circleFill(ctx, xRight, yBot, 3.5, theme.ink); 308 } 309 310 // Probe 1 — dots on top and bottom wire between switch and T-line input 311 const xDetect = Math.round((xSwitch + 18 + xTL0) / 2); 312 circleFill(ctx, xDetect, yTop, 5.5, theme.tdr); 313 circleFill(ctx, xDetect, yBot, 5.5, theme.tdr); 314 315 // Probe 2 — dots at the load node (far end of last T-line) 316 circleFill(ctx, xLoad, yTop, 5.5, theme.probe2); 317 circleFill(ctx, xLoad, yBot, 5.5, theme.probe2); 318 ctx.fillStyle = theme.tdr; 319 ctx.font = "11px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 320 ctx.textAlign = "left"; 321 322 // Wavefront markers (dashed vertical lines inside T-line boxes) 323 const wfY0 = yTop - 16; 324 const wfY1 = yBot + 16; 325 for (const wf of wavefronts) { 326 const xw = xTL0 + wf.z * tlW; 327 ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 328 ctx.lineWidth = 3; 329 ctx.setLineDash([9, 7]); 330 line(ctx, xw, wfY0, xw, wfY1); 331 ctx.setLineDash([]); 332 } 333 334 label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted); 335 label(ctx, "z = \u2113", xTL1 - 18, yBot + 34, theme.muted); 336 } 337 338 // ---- wave-shape helpers ---- 339 340 // Voltage profile of a single wave packet as piecewise-constant segments over [0,1]. 341 // The packet only contributes amplitude in its own z-range [zStart, zEnd]. 342 function segmentsForWave(wf) { 343 const { dir, zStart, A, front } = wf; 344 let segs; 345 if (dir > 0) { 346 // Nonzero in [zStart, front]; zero elsewhere. 347 segs = [ 348 { a: 0, b: zStart, V: 0 }, 349 { a: zStart, b: front, V: A }, 350 { a: front, b: 1, V: 0 }, 351 ]; 352 } else { 353 // Leftward: zStart is the right edge; nonzero in [front, zStart]. 354 segs = [ 355 { a: 0, b: front, V: 0 }, 356 { a: front, b: zStart, V: A }, 357 { a: zStart, b: 1, V: 0 }, 358 ]; 359 } 360 const valid = segs.filter(s => s.b > s.a + 1e-9); 361 return valid.length ? valid : [{ a: 0, b: 1, V: 0 }]; 362 } 363 364 // Sum of all launched waves as piecewise-constant segments over [0,1]. 365 // Breakpoints include every wave's front, zStart, and zEnd so that the 366 // midpoint probe correctly captures all z-range transitions. 367 function totalSegmentsForWaves(waves) { 368 const breakpoints = [0, 1]; 369 for (const wf of waves) { 370 breakpoints.push(wf.front); 371 breakpoints.push(wf.zStart); 372 breakpoints.push(wf.zEnd); 373 } 374 breakpoints.sort((a, b) => a - b); 375 376 const pts = []; 377 for (const x of breakpoints) { 378 if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x); 379 } 380 381 const segs = []; 382 for (let i = 0; i < pts.length - 1; i++) { 383 const a = pts[i], b = pts[i + 1]; 384 const m = (a + b) / 2; 385 let V = 0; 386 for (const wf of waves) { 387 if (wf.dir > 0 && m >= wf.zStart - 1e-9 && m <= wf.front + 1e-9) V += wf.A; 388 if (wf.dir < 0 && m >= wf.front - 1e-9 && m <= wf.zStart + 1e-9) V += wf.A; 389 } 390 segs.push({ a, b, V }); 391 } 392 return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 393 } 394 395 // Draw a piecewise-constant voltage profile. 396 function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 397 ctx.strokeStyle = color; 398 ctx.lineWidth = width; 399 ctx.beginPath(); 400 ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V)); 401 for (const seg of segments) { 402 ctx.lineTo(xOfZ(seg.a), yOfV(seg.V)); 403 ctx.lineTo(xOfZ(seg.b), yOfV(seg.V)); 404 } 405 ctx.stroke(); 406 } 407 408 // Draw a continuous voltage profile by sampling vFn(z) at N evenly-spaced z values. 409 function drawSampledWave(ctx, xOfZ, yOfV, vFn, color, width, N = 400) { 410 ctx.strokeStyle = color; 411 ctx.lineWidth = width; 412 ctx.beginPath(); 413 for (let i = 0; i <= N; i++) { 414 const z = i / N; 415 if (i === 0) ctx.moveTo(xOfZ(z), yOfV(vFn(z))); 416 else ctx.lineTo(xOfZ(z), yOfV(vFn(z))); 417 } 418 ctx.stroke(); 419 } 420 421 // ---- plot canvas ---- 422 // segments = model.segments (used to draw boundary tick marks) 423 function drawPlot(ctx, w, h, tn, dyn, theme, riseTimeTr = 0, riseShape = "step", segments = [{ Z0: 50 }]) { 424 ctx.clearRect(0, 0, w, h); 425 ctx.fillStyle = theme.panel; 426 ctx.fillRect(0, 0, w, h); 427 428 const circuitPad = 18; 429 const xPlot0 = circuitPad + 240; 430 const xPlot1 = w - circuitPad - 210; 431 const plotW = xPlot1 - xPlot0; 432 const xOfZ = (z) => xPlot0 + z * plotW; 433 434 const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 435 const sumSegs = totalSegmentsForWaves(launched); 436 437 const maxAbsWave = Math.max(1e-6, ...launched.map((wf) => Math.abs(wf.A))); 438 const maxAbsSum = Math.max(1e-6, sumSegs.reduce((m, s) => Math.max(m, Math.abs(s.V)), 0)); 439 const vScale = Math.max(maxAbsWave, maxAbsSum); 440 const vLo = -1.15 * vScale; 441 const vHi = 1.15 * vScale; 442 443 ctx.fillStyle = theme.muted; 444 ctx.font = "12px ui-sans-serif, system-ui"; 445 ctx.fillText("Voltage along the T-line", 12, 14); 446 447 // Draw axes/border for one panel, with y-axis tick marks and labels. 448 function drawPanelFrame(top, bot, yOfV, labelText, vLo, vHi) { 449 const step = niceStep(vHi - vLo, 5); 450 const decimals = step >= 1 ? 0 : step >= 0.1 ? 1 : 2; 451 // Index of first tick at or above vLo 452 const iTick0 = Math.ceil(vLo / step); 453 454 // Horizontal grid lines at each tick (drawn first, behind everything) 455 ctx.strokeStyle = theme.grid; 456 ctx.lineWidth = 1; 457 for (let i = iTick0; ; i++) { 458 const v = i * step; 459 if (v > vHi + step * 0.01) break; 460 const yv = yOfV(v); 461 if (yv < top - 1 || yv > bot + 1) continue; 462 line(ctx, xPlot0, yv, xPlot1, yv); 463 } 464 465 // Border (on top of grid lines) 466 ctx.strokeStyle = theme.grid; 467 ctx.lineWidth = 1; 468 line(ctx, xPlot0, top, xPlot1, top); 469 line(ctx, xPlot0, bot, xPlot1, bot); 470 line(ctx, xPlot0, top, xPlot0, bot); 471 line(ctx, xPlot1, top, xPlot1, bot); 472 473 // Zero line — slightly more visible than other grid lines 474 const yZero = yOfV(0); 475 if (yZero >= top - 1 && yZero <= bot + 1) { 476 ctx.strokeStyle = theme.muted; 477 ctx.lineWidth = 1.2; 478 line(ctx, xPlot0, yZero, xPlot1, yZero); 479 } 480 481 // Tick marks and labels on the left y-axis 482 ctx.textAlign = "right"; 483 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 484 for (let i = iTick0; ; i++) { 485 const v = i * step; 486 if (v > vHi + step * 0.01) break; 487 const yv = yOfV(v); 488 if (yv < top - 1 || yv > bot + 1) continue; 489 ctx.strokeStyle = theme.muted; 490 ctx.lineWidth = 1; 491 line(ctx, xPlot0 - 4, yv, xPlot0, yv); 492 ctx.fillStyle = theme.muted; 493 ctx.fillText(v.toFixed(decimals), xPlot0 - 7, yv + 4); 494 } 495 ctx.textAlign = "left"; 496 497 // Panel title and axis labels 498 ctx.fillStyle = theme.muted; 499 ctx.font = "12px ui-sans-serif, system-ui"; 500 ctx.fillText(labelText, xPlot0 + 8, top + 14); 501 ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4); 502 503 // Segment boundary tick marks (for multi-segment lines) 504 const N = segments.length; 505 if (N > 1) { 506 ctx.strokeStyle = theme.grid; 507 ctx.lineWidth = 1; 508 ctx.setLineDash([3, 4]); 509 for (let i = 1; i < N; i++) { 510 const xb = xOfZ(i / N); 511 line(ctx, xb, top, xb, bot); 512 } 513 ctx.setLineDash([]); 514 } 515 } 516 517 // Dashed wave-front position markers 518 function drawFrontMarkers(top, bot, fronts) { 519 if (!fronts.length) return; 520 ctx.strokeStyle = theme.warn; 521 ctx.lineWidth = 1.2; 522 ctx.setLineDash([4, 5]); 523 for (const zf of fronts) line(ctx, xOfZ(zf), top, xOfZ(zf), bot); 524 ctx.setLineDash([]); 525 } 526 527 const panelH = PLOT_PANEL_H; 528 const { waveVoltageAt, totalVoltageAt } = TLPhysics; 529 const smooth = riseTimeTr > 0 && riseShape !== "step"; 530 531 // Panel 1 — sum of all waves 532 const top0 = PLOT_PAD_T; 533 const bot0 = top0 + panelH; 534 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 535 drawPanelFrame(top0, bot0, y0, "Sum (all waves)", vLo, vHi); 536 if (smooth) { 537 drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTr, riseShape, segments.length), theme.ok, 2.4); 538 } else { 539 drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 540 } 541 drawFrontMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front)); 542 543 // Panel 2 — individual component waves 544 const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP; 545 const bot1 = top1 + panelH; 546 const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 547 drawPanelFrame(top1, bot1, y1, "Components (all waves)", vLo, vHi); 548 549 const waveStyles = [ 550 { color: theme.accent, dash: [] }, 551 { color: theme.accent2, dash: [] }, 552 { color: theme.accent, dash: [9, 6] }, 553 { color: theme.accent2, dash: [9, 6] }, 554 { color: theme.accent, dash: [2, 5] }, 555 { color: theme.accent2, dash: [2, 5] }, 556 { color: theme.accent, dash: [16, 5] }, 557 { color: theme.accent2, dash: [16, 5] }, 558 { color: theme.accent, dash: [8, 4, 2, 4] }, 559 { color: theme.accent2, dash: [8, 4, 2, 4] }, 560 ]; 561 562 const FADE_DURATION = 4; 563 const FADE_MIN = 0.08; 564 565 for (let i = 0; i < launched.length; i++) { 566 const wf = launched[i]; 567 const style = waveStyles[i % waveStyles.length]; 568 // Fade after the wave front has reached its endpoint (tDie). 569 const alpha = TLUtils.clamp(1 - (tn - wf.tDie) / FADE_DURATION, FADE_MIN, 1); 570 ctx.globalAlpha = alpha; 571 ctx.setLineDash(style.dash); 572 if (smooth) { 573 drawSampledWave(ctx, xOfZ, y1, (z) => waveVoltageAt(wf, z, riseTimeTr, riseShape), style.color, 2.0); 574 } else { 575 drawPiecewise(ctx, xOfZ, y1, segmentsForWave(wf), style.color, 2.0); 576 } 577 ctx.setLineDash([]); 578 ctx.globalAlpha = 1; 579 } 580 drawFrontMarkers(top1, bot1, launched.filter((wf) => wf.u < Math.abs(wf.zEnd - wf.zStart)).map((wf) => wf.front)); 581 582 // z-axis endpoint labels 583 ctx.fillStyle = theme.muted; 584 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 585 ctx.fillText("0", xPlot0 - 4, h - 6); 586 ctx.fillText("\u2113", xPlot1 - 4, h - 6); 587 } 588 589 // ---- plot canvas (MoC sim mode) ---- 590 // Draws total V(z,t) and per-segment V⁺/V⁻ from simulateTimeDomain output. 591 // sim: return value of simulateTimeDomain 592 // tIdx: time step index (tNorm / sim.dt, clamped to [0, nSteps-1]) 593 function drawPlotSim(ctx, w, h, tIdx, sim, theme) { 594 ctx.clearRect(0, 0, w, h); 595 ctx.fillStyle = theme.panel; 596 ctx.fillRect(0, 0, w, h); 597 598 const circuitPad = 18; 599 const xPlot0 = circuitPad + 240; 600 const xPlot1 = w - circuitPad - 210; 601 const plotW = xPlot1 - xPlot0; 602 const xOfZ = (z) => xPlot0 + z * plotW; 603 604 const { voltageAt } = TLPhysics; 605 const N = sim.segs.length; 606 const step = Math.max(0, Math.min(tIdx, sim.nSteps - 1)); 607 608 // Sample total voltage at evenly-spaced z values 609 const NPTS = 400; 610 const vArr = []; 611 for (let i = 0; i <= NPTS; i++) { 612 vArr.push(voltageAt(i / NPTS, step, sim)); 613 } 614 615 // Per-segment V⁺ and V⁻ (sampled from vPlusHist / vMinusHist via voltageAt internals) 616 // We read them directly from the history arrays using the same delay logic as voltageAt. 617 const totalTau = sim.segs.reduce((s, seg) => s + seg.tau, 0); 618 619 const vScale = Math.max(1e-6, ...vArr.map(Math.abs)); 620 const vLo = -1.15 * vScale, vHi = 1.15 * vScale; 621 622 ctx.fillStyle = theme.muted; 623 ctx.font = "12px ui-sans-serif, system-ui"; 624 ctx.fillText("Voltage along the T-line (MoC simulation)", 12, 14); 625 626 const panelH = PLOT_PANEL_H; 627 const top0 = PLOT_PAD_T; 628 const bot0 = top0 + panelH; 629 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 630 631 // Panel frame helper (inline) 632 function drawFrame(top, bot, yOfV, labelText) { 633 const step_ = niceStep(vHi - vLo, 5); 634 const dec = step_ >= 1 ? 0 : step_ >= 0.1 ? 1 : 2; 635 const iTick0 = Math.ceil(vLo / step_); 636 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; 637 for (let i = iTick0; ; i++) { 638 const v = i * step_; 639 if (v > vHi + step_ * 0.01) break; 640 const yv = yOfV(v); 641 if (yv < top - 1 || yv > bot + 1) continue; 642 line(ctx, xPlot0, yv, xPlot1, yv); 643 } 644 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; 645 line(ctx, xPlot0, top, xPlot1, top); 646 line(ctx, xPlot0, bot, xPlot1, bot); 647 line(ctx, xPlot0, top, xPlot0, bot); 648 line(ctx, xPlot1, top, xPlot1, bot); 649 const yZero = yOfV(0); 650 if (yZero >= top - 1 && yZero <= bot + 1) { 651 ctx.strokeStyle = theme.muted; ctx.lineWidth = 1.2; 652 line(ctx, xPlot0, yZero, xPlot1, yZero); 653 } 654 ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 655 for (let i = iTick0; ; i++) { 656 const v = i * step_; 657 if (v > vHi + step_ * 0.01) break; 658 const yv = yOfV(v); 659 if (yv < top - 1 || yv > bot + 1) continue; 660 ctx.strokeStyle = theme.muted; ctx.lineWidth = 1; 661 line(ctx, xPlot0 - 4, yv, xPlot0, yv); 662 ctx.fillStyle = theme.muted; 663 ctx.fillText(v.toFixed(dec), xPlot0 - 7, yv + 4); 664 } 665 ctx.textAlign = "left"; 666 ctx.fillStyle = theme.muted; ctx.font = "12px ui-sans-serif, system-ui"; 667 ctx.fillText(labelText, xPlot0 + 8, top + 14); 668 ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4); 669 // Segment boundary tick marks 670 if (N > 1) { 671 let cumZ = 0; 672 ctx.strokeStyle = theme.grid; ctx.lineWidth = 1; ctx.setLineDash([3, 4]); 673 for (let i = 0; i < N - 1; i++) { 674 cumZ += sim.segs[i].tau / totalTau; 675 const xb = xOfZ(cumZ); 676 line(ctx, xb, top, xb, bot); 677 } 678 ctx.setLineDash([]); 679 } 680 } 681 682 drawFrame(top0, bot0, y0, "V(z, t) — total"); 683 684 // Draw total V(z,t) 685 ctx.strokeStyle = theme.ok; ctx.lineWidth = 2.4; 686 ctx.beginPath(); 687 for (let i = 0; i <= NPTS; i++) { 688 const x = xOfZ(i / NPTS), y = y0(vArr[i]); 689 if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 690 } 691 ctx.stroke(); 692 693 // Panel 2: V⁺ and V⁻ per segment 694 const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP; 695 const bot1 = top1 + panelH; 696 const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 697 drawFrame(top1, bot1, y1, "V\u207A (forward) and V\u207B (backward) per segment"); 698 699 let cumZ = 0; 700 for (let i = 0; i < N; i++) { 701 const segFrac = sim.segs[i].tau / totalTau; 702 const D = sim.nDelay[i]; 703 const zL = cumZ, zR = cumZ + segFrac; 704 705 // Sample V⁺ and V⁻ across this segment 706 const NSEG = Math.max(2, Math.round(NPTS * segFrac)); 707 ctx.strokeStyle = theme.accent; ctx.lineWidth = 1.8; 708 ctx.beginPath(); 709 for (let j = 0; j <= NSEG; j++) { 710 const f = j / NSEG; 711 const kFwd = Math.round(f * D); 712 const tF = step - kFwd; 713 const vP = (tF >= 0) ? sim.vPlusHist[i][tF] : 0; 714 const x = xOfZ(zL + f * segFrac), y = y1(vP); 715 if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 716 } 717 ctx.stroke(); 718 719 ctx.strokeStyle = theme.accent2; ctx.lineWidth = 1.8; 720 ctx.beginPath(); 721 for (let j = 0; j <= NSEG; j++) { 722 const f = j / NSEG; 723 const kBwd = Math.round((1 - f) * D); 724 const tB = step - kBwd; 725 const vM = (tB >= 0) ? sim.vMinusHist[i][tB] : 0; 726 const x = xOfZ(zL + f * segFrac), y = y1(vM); 727 if (j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); 728 } 729 ctx.stroke(); 730 731 cumZ += segFrac; 732 } 733 734 // z-axis labels 735 ctx.fillStyle = theme.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 736 ctx.fillText("0", xPlot0 - 4, h - 6); 737 ctx.fillText("\u2113", xPlot1 - 4, h - 6); 738 } 739 740 // ---- Probe canvas (MoC sim mode) ---- 741 // Draws V_source(t) from sim.nodeV[0]. 742 function drawProbeSim(ctx, w, h, tn, sim, timeHorizon_, theme_) { 743 ctx.clearRect(0, 0, w, h); 744 ctx.fillStyle = theme_.panel; 745 ctx.fillRect(0, 0, w, h); 746 747 const padL = 55, padR = 30, padT = 20, padB = 30; 748 const plotW_ = w - padL - padR; 749 const plotH_ = h - padT - padB; 750 const tMax = timeHorizon_; 751 const xOfT = (t) => padL + (t / tMax) * plotW_; 752 753 const vArr = sim.nodeV[0]; 754 const vArrL = sim.nodeV[sim.nodeV.length - 1]; 755 let vMax = 0, vMin = 0; 756 for (let k = 0; k < sim.nSteps; k++) { 757 if (vArr[k] > vMax) vMax = vArr[k]; 758 if (vArr[k] < vMin) vMin = vArr[k]; 759 if (vArrL[k] > vMax) vMax = vArrL[k]; 760 if (vArrL[k] < vMin) vMin = vArrL[k]; 761 } 762 vMax = Math.max(vMax, 0.01); 763 const span_ = Math.max(vMax - vMin, 0.05); 764 const vHi_ = vMax + 0.2 * span_; 765 const vLo_ = vMin - 0.15 * span_; 766 const yOfV_ = (v) => padT + (vHi_ - v) / (vHi_ - vLo_) * plotH_; 767 768 ctx.fillStyle = theme_.muted; ctx.font = "12px ui-sans-serif, system-ui"; 769 ctx.fillText("Probe \u2014 node voltages vs time", padL + 8, padT + 14); 770 771 const vStep_ = niceStep(vHi_ - vLo_, 4); 772 ctx.strokeStyle = theme_.grid; ctx.lineWidth = 1; 773 for (let i = Math.ceil(vLo_ / vStep_); ; i++) { 774 const v = i * vStep_; 775 if (v > vHi_ + vStep_ * 0.01) break; 776 const yv = yOfV_(v); 777 if (yv < padT - 1 || yv > padT + plotH_ + 1) continue; 778 line(ctx, padL, yv, padL + plotW_, yv); 779 } 780 line(ctx, padL, padT, padL + plotW_, padT); 781 line(ctx, padL, padT + plotH_, padL + plotW_, padT + plotH_); 782 line(ctx, padL, padT, padL, padT + plotH_); 783 line(ctx, padL + plotW_, padT, padL + plotW_, padT + plotH_); 784 const yZero_ = yOfV_(0); 785 if (yZero_ >= padT && yZero_ <= padT + plotH_) { 786 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1.2; 787 line(ctx, padL, yZero_, padL + plotW_, yZero_); 788 } 789 790 const vDec_ = vStep_ >= 1 ? 0 : vStep_ >= 0.1 ? 1 : 2; 791 ctx.textAlign = "right"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 792 for (let i = Math.ceil(vLo_ / vStep_); ; i++) { 793 const v = i * vStep_; 794 if (v > vHi_ + vStep_ * 0.01) break; 795 const yv = yOfV_(v); 796 if (yv < padT - 1 || yv > padT + plotH_ + 1) continue; 797 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1; 798 line(ctx, padL - 4, yv, padL, yv); 799 ctx.fillStyle = theme_.muted; 800 ctx.fillText(v.toFixed(vDec_), padL - 7, yv + 4); 801 } 802 ctx.textAlign = "left"; 803 804 const tStep_ = niceStep(tMax, 6); 805 const tDec_ = tStep_ >= 1 ? 0 : 1; 806 ctx.textAlign = "center"; ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 807 for (let t = 0; t <= tMax + tStep_ * 0.01; t += tStep_) { 808 const xt = xOfT(t); 809 if (xt < padL - 1 || xt > padL + plotW_ + 1) continue; 810 ctx.strokeStyle = theme_.muted; ctx.lineWidth = 1; 811 line(ctx, xt, padT + plotH_, xt, padT + plotH_ + 4); 812 ctx.fillStyle = theme_.muted; 813 ctx.fillText(t.toFixed(tDec_), xt, padT + plotH_ + 16); 814 } 815 ctx.textAlign = "left"; 816 ctx.fillStyle = theme_.muted; ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 817 ctx.fillText("t / \u03C4d", padL + plotW_ + 6, padT + plotH_ + 4); 818 819 const tnClamped_ = Math.min(tn, tMax); 820 const kEnd = Math.min(Math.round(tnClamped_ / sim.dt), sim.nSteps - 1); 821 822 // VS trace (probe 1 — orange) 823 ctx.strokeStyle = theme_.tdr; ctx.lineWidth = 2.2; 824 ctx.beginPath(); 825 for (let k = 0; k <= kEnd; k++) { 826 const t = k * sim.dt; 827 if (k === 0) ctx.moveTo(xOfT(t), yOfV_(vArr[k])); 828 else ctx.lineTo(xOfT(t), yOfV_(vArr[k])); 829 } 830 ctx.stroke(); 831 832 // VL trace (probe 2 — blue) 833 ctx.strokeStyle = theme_.probe2; ctx.lineWidth = 2.2; 834 ctx.beginPath(); 835 for (let k = 0; k <= kEnd; k++) { 836 const t = k * sim.dt; 837 if (k === 0) ctx.moveTo(xOfT(t), yOfV_(vArrL[k])); 838 else ctx.lineTo(xOfT(t), yOfV_(vArrL[k])); 839 } 840 ctx.stroke(); 841 842 if (tn > 0 && tn <= tMax) { 843 ctx.strokeStyle = theme_.warn; ctx.lineWidth = 1; ctx.setLineDash([4, 5]); 844 line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH_); 845 ctx.setLineDash([]); 846 } 847 848 const kNow = Math.max(0, Math.min(Math.round(tn / sim.dt), sim.nSteps - 1)); 849 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 850 ctx.textAlign = "right"; 851 ctx.fillStyle = theme_.tdr; 852 ctx.fillText("VS = " + vArr[kNow].toFixed(4) + " V", padL + plotW_ - 6, padT + 18); 853 ctx.fillStyle = theme_.probe2; 854 ctx.fillText("VL = " + vArrL[kNow].toFixed(4) + " V", padL + plotW_ - 6, padT + 34); 855 ctx.textAlign = "left"; 856 } 857 858 // ---- Probe canvas (bounce-series mode) ---- 859 // Draws VS(t) — voltage at the source node (z=0) vs time. 860 // The trace grows up to the current tNorm; a cursor line shows current time. 861 function drawProbe(ctx, w, h, tn, bounce, riseTimeTr, riseShape, timeHorizon, theme) { 862 ctx.clearRect(0, 0, w, h); 863 ctx.fillStyle = theme.panel; 864 ctx.fillRect(0, 0, w, h); 865 866 const padL = 55, padR = 30, padT = 20, padB = 30; 867 const plotW = w - padL - padR; 868 const plotH = h - padT - padB; 869 870 const tMax = timeHorizon; 871 const xOfT = (t) => padL + (t / tMax) * plotW; 872 873 const { sumEventsAtTime, sumEventsWithRise, sumEventsWithLinearRamp } = TLPhysics; 874 const eventsAt = (evts, t) => { 875 if (riseTimeTr <= 0 || riseShape === "step") return sumEventsAtTime(evts, t); 876 if (riseShape === "linear") return sumEventsWithLinearRamp(evts, t, riseTimeTr); 877 return sumEventsWithRise(evts, t, riseTimeTr); 878 }; 879 const vsAtTime = (t) => eventsAt(bounce.srcEvents, t); 880 const vlAtTime = (t) => eventsAt(bounce.loadEvents, t); 881 882 // y range from both srcEvents and loadEvents cumulative sums 883 const events = [...bounce.srcEvents].sort((a, b) => a.t - b.t); 884 const eventsL = [...bounce.loadEvents].sort((a, b) => a.t - b.t); 885 let cumV = 0, vMax = 0, vMin = 0; 886 for (const ev of events) { 887 cumV += ev.dV; 888 if (cumV > vMax) vMax = cumV; 889 if (cumV < vMin) vMin = cumV; 890 } 891 cumV = 0; 892 for (const ev of eventsL) { 893 cumV += ev.dV; 894 if (cumV > vMax) vMax = cumV; 895 if (cumV < vMin) vMin = cumV; 896 } 897 vMax = Math.max(vMax, 0.01); 898 vMin = Math.min(vMin, 0); 899 const span = Math.max(vMax - vMin, 0.05); 900 const vHi = vMax + 0.2 * span; 901 const vLo = vMin - 0.15 * span; 902 const yOfV = (v) => padT + (vHi - v) / (vHi - vLo) * plotH; 903 904 // Title 905 ctx.fillStyle = theme.muted; 906 ctx.font = "12px ui-sans-serif, system-ui"; 907 ctx.fillText("Probe \u2014 node voltages vs time", padL + 8, padT + 14); 908 909 // Grid lines 910 const vStep = niceStep(vHi - vLo, 4); 911 ctx.strokeStyle = theme.grid; 912 ctx.lineWidth = 1; 913 for (let i = Math.ceil(vLo / vStep); ; i++) { 914 const v = i * vStep; 915 if (v > vHi + vStep * 0.01) break; 916 const yv = yOfV(v); 917 if (yv < padT - 1 || yv > padT + plotH + 1) continue; 918 line(ctx, padL, yv, padL + plotW, yv); 919 } 920 921 // Border 922 ctx.strokeStyle = theme.grid; 923 ctx.lineWidth = 1; 924 line(ctx, padL, padT, padL + plotW, padT); 925 line(ctx, padL, padT + plotH, padL + plotW, padT + plotH); 926 line(ctx, padL, padT, padL, padT + plotH); 927 line(ctx, padL + plotW, padT, padL + plotW, padT + plotH); 928 929 // Zero line 930 const yZero = yOfV(0); 931 if (yZero >= padT && yZero <= padT + plotH) { 932 ctx.strokeStyle = theme.muted; 933 ctx.lineWidth = 1.2; 934 line(ctx, padL, yZero, padL + plotW, yZero); 935 } 936 937 // Y-axis ticks + labels 938 const vDec = vStep >= 1 ? 0 : vStep >= 0.1 ? 1 : 2; 939 ctx.textAlign = "right"; 940 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 941 for (let i = Math.ceil(vLo / vStep); ; i++) { 942 const v = i * vStep; 943 if (v > vHi + vStep * 0.01) break; 944 const yv = yOfV(v); 945 if (yv < padT - 1 || yv > padT + plotH + 1) continue; 946 ctx.strokeStyle = theme.muted; 947 ctx.lineWidth = 1; 948 line(ctx, padL - 4, yv, padL, yv); 949 ctx.fillStyle = theme.muted; 950 ctx.fillText(v.toFixed(vDec), padL - 7, yv + 4); 951 } 952 ctx.textAlign = "left"; 953 954 // X-axis ticks + labels 955 const tStep = niceStep(tMax, 6); 956 const tDec = tStep >= 1 ? 0 : 1; 957 ctx.textAlign = "center"; 958 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 959 for (let t = 0; t <= tMax + tStep * 0.01; t += tStep) { 960 const xt = xOfT(t); 961 if (xt < padL - 1 || xt > padL + plotW + 1) continue; 962 ctx.strokeStyle = theme.muted; 963 ctx.lineWidth = 1; 964 line(ctx, xt, padT + plotH, xt, padT + plotH + 4); 965 ctx.fillStyle = theme.muted; 966 ctx.fillText(t.toFixed(tDec), xt, padT + plotH + 16); 967 } 968 ctx.textAlign = "left"; 969 970 // X-axis label 971 ctx.fillStyle = theme.muted; 972 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 973 ctx.fillText("t / \u03C4d", padL + plotW + 6, padT + plotH + 4); 974 975 const tnClamped = Math.min(tn, tMax); 976 const smooth = riseTimeTr > 0 && riseShape !== "step"; 977 978 // Helper: draw a staircase trace from an event list 979 function drawStaircase(evts, color) { 980 ctx.strokeStyle = color; ctx.lineWidth = 2.2; 981 ctx.beginPath(); 982 let prevV = 0; 983 ctx.moveTo(xOfT(0), yOfV(0)); 984 for (const ev of evts) { 985 if (ev.t > tnClamped) break; 986 ctx.lineTo(xOfT(ev.t), yOfV(prevV)); 987 prevV += ev.dV; 988 ctx.lineTo(xOfT(ev.t), yOfV(prevV)); 989 } 990 ctx.lineTo(xOfT(tnClamped), yOfV(prevV)); 991 ctx.stroke(); 992 } 993 994 // VS trace (probe 1 — orange) 995 if (smooth) { 996 ctx.strokeStyle = theme.tdr; ctx.lineWidth = 2.2; 997 ctx.beginPath(); 998 for (let i = 0; i <= 500; i++) { 999 const t = (i / 500) * tnClamped; 1000 if (i === 0) ctx.moveTo(xOfT(t), yOfV(vsAtTime(t))); 1001 else ctx.lineTo(xOfT(t), yOfV(vsAtTime(t))); 1002 } 1003 ctx.stroke(); 1004 } else { 1005 drawStaircase(events, theme.tdr); 1006 } 1007 1008 // VL trace (probe 2 — blue) 1009 if (smooth) { 1010 ctx.strokeStyle = theme.probe2; ctx.lineWidth = 2.2; 1011 ctx.beginPath(); 1012 for (let i = 0; i <= 500; i++) { 1013 const t = (i / 500) * tnClamped; 1014 if (i === 0) ctx.moveTo(xOfT(t), yOfV(vlAtTime(t))); 1015 else ctx.lineTo(xOfT(t), yOfV(vlAtTime(t))); 1016 } 1017 ctx.stroke(); 1018 } else { 1019 drawStaircase(eventsL, theme.probe2); 1020 } 1021 1022 // Current-time cursor (vertical dashed line) 1023 if (tn > 0 && tn <= tMax) { 1024 ctx.strokeStyle = theme.warn; 1025 ctx.lineWidth = 1; 1026 ctx.setLineDash([4, 5]); 1027 line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH); 1028 ctx.setLineDash([]); 1029 } 1030 1031 // Current voltage readouts 1032 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 1033 ctx.textAlign = "right"; 1034 ctx.fillStyle = theme.tdr; 1035 ctx.fillText("VS = " + vsAtTime(tnClamped).toFixed(4) + " V", padL + plotW - 6, padT + 18); 1036 ctx.fillStyle = theme.probe2; 1037 ctx.fillText("VL = " + vlAtTime(tnClamped).toFixed(4) + " V", padL + plotW - 6, padT + 34); 1038 ctx.textAlign = "left"; 1039 } 1040 1041 // Resize the plot canvas height to fit the given number of panels. 1042 function ensurePlotCanvasHeight(plotEl, nPanels) { 1043 const targetH = Math.round( 1044 PLOT_PAD_T + PLOT_PAD_B + 1045 nPanels * PLOT_PANEL_H + 1046 (nPanels - 1) * PLOT_PANEL_GAP 1047 ); 1048 const curr = parseInt(plotEl.getAttribute("height") || "0", 10); 1049 if (curr !== targetH) plotEl.setAttribute("height", String(targetH)); 1050 } 1051 1052 return { 1053 PLOT_PANEL_H, PLOT_PANEL_GAP, PLOT_PAD_T, PLOT_PAD_B, 1054 getTheme, 1055 drawCircuit, 1056 drawPlot, 1057 drawPlotSim, 1058 drawProbe, 1059 drawProbeSim, 1060 ensurePlotCanvasHeight, 1061 }; 1062})();