Rework power-scale scene: relatable analogies, no clipping, visceral framing

This commit is contained in:
Graham McIntire 2026-04-22 16:04:53 -05:00
parent 9d81de447d
commit 41ea27993a
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -136,20 +136,22 @@
// ---------- 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.',
height: 260,
caption: 'Each tick is ten times smaller than the one to its right. From the transmitter on the far right down to the echo on the far left, the signal shrinks by a factor of one billion trillion. The moving pulse is the signal going on its round trip.',
});
if (!s) return;
// Powers of ten from 10^3 W down to 10^-18 W. Labels use relatable objects
// instead of SI prefixes so non-hams can follow.
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 },
{ p: 1e3, label: 'microwave oven', align: 'right', color: C.red },
{ p: 1, label: 'handheld radio', align: 'center', color: C.orange },
{ p: 1e-3, label: 'laser pointer', align: 'center', color: C.yellow },
{ p: 1e-6, label: 'faint LED indicator', align: 'center', color: C.green },
{ p: 1e-9, label: 'Wi-Fi at the router', align: 'center', color: C.cyan },
{ p: 1e-12, label: 'cell phone at edge of coverage', align: 'center', color: C.blue },
{ p: 1e-15, label: 'weak HF radio signal', align: 'center', color: '#b199e4' },
{ p: 1e-18, label: 'echo from the Moon', align: 'left', color: C.magenta },
];
const start = performance.now();
@ -158,12 +160,12 @@
const ctx = s.ctx;
clear(ctx, w, h);
const m = { l: 30, r: 30, t: 70, b: 70 };
// Generous margins so the endpoint labels can't clip
const m = { l: 60, r: 60, t: 80, 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
@ -179,7 +181,7 @@
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)
// Decade ticks
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);
@ -191,22 +193,21 @@
ctx.stroke();
}
// Labels — alternate above / below to avoid collisions
// Labels. Alternate above/below and align endpoints inward so they never clip.
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');
// Two-line label: the friendly name + the exact power in words on a second line
pill(ctx, mk.label, x, y, mk.color, mk.align);
});
// Animated pulse from kW (right) to aW (left) every 6 s
const t = ((performance.now() - start) / 1000) % 6;
const pulseLogP = 3 - (t / 6) * 21;
// Animated pulse from right (microwave oven) to left (Moon echo)
const t = ((performance.now() - start) / 1000) % 7;
const pulseLogP = 3 - (t / 7) * 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)');
@ -216,21 +217,21 @@
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');
// How many times smaller right now: 10^(3 - logP) = 10^(21 * t/7)
const shrink = Math.pow(10, 3 - pulseLogP);
let shrinkStr;
if (shrink < 10) shrinkStr = `${shrink.toFixed(1)}x smaller than the transmitter`;
else if (shrink < 1e6) shrinkStr = `${shrink.toExponential(0)}x smaller`;
else {
const exp = Math.log10(shrink).toFixed(0);
shrinkStr = `10^${exp}x smaller`;
}
pill(ctx, shrinkStr, Math.max(m.l + 50, Math.min(w - m.r - 50, 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);
// Title: visceral, not technical
text(ctx, 'From your transmitter to the echo off the Moon', w / 2, 22, C.fg, 'center', 'alphabetic', 14);
text(ctx, 'the signal gets one billion trillion times weaker', w / 2, 42, C.dim, 'center', 'alphabetic', 12);
text(ctx, '(that is a 1 followed by 21 zeros)', w / 2, 60, C.dim, 'center', 'alphabetic', 11);
requestAnimationFrame(draw);
}