New post cascaded-noise-figure (scheduled 2026-04-29) with four interactive scenes on noise floor, single-stage NF, the Friis formula as a block builder, and LNA placement. Upgrade the three static TCP scenes (data, sliding window, UDP) to animated packet flow using a shared drawPartialPacket primitive. Expand IDEAS.md with roughly thirty more post concepts across electronics, networking, ham radio, microwave, and longer arcs. Add outbound links to relevant first-mentions across posts: Fresnel zone, effective Earth radius, atmospheric duct, HRRR, BJT, common-emitter, TCP/UDP RFCs, slow start, QUIC, Friis, noise figure, Boltzmann constant. Scrub Unicode arrows and multiplication symbols out of prose and scene labels.
820 lines
31 KiB
JavaScript
820 lines
31 KiB
JavaScript
// TCP / UDP explainer. Vanilla JS + 2D canvas.
|
|
// Scoped to a single post via <link>/<script> in the markdown.
|
|
(() => {
|
|
'use strict';
|
|
|
|
const C = {
|
|
bg: '#1f232b',
|
|
panel: '#242932',
|
|
fg: '#d4d8df',
|
|
dim: '#5c6370',
|
|
grid: '#323844',
|
|
blue: '#61afef',
|
|
cyan: '#56b6c2',
|
|
green: '#98c379',
|
|
orange: '#d19a66',
|
|
red: '#e06c75',
|
|
yellow: '#e5c07b',
|
|
magenta: '#c678dd',
|
|
};
|
|
|
|
// ----- framework (same shape as transistor/nlos scenes) -----
|
|
function setupCanvas(canvas) {
|
|
const ctx = canvas.getContext('2d');
|
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
const fit = () => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = Math.round(rect.width * dpr);
|
|
canvas.height = Math.round(rect.height * dpr);
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
};
|
|
fit();
|
|
new ResizeObserver(fit).observe(canvas);
|
|
return { ctx, getSize: () => ({ w: canvas.clientWidth, h: canvas.clientHeight }) };
|
|
}
|
|
|
|
function scene(id, { height = 360, controls = [], readout = [], caption = '', threeCol = false } = {}) {
|
|
const root = document.getElementById(id);
|
|
if (!root) return null;
|
|
root.classList.add('scene-box');
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.style.width = '100%';
|
|
canvas.style.height = `${height}px`;
|
|
root.appendChild(canvas);
|
|
|
|
const readoutEl = document.createElement('div');
|
|
readoutEl.className = 'scene-readout';
|
|
const readoutSpans = {};
|
|
readout.forEach(r => {
|
|
const item = document.createElement('span');
|
|
item.className = 'scene-readout-item';
|
|
const label = document.createElement('span');
|
|
label.textContent = r.label + ':';
|
|
const val = document.createElement('span');
|
|
val.textContent = r.init ?? '';
|
|
item.appendChild(label); item.appendChild(val);
|
|
readoutEl.appendChild(item);
|
|
readoutSpans[r.key] = val;
|
|
});
|
|
if (readout.length) root.appendChild(readoutEl);
|
|
|
|
const controlsEl = document.createElement('div');
|
|
controlsEl.className = 'scene-controls' + (threeCol ? ' scene-controls--three' : '');
|
|
const values = {};
|
|
controls.forEach(c => {
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'scene-control';
|
|
const label = document.createElement('label');
|
|
const name = document.createElement('span'); name.textContent = c.label;
|
|
const valSpan = document.createElement('span'); valSpan.className = 'scene-value';
|
|
label.appendChild(name); label.appendChild(valSpan);
|
|
const input = document.createElement('input');
|
|
input.type = 'range';
|
|
input.min = c.min; input.max = c.max; input.step = c.step ?? 'any';
|
|
input.value = c.value;
|
|
values[c.key] = parseFloat(input.value);
|
|
const fmt = c.format || (v => v.toFixed(2));
|
|
valSpan.textContent = fmt(values[c.key]);
|
|
input.addEventListener('input', () => {
|
|
values[c.key] = parseFloat(input.value);
|
|
valSpan.textContent = fmt(values[c.key]);
|
|
});
|
|
wrap.appendChild(label); wrap.appendChild(input);
|
|
controlsEl.appendChild(wrap);
|
|
});
|
|
if (controls.length) root.appendChild(controlsEl);
|
|
|
|
if (caption) {
|
|
const cap = document.createElement('div');
|
|
cap.className = 'scene-caption';
|
|
cap.textContent = caption;
|
|
root.appendChild(cap);
|
|
}
|
|
|
|
const { ctx, getSize } = setupCanvas(canvas);
|
|
const setReadout = (key, text) => {
|
|
if (readoutSpans[key]) readoutSpans[key].textContent = text;
|
|
};
|
|
return { canvas, ctx, getSize, values, setReadout };
|
|
}
|
|
|
|
function clear(ctx, w, h) { ctx.clearRect(0, 0, w, h); }
|
|
function text(ctx, str, x, y, color = C.fg, align = 'left', baseline = 'alphabetic', size = 12) {
|
|
ctx.save();
|
|
ctx.fillStyle = color;
|
|
ctx.font = `${size}px system-ui, sans-serif`;
|
|
ctx.textAlign = align;
|
|
ctx.textBaseline = baseline;
|
|
ctx.fillText(str, x, y);
|
|
ctx.restore();
|
|
}
|
|
|
|
// ----- sequence-diagram primitives -----
|
|
|
|
// Draw a partial / completed packet. `frac` in 0..1 controls how far along
|
|
// the arrow has advanced from (fromX, yStart) toward (toX, yEnd). If the
|
|
// packet is lost, the arrow dashes out and an X appears where it dies.
|
|
function drawPartialPacket(ctx, fromX, toX, yStart, yEnd, frac, label, color, { lost = false, dropFrac = 0.5 } = {}) {
|
|
frac = Math.max(0, Math.min(1, frac));
|
|
if (frac <= 0) return;
|
|
ctx.save();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1.8;
|
|
if (lost && frac >= dropFrac) {
|
|
// Drew up to drop point then stop.
|
|
const mx = fromX + (toX - fromX) * dropFrac;
|
|
const my = yStart + (yEnd - yStart) * dropFrac;
|
|
ctx.setLineDash([4, 4]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromX, yStart); ctx.lineTo(mx, my);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.strokeStyle = C.red;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(mx - 6, my - 6); ctx.lineTo(mx + 6, my + 6);
|
|
ctx.moveTo(mx - 6, my + 6); ctx.lineTo(mx + 6, my - 6);
|
|
ctx.stroke();
|
|
} else {
|
|
const endX = fromX + (toX - fromX) * frac;
|
|
const endY = yStart + (yEnd - yStart) * frac;
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromX, yStart);
|
|
ctx.lineTo(endX, endY);
|
|
if (lost) ctx.setLineDash([4, 4]);
|
|
ctx.stroke();
|
|
if (lost) ctx.setLineDash([]);
|
|
// arrowhead on the advancing tip only when the packet is not lost yet
|
|
if (!lost) {
|
|
const dx = toX - fromX, dy = yEnd - yStart;
|
|
const L = Math.hypot(dx, dy);
|
|
const ux = dx / L, uy = dy / L;
|
|
const ax = endX - ux * 8;
|
|
const ay = endY - uy * 8;
|
|
const px = -uy, py = ux;
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(endX, endY);
|
|
ctx.lineTo(ax + px * 4, ay + py * 4);
|
|
ctx.lineTo(ax - px * 4, ay - py * 4);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
}
|
|
// Label at midpoint of the traveled line, fading in as the packet advances
|
|
const lx = (fromX + (toX - fromX) * frac * 0.5) + fromX * 0 + (frac === 1 ? (fromX + toX) / 2 - fromX : 0);
|
|
const midX = fromX + (toX - fromX) * frac / 2;
|
|
const midY = yStart + (yEnd - yStart) * frac / 2 - 4;
|
|
ctx.globalAlpha = Math.min(1, frac * 2);
|
|
text(ctx, label, midX, midY, color, 'center');
|
|
ctx.globalAlpha = 1;
|
|
ctx.restore();
|
|
}
|
|
|
|
// Back-compat: the original non-animated draw that's still used by the
|
|
// handshake scene's progress bar drawing and a couple of other places.
|
|
function drawPacket(ctx, fromX, toX, yStart, yEnd, label, color, lost = false, dropFrac = 0.5) {
|
|
ctx.save();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1.8;
|
|
if (lost) {
|
|
ctx.setLineDash([4, 4]);
|
|
const mx = fromX + (toX - fromX) * dropFrac;
|
|
const my = yStart + (yEnd - yStart) * dropFrac;
|
|
ctx.beginPath(); ctx.moveTo(fromX, yStart); ctx.lineTo(mx, my); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
// X at drop point
|
|
ctx.strokeStyle = C.red;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(mx - 6, my - 6); ctx.lineTo(mx + 6, my + 6);
|
|
ctx.moveTo(mx - 6, my + 6); ctx.lineTo(mx + 6, my - 6);
|
|
ctx.stroke();
|
|
} else {
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromX, yStart);
|
|
ctx.lineTo(toX, yEnd);
|
|
ctx.stroke();
|
|
// arrowhead
|
|
const dx = toX - fromX, dy = yEnd - yStart;
|
|
const L = Math.hypot(dx, dy);
|
|
const ux = dx / L, uy = dy / L;
|
|
const ax = toX - ux * 8;
|
|
const ay = yEnd - uy * 8;
|
|
const px = -uy, py = ux;
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(toX, yEnd);
|
|
ctx.lineTo(ax + px * 4, ay + py * 4);
|
|
ctx.lineTo(ax - px * 4, ay - py * 4);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
// label, place near the midpoint, slightly above the line
|
|
const lx = (fromX + toX) / 2;
|
|
const ly = (yStart + yEnd) / 2 - 4;
|
|
text(ctx, label, lx, ly, color, 'center');
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawTimelines(ctx, leftX, rightX, topY, botY, clientLabel = 'Client', serverLabel = 'Server') {
|
|
ctx.save();
|
|
ctx.strokeStyle = C.dim;
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([2, 4]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(leftX, topY); ctx.lineTo(leftX, botY);
|
|
ctx.moveTo(rightX, topY); ctx.lineTo(rightX, botY);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
// boxes at the top
|
|
ctx.fillStyle = C.panel;
|
|
ctx.strokeStyle = C.cyan;
|
|
ctx.fillRect(leftX - 40, topY - 30, 80, 24);
|
|
ctx.strokeRect(leftX - 40, topY - 30, 80, 24);
|
|
ctx.fillRect(rightX - 40, topY - 30, 80, 24);
|
|
ctx.strokeRect(rightX - 40, topY - 30, 80, 24);
|
|
text(ctx, clientLabel, leftX, topY - 14, C.fg, 'center');
|
|
text(ctx, serverLabel, rightX, topY - 14, C.fg, 'center');
|
|
// "time" arrow on the far left
|
|
ctx.strokeStyle = C.dim;
|
|
ctx.beginPath();
|
|
ctx.moveTo(20, topY); ctx.lineTo(20, botY - 8);
|
|
ctx.lineTo(16, botY - 14);
|
|
ctx.moveTo(20, botY - 8); ctx.lineTo(24, botY - 14);
|
|
ctx.stroke();
|
|
text(ctx, 'time', 26, botY - 4, C.dim);
|
|
ctx.restore();
|
|
}
|
|
|
|
// ---------- Scene 1: TCP three-way handshake ----------
|
|
function sceneHandshake() {
|
|
const s = scene('scene-handshake', {
|
|
height: 360,
|
|
readout: [
|
|
{ key: 'cseq', label: 'Client seq' },
|
|
{ key: 'sseq', label: 'Server seq' },
|
|
{ key: 'state', label: 'Connection state' },
|
|
],
|
|
caption: 'Every TCP connection opens with the same short ritual. The client announces a starting sequence number, the server acknowledges it and announces its own, and the client acknowledges that. After those three packets the connection is open. TCP wants you to have made eye contact before exchanging any data.',
|
|
});
|
|
if (!s) return;
|
|
|
|
const CLIENT_SEQ = 1000;
|
|
const SERVER_SEQ = 5000;
|
|
|
|
// Animated cycle schedule (seconds).
|
|
const CYCLE = 7.0;
|
|
const schedule = [
|
|
{ name: 'SYN', t0: 0.3, t1: 1.5, color: C.yellow, dir: 'right', label: `SYN seq=${CLIENT_SEQ}` },
|
|
{ name: 'SYN-ACK', t0: 2.0, t1: 3.2, color: C.orange, dir: 'left', label: `SYN-ACK seq=${SERVER_SEQ} ack=${CLIENT_SEQ + 1}` },
|
|
{ name: 'ACK', t0: 3.7, t1: 4.9, color: C.green, dir: 'right', label: `ACK seq=${CLIENT_SEQ + 1} ack=${SERVER_SEQ + 1}` },
|
|
];
|
|
const startWall = performance.now();
|
|
|
|
function drawPartial(ctx, fromX, toX, yStart, yEnd, frac, label, color) {
|
|
// Draw a forward-progress arrow whose head has advanced `frac` along the
|
|
// full line from (fromX, yStart) to (toX, yEnd). 1.0 = fully arrived.
|
|
frac = Math.max(0, Math.min(1, frac));
|
|
if (frac <= 0) return;
|
|
const endX = fromX + (toX - fromX) * frac;
|
|
const endY = yStart + (yEnd - yStart) * frac;
|
|
ctx.save();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1.8;
|
|
ctx.beginPath();
|
|
ctx.moveTo(fromX, yStart);
|
|
ctx.lineTo(endX, endY);
|
|
ctx.stroke();
|
|
// Arrowhead
|
|
const dx = toX - fromX, dy = yEnd - yStart;
|
|
const L = Math.hypot(dx, dy);
|
|
const ux = dx / L, uy = dy / L;
|
|
const ax = endX - ux * 8;
|
|
const ay = endY - uy * 8;
|
|
const px = -uy, py = ux;
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(endX, endY);
|
|
ctx.lineTo(ax + px * 4, ay + py * 4);
|
|
ctx.lineTo(ax - px * 4, ay - py * 4);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
// Label at midpoint of the traveled portion
|
|
const lx = (fromX + endX) / 2;
|
|
const ly = (yStart + endY) / 2 - 4;
|
|
text(ctx, label, lx, ly, color, 'center');
|
|
ctx.restore();
|
|
}
|
|
|
|
function draw() {
|
|
const { w, h } = s.getSize();
|
|
const ctx = s.ctx;
|
|
clear(ctx, w, h);
|
|
|
|
const topY = 60, botY = h - 40;
|
|
const leftX = 150, rightX = w - 60;
|
|
drawTimelines(ctx, leftX, rightX, topY, botY);
|
|
|
|
const slant = 24;
|
|
const rowH = (botY - topY - slant) / 3.5;
|
|
const y1a = topY + rowH * 0.2, y1b = y1a + slant;
|
|
const y2a = y1b + rowH * 0.5, y2b = y2a + slant;
|
|
const y3a = y2b + rowH * 0.5, y3b = y3a + slant;
|
|
const rowsY = [
|
|
{ a: y1a, b: y1b },
|
|
{ a: y2a, b: y2b },
|
|
{ a: y3a, b: y3b },
|
|
];
|
|
|
|
const now = ((performance.now() - startWall) / 1000) % CYCLE;
|
|
|
|
schedule.forEach((pkt, i) => {
|
|
const { a, b } = rowsY[i];
|
|
const fromX = pkt.dir === 'right' ? leftX : rightX;
|
|
const toX = pkt.dir === 'right' ? rightX : leftX;
|
|
const frac = (now - pkt.t0) / (pkt.t1 - pkt.t0);
|
|
if (frac <= 0) return;
|
|
drawPartial(ctx, fromX, toX, a, b, frac, pkt.label, pkt.color);
|
|
});
|
|
|
|
// Progressive state labels on the client timeline. Place them in the
|
|
// vertical gaps between the three packet rows so they never collide with
|
|
// each other or with the packet arrows.
|
|
const gutterRight = leftX - 10;
|
|
const state =
|
|
now < schedule[0].t0 ? 'CLOSED' :
|
|
now < schedule[1].t1 ? 'SYN-SENT' :
|
|
'ESTABLISHED';
|
|
const yClosed = topY + 10;
|
|
const ySynSent = (y1b + y2a) / 2;
|
|
const yEstablished = (y2b + y3a) / 2;
|
|
const yOpen = y3b + 22;
|
|
text(ctx, 'CLOSED', gutterRight, yClosed, state === 'CLOSED' ? C.fg : C.dim, 'right', 'middle');
|
|
text(ctx, 'SYN-SENT', gutterRight, ySynSent, state === 'SYN-SENT' ? C.yellow : C.dim, 'right', 'middle');
|
|
text(ctx, 'ESTABLISHED', gutterRight, yEstablished, state === 'ESTABLISHED' ? C.green : C.dim, 'right', 'middle');
|
|
if (state === 'ESTABLISHED') {
|
|
text(ctx, 'connection open', gutterRight, yOpen, C.green, 'right', 'middle');
|
|
}
|
|
|
|
// Time progress bar along the very top, subtle.
|
|
const barY = 18, barX0 = 20, barX1 = w - 20;
|
|
ctx.strokeStyle = C.grid;
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(barX0, barY); ctx.lineTo(barX1, barY); ctx.stroke();
|
|
ctx.fillStyle = C.cyan;
|
|
ctx.beginPath();
|
|
ctx.arc(barX0 + (barX1 - barX0) * (now / CYCLE), barY, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
s.setReadout('cseq', `${CLIENT_SEQ + 1}`);
|
|
s.setReadout('sseq', `${SERVER_SEQ + 1}`);
|
|
s.setReadout('state', state);
|
|
}
|
|
|
|
function loop() { draw(); requestAnimationFrame(loop); }
|
|
loop();
|
|
}
|
|
|
|
// ---------- Scene 2: Data with sequence numbers, ACKs, and loss ----------
|
|
function sceneData() {
|
|
const s = scene('scene-data', {
|
|
height: 460,
|
|
controls: [
|
|
{ key: 'loss', label: 'Packet loss', min: 0, max: 4, value: 1, step: 1,
|
|
format: v => ['none', '1 packet', '2 packets', '3 packets', 'burst'][Math.floor(v)] },
|
|
],
|
|
readout: [
|
|
{ key: 'sent', label: 'Segments sent' },
|
|
{ key: 'retx', label: 'Retransmissions' },
|
|
],
|
|
caption: 'Once the connection is open, each byte has a sequence number. The receiver acknowledges the next byte it expects, not the one it just got. If a segment goes missing, the receiver keeps asking for the same byte (duplicate ACKs) and the sender, eventually noticing, hands it over again. This is TCP being stubborn about delivery.',
|
|
});
|
|
if (!s) return;
|
|
|
|
// Scenario data: sequence of sends and the receiver response.
|
|
const scenarios = [
|
|
// 0: none lost
|
|
[
|
|
{ kind: 'data', seq: 1, lost: false },
|
|
{ kind: 'data', seq: 2, lost: false },
|
|
{ kind: 'data', seq: 3, lost: false },
|
|
{ kind: 'data', seq: 4, lost: false },
|
|
],
|
|
// 1: one lost (seq 2)
|
|
[
|
|
{ kind: 'data', seq: 1, lost: false },
|
|
{ kind: 'data', seq: 2, lost: true },
|
|
{ kind: 'data', seq: 3, lost: false },
|
|
{ kind: 'data', seq: 4, lost: false },
|
|
{ kind: 'retx', seq: 2, lost: false },
|
|
],
|
|
// 2: two lost (seq 2 and 4)
|
|
[
|
|
{ kind: 'data', seq: 1, lost: false },
|
|
{ kind: 'data', seq: 2, lost: true },
|
|
{ kind: 'data', seq: 3, lost: false },
|
|
{ kind: 'data', seq: 4, lost: true },
|
|
{ kind: 'retx', seq: 2, lost: false },
|
|
{ kind: 'retx', seq: 4, lost: false },
|
|
],
|
|
// 3: three lost
|
|
[
|
|
{ kind: 'data', seq: 1, lost: true },
|
|
{ kind: 'data', seq: 2, lost: true },
|
|
{ kind: 'data', seq: 3, lost: false },
|
|
{ kind: 'retx', seq: 1, lost: false },
|
|
{ kind: 'retx', seq: 2, lost: false },
|
|
],
|
|
// 4: burst loss
|
|
[
|
|
{ kind: 'data', seq: 1, lost: false },
|
|
{ kind: 'data', seq: 2, lost: true },
|
|
{ kind: 'data', seq: 3, lost: true },
|
|
{ kind: 'data', seq: 4, lost: true },
|
|
{ kind: 'retx', seq: 2, lost: false },
|
|
{ kind: 'retx', seq: 3, lost: false },
|
|
{ kind: 'retx', seq: 4, lost: false },
|
|
],
|
|
];
|
|
|
|
// Timing parameters for the animation.
|
|
const PACKET_DUR = 1.0; // seconds per packet in flight
|
|
const GAP = 0.25; // seconds of pause between each send/ack step
|
|
const PAUSE_END = 2.0; // hold time after the cycle completes before looping
|
|
const startWall = performance.now();
|
|
let lastScenario = -1;
|
|
// Precomputed rows (each row = one send or one ack) per scenario.
|
|
const scenarioRows = scenarios.map(scenario => {
|
|
const rows = [];
|
|
let nextExpected = 1;
|
|
scenario.forEach(pkt => {
|
|
rows.push({ type: 'send', pkt });
|
|
if (!pkt.lost) {
|
|
if (pkt.seq === nextExpected) nextExpected++;
|
|
rows.push({ type: 'ack', expect: nextExpected });
|
|
} else {
|
|
rows.push({ type: 'ack', expect: nextExpected, dup: true });
|
|
}
|
|
});
|
|
// attach t0 / t1 to each row sequentially
|
|
let t = 0;
|
|
rows.forEach(r => {
|
|
r.t0 = t;
|
|
r.t1 = t + PACKET_DUR;
|
|
t = r.t1 + GAP;
|
|
});
|
|
return { rows, total: t };
|
|
});
|
|
|
|
function draw() {
|
|
const { w, h } = s.getSize();
|
|
const ctx = s.ctx;
|
|
clear(ctx, w, h);
|
|
|
|
const topY = 50, botY = h - 30;
|
|
const leftX = 90, rightX = w - 90;
|
|
drawTimelines(ctx, leftX, rightX, topY, botY);
|
|
|
|
const scenarioIdx = Math.floor(s.values.loss);
|
|
const { rows, total } = scenarioRows[scenarioIdx];
|
|
const cycleDur = total + PAUSE_END;
|
|
|
|
// restart the animation clock when scenario changes so users always
|
|
// see the full sequence when they flip the slider.
|
|
if (scenarioIdx !== lastScenario) lastScenario = scenarioIdx;
|
|
|
|
const now = ((performance.now() - startWall) / 1000) % cycleDur;
|
|
|
|
const slant = 22;
|
|
const rowH = Math.min(34, (botY - topY - slant - 10) / rows.length);
|
|
|
|
let sent = 0, retx = 0;
|
|
rows.forEach((r, i) => {
|
|
const y = topY + 10 + i * rowH;
|
|
const frac = Math.max(0, Math.min(1, (now - r.t0) / (r.t1 - r.t0)));
|
|
if (r.type === 'send') {
|
|
const color = r.pkt.kind === 'retx' ? C.orange : C.cyan;
|
|
const label = r.pkt.kind === 'retx' ? `retx seq=${r.pkt.seq}` : `DATA seq=${r.pkt.seq}`;
|
|
drawPartialPacket(ctx, leftX, rightX, y, y + slant, frac, label, color, { lost: r.pkt.lost, dropFrac: 0.5 });
|
|
if (frac > 0 && r.pkt.kind === 'retx') retx++;
|
|
if (frac > 0) sent++;
|
|
} else {
|
|
const label = r.dup ? `ACK ack=${r.expect} (dup)` : `ACK ack=${r.expect}`;
|
|
const color = r.dup ? C.red : C.green;
|
|
drawPartialPacket(ctx, rightX, leftX, y, y + slant, frac, label, color);
|
|
}
|
|
});
|
|
|
|
// Time progress bar at the top
|
|
const barY = 18, barX0 = 20, barX1 = w - 20;
|
|
ctx.strokeStyle = C.grid;
|
|
ctx.beginPath(); ctx.moveTo(barX0, barY); ctx.lineTo(barX1, barY); ctx.stroke();
|
|
ctx.fillStyle = C.cyan;
|
|
ctx.beginPath();
|
|
ctx.arc(barX0 + (barX1 - barX0) * (now / cycleDur), barY, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
s.setReadout('sent', sent);
|
|
s.setReadout('retx', retx);
|
|
}
|
|
function loop() { draw(); requestAnimationFrame(loop); }
|
|
loop();
|
|
}
|
|
|
|
// ---------- Scene 3: Sliding window ----------
|
|
function sceneWindow() {
|
|
const s = scene('scene-window', {
|
|
height: 520,
|
|
controls: [
|
|
{ key: 'win', label: 'Window size (packets)', min: 1, max: 8, value: 4, step: 1, format: v => `${v}` },
|
|
],
|
|
readout: [
|
|
{ key: 'rtts', label: 'RTTs to send 8 packets' },
|
|
{ key: 'throughput', label: 'Throughput vs stop-and-wait' },
|
|
],
|
|
caption: 'Sending one packet and then waiting for its ACK wastes nearly the entire round-trip time. TCP instead keeps a pile of packets in flight at once, up to the window size, and only waits when that pile is full. Doubling the window roughly doubles the throughput, up to the point where something else in the network becomes the bottleneck.',
|
|
});
|
|
if (!s) return;
|
|
|
|
// Animation timing: in a single RTT all `win` packets go out in quick
|
|
// succession, they arrive at the server after half an RTT, the acks come
|
|
// back during the second half, and then the next batch goes.
|
|
const RTT_DUR = 2.2; // seconds per round trip
|
|
const INTRA_STAGGER = 0.08;// stagger between packets within a batch
|
|
const PAUSE_END = 1.6;
|
|
const startWall = performance.now();
|
|
|
|
function draw() {
|
|
const { w, h } = s.getSize();
|
|
const ctx = s.ctx;
|
|
clear(ctx, w, h);
|
|
|
|
const topY = 50, botY = h - 30;
|
|
const leftX = 90, rightX = w - 90;
|
|
drawTimelines(ctx, leftX, rightX, topY, botY);
|
|
|
|
const win = Math.floor(s.values.win);
|
|
const totalPackets = 8;
|
|
const rtts = Math.ceil(totalPackets / win);
|
|
|
|
const slant = 20;
|
|
const avail = botY - topY - 20;
|
|
const rowsCount = totalPackets * 2;
|
|
const gapCount = rtts - 1;
|
|
const unit = avail / (rowsCount + gapCount * 0.7);
|
|
const rowStep = Math.max(slant + 8, Math.min(32, unit));
|
|
const rttGap = Math.min(rowStep * 0.7, 24);
|
|
|
|
const cycleDur = rtts * RTT_DUR + PAUSE_END;
|
|
const now = ((performance.now() - startWall) / 1000) % cycleDur;
|
|
|
|
let yCursor = topY + 12;
|
|
let packetN = 1;
|
|
for (let batch = 0; batch < rtts; batch++) {
|
|
const batchSize = Math.min(win, totalPackets - batch * win);
|
|
const batchStart = batch * RTT_DUR;
|
|
|
|
// Send phase: 0 -> RTT/2 of this batch, staggered
|
|
for (let i = 0; i < batchSize; i++) {
|
|
const sendT0 = batchStart + i * INTRA_STAGGER;
|
|
const sendT1 = sendT0 + RTT_DUR / 2;
|
|
const frac = Math.max(0, Math.min(1, (now - sendT0) / (sendT1 - sendT0)));
|
|
drawPartialPacket(ctx, leftX, rightX, yCursor, yCursor + slant, frac, `seq=${packetN}`, C.cyan);
|
|
packetN++;
|
|
yCursor += rowStep;
|
|
}
|
|
// ACK phase: RTT/2 -> RTT of this batch, staggered
|
|
for (let i = 0; i < batchSize; i++) {
|
|
const ackT0 = batchStart + RTT_DUR / 2 + i * INTRA_STAGGER;
|
|
const ackT1 = ackT0 + RTT_DUR / 2;
|
|
const frac = Math.max(0, Math.min(1, (now - ackT0) / (ackT1 - ackT0)));
|
|
const ackNum = batch * win + i + 2;
|
|
drawPartialPacket(ctx, rightX, leftX, yCursor, yCursor + slant, frac, `ack=${ackNum}`, C.green);
|
|
yCursor += rowStep;
|
|
}
|
|
if (batch < rtts - 1) yCursor += rttGap;
|
|
}
|
|
|
|
// Time progress bar
|
|
const barY = 18, barX0 = 20, barX1 = w - 20;
|
|
ctx.strokeStyle = C.grid;
|
|
ctx.beginPath(); ctx.moveTo(barX0, barY); ctx.lineTo(barX1, barY); ctx.stroke();
|
|
ctx.fillStyle = C.cyan;
|
|
ctx.beginPath();
|
|
ctx.arc(barX0 + (barX1 - barX0) * (now / cycleDur), barY, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
s.setReadout('rtts', `${rtts}`);
|
|
const speedup = Math.ceil(totalPackets / rtts);
|
|
s.setReadout('throughput', `${speedup}x faster`);
|
|
}
|
|
function loop() { draw(); requestAnimationFrame(loop); }
|
|
loop();
|
|
}
|
|
|
|
// ---------- Scene 4: Congestion control ----------
|
|
function sceneCongestion() {
|
|
const s = scene('scene-congestion', {
|
|
height: 380,
|
|
controls: [
|
|
{ key: 'lossAt', label: 'First loss at time', min: 3, max: 20, value: 8, step: 0.5, format: v => `RTT ${v.toFixed(1)}` },
|
|
{ key: 'ssthresh0', label: 'Initial ssthresh', min: 8, max: 64, value: 32, step: 1, format: v => `${v}` },
|
|
],
|
|
readout: [
|
|
{ key: 'peak', label: 'Peak cwnd' },
|
|
{ key: 'final', label: 'Final cwnd' },
|
|
],
|
|
caption: 'TCP has no idea how much bandwidth is available. It finds out by sending more and more until something breaks. Slow start ramps the congestion window up exponentially until it hits the slow-start threshold, then tiptoes up by one packet per RTT. A packet loss halves the window and the whole thing starts over. This is a protocol that lives its entire life anxious about whether it should be going faster.',
|
|
});
|
|
if (!s) return;
|
|
|
|
function draw() {
|
|
const { w, h } = s.getSize();
|
|
const ctx = s.ctx;
|
|
clear(ctx, w, h);
|
|
|
|
const margin = { l: 50, r: 20, t: 25, b: 40 };
|
|
const pW = w - margin.l - margin.r;
|
|
const pH = h - margin.t - margin.b;
|
|
|
|
const tMax = 40; // RTTs
|
|
const cwndMax = 80;
|
|
const xOf = t => margin.l + (t / tMax) * pW;
|
|
const yOf = c => margin.t + (1 - c / cwndMax) * pH;
|
|
|
|
ctx.strokeStyle = C.grid;
|
|
ctx.strokeRect(margin.l, margin.t, pW, pH);
|
|
for (let t = 0; t <= tMax; t += 5) {
|
|
ctx.beginPath(); ctx.moveTo(xOf(t), margin.t); ctx.lineTo(xOf(t), margin.t + pH); ctx.stroke();
|
|
text(ctx, `${t}`, xOf(t), margin.t + pH + 14, C.dim, 'center');
|
|
}
|
|
for (let c = 0; c <= cwndMax; c += 20) {
|
|
ctx.beginPath(); ctx.moveTo(margin.l, yOf(c)); ctx.lineTo(margin.l + pW, yOf(c)); ctx.stroke();
|
|
text(ctx, `${c}`, margin.l - 5, yOf(c), C.dim, 'right', 'middle');
|
|
}
|
|
text(ctx, 'time (RTTs)', margin.l + pW / 2, margin.t + pH + 28, C.dim, 'center');
|
|
ctx.save();
|
|
ctx.translate(margin.l - 40, margin.t + pH / 2);
|
|
ctx.rotate(-Math.PI / 2);
|
|
text(ctx, 'cwnd (segments)', 0, 0, C.dim, 'center');
|
|
ctx.restore();
|
|
|
|
// Run the classic Reno state machine
|
|
let cwnd = 1;
|
|
let ssthresh = s.values.ssthresh0;
|
|
const firstLoss = s.values.lossAt;
|
|
const lossInterval = 12; // RTTs between simulated losses
|
|
const points = [{ t: 0, cwnd, phase: 'slow' }];
|
|
let nextLoss = firstLoss;
|
|
let peak = cwnd;
|
|
|
|
for (let t = 1; t <= tMax; t += 1) {
|
|
if (t >= nextLoss) {
|
|
// Multiplicative decrease: ssthresh = cwnd / 2, cwnd resumes at ssthresh (fast recovery)
|
|
ssthresh = Math.max(2, Math.floor(cwnd / 2));
|
|
cwnd = ssthresh;
|
|
nextLoss += lossInterval;
|
|
points.push({ t, cwnd, phase: 'loss' });
|
|
continue;
|
|
}
|
|
if (cwnd < ssthresh) {
|
|
cwnd = cwnd * 2; // slow start: exponential
|
|
} else {
|
|
cwnd = cwnd + 1; // congestion avoidance: linear
|
|
}
|
|
if (cwnd > cwndMax) cwnd = cwndMax;
|
|
peak = Math.max(peak, cwnd);
|
|
points.push({ t, cwnd, phase: cwnd < ssthresh ? 'slow' : 'avoid' });
|
|
}
|
|
|
|
// Plot cwnd
|
|
ctx.strokeStyle = C.cyan;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
points.forEach((p, i) => {
|
|
const x = xOf(p.t), y = yOf(p.cwnd);
|
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
});
|
|
ctx.stroke();
|
|
|
|
// Mark losses
|
|
points.filter(p => p.phase === 'loss').forEach(p => {
|
|
ctx.fillStyle = C.red;
|
|
ctx.beginPath();
|
|
ctx.arc(xOf(p.t), yOf(p.cwnd), 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
text(ctx, 'loss', xOf(p.t) + 6, yOf(p.cwnd) - 4, C.red);
|
|
});
|
|
|
|
// Phase legend
|
|
text(ctx, 'slow start -> congestion avoidance -> loss -> repeat', margin.l + 6, margin.t + 14, C.dim);
|
|
|
|
s.setReadout('peak', `${peak.toFixed(0)} segments`);
|
|
s.setReadout('final', `${points[points.length - 1].cwnd.toFixed(0)} segments`);
|
|
|
|
requestAnimationFrame(() => {});
|
|
}
|
|
function loop() { draw(); requestAnimationFrame(loop); }
|
|
loop();
|
|
}
|
|
|
|
// ---------- Scene 5: UDP: send and forget ----------
|
|
function sceneUDP() {
|
|
const s = scene('scene-udp', {
|
|
height: 400,
|
|
controls: [
|
|
{ key: 'loss', label: 'Network loss rate', min: 0, max: 60, value: 15, step: 1, format: v => v.toFixed(0) + '%' },
|
|
],
|
|
readout: [
|
|
{ key: 'sent', label: 'Sent' },
|
|
{ key: 'rcvd', label: 'Received' },
|
|
{ key: 'recovery', label: 'Recovery' },
|
|
],
|
|
caption: 'UDP lives in the same IP packets that TCP does but deliberately refuses almost all of TCP\'s help. There is no handshake, no sequence tracking, no retransmission, no flow control, no congestion control. Packets either arrive or they don\'t, and if they don\'t, UDP would like you to deal with it. This sounds bad until you need to send voice, video, DNS queries, or game state, at which point it sounds wonderful.',
|
|
});
|
|
if (!s) return;
|
|
|
|
// Deterministic pseudo-random so the scene is reproducible.
|
|
function hash(seed, n) {
|
|
let x = Math.sin(seed * 9301 + n * 49297) * 233280;
|
|
return x - Math.floor(x);
|
|
}
|
|
|
|
const PACKET_DUR = 0.8;
|
|
const GAP = 0.15;
|
|
const PAUSE_END = 2.2;
|
|
const startWall = performance.now();
|
|
|
|
function draw() {
|
|
const { w, h } = s.getSize();
|
|
const ctx = s.ctx;
|
|
clear(ctx, w, h);
|
|
|
|
const topY = 50, botY = h - 30;
|
|
const leftX = 90, rightX = w - 90;
|
|
drawTimelines(ctx, leftX, rightX, topY, botY, 'Client', 'Server');
|
|
|
|
const totalPackets = 10;
|
|
const lossPct = s.values.loss / 100;
|
|
const slant = 18;
|
|
const rowH = Math.min(28, (botY - topY - slant - 10) / totalPackets);
|
|
|
|
const cycleDur = totalPackets * (PACKET_DUR + GAP) + PAUSE_END;
|
|
const now = ((performance.now() - startWall) / 1000) % cycleDur;
|
|
|
|
let received = 0;
|
|
for (let i = 0; i < totalPackets; i++) {
|
|
const y = topY + 10 + i * rowH;
|
|
const lost = hash(1, i) < lossPct;
|
|
const dropFrac = 0.5 + (hash(2, i) - 0.5) * 0.3;
|
|
const t0 = i * (PACKET_DUR + GAP);
|
|
const t1 = t0 + PACKET_DUR;
|
|
const frac = Math.max(0, Math.min(1, (now - t0) / (t1 - t0)));
|
|
drawPartialPacket(ctx, leftX, rightX, y, y + slant, frac, `UDP seq=${i + 1}`, C.magenta, { lost, dropFrac });
|
|
// count received only once the packet has completed traveling
|
|
if (!lost && frac >= 1) received++;
|
|
}
|
|
|
|
// Time progress bar
|
|
const barY = 18, barX0 = 20, barX1 = w - 20;
|
|
ctx.strokeStyle = C.grid;
|
|
ctx.beginPath(); ctx.moveTo(barX0, barY); ctx.lineTo(barX1, barY); ctx.stroke();
|
|
ctx.fillStyle = C.magenta;
|
|
ctx.beginPath();
|
|
ctx.arc(barX0 + (barX1 - barX0) * (now / cycleDur), barY, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// totals — show once the full cycle has played
|
|
const cycleFraction = now / cycleDur;
|
|
const totalCycled = cycleFraction > 0.95 ? totalPackets : Math.min(totalPackets, Math.ceil(now / (PACKET_DUR + GAP)));
|
|
let arrived = 0;
|
|
for (let i = 0; i < totalCycled; i++) {
|
|
const lost = hash(1, i) < lossPct;
|
|
if (!lost) arrived++;
|
|
}
|
|
s.setReadout('sent', `${totalCycled}`);
|
|
s.setReadout('rcvd', `${arrived}`);
|
|
s.setReadout('recovery', arrived === totalCycled ? 'none needed' : 'none (UDP does not care)');
|
|
}
|
|
function loop() { draw(); requestAnimationFrame(loop); }
|
|
loop();
|
|
}
|
|
|
|
function boot() {
|
|
const host = document.querySelector('.scene-host, [data-scene-host]');
|
|
if (host) host.classList.add('scene-host');
|
|
sceneHandshake();
|
|
sceneData();
|
|
sceneWindow();
|
|
sceneCongestion();
|
|
sceneUDP();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
})();
|