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() { function sceneScale() {
const s = scene('scene-scale', { const s = scene('scene-scale', {
height: 260, 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; if (!s) return;
// Powers of ten from 10^3 W down to 10^-18 W. Labels use relatable objects // Fewer labels spaced every three decades so nothing collides on narrow viewports.
// instead of SI prefixes so non-hams can follow.
const marks = [ const marks = [
{ p: 1e3, label: 'microwave oven', align: 'right', color: C.red }, { p: 1e3, label: 'microwave', color: C.red, anchor: 'right' },
{ p: 1, label: 'handheld radio', align: 'center', color: C.orange }, { p: 1, label: 'handheld radio', color: C.orange, anchor: 'center' },
{ p: 1e-3, label: 'laser pointer', align: 'center', color: C.yellow }, { p: 1e-3, label: 'laser pointer', color: C.yellow, anchor: 'center' },
{ p: 1e-6, label: 'faint LED indicator', align: 'center', color: C.green }, { p: 1e-6, label: 'LED glow', color: C.green, anchor: 'center' },
{ p: 1e-9, label: 'Wi-Fi at the router', align: 'center', color: C.cyan }, { p: 1e-9, label: 'Wi-Fi signal', color: C.cyan, anchor: 'center' },
{ p: 1e-12, label: 'cell phone at edge of coverage', align: 'center', color: C.blue }, { p: 1e-12, label: 'faint radio', color: C.blue, anchor: 'center' },
{ p: 1e-15, label: 'weak HF radio signal', align: 'center', color: '#b199e4' }, { p: 1e-15, label: 'weak DX', color: '#b199e4', anchor: 'center' },
{ p: 1e-18, label: 'echo from the Moon', align: 'left', color: C.magenta }, { p: 1e-18, label: 'Moon echo', color: C.magenta, anchor: 'left' },
]; ];
const start = performance.now(); const start = performance.now();
@ -160,13 +159,14 @@
const ctx = s.ctx; const ctx = s.ctx;
clear(ctx, w, h); clear(ctx, w, h);
// Generous margins so the endpoint labels can't clip // Generous margins on all sides.
const m = { l: 60, r: 60, t: 80, b: 70 }; const m = { l: 70, r: 70, t: 80, b: 70 };
const pW = w - m.l - m.r; const pW = Math.max(120, w - m.l - m.r);
const barY = m.t + 20; const barY = m.t + 20;
const barH = 14; const barH = 14;
const xOf = logP => m.l + (logP + 18) / 21 * pW; 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 // Gradient bar
const grad = ctx.createLinearGradient(m.l, 0, m.l + pW, 0); const grad = ctx.createLinearGradient(m.l, 0, m.l + pW, 0);
@ -193,16 +193,15 @@
ctx.stroke(); 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) => { marks.forEach((mk, i) => {
const x = xOf(Math.log10(mk.p)); const x = xOf(Math.log10(mk.p));
const above = i % 2 === 0; const above = i % 2 === 0;
const y = above ? barY - 18 : barY + barH + 22; 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.anchor);
pill(ctx, mk.label, x, y, mk.color, mk.align);
}); });
// 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 t = ((performance.now() - start) / 1000) % 7;
const pulseLogP = 3 - (t / 7) * 21; const pulseLogP = 3 - (t / 7) * 21;
const pulseX = xOf(pulseLogP); const pulseX = xOf(pulseLogP);
@ -217,21 +216,29 @@
ctx.fillStyle = C.yellow; ctx.fillStyle = C.yellow;
ctx.beginPath(); ctx.arc(pulseX, pulseY, visualR, 0, Math.PI * 2); ctx.fill(); 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); const shrink = Math.pow(10, 3 - pulseLogP);
let shrinkStr; let shrinkStr;
if (shrink < 10) shrinkStr = `${shrink.toFixed(1)}x smaller than the transmitter`; if (shrink < 10) shrinkStr = `${shrink.toFixed(0)}x smaller`;
else if (shrink < 1e6) shrinkStr = `${shrink.toExponential(0)}x smaller`; else if (shrink < 1000) shrinkStr = `${shrink.toFixed(0)}x smaller`;
else { else {
const exp = Math.log10(shrink).toFixed(0); const exp = Math.round(Math.log10(shrink));
shrinkStr = `10^${exp}x smaller`; 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 // Title, sized to fit narrow viewports
text(ctx, 'From your transmitter to the echo off the Moon', w / 2, 22, C.fg, 'center', 'alphabetic', 14); const titleSize = w < 460 ? 12 : 14;
text(ctx, 'the signal gets one billion trillion times weaker', w / 2, 42, C.dim, 'center', 'alphabetic', 12); const subSize = w < 460 ? 10 : 12;
text(ctx, '(that is a 1 followed by 21 zeros)', w / 2, 60, C.dim, 'center', 'alphabetic', 11); 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); requestAnimationFrame(draw);
} }