Add power-ladder scene to opening paragraph

This commit is contained in:
Graham McIntire 2026-04-22 16:02:37 -05:00
parent 83cdeb5fce
commit 9d81de447d
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 109 additions and 0 deletions

View file

@ -10,6 +10,10 @@ draft = true
Take an antenna. Point it at the Moon. Transmit. A microscopic fraction of what you sent makes it across 384,000 km of empty space, smacks into the pocked gray surface, scatters off in every direction because the Moon is a deeply unimpressive mirror, and a microscopic fraction of that comes back. A few kilowatts leave; a few attowatts return. On the other side of the planet, someone pointed at the same Moon hears it.
<div id="scene-scale"></div>
A kilowatt is 1000 watts. An attowatt is 10<sup>-18</sup> watts. That's twenty-one decades between what you put in and what eventually comes out the other end, which is roughly the difference between the Sun's output and a single LED in somebody's living room.
This is moonbounce. As a method of having a radio contact it is absurd in a way that very little else in the hobby manages. You can sit a non-ham at a reasonable HF station and explain how a signal gets from Texas to Argentina, and if they squint they'll accept it. Try to explain moonbounce with a straight face and you look slightly unwell.
It does, annoyingly, just work. You need respectable antennas, respectable power, and a computer willing to spend about a minute per transmission sifting what came back from what didn't.

View file

@ -133,6 +133,110 @@
ctx.restore();
}
// ---------- Scene 0: kW to aW power ladder ----------
function sceneScale() {
const s = scene('scene-scale', {
height: 230,
caption: 'A logarithmic ladder from 1 kW on the right to 1 aW on the left. Each tick is a factor of ten. There are twenty-one of them, and the pulse that starts out at the transmitter has to fall all the way to the far end before anyone can decode it.',
});
if (!s) return;
const marks = [
{ p: 1e3, label: '1 kW — your transmitter', color: C.red },
{ p: 1, label: '1 W — a handheld HT', color: C.orange },
{ p: 1e-3, label: '1 mW — an indicator LED', color: C.yellow },
{ p: 1e-6, label: '1 μW', color: C.green },
{ p: 1e-9, label: '1 nW — strong Wi-Fi at the receiver', color: C.cyan },
{ p: 1e-12, label: '1 pW', color: C.blue },
{ p: 1e-15, label: '1 fW — weak HF DX at the antenna', color: '#b199e4' },
{ p: 1e-18, label: '1 aW — what comes back from the Moon', color: C.magenta },
];
const start = performance.now();
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const m = { l: 30, r: 30, t: 70, b: 70 };
const pW = w - m.l - m.r;
const barY = m.t + 20;
const barH = 14;
// log scale: logP from -18 to +3
const xOf = logP => m.l + (logP + 18) / 21 * pW;
// Gradient bar
const grad = ctx.createLinearGradient(m.l, 0, m.l + pW, 0);
grad.addColorStop(0, 'rgba(199, 141, 224, 0.22)');
grad.addColorStop(0.5, 'rgba(109, 197, 211, 0.30)');
grad.addColorStop(1, 'rgba(229, 128, 137, 0.55)');
ctx.fillStyle = grad;
if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(m.l, barY, pW, barH, 4); ctx.fill(); }
else ctx.fillRect(m.l, barY, pW, barH);
ctx.strokeStyle = C.grid;
ctx.lineWidth = 1;
if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(m.l, barY, pW, barH, 4); ctx.stroke(); }
else ctx.strokeRect(m.l, barY, pW, barH);
// Minor ticks at every decade (22 positions from -18..+3)
for (let d = -18; d <= 3; d++) {
const x = xOf(d);
const important = marks.some(mk => Math.abs(Math.log10(mk.p) - d) < 0.01);
ctx.strokeStyle = important ? C.fg : C.dim;
ctx.lineWidth = important ? 1.5 : 0.75;
ctx.beginPath();
ctx.moveTo(x, barY - (important ? 10 : 5));
ctx.lineTo(x, barY + barH + (important ? 10 : 5));
ctx.stroke();
}
// Labels — alternate above / below to avoid collisions
marks.forEach((mk, i) => {
const x = xOf(Math.log10(mk.p));
const above = i % 2 === 0;
const y = above ? barY - 18 : barY + barH + 22;
pill(ctx, mk.label, x, y, mk.color, 'center');
});
// Animated pulse from kW (right) to aW (left) every 6 s
const t = ((performance.now() - start) / 1000) % 6;
const pulseLogP = 3 - (t / 6) * 21;
const pulseX = xOf(pulseLogP);
const pulseY = barY + barH / 2;
// Visual radius shrinks *slightly* (log-visible), not exponentially or it becomes invisible
const visualR = 3 + 12 * (pulseLogP + 18) / 21;
// Glow
const glow = ctx.createRadialGradient(pulseX, pulseY, 0, pulseX, pulseY, visualR * 3);
glow.addColorStop(0, 'rgba(226, 195, 125, 0.9)');
glow.addColorStop(0.5, 'rgba(226, 195, 125, 0.2)');
glow.addColorStop(1, 'rgba(226, 195, 125, 0)');
ctx.fillStyle = glow;
ctx.beginPath(); ctx.arc(pulseX, pulseY, visualR * 3, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = C.yellow;
ctx.beginPath(); ctx.arc(pulseX, pulseY, visualR, 0, Math.PI * 2); ctx.fill();
// Current-power readout riding the pulse
const currentW = Math.pow(10, pulseLogP);
let currentStr;
if (currentW >= 1) currentStr = `${currentW.toFixed(0)} W`;
else if (currentW >= 1e-3) currentStr = `${(currentW * 1e3).toFixed(1)} mW`;
else if (currentW >= 1e-6) currentStr = `${(currentW * 1e6).toFixed(1)} μW`;
else if (currentW >= 1e-9) currentStr = `${(currentW * 1e9).toFixed(1)} nW`;
else if (currentW >= 1e-12) currentStr = `${(currentW * 1e12).toFixed(1)} pW`;
else if (currentW >= 1e-15) currentStr = `${(currentW * 1e15).toFixed(1)} fW`;
else currentStr = `${(currentW * 1e18).toFixed(1)} aW`;
pill(ctx, currentStr, pulseX, pulseY + 42, C.yellow, 'center');
// Title
text(ctx, 'power out vs power back: 21 orders of magnitude', w / 2, 22, C.fg, 'center', 'alphabetic', 13);
text(ctx, '(+30 dBW on one end, 180 dBW on the other)', w / 2, 40, C.dim, 'center', 'alphabetic', 11);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
// ---------- Scene 1: Earth-Moon geometry ----------
function sceneGeometry() {
const s = scene('scene-geometry', {
@ -1155,6 +1259,7 @@
// Init after DOM
function init() {
sceneScale();
sceneGeometry();
sceneDistance();
scenePathloss();