@johnmorrisdotca/hoshi-stars 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +146 -0
  4. package/README.md +337 -2
  5. package/dist/boundaries.d.ts +95 -0
  6. package/dist/boundaries.js +195 -0
  7. package/dist/constellations.d.ts +72 -0
  8. package/dist/constellations.js +105 -0
  9. package/dist/data/boundaries.d.ts +4 -0
  10. package/dist/data/boundaries.js +449 -0
  11. package/dist/data/constellations.d.ts +1 -0
  12. package/dist/data/constellations.js +91 -0
  13. package/dist/data/lines.d.ts +2 -0
  14. package/dist/data/lines.js +92 -0
  15. package/dist/data/messier.d.ts +2 -0
  16. package/dist/data/messier.js +116 -0
  17. package/dist/data/milky-way.d.ts +5 -0
  18. package/dist/data/milky-way.js +7 -0
  19. package/dist/data/ngc-ic.d.ts +2 -0
  20. package/dist/data/ngc-ic.js +11794 -0
  21. package/dist/data/star-names-ja.d.ts +2 -0
  22. package/dist/data/star-names-ja.js +4 -0
  23. package/dist/data/stars.d.ts +4 -0
  24. package/dist/data/stars.js +5088 -0
  25. package/dist/deep-sky-core.d.ts +123 -0
  26. package/dist/deep-sky-core.js +139 -0
  27. package/dist/deep-sky-ngc-ic.d.ts +34 -0
  28. package/dist/deep-sky-ngc-ic.js +43 -0
  29. package/dist/deep-sky.d.ts +49 -0
  30. package/dist/deep-sky.js +59 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +2 -0
  33. package/dist/lines.d.ts +49 -0
  34. package/dist/lines.js +64 -0
  35. package/dist/milky-way.d.ts +144 -0
  36. package/dist/milky-way.js +242 -0
  37. package/dist/projection.d.ts +127 -0
  38. package/dist/projection.js +284 -0
  39. package/dist/sky.d.ts +202 -0
  40. package/dist/sky.js +283 -0
  41. package/dist/stars.d.ts +162 -0
  42. package/dist/stars.js +195 -0
  43. package/dist/time-scale.d.ts +14 -0
  44. package/dist/time-scale.js +71 -0
  45. package/dist/when.d.ts +16 -0
  46. package/dist/when.js +18 -0
  47. package/package.json +184 -4
