this repo has no description
0
fork

Configure Feed

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

at canon 676 lines 24 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 }; 34 } 35 36 // ---- primitive helpers ---- 37 function line(ctx, x0, y0, x1, y1) { 38 ctx.beginPath(); 39 ctx.moveTo(x0, y0); 40 ctx.lineTo(x1, y1); 41 ctx.stroke(); 42 } 43 44 function circle(ctx, x, y, r, strokeStyle) { 45 ctx.strokeStyle = strokeStyle; 46 ctx.lineWidth = 2; 47 ctx.beginPath(); 48 ctx.arc(x, y, r, 0, Math.PI * 2); 49 ctx.stroke(); 50 } 51 52 function circleFill(ctx, x, y, r, fillStyle) { 53 ctx.fillStyle = fillStyle; 54 ctx.beginPath(); 55 ctx.arc(x, y, r, 0, Math.PI * 2); 56 ctx.fill(); 57 } 58 59 function label(ctx, text, x, y, color) { 60 ctx.fillStyle = color; 61 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 62 ctx.fillText(text, x, y); 63 } 64 65 // Return a "nice" tick step for a given range and target number of ticks. 66 function niceStep(range, targetCount) { 67 const raw = range / targetCount; 68 const mag = Math.pow(10, Math.floor(Math.log10(raw))); 69 const norm = raw / mag; 70 const nice = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10; 71 return nice * mag; 72 } 73 74 // ---- circuit component primitives ---- 75 function drawResistor(ctx, x0, y0, x1, y1, zigZagCount, amp, theme) { 76 ctx.strokeStyle = theme.ink; 77 ctx.lineWidth = 2; 78 const dx = x1 - x0, dy = y1 - y0; 79 const len = Math.hypot(dx, dy); 80 if (len < 1e-6) return; 81 const ux = dx / len, uy = dy / len; 82 const px = -uy, py = ux; 83 84 const lead = 10; 85 const start = { x: x0 + ux * lead, y: y0 + uy * lead }; 86 const end = { x: x1 - ux * lead, y: y1 - uy * lead }; 87 88 line(ctx, x0, y0, start.x, start.y); 89 line(ctx, end.x, end.y, x1, y1); 90 91 const segs = zigZagCount * 2 - 1; 92 const segLen = (len - 2 * lead) / segs; 93 ctx.beginPath(); 94 ctx.moveTo(start.x, start.y); 95 for (let i = 1; i < segs; i++) { 96 const s = i * segLen; 97 const flip = (i % 2 === 0) ? -1 : 1; 98 ctx.lineTo(start.x + ux * s + px * amp * flip, 99 start.y + uy * s + py * amp * flip); 100 } 101 ctx.lineTo(end.x, end.y); 102 ctx.stroke(); 103 } 104 105 function drawSwitch(ctx, x, y, tn, theme) { 106 const closed = tn > 0; 107 ctx.strokeStyle = theme.ink; 108 ctx.lineWidth = 2; 109 const a = { x: x - 18, y }; 110 const b = { x: x + 18, y }; 111 circleFill(ctx, a.x, a.y, 3.2, theme.ink); 112 circleFill(ctx, b.x, b.y, 3.2, theme.ink); 113 if (closed) { 114 line(ctx, a.x, a.y, b.x, b.y); 115 } else { 116 line(ctx, a.x + 1, a.y - 1, b.x - 2, b.y - 14); 117 } 118 label(ctx, "t = 0", x - 18, y + 26, theme.muted); 119 } 120 121 function drawVoltageProbe(ctx, x, yTop, yBot, name, theme) { 122 ctx.fillStyle = theme.muted; 123 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 124 ctx.fillText("+", x + 6, yTop + 12); 125 ctx.fillText("\u2212", x + 6, yBot - 4); 126 label(ctx, name, x + 5, (yTop + yBot) / 2 - 6, theme.muted); 127 } 128 129 // ---- circuit canvas ---- 130 // segments = model.segments = [{Z0}, …] (N entries, equal-length) 131 // RL = model.RL — pass Infinity for open circuit 132 function drawCircuit(ctx, w, h, tn, dyn, theme, segments, RL) { 133 const rlIsOpen = !isFinite(RL); 134 const rlIsShort = RL === 0; 135 ctx.clearRect(0, 0, w, h); 136 ctx.fillStyle = theme.panel; 137 ctx.fillRect(0, 0, w, h); 138 139 const pad = 18; 140 const yTop = 70; 141 const yBot = 190; 142 143 const xSourceL = pad + 60; 144 const xSwitch = pad + 170; 145 const xTL0 = pad + 240; 146 const xTL1 = w - pad - 210; 147 const xLoad = w - pad - 160; 148 const xRight = w - pad - 90; 149 150 ctx.lineWidth = 2; 151 ctx.strokeStyle = theme.ink; 152 153 // Top and bottom wires — always run to xRight regardless of termination 154 line(ctx, xSwitch + 18, yTop, xTL0, yTop); 155 line(ctx, xTL1, yTop, xRight, yTop); 156 line(ctx, xSourceL, yBot, xTL0, yBot); 157 line(ctx, xTL1, yBot, xRight, yBot); 158 159 // Voltage source 160 const vsx = xSourceL, vsy = (yTop + yBot) / 2; 161 line(ctx, xSourceL, yTop, xSourceL, vsy - 20); 162 line(ctx, xSourceL, vsy + 20, xSourceL, yBot); 163 circle(ctx, vsx, vsy, 20, theme.ink); 164 ctx.fillStyle = theme.ink; 165 ctx.font = "14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 166 ctx.fillText("+", vsx - 4, vsy - 6); 167 ctx.fillText("\u2212", vsx - 4, vsy + 14); 168 label(ctx, "Vg", vsx - 38, vsy - 20, theme.muted); 169 170 // Source resistor Rg 171 const r0 = xSourceL + 20, r1 = xSwitch - 30; 172 line(ctx, xSourceL, yTop, r0, yTop); 173 drawResistor(ctx, r0, yTop, r1, yTop, 5, 8, theme); 174 line(ctx, r1, yTop, xSwitch - 18, yTop); 175 label(ctx, "Rg", (r0 + r1) / 2 - 10, yTop - 22, theme.muted); 176 177 drawSwitch(ctx, xSwitch, yTop, tn, theme); 178 179 // Transmission line boxes — N abutting pairs (top + bottom conductor) 180 const N = segments.length; 181 const tlW = xTL1 - xTL0; 182 const segPxW = tlW / N; 183 ctx.lineWidth = 2; 184 ctx.strokeStyle = theme.ink; 185 186 ctx.font = "13px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 187 for (let i = 0; i < N; i++) { 188 const bx = xTL0 + i * segPxW; 189 ctx.strokeRect(bx, yTop - 16, segPxW, 32); 190 ctx.strokeRect(bx, yBot - 16, segPxW, 32); 191 192 // Label above top box: "Z₀" for single segment, "50Ω" etc. for multi 193 const lbl = N === 1 194 ? "Z\u2080" 195 : `${segments[i].Z0}\u03A9`; 196 const lblX = bx + segPxW / 2 - ctx.measureText(lbl).width / 2; 197 label(ctx, lbl, lblX, yTop - 22, theme.muted); 198 } 199 200 // Load — resistor, short circuit wire, or open-circuit terminals 201 const rlTop = yTop + 22, rlBot = yBot - 22; 202 ctx.strokeStyle = theme.ink; 203 ctx.lineWidth = 2; 204 if (rlIsOpen) { 205 circleFill(ctx, xRight, yTop, 4.5, theme.ink); 206 circleFill(ctx, xRight, yBot, 4.5, theme.ink); 207 } else if (rlIsShort) { 208 ctx.lineWidth = 3; 209 line(ctx, xLoad, yTop, xLoad, yBot); 210 ctx.lineWidth = 2; 211 } else { 212 line(ctx, xLoad, yTop, xLoad, rlTop); 213 drawResistor(ctx, xLoad, rlTop, xLoad, rlBot, 5, 8, theme); 214 line(ctx, xLoad, rlBot, xLoad, yBot); 215 label(ctx, "RL", xLoad + 10, (yTop + yBot) / 2, theme.muted); 216 } 217 218 // Voltage probes 219 drawVoltageProbe(ctx, xSwitch + 12, yTop, yBot, "VS", theme); 220 drawVoltageProbe(ctx, xLoad + 55, yTop, yBot, "VL", theme); 221 222 // Terminal dots at the VL port — top and bottom 223 if (!rlIsOpen) { 224 circleFill(ctx, xRight, yTop, 3.5, theme.ink); 225 circleFill(ctx, xRight, yBot, 3.5, theme.ink); 226 } 227 228 // TDR detection point — green dot on top wire between switch and T-line input 229 const xDetect = Math.round((xSwitch + 18 + xTL0) / 2); 230 circleFill(ctx, xDetect, yTop, 5.5, theme.tdr); 231 ctx.fillStyle = theme.tdr; 232 ctx.font = "11px ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial"; 233 ctx.textAlign = "left"; 234 235 // Wavefront markers (dashed vertical lines inside T-line boxes) 236 const wfY0 = yTop - 16; 237 const wfY1 = yBot + 16; 238 for (const wf of dyn.activeWaves) { 239 const xw = xTL0 + wf.front * tlW; 240 ctx.strokeStyle = (wf.dir > 0) ? theme.accent : theme.accent2; 241 ctx.lineWidth = 3; 242 ctx.setLineDash([9, 7]); 243 line(ctx, xw, wfY0, xw, wfY1); 244 ctx.setLineDash([]); 245 } 246 247 label(ctx, "z = 0", xTL0 - 20, yBot + 34, theme.muted); 248 label(ctx, "z = \u2113", xTL1 - 18, yBot + 34, theme.muted); 249 } 250 251 // ---- wave-shape helpers ---- 252 253 // Voltage profile of a single wave packet as piecewise-constant segments over [0,1]. 254 // The packet only contributes amplitude in its own z-range [zStart, zEnd]. 255 function segmentsForWave(wf) { 256 const { dir, zStart, A, front } = wf; 257 let segs; 258 if (dir > 0) { 259 // Nonzero in [zStart, front]; zero elsewhere. 260 segs = [ 261 { a: 0, b: zStart, V: 0 }, 262 { a: zStart, b: front, V: A }, 263 { a: front, b: 1, V: 0 }, 264 ]; 265 } else { 266 // Leftward: zStart is the right edge; nonzero in [front, zStart]. 267 segs = [ 268 { a: 0, b: front, V: 0 }, 269 { a: front, b: zStart, V: A }, 270 { a: zStart, b: 1, V: 0 }, 271 ]; 272 } 273 const valid = segs.filter(s => s.b > s.a + 1e-9); 274 return valid.length ? valid : [{ a: 0, b: 1, V: 0 }]; 275 } 276 277 // Sum of all launched waves as piecewise-constant segments over [0,1]. 278 // Breakpoints include every wave's front, zStart, and zEnd so that the 279 // midpoint probe correctly captures all z-range transitions. 280 function totalSegmentsForWaves(waves) { 281 const breakpoints = [0, 1]; 282 for (const wf of waves) { 283 breakpoints.push(wf.front); 284 breakpoints.push(wf.zStart); 285 breakpoints.push(wf.zEnd); 286 } 287 breakpoints.sort((a, b) => a - b); 288 289 const pts = []; 290 for (const x of breakpoints) { 291 if (!pts.length || Math.abs(x - pts[pts.length - 1]) > 1e-6) pts.push(x); 292 } 293 294 const segs = []; 295 for (let i = 0; i < pts.length - 1; i++) { 296 const a = pts[i], b = pts[i + 1]; 297 const m = (a + b) / 2; 298 let V = 0; 299 for (const wf of waves) { 300 if (wf.dir > 0 && m >= wf.zStart - 1e-9 && m <= wf.front + 1e-9) V += wf.A; 301 if (wf.dir < 0 && m >= wf.front - 1e-9 && m <= wf.zStart + 1e-9) V += wf.A; 302 } 303 segs.push({ a, b, V }); 304 } 305 return segs.length ? segs : [{ a: 0, b: 1, V: 0 }]; 306 } 307 308 // Draw a piecewise-constant voltage profile. 309 function drawPiecewise(ctx, xOfZ, yOfV, segments, color, width) { 310 ctx.strokeStyle = color; 311 ctx.lineWidth = width; 312 ctx.beginPath(); 313 ctx.moveTo(xOfZ(segments[0].a), yOfV(segments[0].V)); 314 for (const seg of segments) { 315 ctx.lineTo(xOfZ(seg.a), yOfV(seg.V)); 316 ctx.lineTo(xOfZ(seg.b), yOfV(seg.V)); 317 } 318 ctx.stroke(); 319 } 320 321 // Draw a continuous voltage profile by sampling vFn(z) at N evenly-spaced z values. 322 function drawSampledWave(ctx, xOfZ, yOfV, vFn, color, width, N = 400) { 323 ctx.strokeStyle = color; 324 ctx.lineWidth = width; 325 ctx.beginPath(); 326 for (let i = 0; i <= N; i++) { 327 const z = i / N; 328 if (i === 0) ctx.moveTo(xOfZ(z), yOfV(vFn(z))); 329 else ctx.lineTo(xOfZ(z), yOfV(vFn(z))); 330 } 331 ctx.stroke(); 332 } 333 334 // ---- plot canvas ---- 335 // segments = model.segments (used to draw boundary tick marks) 336 function drawPlot(ctx, w, h, tn, dyn, theme, riseTimeTr = 0, riseShape = "step", segments = [{ Z0: 50 }]) { 337 ctx.clearRect(0, 0, w, h); 338 ctx.fillStyle = theme.panel; 339 ctx.fillRect(0, 0, w, h); 340 341 const circuitPad = 18; 342 const xPlot0 = circuitPad + 240; 343 const xPlot1 = w - circuitPad - 210; 344 const plotW = xPlot1 - xPlot0; 345 const xOfZ = (z) => xPlot0 + z * plotW; 346 347 const launched = [...dyn.launchedWaves].sort((a, b) => a.n - b.n); 348 const sumSegs = totalSegmentsForWaves(launched); 349 350 const maxAbsWave = Math.max(1e-6, ...launched.map((wf) => Math.abs(wf.A))); 351 const maxAbsSum = Math.max(1e-6, sumSegs.reduce((m, s) => Math.max(m, Math.abs(s.V)), 0)); 352 const vScale = Math.max(maxAbsWave, maxAbsSum); 353 const vLo = -1.15 * vScale; 354 const vHi = 1.15 * vScale; 355 356 ctx.fillStyle = theme.muted; 357 ctx.font = "12px ui-sans-serif, system-ui"; 358 ctx.fillText("Voltage along the T-line", 12, 14); 359 360 // Draw axes/border for one panel, with y-axis tick marks and labels. 361 function drawPanelFrame(top, bot, yOfV, labelText, vLo, vHi) { 362 const step = niceStep(vHi - vLo, 5); 363 const decimals = step >= 1 ? 0 : step >= 0.1 ? 1 : 2; 364 // Index of first tick at or above vLo 365 const iTick0 = Math.ceil(vLo / step); 366 367 // Horizontal grid lines at each tick (drawn first, behind everything) 368 ctx.strokeStyle = theme.grid; 369 ctx.lineWidth = 1; 370 for (let i = iTick0; ; i++) { 371 const v = i * step; 372 if (v > vHi + step * 0.01) break; 373 const yv = yOfV(v); 374 if (yv < top - 1 || yv > bot + 1) continue; 375 line(ctx, xPlot0, yv, xPlot1, yv); 376 } 377 378 // Border (on top of grid lines) 379 ctx.strokeStyle = theme.grid; 380 ctx.lineWidth = 1; 381 line(ctx, xPlot0, top, xPlot1, top); 382 line(ctx, xPlot0, bot, xPlot1, bot); 383 line(ctx, xPlot0, top, xPlot0, bot); 384 line(ctx, xPlot1, top, xPlot1, bot); 385 386 // Zero line — slightly more visible than other grid lines 387 const yZero = yOfV(0); 388 if (yZero >= top - 1 && yZero <= bot + 1) { 389 ctx.strokeStyle = theme.muted; 390 ctx.lineWidth = 1.2; 391 line(ctx, xPlot0, yZero, xPlot1, yZero); 392 } 393 394 // Tick marks and labels on the left y-axis 395 ctx.textAlign = "right"; 396 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 397 for (let i = iTick0; ; i++) { 398 const v = i * step; 399 if (v > vHi + step * 0.01) break; 400 const yv = yOfV(v); 401 if (yv < top - 1 || yv > bot + 1) continue; 402 ctx.strokeStyle = theme.muted; 403 ctx.lineWidth = 1; 404 line(ctx, xPlot0 - 4, yv, xPlot0, yv); 405 ctx.fillStyle = theme.muted; 406 ctx.fillText(v.toFixed(decimals), xPlot0 - 7, yv + 4); 407 } 408 ctx.textAlign = "left"; 409 410 // Panel title and axis labels 411 ctx.fillStyle = theme.muted; 412 ctx.font = "12px ui-sans-serif, system-ui"; 413 ctx.fillText(labelText, xPlot0 + 8, top + 14); 414 ctx.fillText("z", xPlot1 + 6, (top + bot) / 2 + 4); 415 416 // Segment boundary tick marks (for multi-segment lines) 417 const N = segments.length; 418 if (N > 1) { 419 ctx.strokeStyle = theme.grid; 420 ctx.lineWidth = 1; 421 ctx.setLineDash([3, 4]); 422 for (let i = 1; i < N; i++) { 423 const xb = xOfZ(i / N); 424 line(ctx, xb, top, xb, bot); 425 } 426 ctx.setLineDash([]); 427 } 428 } 429 430 // Dashed wave-front position markers 431 function drawFrontMarkers(top, bot, fronts) { 432 if (!fronts.length) return; 433 ctx.strokeStyle = theme.warn; 434 ctx.lineWidth = 1.2; 435 ctx.setLineDash([4, 5]); 436 for (const zf of fronts) line(ctx, xOfZ(zf), top, xOfZ(zf), bot); 437 ctx.setLineDash([]); 438 } 439 440 const panelH = PLOT_PANEL_H; 441 const { waveVoltageAt, totalVoltageAt } = TLPhysics; 442 const smooth = riseTimeTr > 0 && riseShape !== "step"; 443 444 // Panel 1 — sum of all waves 445 const top0 = PLOT_PAD_T; 446 const bot0 = top0 + panelH; 447 const y0 = (V) => top0 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 448 drawPanelFrame(top0, bot0, y0, "Sum (all waves)", vLo, vHi); 449 if (smooth) { 450 drawSampledWave(ctx, xOfZ, y0, (z) => totalVoltageAt(z, launched, riseTimeTr, riseShape, segments.length), theme.ok, 2.4); 451 } else { 452 drawPiecewise(ctx, xOfZ, y0, sumSegs, theme.ok, 2.4); 453 } 454 drawFrontMarkers(top0, bot0, dyn.activeWaves.map((wf) => wf.front)); 455 456 // Panel 2 — individual component waves 457 const top1 = PLOT_PAD_T + panelH + PLOT_PANEL_GAP; 458 const bot1 = top1 + panelH; 459 const y1 = (V) => top1 + 4 + (vHi - V) / (vHi - vLo) * (panelH - 8); 460 drawPanelFrame(top1, bot1, y1, "Components (all waves)", vLo, vHi); 461 462 const waveStyles = [ 463 { color: theme.accent, dash: [] }, 464 { color: theme.accent2, dash: [] }, 465 { color: theme.accent, dash: [9, 6] }, 466 { color: theme.accent2, dash: [9, 6] }, 467 { color: theme.accent, dash: [2, 5] }, 468 { color: theme.accent2, dash: [2, 5] }, 469 { color: theme.accent, dash: [16, 5] }, 470 { color: theme.accent2, dash: [16, 5] }, 471 { color: theme.accent, dash: [8, 4, 2, 4] }, 472 { color: theme.accent2, dash: [8, 4, 2, 4] }, 473 ]; 474 475 const FADE_DURATION = 4; 476 const FADE_MIN = 0.08; 477 478 for (let i = 0; i < launched.length; i++) { 479 const wf = launched[i]; 480 const style = waveStyles[i % waveStyles.length]; 481 // Fade after the wave front has reached its endpoint (tDie). 482 const alpha = TLUtils.clamp(1 - (tn - wf.tDie) / FADE_DURATION, FADE_MIN, 1); 483 ctx.globalAlpha = alpha; 484 ctx.setLineDash(style.dash); 485 if (smooth) { 486 drawSampledWave(ctx, xOfZ, y1, (z) => waveVoltageAt(wf, z, riseTimeTr, riseShape), style.color, 2.0); 487 } else { 488 drawPiecewise(ctx, xOfZ, y1, segmentsForWave(wf), style.color, 2.0); 489 } 490 ctx.setLineDash([]); 491 ctx.globalAlpha = 1; 492 } 493 drawFrontMarkers(top1, bot1, launched.filter((wf) => wf.u < Math.abs(wf.zEnd - wf.zStart)).map((wf) => wf.front)); 494 495 // z-axis endpoint labels 496 ctx.fillStyle = theme.muted; 497 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 498 ctx.fillText("0", xPlot0 - 4, h - 6); 499 ctx.fillText("\u2113", xPlot1 - 4, h - 6); 500 } 501 502 // ---- TDR canvas ---- 503 // Draws VS(t) — voltage at the detection point (z=0, between Rg and T-line) versus time. 504 // The trace grows up to the current tNorm; a cursor line shows current time. 505 function drawTDR(ctx, w, h, tn, bounce, riseTimeTr, riseShape, timeHorizon, theme) { 506 ctx.clearRect(0, 0, w, h); 507 ctx.fillStyle = theme.panel; 508 ctx.fillRect(0, 0, w, h); 509 510 const padL = 55, padR = 30, padT = 20, padB = 30; 511 const plotW = w - padL - padR; 512 const plotH = h - padT - padB; 513 514 const tMax = timeHorizon; 515 const xOfT = (t) => padL + (t / tMax) * plotW; 516 517 const { sumEventsAtTime, sumEventsWithRise, sumEventsWithLinearRamp } = TLPhysics; 518 const vsAtTime = (t) => { 519 if (riseTimeTr <= 0 || riseShape === "step") return sumEventsAtTime(bounce.srcEvents, t); 520 if (riseShape === "linear") return sumEventsWithLinearRamp(bounce.srcEvents, t, riseTimeTr); 521 return sumEventsWithRise(bounce.srcEvents, t, riseTimeTr); 522 }; 523 524 // y range from all srcEvents (cumulative sum) 525 const events = [...bounce.srcEvents].sort((a, b) => a.t - b.t); 526 let cumV = 0, vMax = 0, vMin = 0; 527 for (const ev of events) { 528 cumV += ev.dV; 529 if (cumV > vMax) vMax = cumV; 530 if (cumV < vMin) vMin = cumV; 531 } 532 vMax = Math.max(vMax, 0.01); 533 vMin = Math.min(vMin, 0); 534 const span = Math.max(vMax - vMin, 0.05); 535 const vHi = vMax + 0.2 * span; 536 const vLo = vMin - 0.15 * span; 537 const yOfV = (v) => padT + (vHi - v) / (vHi - vLo) * plotH; 538 539 // Title 540 ctx.fillStyle = theme.muted; 541 ctx.font = "12px ui-sans-serif, system-ui"; 542 ctx.fillText("TDR \u2014 V at detection point vs time", padL + 8, padT + 14); 543 544 // Grid lines 545 const vStep = niceStep(vHi - vLo, 4); 546 ctx.strokeStyle = theme.grid; 547 ctx.lineWidth = 1; 548 for (let i = Math.ceil(vLo / vStep); ; i++) { 549 const v = i * vStep; 550 if (v > vHi + vStep * 0.01) break; 551 const yv = yOfV(v); 552 if (yv < padT - 1 || yv > padT + plotH + 1) continue; 553 line(ctx, padL, yv, padL + plotW, yv); 554 } 555 556 // Border 557 ctx.strokeStyle = theme.grid; 558 ctx.lineWidth = 1; 559 line(ctx, padL, padT, padL + plotW, padT); 560 line(ctx, padL, padT + plotH, padL + plotW, padT + plotH); 561 line(ctx, padL, padT, padL, padT + plotH); 562 line(ctx, padL + plotW, padT, padL + plotW, padT + plotH); 563 564 // Zero line 565 const yZero = yOfV(0); 566 if (yZero >= padT && yZero <= padT + plotH) { 567 ctx.strokeStyle = theme.muted; 568 ctx.lineWidth = 1.2; 569 line(ctx, padL, yZero, padL + plotW, yZero); 570 } 571 572 // Y-axis ticks + labels 573 const vDec = vStep >= 1 ? 0 : vStep >= 0.1 ? 1 : 2; 574 ctx.textAlign = "right"; 575 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 576 for (let i = Math.ceil(vLo / vStep); ; i++) { 577 const v = i * vStep; 578 if (v > vHi + vStep * 0.01) break; 579 const yv = yOfV(v); 580 if (yv < padT - 1 || yv > padT + plotH + 1) continue; 581 ctx.strokeStyle = theme.muted; 582 ctx.lineWidth = 1; 583 line(ctx, padL - 4, yv, padL, yv); 584 ctx.fillStyle = theme.muted; 585 ctx.fillText(v.toFixed(vDec), padL - 7, yv + 4); 586 } 587 ctx.textAlign = "left"; 588 589 // X-axis ticks + labels 590 const tStep = niceStep(tMax, 6); 591 const tDec = tStep >= 1 ? 0 : 1; 592 ctx.textAlign = "center"; 593 ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 594 for (let t = 0; t <= tMax + tStep * 0.01; t += tStep) { 595 const xt = xOfT(t); 596 if (xt < padL - 1 || xt > padL + plotW + 1) continue; 597 ctx.strokeStyle = theme.muted; 598 ctx.lineWidth = 1; 599 line(ctx, xt, padT + plotH, xt, padT + plotH + 4); 600 ctx.fillStyle = theme.muted; 601 ctx.fillText(t.toFixed(tDec), xt, padT + plotH + 16); 602 } 603 ctx.textAlign = "left"; 604 605 // X-axis label 606 ctx.fillStyle = theme.muted; 607 ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 608 ctx.fillText("t / \u03C4d", padL + plotW + 6, padT + plotH + 4); 609 610 // VS(t) trace up to tn 611 const tnClamped = Math.min(tn, tMax); 612 const smooth = riseTimeTr > 0 && riseShape !== "step"; 613 ctx.strokeStyle = theme.tdr; 614 ctx.lineWidth = 2.2; 615 ctx.beginPath(); 616 617 if (smooth) { 618 const SAMPLES = 500; 619 for (let i = 0; i <= SAMPLES; i++) { 620 const t = (i / SAMPLES) * tnClamped; 621 const v = vsAtTime(t); 622 if (i === 0) ctx.moveTo(xOfT(t), yOfV(v)); 623 else ctx.lineTo(xOfT(t), yOfV(v)); 624 } 625 } else { 626 // Staircase: horizontal segment then vertical jump at each srcEvent 627 let prevV = 0; 628 ctx.moveTo(xOfT(0), yOfV(0)); 629 for (const ev of events) { 630 if (ev.t > tnClamped) break; 631 ctx.lineTo(xOfT(ev.t), yOfV(prevV)); 632 prevV += ev.dV; 633 ctx.lineTo(xOfT(ev.t), yOfV(prevV)); 634 } 635 ctx.lineTo(xOfT(tnClamped), yOfV(prevV)); 636 } 637 ctx.stroke(); 638 639 // Current-time cursor (vertical dashed line) 640 if (tn > 0 && tn <= tMax) { 641 ctx.strokeStyle = theme.warn; 642 ctx.lineWidth = 1; 643 ctx.setLineDash([4, 5]); 644 line(ctx, xOfT(tn), padT, xOfT(tn), padT + plotH); 645 ctx.setLineDash([]); 646 } 647 648 // Current voltage readout 649 const vNow = vsAtTime(tnClamped); 650 ctx.fillStyle = theme.tdr; 651 ctx.font = "13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; 652 ctx.textAlign = "right"; 653 ctx.fillText("V(det) = " + vNow.toFixed(4) + " V", padL + plotW - 6, padT + 18); 654 ctx.textAlign = "left"; 655 } 656 657 // Resize the plot canvas height to fit the given number of panels. 658 function ensurePlotCanvasHeight(plotEl, nPanels) { 659 const targetH = Math.round( 660 PLOT_PAD_T + PLOT_PAD_B + 661 nPanels * PLOT_PANEL_H + 662 (nPanels - 1) * PLOT_PANEL_GAP 663 ); 664 const curr = parseInt(plotEl.getAttribute("height") || "0", 10); 665 if (curr !== targetH) plotEl.setAttribute("height", String(targetH)); 666 } 667 668 return { 669 PLOT_PANEL_H, PLOT_PANEL_GAP, PLOT_PAD_T, PLOT_PAD_B, 670 getTheme, 671 drawCircuit, 672 drawPlot, 673 drawTDR, 674 ensurePlotCanvasHeight, 675 }; 676})();