Star Map
by sloprun seed
2026-08-14
0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
A whole night sky in a box: 533 real stars drawn at their true positions and brightnesses, wired to 66 constellation figures and a scattering of clusters and nebulae. Drag to look around, slide the clock from dusk to dawn and watch the whole dome wheel overhead, or move yourself from London to Sydney and see the sky tip right over — things that never set from home sink below the horizon, and the Southern Cross climbs into view.
It opens on a January evening with Orion straight ahead, so there is nothing to set up. Try the time slider first, then hit Sydney and pick Crux from "turn and look at" to watch a constellation that is simply unreachable from the north. Every star is hoverable for its name and magnitude, and clickable for a sentence about what it actually is.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Star Map — sloprun.dev</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
:root[data-theme="light"] {
--bg: #F6F6F3; --surface: #FFFFFF; --ink: #1A1D21; --muted: #5A6068;
--line: #E3E2DC; --accent: #C05600; --accent-soft: #FFF3E6;
--run: #1A7F37; --run-soft: #E7F4EA; --danger: #C93C37;
}
/* ── end tokens ───────────────────────────────────────────────────────────── */
/* Demo-local additions. Two reasons these exist: scrims need alpha (tokens are
opaque hex), and the sky itself is dark in BOTH themes — a night sky drawn on
white paper is a lie. Light theme gets a deep slate chart-sky, dark theme goes
nearly black. The frame around it is pure tokens. */
:root {
color-scheme: light;
--scrim: rgba(246,246,243,.9);
--shadow: 0 1px 2px rgba(26,29,33,.05);
--sky-hi: #1B2029; /* sky near the horizon */
--sky-lo: #0E1117; /* sky overhead */
--ground: #090B0F;
--ground-line: #3B424F;
--sky-line: #D9822B; /* constellation lines, on the dark sky */
--sky-label: #A8B0BD;
--sky-ink: #EFEDE8;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--scrim: rgba(20,22,26,.9);
--shadow: 0 1px 2px rgba(0,0,0,.35);
--sky-hi: #12161D; --sky-lo: #06080B; --ground: #040508;
--ground-line: #2D323C; --sky-line: #FFB454; --sky-label: #8F97A5;
--sky-ink: #F2F0EB;
}
}
:root[data-theme="light"] {
color-scheme: light;
--scrim: rgba(246,246,243,.9);
--shadow: 0 1px 2px rgba(26,29,33,.05);
--sky-hi: #1B2029; --sky-lo: #0E1117; --ground: #090B0F;
--ground-line: #3B424F; --sky-line: #D9822B; --sky-label: #A8B0BD;
--sky-ink: #EFEDE8;
}
:root[data-theme="dark"] {
color-scheme: dark;
--scrim: rgba(20,22,26,.9);
--shadow: 0 1px 2px rgba(0,0,0,.35);
--sky-hi: #12161D; --sky-lo: #06080B; --ground: #040508;
--ground-line: #2D323C; --sky-line: #FFB454; --sky-label: #8F97A5;
--sky-ink: #F2F0EB;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100%; overflow-x: hidden; }
.wrap {
max-width: 1180px; margin: 0 auto;
padding: 22px 16px 26px;
display: flex; flex-direction: column; gap: 16px;
}
h1 { margin: 0 0 6px; font-size: clamp(21px, 3.6vw, 29px); letter-spacing: -.021em; line-height: 1.15; }
.lede { margin: 0; max-width: 70ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }
.stage { display: grid; grid-template-columns: minmax(0,1fr) 302px; gap: 16px; align-items: start; }
@media (max-width: 940px) { .stage { grid-template-columns: minmax(0,1fr); } }
.left { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.under { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 16px; align-items: start; }
@media (max-width: 620px) { .under { grid-template-columns: minmax(0,1fr); } }
.field {
position: relative; overflow: hidden;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--sky-lo);
aspect-ratio: 16 / 10; box-shadow: var(--shadow);
}
@media (max-width: 940px) { .field { aspect-ratio: 4 / 3; } }
@media (max-width: 560px) { .field { aspect-ratio: 5 / 6; } }
canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; }
canvas.grabbing { cursor: grabbing; }
canvas:focus { outline: none; }
canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
.hint {
position: absolute; left: 10px; bottom: 10px; pointer-events: none;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .01em;
color: var(--sky-label); background: rgba(8,10,14,.72);
border: 1px solid var(--ground-line); border-radius: 999px; padding: 5px 10px;
transition: opacity .5s ease; max-width: calc(100% - 116px);
}
.hint.gone { opacity: 0; }
.zoomers { position: absolute; right: 10px; bottom: 10px; display: flex; gap: 6px; }
.zoomers button {
width: 32px; height: 32px; padding: 0; font-size: 16px; line-height: 1;
background: rgba(8,10,14,.72); color: var(--sky-label);
border-color: var(--ground-line); border-radius: 8px;
}
.zoomers button:hover:not(:disabled) { color: var(--sky-line); border-color: var(--sky-line); }
.tip {
position: absolute; z-index: 4; pointer-events: none; max-width: 210px;
background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
padding: 7px 9px; box-shadow: 0 4px 14px rgba(0,0,0,.28);
transform: translateY(-50%); /* left/top are computed and clamped in JS */
}
.tip[hidden] { display: none; }
.tip .n { font-size: 12.5px; font-weight: 600; line-height: 1.25; }
.tip .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-top: 3px; letter-spacing: .02em; }
.panel { display: flex; flex-direction: column; gap: 12px; }
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 12px 13px; box-shadow: var(--shadow);
}
.card h2 {
margin: 0 0 10px; font-family: var(--font-mono); font-size: 10.5px;
letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 600;
}
.stats { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 11px 10px; }
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat .v {
font-family: var(--font-mono); font-size: 19px; line-height: 1.05;
font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.stat .v .u { font-size: 11px; color: var(--muted); margin-left: 1px; letter-spacing: 0; }
.stat .k {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.pill {
display: inline-flex; align-items: center; gap: 7px; margin-top: 12px;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .03em;
padding: 4px 10px 4px 8px; border-radius: 999px; line-height: 1.35;
border: 1px solid var(--line); color: var(--muted); background: var(--bg);
max-width: 100%;
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.pill.up { color: var(--run); background: var(--run-soft); border-color: transparent; }
.pill.down { color: var(--accent); background: var(--accent-soft); border-color: transparent; }
.ctl { display: block; }
.ctl-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
.ctl-top label { font-size: 13px; }
.ctl-top .val { font-family: var(--font-mono); font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.scale { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 9.5px; color: var(--muted); margin-top: 1px; }
input[type=range] {
-webkit-appearance: none; appearance: none; display: block;
width: 100%; height: 20px; margin: 0; background: transparent; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 15px; height: 15px; margin-top: -5.5px;
border-radius: 50%; background: var(--accent); border: 2px solid var(--surface);
box-shadow: 0 0 0 1px var(--line);
}
input[type=range]::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-moz-range-thumb {
width: 13px; height: 13px; border-radius: 50%; background: var(--accent);
border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--line);
}
input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
button {
font: inherit; font-size: 13px; font-family: var(--font-sans);
color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 8px;
padding: 7px 11px; cursor: pointer; line-height: 1.2;
transition: border-color .12s ease, color .12s ease, background .12s ease;
}
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:disabled { opacity: .42; cursor: not-allowed; }
button.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
button.primary:hover:not(:disabled) { color: var(--surface); filter: brightness(1.07); }
button.wide { width: 100%; }
select {
font: inherit; font-size: 13px; font-family: var(--font-sans); width: 100%;
color: var(--ink); background: var(--surface); border: 1px solid var(--line);
border-radius: 8px; padding: 7px 9px; cursor: pointer;
}
select:hover { border-color: var(--accent); }
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.row { display: flex; flex-wrap: wrap; gap: 7px; }
.row > button { flex: 1 1 auto; }
.places { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 7px; }
.places button { font-size: 12.5px; padding: 6px 8px; text-align: left; }
.places button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.places button .lat { display: block; font-family: var(--font-mono); font-size: 9.5px; color: var(--muted); margin-top: 1px; }
.places button[aria-pressed="true"] .lat { color: var(--accent); }
.switch { display: flex; align-items: center; gap: 9px; cursor: pointer; font-size: 13px; margin-bottom: 9px; }
.switch:last-of-type { margin-bottom: 0; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.track {
width: 34px; height: 19px; border-radius: 999px; background: var(--line);
border: 1px solid var(--line); position: relative; flex: none; transition: background .15s ease;
}
.track::after {
content: ""; position: absolute; top: 1.5px; left: 2px; width: 14px; height: 14px;
border-radius: 50%; background: var(--surface); transition: transform .15s ease;
box-shadow: 0 1px 2px rgba(0,0,0,.25);
}
.switch input:checked + .track { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .track::after { transform: translateX(15px); }
.switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch input:disabled + .track { opacity: .4; }
.switch input:disabled ~ span:last-child { color: var(--muted); }
.sel { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 10px; }
.sel .name { font-size: 14.5px; font-weight: 600; line-height: 1.25; }
.sel .meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-top: 4px; letter-spacing: .02em; line-height: 1.55; }
.sel .fact { font-size: 12.5px; line-height: 1.5; margin: 7px 0 0; color: var(--muted); }
.sel .fact b { color: var(--ink); font-weight: 600; }
.sel .empty { font-size: 12.5px; color: var(--muted); line-height: 1.5; margin: 0; }
.sel .acts { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
.sel .acts button { font-size: 12px; padding: 5px 9px; }
.cap { margin: 9px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); }
.cap b { color: var(--ink); font-weight: 600; }
.cap .n { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink); }
.keys { margin: 10px 0 0; font-family: var(--font-mono); font-size: 10.5px; line-height: 1.75; color: var(--muted); }
kbd {
font-family: var(--font-mono); font-size: 10px; border: 1px solid var(--line);
border-radius: 4px; padding: 1px 4px; color: var(--ink); background: var(--bg);
}
footer { font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .05em; padding-top: 2px; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Star Map</h1>
<p class="lede">
A whole night sky in a box. <b>Drag to look around</b>, slide the clock to watch everything
wheel overhead, and <b>tap any star</b> to find out what you were looking at. It opens on a
January evening with Orion straight ahead — the easiest thing in the sky to learn first.
</p>
</header>
<main class="stage">
<div class="left">
<div class="field" id="field">
<canvas id="sky" tabindex="0"
aria-label="Night sky map. Drag or use the arrow keys to look around, plus and minus to zoom, and Enter to name the star nearest the middle of the view.">
A drawing of the night sky. Your browser does not support canvas.
</canvas>
<div class="hint" id="hint">drag the sky to look around</div>
<div class="zoomers">
<button type="button" id="zoomOut" aria-label="Zoom out, see more sky">−</button>
<button type="button" id="zoomIn" aria-label="Zoom in, see less sky">+</button>
</div>
<div class="tip" id="tip" hidden aria-hidden="true">
<div class="n" id="tipName"></div>
<div class="d" id="tipMeta"></div>
</div>
</div>
<div class="under">
<section class="card" aria-label="Where you are">
<h2>Where you are</h2>
<div class="places" id="places" role="group" aria-label="Viewing place"></div>
<p class="cap">Move south and the whole sky tips over: things that never set from London sink below the horizon, and the Southern Cross climbs into view.</p>
</section>
<section class="card" aria-label="What to show">
<h2>Show</h2>
<label class="switch" for="tLines"><input type="checkbox" id="tLines" checked><span class="track" aria-hidden="true"></span><span>Constellation lines</span></label>
<label class="switch" for="tLabels"><input type="checkbox" id="tLabels" checked><span class="track" aria-hidden="true"></span><span>Names</span></label>
<label class="switch" for="tDso"><input type="checkbox" id="tDso" checked><span class="track" aria-hidden="true"></span><span>Clusters & nebulae</span></label>
<label class="switch" for="tMw"><input type="checkbox" id="tMw" checked><span class="track" aria-hidden="true"></span><span>Milky Way</span></label>
<label class="switch" for="tTwinkle"><input type="checkbox" id="tTwinkle" checked><span class="track" aria-hidden="true"></span><span>Twinkle</span></label>
<p class="keys">
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd> look around · <kbd>+</kbd><kbd>−</kbd> zoom<br>
<kbd>Enter</kbd> name what is in the middle
</p>
</section>
</div>
</div>
<div class="panel">
<section class="card" aria-label="Sky readout">
<h2>Readout</h2>
<div class="stats">
<div class="stat"><span class="v" id="sClock">21:30</span><span class="k">local time</span></div>
<div class="stat"><span class="v" id="sUp">—</span><span class="k">stars above horizon</span></div>
<div class="stat"><span class="v" id="sLook">—</span><span class="k">looking</span></div>
<div class="stat"><span class="v" id="sFov">—</span><span class="k">view width</span></div>
</div>
<span class="pill" id="pill"><span class="dot"></span><span id="pillText">—</span></span>
<p class="cap"><span class="n" id="capN">0</span> catalogued stars, each drawn at its real position and brightness for the hour, month and place you pick. The faint dust behind them stands in for the millions too dim to name.</p>
</section>
<section class="card" aria-label="Time of night">
<h2>Tonight</h2>
<div class="ctl">
<div class="ctl-top">
<label for="time">Time of night</label><span class="val" id="timeV">21:30</span>
</div>
<input type="range" id="time" min="1080" max="1800" step="1" value="1290" aria-valuetext="21:30">
<div class="scale" aria-hidden="true"><span>18:00</span><span>midnight</span><span>06:00</span></div>
</div>
<div class="row" style="margin-top:11px">
<button type="button" class="primary wide" id="playBtn">Pause the sky</button>
</div>
<div class="ctl" style="margin-top:12px">
<div class="ctl-top"><label for="month">Month</label><span class="val">15th</span></div>
<select id="month">
<option value="1" selected>January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</div>
<p class="cap">Same clock time, six months apart, and you are facing the opposite half of the galaxy.</p>
</section>
<section class="card" aria-label="Find something">
<h2>Find</h2>
<label for="goto" class="sr-only">Turn the view toward a constellation</label>
<select id="goto">
<option value="">Turn and look at…</option>
</select>
<div class="sel" id="selBox">
<p class="empty" id="selEmpty">Tap a star to name it — or press <kbd>Enter</kbd> with the sky focused to name whatever sits in the middle of the view.</p>
<div id="selInfo" hidden>
<div class="name" id="selName"></div>
<div class="meta" id="selMeta"></div>
<p class="fact" id="selFact"></p>
<div class="acts">
<button type="button" id="selCentre">Centre on it</button>
<button type="button" id="selJump" hidden></button>
<button type="button" id="selClear">Clear</button>
</div>
</div>
</div>
</section>
</div>
</main>
<footer>demo · sloprun.dev</footer>
</div>
<div class="sr-only" role="status" aria-live="polite" id="live"></div>
<script>
(function () {
"use strict";
/* ═══════════════════════════════════════════════════════════════════════════
CATALOGUE
Real coordinates. Right ascension in hours, declination in degrees, epoch
J2000; magnitude is the usual backwards scale where smaller = brighter.
Packed as "name,greek,ra,dec,mag,spectral;" per star to keep the file small.
Line figures index into each constellation's own star list, so a figure is
just a list of pairs. "Peg:0" reaches across to another constellation for
the handful of shapes that share a corner star.
═════════════════════════════════════════════════════════════════════════ */
var FULL = {
Ori:"Orion", CMa:"Canis Major", CMi:"Canis Minor", Tau:"Taurus", Gem:"Gemini",
Aur:"Auriga", UMa:"Ursa Major", UMi:"Ursa Minor", Cas:"Cassiopeia", Cep:"Cepheus",
Per:"Perseus", And:"Andromeda", Peg:"Pegasus", Cyg:"Cygnus", Lyr:"Lyra",
Aql:"Aquila", Sco:"Scorpius", Sgr:"Sagittarius", Leo:"Leo", Boo:"Bootes",
Vir:"Virgo", Cnc:"Cancer", CVn:"Canes Venatici", CrB:"Corona Borealis",
Her:"Hercules", Dra:"Draco", Ari:"Aries", Psc:"Pisces", Aqr:"Aquarius",
Cap:"Capricornus", Cet:"Cetus", Eri:"Eridanus", PsA:"Piscis Austrinus",
Gru:"Grus", Cen:"Centaurus", Cru:"Crux", Car:"Carina", Vel:"Vela", Pup:"Puppis",
Hya:"Hydra", Crv:"Corvus", Lib:"Libra", Oph:"Ophiuchus", Ser:"Serpens",
Del:"Delphinus", Sge:"Sagitta", Tri:"Triangulum", Lep:"Lepus", Mon:"Monoceros",
Col:"Columba", Ara:"Ara", Lup:"Lupus", TrA:"Triangulum Australe", Pav:"Pavo",
Phe:"Phoenix", Hyi:"Hydrus", Tuc:"Tucana", Mus:"Musca", CrA:"Corona Australis",
Sct:"Scutum", Lyn:"Lynx", Com:"Coma Berenices", Lac:"Lacerta",
Cam:"Camelopardalis", Crt:"Crater", Ret:"Reticulum", Ant:"Antlia", Pyx:"Pyxis",
For:"Fornax", Scl:"Sculptor", Sex:"Sextans", Vul:"Vulpecula", Equ:"Equuleus",
Cae:"Caelum", Nor:"Norma", Cir:"Circinus", Aps:"Apus", Vol:"Volans",
Cha:"Chamaeleon", Dor:"Dorado", Hor:"Horologium", Oct:"Octans", Ind:"Indus",
Tel:"Telescopium", Mic:"Microscopium", Men:"Mensa", LMi:"Leo Minor"
};
var PACK = {
Ori:"Betelgeuse,α,5.919,7.407,0.45,M;Rigel,β,5.242,-8.202,0.18,B;Bellatrix,γ,5.418,6.350,1.64,B;Mintaka,δ,5.533,-0.299,2.25,O;Alnilam,ε,5.604,-1.202,1.69,B;Alnitak,ζ,5.679,-1.943,1.74,O;Saiph,κ,5.796,-9.670,2.07,B;Meissa,λ,5.585,9.934,3.39,O;,η,5.408,-2.397,3.36,B;Hatysa,ι,5.590,-5.910,2.77,O;,τ,5.298,-6.844,3.60,B;Tabit,π3,4.831,6.961,3.19,F;,π4,4.851,5.605,3.69,B;,π2,4.799,8.900,4.35,A;,π5,4.904,2.441,3.72,B;,π1,4.752,10.150,4.65,A;,π6,4.977,1.714,4.47,K;,μ,6.038,9.647,4.12,A;,χ1,5.911,20.276,4.39,G;,ν,6.132,14.768,4.42,B;,ξ,6.198,14.209,4.45,B",
CMa:"Sirius,α,6.752,-16.716,-1.44,A;Mirzam,β,6.378,-17.956,1.98,B;Wezen,δ,7.140,-26.393,1.83,F;Adhara,ε,6.977,-28.972,1.50,B;Aludra,η,7.402,-29.303,2.45,B;Furud,ζ,6.339,-30.063,3.02,B;,σ,7.028,-27.935,3.47,K;,ο2,7.050,-23.833,3.02,B;Muliphein,γ,7.063,-15.633,4.11,B;,ι,6.936,-17.054,4.37,B;,θ,6.913,-12.038,4.07,K",
CMi:"Procyon,α,7.655,5.225,0.40,F;Gomeisa,β,7.452,8.289,2.89,B;,γ,7.469,8.925,4.32,K",
Tau:"Aldebaran,α,4.599,16.509,0.87,K;Elnath,β,5.438,28.608,1.65,B;,ζ,5.627,21.143,3.00,B;,θ2,4.477,15.871,3.40,A;,γ,4.330,15.628,3.65,G;,δ,4.382,17.542,3.76,K;,ε,4.478,19.180,3.53,K;,λ,4.011,12.490,3.47,B;,ξ,3.454,9.733,3.74,B;,ο,3.412,9.029,3.60,G;Alcyone,η,3.791,24.105,2.85,B;Atlas,27,3.819,24.053,3.62,B;Electra,17,3.748,24.113,3.70,B;Maia,20,3.767,24.367,3.87,B;Merope,23,3.772,23.948,4.14,B;Taygeta,19,3.752,24.470,4.30,B;,ν,4.052,5.989,3.91,A",
Gem:"Pollux,β,7.755,28.026,1.16,K;Castor,α,7.577,31.888,1.58,A;Alhena,γ,6.629,16.399,1.93,A;Wasat,δ,7.335,21.982,3.53,F;Mebsuta,ε,6.732,25.131,3.06,G;Tejat,μ,6.383,22.514,2.87,M;Propus,η,6.248,22.507,3.28,M;,ξ,6.755,12.896,3.36,F;,κ,7.740,24.398,3.57,G;,λ,7.303,16.540,3.58,A;,ζ,7.068,20.570,3.79,F;,ι,7.429,27.798,3.79,G;,τ,7.190,30.245,4.41,K;,θ,6.883,33.961,3.60,A",
Aur:"Capella,α,5.278,45.998,0.08,G;Menkalinan,β,5.995,44.947,1.90,A;Mahasim,θ,5.995,37.213,2.62,A;Hassaleh,ι,4.950,33.166,2.69,K;Almaaz,ε,5.033,43.823,3.03,F;,η,5.108,41.234,3.17,B;,ζ,5.041,41.076,3.75,K;,δ,5.993,54.285,3.72,K",
UMa:"Dubhe,α,11.062,61.751,1.81,K;Merak,β,11.031,56.382,2.34,A;Phecda,γ,11.897,53.695,2.41,A;Megrez,δ,12.257,57.033,3.32,A;Alioth,ε,12.900,55.960,1.76,A;Mizar,ζ,13.399,54.925,2.23,A;Alkaid,η,13.792,49.313,1.85,B;Alcor,80,13.421,54.988,3.99,A;Muscida,ο,8.504,60.718,3.35,G;,ψ,11.161,44.499,3.01,K;,μ,10.372,41.500,3.05,M;,λ,10.285,42.914,3.45,A;,θ,9.547,51.677,3.17,F;,ι,8.986,48.042,3.14,A;,κ,9.060,47.157,3.60,A;,υ,9.849,59.039,3.78,F;,χ,11.767,47.779,3.71,K;,ν,11.307,33.094,3.49,K;,ξ,11.303,31.529,3.79,G;,23,11.517,63.062,3.67,F",
UMi:"Polaris,α,2.530,89.264,1.97,F;Kochab,β,14.845,74.156,2.07,K;Pherkad,γ,15.345,71.834,3.00,A;,ε,16.766,82.037,4.21,G;,ζ,15.734,77.794,4.29,A;,δ,17.537,86.586,4.35,A;,η,16.291,75.755,4.95,F",
Cas:"Schedar,α,0.675,56.537,2.24,K;Caph,β,0.153,59.150,2.28,F;,γ,0.945,60.717,2.47,B;Ruchbah,δ,1.430,60.235,2.68,A;Segin,ε,1.906,63.670,3.35,B",
Cep:"Alderamin,α,21.310,62.586,2.45,A;Alfirk,β,21.478,70.561,3.23,B;Errai,γ,23.656,77.632,3.21,K;,ζ,22.181,58.201,3.35,K;,ι,22.828,66.200,3.52,K;,δ,22.482,58.415,4.07,F;,ε,22.250,57.043,4.19,F;,η,20.755,61.839,3.43,K",
Per:"Mirfak,α,3.405,49.861,1.79,F;Algol,β,3.136,40.956,2.09,B;,ζ,3.902,31.884,2.85,B;,ε,3.964,40.010,2.89,B;,γ,3.079,53.506,2.93,G;,δ,3.715,47.788,3.01,B;,ρ,3.086,38.840,3.39,M;,η,2.845,55.895,3.76,K;,ν,3.152,42.579,3.77,F;,ξ,3.980,35.791,4.04,O",
And:"Alpheratz,α,0.140,29.091,2.06,B;Mirach,β,1.162,35.621,2.06,M;Almach,γ,2.065,42.330,2.10,K;,δ,0.655,30.861,3.27,K;,μ,0.949,38.499,3.86,A;,ν,0.831,41.079,4.53,B;,π,0.612,33.719,4.34,B",
Peg:"Markab,α,23.079,15.205,2.48,B;Scheat,β,23.063,28.083,2.42,M;Algenib,γ,0.221,15.184,2.83,B;Enif,ε,21.736,9.875,2.38,K;Matar,η,22.717,30.221,2.94,G;Homam,ζ,22.691,10.831,3.40,B;Biham,θ,22.170,6.198,3.53,A;,μ,22.833,24.602,3.48,G;,ι,22.117,25.345,3.76,F",
Cyg:"Deneb,α,20.690,45.280,1.25,A;Albireo,β,19.512,27.960,3.08,K;Sadr,γ,20.371,40.257,2.23,F;,δ,19.750,45.131,2.87,B;Gienah,ε,20.770,33.970,2.48,K;,ζ,21.216,30.227,3.20,G;,ι,19.497,51.730,3.79,A;,κ,19.286,53.368,3.77,G",
Lyr:"Vega,α,18.615,38.784,0.03,A;Sheliak,β,18.834,33.363,3.45,B;Sulafat,γ,18.982,32.690,3.24,B;,δ2,18.907,36.899,4.30,M;,ζ,18.746,37.605,4.36,A;,ε,18.737,39.670,4.60,A",
Aql:"Altair,α,19.846,8.868,0.76,A;Tarazed,γ,19.771,10.613,2.72,K;Alshain,β,19.921,6.407,3.71,G;,ζ,19.090,13.863,2.99,A;,θ,20.188,-0.821,3.23,B;,δ,19.425,3.115,3.36,F;,λ,19.100,-4.883,3.43,B;,η,19.879,1.006,3.90,F",
Sco:"Antares,α,16.490,-26.432,1.06,M;Shaula,λ,17.560,-37.104,1.62,B;Sargas,θ,17.622,-42.998,1.86,F;Dschubba,δ,16.005,-22.622,2.29,B;Wei,ε,16.836,-34.293,2.29,K;Girtab,κ,17.708,-39.030,2.39,B;Acrab,β,16.091,-19.805,2.56,B;Lesath,υ,17.505,-37.296,2.69,B;,τ,16.598,-28.216,2.82,B;,π,15.981,-26.114,2.89,B;Alniyat,σ,16.353,-25.593,2.89,B;,ι1,17.793,-40.127,3.03,F;,μ1,16.865,-38.048,3.08,B;,η,17.203,-43.239,3.32,F;,ρ,15.951,-29.214,3.87,B;,ν,16.199,-19.461,4.00,B;,ζ2,16.911,-42.362,3.62,K",
Sgr:"Kaus Australis,ε,18.403,-34.385,1.79,B;Nunki,σ,18.921,-26.297,2.05,B;Ascella,ζ,19.044,-29.880,2.60,A;Kaus Media,δ,18.350,-29.828,2.70,K;Kaus Borealis,λ,18.466,-25.422,2.81,K;,φ,18.762,-26.991,3.17,B;,τ,19.116,-27.670,3.32,K;,γ2,18.097,-30.424,2.99,K;,η,18.293,-36.762,3.11,M;,π,19.162,-21.024,2.89,F;,ξ2,18.958,-21.107,3.51,K;,ο,19.077,-21.742,3.77,K",
Leo:"Regulus,α,10.140,11.967,1.36,B;Denebola,β,11.818,14.572,2.14,A;Algieba,γ,10.333,19.841,2.01,K;Zosma,δ,11.235,20.524,2.56,A;Chort,θ,11.237,15.430,3.33,A;,ε,9.764,23.774,2.98,G;,ζ,10.278,23.417,3.44,F;,μ,9.880,26.007,3.88,K;,η,10.122,16.763,3.48,A;,ο,9.685,9.892,3.52,F;,ρ,10.538,9.310,3.85,B;,ι,11.399,10.530,3.94,F;,σ,11.352,6.030,4.05,B",
Boo:"Arcturus,α,14.261,19.182,-0.05,K;Nekkar,β,15.032,40.390,3.49,G;Seginus,γ,14.535,38.308,3.03,A;Izar,ε,14.750,27.074,2.35,K;,δ,15.258,33.315,3.47,G;,ρ,14.532,30.371,3.58,K;Muphrid,η,13.911,18.398,2.68,G;,ζ,14.685,13.728,3.78,A;,τ,13.788,17.457,4.50,F",
Vir:"Spica,α,13.420,-11.161,0.98,B;Vindemiatrix,ε,13.036,10.959,2.83,G;Porrima,γ,12.694,-1.449,2.74,F;,ζ,13.578,-0.596,3.37,A;Auva,δ,12.927,3.398,3.38,M;Zavijava,β,11.845,1.765,3.59,F;Zaniah,η,12.333,-0.667,3.89,A;Syrma,ι,14.269,-6.001,4.07,F;,μ,14.716,-5.658,3.87,F;,109,14.779,1.893,3.72,A",
Cnc:"Altarf,β,8.275,9.186,3.53,K;Asellus Australis,δ,8.745,18.154,3.94,K;Asellus Borealis,γ,8.721,21.469,4.66,A;Acubens,α,8.975,11.858,4.25,A;,ι,8.778,28.760,4.02,G",
CVn:"Cor Caroli,α2,12.934,38.318,2.89,A;Chara,β,12.562,41.357,4.24,G",
CrB:"Alphecca,α,15.578,26.715,2.22,A;Nusakan,β,15.464,29.106,3.66,F;,γ,15.711,26.296,3.81,A;,θ,15.548,31.359,4.14,B;,ε,15.960,26.878,4.13,K;,ι,16.024,29.851,4.98,A;,δ,15.821,26.068,4.63,G",
Her:"Kornephoros,β,16.503,21.490,2.77,G;,ζ,16.688,31.603,2.81,G;Rasalgethi,α,17.244,14.390,3.35,M;Sarin,δ,17.250,24.839,3.12,A;,π,17.251,36.809,3.16,K;,η,16.715,38.922,3.53,G;,μ,17.774,27.720,3.42,G;,ξ,17.958,29.248,3.70,K;,ε,17.005,30.926,3.92,A;,γ,16.362,19.153,3.75,A;,ο,18.126,28.762,3.83,A;,ι,17.658,46.006,3.80,B;,θ,17.945,37.250,3.86,K;,τ,16.331,46.313,3.89,B",
Dra:"Eltanin,γ,17.943,51.489,2.24,K;Rastaban,β,17.507,52.301,2.79,G;Altais,δ,19.209,67.661,3.07,G;,ζ,17.146,65.714,3.17,B;Athebyne,η,16.399,61.514,2.73,G;Edasich,ι,15.415,58.966,3.29,K;,χ,18.352,72.733,3.55,F;,ξ,17.892,56.873,3.75,K;Thuban,α,14.073,64.376,3.65,A;Giausar,λ,11.531,69.331,3.84,M;,κ,12.558,69.788,3.87,B;,ε,19.803,70.268,3.83,G;,ν,17.542,55.183,4.88,A;,θ,16.033,58.565,4.01,F",
Ari:"Hamal,α,2.119,23.462,2.01,K;Sheratan,β,1.911,20.808,2.64,A;Mesarthim,γ,1.892,19.294,3.86,B;Bharani,41,2.833,27.261,3.61,B",
Psc:"Kullat Nunu,η,1.525,15.346,3.62,G;,γ,23.286,3.282,3.69,G;,ω,23.994,6.863,4.03,F;,ι,23.657,5.627,4.13,F;Alrescha,α,2.033,2.764,3.82,A;,ε,1.045,7.890,4.27,K;,δ,0.811,7.585,4.44,K;,λ,23.703,1.780,4.49,A;,κ,23.470,1.256,4.94,A;,θ,23.466,6.379,4.28,K;,ν,1.687,5.487,4.44,K",
Aqr:"Sadalsuud,β,21.526,-5.571,2.90,G;Sadalmelik,α,22.096,-0.320,2.95,G;Skat,δ,22.911,-15.821,3.27,A;,ζ,22.481,-0.020,3.65,F;Sadachbia,γ,22.361,-1.387,3.84,A;,λ,22.877,-7.580,3.73,M;,ε,20.795,-9.496,3.77,A;,η,22.599,-0.117,4.02,B;,θ,22.281,-7.783,4.16,G;,τ,22.827,-13.593,4.05,K;,φ,23.240,-6.049,4.22,M",
Cap:"Deneb Algedi,δ,21.784,-16.127,2.85,A;Dabih,β,20.351,-14.781,3.05,K;Algedi,α2,20.300,-12.545,3.57,G;Nashira,γ,21.668,-16.662,3.68,A;,ζ,21.444,-22.411,3.74,G;,ω,20.865,-26.919,4.11,M;,θ,21.098,-17.233,4.07,A;,ψ,20.769,-25.271,4.13,F;,ι,21.371,-16.834,4.27,G",
Cet:"Diphda,β,0.726,-17.987,2.04,K;Menkar,α,3.038,4.090,2.53,M;,η,1.143,-10.182,3.45,K;Kaffaljidhma,γ,2.722,3.236,3.47,A;,τ,1.734,-15.938,3.50,G;,ι,0.323,-8.824,3.56,K;,θ,1.400,-8.183,3.60,K;,ζ,1.857,-10.335,3.73,K;Mira,ο,2.322,-2.977,3.50,M;,δ,2.657,0.328,4.07,B;,μ,2.749,10.114,4.27,F;,ξ2,2.786,8.460,4.28,A",
Eri:"Achernar,α,1.629,-57.237,0.45,B;Cursa,β,5.131,-5.086,2.78,A;Acamar,θ,2.971,-40.305,2.88,A;Zaurak,γ,3.968,-13.509,2.95,M;Rana,δ,3.721,-9.765,3.54,K;,ε,3.548,-9.458,3.73,K;,τ4,3.323,-21.758,3.69,M;,υ4,4.298,-33.798,3.56,B;,χ,1.977,-51.612,3.70,G;,φ,2.276,-51.512,3.56,B;,η,2.938,-8.898,3.89,K;,ν,4.601,-3.352,3.93,B;,ο1,4.198,-6.838,4.04,F",
PsA:"Fomalhaut,α,22.961,-29.622,1.16,A;,ε,22.677,-27.044,4.17,B;,δ,22.933,-32.540,4.21,G;,β,22.526,-32.346,4.29,A;,ι,21.749,-33.026,4.34,B",
Gru:"Alnair,α,22.137,-46.961,1.74,B;Tiaki,β,22.711,-46.885,2.07,M;,γ,21.899,-37.365,3.00,B;,ε,22.804,-51.317,3.49,A;,δ1,22.487,-43.496,3.97,G;,ζ,23.028,-52.754,4.11,K",
Cen:"Rigil Kentaurus,α,14.660,-60.834,-0.01,G;Hadar,β,14.064,-60.373,0.61,B;Menkent,θ,14.111,-36.370,2.06,K;,γ,12.692,-48.958,2.20,A;,ε,13.665,-53.466,2.30,B;,η,14.596,-42.158,2.31,B;,ζ,13.926,-47.288,2.55,B;,δ,12.140,-50.722,2.60,B;,ι,13.343,-36.712,2.75,A;,ν,13.822,-41.688,3.41,B;,μ,13.827,-42.474,3.04,B;,κ,14.983,-42.104,3.13,B",
Cru:"Acrux,α,12.443,-63.099,0.77,B;Mimosa,β,12.795,-59.689,1.25,B;Gacrux,γ,12.519,-57.113,1.59,M;Imai,δ,12.253,-58.749,2.79,B;Ginan,ε,12.356,-60.401,3.59,K",
Car:"Canopus,α,6.399,-52.696,-0.62,F;Miaplacidus,β,9.220,-69.717,1.67,A;Avior,ε,8.375,-59.510,1.86,K;Aspidiske,ι,9.285,-59.275,2.21,A;,θ,10.716,-64.394,2.76,B;,υ,9.785,-65.072,2.97,A;,ω,10.229,-70.038,3.29,B;,p,10.535,-61.685,3.30,B",
Vel:"Suhail,γ2,8.158,-47.337,1.75,O;Alsephina,δ,8.745,-54.709,1.96,A;,λ,9.133,-43.433,2.21,K;,κ,9.368,-55.011,2.47,B;,μ,10.778,-49.420,2.69,G;,φ,9.947,-54.568,3.52,B;,ψ,9.512,-40.467,3.60,F",
Pup:"Naos,ζ,8.060,-40.003,2.21,O;,π,7.290,-37.097,2.71,K;,ρ,8.126,-24.304,2.83,F;,τ,6.833,-50.614,2.94,K;,ν,6.615,-43.196,3.17,B;,σ,7.487,-43.301,3.25,K;,ξ,7.821,-24.860,3.34,G",
Hya:"Alphard,α,9.460,-8.659,1.99,K;,γ,13.315,-23.171,2.99,G;,ζ,8.925,5.946,3.11,K;,ν,10.827,-16.194,3.11,K;,π,14.107,-26.682,3.27,K;,ε,8.779,6.419,3.38,G;,ξ,11.553,-31.858,3.54,G;,λ,10.181,-12.354,3.61,K;,μ,10.436,-16.836,3.81,K;,θ,9.239,2.314,3.88,A;,ι,9.665,-1.143,3.90,K;,δ,8.628,5.704,4.14,A;,σ,8.645,3.342,4.44,K;,η,8.720,3.399,4.30,B;,β,11.882,-33.908,4.28,B",
Crv:"Gienah Corvi,γ,12.263,-17.542,2.59,B;Kraz,β,12.573,-23.397,2.65,G;Algorab,δ,12.497,-16.516,2.95,B;,ε,12.168,-22.620,3.00,K;Alchiba,α,12.140,-24.729,4.02,F",
Lib:"Zubeneschamali,β,15.283,-9.383,2.61,B;Zubenelgenubi,α2,14.848,-16.042,2.75,A;,σ,15.068,-25.282,3.29,M;,υ,15.616,-28.135,3.58,K;,γ,15.596,-14.789,3.91,K",
Oph:"Rasalhague,α,17.582,12.560,2.08,A;Sabik,η,17.173,-15.725,2.43,A;,ζ,16.619,-10.567,2.54,O;Yed Prior,δ,16.239,-3.694,2.73,M;Cebalrai,β,17.725,4.567,2.76,K;,κ,16.962,9.375,3.19,K;Yed Posterior,ε,16.309,-4.693,3.23,G;,θ,17.372,-24.999,3.27,B;,ν,17.984,-9.774,3.32,K;,γ,17.798,2.707,3.75,A;,λ,16.515,1.984,3.82,A",
Ser:"Unukalhai,α,15.738,6.426,2.63,K;,μ,15.827,-3.430,3.53,A;,β,15.769,15.422,3.65,A;,ε,15.846,4.478,3.71,A;,δ,15.580,10.539,3.80,F;,η,18.355,-2.899,3.26,K;,ξ,17.626,-15.399,3.54,A;,γ,15.947,15.662,3.85,F;,θ,18.937,4.204,4.62,A;,ν,17.343,-12.846,4.32,A",
Del:"Rotanev,β,20.626,14.595,3.63,F;Sualocin,α,20.660,15.912,3.77,B;,γ,20.777,16.124,4.27,K;,δ,20.729,15.075,4.43,A;,ε,20.553,11.303,4.03,B",
Sge:",γ,19.979,19.492,3.47,M;,δ,19.789,18.534,3.82,M;Sham,α,19.668,18.014,4.37,G;,β,19.686,17.476,4.37,G",
Tri:"Mothallah,α,1.885,29.579,3.41,F;,β,2.160,34.987,3.00,A;,γ,2.289,33.847,4.01,A",
Lep:"Arneb,α,5.545,-17.822,2.58,F;Nihal,β,5.470,-20.759,2.84,G;,ε,5.093,-22.371,3.19,K;,μ,5.216,-16.206,3.31,B;,ζ,5.783,-14.822,3.55,A;,γ,5.744,-22.448,3.59,F;,δ,5.855,-20.879,3.81,G;,η,5.940,-14.168,3.71,F",
Mon:",β,6.483,-7.033,3.76,B;,α,7.687,-9.551,3.93,K;,γ,6.247,-6.275,3.98,K;,δ,7.198,-0.493,4.15,A;,ζ,8.145,-2.983,4.34,G;,ε,6.400,4.593,4.39,A",
Col:"Phact,α,5.661,-34.074,2.65,B;Wazn,β,5.849,-35.768,3.12,K;,δ,6.372,-33.436,3.85,G;,ε,5.510,-35.470,3.87,K",
Ara:",β,17.421,-55.530,2.85,K;,α,17.531,-49.876,2.95,B;,ζ,16.977,-55.990,3.13,K;,γ,17.418,-56.378,3.34,B;,δ,17.518,-60.684,3.62,B;,ε1,16.993,-53.160,4.06,K",
Lup:",α,14.699,-47.388,2.30,B;,β,14.976,-43.134,2.68,B;,γ,15.585,-41.167,2.78,B;,δ,15.356,-40.648,3.22,B;,ε,15.379,-44.690,3.37,B;,ζ,15.199,-52.099,3.41,G;,η,16.020,-38.396,3.41,B",
TrA:"Atria,α,16.811,-69.028,1.91,K;,β,15.919,-63.431,2.83,F;,γ,15.315,-68.679,2.87,A",
Pav:"Peacock,α,20.427,-56.735,1.94,B;,β,20.749,-66.203,3.42,A;,δ,20.145,-66.182,3.55,G;,ε,20.009,-72.911,3.96,A;,η,17.762,-64.724,3.62,K",
Phe:"Ankaa,α,0.438,-42.306,2.39,K;,β,1.101,-46.718,3.31,G;,γ,1.472,-43.318,3.41,K;,ε,0.157,-45.747,3.88,K",
Hyi:",α,1.980,-61.570,2.86,F;,β,0.429,-77.254,2.80,G;,γ,3.788,-74.239,3.24,M",
Tuc:",α,22.308,-60.260,2.86,K;,γ,23.290,-58.236,3.99,F;,ζ,0.335,-64.874,4.23,F",
Mus:",α,12.620,-69.136,2.69,B;,β,12.772,-68.108,3.05,B;,δ,13.033,-71.549,3.62,K;,γ,12.542,-72.133,3.84,B",
CrA:",α,19.158,-37.904,4.10,A;,β,19.167,-39.341,4.11,K;,γ,19.106,-37.063,4.20,F;,δ,19.148,-40.497,4.57,K",
Sct:",α,18.586,-8.244,3.85,K;,β,18.786,-4.747,4.22,G;,γ,18.492,-14.566,4.67,A",
Lyn:",α,9.351,34.393,3.13,K;,38,9.311,36.803,3.82,A;,31,8.386,43.188,4.25,K;,21,7.446,49.212,4.60,A",
Com:",β,13.198,27.878,4.26,G;,α,13.166,17.529,4.32,F;,γ,12.442,28.268,4.36,K",
Lac:",α,22.523,50.283,3.77,A;,β,22.394,52.229,4.42,G;,1,22.130,37.749,4.13,K",
Cam:",β,5.058,60.442,4.03,G;,α,4.900,66.343,4.29,O;,γ,3.839,71.332,4.59,A",
Crt:",δ,11.322,-14.779,3.56,K;,γ,11.412,-17.684,4.06,A;,α,10.996,-18.299,4.07,K;,β,11.194,-22.826,4.46,A",
Ret:",α,4.240,-62.474,3.35,G;,β,3.737,-64.807,3.85,K",
Ant:",α,10.452,-31.068,4.25,K",
Pyx:",α,8.726,-33.186,3.68,B",
For:",α,3.201,-28.988,3.80,F",
Scl:",α,0.977,-29.357,4.31,B",
Sex:",α,10.132,-0.372,4.48,A",
Vul:"Anser,α,19.478,24.665,4.44,M",
Equ:"Kitalpha,α,21.264,5.248,3.92,G",
Cae:",α,4.676,-41.864,4.44,F",
Nor:",γ2,16.328,-50.156,4.02,G",
Cir:",α,14.708,-64.975,3.19,F",
Aps:",α,14.798,-79.045,3.83,K",
Vol:",β,8.132,-66.137,3.77,K",
Cha:",α,8.309,-76.920,4.07,F",
Dor:",α,4.567,-55.045,3.27,A",
Hor:",α,4.230,-42.294,3.85,K",
Oct:",ν,21.691,-77.390,3.73,K",
Ind:",α,20.626,-47.291,3.11,K",
Tel:",α,18.449,-45.968,3.49,B",
Mic:",γ,21.017,-32.257,4.67,G",
Men:",α,6.170,-74.753,5.09,G",
LMi:",β,10.464,36.707,4.20,G"
};
var FIG = {
Ori:[[3,4],[4,5],[0,2],[2,3],[0,5],[5,6],[3,1],[0,7],[2,7],[4,9],[15,13],[13,11],[11,12],[12,14],[14,16],[2,11],[0,17],[17,19],[19,20]],
CMa:[[0,1],[0,7],[7,2],[2,4],[2,3],[3,5],[5,1],[0,8]],
CMi:[[0,1]],
Tau:[[6,5],[5,4],[4,3],[3,0],[0,2],[6,1],[4,7],[7,9],[9,8]],
Gem:[[1,12],[12,11],[11,0],[0,3],[3,10],[10,4],[4,6],[6,5],[3,9],[9,2],[2,7],[1,13],[13,4]],
Aur:[[0,1],[1,2],[2,"Tau:1"],["Tau:1",3],[3,0],[0,4],[4,6],[6,5],[0,7]],
UMa:[[0,1],[1,2],[2,3],[3,0],[3,4],[4,5],[5,6],[0,15],[15,8],[8,12],[12,13],[13,14],[1,10],[10,11],[2,16],[16,9],[9,17],[17,18]],
UMi:[[0,5],[5,3],[3,4],[4,1],[1,2],[2,6],[6,4]],
Cas:[[1,0],[0,2],[2,3],[3,4]],
Cep:[[0,1],[1,2],[2,4],[4,0],[0,3],[3,5]],
Per:[[7,4],[4,0],[0,5],[5,3],[3,9],[9,2],[0,8],[8,1],[1,6]],
And:[[0,3],[3,1],[1,2],[1,4],[4,5],[0,6]],
Peg:[[0,1],[1,"And:0"],["And:0",2],[2,0],[1,4],[4,7],[0,5],[5,6],[6,3],[4,8]],
Cyg:[[0,2],[2,1],[2,3],[3,6],[6,7],[2,4],[4,5]],
Lyr:[[0,5],[0,4],[4,3],[3,2],[2,1],[1,4]],
Aql:[[2,0],[0,1],[1,3],[0,5],[5,4],[5,6]],
Sco:[[6,3],[3,9],[9,14],[3,10],[10,0],[0,8],[8,4],[4,12],[12,16],[16,13],[13,2],[2,11],[11,5],[5,1],[1,7]],
Sgr:[[7,3],[3,0],[0,2],[2,6],[6,1],[1,5],[5,3],[5,4],[4,3]],
Leo:[[0,8],[8,2],[2,6],[6,7],[7,5],[2,3],[3,1],[1,4],[4,0],[3,4]],
Boo:[[0,6],[0,5],[5,2],[2,1],[1,4],[4,3],[3,0],[0,7]],
Vir:[[5,6],[6,2],[2,4],[4,1],[2,0],[0,3],[3,9],[3,7],[7,8]],
Cnc:[[0,1],[1,2],[2,4],[1,3]],
CVn:[[0,1]],
CrB:[[3,1],[1,0],[0,2],[2,6],[6,4],[4,5]],
Her:[[2,0],[0,9],[0,1],[1,8],[8,4],[4,5],[5,1],[1,3],[3,2],[3,6],[6,7],[7,10],[4,12],[12,11],[11,13]],
Dra:[[9,10],[10,8],[8,5],[5,4],[4,3],[3,2],[2,11],[2,6],[3,7],[7,0],[0,1],[1,12],[12,7]],
Ari:[[3,0],[0,1],[1,2]],
Psc:[[4,10],[10,5],[5,6],[6,2],[2,3],[3,1],[1,7],[7,8],[8,9],[9,1]],
Aqr:[[6,0],[0,1],[1,4],[4,3],[3,7],[1,8],[8,5],[5,2],[5,9]],
Cap:[[2,1],[1,7],[7,5],[5,4],[4,3],[3,0],[0,8],[8,6],[6,2]],
Cet:[[5,0],[0,2],[2,6],[6,7],[7,8],[8,9],[9,3],[3,1],[1,10],[4,0],[7,4]],
Eri:[[1,11],[11,12],[12,10],[10,5],[5,4],[4,3],[3,6],[6,2],[2,9],[9,8],[8,0]],
PsA:[[0,1],[1,4],[4,3],[3,2],[2,0]],
Gru:[[2,4],[4,0],[0,1],[1,3],[3,5]],
Cen:[[0,1],[1,4],[4,6],[6,3],[3,7],[6,10],[10,9],[9,5],[5,2],[2,8],[5,11]],
Cru:[[0,2],[1,3]],
Car:[[0,2],[2,3],[3,5],[5,1],[1,6],[6,4],[4,7],[7,3]],
Vel:[[0,1],[1,3],[3,5],[5,4],[4,6],[6,2],[2,0]],
Pup:[[3,4],[4,1],[1,5],[5,0],[0,6],[6,2]],
Hya:[[11,13],[13,5],[5,2],[2,9],[9,10],[10,0],[0,7],[7,8],[8,3],[3,6],[6,1],[1,4]],
Crv:[[4,3],[3,0],[0,2],[2,1],[1,3]],
Lib:[[1,0],[0,4],[4,2],[2,1],[4,3]],
Oph:[[0,5],[5,3],[3,6],[6,2],[2,1],[1,7],[1,8],[8,4],[4,0]],
Ser:[[7,2],[2,4],[4,0],[0,3],[3,1],[6,9],[9,5],[5,8]],
Del:[[1,2],[2,3],[3,0],[0,1],[0,4]],
Sge:[[0,1],[1,2],[1,3]],
Tri:[[0,1],[1,2],[2,0]],
Lep:[[3,0],[0,1],[1,2],[1,5],[5,6],[6,4],[0,4],[4,7]],
Mon:[[2,0],[0,3],[3,1],[1,4],[0,5]],
Col:[[3,0],[0,1],[1,2]],
Ara:[[1,0],[0,3],[3,4],[2,5],[5,1],[2,3]],
Lup:[[5,0],[0,1],[1,4],[4,2],[2,3],[3,6],[1,3]],
TrA:[[0,1],[1,2],[2,0]],
Pav:[[0,1],[1,2],[2,3],[2,4]],
Phe:[[3,0],[0,1],[1,2]],
Hyi:[[1,0],[0,2],[2,1]],
Tuc:[[0,1],[1,2]],
Mus:[[1,0],[0,3],[3,2],[2,0]],
CrA:[[2,0],[0,1],[1,3]],
Sct:[[0,1]],
Lyn:[[0,1],[1,2],[2,3]],
Com:[[1,0],[0,2]],
Lac:[[1,0],[0,2]],
Cam:[[0,1],[1,2]],
Crt:[[2,0],[0,1],[1,3],[3,2]],
Ret:[[0,1]]
};
/* Clusters, nebulae and galaxies you can actually pick out with eyes or
binoculars from a dark spot. */
var DSO = [
["Orion Nebula","M42",5.588,-5.391,"nebula","A star nursery, and the fuzzy middle star of Orion's sword."],
["Pleiades","M45",3.790,24.117,"cluster","Seven Sisters. Most people see six; binoculars show dozens."],
["Hyades","",4.450,15.870,"cluster","The V-shaped face of the bull. The nearest open cluster to us."],
["Andromeda Galaxy","M31",0.712,41.269,"galaxy","Another galaxy, 2.5 million years of travel away, visible to the naked eye."],
["Hercules Cluster","M13",16.695,36.461,"cluster","Several hundred thousand stars packed into a ball."],
["Beehive","M44",8.670,19.667,"cluster","A smudge to the eye, a swarm of stars in binoculars."],
["Double Cluster","",2.330,57.140,"cluster","Two rich clusters side by side between Perseus and Cassiopeia."],
["Lagoon Nebula","M8",18.060,-24.383,"nebula","A glowing cloud in the thickest part of the Milky Way."],
["Great Sagittarius Cluster","M22",18.606,-23.904,"cluster","One of the brightest globular clusters in the sky."],
["Ptolemy Cluster","M7",17.897,-34.793,"cluster","Noticed and written down nearly two thousand years ago."],
["Butterfly Cluster","M6",17.668,-32.242,"cluster","A loose scatter of stars shaped a bit like open wings."],
["Wild Duck Cluster","M11",18.851,-6.271,"cluster","Tight, rich, and shaped like a flight of birds."],
["","M35",6.148,24.340,"cluster","A full-moon-sized cluster at the feet of the twins."],
["","M4",16.393,-26.526,"cluster","A globular cluster sitting right next to Antares."],
["Omega Centauri","",13.446,-47.480,"cluster","The biggest globular cluster we have — millions of stars."],
["47 Tucanae","",0.401,-72.081,"cluster","Second only to Omega Centauri, and just as spectacular."],
["Large Magellanic Cloud","",5.393,-69.756,"galaxy","A small galaxy orbiting ours. Southern skies only."],
["Small Magellanic Cloud","",0.877,-72.828,"galaxy","The Large Cloud's smaller companion."],
["Bode's Galaxy","M81",9.926,69.066,"galaxy","A spiral galaxy that never sets from northern latitudes."],
["Dumbbell Nebula","M27",19.994,22.721,"nebula","A dying star that puffed off its outer layers."],
["Ring Nebula","M57",18.893,33.029,"nebula","A perfect smoke ring, tucked inside the parallelogram of Lyra."],
["Coathanger","",19.436,20.183,"cluster","A chance line-up of stars that really does look like a coathanger."],
["Coma Cluster","",12.400,26.000,"cluster","A loose sprinkle of stars best seen with the naked eye."],
["","M15",21.500,12.167,"cluster","A dense globular near the nose of the winged horse."],
["Carina Nebula","",10.750,-59.867,"nebula","Far bigger and brighter than the Orion Nebula, but far south."],
["Coalsack","",12.867,-63.000,"dark","A cloud of dust blotting out the Milky Way beside the Southern Cross."]
];
/* Short notes for the stars people ask about. Everything else gets an honest
description generated from its own numbers. */
var NOTE = {
"Sirius":"The brightest star in the night sky — and close, only 8.6 light years off. Low down it flashes every colour of the rainbow.",
"Canopus":"Second brightest star in the sky, but it never rises for most of Europe and North America.",
"Arcturus":"Follow the curve of the Plough's handle and you arrive here. An old orange giant, tearing through the galaxy at an angle to everything else.",
"Vega":"Was the pole star 14,000 years ago, and will be again in another 12,000.",
"Capella":"Actually four stars: two yellow giants circling each other, plus a distant pair of red dwarfs.",
"Rigel":"A blue supergiant so luminous that at Sirius's distance it would cast shadows.",
"Procyon":"The other bright star of the Winter Triangle, with Sirius and Betelgeuse.",
"Betelgeuse":"A red supergiant near the end of its life. It visibly dimmed in 2019 and 2020, then recovered.",
"Achernar":"Spinning so fast it is squashed into an egg shape.",
"Rigil Kentaurus":"The closest star system to the Sun, 4.4 light years away.",
"Altair":"One full turn every nine hours. That spin flattens it noticeably.",
"Acrux":"The bottom star of the Southern Cross, and the southernmost first-magnitude star.",
"Aldebaran":"Looks like it belongs to the Hyades cluster behind it, but it sits at less than half the distance.",
"Antares":"So big that if you dropped it where the Sun is, it would swallow Mars. Its name means rival of Mars.",
"Spica":"Two stars so close together they pull each other out of round.",
"Pollux":"An orange giant with a planet several times the mass of Jupiter.",
"Fomalhaut":"Ringed by a lopsided belt of dust and debris.",
"Deneb":"Very far away and still one of the brightest stars we see — it is genuinely enormous.",
"Mimosa":"One of the hottest stars easily visible without help.",
"Regulus":"Sits almost exactly on the path the Sun traces through the sky, so the Moon regularly passes it.",
"Adhara":"About five million years ago it passed close enough to be the brightest star in the sky by far.",
"Castor":"Six stars in one: three pairs, all bound together.",
"Gacrux":"The nearest red giant to us, at the top of the Southern Cross.",
"Bellatrix":"Orion's other shoulder, and a good yardstick for judging how dark your sky is.",
"Polaris":"Sits almost exactly above the north pole, so the whole northern sky turns around it.",
"Mizar":"Split it with sharp eyes and you find Alcor beside it — the classic eyesight test.",
"Alcor":"The faint companion beside Mizar. Seeing both was once a test of good vision.",
"Algol":"Dims by a third every 2.87 days as a dimmer companion crosses in front of it.",
"Mira":"Swings between naked-eye bright and completely invisible over about eleven months.",
"Albireo":"Through even a small telescope it splits into a gold star and a blue one.",
"Alphard":"Its name means the solitary one — there is nothing else bright anywhere near it.",
"Thuban":"Was the pole star when the Egyptian pyramids were built.",
"Kochab":"Circled the pole alongside Polaris for centuries; the two are called the guardians of the pole.",
"Alnilam":"The middle star of Orion's Belt, and the most distant of the three by a long way.",
"Rasalgethi":"A red giant that swells and shrinks, wandering by a factor of three in brightness.",
"Alphecca":"The jewel of the Northern Crown, and an eclipsing pair.",
"Alnair":"Marks the bright end of a crane-shaped figure low in southern skies.",
"Peacock":"Named in the 1930s, one of the newest star names in common use."
};
var DSOSZ = {
"Large Magellanic Cloud": 3.4, "Small Magellanic Cloud": 2.3, "Coalsack": 3.0,
"Hyades": 3.2, "Pleiades": 1.9, "Coathanger": 1.7, "Coma Cluster": 2.8,
"Carina Nebula": 1.9, "Double Cluster": 1.6, "Orion Nebula": 1.3,
"Andromeda Galaxy": 1.8
};
var PLACES = [
{ n: "London", lat: 51.5, lon: -0.1 },
{ n: "New York", lat: 40.7, lon: -74.0 },
{ n: "Delhi", lat: 28.6, lon: 77.2 },
{ n: "Singapore", lat: 1.4, lon: 103.8 },
{ n: "Rio", lat: -22.9, lon: -43.2 },
{ n: "Sydney", lat: -33.9, lon: 151.2 }
];
var MONTHS = ["", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
/* ═══════════════════════════════════════════════════════════════════════════
GEOMETRY
Everything is one unit vector in the (north, east, up) frame of wherever you
are standing, then a stereographic projection onto the screen. Stereographic
because it stays honest at wide angles: a 130-degree view still shows round
constellations instead of the smeared corners you get from a flat projection.
═══════════════════════════════════════════════════════════════════════════ */
var D2R = Math.PI / 180, R2D = 180 / Math.PI, TAU = Math.PI * 2;
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
/* cache sin/cos of declination and right ascension so the draw loop does no
trigonometry at all — the hour angle turns into one rotation identity. */
function prep(o) {
var d = o.dec * D2R, a = o.ra * 15 * D2R;
o.sd = Math.sin(d); o.cd = Math.cos(d);
o.cr = Math.cos(a); o.sr = Math.sin(a);
return o;
}
function hexa(hex, a) {
hex = (hex || "#000").trim();
if (hex.charAt(0) === "#") hex = hex.slice(1);
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
var n = parseInt(hex, 16);
if (isNaN(n)) n = 0;
return "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a + ")";
}
/* ── build the catalogue ─────────────────────────────────────────────────── */
var STARS = [], CON = {}, LINES = [], GROUPS = [], BYNAME = {};
(function build() {
var key, rows, i, e, f, s;
for (key in PACK) {
var arr = [];
rows = PACK[key].split(";");
for (i = 0; i < rows.length; i++) {
f = rows[i].split(",");
s = prep({
name: f[0], bay: f[1], ra: +f[2], dec: +f[3], mag: +f[4],
sp: f[5], con: key
});
s.ph = ((s.ra * 11.3 + s.dec * 4.7) % TAU + TAU) % TAU;
arr.push(s); STARS.push(s);
if (s.name) BYNAME[s.name] = s;
}
CON[key] = arr;
}
function ref(con, r) {
if (typeof r === "number") return CON[con] ? CON[con][r] : null;
var p = String(r).split(":");
return CON[p[0]] ? CON[p[0]][+p[1]] : null;
}
for (key in FIG) {
var pairs = FIG[key], members = [], seen = {};
for (i = 0; i < pairs.length; i++) {
var a = ref(key, pairs[i][0]), b = ref(key, pairs[i][1]);
if (!a || !b) continue;
LINES.push([a, b]);
for (e = 0; e < 2; e++) {
var rr = pairs[i][e];
if (typeof rr === "number" && !seen[rr] && CON[key][rr]) {
seen[rr] = 1; members.push(CON[key][rr]);
}
}
}
if (!members.length) continue;
var vx = 0, vy = 0, vz = 0, m;
for (i = 0; i < members.length; i++) {
m = members[i]; vx += m.cd * m.cr; vy += m.cd * m.sr; vz += m.sd;
}
var L = Math.sqrt(vx * vx + vy * vy + vz * vz) || 1;
vx /= L; vy /= L; vz /= L;
var g = prep({
key: key, name: FULL[key] || key, stars: members,
dec: Math.asin(clamp(vz, -1, 1)) * R2D,
ra: ((Math.atan2(vy, vx) * R2D / 15) % 24 + 24) % 24
});
var mx = 0;
for (i = 0; i < members.length; i++) {
m = members[i];
mx = Math.max(mx, Math.acos(clamp(m.cd * m.cr * vx + m.cd * m.sr * vy + m.sd * vz, -1, 1)));
}
g.rad = mx;
GROUPS.push(g);
}
GROUPS.sort(function (p, q) { return p.name < q.name ? -1 : (p.name > q.name ? 1 : 0); });
})();
var SIGHTS = DSO.map(function (d) {
return prep({
name: d[0] || d[1], cat: d[1], ra: d[2], dec: d[3], kind: d[4],
note: d[5], size: DSOSZ[d[0] || d[1]] || 1
});
});
SIGHTS.sort(function (p, q) { return p.name < q.name ? -1 : (p.name > q.name ? 1 : 0); });
/* ── galactic → equatorial, for the Milky Way band and the faint field ───── */
var GP_DEC = 27.12825 * D2R, GP_RA = 192.85948 * D2R, GP_L = 122.93192 * D2R;
var sGD = Math.sin(GP_DEC), cGD = Math.cos(GP_DEC);
function gal2eq(l, b) {
var lr = l * D2R, br = b * D2R;
var sb = Math.sin(br), cb = Math.cos(br);
var c = Math.cos(GP_L - lr), s = Math.sin(GP_L - lr);
var dec = Math.asin(clamp(sb * sGD + cb * cGD * c, -1, 1));
var ra = Math.atan2(cb * s, cGD * sb - sGD * cb * c) + GP_RA;
return { ra: ((ra * R2D / 15) % 24 + 24) % 24, dec: dec * R2D };
}
/* Milky Way: a chain of soft glows laid along the galactic equator. Overlapping
round gradients blend into one another, which is what keeps the band looking
like light rather than like a stack of translucent rectangles. */
var MW_STEP = 4, MW = [];
(function () {
for (var i = 0; i < 360 / MW_STEP; i++) {
var l = i * MW_STEP;
MW.push({ c: prep(gal2eq(l, 0)), e: prep(gal2eq(l, 15)), a: mwGlow(l) });
}
function mwGlow(l) {
var d = Math.abs(((l + 180) % 360) - 180); /* degrees from the middle */
var core = Math.exp(-Math.pow(d / 62, 2));
var cyg = Math.exp(-Math.pow((d - 80) / 26, 2)); /* the second rise, near Cygnus */
return 0.34 + 0.80 * core + 0.34 * cyg;
}
})();
/* The faint field: stand-ins for the millions of stars too dim to name, laid
down with a fixed seed so the sky is the same sky every time you open it. */
var seed = 20260115 >>> 0;
function rnd() { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296; }
var DUST = [[], [], []];
(function () {
for (var i = 0; i < 1500; i++) {
var p;
if (i % 5 < 3) {
var b = (rnd() + rnd() + rnd() - 1.5) * 15;
p = gal2eq(rnd() * 360, b);
} else {
p = { ra: rnd() * 24, dec: Math.asin(2 * rnd() - 1) * R2D };
}
var t = rnd();
prep(p);
p.s = t > 0.93 ? 1.7 : (t > 0.7 ? 1.3 : 1);
DUST[t > 0.93 ? 2 : (t > 0.7 ? 1 : 0)].push(p);
}
})();
/* ═══════════════════════════════════════════════════════════════════════════
TIME
═══════════════════════════════════════════════════════════════════════════ */
function jd0(y, m, d) {
var a = Math.floor((14 - m) / 12), yy = y + 4800 - a, mm = m + 12 * a - 3;
return d + Math.floor((153 * mm + 2) / 5) + 365 * yy + Math.floor(yy / 4)
- Math.floor(yy / 100) + Math.floor(yy / 400) - 32045 - 0.5;
}
/* Local sidereal time: which slice of sky is on your meridian right now.
Clocks are assumed to run on the nearest whole hour to the local sun. */
function lstHours() {
var offset = Math.round(st.lon / 15);
var ut = st.minutes / 60 - offset;
var jd = jd0(2026, st.month, 15) + ut / 24;
var d = jd - 2451545.0;
var gmst = 18.697374558 + 24.06570982441908 * d;
return ((gmst + st.lon / 15) % 24 + 24) % 24;
}
function hhmm(min) {
var m = Math.floor(((min % 1440) + 1440) % 1440);
var h = Math.floor(m / 60);
return (h < 10 ? "0" : "") + h + ":" + (m % 60 < 10 ? "0" : "") + (m % 60);
}
var COMPASS = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west"];
var COMPASS_S = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
function compass(azRad, short) {
var i = Math.round((((azRad * R2D) % 360 + 360) % 360) / 45) % 8;
return short ? COMPASS_S[i] : COMPASS[i];
}
/* ═══════════════════════════════════════════════════════════════════════════
STATE
═══════════════════════════════════════════════════════════════════════════ */
var mqMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
var reduce = mqMotion.matches;
var st = {
minutes: 1290, month: 1, place: 0, lat: PLACES[0].lat, lon: PLACES[0].lon,
az: 180 * D2R, alt: 35 * D2R, fov: 92 * D2R,
playing: !reduce, lines: true, labels: true, dso: true, mw: true,
twinkle: !reduce, sel: null
};
var $ = function (id) { return document.getElementById(id); };
var field = $("field"), cv = $("sky"), ctx = cv.getContext("2d");
var tip = $("tip"), tipName = $("tipName"), tipMeta = $("tipMeta");
var live = $("live"), hint = $("hint");
/* ═══════════════════════════════════════════════════════════════════════════
PROJECTION — scratch globals, deliberately: this runs ~4000 times a frame
and allocating a vector each time is how you lose 60fps.
═══════════════════════════════════════════════════════════════════════════ */
var CL = 1, SL = 0, SLAT = 0, CLAT = 1;
var F1 = 0, F2 = 0, F3 = 0, R1 = 0, R2 = 0, U1 = 0, U2 = 0, U3 = 0;
var CX = 0, CY = 0, SC = 1;
var VN = 0, VE = 0, VU = 0, PX = 0, PY = 0, PD = 0;
function horiz(o) {
var ch = CL * o.cr + SL * o.sr;
var sh = SL * o.cr - CL * o.sr;
VU = o.sd * SLAT + o.cd * CLAT * ch;
VE = -o.cd * sh;
VN = o.sd * CLAT - o.cd * SLAT * ch;
}
function project() {
PD = VN * F1 + VE * F2 + VU * F3;
var den = 1 + PD;
if (den < 0.18) return false;
PX = CX + ((VN * R1 + VE * R2) / den) * SC;
PY = CY - ((VN * U1 + VE * U2 + VU * U3) / den) * SC;
return true;
}
function setCamera() {
var cA = Math.cos(st.alt), sA = Math.sin(st.alt);
var cZ = Math.cos(st.az), sZ = Math.sin(st.az);
F1 = cA * cZ; F2 = cA * sZ; F3 = sA;
R1 = -sZ; R2 = cZ;
U1 = -sA * cZ; U2 = -sA * sZ; U3 = cA;
var lst = lstHours() * 15 * D2R;
CL = Math.cos(lst); SL = Math.sin(lst);
SLAT = Math.sin(st.lat * D2R); CLAT = Math.cos(st.lat * D2R);
CX = cw / 2; CY = ch / 2;
SC = (Math.max(cw, ch) / 2) / Math.tan(st.fov / 4);
}
/* where a point on the sky sits, as altitude + azimuth */
function altaz(o) {
setCamera();
horiz(o);
return { alt: Math.asin(clamp(VU, -1, 1)), az: Math.atan2(VE, VN) };
}
/* ═══════════════════════════════════════════════════════════════════════════
STAR APPEARANCE
═══════════════════════════════════════════════════════════════════════════ */
var SPCOL = {
O: "#a9c3ff", B: "#b9d0ff", A: "#dde7ff", F: "#f6f4ee",
G: "#ffeec2", K: "#ffd49a", M: "#ffb184"
};
var SPWORD = {
O: "blue", B: "blue-white", A: "white", F: "yellow-white",
G: "yellow", K: "orange", M: "red"
};
var SPRITES = {};
(function () {
for (var k in SPCOL) SPRITES[k] = sprite(SPCOL[k]);
SPRITES["?"] = SPRITES.F;
function sprite(col) {
var c = document.createElement("canvas");
c.width = c.height = 64;
var g = c.getContext("2d");
var rg = g.createRadialGradient(32, 32, 0, 32, 32, 32);
rg.addColorStop(0, "rgba(255,255,255,1)");
rg.addColorStop(0.13, "rgba(255,255,255,0.98)");
rg.addColorStop(0.24, hexa(col, 0.85));
rg.addColorStop(0.44, hexa(col, 0.20));
rg.addColorStop(0.72, hexa(col, 0.05));
rg.addColorStop(1, hexa(col, 0));
g.fillStyle = rg; g.fillRect(0, 0, 64, 64);
return c;
}
})();
var LIMIT = 6.2;
function radiusOf(mag, zf) { return Math.max(0.42, (LIMIT - mag) * 0.62 + 0.4) * zf; }
/* ═══════════════════════════════════════════════════════════════════════════
CANVAS PLUMBING
═══════════════════════════════════════════════════════════════════════════ */
var cw = 300, ch = 200, dpr = 1, dirty = true;
function mark() { dirty = true; }
function resize() {
var r = field.getBoundingClientRect();
cw = Math.max(120, Math.round(r.width));
ch = Math.max(120, Math.round(r.height));
dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.round(cw * dpr);
cv.height = Math.round(ch * dpr);
mark();
}
var COL = {};
var COLKEYS = ["sky-hi", "sky-lo", "ground", "ground-line", "sky-line", "sky-label", "sky-ink"];
function readTheme() {
/* Some engines keep stale custom-property values on form controls when the
theme attribute flips at runtime. One forced reflow settles everything —
but collapsing the body clamps the scroll position to 0, so put it back. */
var b = document.body;
if (b) {
var sx = window.pageXOffset, sy = window.pageYOffset;
b.style.display = "none"; void b.offsetHeight; b.style.display = "";
if (sx || sy) window.scrollTo(sx, sy);
}
var cs = getComputedStyle(document.documentElement);
for (var i = 0; i < COLKEYS.length; i++) {
COL[COLKEYS[i]] = (cs.getPropertyValue("--" + COLKEYS[i]) || "").trim() || "#000000";
}
mark();
}
window.addEventListener("message", function (ev) {
var d = ev.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
readTheme();
}
});
var mqDark = window.matchMedia("(prefers-color-scheme: dark)");
(mqDark.addEventListener ? mqDark.addEventListener.bind(mqDark, "change") :
mqDark.addListener.bind(mqDark))(readTheme);
(mqMotion.addEventListener ? mqMotion.addEventListener.bind(mqMotion, "change") :
mqMotion.addListener.bind(mqMotion))(function () {
reduce = mqMotion.matches;
if (reduce) { st.twinkle = false; tTwinkle.checked = false; tTwinkle.disabled = true; setPlaying(false); }
else { tTwinkle.disabled = false; }
mark();
});
/* ═══════════════════════════════════════════════════════════════════════════
DRAW
═══════════════════════════════════════════════════════════════════════════ */
var vis = []; /* what is on screen right now, for hit testing */
var labels = []; /* label boxes already claimed this frame */
var upCount = 0, brightest = null;
function draw(now) {
setCamera();
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
vis.length = 0; labels.length = 0;
upCount = 0; brightest = null;
var zf = clamp(Math.sqrt(78 / (st.fov * R2D)), 0.8, 1.7);
var tw = (st.twinkle && !reduce) ? now / 1000 : -1;
/* 1 — sky, brighter toward the horizon */
var horizonY = CY + Math.tan(st.alt / 2) * SC;
var gy0 = clamp(horizonY, -ch * 4, ch * 5);
var grad = ctx.createLinearGradient(0, gy0, 0, gy0 - ch * 1.25);
grad.addColorStop(0, COL["sky-hi"]);
grad.addColorStop(1, COL["sky-lo"]);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, cw, ch);
if (st.mw) drawMilkyWay();
drawDust();
/* 2 — the ground. The horizon is a great circle, and stereographic
projection turns every great circle into a circle on screen, so the fill
is exact rather than a polygon approximation. */
drawGround();
/* 3 — the figures */
if (st.lines) drawLines();
/* 4 — the stars themselves */
drawStars(zf, tw);
if (st.dso) drawSights(zf);
if (st.labels) drawLabels();
drawSelection(now);
drawCardinals();
}
function drawMilkyWay() {
var lim = Math.max(cw, ch) * 3;
for (var i = 0; i < MW.length; i++) {
var m = MW[i];
horiz(m.c);
if (!project()) continue;
var x = PX, y = PY;
if (x < -cw * 2 || x > cw * 3 || y < -ch * 2 || y > ch * 3) continue;
horiz(m.e);
if (!project()) continue;
var r = Math.sqrt((PX - x) * (PX - x) + (PY - y) * (PY - y));
if (!(r > 2) || r > lim) continue;
var a = 0.055 * m.a;
var g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, "rgba(198,214,244," + a.toFixed(4) + ")");
g.addColorStop(0.5, "rgba(198,214,244," + (a * 0.55).toFixed(4) + ")");
g.addColorStop(1, "rgba(198,214,244,0)");
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(x, y, r, 0, TAU); ctx.fill();
}
}
function drawDust() {
var alphas = [0.16, 0.3, 0.5];
for (var b = 0; b < DUST.length; b++) {
ctx.fillStyle = "rgba(226,232,244," + alphas[b] + ")";
var arr = DUST[b];
for (var i = 0; i < arr.length; i++) {
var p = arr[i];
horiz(p);
if (VU <= 0.01) continue;
if (!project()) continue;
if (PX < -4 || PX > cw + 4 || PY < -4 || PY > ch + 4) continue;
ctx.fillRect(PX, PY, p.s, p.s);
}
}
}
function drawGround() {
var sA = Math.sin(st.alt);
var hy = CY + Math.tan(st.alt / 2) * SC;
var rad = Math.abs(sA) > 1e-6 ? SC / Math.abs(sA) : Infinity;
ctx.fillStyle = COL.ground;
if (rad > 6000 || Math.abs(sA) < 0.02) {
if (hy < ch) ctx.fillRect(0, Math.max(0, hy), cw, ch - Math.max(0, hy));
} else {
var ccy = CY - (Math.cos(st.alt) / sA) * SC;
ctx.beginPath();
if (st.alt > 0) {
ctx.rect(0, 0, cw, ch);
ctx.moveTo(CX + rad, ccy);
ctx.arc(CX, ccy, rad, 0, TAU);
ctx.fill("evenodd");
} else {
ctx.arc(CX, ccy, rad, 0, TAU);
ctx.fill();
}
}
/* the horizon itself */
ctx.save();
ctx.strokeStyle = COL["ground-line"];
ctx.lineWidth = 1.25;
ctx.beginPath();
if (rad > 6000 || Math.abs(sA) < 0.02) { ctx.moveTo(0, hy); ctx.lineTo(cw, hy); }
else { ctx.arc(CX, CY - (Math.cos(st.alt) / sA) * SC, rad, 0, TAU); }
ctx.stroke();
ctx.restore();
}
function drawLines() {
ctx.save();
ctx.strokeStyle = hexa(COL["sky-line"], 0.4);
ctx.lineWidth = 1;
ctx.lineCap = "round";
ctx.beginPath();
for (var i = 0; i < LINES.length; i++) {
var a = LINES[i][0], b = LINES[i][1];
horiz(a); if (VU < 0.005 || !project()) continue;
var x1 = PX, y1 = PY;
horiz(b); if (VU < 0.005 || !project()) continue;
if (Math.abs(PX - x1) > cw * 2.2 || Math.abs(PY - y1) > ch * 2.2) continue;
ctx.moveTo(x1, y1); ctx.lineTo(PX, PY);
}
ctx.stroke();
ctx.restore();
}
function drawStars(zf, tw) {
var lim = st.fov * R2D > 100 ? 1.9 : (st.fov * R2D > 62 ? 2.7 : (st.fov * R2D > 38 ? 3.5 : 4.7));
for (var i = 0; i < STARS.length; i++) {
var s = STARS[i];
horiz(s);
if (VU > 0) upCount++;
if (VU < -0.004) continue;
if (!project()) continue;
if (PX < -30 || PX > cw + 30 || PY < -30 || PY > ch + 30) continue;
var alt = VU; /* sin(altitude) */
var haze = clamp(alt / 0.12, 0.12, 1); /* thick air near the horizon */
var r = radiusOf(s.mag, zf);
var a = clamp(1.12 - (s.mag - 3.8) * 0.2, 0.34, 1) * haze;
if (tw >= 0) {
var amp = 0.055 + 0.11 * (1 - haze);
var f = 1 + amp * Math.sin(tw * 2.7 + s.ph) + amp * 0.6 * Math.sin(tw * 4.3 + s.ph * 1.7);
r *= f; a = clamp(a * (0.86 + 0.2 * f), 0, 1);
}
var e = { x: PX, y: PY, r: Math.max(r, 5), o: s, t: 0 };
vis.push(e);
if (!brightest || s.mag < brightest.mag) {
if (PX > 0 && PX < cw && PY > 0 && PY < ch) brightest = s;
}
ctx.globalAlpha = a;
if (r >= 1.25) {
var sp = SPRITES[s.sp] || SPRITES["?"];
var sz = r * 8.6;
ctx.drawImage(sp, PX - sz / 2, PY - sz / 2, sz, sz);
} else {
ctx.fillStyle = SPCOL[s.sp] || "#f6f4ee";
ctx.fillRect(PX - r, PY - r, r * 2, r * 2);
}
ctx.globalAlpha = 1;
if (st.labels && s.name && s.mag <= lim) {
pushLabel(s.name, PX + r + 4, PY - 1, s.mag, COL["sky-ink"], 11.5);
}
}
}
function drawSights(zf) {
ctx.save();
for (var i = 0; i < SIGHTS.length; i++) {
var d = SIGHTS[i];
horiz(d);
if (VU < 0.01 || !project()) continue;
if (PX < -40 || PX > cw + 40 || PY < -40 || PY > ch + 40) continue;
var r = clamp(4.4 * zf * d.size, 3, 22);
vis.push({ x: PX, y: PY, r: Math.max(r, 9), o: d, t: 1 });
ctx.globalAlpha = 0.72;
if (d.kind === "nebula") {
ctx.fillStyle = "rgba(150,205,190,0.16)";
ctx.beginPath(); ctx.ellipse(PX, PY, r, r * 0.78, 0.5, 0, TAU); ctx.fill();
ctx.strokeStyle = "rgba(160,215,200,0.5)";
} else if (d.kind === "galaxy") {
ctx.fillStyle = "rgba(210,205,235,0.13)";
ctx.beginPath(); ctx.ellipse(PX, PY, r, r * 0.5, 0.9, 0, TAU); ctx.fill();
ctx.strokeStyle = "rgba(214,208,240,0.55)";
} else if (d.kind === "dark") {
ctx.fillStyle = "rgba(4,5,9,0.55)";
ctx.beginPath(); ctx.ellipse(PX, PY, r, r * 0.72, 0.3, 0, TAU); ctx.fill();
ctx.strokeStyle = "rgba(120,128,142,0.4)";
} else {
ctx.strokeStyle = "rgba(200,214,238,0.55)";
}
ctx.lineWidth = 1;
ctx.setLineDash([2.5, 2.5]);
ctx.beginPath(); ctx.arc(PX, PY, r, 0, TAU); ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
if (st.labels && (st.fov * R2D < 96 || d.size > 1.5)) {
pushLabel(d.name, PX + r + 4, PY - 1, 3.2, COL["sky-label"], 10.5);
}
}
ctx.restore();
}
var LABELQ = [];
function pushLabel(text, x, y, pri, col, size) {
LABELQ.push({ t: text, x: x, y: y, p: pri, c: col, s: size });
}
function drawLabels() {
/* constellation names get in ahead of everything but the top few stars */
for (var i = 0; i < GROUPS.length; i++) {
var g = GROUPS[i];
horiz(g);
if (VU < 0.06 || !project()) continue;
if (PX < 0 || PX > cw || PY < 0 || PY > ch) continue;
if (g.rad * R2D * 3.2 < st.fov * R2D * 0.09) continue;
LABELQ.push({ t: g.name.toUpperCase(), x: PX, y: PY, p: 1.2, c: COL["sky-label"], s: 10, mid: true });
}
LABELQ.sort(function (a, b) { return a.p - b.p; });
ctx.save();
ctx.textBaseline = "middle";
for (var j = 0; j < LABELQ.length; j++) {
var L = LABELQ[j];
ctx.font = (L.mid ? "600 " : "500 ") + L.s + "px " + (L.mid
? "ui-monospace, Menlo, Consolas, monospace"
: "system-ui, -apple-system, Segoe UI, sans-serif");
if (L.mid) ctx.letterSpacing = "0.12em";
var w = ctx.measureText(L.t).width;
var x = L.mid ? L.x - w / 2 : L.x;
var box = [x - 3, L.y - L.s * 0.7, x + w + 3, L.y + L.s * 0.7];
if (box[0] < 2 || box[2] > cw - 2 || box[1] < 2 || box[3] > ch - 2) { ctx.letterSpacing = "0px"; continue; }
if (hits(box)) { ctx.letterSpacing = "0px"; continue; }
labels.push(box);
ctx.globalAlpha = L.mid ? 0.62 : 0.9;
ctx.fillStyle = L.c;
ctx.fillText(L.t, x, L.y);
ctx.globalAlpha = 1;
ctx.letterSpacing = "0px";
}
ctx.restore();
LABELQ.length = 0;
}
function hits(b) {
for (var i = 0; i < labels.length; i++) {
var o = labels[i];
if (b[0] < o[2] && b[2] > o[0] && b[1] < o[3] && b[3] > o[1]) return true;
}
return false;
}
function drawSelection(now) {
if (!st.sel) return;
var o = st.sel;
horiz(o);
if (!project() || VU < -0.02) return;
var pulse = reduce ? 0 : Math.sin(now / 420) * 1.6;
var r = 13 + pulse;
ctx.save();
ctx.strokeStyle = COL["sky-line"];
ctx.lineWidth = 1.4;
ctx.globalAlpha = 0.95;
ctx.beginPath();
for (var k = 0; k < 4; k++) {
var a0 = k * TAU / 4 + 0.36, a1 = a0 + TAU / 4 - 0.72;
ctx.moveTo(PX + Math.cos(a0) * r, PY + Math.sin(a0) * r);
ctx.arc(PX, PY, r, a0, a1);
}
ctx.stroke();
ctx.restore();
}
function drawCardinals() {
ctx.save();
ctx.font = "600 10px ui-monospace, Menlo, Consolas, monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.letterSpacing = "0.1em";
for (var i = 0; i < 8; i++) {
var az = i * 45 * D2R;
VN = Math.cos(az); VE = Math.sin(az); VU = 0;
if (!project()) continue;
if (PX < 12 || PX > cw - 12 || PY < 10 || PY > ch - 10) continue;
ctx.fillStyle = COL["sky-label"];
ctx.globalAlpha = i % 2 ? 0.5 : 0.85;
ctx.fillText(COMPASS_S[i], PX, PY - 9);
ctx.globalAlpha = 1;
}
ctx.letterSpacing = "0px";
ctx.restore();
}
/* ═══════════════════════════════════════════════════════════════════════════
LOOP
═══════════════════════════════════════════════════════════════════════════ */
var tween = null, lastT = 0, lastRead = 0;
function frame(now) {
requestAnimationFrame(frame);
var dt = lastT ? Math.min(now - lastT, 120) : 16;
lastT = now;
if (st.playing) {
st.minutes += dt * 0.011; /* dusk to dawn in about a minute */
if (st.minutes > 1800) st.minutes -= 720;
timeEl.value = String(Math.round(st.minutes));
syncTimeLabel();
dirty = true;
}
if (tween) {
var k = clamp((now - tween.t0) / tween.ms, 0, 1);
var e = k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2;
st.az = tween.az0 + tween.daz * e;
st.alt = tween.alt0 + tween.dalt * e;
st.fov = tween.fov0 + tween.dfov * e;
if (k >= 1) { tween = null; syncZoom(); }
dirty = true;
}
if (st.twinkle && !reduce) dirty = true;
if (st.sel && !reduce) dirty = true;
if (dirty) { dirty = false; draw(now); }
if (now - lastRead > 130) { lastRead = now; readout(); }
}
/* ═══════════════════════════════════════════════════════════════════════════
READOUT
═══════════════════════════════════════════════════════════════════════════ */
var sClock = $("sClock"), sUp = $("sUp"), sLook = $("sLook"), sFov = $("sFov");
var pill = $("pill"), pillText = $("pillText");
var pinned = null;
function readout() {
sClock.textContent = hhmm(st.minutes);
sUp.textContent = String(upCount);
sLook.innerHTML = compass(st.az, true) + " " + Math.round(st.alt * R2D) + '<span class="u">°</span>';
var hfov = 4 * Math.atan((cw / 2) / SC) * R2D;
sFov.innerHTML = Math.round(hfov) + '<span class="u">°</span>';
/* the selected object keeps moving after you pick it — the clock runs, you
change month or hop to another latitude. Recompute rather than freeze. */
refreshSel();
if (pinned) return;
pill.className = "pill";
if (brightest) {
pillText.textContent = "brightest in view: " + starLabel(brightest) +
" (mag " + brightest.mag.toFixed(2) + ")";
} else {
pillText.textContent = "nothing bright in this direction";
}
}
function starLabel(s) {
return s.name || ((s.bay ? s.bay + " " : "") + (FULL[s.con] || s.con));
}
/* where something sits right now, in words */
function whereOf(o) {
var p = altaz(o);
return {
up: p.alt >= 0,
text: p.alt >= 0
? Math.round(p.alt * R2D) + "° up, " + compass(p.az)
: "below the horizon"
};
}
var flashTok = 0;
function flash(text, kind) {
setPill(text, kind);
var t = ++flashTok;
setTimeout(function () { if (t === flashTok) restPill(); }, 2800);
}
/* live:true means readout() keeps rewriting this pill from st.sel */
function setPill(text, kind, live) {
flashTok++;
pinned = text ? { kind: kind || "", live: !!live } : null;
if (!text) { readout(); return; }
pill.className = "pill" + (kind ? " " + kind : "");
pillText.textContent = text;
}
function pinSel() {
if (!st.sel) return;
flashTok++;
pinned = { kind: "", live: true };
refreshSel();
}
/* what the pill falls back to once a temporary message has had its say */
function restPill() { if (st.sel) pinSel(); else setPill(null); }
/* ═══════════════════════════════════════════════════════════════════════════
CONTROLS
═══════════════════════════════════════════════════════════════════════════ */
var timeEl = $("time"), timeV = $("timeV"), playBtn = $("playBtn");
var monthEl = $("month"), gotoEl = $("goto");
var tLines = $("tLines"), tLabels = $("tLabels"), tDso = $("tDso"),
tMw = $("tMw"), tTwinkle = $("tTwinkle");
function syncTimeLabel() {
var t = hhmm(st.minutes);
timeV.textContent = t;
timeEl.setAttribute("aria-valuetext", t);
}
function setPlaying(on) {
st.playing = !!on;
playBtn.textContent = on ? "Pause the sky" : "Let the sky turn";
playBtn.setAttribute("aria-pressed", on ? "true" : "false");
mark();
}
timeEl.addEventListener("input", function () {
st.minutes = +timeEl.value;
if (st.playing) setPlaying(false);
syncTimeLabel(); restPill(); refreshSel(); mark();
});
playBtn.addEventListener("click", function () { setPlaying(!st.playing); });
monthEl.addEventListener("change", function () {
st.month = +monthEl.value;
mark(); refreshSel();
say("Sky set to " + MONTHS[st.month] + " 15th.");
flash("same clock, different half of the sky \u00b7 " + MONTHS[st.month] + " 15th", "");
});
/* places */
(function () {
var host = $("places");
PLACES.forEach(function (p, i) {
var b = document.createElement("button");
b.type = "button";
b.setAttribute("aria-pressed", i === 0 ? "true" : "false");
b.innerHTML = p.n + '<span class="lat">' + Math.abs(Math.round(p.lat)) + "°" +
(p.lat >= 0 ? "N" : "S") + "</span>";
b.addEventListener("click", function () {
st.place = i; st.lat = p.lat; st.lon = p.lon;
Array.prototype.forEach.call(host.children, function (c, j) {
c.setAttribute("aria-pressed", j === i ? "true" : "false");
});
mark(); refreshSel();
say("Now looking from " + p.n + ", latitude " + Math.round(p.lat) + " degrees.");
flash("standing at " + Math.abs(Math.round(p.lat)) + "°" + (p.lat >= 0 ? "N" : "S") + " · " + p.n, "");
});
host.appendChild(b);
});
})();
/* toggles */
function bindToggle(el, key, extra) {
el.addEventListener("change", function () {
st[key] = el.checked;
if (extra) extra(el.checked);
mark();
});
}
bindToggle(tLines, "lines");
bindToggle(tLabels, "labels");
bindToggle(tDso, "dso");
bindToggle(tMw, "mw");
bindToggle(tTwinkle, "twinkle");
if (reduce) { tTwinkle.checked = false; tTwinkle.disabled = true; st.twinkle = false; }
/* the go-to list */
(function () {
function group(label) {
var g = document.createElement("optgroup");
g.label = label; gotoEl.appendChild(g); return g;
}
var gs = group("Constellations");
GROUPS.forEach(function (g, i) {
var o = document.createElement("option");
o.value = "c:" + i; o.textContent = g.name; gs.appendChild(o);
});
var bright = STARS.filter(function (s) { return s.name && s.mag < 1.7; })
.sort(function (a, b) { return a.mag - b.mag; });
var gb = group("Bright stars");
bright.forEach(function (s) {
var o = document.createElement("option");
o.value = "s:" + s.name; o.textContent = s.name; gb.appendChild(o);
});
var gd = group("Clusters, nebulae, galaxies");
SIGHTS.forEach(function (d, i) {
var o = document.createElement("option");
o.value = "d:" + i;
o.textContent = d.name + (d.cat && d.cat !== d.name ? " (" + d.cat + ")" : "");
gd.appendChild(o);
});
})();
gotoEl.addEventListener("change", function () {
var v = gotoEl.value;
if (!v) return;
var kind = v.slice(0, 1), rest = v.slice(2);
if (kind === "c") {
var g = GROUPS[+rest];
lookAt(g, clamp(g.rad * 2 * 2.4, 26 * D2R, 120 * D2R), g.name);
} else if (kind === "s") {
var s = BYNAME[rest];
if (s) { lookAt(s, clamp(st.fov, 26 * D2R, 70 * D2R), s.name); select(s); }
} else {
var d = SIGHTS[+rest];
if (d) { lookAt(d, 34 * D2R, d.name); select(d); }
}
gotoEl.value = "";
});
function lookAt(o, fov, label) {
var p = altaz(o);
var below = p.alt < 0;
var alt = below ? 6 * D2R : clamp(p.alt, -20 * D2R, 87 * D2R);
goTo(p.az, alt, fov, 780);
if (below) {
flash(label + " is below the horizon right now", "down");
say(label + " is below the horizon from " + PLACES[st.place].n + " at " + hhmm(st.minutes) +
". The view is turned that way anyway — try another hour or another month.");
} else {
flash(label + " · " + Math.round(p.alt * R2D) + "° up, " + compass(p.az), "up");
say(label + " is " + Math.round(p.alt * R2D) + " degrees above the " + compass(p.az) + " horizon.");
}
}
function goTo(az, alt, fov, ms) {
var daz = ((az - st.az + Math.PI * 3) % TAU) - Math.PI;
if (reduce || !ms) {
st.az = az; st.alt = alt; if (fov) st.fov = fov; tween = null; syncZoom(); mark(); return;
}
tween = {
t0: performance.now(), ms: ms,
az0: st.az, daz: daz,
alt0: st.alt, dalt: alt - st.alt,
fov0: st.fov, dfov: (fov || st.fov) - st.fov
};
}
/* zoom */
function syncZoom() {
$("zoomIn").disabled = st.fov <= 22.01 * D2R;
$("zoomOut").disabled = st.fov >= 134.9 * D2R;
}
function zoom(mult) {
st.fov = clamp(st.fov * mult, 22 * D2R, 135 * D2R);
tween = null;
syncZoom();
mark();
}
$("zoomIn").addEventListener("click", function () { zoom(1 / 1.32); });
$("zoomOut").addEventListener("click", function () { zoom(1.32); });
cv.addEventListener("wheel", function (ev) {
ev.preventDefault();
zoom(Math.exp(clamp(ev.deltaY, -120, 120) * 0.0016));
}, { passive: false });
/* ═══════════════════════════════════════════════════════════════════════════
POINTER — drag to look around, tap to name
═══════════════════════════════════════════════════════════════════════════ */
var pts = {}, dragging = false, moved = 0, lastX = 0, lastY = 0, pinch = 0;
cv.addEventListener("pointerdown", function (ev) {
cv.setPointerCapture(ev.pointerId);
pts[ev.pointerId] = { x: ev.clientX, y: ev.clientY };
if (Object.keys(pts).length === 2) {
pinch = pinchDist();
} else {
dragging = true; moved = 0;
lastX = ev.clientX; lastY = ev.clientY;
cv.classList.add("grabbing");
tween = null;
}
hideHint();
});
function pinchDist() {
var k = Object.keys(pts);
if (k.length < 2) return 0;
var a = pts[k[0]], b = pts[k[1]];
return Math.hypot(a.x - b.x, a.y - b.y);
}
cv.addEventListener("pointermove", function (ev) {
if (pts[ev.pointerId]) { pts[ev.pointerId].x = ev.clientX; pts[ev.pointerId].y = ev.clientY; }
if (Object.keys(pts).length === 2) {
var d = pinchDist();
if (pinch > 8 && d > 8) { zoom(pinch / d); pinch = d; }
return;
}
if (dragging) {
var dx = ev.clientX - lastX, dy = ev.clientY - lastY;
lastX = ev.clientX; lastY = ev.clientY;
moved += Math.abs(dx) + Math.abs(dy);
var perPx = (2 * Math.tan(st.fov / 4)) / (Math.max(cw, ch) / 2) * 0.98;
st.az -= dx * perPx / Math.max(0.28, Math.cos(st.alt));
st.alt = clamp(st.alt + dy * perPx, -32 * D2R, 87 * D2R);
st.az = ((st.az % TAU) + TAU) % TAU;
mark();
hideTip();
return;
}
if (ev.pointerType === "mouse") hover(ev);
});
function endPointer(ev) {
if (pts[ev.pointerId]) delete pts[ev.pointerId];
if (Object.keys(pts).length < 2) pinch = 0;
if (dragging && Object.keys(pts).length === 0) {
dragging = false;
cv.classList.remove("grabbing");
if (moved < 6) pick(ev);
}
}
cv.addEventListener("pointerup", endPointer);
cv.addEventListener("pointercancel", endPointer);
cv.addEventListener("pointerleave", function () { hideTip(); });
function localXY(ev) {
var r = cv.getBoundingClientRect();
return { x: ev.clientX - r.left, y: ev.clientY - r.top };
}
function nearest(x, y, maxD) {
var best = null, bd = maxD * maxD;
for (var i = 0; i < vis.length; i++) {
var v = vis[i];
var dx = v.x - x, dy = v.y - y, d = dx * dx + dy * dy;
var pull = v.t === 1 ? 1.15 : (1 + Math.max(0, (3.2 - v.o.mag)) * 0.09);
d /= pull;
if (d < bd) { bd = d; best = v; }
}
return best;
}
function hover(ev) {
var p = localXY(ev);
var v = nearest(p.x, p.y, 17);
if (!v) { hideTip(); return; }
var o = v.o;
tipName.textContent = v.t === 1 ? o.name : starLabel(o);
tipMeta.textContent = v.t === 1
? (o.cat ? o.cat + " · " : "") + o.kind
: "mag " + o.mag.toFixed(2) + " · " + (FULL[o.con] || o.con);
/* the field clips anything that overflows it, so place the card by hand:
prefer the right of the star, fall back to the left, and never let either
edge leave the frame — which is what happens on a narrow window. */
tip.hidden = false;
var tw = tip.offsetWidth, th = tip.offsetHeight;
var left = v.x + 14;
if (left + tw > cw - 4) left = v.x - 14 - tw;
tip.style.left = Math.round(clamp(left, 4, Math.max(4, cw - tw - 4))) + "px";
tip.style.top = Math.round(clamp(v.y, th / 2 + 4, Math.max(th / 2 + 4, ch - th / 2 - 4))) + "px";
}
function hideTip() { tip.hidden = true; }
function pick(ev) {
var p = localXY(ev);
var v = nearest(p.x, p.y, 22);
if (v) select(v.o); else select(null);
}
/* ═══════════════════════════════════════════════════════════════════════════
KEYBOARD
═══════════════════════════════════════════════════════════════════════════ */
cv.addEventListener("keydown", function (ev) {
var step = st.fov * 0.13, used = true;
switch (ev.key) {
case "ArrowLeft": st.az -= step / Math.max(0.28, Math.cos(st.alt)); break;
case "ArrowRight": st.az += step / Math.max(0.28, Math.cos(st.alt)); break;
case "ArrowUp": st.alt = clamp(st.alt + step, -32 * D2R, 87 * D2R); break;
case "ArrowDown": st.alt = clamp(st.alt - step, -32 * D2R, 87 * D2R); break;
case "+": case "=": zoom(1 / 1.32); break;
case "-": case "_": zoom(1.32); break;
case "Enter": case " ":
var v = nearest(cw / 2, ch / 2, Math.min(cw, ch) * 0.42);
if (v) { select(v.o); }
else { say("Nothing catalogued near the middle of the view. Look around and try again."); }
break;
default: used = false;
}
if (used) {
ev.preventDefault();
tween = null;
st.az = ((st.az % TAU) + TAU) % TAU;
hideHint(); mark();
}
});
/* ═══════════════════════════════════════════════════════════════════════════
SELECTION PANEL
═══════════════════════════════════════════════════════════════════════════ */
var selEmpty = $("selEmpty"), selInfo = $("selInfo"), selName = $("selName"),
selMeta = $("selMeta"), selFact = $("selFact"),
selCentre = $("selCentre"), selJump = $("selJump"), selClear = $("selClear");
var selPrefix = "";
/* selPrefix is the part of the selection line that never changes; where the
thing actually is gets appended fresh, because it keeps moving. */
function refreshSel() {
if (!st.sel) return;
var w = whereOf(st.sel);
selMeta.textContent = selPrefix + w.text;
if (pinned && pinned.live) {
pinned.kind = w.up ? "up" : "down";
pill.className = "pill " + pinned.kind;
pillText.textContent = (st.sel.name || starLabel(st.sel)) + " · " + w.text;
}
}
function groupFor(key) {
for (var i = 0; i < GROUPS.length; i++) if (GROUPS[i].key === key) return GROUPS[i];
return null;
}
function brightWord(mag) {
if (mag < 0) return "one of the half-dozen brightest stars in the whole sky";
if (mag < 1.5) return "bright enough to spot from a lit street";
if (mag < 2.5) return "easy to pick out once your eyes adjust";
if (mag < 3.5) return "clear on a decent night away from street lights";
if (mag < 4.5) return "faint — you want a properly dark spot";
return "right at the edge of what eyes alone can do";
}
function select(o) {
st.sel = o;
mark();
if (!o) {
selEmpty.hidden = false; selInfo.hidden = true; selPrefix = ""; setPill(null);
return;
}
selEmpty.hidden = true; selInfo.hidden = false;
var w = whereOf(o);
if (o.kind) {
selName.textContent = o.name;
selPrefix = (o.cat ? o.cat + " · " : "") + o.kind + " · ";
selFact.innerHTML = o.note;
selJump.hidden = true;
say(o.name + ", " + o.kind + ", " + w.text + ".");
} else {
selName.textContent = starLabel(o);
selPrefix = "mag " + o.mag.toFixed(2) + " · " +
(SPWORD[o.sp] || "white") + " · " + (FULL[o.con] || o.con) + " · ";
var note = NOTE[o.name];
selFact.innerHTML = note ? note
: (/^[aeiou]/.test(SPWORD[o.sp] || "white") ? "An " : "A ") + (SPWORD[o.sp] || "white") + " star" +
(o.bay ? ", the " + o.bay + " of " + (FULL[o.con] || o.con) : "") +
" — at magnitude " + o.mag.toFixed(2) + " it is " + brightWord(o.mag) + ".";
/* 21 of the catalogued constellations are a single lone star with no
figure to draw — no point offering a button that can only apologise. */
var grp = groupFor(o.con);
selJump.hidden = !grp;
if (grp) selJump.textContent = "See all of " + grp.name;
say(starLabel(o) + ", magnitude " + o.mag.toFixed(2) + ", " + w.text + ".");
}
pinSel(); /* writes selMeta and the live pill from selPrefix */
}
selCentre.addEventListener("click", function () {
if (!st.sel) return;
var p = altaz(st.sel);
goTo(p.az, clamp(p.alt, -20 * D2R, 87 * D2R), clamp(st.fov, 24 * D2R, 60 * D2R), 620);
cv.focus();
});
selJump.addEventListener("click", function () {
if (!st.sel || st.sel.kind) return;
var g = groupFor(st.sel.con);
if (g) lookAt(g, clamp(g.rad * 2 * 2.4, 26 * D2R, 120 * D2R), g.name);
});
selClear.addEventListener("click", function () { select(null); cv.focus(); });
function say(msg) { live.textContent = msg; }
/* ═══════════════════════════════════════════════════════════════════════════
BOOT
═══════════════════════════════════════════════════════════════════════════ */
var hintGone = false;
function hideHint() {
if (hintGone) return;
hintGone = true;
hint.classList.add("gone");
setTimeout(function () { hint.style.display = "none"; }, 600);
}
setTimeout(hideHint, 9000);
$("capN").textContent = String(STARS.length);
syncTimeLabel();
setPlaying(st.playing);
syncZoom();
readTheme();
resize();
if (window.ResizeObserver) {
new ResizeObserver(resize).observe(field);
} else {
window.addEventListener("resize", resize);
}
/* open on Orion, already up and already turning */
select(BYNAME["Betelgeuse"]);
setPill(null);
live.textContent = "";
draw(performance.now());
readout();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.