This has been rattling around my head since I was a kid lying in the yard: if the universe is that old and that full of stars, the night sky seems like it should be a wall of light. It turns out the darkness is the interesting part.
(Built as a worked example of this transcript format: prompts upfront, model responses folded away, and the interactive toys inline.)
The intuition in the prompt is actually right, which is what makes it a good question — in an infinite, eternal, unchanging universe every sight line does end on a star, and the sky would glow at the surface temperature of one everywhere, forever. No night. The reason ours doesn’t is the good part:
Olbers' paradox — the sky is dark because the stars haven't been on forever 178 words
This is Olbers’ paradox, and your geometric intuition is correct: in an infinite, static, eternally-old universe, every line of sight terminates on a stellar surface, and the entire sky would blaze at roughly 5,800 K. Doubling the distance makes each star four times dimmer, but the shell at that distance holds four times as many stars — the effects cancel exactly.
Dust doesn’t save you: in an eternal universe, dust absorbing starlight heats up until it re-radiates just as brightly as what it absorbs.
The sky is dark for two reasons. First and most important, the universe has a finite age: light has only had 13.8 billion years to travel, so we can only see stars inside that horizon — and there simply aren’t enough of them along most sight lines. Second, the universe is expanding, so light from the most distant sources is redshifted out of the visible band and its energy diluted.
The darkness of the night sky is therefore direct, naked-eye evidence that the universe is not infinitely old — that it had a beginning.
The shell cancellation is the trap: step out to distance kR and each
star fades by 1/k², which feels like it should rescue the dark sky.
Except the wedge of space at kR captures k² times as many stars, so
every concentric shell delivers the exact same 1.00× slice of light —
click between the shells below or stack them together to watch the dark
patch disappear:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
:root {
--paper: #F5F0E8;
--paper-elev: #FAF7F1;
--paper-sunk: #EAE3D5;
--ink: #1A1A1A;
--ink-faded: #666159;
--accent: #C41E1E;
--accent-soft: rgba(196, 30, 30, 0.14);
--border: #1A1A1A;
--border-light: #D2CBB8;
}
[data-theme="dark"] {
--paper: #161514;
--paper-elev: #1E1D1B;
--paper-sunk: #262421;
--ink: #EFECE6;
--ink-faded: #9E988E;
--accent: #E24A4A;
--accent-soft: rgba(226, 74, 74, 0.18);
--border: #3D3A35;
--border-light: #2B2925;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Hack, "Courier New", monospace;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 780px;
margin: 0 auto;
padding: 14px;
}
.controls-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.pills {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
button {
font: inherit;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
background: var(--paper-elev);
color: var(--ink);
border: 1.5px solid var(--border);
border-radius: 3px;
padding: 5px 9px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
button:hover {
border-color: var(--accent);
color: var(--accent);
}
button.is-active {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
button.is-stack.is-active {
background: var(--accent);
border-color: var(--accent);
color: #F5F0E8;
}
.stack-slider-wrap {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--ink-faded);
}
.stack-slider-wrap input[type=range] {
width: 110px;
accent-color: var(--accent);
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: 12px;
align-items: stretch;
}
@media (max-width: 540px) {
.grid {
grid-template-columns: 1fr;
}
}
.panel {
border: 1.5px solid var(--border-light);
border-radius: 4px;
background: var(--paper-elev);
padding: 10px;
display: flex;
flex-direction: column;
}
.panel-title {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ink-faded);
margin-bottom: 6px;
display: flex;
justify-content: space-between;
}
canvas {
display: block;
width: 100%;
}
.patch-wrap {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
justify-content: space-between;
}
#patchCanvas {
border: 1.5px solid var(--border);
border-radius: 3px;
aspect-ratio: 1 / 1;
max-height: 175px;
width: auto;
margin: 0 auto;
}
.ledger {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
font-size: 11px;
}
.stat {
border: 1px solid var(--border-light);
background: var(--paper);
border-radius: 3px;
padding: 5px 7px;
}
.stat-k {
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-faded);
margin-bottom: 2px;
}
.stat-v {
font-size: 13px;
font-weight: 700;
}
.stat-v.accent {
color: var(--accent);
}
.caption {
font-size: 11px;
color: var(--ink-faded);
line-height: 1.45;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="controls-bar">
<div class="pills" id="shellButtons">
<button type="button" data-shell="1">Shell 1R</button>
<button type="button" data-shell="2" class="is-active">Shell 2R</button>
<button type="button" data-shell="3">Shell 3R</button>
<button type="button" data-shell="4">Shell 4R</button>
<button type="button" data-shell="5">Shell 5R</button>
<button type="button" id="stackBtn" class="is-stack">Stack 1..N</button>
</div>
<label class="stack-slider-wrap" id="stackSliderWrap">
<span>Depth</span>
<input type="range" id="stackRange" min="1" max="24" step="1" value="8">
<strong id="stackVal">8</strong>
</label>
</div>
<div class="grid">
<div class="panel">
<div class="panel-title">
<span>Sight Cone dΩ</span>
<span id="coneSubtitle">Shell 2R highlighted</span>
</div>
<canvas id="coneCanvas"></canvas>
</div>
<div class="panel">
<div class="panel-title">
<span>1° × 1° Sky Patch</span>
<span id="patchSubtitle">4 stars × 1/4 flux</span>
</div>
<div class="patch-wrap">
<canvas id="patchCanvas"></canvas>
<div class="ledger" id="ledger"></div>
</div>
</div>
</div>
<div class="caption" id="caption"></div>
</div>
<script>
(function() {
const wrapEl = document.querySelector('.wrap');
const coneCanvas = document.getElementById('coneCanvas');
const patchCanvas = document.getElementById('patchCanvas');
const shellBtns = document.querySelectorAll('[data-shell]');
const stackBtn = document.getElementById('stackBtn');
const stackRange = document.getElementById('stackRange');
const stackVal = document.getElementById('stackVal');
const coneSubtitle = document.getElementById('coneSubtitle');
const patchSubtitle = document.getElementById('patchSubtitle');
const ledgerEl = document.getElementById('ledger');
const captionEl = document.getElementById('caption');
let mode = 'single'; // 'single' | 'stack'
let activeShell = 2;
let stackDepth = 8;
// Deterministic star placements per shell (k = 1..24) in normalized cone coords and patch coords
function rngFactory(seed) {
let s = seed >>> 0;
return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
}
const shellStars = {};
for (let k = 1; k <= 24; k++) {
const rand = rngFactory(1000 + k * 97);
const count = k * k;
const pts = [];
for (let i = 0; i < count; i++) {
// Spread points cleanly inside normalized patch [0.06..0.94]^2 and cone vertical [-0.84..0.84]
pts.push({
px: 0.06 + rand() * 0.88,
py: 0.06 + rand() * 0.88,
cy: (i + 0.5) / count * 1.66 - 0.83 + (rand() - 0.5) * 0.06,
cr: (rand() - 0.5) * 0.34
});
}
shellStars[k] = pts;
}
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function renderCone() {
const w = coneCanvas.parentNode.clientWidth - 22;
const h = Math.max(200, Math.min(250, Math.round(w * 0.54)));
const dpr = window.devicePixelRatio || 1;
coneCanvas.width = w * dpr;
coneCanvas.height = h * dpr;
coneCanvas.style.height = h + 'px';
const ctx = coneCanvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const paper = cssVar('--paper');
const ink = cssVar('--ink');
const inkFaded = cssVar('--ink-faded');
const accent = cssVar('--accent');
const borderLight = cssVar('--border-light');
ctx.fillStyle = paper;
ctx.fillRect(0, 0, w, h);
const ox = 28;
const oy = h / 2;
const maxR = w - 46;
const halfAngle = 0.31; // cone half-angle
// Cone fill
ctx.fillStyle = document.documentElement.dataset.theme === 'dark'
? 'rgba(239, 236, 230, 0.04)'
: 'rgba(26, 26, 26, 0.035)';
ctx.beginPath();
ctx.moveTo(ox, oy);
ctx.lineTo(ox + maxR * Math.cos(halfAngle), oy - maxR * Math.sin(halfAngle));
ctx.arc(ox, oy, maxR, -halfAngle, halfAngle);
ctx.closePath();
ctx.fill();
// Cone edges
ctx.strokeStyle = inkFaded;
ctx.lineWidth = 1;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(ox, oy);
ctx.lineTo(ox + maxR * 1.03 * Math.cos(halfAngle), oy - maxR * 1.03 * Math.sin(halfAngle));
ctx.moveTo(ox, oy);
ctx.lineTo(ox + maxR * 1.03 * Math.cos(halfAngle), oy + maxR * 1.03 * Math.sin(halfAngle));
ctx.stroke();
ctx.setLineDash([]);
// Draw concentric shells 1..5
const bandWidth = maxR / 5.4;
for (let k = 1; k <= 5; k++) {
const rMid = k * bandWidth;
const isLit = mode === 'single' ? (k === activeShell) : (k <= stackDepth);
ctx.strokeStyle = isLit ? accent : borderLight;
ctx.lineWidth = isLit ? 2.5 : 1.25;
ctx.beginPath();
ctx.arc(ox, oy, rMid, -halfAngle, halfAngle);
ctx.stroke();
// Label above cone
const lx = ox + rMid * Math.cos(-halfAngle - 0.04);
const ly = oy + rMid * Math.sin(-halfAngle - 0.04) - 6;
ctx.fillStyle = isLit ? accent : inkFaded;
ctx.font = (isLit ? '700 ' : '') + '10px Hack, monospace';
ctx.textAlign = 'center';
ctx.fillText(k + 'R', lx, ly);
// Stars inside shell k
const stars = shellStars[k];
const dotR = isLit ? Math.max(2.1, 4.4 - k * 0.4) : 1.8;
for (const st of stars) {
const rr = rMid + st.cr * bandWidth * 0.38;
const ang = st.cy * (halfAngle * 0.88);
const sx = ox + rr * Math.cos(ang);
const sy = oy + rr * Math.sin(ang);
if (isLit && (mode === 'single' || k <= 4)) {
ctx.strokeStyle = document.documentElement.dataset.theme === 'dark'
? 'rgba(226, 74, 74, 0.25)'
: 'rgba(196, 30, 30, 0.22)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(ox, oy);
ctx.lineTo(sx, sy);
ctx.stroke();
}
ctx.fillStyle = isLit ? accent : inkFaded;
ctx.globalAlpha = isLit ? 1 : 0.45;
ctx.beginPath();
ctx.arc(sx, sy, dotR, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
// Extra badge if stackDepth > 5
if (mode === 'stack' && stackDepth > 5) {
ctx.fillStyle = accent;
ctx.font = '700 10px Hack, monospace';
ctx.textAlign = 'right';
ctx.fillText('+ shells 6..' + stackDepth + ' ->', w - 8, h - 8);
}
// Observer eye dot
ctx.fillStyle = accent;
ctx.beginPath();
ctx.arc(ox, oy, 5, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 1.5;
ctx.stroke();
}
function renderPatch() {
const size = Math.min(175, patchCanvas.parentNode.clientWidth - 4);
const dpr = window.devicePixelRatio || 1;
patchCanvas.width = size * dpr;
patchCanvas.height = size * dpr;
patchCanvas.style.width = size + 'px';
patchCanvas.style.height = size + 'px';
const ctx = patchCanvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Night sky background behind stars
ctx.fillStyle = '#0E0D0C';
ctx.fillRect(0, 0, size, size);
const kStart = mode === 'single' ? activeShell : 1;
const kEnd = mode === 'single' ? activeShell : stackDepth;
let litPixels = 0;
const totalStars = mode === 'single'
? activeShell * activeShell
: (stackDepth * (stackDepth + 1) * (2 * stackDepth + 1)) / 6;
// Draw far shells first, closer shells on top
for (let k = kEnd; k >= kStart; k--) {
const stars = shellStars[k];
// Apparent angular area drops as 1/k^2 while count grows as k^2
const rDisk = Math.max(1.3, (size * 0.145) / k);
const glowR = rDisk * 2.1;
const alpha = Math.max(0.28, Math.min(1, 1.35 / Math.pow(k, 0.45)));
for (const st of stars) {
const x = st.px * size;
const y = st.py * size;
const g = ctx.createRadialGradient(x, y, 0, x, y, glowR);
g.addColorStop(0, `rgba(255, 244, 224, ${alpha})`);
g.addColorStop(0.45, `rgba(255, 210, 150, ${alpha * 0.55})`);
g.addColorStop(1, 'rgba(255, 210, 150, 0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, glowR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFFDF7';
ctx.beginPath();
ctx.arc(x, y, rDisk * 0.65, 0, Math.PI * 2);
ctx.fill();
}
}
// Sample pixel brightness on sky patch for coverage estimate
const imgData = ctx.getImageData(0, 0, patchCanvas.width, patchCanvas.height).data;
for (let i = 0; i < imgData.length; i += 16) {
if (imgData[i] + imgData[i + 1] + imgData[i + 2] > 180) litPixels++;
}
const covPct = Math.min(100, Math.round((litPixels / (imgData.length / 16)) * 100));
if (mode === 'single') {
const k = activeShell;
const starsN = k * k;
coneSubtitle.textContent = `Shell ${k}R active`;
patchSubtitle.textContent = `${starsN} star${starsN === 1 ? '' : 's'} × 1/${starsN} flux`;
ledgerEl.innerHTML = `
<div class="stat"><div class="stat-k">Distance</div><div class="stat-v">${k}R</div></div>
<div class="stat"><div class="stat-k">Stars in cone</div><div class="stat-v">${starsN}</div></div>
<div class="stat"><div class="stat-k">Flux per star</div><div class="stat-v">1/${starsN}×</div></div>
<div class="stat"><div class="stat-k">Shell total</div><div class="stat-v accent">1.00×</div></div>
`;
captionEl.textContent = `Shell ${k}R holds ${starsN}× as many stars inside the cone, each 1/${starsN} as bright. Every single shell contributes the exact same 1.00× slice of light.`;
} else {
coneSubtitle.textContent = `Stacking shells 1R..${stackDepth}R`;
patchSubtitle.textContent = `${totalStars} cumulative stars`;
ledgerEl.innerHTML = `
<div class="stat"><div class="stat-k">Shells stacked</div><div class="stat-v">1..${stackDepth}</div></div>
<div class="stat"><div class="stat-k">Stars in patch</div><div class="stat-v">${totalStars}</div></div>
<div class="stat"><div class="stat-k">Sky filled</div><div class="stat-v">${covPct}%</div></div>
<div class="stat"><div class="stat-k">Total patch flux</div><div class="stat-v accent">${stackDepth}.00×</div></div>
`;
captionEl.textContent = `Each concentric shell adds another +1.00× of light. Stack enough shells and their dimmed disks overlap until no dark sky remains between them.`;
}
}
function renderAll() {
shellBtns.forEach(btn => {
btn.classList.toggle('is-active', mode === 'single' && Number(btn.dataset.shell) === activeShell);
});
stackBtn.classList.toggle('is-active', mode === 'stack');
stackVal.textContent = stackDepth;
renderCone();
renderPatch();
reportHeight();
}
let lastH = 0;
function reportHeight() {
const h = Math.ceil(wrapEl.getBoundingClientRect().height);
if (h && h !== lastH) {
lastH = h;
try { parent.postMessage({ type: 'ws-artifact-height', height: h }, '*'); } catch (e) {}
}
}
shellBtns.forEach(btn => {
btn.addEventListener('click', () => {
mode = 'single';
activeShell = Number(btn.dataset.shell);
renderAll();
});
});
stackBtn.addEventListener('click', () => {
mode = 'stack';
renderAll();
});
stackRange.addEventListener('input', () => {
mode = 'stack';
stackDepth = Number(stackRange.value);
renderAll();
});
window.addEventListener('message', e => {
if (e.data && e.data.type === 'ws-theme') {
if (e.data.theme === 'dark') {
document.documentElement.dataset.theme = 'dark';
} else {
delete document.documentElement.dataset.theme;
}
renderAll();
}
});
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(() => {
renderCone();
renderPatch();
reportHeight();
}).observe(wrapEl);
}
window.addEventListener('resize', renderAll);
try { parent.postMessage({ type: 'ws-artifact-ready' }, '*'); } catch (e) {}
renderAll();
})();
</script>
</body>
</html>
The inversion is what got me: the darkness isn’t an absence of information, it is the information. You can stand in a field, look up, and rule out an eternal static cosmos with no equipment at all. So I pushed on where the “bright sky” went:
The bright sky exists — it's just been stretched into microwaves 167 words
Yes on both counts. An eternal static universe gives you a sky that is everywhere as bright as the surface of a star — about 5,800 K in every direction, day and “night” indistinguishable.
And the CMB is exactly the kind of thing your question suggests: we do see a wall of light covering the entire sky, from the moment the universe first became transparent, 380,000 years after the Big Bang. It started as a ~3,000 K orange-white glow filling every direction. Expansion has stretched those wavelengths by a factor of about 1,100, so today it arrives as 2.7 K microwave radiation — invisible to your eyes, unmissable to a radio telescope. The sky is still blazing in every direction; it’s just been redshifted out of sight.
Historical footnote: Edgar Allan Poe, in Eureka (1848), proposed that the sky is dark because light from the most distant stars hasn’t had time to reach us — essentially the modern answer, seventy years before cosmology could back him up.
Poe got there first, in a prose poem, in 1848:
Were the succession of stars endless, then the background of the sky would present us an uniform luminosity, like that displayed by the Galaxy — since there could be absolutely no point, in all that background, at which would not exist a star. The only mode, therefore, in which, under such a state of affairs, we could comprehend the voids which our telescopes find in innumerable directions, would be by supposing the distance of the invisible background so immense that no ray from it has yet been able to reach us at all.
And I love that the sky actually is uniformly bright right now — we’re just tuned to the wrong channel. Slide forward from recombination to today and switch between your eyes and a 160 GHz microwave dish to see what is actually overhead:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
:root {
--paper: #F5F0E8;
--paper-elev: #FAF7F1;
--paper-sunk: #EAE3D5;
--ink: #1A1A1A;
--ink-faded: #666159;
--accent: #C41E1E;
--border: #1A1A1A;
--border-light: #D2CBB8;
}
[data-theme="dark"] {
--paper: #161514;
--paper-elev: #1E1D1B;
--paper-sunk: #262421;
--ink: #EFECE6;
--ink-faded: #9E988E;
--accent: #E24A4A;
--border: #3D3A35;
--border-light: #2B2925;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Hack, "Courier New", monospace;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 780px;
margin: 0 auto;
padding: 14px;
}
.controls-top {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.group {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
button {
font: inherit;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
background: var(--paper-elev);
color: var(--ink);
border: 1.5px solid var(--border);
border-radius: 3px;
padding: 5px 9px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
button:hover {
border-color: var(--accent);
color: var(--accent);
}
button.is-active {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
button.detector.is-active {
background: var(--accent);
border-color: var(--accent);
color: #F5F0E8;
}
.slider-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
font-size: 11px;
color: var(--ink-faded);
}
.slider-row input[type=range] {
flex: 1;
accent-color: var(--accent);
cursor: pointer;
}
.stage {
display: grid;
grid-template-columns: 1fr 1.35fr;
gap: 12px;
align-items: stretch;
}
@media (max-width: 540px) {
.stage { grid-template-columns: 1fr; }
}
.panel {
border: 1.5px solid var(--border-light);
border-radius: 4px;
background: var(--paper-elev);
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.panel-title {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ink-faded);
margin-bottom: 6px;
display: flex;
justify-content: space-between;
gap: 6px;
}
canvas { display: block; width: 100%; }
#domeCanvas {
border: 1.5px solid var(--border);
border-radius: 4px;
}
.spectrum-bar {
margin-top: 8px;
position: relative;
height: 18px;
border: 1px solid var(--border-light);
border-radius: 3px;
display: grid;
grid-template-columns: 28% 36% 36%;
overflow: visible;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.band {
display: flex;
align-items: center;
justify-content: center;
color: #F5F0E8;
}
.band-vis {
background: linear-gradient(90deg, #5469d4 0%, #e09f3e 55%, #C41E1E 100%);
}
.band-ir {
background: linear-gradient(90deg, #7f1d1d 0%, #3a1a1a 100%);
color: #e6d5cf;
}
.band-mw {
background: #1B1918;
color: #B4ABA0;
}
.needle {
position: absolute;
top: -4px;
bottom: -4px;
width: 3px;
background: var(--accent);
box-shadow: 0 0 0 1px #F5F0E8;
transform: translateX(-50%);
pointer-events: none;
}
.readout-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
margin-top: 10px;
}
@media (max-width: 560px) {
.readout-grid { grid-template-columns: repeat(2, 1fr); }
}
.stat {
border: 1px solid var(--border-light);
background: var(--paper-elev);
border-radius: 3px;
padding: 5px 7px;
}
.stat-k {
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-faded);
}
.stat-v {
font-size: 12.5px;
font-weight: 700;
margin-top: 2px;
}
.stat-v.accent { color: var(--accent); }
.caption {
font-size: 11px;
color: var(--ink-faded);
line-height: 1.45;
margin-top: 9px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="controls-top">
<div class="group" id="presets">
<button type="button" data-p="0" class="is-active">380 kyr · z=1090</button>
<button type="button" data-p="0.47">Dark Ages · z=40</button>
<button type="button" data-p="1">Today · z=0</button>
</div>
<div class="group" id="detectors">
<button type="button" class="detector is-active" data-det="eye">Human Eye (visible)</button>
<button type="button" class="detector" data-det="dish">Microwave Dish (160 GHz)</button>
</div>
</div>
<div class="slider-row">
<span>380 kyr</span>
<input type="range" id="epochSlider" min="0" max="1" step="0.002" value="1">
<span>Today · 13.8 Gyr</span>
</div>
<div class="stage">
<div class="panel">
<div class="panel-title">
<span>All-Sky Receiver</span>
<span id="detectorBadge">Eye</span>
</div>
<canvas id="domeCanvas"></canvas>
</div>
<div class="panel">
<div class="panel-title">
<span>Photon Stretch</span>
<span id="stretchBadge">×1.0</span>
</div>
<canvas id="waveCanvas"></canvas>
<div class="spectrum-bar">
<div class="band band-vis">Visible</div>
<div class="band band-ir">Infrared</div>
<div class="band band-mw">Microwave</div>
<div class="needle" id="specNeedle" style="left: 90%"></div>
</div>
</div>
</div>
<div class="readout-grid" id="readouts"></div>
<div class="caption" id="caption"></div>
</div>
<script>
(function() {
const wrapEl = document.querySelector('.wrap');
const slider = document.getElementById('epochSlider');
const domeCanvas = document.getElementById('domeCanvas');
const waveCanvas = document.getElementById('waveCanvas');
const presetBtns = document.querySelectorAll('#presets button');
const detBtns = document.querySelectorAll('#detectors button');
const detectorBadge = document.getElementById('detectorBadge');
const stretchBadge = document.getElementById('stretchBadge');
const specNeedle = document.getElementById('specNeedle');
const readoutsEl = document.getElementById('readouts');
const captionEl = document.getElementById('caption');
let detector = 'eye';
let phase = 0;
// Deterministic field stars & CMB hot/cold patches
let seed = 77;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const fgStars = Array.from({ length: 65 }, () => ({
x: rnd(), y: rnd() * 0.86, r: 0.6 + rnd() * 1.3, b: 0.45 + rnd() * 0.55
}));
const cmbSpots = Array.from({ length: 120 }, () => ({
x: rnd(), y: rnd(), r: 6 + rnd() * 18, delta: (rnd() - 0.5) * 2
}));
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function stateFromSlider() {
const p = +slider.value; // 0 = recombination (z=1090), 1 = today (z=0)
const onePlusZ = Math.pow(1091, 1 - p);
const z = Math.max(0, onePlusZ - 1);
const stretchSinceRecomb = 1091 / onePlusZ; // 1x at recombination -> 1091x today
const temp = 2.725 * onePlusZ; // 2973 K -> 2.725 K
const peakMm = 2.898 / temp; // Wien displacement in mm
const band = peakMm < 0.00075 ? 'Visible' : (peakMm < 0.05 ? 'Infrared' : 'Microwave');
return { p, z, onePlusZ, stretchSinceRecomb, temp, peakMm, band };
}
function formatLambda(mm) {
if (mm < 0.001) return Math.round(mm * 1e6) + ' nm';
if (mm < 0.5) return (mm * 1000).toFixed(1) + ' μm';
return mm.toFixed(2) + ' mm';
}
function formatTemp(t) {
if (t >= 100) return Math.round(t).toLocaleString() + ' K';
if (t >= 10) return t.toFixed(1) + ' K';
return t.toFixed(2) + ' K';
}
function blackbodyEyeColor(temp) {
if (temp >= 2400) return [255, 206, 140];
if (temp >= 1500) {
const t = (temp - 1500) / 900;
return [Math.round(215 + 40 * t), Math.round(90 + 116 * t), Math.round(28 + 112 * t)];
}
if (temp >= 650) {
const t = (temp - 650) / 850;
return [Math.round(215 * t), Math.round(90 * t * 0.7), Math.round(28 * t * 0.5)];
}
return [12, 11, 12];
}
function renderDome(st) {
const w = domeCanvas.parentNode.clientWidth - 22;
const h = 158;
const dpr = window.devicePixelRatio || 1;
domeCanvas.width = w * dpr;
domeCanvas.height = h * dpr;
domeCanvas.style.height = h + 'px';
const ctx = domeCanvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (detector === 'eye') {
const rgb = blackbodyEyeColor(st.temp);
ctx.fillStyle = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
ctx.fillRect(0, 0, w, h);
// Stars emerge late in cosmic history when plasma fades into IR
if (st.z < 18) {
const starVis = Math.max(0, Math.min(1, (18 - st.z) / 14));
for (const s of fgStars) {
ctx.fillStyle = `rgba(255, 250, 240, ${s.b * starVis})`;
ctx.beginPath();
ctx.arc(s.x * w, s.y * h, s.r, 0, Math.PI * 2);
ctx.fill();
}
}
detectorBadge.textContent = st.temp > 750 ? 'Eye · sky glowing' : 'Eye · sky invisible';
} else {
// Microwave dish tuned to 160 GHz (today's CMB peak)
const mwStrength = Math.max(0.18, Math.min(1, st.stretchSinceRecomb / 300));
ctx.fillStyle = '#1E100E';
ctx.fillRect(0, 0, w, h);
for (const spot of cmbSpots) {
const cx = spot.x * w, cy = spot.y * h;
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, spot.r * 1.4);
const warm = spot.delta > 0;
const c = warm ? '226, 74, 74' : '240, 175, 95';
const alpha = (0.14 + Math.abs(spot.delta) * 0.22) * (0.35 + 0.65 * mwStrength);
g.addColorStop(0, `rgba(${c}, ${alpha})`);
g.addColorStop(1, `rgba(${c}, 0)`);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, spot.r * 1.4, 0, Math.PI * 2);
ctx.fill();
}
// All-sky baseline glow
ctx.fillStyle = `rgba(196, 30, 30, ${0.24 * mwStrength})`;
ctx.fillRect(0, 0, w, h);
detectorBadge.textContent = st.z < 20 ? '160 GHz · wall of 2.7 K glow' : '160 GHz · pre-microwave tail';
}
// Subtle treeline horizon silhouette at bottom
ctx.fillStyle = '#0D0C0B';
ctx.beginPath();
ctx.moveTo(0, h);
for (let x = 0; x <= w; x += 6) {
const treeH = 11 + Math.sin(x * 0.14) * 4 + Math.cos(x * 0.37) * 3;
ctx.lineTo(x, h - treeH);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
}
function renderWave(st) {
const w = waveCanvas.parentNode.clientWidth - 22;
const h = 116;
const dpr = window.devicePixelRatio || 1;
waveCanvas.width = w * dpr;
waveCanvas.height = h * dpr;
waveCanvas.style.height = h + 'px';
const ctx = waveCanvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const paper = cssVar('--paper');
const inkFaded = cssVar('--ink-faded');
const accent = cssVar('--accent');
ctx.fillStyle = paper;
ctx.fillRect(0, 0, w, h);
// Axis line
ctx.strokeStyle = cssVar('--border-light');
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(8, h / 2);
ctx.lineTo(w - 8, h / 2);
ctx.stroke();
// Visual wavelength maps smoothly from 14px (at z=1090) -> 126px (at z=0)
const logStretch = Math.log10(st.stretchSinceRecomb) / Math.log10(1091); // 0..1
const waveLenPx = 14 + logStretch * 114;
const amp = 32 - logStretch * 6;
// Color follows band
let stroke = '#E09F3E';
if (st.band === 'Infrared') stroke = '#9E3224';
if (st.band === 'Microwave') stroke = accent;
ctx.strokeStyle = stroke;
ctx.lineWidth = 2.5;
ctx.beginPath();
for (let x = 8; x <= w - 8; x += 2) {
const y = h / 2 + Math.sin((x / waveLenPx) * Math.PI * 2 - phase) * amp;
if (x === 8) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Label wavelength bracket
ctx.fillStyle = inkFaded;
ctx.font = '10px Hack, monospace';
ctx.textAlign = 'left';
ctx.fillText(`peak λ = ${formatLambda(st.peakMm)} (${st.band})`, 10, 16);
// Position needle along spectrum bar (log wavelength from 300nm to 3mm)
const minLog = Math.log10(0.00035);
const maxLog = Math.log10(3.0);
const norm = Math.max(2, Math.min(98, ((Math.log10(st.peakMm) - minLog) / (maxLog - minLog)) * 100));
specNeedle.style.left = norm.toFixed(1) + '%';
}
function updateUI(st) {
presetBtns.forEach(btn => {
btn.classList.toggle('is-active', Math.abs(Number(btn.dataset.p) - st.p) < 0.025);
});
detBtns.forEach(btn => {
btn.classList.toggle('is-active', btn.dataset.det === detector);
});
const stretchLabel = st.stretchSinceRecomb < 10
? st.stretchSinceRecomb.toFixed(1) + '×'
: Math.round(st.stretchSinceRecomb).toLocaleString() + '×';
stretchBadge.textContent = 'stretched ' + stretchLabel;
const zText = st.z >= 10 ? Math.round(st.z).toLocaleString() : st.z.toFixed(1);
readoutsEl.innerHTML = `
<div class="stat"><div class="stat-k">Redshift z</div><div class="stat-v">${zText}</div></div>
<div class="stat"><div class="stat-k">Stretch</div><div class="stat-v">${stretchLabel}</div></div>
<div class="stat"><div class="stat-k">Sky Temp</div><div class="stat-v accent">${formatTemp(st.temp)}</div></div>
<div class="stat"><div class="stat-k">Peak λ</div><div class="stat-v">${formatLambda(st.peakMm)}</div></div>
`;
if (st.p < 0.05) {
captionEl.textContent = 'At 380,000 years (recombination), the entire universe became transparent at once: every direction glowed like a 3,000 K oven.';
} else if (detector === 'eye' && st.z < 10) {
captionEl.textContent = 'To human eyes at z = 0, space looks black between the stars. The old 3,000 K wall of light is still arriving, stretched 1,091× into microwaves.';
} else if (detector === 'dish' && st.z < 10) {
captionEl.textContent = 'Tune a radio receiver to 160 GHz today and the "dark" sky lights back up uniformly in every direction at 2.725 K.';
} else {
captionEl.textContent = `Cosmic expansion has stretched space by ${stretchLabel} since recombination, cooling the background glow from 3,000 K down to ${formatTemp(st.temp)}.`;
}
}
let lastH = 0;
function reportHeight() {
const h = Math.ceil(wrapEl.getBoundingClientRect().height);
if (h && h !== lastH) {
lastH = h;
try { parent.postMessage({ type: 'ws-artifact-height', height: h }, '*'); } catch (e) {}
}
}
function renderFrame() {
const st = stateFromSlider();
renderDome(st);
renderWave(st);
updateUI(st);
reportHeight();
}
function tick() {
phase += 0.06;
const st = stateFromSlider();
renderWave(st);
requestAnimationFrame(tick);
}
slider.addEventListener('input', renderFrame);
presetBtns.forEach(btn => {
btn.addEventListener('click', () => {
slider.value = btn.dataset.p;
renderFrame();
});
});
detBtns.forEach(btn => {
btn.addEventListener('click', () => {
detector = btn.dataset.det;
renderFrame();
});
});
window.addEventListener('message', e => {
if (e.data && e.data.type === 'ws-theme') {
if (e.data.theme === 'dark') {
document.documentElement.dataset.theme = 'dark';
} else {
delete document.documentElement.dataset.theme;
}
renderFrame();
}
});
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(renderFrame).observe(wrapEl);
}
window.addEventListener('resize', renderFrame);
try { parent.postMessage({ type: 'ws-artifact-ready' }, '*'); } catch (e) {}
renderFrame();
requestAnimationFrame(tick);
})();
</script>
</body>
</html>
At this point I wanted to see the horizon geometry from the ground up, so:
Claude precomputes the ray–star intersections and hangs it on a log slider 143 words
The trick that makes it responsive: for each of the 540 sight lines I precompute, once at load, the distance at which it first intersects a star (standard ray–circle intersection over a deterministic field of 24,000 stars). After that, moving the slider is just a comparison — a sight line is “lit” if its first hit is inside the current light horizon — so the whole thing re-renders instantly.
The slider is logarithmic from 1 to 2,000 billion years, since the interesting behavior spans orders of magnitude. The view auto-zooms to keep the horizon in frame: at small ages you see a huge dark horizon with a few lit lines, and as the age grows the field fills until every direction terminates on a star. Stars beyond the horizon render as faint ghosts — they exist, but their light hasn’t reached you yet.
Here it is, paired with what that exact slice of universe looks like looking up above the pines in the yard. It starts at today — press Run clock (or drag toward Forever) to watch the dark directions run out, hover any ray to inspect when its light arrives, or toggle Expanding (redshift) to see how expansion fades distant shells back out of view:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
:root {
--paper: #F5F0E8;
--paper-elev: #FAF7F1;
--paper-sunk: #EAE3D5;
--ink: #1A1A1A;
--ink-faded: #666159;
--accent: #C41E1E;
--border: #1A1A1A;
--border-light: #D2CBB8;
}
[data-theme="dark"] {
--paper: #161514;
--paper-elev: #1E1D1B;
--paper-sunk: #262421;
--ink: #EFECE6;
--ink-faded: #9E988E;
--accent: #E24A4A;
--border: #3D3A35;
--border-light: #2B2925;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Hack, "Courier New", monospace;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 780px;
margin: 0 auto;
padding: 14px;
}
.telemetry {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 10px;
}
@media (max-width: 560px) {
.telemetry { grid-template-columns: 1fr; }
}
.stat {
border: 1px solid var(--border-light);
background: var(--paper-elev);
border-radius: 3px;
padding: 6px 9px;
}
.stat-k {
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ink-faded);
}
.stat-v {
font-size: 13.5px;
font-weight: 700;
margin-top: 2px;
}
.stat-v strong {
color: var(--accent);
}
.canvas-frame {
position: relative;
border: 1.5px solid var(--border);
border-radius: 4px;
background: var(--paper);
overflow: hidden;
}
canvas {
display: block;
width: 100%;
}
#sky {
cursor: crosshair;
}
.hover-readout {
min-height: 24px;
padding: 4px 10px;
font-size: 11px;
color: var(--ink-faded);
border-top: 1px solid var(--border-light);
background: var(--paper-elev);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.hover-readout strong {
color: var(--accent);
}
.yard-block {
margin-top: 10px;
border: 1.5px solid var(--border);
border-radius: 4px;
overflow: hidden;
background: #0D0C0B;
}
.yard-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 9px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(245, 240, 232, 0.78);
background: #171614;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-top: 10px;
}
.slider-group {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 220px;
font-size: 10.5px;
color: var(--ink-faded);
}
input[type=range] {
flex: 1;
accent-color: var(--accent);
cursor: pointer;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
button {
font: inherit;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
background: var(--paper-elev);
color: var(--ink);
border: 1.5px solid var(--border);
border-radius: 3px;
padding: 5px 9px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
button:hover {
border-color: var(--accent);
color: var(--accent);
}
button.is-active {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
button.is-accent.is-active {
background: var(--accent);
border-color: var(--accent);
color: #F5F0E8;
}
.hint {
font-size: 11px;
color: var(--ink-faded);
margin-top: 9px;
line-height: 1.48;
}
</style>
</head>
<body>
<div class="wrap">
<div class="telemetry">
<div class="stat">
<div class="stat-k">Light Travel Time</div>
<div class="stat-v" id="telAge">13.8 Gyr</div>
</div>
<div class="stat">
<div class="stat-k">Sight Lines Lit</div>
<div class="stat-v" id="telLit"></div>
</div>
<div class="stat">
<div class="stat-k">Sky State</div>
<div class="stat-v" id="telState"></div>
</div>
</div>
<div class="canvas-frame">
<canvas id="sky"></canvas>
<div class="hover-readout" id="hoverReadout">
<span>Move over the starfield to inspect any sight line</span>
<span id="hoverDetail">540 rays · 24,000 stars</span>
</div>
</div>
<div class="yard-block">
<div class="yard-bar">
<span>Looking up from the yard (360° horizon)</span>
<span id="yardLabel">night sky</span>
</div>
<canvas id="yard"></canvas>
</div>
<div class="controls">
<div class="slider-group">
<span>1 Gyr</span>
<input type="range" id="age" min="0" max="1" step="0.001">
<span>2,000 Gyr</span>
</div>
<div class="btn-group">
<button type="button" id="playBtn" class="is-accent">▶ Run clock</button>
<button type="button" id="today">Today · 13.8</button>
<button type="button" id="hundred">100 Gyr</button>
<button type="button" id="forever">Forever</button>
</div>
<div class="btn-group">
<button type="button" id="modeStatic" class="is-active">Static</button>
<button type="button" id="modeExpand">Expanding (redshift)</button>
</div>
</div>
<div class="hint">Drag or press <strong>Run clock</strong> to give starlight more time to travel. Lit sight lines glow vermilion from star to observer; toggle <strong>Expanding (redshift)</strong> to dim distant shells beyond visible light.</div>
</div>
<script>
(function() {
const FIELD = 4000, STAR_R = 3, N = 24000, RAYS = 540;
const MAX_AGE = 2000, PX_PER_GYR = 4, TODAY = 13.8;
const wrapEl = document.querySelector('.wrap');
const canvas = document.getElementById('sky');
const ctx = canvas.getContext('2d');
const yardCanvas = document.getElementById('yard');
const yctx = yardCanvas.getContext('2d');
const slider = document.getElementById('age');
const telAge = document.getElementById('telAge');
const telLit = document.getElementById('telLit');
const telState = document.getElementById('telState');
const hoverReadout = document.getElementById('hoverReadout');
const hoverDetail = document.getElementById('hoverDetail');
const yardLabel = document.getElementById('yardLabel');
const playBtn = document.getElementById('playBtn');
const todayBtn = document.getElementById('today');
const hundredBtn = document.getElementById('hundred');
const foreverBtn = document.getElementById('forever');
const modeStaticBtn = document.getElementById('modeStatic');
const modeExpandBtn = document.getElementById('modeExpand');
// Deterministic star field
let seed = 42;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const stars = [];
while (stars.length < N) {
const r = FIELD * Math.sqrt(rnd()), a = rnd() * 2 * Math.PI;
if (r < 30) continue;
stars.push({
x: r * Math.cos(a),
y: r * Math.sin(a),
d: r,
az: (a < 0 ? a + 2 * Math.PI : a) / (2 * Math.PI),
alt: 0.14 + rnd() * 0.78
});
}
// Precompute first star hit along each sight line
const hit = new Float64Array(RAYS);
for (let i = 0; i < RAYS; i++) {
const a = i / RAYS * 2 * Math.PI, dx = Math.cos(a), dy = Math.sin(a);
let best = Infinity;
for (let j = 0; j < N; j++) {
const s = stars[j], t = s.x * dx + s.y * dy;
if (t <= 0 || t - STAR_R >= best) continue;
const d2 = s.x * s.x + s.y * s.y - t * t, rr = STAR_R * STAR_R;
if (d2 < rr) {
const h = t - Math.sqrt(rr - d2);
if (h < best) best = h;
}
}
hit[i] = best;
}
let expanding = false;
let playing = false;
let rafId = null;
let lastTs = 0;
let hoverIdx = -1;
const uOf = age => Math.log10(age) / Math.log10(MAX_AGE);
const ageOf = u => Math.pow(10, u * Math.log10(MAX_AGE));
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function expandVis(distPx) {
if (!expanding) return 1;
const distGyr = distPx / PX_PER_GYR;
return Math.exp(-Math.max(0, distGyr - 14) / 68);
}
function renderSky(age, R) {
const w = canvas.parentNode.clientWidth;
const size = Math.min(w, 460);
if (w < 40 || size < 40) return null;
const dpr = window.devicePixelRatio || 1;
canvas.width = w * dpr;
canvas.height = size * dpr;
canvas.style.height = size + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const paper = cssVar('--paper');
const ink = cssVar('--ink');
const inkFaded = cssVar('--ink-faded');
const accent = cssVar('--accent');
const isDark = document.documentElement.dataset.theme === 'dark';
const cx = w / 2;
const cy = size / 2;
const s = Math.max(0.01, (Math.min(cx, cy) - 18) / Math.max(R, 250));
ctx.fillStyle = paper;
ctx.fillRect(0, 0, w, size);
// Reference distance scale rings (10, 50, 100, 500, 1000 Gyr)
const rings = [10, 50, 100, 500, 1000];
ctx.font = '10px Hack, monospace';
ctx.textAlign = 'left';
for (const gyr of rings) {
const rrPx = gyr * PX_PER_GYR * s;
if (rrPx < 28 || rrPx > Math.max(w, size) * 0.65) continue;
ctx.strokeStyle = isDark ? 'rgba(239, 236, 230, 0.08)' : 'rgba(26, 26, 26, 0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, rrPx, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = inkFaded;
ctx.globalAlpha = 0.65;
ctx.fillText(gyr + ' Gyr', cx + 4, cy - rrPx + 11);
ctx.globalAlpha = 1;
}
// Horizon boundary circle
ctx.setLineDash([5, 5]);
ctx.strokeStyle = inkFaded;
ctx.lineWidth = 1.25;
ctx.beginPath();
ctx.arc(cx, cy, R * s, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Stars: inside horizon sharp, outside faint ghosts
const sr = Math.min(Math.max(STAR_R * s, 0.85), 4.6);
const maxDraw = Math.hypot(cx, cy) * 1.1;
for (let i = 0; i < N; i++) {
const st = stars[i];
if (st.d * s > maxDraw) continue;
const inside = st.d <= R;
if (inside) {
const vis = expandVis(st.d);
const a = Math.max(0.15, (isDark ? 0.88 : 0.86) * vis);
ctx.fillStyle = isDark
? `rgba(239, 236, 230, ${a})`
: `rgba(26, 26, 26, ${a})`;
} else {
ctx.fillStyle = isDark
? 'rgba(239, 236, 230, 0.13)'
: 'rgba(26, 26, 26, 0.13)';
}
ctx.beginPath();
ctx.arc(cx + st.x * s, cy + st.y * s, sr, 0, Math.PI * 2);
ctx.fill();
}
// Sight lines: unlit in faint ink, lit in glowing vermilion
let lit = 0;
let weightedBright = 0;
const unlitStroke = isDark ? 'rgba(239, 236, 230, 0.08)' : 'rgba(26, 26, 26, 0.09)';
ctx.lineWidth = 1;
for (let i = 0; i < RAYS; i++) {
if (hit[i] <= R) continue;
const a = i / RAYS * 2 * Math.PI;
const x = cx + Math.cos(a) * R * s;
const y = cy + Math.sin(a) * R * s;
ctx.strokeStyle = unlitStroke;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.stroke();
}
for (let i = 0; i < RAYS; i++) {
if (hit[i] > R) continue;
lit++;
const vis = expandVis(hit[i]);
weightedBright += vis;
const a = i / RAYS * 2 * Math.PI;
const x = cx + Math.cos(a) * hit[i] * s;
const y = cy + Math.sin(a) * hit[i] * s;
const alpha = expanding ? (0.12 + 0.72 * vis) : 0.68;
ctx.strokeStyle = isDark
? `rgba(226, 74, 74, ${alpha})`
: `rgba(196, 30, 30, ${alpha})`;
ctx.lineWidth = 1.15;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.stroke();
ctx.fillStyle = accent;
ctx.globalAlpha = expanding ? Math.max(0.2, vis) : 1;
ctx.beginPath();
ctx.arc(x, y, 1.9, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
// Highlight hovered ray
if (hoverIdx >= 0 && hoverIdx < RAYS) {
const a = hoverIdx / RAYS * 2 * Math.PI;
const isLitRay = hit[hoverIdx] <= R;
const d = isLitRay ? hit[hoverIdx] : R;
const hx = cx + Math.cos(a) * d * s;
const hy = cy + Math.sin(a) * d * s;
ctx.strokeStyle = isLitRay ? accent : ink;
ctx.lineWidth = 2.4;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(hx, hy);
ctx.stroke();
ctx.fillStyle = isLitRay ? accent : ink;
ctx.beginPath();
ctx.arc(hx, hy, 4, 0, Math.PI * 2);
ctx.fill();
}
// Center observer dot
ctx.fillStyle = accent;
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = ink;
ctx.lineWidth = 1.5;
ctx.stroke();
return { lit, weightedBright };
}
function renderYard(age, R, stats) {
const w = yardCanvas.parentNode.clientWidth;
const h = 112;
const dpr = window.devicePixelRatio || 1;
yardCanvas.width = w * dpr;
yardCanvas.height = h * dpr;
yardCanvas.style.height = h + 'px';
yctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const effRatio = Math.min(1, stats.weightedBright / RAYS);
// Warm up the sky as weighted bright lines saturate
const bgMix = Math.pow(effRatio, 1.4);
const rCol = Math.round(13 + bgMix * 238);
const gCol = Math.round(12 + bgMix * 212);
const bCol = Math.round(11 + bgMix * 168);
yctx.fillStyle = `rgb(${rCol},${gCol},${bCol})`;
yctx.fillRect(0, 0, w, h);
const colW = w / RAYS;
// Column wash for each hit sight line
for (let i = 0; i < RAYS; i++) {
if (hit[i] > R) continue;
const vis = expandVis(hit[i]);
const distFactor = Math.max(0.18, 1 - hit[i] / (FIELD * 1.1));
const x = (i / RAYS) * w;
yctx.fillStyle = expanding
? `rgba(215, 110, 75, ${0.42 * vis})`
: `rgba(255, 224, 175, ${0.34 + 0.52 * distFactor})`;
yctx.fillRect(x, 0, Math.ceil(colW) + 0.6, h - 18);
}
// Crisp visible foreground stars inside horizon
for (let i = 0; i < 650; i++) {
const st = stars[i];
if (st.d > R) continue;
const vis = expandVis(st.d);
if (vis < 0.08) continue;
const x = st.az * w;
const y = (1 - st.alt) * (h - 24);
const rad = Math.max(0.7, Math.min(2.6, 55 / Math.max(st.d, 25)));
yctx.fillStyle = `rgba(255, 252, 240, ${Math.min(1, 0.55 + vis * 0.45)})`;
yctx.beginPath();
yctx.arc(x, y, rad, 0, Math.PI * 2);
yctx.fill();
}
// Highlight inspected azimuth on panorama
if (hoverIdx >= 0) {
const hx = (hoverIdx / RAYS) * w;
yctx.strokeStyle = cssVar('--accent');
yctx.lineWidth = 1.5;
yctx.beginPath();
yctx.moveTo(hx, 0);
yctx.lineTo(hx, h - 18);
yctx.stroke();
}
// Northwest Connecticut pines + barn roofline silhouette
yctx.fillStyle = '#0B0A09';
yctx.beginPath();
yctx.moveTo(0, h);
for (let x = 0; x <= w; x += 4) {
const pct = x / w;
let y = h - 16 - Math.abs(Math.sin(x * 0.13) * 11) - Math.abs(Math.cos(x * 0.31) * 6);
// Small barn gable around 72% across
if (pct > 0.68 && pct < 0.77) {
const b = (pct - 0.68) / 0.09;
const gable = b < 0.5 ? b * 2 : (1 - b) * 2;
y = Math.min(y, h - 22 - gable * 14);
}
yctx.lineTo(x, y);
}
yctx.lineTo(w, h);
yctx.closePath();
yctx.fill();
yardLabel.textContent = effRatio < 0.2
? 'night sky'
: (effRatio < 0.7 ? 'closing in' : "Olbers' bright sky");
}
function updateUI(age, stats) {
const litPct = ((100 * stats.lit) / RAYS).toFixed(1);
const visPct = ((100 * stats.weightedBright) / RAYS).toFixed(1);
const fmtAge = age < 10 ? age.toFixed(1) : Math.round(age).toLocaleString();
telAge.textContent = fmtAge + ' Gyr';
if (expanding) {
telLit.innerHTML = `${stats.lit} / ${RAYS} · visible: <strong>${visPct}%</strong>`;
telState.textContent = stats.lit / RAYS > 0.8 ? 'Stars hit, light redshifted' : 'Sparse + redshifted';
} else {
telLit.innerHTML = `${stats.lit} / ${RAYS} (<strong>${litPct}%</strong>)`;
telState.textContent = litPct >= 99.5 ? "Olbers' bright wall" : (stats.lit / RAYS > 0.4 ? 'Closing in' : 'Sparse night sky');
}
todayBtn.classList.toggle('is-active', Math.abs(age - TODAY) < 0.4);
hundredBtn.classList.toggle('is-active', Math.abs(age - 100) < 3);
foreverBtn.classList.toggle('is-active', age >= MAX_AGE - 20);
modeStaticBtn.classList.toggle('is-active', !expanding);
modeExpandBtn.classList.toggle('is-active', expanding);
playBtn.classList.toggle('is-active', playing);
playBtn.innerHTML = playing ? '▮▮ Pause' : '▶ Run clock';
if (hoverIdx >= 0) {
const deg = Math.round((hoverIdx / RAYS) * 360);
const hitGyr = hit[hoverIdx] / PX_PER_GYR;
if (hit[hoverIdx] === Infinity) {
hoverReadout.firstElementChild.innerHTML = `Ray ${deg}° · <strong>escapes field</strong>`;
} else if (hitGyr <= age) {
hoverReadout.firstElementChild.innerHTML = `Ray ${deg}° · <strong>lit</strong> (star at ${hitGyr.toFixed(1)} Gyr)`;
} else {
const wait = (hitGyr - age).toFixed(1);
hoverReadout.firstElementChild.innerHTML = `Ray ${deg}° · star at ${hitGyr.toFixed(1)} Gyr (arrives in <strong>+${wait} Gyr</strong>)`;
}
hoverDetail.textContent = expanding ? `redshift vis ${Math.round(expandVis(hit[hoverIdx]) * 100)}%` : 'unredshifted';
}
}
let lastH = 0;
function reportHeight() {
const h = Math.ceil(wrapEl.getBoundingClientRect().height);
if (h && h !== lastH) {
lastH = h;
try { parent.postMessage({ type: 'ws-artifact-height', height: h }, '*'); } catch (e) {}
}
}
function render() {
const age = ageOf(+slider.value);
const R = age * PX_PER_GYR;
const stats = renderSky(age, R);
if (!stats) return;
renderYard(age, R, stats);
updateUI(age, stats);
reportHeight();
}
function stepPlay(ts) {
if (!playing) return;
if (!lastTs) lastTs = ts;
const dt = Math.min(0.05, (ts - lastTs) / 1000);
lastTs = ts;
const next = Math.min(1, +slider.value + dt * 0.095);
slider.value = next;
render();
if (next >= 1) {
playing = false;
updateUI(ageOf(next), { lit: RAYS, weightedBright: expanding ? 40 : RAYS });
render();
return;
}
rafId = requestAnimationFrame(stepPlay);
}
function stopPlay() {
playing = false;
if (rafId) cancelAnimationFrame(rafId);
lastTs = 0;
}
slider.value = uOf(TODAY);
slider.addEventListener('input', () => { stopPlay(); render(); });
playBtn.addEventListener('click', () => {
if (playing) {
stopPlay();
render();
} else {
if (+slider.value >= 0.995) slider.value = uOf(1);
playing = true;
lastTs = 0;
rafId = requestAnimationFrame(stepPlay);
render();
}
});
todayBtn.addEventListener('click', () => { stopPlay(); slider.value = uOf(TODAY); render(); });
hundredBtn.addEventListener('click', () => { stopPlay(); slider.value = uOf(100); render(); });
foreverBtn.addEventListener('click', () => { stopPlay(); slider.value = 1; render(); });
modeStaticBtn.addEventListener('click', () => { expanding = false; render(); });
modeExpandBtn.addEventListener('click', () => { expanding = true; render(); });
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const dx = (e.clientX - rect.left) - rect.width / 2;
const dy = (e.clientY - rect.top) - rect.height / 2;
let ang = Math.atan2(dy, dx);
if (ang < 0) ang += 2 * Math.PI;
hoverIdx = Math.round((ang / (2 * Math.PI)) * RAYS) % RAYS;
render();
});
canvas.addEventListener('mouseleave', () => {
hoverIdx = -1;
hoverReadout.firstElementChild.textContent = 'Move over the starfield to inspect any sight line';
hoverDetail.textContent = '540 rays · 24,000 stars';
render();
});
window.addEventListener('message', e => {
if (e.data && e.data.type === 'ws-theme') {
if (e.data.theme === 'dark') {
document.documentElement.dataset.theme = 'dark';
} else {
delete document.documentElement.dataset.theme;
}
render();
}
});
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(render).observe(wrapEl);
}
window.addEventListener('resize', render);
try { parent.postMessage({ type: 'ws-artifact-ready' }, '*'); } catch (e) {}
render();
})();
</script>
</body>
</html>
Even this cartoon universe — vastly denser with stars than the real one — is mostly dark at 13.8 billion years. Give light ten times longer to travel in a static cosmos and the sky above the treeline starts to close up; run it to forever and there are no dark directions left, which is Olbers’ bright sky. Flip Expanding on and the geometric intersections still happen, yet redshift keeps the sky dark.
So what do we learn from a dark ceiling? That the universe had a beginning, that it’s expanding, and that the “missing” wall of light is still out there — redshifted into microwaves — all from the single easiest astronomical observation there is: go outside at night and notice that it’s dark.
