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