package/dist/sky.js ADDED
@@ -0,0 +1,283 @@
1
+ // Your sky: where a star stands over your head at a place and a moment, and where it would stand over another planet's surface.
2
+ // Pure maths, no DOM, no clock reads and no dependencies. The formulas are written from J. Meeus, "Astronomical Algorithms", 2nd ed. (Willmann-Bell, 1998):
3
+ // chapter 12 (Greenwich mean sidereal time, the IAU 1982 expression), chapter 13 (equatorial to horizontal), chapter 16 (refraction, Saemundsson's formula) and chapter 21 (precession, the IAU 1976 constants).
4
+ // For another planet the sky turns about that planet's pole: the pole and prime meridian are the IAU Working Group on Cartographic Coordinates and Rotational Elements (WGCCRE) quantities, passed in, so the planet data stays with the caller.
5
+ // Azimuth runs from north through east (north 0, east 90, south 180, west 270), the compass way, not Meeus's way from south.
6
+ import { terrestrialMinusUtc } from "./time-scale.js";
7
+ import { J2000_MS, moment } from "./when.js";
8
+ const RAD = Math.PI / 180;
9
+ const DEG = 180 / Math.PI;
10
+ const DAY_MS = 86_400_000;
11
+ const ARCSEC = RAD / 3600;
12
+ function finite(name, value) {
13
+ if (typeof value !== "number" || !Number.isFinite(value))
14
+ throw new RangeError(`${name} must be a finite number, got ${String(value)}`);
15
+ return value;
16
+ }
17
+ function point(name, value) {
18
+ if (typeof value !== "object" || value === null)
19
+ throw new TypeError(`${name} must be an object with rightAscension and declination, got ${String(value)}`);
20
+ const p = value;
21
+ finite(`${name}.rightAscension`, p.rightAscension);
22
+ const declination = finite(`${name}.declination`, p.declination);
23
+ if (declination < -90 || declination > 90)
24
+ throw new RangeError(`${name}.declination must be between -90 and 90 degrees, got ${declination}`);
25
+ return p;
26
+ }
27
+ function place(name, value) {
28
+ if (typeof value !== "object" || value === null)
29
+ throw new TypeError(`${name} must be an object with latitude and longitude, got ${String(value)}`);
30
+ const o = value;
31
+ const latitude = finite(`${name}.latitude`, o.latitude);
32
+ if (latitude < -90 || latitude > 90)
33
+ throw new RangeError(`${name}.latitude must be between -90 and 90 degrees, got ${latitude}`);
34
+ finite(`${name}.longitude`, o.longitude);
35
+ if (o.heightM !== undefined)
36
+ finite(`${name}.heightM`, o.heightM);
37
+ return o;
38
+ }
39
+ /** An angle in degrees brought to 0 up to but not including 360. */
40
+ function wrap360(angle) {
41
+ const a = angle % 360;
42
+ const r = a < 0 ? a + 360 : a;
43
+ return r >= 360 ? 0 : r;
44
+ }
45
+ /** Julian centuries since J2000.0, taking the moment as UT (the seconds between UT and the dynamical time are far below what these formulas can see). */
46
+ function centuries(ms) {
47
+ return (ms - J2000_MS) / (DAY_MS * 36525);
48
+ }
49
+ function siderealDegrees(ms) {
50
+ const days = (ms - J2000_MS) / DAY_MS;
51
+ const T = days / 36525;
52
+ return 280.46061837 + 360.98564736629 * days + 0.000387933 * T * T - (T * T * T) / 38_710_000;
53
+ }
54
+ /**
55
+ * The Greenwich mean sidereal time: how far the Earth has turned against the stars, as the right ascension on the meridian of Greenwich, in degrees (Meeus 12.4, the IAU 1982 expression). It uses UT, which is UTC to under a second, and is the mean time, without the nutation term (at most about 1.1 seconds of time, 17 arc-seconds).
56
+ *
57
+ * @param when - The moment, a `Date` or milliseconds since 1970.
58
+ * @returns Degrees, 0 up to but not including 360. Divide by 15 for hours.
59
+ * @throws TypeError if `when` is not a Date or a number, RangeError if it is an invalid Date or not finite.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import { greenwichSiderealTime } from "@johnmorrisdotca/hoshi-stars/sky";
64
+ *
65
+ * // Meeus example 12.a: 1987 April 10 at 0h UT is 13h10m46.3668s, 197.693195 degrees
66
+ * console.log(greenwichSiderealTime(Date.UTC(1987, 3, 10)).toFixed(6));
67
+ * ```
68
+ */
69
+ export function greenwichSiderealTime(when) {
70
+ return wrap360(siderealDegrees(moment(when)));
71
+ }
72
+ /**
73
+ * The local mean sidereal time: the right ascension that is on your meridian, due south or north of you, in degrees. A star with this right ascension is crossing the meridian.
74
+ *
75
+ * @param when - The moment, a `Date` or milliseconds since 1970.
76
+ * @param longitude - Degrees east of Greenwich (west is negative). Any finite number.
77
+ * @returns Degrees, 0 up to but not including 360.
78
+ * @throws TypeError if `when` is not a Date or a number, RangeError if it is invalid or `longitude` is not finite.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * import { localSiderealTime } from "@johnmorrisdotca/hoshi-stars/sky";
83
+ *
84
+ * // Greenwich's 197.693195 degrees at 0h UT, 1987 April 10, and Washington is 77.07 degrees west of it
85
+ * console.log(localSiderealTime(Date.UTC(1987, 3, 10), -77.0656).toFixed(3)); // 120.628
86
+ * ```
87
+ */
88
+ export function localSiderealTime(when, longitude) {
89
+ const ms = moment(when);
90
+ return wrap360(siderealDegrees(ms) + finite("longitude", longitude));
91
+ }
92
+ /** The three IAU 1976 precession angles zeta, z and theta, in radians, from J2000.0 to T Julian centuries after it (Meeus 21.2). */
93
+ function precessionAngles(T) {
94
+ const zeta = (2306.2181 * T + 0.30188 * T * T + 0.017998 * T * T * T) * ARCSEC;
95
+ const z = (2306.2181 * T + 1.09468 * T * T + 0.018203 * T * T * T) * ARCSEC;
96
+ const theta = (2004.3109 * T - 0.42665 * T * T - 0.041833 * T * T * T) * ARCSEC;
97
+ return [zeta, z, theta];
98
+ }
99
+ /**
100
+ * A J2000 position carried to the mean equator and equinox of a date: the precession of the Earth's axis (Meeus chapter 21, the rigorous method with the IAU 1976 constants, good for a few centuries either side of 2000 to far under an arc-second). It leaves out the star's own proper motion (which `starPosition` adds), nutation and aberration, together under a minute of arc.
101
+ *
102
+ * @param position - A right ascension and declination in degrees at J2000.
103
+ * @param when - The date to carry it to.
104
+ * @returns The right ascension (0 up to but not including 360) and declination in degrees.
105
+ * @throws TypeError or RangeError, naming the argument, if `position` or `when` is not valid.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { precess } from "@johnmorrisdotca/hoshi-stars/sky";
110
+ *
111
+ * // Meeus example 21.b: theta Persei, carried to 2028 November 13.19 (about 15 minutes of arc east in right ascension)
112
+ * const there = precess({ rightAscension: 41.054063, declination: 49.227750 }, Date.UTC(2028, 10, 13, 4, 33, 36));
113
+ * console.log(there.rightAscension.toFixed(3), there.declination.toFixed(3)); // 41.547 49.348
114
+ * ```
115
+ */
116
+ export function precess(position, when) {
117
+ point("position", position);
118
+ const T = centuries(moment(when));
119
+ const [zeta, z, theta] = precessionAngles(T);
120
+ const a0 = position.rightAscension * RAD + zeta;
121
+ const d0 = position.declination * RAD;
122
+ const cosD = Math.cos(d0);
123
+ const A = cosD * Math.sin(a0);
124
+ const B = Math.cos(theta) * cosD * Math.cos(a0) - Math.sin(theta) * Math.sin(d0);
125
+ const C = Math.sin(theta) * cosD * Math.cos(a0) + Math.cos(theta) * Math.sin(d0);
126
+ const declination = Math.abs(C) < 0.9999 ? Math.asin(C) : Math.sign(C) * Math.acos(Math.hypot(A, B));
127
+ return { rightAscension: wrap360((Math.atan2(A, B) + z) * DEG), declination: declination * DEG };
128
+ }
129
+ /** The east, north and up parts of a direction, from its declination and hour angle (Meeus 13.5 and 13.6, turned to measure azimuth from north through east), for an observer at latitude phi. */
130
+ function toHorizontal(cosDcosH, cosDsinH, sinD, sinPhi, cosPhi) {
131
+ const east = -cosDsinH;
132
+ const north = sinD * cosPhi - cosDcosH * sinPhi;
133
+ const up = sinD * sinPhi + cosDcosH * cosPhi;
134
+ const across = Math.hypot(east, north);
135
+ return { altitude: Math.atan2(up, across) * DEG, azimuth: across < 1e-15 ? 0 : wrap360(Math.atan2(east, north) * DEG) };
136
+ }
137
+ /** Saemundsson's refraction (Meeus 16.4) in degrees for a true altitude in degrees, scaled for the air; 0 below -1 degree. */
138
+ function refractionDegrees(altitude, pressureHpa, temperatureC) {
139
+ if (altitude < -1)
140
+ return 0;
141
+ // the +0.0019279 arc-minute makes the bending exactly 0 at the zenith, where the bare formula gives a hair under
142
+ const arcminutes = 1.02 / Math.tan((altitude + 10.3 / (altitude + 5.11)) * RAD) + 0.0019279;
143
+ return (arcminutes / 60) * (pressureHpa / 1010) * (283 / (273 + temperatureC));
144
+ }
145
+ /**
146
+ * Where a star (or any J2000 position) stands in the sky of a place on Earth at a moment: its altitude above the horizon and azimuth from north through east. The position is carried from J2000 to the equinox of the date (`precess`), the hour angle comes from the local mean sidereal time, and the horizon formulas are Meeus's (chapter 13). The result is geometric (no refraction) unless you ask for it. UTC stands for UT; nutation, aberration and parallax are left out, so it is good to about an arc-minute, far finer than the eye. The star's own proper motion is not added: pass `starPosition(star, when)` if you want it.
147
+ *
148
+ * @param position - A right ascension and declination in degrees, J2000 (the catalogue's frame), or of the date with `equinox: "date"`.
149
+ * @param observer - Latitude north and longitude east in degrees, and optionally `heightM`.
150
+ * @param when - The moment, a `Date` or milliseconds since 1970.
151
+ * @param options - `equinox`, and `refraction` with `pressureHpa` and `temperatureC`.
152
+ * @returns `{ altitude, azimuth }` in degrees.
153
+ * @throws TypeError or RangeError, naming the argument and the value, for a position, observer, moment or option that is not valid (a latitude or declination outside -90 to 90, a number that is not finite, a `pressureHpa` of 0 or less).
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * import { starByName, starPosition } from "@johnmorrisdotca/hoshi-stars";
158
+ * import { horizontal } from "@johnmorrisdotca/hoshi-stars/sky";
159
+ *
160
+ * const polaris = starPosition(starByName("Polaris")!);
161
+ * const view = horizontal(polaris, { latitude: 49.28, longitude: -123.12 }, new Date("2026-11-08T06:00:00Z"));
162
+ * console.log(view.altitude.toFixed(1)); // 49.8: within a degree of the latitude, 49.28
163
+ * ```
164
+ */
165
+ export function horizontal(position, observer, when, options = {}) {
166
+ point("position", position);
167
+ const o = place("observer", observer);
168
+ const ms = moment(when);
169
+ const equinox = options.equinox ?? "J2000";
170
+ if (equinox !== "J2000" && equinox !== "date")
171
+ throw new RangeError(`options.equinox must be "J2000" or "date", got ${String(equinox)}`);
172
+ let pressure = 1010;
173
+ let temperature = 10;
174
+ if (options.refraction === true) {
175
+ if (options.pressureHpa !== undefined && !(finite("options.pressureHpa", options.pressureHpa) > 0))
176
+ throw new RangeError(`options.pressureHpa must be greater than 0, got ${options.pressureHpa}`);
177
+ if (options.temperatureC !== undefined && !(finite("options.temperatureC", options.temperatureC) > -100))
178
+ throw new RangeError(`options.temperatureC must be above -100, got ${options.temperatureC}`);
179
+ pressure = options.pressureHpa ?? pressure;
180
+ temperature = options.temperatureC ?? temperature;
181
+ }
182
+ const of = equinox === "date" ? position : precess(position, ms);
183
+ const hourAngle = (siderealDegrees(ms) + o.longitude - of.rightAscension) * RAD;
184
+ const dec = of.declination * RAD;
185
+ const phi = o.latitude * RAD;
186
+ const cosD = Math.cos(dec);
187
+ const view = toHorizontal(cosD * Math.cos(hourAngle), cosD * Math.sin(hourAngle), Math.sin(dec), Math.sin(phi), Math.cos(phi));
188
+ if (options.refraction === true)
189
+ view.altitude += refractionDegrees(view.altitude, pressure, temperature);
190
+ return view;
191
+ }
192
+ /**
193
+ * Whether a position is above the horizon of a place at a moment (altitude greater than 0). With `refraction: true` it counts the bending of the light, so a star can be seen a little below the geometric horizon.
194
+ *
195
+ * @param position - A right ascension and declination in degrees, J2000 or (with `equinox: "date"`) of the date.
196
+ * @param observer - Latitude north and longitude east in degrees.
197
+ * @param when - The moment, a `Date` or milliseconds since 1970.
198
+ * @param options - The same as `horizontal`.
199
+ * @returns True when it is up.
200
+ * @throws TypeError or RangeError, as `horizontal` does.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * import { starByName, starPosition } from "@johnmorrisdotca/hoshi-stars";
205
+ * import { isAboveHorizon } from "@johnmorrisdotca/hoshi-stars/sky";
206
+ *
207
+ * // Polaris never sets for anyone in the northern hemisphere
208
+ * const polaris = starPosition(starByName("Polaris")!);
209
+ * console.log(isAboveHorizon(polaris, { latitude: 49.28, longitude: -123.12 }, Date.now()));
210
+ * ```
211
+ */
212
+ export function isAboveHorizon(position, observer, when, options = {}) {
213
+ return horizontal(position, observer, when, options).altitude > 0;
214
+ }
215
+ /**
216
+ * Where a star stands in the sky of a place on ANOTHER planet: the sky turns about that planet's pole (the IAU WGCCRE pole and prime meridian, passed in as `spin`), so the star is rotated into the planet's own equatorial frame, its hour angle is the prime meridian plus the observer's east longitude minus its right ascension there, and the horizon formulas are the same as for Earth.
217
+ *
218
+ * The stars look the same from every planet: the nearest star, Proxima Centauri, shifts only about 40 arc-seconds seen from Pluto (annual parallax 0.77 arc-seconds times Pluto's 49 AU at its farthest), and a typical star far less, so a star's J2000 position serves anywhere in the solar system. Only the pole and the turning change.
219
+ *
220
+ * Limits: the latitude is used as the angle from the planet's equator on a sphere (the difference between planetographic and planetocentric latitude, up to a fifth of a degree on Mars, is ignored); the pole's slow drift is the caller's to add in if wanted (`spin.pole` is read as given); there is no atmosphere, and no precession of the pole is applied. `when` is UTC, like every moment in the package: it is turned into Terrestrial Time inside, the scale the IAU rates are in (69.184 s ahead of UTC since 2017, by the leap-second table back to 1972 and by Espenak and Meeus's delta T before it; a leap second announced after 2017 is not known until the table gets its row). TDB differs from TT by under 2 milliseconds, which is ignored. Without the conversion Mars would be turned 0.28 degrees out.
221
+ *
222
+ * @param position - A right ascension and declination in degrees at J2000.
223
+ * @param spin - The planet's `pole` (right ascension and declination, degrees) and `primeMeridian` (`w0` degrees at J2000 and `rate` degrees a day).
224
+ * @param observer - Latitude north and longitude east on that planet, in degrees (north being the side of the pole).
225
+ * @param when - The moment, a `Date` or milliseconds since 1970.
226
+ * @returns `{ altitude, azimuth }` in degrees, the azimuth from north through east.
227
+ * @throws TypeError or RangeError, naming the argument and the value, for a position, spin, observer or moment that is not valid.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * import { starByName, starPosition } from "@johnmorrisdotca/hoshi-stars";
232
+ * import { horizontalFrom } from "@johnmorrisdotca/hoshi-stars/sky";
233
+ *
234
+ * // Mars, IAU 2015 values: its north celestial pole is only 10 degrees from Deneb, so from the pole Deneb stands almost overhead
235
+ * const mars = { pole: { rightAscension: 317.269202, declination: 54.432516 }, primeMeridian: { w0: 176.049863, rate: 350.891982443297 } };
236
+ * const deneb = starPosition(starByName("Deneb")!);
237
+ * console.log(horizontalFrom(deneb, mars, { latitude: 90, longitude: 0 }, Date.UTC(2026, 10, 8)).altitude.toFixed(1)); // 79.8
238
+ * ```
239
+ */
240
+ export function horizontalFrom(position, spin, observer, when) {
241
+ point("position", position);
242
+ if (typeof spin !== "object" || spin === null)
243
+ throw new TypeError(`spin must be an object with a pole and a primeMeridian, got ${String(spin)}`);
244
+ point("spin.pole", spin.pole);
245
+ const meridian = spin.primeMeridian;
246
+ if (typeof meridian !== "object" || meridian === null)
247
+ throw new TypeError(`spin.primeMeridian must be an object with w0 and rate, got ${String(meridian)}`);
248
+ const w0 = finite("spin.primeMeridian.w0", meridian.w0);
249
+ const rate = finite("spin.primeMeridian.rate", meridian.rate);
250
+ const o = place("observer", observer);
251
+ const utc = moment(when);
252
+ const days = (utc + terrestrialMinusUtc(utc) * 1000 - J2000_MS) / DAY_MS;
253
+ const turned = (w0 + rate * days + o.longitude) * RAD;
254
+ // The planet's frame: z toward its pole, x toward the node where its equator crosses the ICRF equator (right ascension of the pole + 90 degrees), y = z cross x.
255
+ const a0 = spin.pole.rightAscension * RAD;
256
+ const d0 = spin.pole.declination * RAD;
257
+ const sinA = Math.sin(a0);
258
+ const cosA = Math.cos(a0);
259
+ const sinD0 = Math.sin(d0);
260
+ const cosD0 = Math.cos(d0);
261
+ const zx = cosD0 * cosA;
262
+ const zy = cosD0 * sinA;
263
+ const zz = sinD0;
264
+ const xx = -sinA;
265
+ const xy = cosA;
266
+ const yx = -zz * xy;
267
+ const yy = zz * xx;
268
+ const yz = zx * xy - zy * xx;
269
+ const dec = position.declination * RAD;
270
+ const ra = position.rightAscension * RAD;
271
+ const cosD = Math.cos(dec);
272
+ const sx = cosD * Math.cos(ra);
273
+ const sy = cosD * Math.sin(ra);
274
+ const sz = Math.sin(dec);
275
+ const px = sx * xx + sy * xy;
276
+ const py = sx * yx + sy * yy + sz * yz;
277
+ const pz = sx * zx + sy * zy + sz * zz;
278
+ // cos(dec') cos(H) and cos(dec') sin(H), with H = turned - alpha'
279
+ const sinT = Math.sin(turned);
280
+ const cosT = Math.cos(turned);
281
+ const phi = o.latitude * RAD;
282
+ return toHorizontal(px * cosT + py * sinT, px * sinT - py * cosT, pz, Math.sin(phi), Math.cos(phi));
283
+ }
@@ -0,0 +1,162 @@
1
+ import { type When } from "./when.ts";
2
+ /**
3
+ * One star, as the Yale Bright Star Catalogue gives it. Positions are J2000 (equator and equinox of 2000, the FK5 frame, which is the ICRS to a fraction of an arc-second).
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { starByName, type Star } from "@johnmorrisdotca/hoshi-stars";
8
+ *
9
+ * const sirius: Star = starByName("Sirius")!;
10
+ * console.log(sirius.designation, sirius.magnitude);
11
+ * ```
12
+ */
13
+ export interface Star {
14
+ /** The Bright Star Catalogue (Harvard Revised) number: stable, and the key for `starByHr`. */
15
+ hr: number;
16
+ /** The common name where the star has one (the IAU Working Group on Star Names' spelling), else `undefined`. */
17
+ name: string | undefined;
18
+ /** The name in katakana (`シリウス`) for every star that has a `name`, else `undefined`. */
19
+ nameJa: string | undefined;
20
+ /** The Bayer letter and constellation (`α CMa`), else the Flamsteed number and constellation (`9 CMa`), else `undefined`. */
21
+ designation: string | undefined;
22
+ /** The IAU three-letter constellation abbreviation (`CMa`) the star lies in, for every star: the catalogue's where the star has a Bayer letter or Flamsteed number, else the one the boundaries give. */
23
+ constellation: string;
24
+ /** Right ascension in degrees at J2000, 0 up to 360. The catalogue gives it to a tenth of a second of time (1.5 arc-seconds at the equator). */
25
+ rightAscension: number;
26
+ /** Declination in degrees at J2000, -90 to 90. The catalogue gives it to an arc-second. */
27
+ declination: number;
28
+ /** Visual magnitude: smaller is brighter, and Sirius is -1.46. */
29
+ magnitude: number;
30
+ /** The colour index B-V in magnitudes, or `undefined` where the catalogue has none. Hot blue stars are about -0.3, the Sun 0.65, cool red stars above 1.5. */
31
+ colourIndex: number | undefined;
32
+ /** The spectral class and subclass (`A1`, `K1`), or an empty string where the catalogue gives none we read. */
33
+ spectralClass: string;
34
+ /** Proper motion in right ascension, in milliarcseconds a year, already multiplied by cos(declination) (a true angle on the sky). */
35
+ properMotionRaMas: number;
36
+ /** Proper motion in declination, in milliarcseconds a year. */
37
+ properMotionDecMas: number;
38
+ /** The annual parallax in milliarcseconds; 0 where the catalogue has none (the distance is then unknown). */
39
+ parallaxMas: number;
40
+ }
41
+ /**
42
+ * The stars to a magnitude, brightest first. The catalogue stops at magnitude 6.0, so the default is all 5,080.
43
+ *
44
+ * @param options - `magnitude`: the faintest to include (a star of exactly that magnitude is in). Brighter limits give shorter lists: 1 is 15 stars, 4 is 518, 5 is 1,630.
45
+ * @returns A fresh array of the shared, frozen stars.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { stars } from "@johnmorrisdotca/hoshi-stars";
50
+ *
51
+ * console.log(stars().length, stars({ magnitude: 2 }).map((s) => s.name ?? s.designation).slice(0, 3));
52
+ * ```
53
+ */
54
+ export declare function stars(options?: {
55
+ magnitude?: number;
56
+ }): Star[];
57
+ /**
58
+ * A star by its Bright Star Catalogue number.
59
+ *
60
+ * @param hr - The HR number (Sirius is 2491).
61
+ * @returns The star, or `undefined` if that number is not in this table (the table holds the stars to magnitude 6.0 only).
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { starByHr } from "@johnmorrisdotca/hoshi-stars";
66
+ *
67
+ * console.log(starByHr(2491)?.name);
68
+ * ```
69
+ */
70
+ export declare function starByHr(hr: number): Star | undefined;
71
+ /**
72
+ * A star by its common name (`Sirius`), its name in katakana (`シリウス`) or its designation (`α CMa`, `9 CMa`), ignoring case.
73
+ *
74
+ * @param text - The name or designation.
75
+ * @returns The star, or `undefined`.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { starByName } from "@johnmorrisdotca/hoshi-stars";
80
+ *
81
+ * console.log(starByName("betelgeuse")?.designation, starByName("α Lyr")?.name, starByName("シリウス")?.hr);
82
+ * ```
83
+ */
84
+ export declare function starByName(text: string): Star | undefined;
85
+ /**
86
+ * Where a star is at a moment: its J2000 position carried forward (or back) by its proper motion in a straight line. The frame stays J2000, so precession to the date is not applied: use
87
+ * the result with J2000 coordinates, or convert it to the date with `/coordinates`. A straight line is good to a fraction of an arc-second for centuries for nearly every star here; it is
88
+ * not good for the few with large motion over millennia.
89
+ *
90
+ * @param star - A star.
91
+ * @param when - The moment as a `Date` or milliseconds since 1970 (UTC); J2000 when left out.
92
+ * @returns Right ascension and declination in degrees, J2000 frame, at that moment.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { starByName, starPosition } from "@johnmorrisdotca/hoshi-stars";
97
+ *
98
+ * const now = starPosition(starByName("Arcturus")!, Date.UTC(2026, 10, 7));
99
+ * console.log(now.declination.toFixed(3));
100
+ * ```
101
+ */
102
+ export declare function starPosition(star: Star, when?: When): {
103
+ rightAscension: number;
104
+ declination: number;
105
+ };
106
+ /**
107
+ * A star's distance from its parallax, in light-years.
108
+ *
109
+ * @param star - A star.
110
+ * @returns Light-years, or `undefined` where the catalogue has no parallax (or a non-positive one).
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import { starByName, starDistanceLightYears } from "@johnmorrisdotca/hoshi-stars";
115
+ *
116
+ * console.log(starDistanceLightYears(starByName("Sirius")!)?.toFixed(1));
117
+ * ```
118
+ */
119
+ export declare function starDistanceLightYears(star: Star): number | undefined;
120
+ /**
121
+ * The surface temperature a colour index implies, in kelvin, by Ballesteros's relation between B-V and a black body. It is an estimate to a few hundred kelvin for most stars, and it runs cool for the hottest (about 16,600 K at -0.3).
122
+ *
123
+ * @param colourIndex - B-V in magnitudes.
124
+ * @returns Kelvin.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { starTemperatureK } from "@johnmorrisdotca/hoshi-stars";
129
+ *
130
+ * console.log(Math.round(starTemperatureK(0.65)));
131
+ * ```
132
+ */
133
+ export declare function starTemperatureK(colourIndex: number): number;
134
+ /**
135
+ * The colour of a black body at a temperature, as a `#rrggbb` string, for drawing a star. It is the colour a camera would show, not a measurement, and it uses an approximation of the
136
+ * black-body locus (good to a few percent between 1,000 and 40,000 K).
137
+ *
138
+ * @param kelvin - Temperature in kelvin.
139
+ * @returns A colour like `#a4bdff`.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { blackBodyColour } from "@johnmorrisdotca/hoshi-stars";
144
+ *
145
+ * console.log(blackBodyColour(10000), blackBodyColour(3500));
146
+ * ```
147
+ */
148
+ export declare function blackBodyColour(kelvin: number): string;
149
+ /**
150
+ * The colour to draw a star: from its B-V index, or a plain white where the catalogue has none.
151
+ *
152
+ * @param star - A star.
153
+ * @returns A `#rrggbb` string.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * import { starByName, starColour } from "@johnmorrisdotca/hoshi-stars";
158
+ *
159
+ * console.log(starColour(starByName("Betelgeuse")!), starColour(starByName("Rigel")!));
160
+ * ```
161
+ */
162
+ export declare function starColour(star: Star): string;
package/dist/stars.js ADDED
@@ -0,0 +1,195 @@
1
+ // The stars you can see without a telescope: the 5,080 of the Yale Bright Star Catalogue to visual magnitude 6.0, with their names, positions, proper motion, brightness and colour.
2
+ // The bright-star tier (`@johnmorrisdotca/hoshi-stars`): the table is about 100 kB gzipped and nothing else imports it. Pure: no DOM, no three.js, no state beyond the parsed table.
3
+ import { J2000_MS } from "./when.js";
4
+ import { STAR_NAMES, STAR_ROWS } from "./data/stars.js";
5
+ import { STAR_NAMES_JA } from "./data/star-names-ja.js";
6
+ import { moment } from "./when.js";
7
+ const stored = [];
8
+ const byHr = new Map();
9
+ const byText = new Map();
10
+ function load() {
11
+ if (stored.length > 0)
12
+ return;
13
+ for (const row of STAR_ROWS.split("\n")) {
14
+ const p = row.split("|");
15
+ const hr = Number(p[0]);
16
+ const letter = p[1];
17
+ const flamsteed = p[2];
18
+ const constellation = p[3];
19
+ const designation = constellation === "" ? undefined : `${letter !== "" ? letter : flamsteed} ${constellation}`;
20
+ const star = Object.freeze({
21
+ hr,
22
+ name: STAR_NAMES[hr],
23
+ nameJa: STAR_NAMES_JA[hr],
24
+ designation: letter === "" && flamsteed === "" ? undefined : designation,
25
+ constellation,
26
+ rightAscension: (Number(p[4]) / 2400),
27
+ declination: Number(p[5]) / 3600,
28
+ magnitude: Number(p[8]) / 100,
29
+ colourIndex: p[9] === "" ? undefined : Number(p[9]) / 100,
30
+ spectralClass: p[10],
31
+ properMotionRaMas: Number(p[6]),
32
+ properMotionDecMas: Number(p[7]),
33
+ parallaxMas: Number(p[11]),
34
+ });
35
+ stored.push(star);
36
+ byHr.set(hr, star);
37
+ for (const text of [star.name, star.nameJa, star.designation])
38
+ if (text !== undefined)
39
+ byText.set(text.toLowerCase(), star);
40
+ }
41
+ }
42
+ /**
43
+ * The stars to a magnitude, brightest first. The catalogue stops at magnitude 6.0, so the default is all 5,080.
44
+ *
45
+ * @param options - `magnitude`: the faintest to include (a star of exactly that magnitude is in). Brighter limits give shorter lists: 1 is 15 stars, 4 is 518, 5 is 1,630.
46
+ * @returns A fresh array of the shared, frozen stars.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * import { stars } from "@johnmorrisdotca/hoshi-stars";
51
+ *
52
+ * console.log(stars().length, stars({ magnitude: 2 }).map((s) => s.name ?? s.designation).slice(0, 3));
53
+ * ```
54
+ */
55
+ export function stars(options = {}) {
56
+ load();
57
+ const limit = options.magnitude ?? Number.POSITIVE_INFINITY;
58
+ if (typeof limit !== "number" || Number.isNaN(limit))
59
+ throw new TypeError(`magnitude must be a number, got ${String(limit)}`);
60
+ return stored.filter((star) => star.magnitude <= limit);
61
+ }
62
+ /**
63
+ * A star by its Bright Star Catalogue number.
64
+ *
65
+ * @param hr - The HR number (Sirius is 2491).
66
+ * @returns The star, or `undefined` if that number is not in this table (the table holds the stars to magnitude 6.0 only).
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { starByHr } from "@johnmorrisdotca/hoshi-stars";
71
+ *
72
+ * console.log(starByHr(2491)?.name);
73
+ * ```
74
+ */
75
+ export function starByHr(hr) {
76
+ load();
77
+ return byHr.get(hr);
78
+ }
79
+ /**
80
+ * A star by its common name (`Sirius`), its name in katakana (`シリウス`) or its designation (`α CMa`, `9 CMa`), ignoring case.
81
+ *
82
+ * @param text - The name or designation.
83
+ * @returns The star, or `undefined`.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { starByName } from "@johnmorrisdotca/hoshi-stars";
88
+ *
89
+ * console.log(starByName("betelgeuse")?.designation, starByName("α Lyr")?.name, starByName("シリウス")?.hr);
90
+ * ```
91
+ */
92
+ export function starByName(text) {
93
+ load();
94
+ return byText.get(text.trim().toLowerCase());
95
+ }
96
+ const JULIAN_YEAR_MS = 365.25 * 86_400_000;
97
+ /**
98
+ * Where a star is at a moment: its J2000 position carried forward (or back) by its proper motion in a straight line. The frame stays J2000, so precession to the date is not applied: use
99
+ * the result with J2000 coordinates, or convert it to the date with `/coordinates`. A straight line is good to a fraction of an arc-second for centuries for nearly every star here; it is
100
+ * not good for the few with large motion over millennia.
101
+ *
102
+ * @param star - A star.
103
+ * @param when - The moment as a `Date` or milliseconds since 1970 (UTC); J2000 when left out.
104
+ * @returns Right ascension and declination in degrees, J2000 frame, at that moment.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * import { starByName, starPosition } from "@johnmorrisdotca/hoshi-stars";
109
+ *
110
+ * const now = starPosition(starByName("Arcturus")!, Date.UTC(2026, 10, 7));
111
+ * console.log(now.declination.toFixed(3));
112
+ * ```
113
+ */
114
+ export function starPosition(star, when) {
115
+ const years = (moment(when ?? J2000_MS) - J2000_MS) / JULIAN_YEAR_MS;
116
+ const declination = star.declination + (star.properMotionDecMas * years) / 3_600_000;
117
+ const cosine = Math.cos((star.declination * Math.PI) / 180);
118
+ // The motion is a true angle on the sky: it stretches by 1 / cos(declination) in right ascension. At the poles the division is kept finite.
119
+ const rightAscension = star.rightAscension + (star.properMotionRaMas * years) / 3_600_000 / Math.max(Math.abs(cosine), 1e-6);
120
+ return { rightAscension: ((rightAscension % 360) + 360) % 360, declination: Math.max(-90, Math.min(90, declination)) };
121
+ }
122
+ /**
123
+ * A star's distance from its parallax, in light-years.
124
+ *
125
+ * @param star - A star.
126
+ * @returns Light-years, or `undefined` where the catalogue has no parallax (or a non-positive one).
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { starByName, starDistanceLightYears } from "@johnmorrisdotca/hoshi-stars";
131
+ *
132
+ * console.log(starDistanceLightYears(starByName("Sirius")!)?.toFixed(1));
133
+ * ```
134
+ */
135
+ export function starDistanceLightYears(star) {
136
+ return star.parallaxMas > 0 ? 3261.563777 / star.parallaxMas : undefined;
137
+ }
138
+ /**
139
+ * The surface temperature a colour index implies, in kelvin, by Ballesteros's relation between B-V and a black body. It is an estimate to a few hundred kelvin for most stars, and it runs cool for the hottest (about 16,600 K at -0.3).
140
+ *
141
+ * @param colourIndex - B-V in magnitudes.
142
+ * @returns Kelvin.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * import { starTemperatureK } from "@johnmorrisdotca/hoshi-stars";
147
+ *
148
+ * console.log(Math.round(starTemperatureK(0.65)));
149
+ * ```
150
+ */
151
+ export function starTemperatureK(colourIndex) {
152
+ if (!Number.isFinite(colourIndex))
153
+ throw new RangeError(`colourIndex must be a finite number, got ${colourIndex}`);
154
+ return 4600 * (1 / (0.92 * colourIndex + 1.7) + 1 / (0.92 * colourIndex + 0.62));
155
+ }
156
+ /**
157
+ * The colour of a black body at a temperature, as a `#rrggbb` string, for drawing a star. It is the colour a camera would show, not a measurement, and it uses an approximation of the
158
+ * black-body locus (good to a few percent between 1,000 and 40,000 K).
159
+ *
160
+ * @param kelvin - Temperature in kelvin.
161
+ * @returns A colour like `#a4bdff`.
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * import { blackBodyColour } from "@johnmorrisdotca/hoshi-stars";
166
+ *
167
+ * console.log(blackBodyColour(10000), blackBodyColour(3500));
168
+ * ```
169
+ */
170
+ export function blackBodyColour(kelvin) {
171
+ if (!Number.isFinite(kelvin) || kelvin <= 0)
172
+ throw new RangeError(`kelvin must be a positive number, got ${kelvin}`);
173
+ const t = Math.min(40_000, Math.max(1_000, kelvin)) / 100;
174
+ const red = t <= 66 ? 255 : 329.698727446 * (t - 60) ** -0.1332047592;
175
+ const green = t <= 66 ? 99.4708025861 * Math.log(t) - 161.1195681661 : 288.1221695283 * (t - 60) ** -0.0755148492;
176
+ const blue = t >= 66 ? 255 : t <= 19 ? 0 : 138.5177312231 * Math.log(t - 10) - 305.0447927307;
177
+ const byte = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
178
+ return `#${byte(red)}${byte(green)}${byte(blue)}`;
179
+ }
180
+ /**
181
+ * The colour to draw a star: from its B-V index, or a plain white where the catalogue has none.
182
+ *
183
+ * @param star - A star.
184
+ * @returns A `#rrggbb` string.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * import { starByName, starColour } from "@johnmorrisdotca/hoshi-stars";
189
+ *
190
+ * console.log(starColour(starByName("Betelgeuse")!), starColour(starByName("Rigel")!));
191
+ * ```
192
+ */
193
+ export function starColour(star) {
194
+ return star.colourIndex === undefined ? "#ffffff" : blackBodyColour(starTemperatureK(star.colourIndex));
195
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The offset TT - UTC in seconds at a moment given in UTC: 42.184 s at the start of 1972 and 69.184 s from 2017 on; before 1972, delta T (TT - UT) from the Espenak and Meeus fit.
3
+ *
4
+ * @param utcMs - Milliseconds since 1970 (UTC), finite.
5
+ * @returns Seconds to add to UTC to get Terrestrial Time.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { terrestrialMinusUtc } from "./time-scale.ts";
10
+ *
11
+ * console.log(terrestrialMinusUtc(Date.UTC(2026, 10, 8)), terrestrialMinusUtc(Date.UTC(2000, 0, 1)));
12
+ * ```
13
+ */
14
+ export declare function terrestrialMinusUtc(utcMs: number): number;