Shorten scale labels, add safe-area clamping, responsive title

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

View file

@ -137,21 +137,20 @@
function sceneScale() {
const s = scene('scene-scale', {
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.',
caption: 'Power out is on the right, power back is on the left. Every tick is ten times smaller than the one to its right. There are twenty-one of them. The glow is the signal on its round trip, shrinking the whole way.',
});
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.
// Fewer labels spaced every three decades so nothing collides on narrow viewports.
const marks = [
{ 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 },
{ p: 1e3, label: 'microwave', color: C.red, anchor: 'right' },
{ p: 1, label: 'handheld radio', color: C.orange, anchor: 'center' },
{ p: 1e-3, label: 'laser pointer', color: C.yellow, anchor: 'center' },
{ p: 1e-6, label: 'LED glow', color: C.green, anchor: 'center' },
{ p: 1e-9, label: 'Wi-Fi signal', color: C.cyan, anchor: 'center' },
{ p: 1e-12, label: 'faint radio', color: C.blue, anchor: 'center' },
{ p: 1e-15, label: 'weak DX', color: '#b199e4', anchor: 'center' },
{ p: 1e-18, label: 'Moon echo', color: C.magenta, anchor: 'left' },
];
const start = performance.now();
@ -160,13 +159,14 @@
const ctx = s.ctx;
clear(ctx, w, h);
// 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;
// Generous margins on all sides.
const m = { l: 70, r: 70, t: 80, b: 70 };
const pW = Math.max(120, w - m.l - m.r);
const barY = m.t + 20;
const barH = 14;
const xOf = logP => m.l + (logP + 18) / 21 * pW;
const clampX = x => Math.max(m.l + 2, Math.min(m.l + pW - 2, x));
// Gradient bar
const grad = ctx.createLinearGradient(m.l, 0, m.l + pW, 0);
@ -193,16 +193,15 @@
ctx.stroke();
}
// Labels. Alternate above/below and align endpoints inward so they never clip.
// Labels alternating above/below the bar. Endpoint anchors pull them inward.
marks.forEach((mk, i) => {
const x = xOf(Math.log10(mk.p));
const above = i % 2 === 0;
const y = above ? barY - 18 : barY + barH + 22;
// 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);
pill(ctx, mk.label, x, y, mk.color, mk.anchor);
});
// Animated pulse from right (microwave oven) to left (Moon echo)
// Animated pulse from right (microwave) to left (Moon echo)
const t = ((performance.now() - start) / 1000) % 7;
const pulseLogP = 3 - (t / 7) * 21;
const pulseX = xOf(pulseLogP);
@ -217,21 +216,29 @@
ctx.fillStyle = C.yellow;
ctx.beginPath(); ctx.arc(pulseX, pulseY, visualR, 0, Math.PI * 2); ctx.fill();
// How many times smaller right now: 10^(3 - logP) = 10^(21 * t/7)
// Readout under the pulse, clamped so it never runs off the canvas.
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`;
if (shrink < 10) shrinkStr = `${shrink.toFixed(0)}x smaller`;
else if (shrink < 1000) shrinkStr = `${shrink.toFixed(0)}x smaller`;
else {
const exp = Math.log10(shrink).toFixed(0);
const exp = Math.round(Math.log10(shrink));
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');
// safe-area clamp: keep the label at least half its assumed width away from each edge
ctx.save();
ctx.font = '12px system-ui, sans-serif';
const assumed = ctx.measureText(shrinkStr).width / 2 + 10;
ctx.restore();
const labelX = Math.max(m.l + assumed, Math.min(m.l + pW - assumed, pulseX));
pill(ctx, shrinkStr, labelX, pulseY + 44, C.yellow, 'center');
// 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);
// Title, sized to fit narrow viewports
const titleSize = w < 460 ? 12 : 14;
const subSize = w < 460 ? 10 : 12;
text(ctx, 'transmitter to Moon echo', w / 2, 24, C.fg, 'center', 'alphabetic', titleSize);
text(ctx, 'one billion trillion times weaker', w / 2, 24 + titleSize + 6, C.dim, 'center', 'alphabetic', subSize);
text(ctx, '(a 1 followed by 21 zeros)', w / 2, 24 + titleSize + subSize + 14, C.dim, 'center', 'alphabetic', subSize - 1);
requestAnimationFrame(draw);
}