From 9d81de447d21b2be45fd95ba397ca10e904a2280 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 22 Apr 2026 16:02:37 -0500 Subject: [PATCH] Add power-ladder scene to opening paragraph --- content/blog/moonbounce.md | 4 ++ static/js/moonbounce.js | 105 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/content/blog/moonbounce.md b/content/blog/moonbounce.md index 28b815d..b76bac3 100644 --- a/content/blog/moonbounce.md +++ b/content/blog/moonbounce.md @@ -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. +
+ +A kilowatt is 1000 watts. An attowatt is 10-18 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. diff --git a/static/js/moonbounce.js b/static/js/moonbounce.js index 8cd9438..8943380 100644 --- a/static/js/moonbounce.js +++ b/static/js/moonbounce.js @@ -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();