@johnmorrisdotca/wakusei-planets 0.0.0-stage → 0.3.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 (114) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +48 -0
  4. package/README.md +214 -2
  5. package/dist/bodies/all.d.ts +16 -0
  6. package/dist/bodies/all.js +16 -0
  7. package/dist/bodies/earth.d.ts +1 -0
  8. package/dist/bodies/earth.js +23 -0
  9. package/dist/bodies/jupiter.d.ts +1 -0
  10. package/dist/bodies/jupiter.js +22 -0
  11. package/dist/bodies/mars.d.ts +1 -0
  12. package/dist/bodies/mars.js +25 -0
  13. package/dist/bodies/mercury.d.ts +1 -0
  14. package/dist/bodies/mercury.js +9 -0
  15. package/dist/bodies/moons-earth.d.ts +1 -0
  16. package/dist/bodies/moons-earth.js +37 -0
  17. package/dist/bodies/moons-jupiter.d.ts +1 -0
  18. package/dist/bodies/moons-jupiter.js +29 -0
  19. package/dist/bodies/moons-mars.d.ts +1 -0
  20. package/dist/bodies/moons-mars.js +4 -0
  21. package/dist/bodies/moons-neptune.d.ts +1 -0
  22. package/dist/bodies/moons-neptune.js +11 -0
  23. package/dist/bodies/moons-pluto.d.ts +1 -0
  24. package/dist/bodies/moons-pluto.js +5 -0
  25. package/dist/bodies/moons-saturn.d.ts +1 -0
  26. package/dist/bodies/moons-saturn.js +34 -0
  27. package/dist/bodies/moons-uranus.d.ts +1 -0
  28. package/dist/bodies/moons-uranus.js +7 -0
  29. package/dist/bodies/neptune.d.ts +1 -0
  30. package/dist/bodies/neptune.js +19 -0
  31. package/dist/bodies/pluto.d.ts +1 -0
  32. package/dist/bodies/pluto.js +22 -0
  33. package/dist/bodies/saturn.d.ts +1 -0
  34. package/dist/bodies/saturn.js +14 -0
  35. package/dist/bodies/sun.d.ts +1 -0
  36. package/dist/bodies/sun.js +20 -0
  37. package/dist/bodies/uranus.d.ts +1 -0
  38. package/dist/bodies/uranus.js +15 -0
  39. package/dist/bodies/venus.d.ts +1 -0
  40. package/dist/bodies/venus.js +10 -0
  41. package/dist/data/orbits.d.ts +20 -0
  42. package/dist/data/orbits.js +27 -0
  43. package/dist/element.d.ts +88 -0
  44. package/dist/element.js +603 -0
  45. package/dist/ephemeris.d.ts +189 -0
  46. package/dist/ephemeris.js +340 -0
  47. package/dist/facts.d.ts +213 -0
  48. package/dist/facts.js +198 -0
  49. package/dist/gate.d.ts +37 -0
  50. package/dist/gate.js +54 -0
  51. package/dist/index.d.ts +5 -0
  52. package/dist/index.js +5 -0
  53. package/dist/line.d.ts +82 -0
  54. package/dist/line.js +280 -0
  55. package/dist/live.d.ts +42 -0
  56. package/dist/live.js +62 -0
  57. package/dist/pose.d.ts +62 -0
  58. package/dist/pose.js +69 -0
  59. package/dist/scene/body.d.ts +68 -0
  60. package/dist/scene/body.js +337 -0
  61. package/dist/scene/glsl.d.ts +13 -0
  62. package/dist/scene/glsl.js +186 -0
  63. package/dist/scene/look.d.ts +106 -0
  64. package/dist/scene/look.js +35 -0
  65. package/dist/scene/recipes.d.ts +49 -0
  66. package/dist/scene/recipes.js +76 -0
  67. package/dist/scene/rings.d.ts +34 -0
  68. package/dist/scene/rings.js +64 -0
  69. package/dist/scene/shared.d.ts +24 -0
  70. package/dist/scene/shared.js +55 -0
  71. package/dist/scene/store.d.ts +19 -0
  72. package/dist/scene/store.js +8 -0
  73. package/dist/textures/callisto-1024.webp +0 -0
  74. package/dist/textures/callisto-512.webp +0 -0
  75. package/dist/textures/enceladus-1024.webp +0 -0
  76. package/dist/textures/enceladus-512.webp +0 -0
  77. package/dist/textures/europa-1024.webp +0 -0
  78. package/dist/textures/europa-512.webp +0 -0
  79. package/dist/textures/ganymede-1024.webp +0 -0
  80. package/dist/textures/ganymede-512.webp +0 -0
  81. package/dist/textures/io-1024.webp +0 -0
  82. package/dist/textures/io-512.webp +0 -0
  83. package/dist/textures/jupiter-1024.webp +0 -0
  84. package/dist/textures/jupiter-512.webp +0 -0
  85. package/dist/textures/mars-1024.webp +0 -0
  86. package/dist/textures/mars-512.webp +0 -0
  87. package/dist/textures/mercury-1024.webp +0 -0
  88. package/dist/textures/mercury-512.webp +0 -0
  89. package/dist/textures/mimas-1024.webp +0 -0
  90. package/dist/textures/mimas-512.webp +0 -0
  91. package/dist/textures/moon-1024.webp +0 -0
  92. package/dist/textures/moon-512.webp +0 -0
  93. package/dist/textures/neptune-1024.webp +0 -0
  94. package/dist/textures/neptune-512.webp +0 -0
  95. package/dist/textures/pluto-1024.webp +0 -0
  96. package/dist/textures/pluto-512.webp +0 -0
  97. package/dist/textures/rhea-1024.webp +0 -0
  98. package/dist/textures/rhea-512.webp +0 -0
  99. package/dist/textures/saturn-1024.webp +0 -0
  100. package/dist/textures/saturn-512.webp +0 -0
  101. package/dist/textures/saturn-ring-2048.webp +0 -0
  102. package/dist/textures/sun-1024.webp +0 -0
  103. package/dist/textures/sun-512.webp +0 -0
  104. package/dist/textures/titan-1024.webp +0 -0
  105. package/dist/textures/titan-512.webp +0 -0
  106. package/dist/textures/uranus-1024.webp +0 -0
  107. package/dist/textures/uranus-512.webp +0 -0
  108. package/dist/textures/venus-1024.webp +0 -0
  109. package/dist/textures/venus-512.webp +0 -0
  110. package/dist/vec.d.ts +19 -0
  111. package/dist/vec.js +32 -0
  112. package/dist/version.d.ts +1 -0
  113. package/dist/version.js +1 -0
  114. package/package.json +236 -4
package/dist/line.js ADDED
@@ -0,0 +1,280 @@
1
+ // The line drawing: a body as thin outlines on a dark disc, in the same style as the family's other line drawings. One SVG string, no canvas, no DOM.
2
+ import { apparent, nearMoons, orbitPath, system } from "./ephemeris.js";
3
+ import { BODIES, ringReach } from "./facts.js";
4
+ import { facing, portrait, silhouette, surfacePoint } from "./pose.js";
5
+ import { cross, RAD, scale, unit } from "./vec.js";
6
+ const NAVY = "#0b1424";
7
+ const hex = (c) => [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16)];
8
+ /**
9
+ * Mix two `#rrggbb` colours: `t` is how much of `b` (0 to 1).
10
+ * @param a - The first colour.
11
+ * @param b - The second colour.
12
+ * @param t - How much of `b`, 0 to 1.
13
+ * @returns The mixed colour as `#rrggbb`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { mix } from "@johnmorrisdotca/wakusei-planets";
18
+ *
19
+ * console.log(mix("#000000", "#ffffff", 0.5));
20
+ * ```
21
+ */
22
+ export function mix(a, b, t) {
23
+ const x = hex(a);
24
+ const y = hex(b);
25
+ return `#${[0, 1, 2].map((i) => Math.round(x[i] + (y[i] - x[i]) * t).toString(16).padStart(2, "0")).join("")}`;
26
+ }
27
+ const n = (v) => (Math.round(v * 100) / 100).toString();
28
+ /** Points (screen units, y up) to an SVG path d-string around `frame`, optionally closed. */
29
+ const path = (frame, points, close = false) => `M${points.map((p) => `${n(frame.cx + p[0] * frame.r)} ${n(frame.cy - p[1] * frame.r)}`).join("L")}${close ? "Z" : ""}`;
30
+ /** Break a run of samples into runs where `visible` holds. */
31
+ function runs(samples, visible) {
32
+ const out = [];
33
+ let current = [];
34
+ samples.forEach((p, i) => {
35
+ if (visible(p, i))
36
+ current.push(p);
37
+ else if (current.length > 0) {
38
+ out.push(current);
39
+ current = [];
40
+ }
41
+ });
42
+ if (current.length > 0)
43
+ out.push(current);
44
+ return out;
45
+ }
46
+ /** A small circle of angular radii (rx east-west, ry north-south, degrees) about a point of the body, as body-frame directions. */
47
+ function around(lon, lat, rx, ry, steps = 28) {
48
+ const out = [];
49
+ for (let k = 0; k < steps; k += 1) {
50
+ const t = (k / steps) * 2 * Math.PI;
51
+ const dlat = ry * Math.sin(t);
52
+ const dlon = rx * Math.cos(t) / Math.max(0.15, Math.cos((lat + dlat) * RAD));
53
+ out.push({ lon: lon + dlon, lat: Math.max(-89.9, Math.min(89.9, lat + dlat)) });
54
+ }
55
+ return out;
56
+ }
57
+ function featurePaths(facts, pose, frame, stroke, hair) {
58
+ const f = facts.flattening;
59
+ const visible = (lon, lat) => facing(pose, lon, lat, f) > 0.02;
60
+ const out = [];
61
+ const draw = (pts, close, fill) => {
62
+ const parts = runs(pts.map((p) => surfacePoint(pose, p.lon, p.lat, f)), (_, i) => visible(pts[i].lon, pts[i].lat));
63
+ for (const part of parts) {
64
+ const whole = close && part.length === pts.length;
65
+ if (part.length < 2)
66
+ continue;
67
+ out.push(`<path d="${path(frame, part, whole || (fill && part.length > 3))}" fill="${fill ? stroke : "none"}" fill-opacity="${fill ? 0.16 : 0}" stroke="${stroke}" stroke-width="${n(hair)}" stroke-opacity="${fill ? 0.55 : 0.9}" stroke-linejoin="round" stroke-linecap="round"/>`);
68
+ }
69
+ };
70
+ const apply = (feature) => {
71
+ if (feature.kind === "circle")
72
+ draw(around(feature.lon, feature.lat, feature.radius, feature.radius), true, feature.fill === true);
73
+ else if (feature.kind === "oval")
74
+ draw(around(feature.lon, feature.lat, feature.rx, feature.ry), true, feature.fill === true);
75
+ else if (feature.kind === "band") {
76
+ const pts = [];
77
+ for (let lon = 0; lon <= 360; lon += 4)
78
+ pts.push({ lon, lat: feature.lat });
79
+ draw(pts, false, false);
80
+ }
81
+ else {
82
+ const pts = [];
83
+ for (let k = 0; k < feature.points.length - (feature.close === true ? 0 : 1); k += 1) {
84
+ const a = feature.points[k];
85
+ const b = feature.points[(k + 1) % feature.points.length];
86
+ for (let s = 0; s < 6; s += 1)
87
+ pts.push({ lon: a[0] + ((b[0] - a[0]) * s) / 6, lat: a[1] + ((b[1] - a[1]) * s) / 6 });
88
+ }
89
+ const last = feature.points[feature.close === true ? 0 : feature.points.length - 1];
90
+ pts.push({ lon: last[0], lat: last[1] });
91
+ draw(pts, feature.close === true, false);
92
+ }
93
+ };
94
+ for (const feature of facts.features ?? [])
95
+ apply(feature);
96
+ return out.join("");
97
+ }
98
+ /** The rings of a planet as ellipse strokes, split into the part behind the planet and the part in front of it. */
99
+ function ringPaths(facts, pose, frame, stroke, hair, scaleBy) {
100
+ const back = [];
101
+ const front = [];
102
+ if (facts.rings === undefined)
103
+ return { back: "", front: "" };
104
+ const e1 = pose.meridian;
105
+ const e2 = cross(pose.pole, pose.meridian);
106
+ const ring = (r, theta) => [r * (Math.cos(theta) * e1[0] + Math.sin(theta) * e2[0]), r * (Math.cos(theta) * e1[1] + Math.sin(theta) * e2[1]), r * (Math.cos(theta) * e1[2] + Math.sin(theta) * e2[2])];
107
+ const edge = (km, opacity, width) => {
108
+ const r = (km / facts.radiusKm) * scaleBy;
109
+ const pts = [];
110
+ for (let k = 0; k <= 120; k += 1)
111
+ pts.push(ring(r, (k / 120) * 2 * Math.PI));
112
+ for (const part of runs(pts, (p) => p[2] < 0))
113
+ back.push(`<path d="${path(frame, part)}" fill="none" stroke="${stroke}" stroke-opacity="${n(opacity)}" stroke-width="${n(width)}"/>`);
114
+ for (const part of runs(pts, (p) => p[2] >= 0))
115
+ front.push(`<path d="${path(frame, part)}" fill="none" stroke="${stroke}" stroke-opacity="${n(opacity)}" stroke-width="${n(width)}"/>`);
116
+ };
117
+ for (const band of facts.rings) {
118
+ const wide = (band.to - band.from) / facts.radiusKm > 0.02;
119
+ if (wide) {
120
+ edge(band.from, 0.35 + band.alpha * 0.4, hair);
121
+ edge(band.to, 0.35 + band.alpha * 0.4, hair);
122
+ }
123
+ else {
124
+ edge((band.from + band.to) / 2, 0.25 + band.alpha * 0.5, hair);
125
+ }
126
+ }
127
+ return { back: back.join(""), front: front.join("") };
128
+ }
129
+ /** The dark side of the disc as a filled shape, from the Sun's direction. Empty for a full disc. */
130
+ function nightPath(pose, frame) {
131
+ const L = pose.light;
132
+ const xy = Math.hypot(L[0], L[1]);
133
+ if (xy < 1e-6 && L[2] > 0)
134
+ return "";
135
+ const c = Math.atan2(-L[1], -L[0]);
136
+ const u = [Math.cos(c - Math.PI / 2), Math.sin(c - Math.PI / 2), 0];
137
+ let v = cross(unit(L), u);
138
+ v = unit(v);
139
+ if (v[2] < 0)
140
+ v = scale(v, -1);
141
+ const pts = [];
142
+ for (let k = 0; k <= 48; k += 1) {
143
+ const t = (k / 48) * Math.PI;
144
+ pts.push([u[0] * Math.cos(t) + v[0] * Math.sin(t), u[1] * Math.cos(t) + v[1] * Math.sin(t), 0]);
145
+ }
146
+ for (let k = 1; k <= 48; k += 1) {
147
+ const a = c + Math.PI / 2 - (k / 48) * Math.PI;
148
+ pts.push([Math.cos(a), Math.sin(a), 0]);
149
+ }
150
+ return path(frame, pts, true);
151
+ }
152
+ function rays(frame, stroke, hair) {
153
+ const out = [];
154
+ for (let k = 0; k < 32; k += 1) {
155
+ const a = (k / 32) * 2 * Math.PI;
156
+ const long = k % 2 === 0;
157
+ const r0 = 1.07;
158
+ const r1 = long ? 1.24 : 1.15;
159
+ out.push(`M${n(frame.cx + Math.cos(a) * r0 * frame.r)} ${n(frame.cy + Math.sin(a) * r0 * frame.r)}L${n(frame.cx + Math.cos(a) * r1 * frame.r)} ${n(frame.cy + Math.sin(a) * r1 * frame.r)}`);
160
+ }
161
+ return `<path d="${out.join("")}" fill="none" stroke="${stroke}" stroke-width="${n(hair * 1.2)}" stroke-linecap="round" stroke-opacity="0.8"/>`;
162
+ }
163
+ const describe = (facts, lang, tail) => (lang === "ja" ? `${facts.name.ja}の線画${tail}` : `Line drawing of ${facts.name.en}${tail}`);
164
+ const orbitMemo = new Map();
165
+ /** A moon's orbit path as it is in the hour of `time`, kept so a moving picture does not work it out again. */
166
+ function cachedOrbit(id, time) {
167
+ const key = `${id}:${Math.floor(time / 3_600_000)}`;
168
+ let path = orbitMemo.get(key);
169
+ if (path === undefined) {
170
+ if (orbitMemo.size > 64)
171
+ orbitMemo.clear();
172
+ path = orbitPath(id, time, 96);
173
+ orbitMemo.set(key, path);
174
+ }
175
+ return path;
176
+ }
177
+ function bodyGroup(id, pose, frame, o, hair, ringScale) {
178
+ const facts = BODIES[id];
179
+ const sil = silhouette(pose, facts.flattening);
180
+ const disc = `<ellipse cx="${n(frame.cx)}" cy="${n(frame.cy)}" rx="${n(frame.r * sil.a)}" ry="${n(frame.r * sil.b)}" transform="rotate(${n(sil.angle)} ${n(frame.cx)} ${n(frame.cy)})"`;
181
+ const rings = ringPaths(facts, pose, frame, o.stroke, hair, ringScale);
182
+ const dark = id === "sun" || facts.kind === "star" ? "" : nightPath(pose, frame);
183
+ const clipId = `wk-${id}-clip`;
184
+ return [
185
+ rings.back,
186
+ `${disc} fill="${o.paper}" stroke="${o.stroke}" stroke-width="${n(hair * 1.6)}"/>`,
187
+ `<clipPath id="${clipId}">${disc}/></clipPath>`,
188
+ `<g clip-path="url(#${clipId})">`,
189
+ featurePaths(facts, pose, frame, o.stroke, hair),
190
+ dark === "" ? "" : `<path d="${dark}" fill="${NAVY}" fill-opacity="0.62" stroke="${o.stroke}" stroke-opacity="0.35" stroke-width="${n(hair * 0.8)}" stroke-dasharray="${n(hair * 3)} ${n(hair * 3)}"/>`,
191
+ `</g>`,
192
+ id === "sun" ? rays(frame, o.stroke, hair) : "",
193
+ rings.front,
194
+ ].join("");
195
+ }
196
+ function poseFor(id, options) {
197
+ if (options.pose !== undefined)
198
+ return options.pose;
199
+ return options.time === undefined ? portrait(id, options) : apparent(id, options.time);
200
+ }
201
+ /**
202
+ * A body as a line drawing, as an SVG string: the outline (squeezed by its flattening and leaning at its axial tilt), bands and features, rings with their gaps, and the dark side
203
+ * dashed off. Give it a `time` to draw the body as the Earth sees it then; leave it out for a portrait. `moons: true` draws a planet with its moons on their orbits.
204
+ *
205
+ * @param id - The body's name: `sun`, a planet or a moon.
206
+ * @param options - The size, the pose (`tilt`, `pitch`, `phase`, `spin` or a `time`), `moons`, `fill`, the language and the colours.
207
+ * @returns The SVG markup, `<svg ...>` to `</svg>`.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * import { lineSvg } from "@johnmorrisdotca/wakusei-planets";
212
+ *
213
+ * const svg = lineSvg("saturn", { size: 320 });
214
+ * console.log(svg.startsWith("<svg"), svg.includes("Saturn"));
215
+ * ```
216
+ */
217
+ export function lineSvg(id, options = {}) {
218
+ const facts = BODIES[id];
219
+ if (facts === undefined)
220
+ throw new Error(`wakusei-planets: no body called "${String(id)}"`);
221
+ const size = options.size ?? 320;
222
+ const lang = options.lang ?? "en";
223
+ const stroke = options.stroke ?? mix(facts.colour, "#ffffff", 0.42);
224
+ const paper = options.paper ?? mix(facts.colour, NAVY, id === "sun" ? 0.62 : 0.84);
225
+ const hair = Math.max(0.6, size / 320);
226
+ const head = (label) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(size)} ${n(size)}" width="${n(size)}" height="${n(size)}" role="img" aria-label="${label}"><title>${label}</title>`;
227
+ if (options.moons === true && facts.parent === undefined && id !== "sun")
228
+ return systemSvg(id, options, size, stroke, paper, hair, head);
229
+ const pose = poseFor(id, options);
230
+ const reach = Math.max(ringReach(facts), id === "sun" ? 1.26 : 1);
231
+ const fit = options.fill === undefined ? 0.94 / reach : options.fill;
232
+ const zoom = options.zoom ?? 1;
233
+ const frame = { cx: (size / 2) * (1 - (options.panX ?? 0)), cy: (size / 2) * (1 + (options.panY ?? 0)), r: (size / 2) * fit * zoom };
234
+ const label = describe(facts, lang, options.time === undefined ? "" : `, ${new Date(options.time).toISOString().slice(0, 16).replace("T", " ")} UTC`);
235
+ return `${head(label)}${bodyGroup(id, pose, frame, { stroke, paper }, hair, 1)}</svg>`;
236
+ }
237
+ /** A planet with its moons on their orbits, as the Earth sees it at a time. */
238
+ function systemSvg(planet, options, size, stroke, paper, hair, head) {
239
+ const facts = BODIES[planet];
240
+ const time = options.time ?? Date.now();
241
+ const sys = options.moonsAt === undefined || options.pose === undefined ? system(planet, time) : undefined;
242
+ const pose = options.pose ?? sys.planet;
243
+ const lang = options.lang ?? "en";
244
+ const near = nearMoons(planet);
245
+ const reach = options.range ?? Math.max(ringReach(facts) * 1.1, ...near.map((m) => m.reach * 1.1), 1.8);
246
+ const all = options.moonsAt ?? sys.moons;
247
+ const moons = all.filter((m) => m.distance <= reach * 1.05);
248
+ const half = size / 2;
249
+ const unitPx = (half * 0.94) / reach;
250
+ const frame = { cx: half * (1 - (options.panX ?? 0)), cy: half * (1 + (options.panY ?? 0)), r: unitPx * (options.zoom ?? 1) };
251
+ const parts = [];
252
+ const names = [];
253
+ const moonPx = (id) => Math.max((BODIES[id].radiusKm / facts.radiusKm) * unitPx, size / 110);
254
+ // the orbits: the path each moon follows over one trip, as it is at that time
255
+ for (const m of moons) {
256
+ const flat = options.orbits?.get(m.id) ?? cachedOrbit(m.id, time);
257
+ const orbitPts = [];
258
+ for (let k = 0; k <= flat.length / 3; k += 1) {
259
+ const i = (k % (flat.length / 3)) * 3;
260
+ orbitPts.push([flat[i], flat[i + 1], flat[i + 2]]);
261
+ }
262
+ if (orbitPts.length > 2)
263
+ parts.push(`<path d="${path(frame, orbitPts)}" fill="none" stroke="${stroke}" stroke-opacity="0.28" stroke-width="${n(hair * 0.7)}"/>`);
264
+ }
265
+ const placed = moons.map((m) => ({ m, px: frame.cx + m.x * unitPx, py: frame.cy - m.y * unitPx }));
266
+ const draw = (items) => items
267
+ .map(({ m, px, py }) => {
268
+ const mf = BODIES[m.id];
269
+ const r = moonPx(m.id);
270
+ const label = options.labels === false ? "" : `<text x="${n(px + r + hair * 3)}" y="${n(py + size / 90)}" font-size="${n(size / 34)}" fill="${stroke}" fill-opacity="0.85" font-family="system-ui,sans-serif">${lang === "ja" ? mf.name.ja : mf.name.en}</text>`;
271
+ names.push(mf.name[lang]);
272
+ return `<circle cx="${n(px)}" cy="${n(py)}" r="${n(r)}" fill="${mix(mf.colour, NAVY, 0.25)}" stroke="${stroke}" stroke-width="${n(hair)}"/>${label}`;
273
+ })
274
+ .join("");
275
+ const behind = placed.filter(({ m }) => m.z < 0);
276
+ const inFront = placed.filter(({ m }) => m.z >= 0);
277
+ parts.push(draw(behind), bodyGroup(planet, pose, frame, { stroke, paper }, hair, 1), draw(inFront));
278
+ const label = lang === "ja" ? `${facts.name.ja}と衛星の線画 (${names.length})` : `Line drawing of ${facts.name.en} and its moons`;
279
+ return `${head(label)}${parts.join("")}</svg>`;
280
+ }
package/dist/live.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { type Pose } from "./ephemeris.ts";
2
+ import { type BodyId } from "./facts.ts";
3
+ import { type PortraitOptions } from "./pose.ts";
4
+ /** What a live picture is asked for. */
5
+ export interface LiveRequest {
6
+ id: BodyId;
7
+ /** The moment at the start of the clock, in milliseconds since 1970; `undefined` for a portrait with no date. */
8
+ start: number | undefined;
9
+ /** When the clock started, in the real milliseconds of `at()`'s `now`. */
10
+ startedAt: number;
11
+ /** Simulated seconds for each real second. */
12
+ rate: number;
13
+ /** Draw the planet's moons. Needs a date; the start is taken as the real time when none is given. */
14
+ moons: boolean;
15
+ portrait: PortraitOptions;
16
+ /** A portrait turns eastward by this many degrees a second. */
17
+ turn: number;
18
+ }
19
+ /** A moon in the system, carried forward. */
20
+ export interface LiveMoon {
21
+ id: BodyId;
22
+ x: number;
23
+ y: number;
24
+ z: number;
25
+ distance: number;
26
+ }
27
+ /** The picture at one moment. */
28
+ export interface LiveFrame {
29
+ pose: Pose;
30
+ moons: readonly LiveMoon[];
31
+ /** The simulated moment in milliseconds since 1970, or `undefined` in a portrait. */
32
+ time: number | undefined;
33
+ /** How many full workings-out have been made so far. */
34
+ anchors: number;
35
+ }
36
+ /** The degrees a second a body's own turn advances its prime meridian (a moon that keeps one face to its planet turns once a trip). */
37
+ export declare function turnRate(id: BodyId): number;
38
+ /** A live picture's maths: ask `at(request, now)` as often as you like; it works out the sky about once a second. */
39
+ export declare function createLive(): {
40
+ at(request: LiveRequest, now: number): LiveFrame;
41
+ readonly anchors: number;
42
+ };
package/dist/live.js ADDED
@@ -0,0 +1,62 @@
1
+ // The moving picture's maths, at a low rate: the pose and the moons are worked out in full about once a second, and in between the body's turn and each moon's walk round its
2
+ // planet are carried forward from there. A frame costs a few turns of vectors, never an ephemeris. Pure, no DOM.
3
+ import { ORBITS } from "./data/orbits.js";
4
+ import { apparent, system } from "./ephemeris.js";
5
+ import { BODIES, MOONS_OF } from "./facts.js";
6
+ import { portrait } from "./pose.js";
7
+ import { turn, unit, cross } from "./vec.js";
8
+ const ANCHOR_MS = 1000;
9
+ /** The degrees a second a body's own turn advances its prime meridian (a moon that keeps one face to its planet turns once a trip). */
10
+ export function turnRate(id) {
11
+ const f = BODIES[id];
12
+ return (f.synchronous === true && ORBITS[id] !== undefined ? ORBITS[id].sid : f.prime.rate) / 86400;
13
+ }
14
+ /** The pictured moon-planet's moons, or none. */
15
+ const moonsOfPlanet = (id) => id in MOONS_OF;
16
+ /** A live picture's maths: ask `at(request, now)` as often as you like; it works out the sky about once a second. */
17
+ export function createLive() {
18
+ let anchor;
19
+ let count = 0;
20
+ return {
21
+ get anchors() {
22
+ return count;
23
+ },
24
+ at(r, now) {
25
+ const wantMoons = r.moons && moonsOfPlanet(r.id);
26
+ const timed = r.start !== undefined || wantMoons;
27
+ const sim = timed ? (r.start ?? r.startedAt) + (now - r.startedAt) * r.rate : undefined;
28
+ const signature = `${r.id}|${r.start}|${r.startedAt}|${r.rate}|${wantMoons}|${JSON.stringify(r.portrait)}`;
29
+ if (anchor === undefined || anchor.signature !== signature || now - anchor.real >= ANCHOR_MS || now < anchor.real) {
30
+ count += 1;
31
+ const pose = timed ? (wantMoons ? system(r.id, sim).planet : apparent(r.id, sim)) : portrait(r.id, r.portrait);
32
+ let place = [];
33
+ let axes = [];
34
+ if (wantMoons) {
35
+ place = system(r.id, sim).moons;
36
+ const later = system(r.id, sim + 3_600_000).moons;
37
+ axes = place.map((m) => {
38
+ const n = later.find((l) => l.id === m.id);
39
+ return unit(cross([m.x, m.y, m.z], [n.x, n.y, n.z]));
40
+ });
41
+ }
42
+ anchor = { real: now, sim, pose, place, axes, signature };
43
+ }
44
+ const a = anchor;
45
+ const seconds = a.sim === undefined || sim === undefined ? (now - r.startedAt) / 1000 : (sim - a.sim) / 1000;
46
+ let pose = a.pose;
47
+ if (a.sim === undefined) {
48
+ if (r.turn !== 0)
49
+ pose = { ...pose, meridian: turn(pose.meridian, pose.pole, r.turn * seconds) };
50
+ }
51
+ else if (seconds !== 0) {
52
+ pose = { ...pose, meridian: turn(pose.meridian, pose.pole, turnRate(r.id) * seconds) };
53
+ }
54
+ const moons = a.place.map((m, i) => {
55
+ const rate = Math.abs(ORBITS[m.id]?.sid ?? 0) / 86400;
56
+ const p = turn([m.x, m.y, m.z], a.axes[i], rate * seconds);
57
+ return { id: m.id, x: p[0], y: p[1], z: p[2], distance: m.distance };
58
+ });
59
+ return { pose, moons, time: sim, anchors: count };
60
+ },
61
+ };
62
+ }
package/dist/pose.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { type BodyFacts, type BodyId } from "./facts.ts";
2
+ import type { Pose } from "./ephemeris.ts";
3
+ import { type Vec3 } from "./vec.ts";
4
+ /**
5
+ * Options for a pose when no date is given. Angles are in degrees: `tilt` leans the pole on the page, `pitch` is how far the viewer looks over the pole, `phase` is the Sun–body–viewer angle and `spin` the longitude at the middle of the disc.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { portrait, type PortraitOptions } from "@johnmorrisdotca/wakusei-planets";
10
+ *
11
+ * const options: PortraitOptions = { tilt: 20, pitch: 15, phase: 60 };
12
+ * console.log(portrait("jupiter", options).light.length);
13
+ * ```
14
+ */
15
+ export interface PortraitOptions {
16
+ /** The axial tilt on the page: how far the north pole leans to the right of straight up. Default: the body's own tilt (turned to 120 or less). */
17
+ tilt?: number;
18
+ /** How far the viewer looks over the north pole: 0 sees the equator edge-on, 90 looks straight down at the pole. Default 12, or 22 for a body with rings. */
19
+ pitch?: number;
20
+ /** The Sun–body–viewer angle: 0 is full, 90 half, 180 new. Default 38. */
21
+ phase?: number;
22
+ /** The longitude at the middle of the disc, degrees east. Default: the longitude of the body's best-known side. */
23
+ spin?: number;
24
+ }
25
+ /**
26
+ * The tilt a drawing shows for a body: its obliquity, turned past 120 degrees to the other side (Venus is nearly upside down, so it is drawn nearly upright).
27
+ * @param facts - A body's facts.
28
+ * @returns The tilt in degrees to lean the drawing by.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { BODIES, drawnTilt } from "@johnmorrisdotca/wakusei-planets";
33
+ *
34
+ * console.log(drawnTilt(BODIES.uranus), drawnTilt(BODIES.venus).toFixed(2));
35
+ * ```
36
+ */
37
+ export declare const drawnTilt: (facts: BodyFacts) => number;
38
+ /**
39
+ * A pose from a tilt, a pitch, a phase and a spin: the body as a portrait, with the Sun a little above the left. The same arguments always give the same pose.
40
+ * @param id - The body's name.
41
+ * @param options - The pose's tilt, pitch, phase and spin.
42
+ * @returns The pose, as unit vectors in the viewer's frame.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { portrait } from "@johnmorrisdotca/wakusei-planets";
47
+ *
48
+ * const pose = portrait("mars", { phase: 90 });
49
+ * console.log(pose.light[0] < -0.9);
50
+ * ```
51
+ */
52
+ export declare function portrait(id: BodyId, options?: PortraitOptions): Pose;
53
+ /** A longitude and latitude (degrees) as a point on the body, in the viewer's frame (unit radius): x right, y up, z toward the viewer. The flattening squeezes it along the pole. */
54
+ export declare function surfacePoint(pose: Pose, lon: number, lat: number, flattening?: number): Vec3;
55
+ /** The outward normal's z at a longitude and latitude: positive when that point faces the viewer. */
56
+ export declare function facing(pose: Pose, lon: number, lat: number, flattening?: number): number;
57
+ /** The silhouette of a body: its half-width, half-height along the pole's direction and the angle of the pole from straight up, clockwise, in degrees. */
58
+ export declare function silhouette(pose: Pose, flattening: number): {
59
+ a: number;
60
+ b: number;
61
+ angle: number;
62
+ };
package/dist/pose.js ADDED
@@ -0,0 +1,69 @@
1
+ // A body's pose when no date is given: its axial tilt on the page, how far the viewer looks over its pole, and where the Sun is. Plus the turns that put a longitude
2
+ // and latitude on the picture. Pure.
3
+ import { BODIES } from "./facts.js";
4
+ import { cross, dot, DEG, RAD, turn, unit } from "./vec.js";
5
+ /**
6
+ * The tilt a drawing shows for a body: its obliquity, turned past 120 degrees to the other side (Venus is nearly upside down, so it is drawn nearly upright).
7
+ * @param facts - A body's facts.
8
+ * @returns The tilt in degrees to lean the drawing by.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { BODIES, drawnTilt } from "@johnmorrisdotca/wakusei-planets";
13
+ *
14
+ * console.log(drawnTilt(BODIES.uranus), drawnTilt(BODIES.venus).toFixed(2));
15
+ * ```
16
+ */
17
+ export const drawnTilt = (facts) => (facts.tiltDeg > 120 ? facts.tiltDeg - 180 : facts.tiltDeg);
18
+ /**
19
+ * A pose from a tilt, a pitch, a phase and a spin: the body as a portrait, with the Sun a little above the left. The same arguments always give the same pose.
20
+ * @param id - The body's name.
21
+ * @param options - The pose's tilt, pitch, phase and spin.
22
+ * @returns The pose, as unit vectors in the viewer's frame.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { portrait } from "@johnmorrisdotca/wakusei-planets";
27
+ *
28
+ * const pose = portrait("mars", { phase: 90 });
29
+ * console.log(pose.light[0] < -0.9);
30
+ * ```
31
+ */
32
+ export function portrait(id, options = {}) {
33
+ const facts = BODIES[id];
34
+ const roll = (options.tilt ?? drawnTilt(facts)) * RAD;
35
+ const pitchDeg = options.pitch ?? (facts.rings !== undefined ? 22 : 12);
36
+ const B = pitchDeg * RAD;
37
+ const pole = [Math.sin(roll) * Math.cos(B), Math.cos(roll) * Math.cos(B), Math.sin(B)];
38
+ const viewer = [0, 0, 1];
39
+ const eq = unit([viewer[0] - dot(viewer, pole) * pole[0], viewer[1] - dot(viewer, pole) * pole[1], viewer[2] - dot(viewer, pole) * pole[2]]);
40
+ const meridian = turn(eq, pole, -(options.spin ?? facts.showLon ?? 0));
41
+ const phase = (options.phase ?? 38) * RAD;
42
+ const lift = 14 * RAD;
43
+ const light = unit([-Math.sin(phase) * Math.cos(lift), Math.sin(lift), Math.cos(phase) * Math.cos(lift)]);
44
+ return { pole, meridian, light };
45
+ }
46
+ /** A longitude and latitude (degrees) as a point on the body, in the viewer's frame (unit radius): x right, y up, z toward the viewer. The flattening squeezes it along the pole. */
47
+ export function surfacePoint(pose, lon, lat, flattening = 0) {
48
+ const cl = Math.cos(lat * RAD);
49
+ const east = cross(pose.pole, pose.meridian);
50
+ const x = cl * Math.cos(lon * RAD);
51
+ const y = cl * Math.sin(lon * RAD);
52
+ const z = Math.sin(lat * RAD) * (1 - flattening);
53
+ return [pose.meridian[0] * x + east[0] * y + pose.pole[0] * z, pose.meridian[1] * x + east[1] * y + pose.pole[1] * z, pose.meridian[2] * x + east[2] * y + pose.pole[2] * z];
54
+ }
55
+ /** The outward normal's z at a longitude and latitude: positive when that point faces the viewer. */
56
+ export function facing(pose, lon, lat, flattening = 0) {
57
+ const cl = Math.cos(lat * RAD);
58
+ const east = cross(pose.pole, pose.meridian);
59
+ const x = cl * Math.cos(lon * RAD);
60
+ const y = cl * Math.sin(lon * RAD);
61
+ const z = Math.sin(lat * RAD) / (1 - flattening);
62
+ return pose.meridian[2] * x + east[2] * y + pose.pole[2] * z;
63
+ }
64
+ /** The silhouette of a body: its half-width, half-height along the pole's direction and the angle of the pole from straight up, clockwise, in degrees. */
65
+ export function silhouette(pose, flattening) {
66
+ const c = 1 - flattening;
67
+ const s = pose.pole[2];
68
+ return { a: 1, b: Math.sqrt(c * c * (1 - s * s) + s * s), angle: Math.atan2(pose.pole[0], pose.pole[1]) * DEG };
69
+ }
@@ -0,0 +1,68 @@
1
+ import type { Item3D, Scene3D } from "@johnmorrisdotca/kazoku-ui/renderer";
2
+ import type * as THREE from "three";
3
+ import type { Pose } from "../ephemeris.ts";
4
+ import { type BodyFacts, type BodyId } from "../facts.ts";
5
+ import { type BodyLook } from "./look.ts";
6
+ /** The parts of three.js the bodies use: a page may hand over a module with only these (see `setThreeLoader`). */
7
+ export type ThreeParts = Pick<typeof THREE, "AdditiveBlending" | "BufferAttribute" | "BufferGeometry" | "ClampToEdgeWrapping" | "DataTexture" | "DoubleSide" | "Line" | "LineBasicMaterial" | "LinearFilter" | "Mesh" | "NoColorSpace" | "OrthographicCamera" | "PlaneGeometry" | "RepeatWrapping" | "RGBAFormat" | "RingGeometry" | "Scene" | "ShaderMaterial" | "SphereGeometry" | "TextureLoader" | "Vector2" | "Vector3" | "WebGLRenderer">;
8
+ /** One moon in a system view: its place in planet radii (viewer's frame) and the points of its orbit, if they changed. */
9
+ export interface MoonDraw {
10
+ id: BodyId;
11
+ x: number;
12
+ y: number;
13
+ z: number;
14
+ /** The orbit as x, y, z triples in planet radii. A new array replaces the old line. */
15
+ orbit?: Float32Array;
16
+ }
17
+ /** Everything that changes how the scene looks. The element fills one in and hands it over; the scene holds no clock of its own. */
18
+ export interface SceneState {
19
+ pose: Pose;
20
+ /** Body radii from the middle to the edge of the picture, at zoom 1. */
21
+ extent: number;
22
+ zoom: number;
23
+ panX: number;
24
+ panY: number;
25
+ moons?: readonly MoonDraw[];
26
+ /** The smallest a moon is drawn, in CSS pixels across its radius. Default 2.5. */
27
+ minMoonPx?: number;
28
+ /** The colour of the orbit lines, `#rrggbb`. */
29
+ orbitColour?: string;
30
+ }
31
+ /** The scene the shared renderer draws, with the one call that moves it. */
32
+ export interface BodyScene extends Scene3D {
33
+ apply(state: SceneState): void;
34
+ /** What the scene is drawing now: the texture width in use, or 0 for the shader. */
35
+ readonly textureWidth: number;
36
+ readonly painted: boolean;
37
+ }
38
+ export interface BodyItemOptions {
39
+ id: BodyId;
40
+ look: "realistic" | "painted";
41
+ /** A key that changes whenever the picture would: the renderer keeps one still for each key. */
42
+ key: string;
43
+ state(): SceneState;
44
+ /** Where the pictures are: a folder address that ends in a slash. */
45
+ textureBase(): string;
46
+ /** Where the widths from `painted.far` up are: a folder address that ends in a slash. */
47
+ farBase(): string;
48
+ /** The widest texture worth fetching for a disc this many CSS pixels across (the element's call). */
49
+ wantedWidth(discPx: number): number;
50
+ /** The scene asks for a redraw when a sharper picture arrives. */
51
+ redraw(): void;
52
+ /** The scene tells the element which way it drew (the look can fall back to the shader). */
53
+ report?(note: {
54
+ painted: boolean;
55
+ width: number;
56
+ failed?: boolean;
57
+ }): void;
58
+ }
59
+ /** A body that has no registered look is still drawn: a lit ball in its mean colour. */
60
+ export declare function fallbackLook(facts: BodyFacts): BodyLook;
61
+ /**
62
+ * The shared-renderer item for a body: what `renderer.view(host, item)` shows. `state()` is read when the scene is built and `apply` moves it afterwards.
63
+ *
64
+ * @param T - The three.js module (the element passes the page's own).
65
+ * @param options - The body, the look, the key and the callbacks.
66
+ * @returns The item for kazoku-ui's shared renderer.
67
+ */
68
+ export declare function createBodyItem(options: BodyItemOptions): Item3D<ThreeParts>;